Release 0.3.0: movements — регистратор ↔ регистры движений
Таблица register_records, мягкая миграция схемы 2→3 и команда query_1c.py movements в обе стороны. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,21 @@
|
|||||||
Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
Версионирование: [Semantic Versioning](https://semver.org/lang/ru/).
|
Версионирование: [Semantic Versioning](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [0.3.0] - 2026-07-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Таблица **`register_records`** (документ → регистры движений из `RegisterRecords`).
|
||||||
|
- Команда **`query_1c.py movements`**:
|
||||||
|
- документ → список регистров движений;
|
||||||
|
- регистр → список документов-регистраторов;
|
||||||
|
- `--type`, `--limit`, `-b` как у остальных команд.
|
||||||
|
- Мягкая миграция схемы **2→3** при открытии индекса (без `reindex --full`): backfill из JSON объектов.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `SCHEMA_VERSION` = 3; инкрементальный `reindex` пишет связи в `register_records`.
|
||||||
|
|
||||||
## [0.2.0] - 2026-07-16
|
## [0.2.0] - 2026-07-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -40,5 +55,6 @@
|
|||||||
- `query_1c.py`: `search`, `show`, `refs`, `modules`, `module`.
|
- `query_1c.py`: `search`, `show`, `refs`, `modules`, `module`.
|
||||||
- Инкремент по `mtime`+`size` файлов; индекс модулей `**/*.bsl`.
|
- Инкремент по `mtime`+`size` файлов; индекс модулей `**/*.bsl`.
|
||||||
|
|
||||||
|
[0.3.0]: https://git.p7net.ru/1c/index/-/tags/v0.3.0
|
||||||
[0.2.0]: https://git.p7net.ru/1c/index/-/tags/v0.2.0
|
[0.2.0]: https://git.p7net.ru/1c/index/-/tags/v0.2.0
|
||||||
[0.1.0]: https://git.p7net.ru/1c/index/-/tags/v0.1.0
|
[0.1.0]: https://git.p7net.ru/1c/index/-/tags/v0.1.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Индекс метаданных и модулей конфигураций 1С
|
# Индекс метаданных и модулей конфигураций 1С
|
||||||
|
|
||||||
**Версия:** см. [`VERSION`](VERSION) (текущая: **0.2.0**) · [CHANGELOG](CHANGELOG.md)
|
**Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.0**) · [CHANGELOG](CHANGELOG.md)
|
||||||
|
|
||||||
Единый кэш проекта: **`cache/1c_meta/index.sqlite`** — все выгрузки 1С в одной БД (SQLite FTS5).
|
Единый кэш проекта: **`cache/1c_meta/index.sqlite`** — все выгрузки 1С в одной БД (SQLite FTS5).
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ python tools/index/query_1c.py --version
|
|||||||
| Найти объект по имени/синониму | `query_1c.py search` / `object` |
|
| Найти объект по имени/синониму | `query_1c.py search` / `object` |
|
||||||
| Структура реквизитов, обязательность, типы | `query_1c.py object` |
|
| Структура реквизитов, обязательность, типы | `query_1c.py object` |
|
||||||
| Кто ссылается на справочник/документ | `query_1c.py refs` |
|
| Кто ссылается на справочник/документ | `query_1c.py refs` |
|
||||||
|
| Документ ↔ регистры движений | `query_1c.py movements` |
|
||||||
| Найти процедуру/фрагмент в BSL | `query_1c.py modules` |
|
| Найти процедуру/фрагмент в BSL | `query_1c.py modules` |
|
||||||
| Сравнить целевую и исходную конфигурацию | `-b target` / `-b source` |
|
| Сравнить целевую и исходную конфигурацию | `-b target` / `-b source` |
|
||||||
|
|
||||||
@@ -84,8 +85,9 @@ python tools/index/query_1c.py --version
|
|||||||
|
|
||||||
### Что индексируется
|
### Что индексируется
|
||||||
|
|
||||||
- **Метаданные** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`.
|
- **Метаданные** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`, `RegisterRecords` (документ → регистры движений).
|
||||||
- **Модули** `**/*.bsl`: путь, owner (`Document.X`), вид модуля, имена процедур/функций, тело (до 200 КБ в FTS).
|
- **Модули** `**/*.bsl`: путь, owner (`Document.X`), вид модуля, имена процедур/функций, тело (до 200 КБ в FTS).
|
||||||
|
- **Связи движений** — таблица `register_records` (обратный индекс регистраторов).
|
||||||
|
|
||||||
По умолчанию пропускаются: `CommonPicture`, `StyleItem`, `XDTOPackage`, `CommonTemplate`, `Language`, `Bot`.
|
По умолчанию пропускаются: `CommonPicture`, `StyleItem`, `XDTOPackage`, `CommonTemplate`, `Language`, `Bot`.
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ python tools/index/index_1c.py list
|
|||||||
| `object` | имя/синоним объекта **или** реквизита | дерево структуры | `-b`, `--type`, `--clear`, `--limit` |
|
| `object` | имя/синоним объекта **или** реквизита | дерево структуры | `-b`, `--type`, `--clear`, `--limit` |
|
||||||
| `show` | имя объекта | полная карточка JSON | `-b` |
|
| `show` | имя объекта | полная карточка JSON | `-b` |
|
||||||
| `refs` | `Catalog.Партнеры` / `ЗаказКлиента` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
|
| `refs` | `Catalog.Партнеры` / `ЗаказКлиента` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
|
||||||
|
| `movements` | документ или регистр | регистратор ↔ регистры движений | `-b`, `--type`, `--limit` |
|
||||||
| `modules` | строка | поиск по BSL | `-b`, `-n` / `--names-only`, `--limit` |
|
| `modules` | строка | поиск по BSL | `-b`, `-n` / `--names-only`, `--limit` |
|
||||||
| `module` | путь или owner | символы модуля, строки | `-b` |
|
| `module` | путь или owner | символы модуля, строки | `-b` |
|
||||||
|
|
||||||
@@ -159,6 +162,21 @@ python tools/index/query_1c.py refs Document.ЗаказКлиента -b source
|
|||||||
Без `-b` — все конфигурации в индексе; `--limit` действует **на каждую**.
|
Без `-b` — все конфигурации в индексе; `--limit` действует **на каждую**.
|
||||||
Составной тип поля: `→ DocumentRef.ЗаказКлиента (среди прочего: DocumentRef.…)`.
|
Составной тип поля: `→ DocumentRef.ЗаказКлиента (среди прочего: DocumentRef.…)`.
|
||||||
|
|
||||||
|
### `movements` — регистратор ↔ регистры движений
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# документ → регистры, в которых он регистратор
|
||||||
|
python tools/index/query_1c.py movements ЗаказКлиента -b target
|
||||||
|
python tools/index/query_1c.py movements Document.РеализацияТоваровУслуг -b target
|
||||||
|
|
||||||
|
# регистр → документы-регистраторы
|
||||||
|
python tools/index/query_1c.py movements AccumulationRegister.ТоварыНаСкладах -b target
|
||||||
|
python tools/index/query_1c.py movements ТоварыНаСкладах -b target --type AccumulationRegister
|
||||||
|
```
|
||||||
|
|
||||||
|
Данные из свойства метаданных `RegisterRecords` (таблица `register_records` в индексе).
|
||||||
|
При первом запуске утилиты v0.3+ схема индекса мигрирует 2→3 автоматически (без полного `reindex`).
|
||||||
|
|
||||||
### `modules` / `module`
|
### `modules` / `module`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.3.0"
|
||||||
__status__ = "in development"
|
__status__ = "in development"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+116
-6
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
SCHEMA_VERSION = 2
|
SCHEMA_VERSION = 3
|
||||||
|
|
||||||
DDL = """
|
DDL = """
|
||||||
PRAGMA journal_mode=WAL;
|
PRAGMA journal_mode=WAL;
|
||||||
@@ -82,6 +82,13 @@ CREATE TABLE IF NOT EXISTS refs (
|
|||||||
to_name TEXT NOT NULL
|
to_name TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS register_records (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
config TEXT NOT NULL,
|
||||||
|
document_full_name TEXT NOT NULL,
|
||||||
|
register_full_name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS modules (
|
CREATE TABLE IF NOT EXISTS modules (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
config TEXT NOT NULL,
|
config TEXT NOT NULL,
|
||||||
@@ -123,12 +130,84 @@ CREATE INDEX IF NOT EXISTS idx_fields_object ON fields(object_id);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_refs_to ON refs(to_kind, to_name);
|
CREATE INDEX IF NOT EXISTS idx_refs_to ON refs(to_kind, to_name);
|
||||||
CREATE INDEX IF NOT EXISTS idx_refs_from ON refs(from_full_name);
|
CREATE INDEX IF NOT EXISTS idx_refs_from ON refs(from_full_name);
|
||||||
CREATE INDEX IF NOT EXISTS idx_refs_config ON refs(config);
|
CREATE INDEX IF NOT EXISTS idx_refs_config ON refs(config);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_regrec_register ON register_records(register_full_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_regrec_document ON register_records(document_full_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_regrec_config ON register_records(config);
|
||||||
CREATE INDEX IF NOT EXISTS idx_modules_config ON modules(config);
|
CREATE INDEX IF NOT EXISTS idx_modules_config ON modules(config);
|
||||||
CREATE INDEX IF NOT EXISTS idx_modules_owner ON modules(owner);
|
CREATE INDEX IF NOT EXISTS idx_modules_owner ON modules(owner);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_config ON files(config);
|
CREATE INDEX IF NOT EXISTS idx_files_config ON files(config);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
||||||
|
(name,),
|
||||||
|
).fetchone()
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_register_records(conn: sqlite3.Connection) -> int:
|
||||||
|
"""Заполнить register_records из JSON объектов (документы → RegisterRecords)."""
|
||||||
|
conn.execute("DELETE FROM register_records")
|
||||||
|
n = 0
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT config, full_name, json FROM objects WHERE object_type='Document'"
|
||||||
|
).fetchall()
|
||||||
|
batch: list[tuple[str, str, str]] = []
|
||||||
|
for r in rows:
|
||||||
|
try:
|
||||||
|
data = json.loads(r["json"])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
for reg in data.get("register_records") or []:
|
||||||
|
if not reg or not isinstance(reg, str):
|
||||||
|
continue
|
||||||
|
batch.append((r["config"], r["full_name"], reg))
|
||||||
|
n += 1
|
||||||
|
if len(batch) >= 500:
|
||||||
|
conn.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO register_records(config, document_full_name, register_full_name)
|
||||||
|
VALUES(?,?,?)
|
||||||
|
""",
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
batch.clear()
|
||||||
|
if batch:
|
||||||
|
conn.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO register_records(config, document_full_name, register_full_name)
|
||||||
|
VALUES(?,?,?)
|
||||||
|
""",
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_schema(conn: sqlite3.Connection, from_version: int) -> None:
|
||||||
|
"""Мягкая миграция без wipe БД."""
|
||||||
|
if from_version < 3:
|
||||||
|
conn.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS register_records (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
config TEXT NOT NULL,
|
||||||
|
document_full_name TEXT NOT NULL,
|
||||||
|
register_full_name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_regrec_register ON register_records(register_full_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_regrec_document ON register_records(document_full_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_regrec_config ON register_records(config);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
backfill_register_records(conn)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO meta(key, value) VALUES('schema_version', ?)",
|
||||||
|
(str(SCHEMA_VERSION),),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def utcnow() -> str:
|
def utcnow() -> str:
|
||||||
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
@@ -161,11 +240,18 @@ def init_db(conn: sqlite3.Connection) -> None:
|
|||||||
"INSERT OR REPLACE INTO meta(key, value) VALUES('created_at', ?)",
|
"INSERT OR REPLACE INTO meta(key, value) VALUES('created_at', ?)",
|
||||||
(utcnow(),),
|
(utcnow(),),
|
||||||
)
|
)
|
||||||
elif int(row["value"]) != SCHEMA_VERSION:
|
else:
|
||||||
raise RuntimeError(
|
ver = int(row["value"])
|
||||||
f"Несовместимая схема индекса (есть {row['value']}, нужна {SCHEMA_VERSION}). "
|
if ver > SCHEMA_VERSION:
|
||||||
"Запустите: python tools/index/index_1c.py reindex --full"
|
raise RuntimeError(
|
||||||
)
|
f"Индекс новее утилиты (схема {ver}, утилита {SCHEMA_VERSION}). "
|
||||||
|
"Обновите tools/index."
|
||||||
|
)
|
||||||
|
if ver < SCHEMA_VERSION:
|
||||||
|
migrate_schema(conn, ver)
|
||||||
|
elif not _table_exists(conn, "register_records"):
|
||||||
|
# schema_version=3, но таблица потеряна — восстановить
|
||||||
|
migrate_schema(conn, 2)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -231,6 +317,11 @@ def delete_file_cascade(conn: sqlite3.Connection, file_id: int) -> None:
|
|||||||
"DELETE FROM refs WHERE config=? AND from_full_name=?",
|
"DELETE FROM refs WHERE config=? AND from_full_name=?",
|
||||||
(row["config"], row["full_name"]),
|
(row["config"], row["full_name"]),
|
||||||
)
|
)
|
||||||
|
if _table_exists(conn, "register_records"):
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM register_records WHERE config=? AND document_full_name=?",
|
||||||
|
(row["config"], row["full_name"]),
|
||||||
|
)
|
||||||
conn.execute("DELETE FROM objects WHERE id=?", (oid,))
|
conn.execute("DELETE FROM objects WHERE id=?", (oid,))
|
||||||
|
|
||||||
mod = conn.execute(
|
mod = conn.execute(
|
||||||
@@ -259,6 +350,8 @@ def delete_config(conn: sqlite3.Connection, config: str) -> None:
|
|||||||
conn.execute("DELETE FROM modules_fts WHERE module_id=?", (mid,))
|
conn.execute("DELETE FROM modules_fts WHERE module_id=?", (mid,))
|
||||||
conn.execute("DELETE FROM fields WHERE config=?", (config,))
|
conn.execute("DELETE FROM fields WHERE config=?", (config,))
|
||||||
conn.execute("DELETE FROM refs WHERE config=?", (config,))
|
conn.execute("DELETE FROM refs WHERE config=?", (config,))
|
||||||
|
if _table_exists(conn, "register_records"):
|
||||||
|
conn.execute("DELETE FROM register_records WHERE config=?", (config,))
|
||||||
conn.execute("DELETE FROM objects WHERE config=?", (config,))
|
conn.execute("DELETE FROM objects WHERE config=?", (config,))
|
||||||
conn.execute("DELETE FROM modules WHERE config=?", (config,))
|
conn.execute("DELETE FROM modules WHERE config=?", (config,))
|
||||||
conn.execute("DELETE FROM files WHERE config=?", (config,))
|
conn.execute("DELETE FROM files WHERE config=?", (config,))
|
||||||
@@ -318,6 +411,11 @@ def upsert_object(
|
|||||||
"DELETE FROM refs WHERE config=? AND from_full_name=?",
|
"DELETE FROM refs WHERE config=? AND from_full_name=?",
|
||||||
(config, full_name),
|
(config, full_name),
|
||||||
)
|
)
|
||||||
|
if _table_exists(conn, "register_records"):
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM register_records WHERE config=? AND document_full_name=?",
|
||||||
|
(config, full_name),
|
||||||
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE objects SET object_type=?, name=?, synonym=?, comment=?, tooltip=?,
|
UPDATE objects SET object_type=?, name=?, synonym=?, comment=?, tooltip=?,
|
||||||
@@ -411,6 +509,18 @@ def upsert_object(
|
|||||||
r.get("name"),
|
r.get("name"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if _table_exists(conn, "register_records"):
|
||||||
|
for reg in obj.get("register_records") or []:
|
||||||
|
if not reg or not isinstance(reg, str):
|
||||||
|
continue
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO register_records(config, document_full_name, register_full_name)
|
||||||
|
VALUES(?,?,?)
|
||||||
|
""",
|
||||||
|
(config, full_name, reg),
|
||||||
|
)
|
||||||
return oid
|
return oid
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,238 @@ def open_index(project_root: Path, *, readonly: bool = True) -> sqlite3.Connecti
|
|||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Индекс не найден: {path}. Запустите: python tools/index/index_1c.py reindex --full"
|
f"Индекс не найден: {path}. Запустите: python tools/index/index_1c.py reindex --full"
|
||||||
)
|
)
|
||||||
|
# мягкая миграция схемы (например 2→3: таблица register_records)
|
||||||
|
probe = dbmod.connect(path, readonly=True)
|
||||||
|
need_migrate = False
|
||||||
|
try:
|
||||||
|
row = probe.execute(
|
||||||
|
"SELECT value FROM meta WHERE key='schema_version'"
|
||||||
|
).fetchone()
|
||||||
|
ver = int(row["value"]) if row else 0
|
||||||
|
if ver < dbmod.SCHEMA_VERSION:
|
||||||
|
need_migrate = True
|
||||||
|
elif ver == dbmod.SCHEMA_VERSION:
|
||||||
|
t = probe.execute(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='register_records'"
|
||||||
|
).fetchone()
|
||||||
|
if t is None:
|
||||||
|
need_migrate = True
|
||||||
|
finally:
|
||||||
|
probe.close()
|
||||||
|
|
||||||
|
if need_migrate:
|
||||||
|
wconn = dbmod.connect(path, readonly=False)
|
||||||
|
try:
|
||||||
|
dbmod.init_db(wconn)
|
||||||
|
finally:
|
||||||
|
wconn.close()
|
||||||
|
|
||||||
return dbmod.connect(path, readonly=readonly)
|
return dbmod.connect(path, readonly=readonly)
|
||||||
|
|
||||||
|
|
||||||
|
REGISTER_OBJECT_TYPES = frozenset(
|
||||||
|
{
|
||||||
|
"AccumulationRegister",
|
||||||
|
"InformationRegister",
|
||||||
|
"AccountingRegister",
|
||||||
|
"CalculationRegister",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_movement_targets(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
target: str,
|
||||||
|
*,
|
||||||
|
configs: list[str] | None = None,
|
||||||
|
object_type: str | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Найти объект(ы) для movements: документ или регистр."""
|
||||||
|
sql = """
|
||||||
|
SELECT config, full_name, object_type, name, synonym
|
||||||
|
FROM objects
|
||||||
|
WHERE (full_name = ? OR name = ?)
|
||||||
|
"""
|
||||||
|
params: list[Any] = [target, target]
|
||||||
|
if configs:
|
||||||
|
sql += f" AND config IN ({','.join('?' * len(configs))})"
|
||||||
|
params.extend(configs)
|
||||||
|
if object_type:
|
||||||
|
sql += " AND object_type = ?"
|
||||||
|
params.append(object_type)
|
||||||
|
sql += " ORDER BY config, full_name"
|
||||||
|
rows = [dict(r) for r in conn.execute(sql, params)]
|
||||||
|
if rows:
|
||||||
|
return rows
|
||||||
|
|
||||||
|
# Document.X / AccumulationRegister.X без точного совпадения типа
|
||||||
|
if "." in target:
|
||||||
|
kind, name = target.split(".", 1)
|
||||||
|
sql = """
|
||||||
|
SELECT config, full_name, object_type, name, synonym
|
||||||
|
FROM objects
|
||||||
|
WHERE object_type = ? AND name = ?
|
||||||
|
"""
|
||||||
|
params = [kind, name]
|
||||||
|
if configs:
|
||||||
|
sql += f" AND config IN ({','.join('?' * len(configs))})"
|
||||||
|
params.extend(configs)
|
||||||
|
sql += " ORDER BY config, full_name"
|
||||||
|
rows = [dict(r) for r in conn.execute(sql, params)]
|
||||||
|
if rows:
|
||||||
|
return rows
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
SELECT config, full_name, object_type, name, synonym
|
||||||
|
FROM objects
|
||||||
|
WHERE full_name LIKE ?
|
||||||
|
"""
|
||||||
|
params = [f"%.{target}"]
|
||||||
|
if configs:
|
||||||
|
sql += f" AND config IN ({','.join('?' * len(configs))})"
|
||||||
|
params.extend(configs)
|
||||||
|
if object_type:
|
||||||
|
sql += " AND object_type = ?"
|
||||||
|
params.append(object_type)
|
||||||
|
sql += " ORDER BY config, full_name LIMIT 20"
|
||||||
|
return [dict(r) for r in conn.execute(sql, params)]
|
||||||
|
|
||||||
|
|
||||||
|
def find_movements(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
target: str,
|
||||||
|
*,
|
||||||
|
configs: list[str] | None = None,
|
||||||
|
object_type: str | None = None,
|
||||||
|
limit: int = 200,
|
||||||
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||||
|
"""Связи регистратор ↔ регистры движений (RegisterRecords).
|
||||||
|
|
||||||
|
Возвращает ``(resolved_objects, rows)``, где каждая строка:
|
||||||
|
- direction: ``document_to_registers`` | ``register_to_documents``
|
||||||
|
- config, from_full_name, to_full_name
|
||||||
|
- from_synonym / to_synonym (если есть в индексе)
|
||||||
|
|
||||||
|
``limit`` — максимум связей **на каждый** найденный объект×конфигурацию.
|
||||||
|
"""
|
||||||
|
resolved = _resolve_movement_targets(
|
||||||
|
conn, target, configs=configs, object_type=object_type
|
||||||
|
)
|
||||||
|
# оставить документы и регистры
|
||||||
|
resolved = [
|
||||||
|
r
|
||||||
|
for r in resolved
|
||||||
|
if r["object_type"] == "Document" or r["object_type"] in REGISTER_OBJECT_TYPES
|
||||||
|
]
|
||||||
|
if not resolved:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
has_table = (
|
||||||
|
conn.execute(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='register_records'"
|
||||||
|
).fetchone()
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
# кэш синонимов
|
||||||
|
def _synonyms(full_names: set[str], cfg: str) -> dict[str, str]:
|
||||||
|
if not full_names:
|
||||||
|
return {}
|
||||||
|
names = sorted(full_names)
|
||||||
|
ph = ",".join("?" * len(names))
|
||||||
|
return {
|
||||||
|
r["full_name"]: r["synonym"] or ""
|
||||||
|
for r in conn.execute(
|
||||||
|
f"SELECT full_name, synonym FROM objects WHERE config=? AND full_name IN ({ph})",
|
||||||
|
[cfg, *names],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for obj in resolved:
|
||||||
|
cfg = obj["config"]
|
||||||
|
full = obj["full_name"]
|
||||||
|
if obj["object_type"] == "Document":
|
||||||
|
if has_table:
|
||||||
|
links = [
|
||||||
|
r["register_full_name"]
|
||||||
|
for r in conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT register_full_name FROM register_records
|
||||||
|
WHERE config=? AND document_full_name=?
|
||||||
|
ORDER BY register_full_name
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(cfg, full, limit),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT json FROM objects WHERE config=? AND full_name=?",
|
||||||
|
(cfg, full),
|
||||||
|
).fetchone()
|
||||||
|
links = []
|
||||||
|
if row:
|
||||||
|
try:
|
||||||
|
links = list(json.loads(row["json"]).get("register_records") or [])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
links = []
|
||||||
|
links = sorted(links)[:limit]
|
||||||
|
syns = _synonyms(set(links), cfg)
|
||||||
|
for reg in links:
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"direction": "document_to_registers",
|
||||||
|
"config": cfg,
|
||||||
|
"from_full_name": full,
|
||||||
|
"from_synonym": obj.get("synonym") or "",
|
||||||
|
"to_full_name": reg,
|
||||||
|
"to_synonym": syns.get(reg, ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# регистр → документы-регистраторы
|
||||||
|
if has_table:
|
||||||
|
docs = [
|
||||||
|
r["document_full_name"]
|
||||||
|
for r in conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT document_full_name FROM register_records
|
||||||
|
WHERE config=? AND register_full_name=?
|
||||||
|
ORDER BY document_full_name
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(cfg, full, limit),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
docs = []
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT full_name, json FROM objects WHERE config=? AND object_type='Document'",
|
||||||
|
(cfg,),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
regs = json.loads(r["json"]).get("register_records") or []
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if full in regs:
|
||||||
|
docs.append(r["full_name"])
|
||||||
|
docs = sorted(docs)[:limit]
|
||||||
|
syns = _synonyms(set(docs), cfg)
|
||||||
|
for doc in docs:
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"direction": "register_to_documents",
|
||||||
|
"config": cfg,
|
||||||
|
"from_full_name": full,
|
||||||
|
"from_synonym": obj.get("synonym") or "",
|
||||||
|
"to_full_name": doc,
|
||||||
|
"to_synonym": syns.get(doc, ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return resolved, out
|
||||||
|
|
||||||
|
|
||||||
def search_objects(
|
def search_objects(
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
query: str,
|
query: str,
|
||||||
|
|||||||
+80
@@ -13,6 +13,8 @@
|
|||||||
python tools/index/query_1c.py show Document.ЗаказКлиента -b crm3-26
|
python tools/index/query_1c.py show Document.ЗаказКлиента -b crm3-26
|
||||||
python tools/index/query_1c.py modules \"РВС_Transfer\" -b ws-rhana
|
python tools/index/query_1c.py modules \"РВС_Transfer\" -b ws-rhana
|
||||||
python tools/index/query_1c.py refs Catalog.Партнеры -b source
|
python tools/index/query_1c.py refs Catalog.Партнеры -b source
|
||||||
|
python tools/index/query_1c.py movements ЗаказКлиента -b target
|
||||||
|
python tools/index/query_1c.py movements AccumulationRegister.ТоварыНаСкладах -b target
|
||||||
python tools/index/query_1c.py object ЗаказКлиента -b target
|
python tools/index/query_1c.py object ЗаказКлиента -b target
|
||||||
python tools/index/query_1c.py object Партнер -b crm3-26 --clear
|
python tools/index/query_1c.py object Партнер -b crm3-26 --clear
|
||||||
python tools/index/query_1c.py module CommonModules/РВС_TransferAPIДокументы
|
python tools/index/query_1c.py module CommonModules/РВС_TransferAPIДокументы
|
||||||
@@ -43,6 +45,7 @@ from index1c.cli import ( # noqa: E402
|
|||||||
from index1c.config import resolve_object_type # noqa: E402
|
from index1c.config import resolve_object_type # noqa: E402
|
||||||
from index1c import read_version_file # noqa: E402
|
from index1c import read_version_file # noqa: E402
|
||||||
from index1c.search import ( # noqa: E402
|
from index1c.search import ( # noqa: E402
|
||||||
|
find_movements,
|
||||||
find_refs_to,
|
find_refs_to,
|
||||||
get_module,
|
get_module,
|
||||||
get_object,
|
get_object,
|
||||||
@@ -182,6 +185,62 @@ def cmd_refs(args: argparse.Namespace) -> int:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_movements(args: argparse.Namespace) -> int:
|
||||||
|
root = resolve_project_root(args)
|
||||||
|
conn = _open_or_exit(root)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
baseconfs = get_baseconfs_for_query(args, conn, root)
|
||||||
|
except BaseconfError as e:
|
||||||
|
return handle_baseconf_error(e)
|
||||||
|
otype = resolve_object_type(args.type) if args.type else None
|
||||||
|
resolved, rows = find_movements(
|
||||||
|
conn,
|
||||||
|
args.target,
|
||||||
|
configs=baseconfs,
|
||||||
|
object_type=otype,
|
||||||
|
limit=args.limit,
|
||||||
|
)
|
||||||
|
if not resolved:
|
||||||
|
print(
|
||||||
|
format_not_found_object(args.target, baseconfs=baseconfs, conn=conn),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
if not rows:
|
||||||
|
names = ", ".join(f"`{r['full_name']}`" for r in resolved)
|
||||||
|
print(f"Связей регистратор↔регистр для {names} не найдено.")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
# группировка: config → from_full_name → links
|
||||||
|
current_cfg = None
|
||||||
|
current_from = None
|
||||||
|
for r in rows:
|
||||||
|
if r["config"] != current_cfg:
|
||||||
|
current_cfg = r["config"]
|
||||||
|
current_from = None
|
||||||
|
print(f"## [{current_cfg}]")
|
||||||
|
if r["from_full_name"] != current_from:
|
||||||
|
current_from = r["from_full_name"]
|
||||||
|
syn = f" — {r['from_synonym']}" if r.get("from_synonym") else ""
|
||||||
|
if r["direction"] == "document_to_registers":
|
||||||
|
print(f"### Документ `{current_from}`{syn} → регистры движений")
|
||||||
|
else:
|
||||||
|
print(f"### Регистр `{current_from}`{syn} → регистраторы")
|
||||||
|
to_syn = f" — {r['to_synonym']}" if r.get("to_synonym") else ""
|
||||||
|
print(f"- `{r['to_full_name']}`{to_syn}")
|
||||||
|
|
||||||
|
if len(resolved) > 1:
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
f"Найдено объектов: {len(resolved)}. "
|
||||||
|
"Уточните полное имя (Document.… / AccumulationRegister.…) или --type."
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def cmd_show(args: argparse.Namespace) -> int:
|
def cmd_show(args: argparse.Namespace) -> int:
|
||||||
root = resolve_project_root(args)
|
root = resolve_project_root(args)
|
||||||
conn = _open_or_exit(root)
|
conn = _open_or_exit(root)
|
||||||
@@ -336,6 +395,27 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
)
|
)
|
||||||
ref.set_defaults(func=cmd_refs)
|
ref.set_defaults(func=cmd_refs)
|
||||||
|
|
||||||
|
mov = sub.add_parser(
|
||||||
|
"movements",
|
||||||
|
help="Регистратор ↔ регистры движений (RegisterRecords)",
|
||||||
|
)
|
||||||
|
mov.add_argument(
|
||||||
|
"target",
|
||||||
|
help="Document.ЗаказКлиента | AccumulationRegister.ТоварыНаСкладах | имя",
|
||||||
|
)
|
||||||
|
add_baseconf_args(mov)
|
||||||
|
mov.add_argument(
|
||||||
|
"--type",
|
||||||
|
help="Уточнить тип: Document, AccumulationRegister, InformationRegister, …",
|
||||||
|
)
|
||||||
|
mov.add_argument(
|
||||||
|
"--limit",
|
||||||
|
type=int,
|
||||||
|
default=200,
|
||||||
|
help="Макс. связей на каждый найденный объект (по умолчанию 200)",
|
||||||
|
)
|
||||||
|
mov.set_defaults(func=cmd_movements)
|
||||||
|
|
||||||
sh = sub.add_parser("show", help="Карточка объекта (JSON: поля, типы, ссылки)")
|
sh = sub.add_parser("show", help="Карточка объекта (JSON: поля, типы, ссылки)")
|
||||||
sh.add_argument("name", help="ЗаказКлиента | Document.ЗаказКлиента")
|
sh.add_argument("name", help="ЗаказКлиента | Document.ЗаказКлиента")
|
||||||
add_baseconf_args(sh)
|
add_baseconf_args(sh)
|
||||||
|
|||||||
Reference in New Issue
Block a user