Release 0.2.0: object tree, refs/modules options, docs
Add query object (--clear), modules -n, composite refs labels, FillChecking/TypeSet parsing, VERSION/CHANGELOG and --version. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Все значимые изменения **tools/index** (индекс метаданных и модулей 1С) фиксируются в этом файле.
|
||||||
|
|
||||||
|
Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
Версионирование: [Semantic Versioning](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-07-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Команда **`query_1c.py object`** — поиск по имени/синониму объекта или реквизита и вывод дерева структуры:
|
||||||
|
- реквизиты, измерения, ресурсы, табличные части и колонки;
|
||||||
|
- обязательность заполнения (`FillChecking`: обяз. / необяз.);
|
||||||
|
- типы данных и ссылочные типы (`CatalogRef.*`, `DocumentRef.*`, `DefinedType.*`, …);
|
||||||
|
- опция **`--clear`** — только найденные реквизиты или только заголовок объекта.
|
||||||
|
- Опция **`query_1c.py modules --names-only` / `-n`** — список модулей (owner + синоним) без фрагментов кода.
|
||||||
|
- В **`refs`**: лимит **на каждую** конфигурацию; группировка по baseconf; пометка **«среди прочего»** для составных типов.
|
||||||
|
- Разбор **`TypeSet`** (определяемые типы) и **`FillChecking`** в парсере метаданных.
|
||||||
|
- Файлы **`VERSION`**, **`CHANGELOG.md`**; флаг **`--version`** у `index_1c.py` и `query_1c.py`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Параметр конфигурации 1С: **`--baseconf` / `-b`** (алиасы `target`→`crm3-26`, `source`→`crm3-dev`).
|
||||||
|
- **`--config` / `-c`** — только файл настроек утилиты (JSON), не выгрузка 1С.
|
||||||
|
- Информативные ошибки: нет индекса / нет папки `src/` / baseconf не проиндексирован / объект не найден (с подсказками).
|
||||||
|
- Расширена документация `README.md` (примеры, режимы, применение в Cursor).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `refs` без `-b` больше не ограничивался первой конфигурацией из‑за `ORDER BY` + `LIMIT`.
|
||||||
|
- Фильтры `-b` / `--type` в `object`: скобки в SQL (`OR` vs `AND`).
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-07-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Единый индекс проекта `cache/1c_meta/index.sqlite` (объекты, поля, ссылки, модули BSL, FTS5).
|
||||||
|
- `index_1c.py`: `reindex` (full / incremental), `status`, `list`; многопоточный разбор (`-j`).
|
||||||
|
- `query_1c.py`: `search`, `show`, `refs`, `modules`, `module`.
|
||||||
|
- Инкремент по `mtime`+`size` файлов; индекс модулей `**/*.bsl`.
|
||||||
|
|
||||||
|
[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
|
||||||
@@ -1,45 +1,61 @@
|
|||||||
# Индекс метаданных и модулей конфигураций 1С
|
# Индекс метаданных и модулей конфигураций 1С
|
||||||
|
|
||||||
Единый кэш: **`cache/1c_meta/index.sqlite`** — все выгрузки 1С проекта в одной БД (FTS5).
|
**Версия:** см. [`VERSION`](VERSION) (текущая: **0.2.0**) · [CHANGELOG](CHANGELOG.md)
|
||||||
|
|
||||||
|
Единый кэш проекта: **`cache/1c_meta/index.sqlite`** — все выгрузки 1С в одной БД (SQLite FTS5).
|
||||||
|
|
||||||
| Скрипт | Назначение |
|
| Скрипт | Назначение |
|
||||||
|--------|------------|
|
|--------|------------|
|
||||||
| [`index_1c.py`](index_1c.py) | построение и обновление индекса |
|
| [`index_1c.py`](index_1c.py) | построение и обновление индекса |
|
||||||
| [`query_1c.py`](query_1c.py) | поиск по готовому индексу |
|
| [`query_1c.py`](query_1c.py) | поиск и просмотр по готовому индексу |
|
||||||
|
|
||||||
Документация для Cursor: [`.cursor/rules/1c-meta-index.mdc`](../.cursor/rules/1c-meta-index.mdc)
|
Репозиторий утилиты: <https://git.p7net.ru/1c/index.git>
|
||||||
|
Правило для Cursor (в корне CRM3-26): `.cursor/rules/1c-meta-index.mdc`
|
||||||
|
|
||||||
---
|
```bash
|
||||||
|
python tools/index/index_1c.py --version
|
||||||
## Параметры командной строки
|
python tools/index/query_1c.py --version
|
||||||
|
```
|
||||||
| Параметр | Короткий | Что это |
|
|
||||||
|----------|----------|---------|
|
|
||||||
| `--config` | `-c` | **Файл настроек утилиты** (JSON), см. [`config.example.json`](config.example.json). Не конфигурация 1С. |
|
|
||||||
| `--root` | `-r` | Корень проекта CRM3-26 (если скрипт запущен не из дерева проекта). |
|
|
||||||
| `--baseconf` | `-b` | **Выгрузка конфигурации 1С** в репозитории — папка `<name>/src/`. Можно указывать несколько раз. |
|
|
||||||
|
|
||||||
### Алиасы `--baseconf`
|
|
||||||
|
|
||||||
| Алиас | Папка в репозитории |
|
|
||||||
|-------|---------------------|
|
|
||||||
| `target`, `crm3_26` | `crm3-26` |
|
|
||||||
| `source`, `crm3_old`, `crm3-old`, `crm3_dev` | `crm3-dev` |
|
|
||||||
|
|
||||||
Список выгрузок и алиасов: `python tools/index/index_1c.py list`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Зачем это нужно
|
## Зачем это нужно
|
||||||
|
|
||||||
При работе в Cursor поиск по XML/BSL всего дерева `crm3-26/src` (десятки тысяч файлов) медленный. Индекс один раз разбирает метаданные и модули и даёт:
|
Обход десятков тысяч XML/BSL в `crm3-26/src` и соседних выгрузках через Grep в Cursor медленный. Индекс один раз разбирает метаданные и модули и даёт:
|
||||||
|
|
||||||
- поиск объектов по имени, синониму, полям;
|
| Задача | Команда |
|
||||||
- обратный индекс ссылок (`refs`);
|
|--------|---------|
|
||||||
- поиск по BSL (процедуры, фрагменты кода);
|
| Найти объект по имени/синониму | `query_1c.py search` / `object` |
|
||||||
- фильтр по конкретной выгрузке (`-b crm3-26`).
|
| Структура реквизитов, обязательность, типы | `query_1c.py object` |
|
||||||
|
| Кто ссылается на справочник/документ | `query_1c.py refs` |
|
||||||
|
| Найти процедуру/фрагмент в BSL | `query_1c.py modules` |
|
||||||
|
| Сравнить целевую и исходную конфигурацию | `-b target` / `-b source` |
|
||||||
|
|
||||||
**Типичный workflow агента:** `query_1c.py search` → `show` / `refs` → открыть конкретный `.bsl` или `.xml`, без полного Grep по конфигурации.
|
**Типичный workflow агента:**
|
||||||
|
`reindex` (после выгрузки) → `object` / `search` → `refs` / `modules` → открыть конкретный `.xml` / `.bsl`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Параметры командной строки (общие)
|
||||||
|
|
||||||
|
| Параметр | Короткий | Назначение |
|
||||||
|
|----------|----------|------------|
|
||||||
|
| `--config` | `-c` | Файл **настроек утилиты** (JSON). **Не** конфигурация 1С. См. [`config.example.json`](config.example.json). |
|
||||||
|
| `--root` | `-r` | Корень проекта CRM3-26 (если запуск не из дерева проекта). |
|
||||||
|
| `--baseconf` | `-b` | Выгрузка конфигурации 1С: папка `<name>/src/` в репозитории. Можно несколько раз. |
|
||||||
|
| `--version` | | Версия утилиты. |
|
||||||
|
|
||||||
|
Приоритет корня проекта: `--root` > `project_root` в `--config` > авто из расположения `tools/index/`.
|
||||||
|
|
||||||
|
### Алиасы `--baseconf`
|
||||||
|
|
||||||
|
| Алиас | Папка |
|
||||||
|
|-------|-------|
|
||||||
|
| `target`, `crm3_26` | `crm3-26` |
|
||||||
|
| `source`, `crm3_old`, `crm3-old`, `crm3_dev` | `crm3-dev` |
|
||||||
|
|
||||||
|
Другие выгрузки без алиаса: `crm3-26.rhana`, `crm3-dev.rhana`, `ws-rhana`, `bu-corp`, …
|
||||||
|
Полный список: `python tools/index/index_1c.py list`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,107 +65,158 @@
|
|||||||
|
|
||||||
| Команда | Описание |
|
| Команда | Описание |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| `reindex` | обновить индекс (по умолчанию только изменившиеся файлы) |
|
| `reindex` | обновить индекс |
|
||||||
| `status` | размер БД, метаданные, список проиндексированных baseconf |
|
| `status` | размер БД, meta, список проиндексированных baseconf |
|
||||||
| `list` | какие `<name>/src/` есть в репозитории и что уже в индексе |
|
| `list` | выгрузки с `src/` в репозитории + что уже в индексе |
|
||||||
|
|
||||||
### Режимы `reindex`
|
### Режимы `reindex`
|
||||||
|
|
||||||
| Вызов | Поведение |
|
| Вызов | Поведение |
|
||||||
|-------|-----------|
|
|-------|-----------|
|
||||||
| `reindex -j 12` | инкремент всех известных выгрузок с `src/` |
|
| `reindex -j 12` | инкремент: только файлы с изменившимися `mtime`/`size` |
|
||||||
| `reindex --full -j 12` | удалить `index.sqlite` и собрать заново |
|
| `reindex --full -j 12` | удалить всю БД и собрать заново все известные выгрузки |
|
||||||
| `reindex --full -b crm3-26` | пересобрать только `crm3-26` (остальные в БД сохраняются) |
|
| `reindex --full -b crm3-26` | пересобрать только указанную конфигурацию |
|
||||||
| `reindex -b target -t Documents,Catalogs` | только документы и справочники целевой конфигурации |
|
| `reindex -b target -t Documents,Catalogs` | только выбранные типы объектов |
|
||||||
| `reindex -b ws-rhana --no-modules` | только XML метаданных, без `.bsl` |
|
| `reindex -b ws-rhana --no-modules` | метаданные без BSL |
|
||||||
|
| `reindex --md` | дополнительно Markdown в `cache/1c_meta/md/<baseconf>/` |
|
||||||
|
|
||||||
Дополнительно: `--md` — экспорт Markdown в `cache/1c_meta/md/<baseconf>/` для `@` в чате.
|
`-j` / `--workers` — число потоков (по умолчанию 4–16).
|
||||||
|
|
||||||
### Что попадает в индекс
|
### Что индексируется
|
||||||
|
|
||||||
- **Метаданные:** `src/<ТипObjects>/*.xml` — имя, синоним, реквизиты, измерения, типы, ссылки.
|
- **Метаданные** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`.
|
||||||
- **Модули:** все `**/*.bsl` — путь, владелец (`Document.ЗаказКлиента`), процедуры/функции, тело (до 200 КБ в FTS).
|
- **Модули** `**/*.bsl`: путь, owner (`Document.X`), вид модуля, имена процедур/функций, тело (до 200 КБ в FTS).
|
||||||
|
|
||||||
Пропускаются по умолчанию: картинки, стили, XDTO, шаблоны, языки.
|
По умолчанию пропускаются: `CommonPicture`, `StyleItem`, `XDTOPackage`, `CommonTemplate`, `Language`, `Bot`.
|
||||||
|
|
||||||
|
### Примеры переиндексации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# после обновления выгрузки целевой конфигурации
|
||||||
|
python tools/index/index_1c.py reindex -b target -j 12
|
||||||
|
|
||||||
|
# полная пересборка всего проекта
|
||||||
|
python tools/index/index_1c.py reindex --full -j 12
|
||||||
|
|
||||||
|
# только документы и справочники исходной + расширения
|
||||||
|
python tools/index/index_1c.py reindex -b source -b crm3-dev.rhana -t Documents,Catalogs -j 8
|
||||||
|
|
||||||
|
python tools/index/index_1c.py status
|
||||||
|
python tools/index/index_1c.py list
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `query_1c.py` — поиск
|
## `query_1c.py` — поиск
|
||||||
|
|
||||||
Требует готовый `cache/1c_meta/index.sqlite` (см. `index_1c.py reindex`).
|
Нужен готовый `cache/1c_meta/index.sqlite`.
|
||||||
|
|
||||||
### Команды
|
### Команды
|
||||||
|
|
||||||
| Команда | Аргумент | Результат |
|
| Команда | Аргумент | Результат | Полезные опции |
|
||||||
|---------|----------|-----------|
|
|---------|----------|-----------|----------------|
|
||||||
| `search` | строка запроса | объекты метаданных (FTS) |
|
| `search` | строка | объекты метаданных (FTS) | `-b`, `--type`, `--limit` |
|
||||||
| `show` | имя объекта | полная карточка JSON |
|
| `object` | имя/синоним объекта **или** реквизита | дерево структуры | `-b`, `--type`, `--clear`, `--limit` |
|
||||||
| `refs` | `Catalog.Партнеры` | кто ссылается полем-ссылкой |
|
| `show` | имя объекта | полная карточка JSON | `-b` |
|
||||||
| `modules` | строка | поиск по BSL |
|
| `refs` | `Catalog.Партнеры` / `ЗаказКлиента` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
|
||||||
| `module` | путь или owner | метаданные модуля (символы, строки) |
|
| `modules` | строка | поиск по BSL | `-b`, `-n` / `--names-only`, `--limit` |
|
||||||
|
| `module` | путь или owner | символы модуля, строки | `-b` |
|
||||||
|
|
||||||
### Примеры
|
### `object` — дерево реквизитов
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# все конфигурации в индексе
|
# полная структура документа
|
||||||
python tools/index/query_1c.py search "ЗаказКлиента"
|
python tools/index/query_1c.py object ЗаказКлиента -b target
|
||||||
|
|
||||||
# только целевая (алиас target)
|
# только реквизиты с именем/синонимом «Партнер» в документах
|
||||||
python tools/index/query_1c.py show ЗаказКлиента -b target
|
python tools/index/query_1c.py object Партнер -b target --type Document --clear
|
||||||
python tools/index/query_1c.py show Document.ЗаказКлиента -b crm3-26
|
|
||||||
|
|
||||||
# исходная + расширение
|
# совпал сам объект → --clear печатает только заголовок
|
||||||
python tools/index/query_1c.py search "Партнер" -b source -b crm3-dev.rhana
|
python tools/index/query_1c.py object ЗаказКлиента -b target --clear
|
||||||
|
|
||||||
# модули ws-rhana
|
|
||||||
python tools/index/query_1c.py modules "ОбработатьHealth" -b ws-rhana
|
|
||||||
|
|
||||||
# обратные ссылки в целевой
|
|
||||||
python tools/index/query_1c.py refs Catalog.Партнеры -b target --limit 20
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ошибки
|
Формат строки реквизита:
|
||||||
|
|
||||||
- **Нет индекса** — подсказка запустить `index_1c.py reindex`.
|
```text
|
||||||
- **`-b unknown`** — список папок в репозитории и алиасы.
|
├── `Attribute.Партнер` — Клиент [обяз.] : CatalogRef.Партнеры
|
||||||
- **`-b crm3-26`, но не проиндексировано** — список того, что есть в БД, и команда переиндексации.
|
├── `Attribute.СуммаДокумента` — … [необяз.] : DefinedType.ДенежнаяСуммаЛюбогоЗнака
|
||||||
- **`show` не нашёл объект** — похожие имена из индекса.
|
├── `TabularSection.Товары` — Товары
|
||||||
|
│ └── `Column.Номенклатура` — … [обяз.] : CatalogRef.Номенклатура
|
||||||
|
```
|
||||||
|
|
||||||
|
- **[обяз.]** — `FillChecking=ShowError`; **[необяз.]** — `DontCheck`
|
||||||
|
- составной тип: несколько вариантов через `|`
|
||||||
|
- при наличии XML структура подтягивается с диска (актуальный `FillChecking`)
|
||||||
|
|
||||||
|
### `refs` — кто ссылается
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/index/query_1c.py refs "ЗаказКлиента"
|
||||||
|
python tools/index/query_1c.py refs Catalog.Партнеры -b target --limit 50
|
||||||
|
python tools/index/query_1c.py refs Document.ЗаказКлиента -b source
|
||||||
|
```
|
||||||
|
|
||||||
|
Без `-b` — все конфигурации в индексе; `--limit` действует **на каждую**.
|
||||||
|
Составной тип поля: `→ DocumentRef.ЗаказКлиента (среди прочего: DocumentRef.…)`.
|
||||||
|
|
||||||
|
### `modules` / `module`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/index/query_1c.py modules "ОбработатьHealth" -b ws-rhana
|
||||||
|
python tools/index/query_1c.py modules "РВС_Transfer" -b ws-rhana -n
|
||||||
|
python tools/index/query_1c.py module CommonModules/РВС_TransferAPIДокументы -b ws-rhana
|
||||||
|
```
|
||||||
|
|
||||||
|
### Прочие примеры
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/index/query_1c.py search "ЗаказКлиента"
|
||||||
|
python tools/index/query_1c.py search "Партнер" --type Document -b crm3-26
|
||||||
|
python tools/index/query_1c.py show Document.ЗаказКлиента -b target
|
||||||
|
python tools/index/query_1c.py search "Партнер" -b source -b crm3-dev.rhana
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки и подсказки
|
||||||
|
|
||||||
|
| Ситуация | Поведение |
|
||||||
|
|----------|-----------|
|
||||||
|
| Нет `index.sqlite` | подсказка `index_1c.py reindex --full` |
|
||||||
|
| `-b` без папки `src/` | список доступных выгрузок + алиасы |
|
||||||
|
| `-b` есть в репо, нет в индексе | список проиндексированных + команда `reindex -b …` |
|
||||||
|
| `show` / `object` не нашли | похожие имена из индекса |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Файл настроек утилиты (`--config`)
|
## Файл настроек утилиты (`--config`)
|
||||||
|
|
||||||
Для нестандартного расположения проекта:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp tools/index/config.example.json tools/index/config.local.json
|
cp tools/index/config.example.json tools/index/config.local.json
|
||||||
# отредактировать project_root
|
# project_root: "/path/to/crm3-26"
|
||||||
python tools/index/query_1c.py --config tools/index/config.local.json search "Заказ"
|
python tools/index/query_1c.py --config tools/index/config.local.json search "Заказ"
|
||||||
```
|
```
|
||||||
|
|
||||||
Приоритет корня: `--root` > `project_root` в JSON > автоопределение от `tools/index/`.
|
`config.local.json` в `.gitignore` (локальные пути).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Применение в Cursor
|
## Применение в Cursor
|
||||||
|
|
||||||
1. После выгрузки из EDT/конфигуратора:
|
1. После выгрузки из EDT/конфигуратора: `python tools/index/index_1c.py reindex -j 12` (или `-b target`).
|
||||||
`python tools/index/index_1c.py reindex -j 12`
|
2. Метаданные и связи — через `query_1c.py`, не Grep по всему `src/`.
|
||||||
или `reindex -b target` если менялась только целевая.
|
3. Разделять контекст: `-b target` vs `-b source`.
|
||||||
2. Агент ищет через `query_1c.py`, не через Grep по `src/`.
|
4. Код: `modules` → открыть `rel_path`; структура документа: `object`.
|
||||||
3. Уточнение контекста: `-b target` / `-b source` чтобы не смешивать старую и новую конфигурацию.
|
5. Правило агента: `.cursor/rules/1c-meta-index.mdc`.
|
||||||
4. Для кода: `modules` → открыть найденный `rel_path` в репозитории.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Артефакты
|
## Артефакты кэша
|
||||||
|
|
||||||
```
|
```text
|
||||||
cache/1c_meta/
|
cache/1c_meta/ # в корне проекта CRM3-26 (gitignore)
|
||||||
index.sqlite # единая БД
|
index.sqlite # единая БД
|
||||||
INDEX.md # сводка последнего reindex
|
INDEX.md
|
||||||
manifest.json
|
manifest.json
|
||||||
md/<baseconf>/ # опционально (--md)
|
md/<baseconf>/ # опционально (--md)
|
||||||
```
|
```
|
||||||
|
|
||||||
Старые per-config `search.sqlite` в подкаталогах не используются.
|
Per-config `search.sqlite` устарели и не используются.
|
||||||
|
|||||||
+13
-2
@@ -1,3 +1,14 @@
|
|||||||
"""Индексация метаданных конфигураций 1С (выгрузки EDT/XML) для быстрого поиска."""
|
"""Индексация метаданных и модулей конфигураций 1С (выгрузки EDT/XML)."""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
from pathlib import Path
|
||||||
|
|
||||||
|
__version__ = "0.2.0"
|
||||||
|
__status__ = "in development"
|
||||||
|
|
||||||
|
|
||||||
|
def read_version_file() -> str:
|
||||||
|
"""Версия из файла VERSION рядом со скриптами (источник истины для релиза)."""
|
||||||
|
path = Path(__file__).resolve().parents[1] / "VERSION"
|
||||||
|
if path.is_file():
|
||||||
|
return path.read_text(encoding="utf-8").strip() or __version__
|
||||||
|
return __version__
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
"""Поиск объекта/реквизита и вывод дерева структуры метаданных."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml
|
||||||
|
|
||||||
|
|
||||||
|
FILL_LABEL = {
|
||||||
|
"ShowError": "обяз.",
|
||||||
|
"DontCheck": "необяз.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(s: str) -> str:
|
||||||
|
return " ".join((s or "").casefold().split())
|
||||||
|
|
||||||
|
|
||||||
|
def _type_label(raw: str) -> str:
|
||||||
|
if raw.startswith("cfg:"):
|
||||||
|
return raw[4:]
|
||||||
|
if raw.startswith("xs:"):
|
||||||
|
return raw[3:]
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_label(fill: str) -> str:
|
||||||
|
if not fill:
|
||||||
|
return "?"
|
||||||
|
return FILL_LABEL.get(fill, fill)
|
||||||
|
|
||||||
|
|
||||||
|
def _field_matches(f: dict[str, Any], q: str, qn: str) -> bool:
|
||||||
|
"""Только точное совпадение имени или синонима реквизита."""
|
||||||
|
name_cf = _norm(f.get("name") or "")
|
||||||
|
syn_cf = _norm(f.get("synonym") or "")
|
||||||
|
# синоним может быть «Клиент | en:Client» — сравниваем по частям
|
||||||
|
syn_parts = [_norm(p) for p in (f.get("synonym") or "").split("|")]
|
||||||
|
return name_cf == qn or syn_cf == qn or qn in syn_parts
|
||||||
|
|
||||||
|
|
||||||
|
def find_structure_matches(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
configs: list[str] | None = None,
|
||||||
|
object_type: str | None = None,
|
||||||
|
limit: int = 20,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Найти объекты по имени/синониму объекта или реквизита.
|
||||||
|
|
||||||
|
Каждый элемент:
|
||||||
|
config, full_name, path, json, match_kind: object|field,
|
||||||
|
matched_fields: list[field dict keys],
|
||||||
|
score: int (меньше — лучше)
|
||||||
|
"""
|
||||||
|
q = query.strip()
|
||||||
|
qn = _norm(q)
|
||||||
|
if not qn:
|
||||||
|
return []
|
||||||
|
|
||||||
|
sql_obj = """
|
||||||
|
SELECT id, config, full_name, name, synonym, path, json, object_type
|
||||||
|
FROM objects
|
||||||
|
WHERE (
|
||||||
|
name = ? OR full_name = ? OR full_name LIKE ?
|
||||||
|
OR synonym LIKE ? OR name LIKE ?
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
params: list[Any] = [q, q, f"%.{q}", f"%{q}%", f"%{q}%"]
|
||||||
|
if configs:
|
||||||
|
sql_obj += f" AND config IN ({','.join('?' * len(configs))})"
|
||||||
|
params.extend(configs)
|
||||||
|
if object_type:
|
||||||
|
sql_obj += " AND object_type = ?"
|
||||||
|
params.append(object_type)
|
||||||
|
sql_obj += " ORDER BY config, full_name LIMIT ?"
|
||||||
|
params.append(max(limit * 5, 50))
|
||||||
|
|
||||||
|
by_key: dict[tuple[str, str], dict[str, Any]] = {}
|
||||||
|
|
||||||
|
def _ensure(row) -> dict[str, Any]:
|
||||||
|
key = (row["config"], row["full_name"])
|
||||||
|
if key not in by_key:
|
||||||
|
oid = row["object_id"] if "object_id" in row.keys() else row["id"]
|
||||||
|
by_key[key] = {
|
||||||
|
"config": row["config"],
|
||||||
|
"full_name": row["full_name"],
|
||||||
|
"path": row["path"],
|
||||||
|
"json": row["json"],
|
||||||
|
"object_type": row["object_type"],
|
||||||
|
"match_kind": "none",
|
||||||
|
"matched_fields": [],
|
||||||
|
"object_id": oid,
|
||||||
|
"score": 100,
|
||||||
|
}
|
||||||
|
return by_key[key]
|
||||||
|
|
||||||
|
for row in conn.execute(sql_obj, params):
|
||||||
|
entry = _ensure(row)
|
||||||
|
name_cf = _norm(row["name"])
|
||||||
|
full_cf = _norm(row["full_name"])
|
||||||
|
syn_cf = _norm(row["synonym"] or "")
|
||||||
|
if name_cf == qn or full_cf == qn or full_cf.endswith("." + qn):
|
||||||
|
entry["match_kind"] = "object"
|
||||||
|
entry["score"] = min(entry["score"], 0)
|
||||||
|
elif syn_cf == qn:
|
||||||
|
entry["match_kind"] = "object"
|
||||||
|
entry["score"] = min(entry["score"], 2)
|
||||||
|
elif qn in name_cf or qn in full_cf:
|
||||||
|
if entry["match_kind"] == "none":
|
||||||
|
entry["match_kind"] = "object"
|
||||||
|
entry["score"] = min(entry["score"], 20)
|
||||||
|
elif qn in syn_cf:
|
||||||
|
if entry["match_kind"] == "none":
|
||||||
|
entry["match_kind"] = "object"
|
||||||
|
entry["score"] = min(entry["score"], 25)
|
||||||
|
|
||||||
|
sql_f = """
|
||||||
|
SELECT o.id AS object_id, o.config, o.full_name, o.path, o.json, o.object_type,
|
||||||
|
o.name AS object_name, o.synonym AS object_synonym,
|
||||||
|
f.kind, f.name, f.synonym, f.types, f.tabular_section, f.refs_json
|
||||||
|
FROM fields f
|
||||||
|
JOIN objects o ON o.id = f.object_id
|
||||||
|
WHERE (
|
||||||
|
f.name = ? OR f.synonym LIKE ? OR f.name LIKE ?
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
params_f: list[Any] = [q, f"%{q}%", f"%{q}%"]
|
||||||
|
if configs:
|
||||||
|
sql_f += f" AND o.config IN ({','.join('?' * len(configs))})"
|
||||||
|
params_f.extend(configs)
|
||||||
|
if object_type:
|
||||||
|
sql_f += " AND o.object_type = ?"
|
||||||
|
params_f.append(object_type)
|
||||||
|
sql_f += " ORDER BY o.config, o.full_name LIMIT ?"
|
||||||
|
params_f.append(max(limit * 15, 100))
|
||||||
|
|
||||||
|
for row in conn.execute(sql_f, params_f):
|
||||||
|
fdict = {
|
||||||
|
"kind": row["kind"],
|
||||||
|
"name": row["name"],
|
||||||
|
"synonym": row["synonym"] or "",
|
||||||
|
"types": [t.strip() for t in (row["types"] or "").split(",") if t.strip()],
|
||||||
|
"tabular_section": row["tabular_section"] or "",
|
||||||
|
"refs": json.loads(row["refs_json"] or "[]"),
|
||||||
|
"fill_checking": "",
|
||||||
|
}
|
||||||
|
if not _field_matches(fdict, q, qn):
|
||||||
|
continue
|
||||||
|
entry = _ensure(row)
|
||||||
|
fname_cf = _norm(fdict["name"])
|
||||||
|
fsyn_cf = _norm(fdict["synonym"])
|
||||||
|
if fname_cf == qn:
|
||||||
|
field_score = 5
|
||||||
|
elif fsyn_cf == qn:
|
||||||
|
field_score = 6
|
||||||
|
else:
|
||||||
|
field_score = 15
|
||||||
|
# если объект уже найден точно — оставляем object, но добавляем поля
|
||||||
|
if entry["score"] > field_score or entry["match_kind"] in {"none", "field"}:
|
||||||
|
if entry["match_kind"] != "object" or entry["score"] > 0:
|
||||||
|
entry["match_kind"] = "field"
|
||||||
|
entry["score"] = min(entry["score"], field_score)
|
||||||
|
elif entry["match_kind"] == "object" and entry["score"] == 0:
|
||||||
|
# точный объект + ещё реквизит с тем же именем (редко) — остаётся object
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
entry["score"] = min(entry["score"], field_score)
|
||||||
|
|
||||||
|
sig = (fdict["tabular_section"], fdict["name"], fdict["kind"])
|
||||||
|
existing = {
|
||||||
|
(x.get("tabular_section"), x.get("name"), x.get("kind"))
|
||||||
|
for x in entry["matched_fields"]
|
||||||
|
}
|
||||||
|
if sig not in existing:
|
||||||
|
entry["matched_fields"].append(fdict)
|
||||||
|
|
||||||
|
# убрать слабые «просто LIKE по имени объекта», если есть точные хиты
|
||||||
|
items = [v for v in by_key.values() if v["match_kind"] != "none"]
|
||||||
|
has_exact = any(v["score"] <= 6 for v in items)
|
||||||
|
if has_exact:
|
||||||
|
items = [v for v in items if v["score"] <= 15]
|
||||||
|
|
||||||
|
items.sort(key=lambda x: (x["score"], x["config"], x["full_name"]))
|
||||||
|
return items[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def load_object_structure(match: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Загрузить структуру: предпочесть свежий разбор XML (FillChecking)."""
|
||||||
|
path_s = match.get("path") or ""
|
||||||
|
path = Path(path_s) if path_s else None
|
||||||
|
config = match["config"]
|
||||||
|
otype = match.get("object_type") or match["full_name"].split(".", 1)[0]
|
||||||
|
|
||||||
|
if path and path.is_file():
|
||||||
|
rec = parse_metadata_xml(path, config=config, object_type=otype)
|
||||||
|
if rec:
|
||||||
|
return rec.to_dict()
|
||||||
|
|
||||||
|
data = json.loads(match["json"])
|
||||||
|
# старые индексы без fill_checking
|
||||||
|
for f in data.get("fields") or []:
|
||||||
|
f.setdefault("fill_checking", "")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def format_types(types: list[str], refs: list[dict[str, Any]] | None = None) -> str:
|
||||||
|
if not types:
|
||||||
|
return "—"
|
||||||
|
labels = [_type_label(t) for t in types]
|
||||||
|
if len(labels) == 1:
|
||||||
|
return labels[0]
|
||||||
|
return " | ".join(labels)
|
||||||
|
|
||||||
|
|
||||||
|
def format_field_line(
|
||||||
|
f: dict[str, Any],
|
||||||
|
*,
|
||||||
|
indent: str = "",
|
||||||
|
last: bool = True,
|
||||||
|
under_ts: bool = False,
|
||||||
|
) -> str:
|
||||||
|
branch = "└── " if last else "├── "
|
||||||
|
name = f.get("name") or ""
|
||||||
|
kind = f.get("kind") or "Attribute"
|
||||||
|
syn = f.get("synonym") or ""
|
||||||
|
syn_s = f" — {syn}" if syn else ""
|
||||||
|
if kind == "TabularSection":
|
||||||
|
return f"{indent}{branch}`{kind}.{name}`{syn_s}"
|
||||||
|
fill = _fill_label(f.get("fill_checking") or "")
|
||||||
|
types_s = format_types(f.get("types") or [], f.get("refs"))
|
||||||
|
if kind == "Column" and not under_ts and f.get("tabular_section"):
|
||||||
|
name = f"{f['tabular_section']}.{name}"
|
||||||
|
return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_object_tree(
|
||||||
|
obj: dict[str, Any],
|
||||||
|
*,
|
||||||
|
clear: bool = False,
|
||||||
|
matched_fields: list[dict[str, Any]] | None = None,
|
||||||
|
match_kind: str = "object",
|
||||||
|
) -> str:
|
||||||
|
"""Дерево реквизитов объекта. clear — только совпадения."""
|
||||||
|
lines: list[str] = []
|
||||||
|
syn = obj.get("synonym") or ""
|
||||||
|
syn_s = f" — {syn}" if syn else ""
|
||||||
|
cfg = obj.get("config") or obj.get("_config") or ""
|
||||||
|
header = f"`{obj.get('full_name') or obj.get('object_type') + '.' + obj.get('name')}`{syn_s}"
|
||||||
|
if cfg:
|
||||||
|
header = f"[{cfg}] {header}"
|
||||||
|
lines.append(header)
|
||||||
|
|
||||||
|
fields: list[dict[str, Any]] = list(obj.get("fields") or [])
|
||||||
|
|
||||||
|
if clear:
|
||||||
|
if match_kind == "field" and matched_fields:
|
||||||
|
want: set[tuple[str, str, str]] = set()
|
||||||
|
need_ts: set[str] = set()
|
||||||
|
for mf in matched_fields:
|
||||||
|
want.add(
|
||||||
|
(
|
||||||
|
mf.get("kind") or "",
|
||||||
|
mf.get("tabular_section") or "",
|
||||||
|
mf.get("name") or "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if mf.get("tabular_section") and mf.get("kind") == "Column":
|
||||||
|
need_ts.add(mf["tabular_section"])
|
||||||
|
selected: list[dict[str, Any]] = []
|
||||||
|
for f in fields:
|
||||||
|
key = (
|
||||||
|
f.get("kind") or "",
|
||||||
|
f.get("tabular_section") or "",
|
||||||
|
f.get("name") or "",
|
||||||
|
)
|
||||||
|
if key in want:
|
||||||
|
selected.append(f)
|
||||||
|
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
|
||||||
|
selected.append(f)
|
||||||
|
if not selected:
|
||||||
|
lines.append(" (совпавшие реквизиты не найдены в структуре)")
|
||||||
|
return "\n".join(lines)
|
||||||
|
lines.extend(_render_field_tree(selected, only_keys=want, need_ts=need_ts))
|
||||||
|
return "\n".join(lines)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
if not fields:
|
||||||
|
lines.append(" (нет реквизитов в индексе)")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
lines.extend(_render_field_tree(fields))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_field_tree(
|
||||||
|
fields: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
only_keys: set[tuple[str, str, str]] | None = None,
|
||||||
|
need_ts: set[str] | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Рендер: сначала шапка объекта уже напечатана; здесь реквизиты и ТЧ."""
|
||||||
|
lines: list[str] = []
|
||||||
|
# группировка: header fields, then TS with columns
|
||||||
|
headers = [
|
||||||
|
f
|
||||||
|
for f in fields
|
||||||
|
if f.get("kind") not in {"TabularSection", "Column"}
|
||||||
|
and not f.get("tabular_section")
|
||||||
|
]
|
||||||
|
tab_sections = [f for f in fields if f.get("kind") == "TabularSection"]
|
||||||
|
columns_by_ts: dict[str, list[dict[str, Any]]] = {}
|
||||||
|
for f in fields:
|
||||||
|
if f.get("kind") == "Column" and f.get("tabular_section"):
|
||||||
|
columns_by_ts.setdefault(f["tabular_section"], []).append(f)
|
||||||
|
|
||||||
|
if only_keys is not None:
|
||||||
|
headers = [
|
||||||
|
f
|
||||||
|
for f in headers
|
||||||
|
if (f.get("kind") or "", f.get("tabular_section") or "", f.get("name") or "")
|
||||||
|
in only_keys
|
||||||
|
]
|
||||||
|
tab_sections = [
|
||||||
|
f
|
||||||
|
for f in tab_sections
|
||||||
|
if f.get("name") in (need_ts or set())
|
||||||
|
or (f.get("kind") or "", "", f.get("name") or "") in only_keys
|
||||||
|
]
|
||||||
|
|
||||||
|
# плоский список узлов верхнего уровня для корректных └──/├──
|
||||||
|
top: list[tuple[str, Any]] = [("h", f) for f in headers] + [("ts", f) for f in tab_sections]
|
||||||
|
for i, (kind, node) in enumerate(top):
|
||||||
|
last = i == len(top) - 1
|
||||||
|
if kind == "h":
|
||||||
|
lines.append(format_field_line(node, indent="", last=last))
|
||||||
|
continue
|
||||||
|
# tabular section
|
||||||
|
lines.append(format_field_line(node, indent="", last=last))
|
||||||
|
ts_name = node.get("name") or ""
|
||||||
|
cols = columns_by_ts.get(ts_name, [])
|
||||||
|
if only_keys is not None:
|
||||||
|
cols = [
|
||||||
|
c
|
||||||
|
for c in cols
|
||||||
|
if (c.get("kind") or "", c.get("tabular_section") or "", c.get("name") or "")
|
||||||
|
in only_keys
|
||||||
|
]
|
||||||
|
child_indent = " " if last else "│ "
|
||||||
|
for j, col in enumerate(cols):
|
||||||
|
lines.append(
|
||||||
|
format_field_line(
|
||||||
|
col, indent=child_indent, last=(j == len(cols) - 1), under_ts=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return lines
|
||||||
@@ -85,6 +85,10 @@ def collect_types(props: ET.Element) -> list[str]:
|
|||||||
raw = (t.text or "").strip()
|
raw = (t.text or "").strip()
|
||||||
if raw:
|
if raw:
|
||||||
types.append(raw)
|
types.append(raw)
|
||||||
|
for t in typ.findall(f".//{V8}TypeSet"):
|
||||||
|
raw = (t.text or "").strip()
|
||||||
|
if raw:
|
||||||
|
types.append(raw)
|
||||||
return types
|
return types
|
||||||
|
|
||||||
|
|
||||||
@@ -113,6 +117,7 @@ class FieldRec:
|
|||||||
types: list[str] = field(default_factory=list)
|
types: list[str] = field(default_factory=list)
|
||||||
refs: list[dict[str, str]] = field(default_factory=list)
|
refs: list[dict[str, str]] = field(default_factory=list)
|
||||||
tabular_section: str = ""
|
tabular_section: str = ""
|
||||||
|
fill_checking: str = "" # ShowError | DontCheck | …
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -173,6 +178,7 @@ def _parse_field_props(props: ET.Element, kind: str, tabular: str = "") -> Field
|
|||||||
types=types,
|
types=types,
|
||||||
refs=parse_ref_targets(types),
|
refs=parse_ref_targets(types),
|
||||||
tabular_section=tabular,
|
tabular_section=tabular,
|
||||||
|
fill_checking=child_text(props, "FillChecking"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+92
-23
@@ -123,14 +123,18 @@ def search_modules(
|
|||||||
*,
|
*,
|
||||||
configs: list[str] | None = None,
|
configs: list[str] | None = None,
|
||||||
limit: int = 30,
|
limit: int = 30,
|
||||||
|
names_only: bool = False,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
fts = _fts_query(query)
|
fts = _fts_query(query)
|
||||||
sql = """
|
snip_col = "'' AS snip" if names_only else "snippet(modules_fts, 4, '[', ']', '…', 16) AS snip"
|
||||||
|
sql = f"""
|
||||||
SELECT m.config, m.rel_path, m.owner, m.module_kind, m.symbols, m.line_count,
|
SELECT m.config, m.rel_path, m.owner, m.module_kind, m.symbols, m.line_count,
|
||||||
snippet(modules_fts, 4, '[', ']', '…', 16) AS snip,
|
o.synonym AS owner_synonym,
|
||||||
|
{snip_col},
|
||||||
bm25(modules_fts, 2.0, 5.0, 2.0, 8.0, 1.0) AS score
|
bm25(modules_fts, 2.0, 5.0, 2.0, 8.0, 1.0) AS score
|
||||||
FROM modules_fts
|
FROM modules_fts
|
||||||
JOIN modules m ON m.id = modules_fts.module_id
|
JOIN modules m ON m.id = modules_fts.module_id
|
||||||
|
LEFT JOIN objects o ON o.config = m.config AND o.full_name = m.owner
|
||||||
WHERE modules_fts MATCH ?
|
WHERE modules_fts MATCH ?
|
||||||
"""
|
"""
|
||||||
params: list[Any] = [fts]
|
params: list[Any] = [fts]
|
||||||
@@ -144,13 +148,15 @@ def search_modules(
|
|||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
like = f"%{query}%"
|
like = f"%{query}%"
|
||||||
sql2 = """
|
sql2 = """
|
||||||
SELECT config, rel_path, owner, module_kind, symbols, line_count, '' AS snip, 0 AS score
|
SELECT m.config, m.rel_path, m.owner, m.module_kind, m.symbols, m.line_count,
|
||||||
FROM modules
|
o.synonym AS owner_synonym, '' AS snip, 0 AS score
|
||||||
WHERE rel_path LIKE ? OR owner LIKE ? OR symbols LIKE ?
|
FROM modules m
|
||||||
|
LEFT JOIN objects o ON o.config = m.config AND o.full_name = m.owner
|
||||||
|
WHERE m.rel_path LIKE ? OR m.owner LIKE ? OR m.symbols LIKE ?
|
||||||
"""
|
"""
|
||||||
params2: list[Any] = [like, like, like]
|
params2: list[Any] = [like, like, like]
|
||||||
if configs:
|
if configs:
|
||||||
sql2 += f" AND config IN ({','.join('?' * len(configs))})"
|
sql2 += f" AND m.config IN ({','.join('?' * len(configs))})"
|
||||||
params2.extend(configs)
|
params2.extend(configs)
|
||||||
sql2 += " LIMIT ?"
|
sql2 += " LIMIT ?"
|
||||||
params2.append(limit)
|
params2.append(limit)
|
||||||
@@ -163,7 +169,13 @@ def find_refs_to(
|
|||||||
*,
|
*,
|
||||||
configs: list[str] | None = None,
|
configs: list[str] | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
) -> list[dict[str, Any]]:
|
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||||
|
"""Обратные ссылки на объект.
|
||||||
|
|
||||||
|
Возвращает (строки, totals_by_config).
|
||||||
|
При поиске по нескольким конфигурациям ``limit`` — максимум **на каждую**
|
||||||
|
конфигурацию (чтобы crm3-26 не вытеснял crm3-dev из-за ORDER BY + LIMIT).
|
||||||
|
"""
|
||||||
kind = ""
|
kind = ""
|
||||||
name = target
|
name = target
|
||||||
if "." in target:
|
if "." in target:
|
||||||
@@ -183,25 +195,82 @@ def find_refs_to(
|
|||||||
}:
|
}:
|
||||||
kind_ref = kind + "Ref"
|
kind_ref = kind + "Ref"
|
||||||
|
|
||||||
|
where = "r.to_name = ?"
|
||||||
|
params: list[Any] = [name]
|
||||||
if kind_ref:
|
if kind_ref:
|
||||||
sql = """
|
where += " AND (r.to_kind = ? OR r.to_kind = ?)"
|
||||||
SELECT config, from_full_name, from_field, to_kind, to_name
|
params.extend([kind_ref, kind])
|
||||||
FROM refs
|
|
||||||
WHERE to_name = ? AND (to_kind = ? OR to_kind = ?)
|
|
||||||
"""
|
|
||||||
params: list[Any] = [name, kind_ref, kind]
|
|
||||||
else:
|
|
||||||
sql = """
|
|
||||||
SELECT config, from_full_name, from_field, to_kind, to_name
|
|
||||||
FROM refs WHERE to_name = ?
|
|
||||||
"""
|
|
||||||
params = [name]
|
|
||||||
if configs:
|
if configs:
|
||||||
sql += f" AND config IN ({','.join('?' * len(configs))})"
|
where += f" AND r.config IN ({','.join('?' * len(configs))})"
|
||||||
params.extend(configs)
|
params.extend(configs)
|
||||||
sql += " ORDER BY config, from_full_name LIMIT ?"
|
|
||||||
params.append(limit)
|
# Итоги по конфигурациям (без LIMIT)
|
||||||
return [dict(r) for r in conn.execute(sql, params)]
|
totals: dict[str, int] = {
|
||||||
|
r["config"]: int(r["n"])
|
||||||
|
for r in conn.execute(
|
||||||
|
f"SELECT r.config, COUNT(*) AS n FROM refs r WHERE {where} GROUP BY r.config",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# limit на каждую конфигурацию — иначе алфавитный ORDER BY отрезает остальные
|
||||||
|
sql = f"""
|
||||||
|
WITH matched AS (
|
||||||
|
SELECT
|
||||||
|
r.config,
|
||||||
|
r.from_full_name,
|
||||||
|
r.from_field,
|
||||||
|
r.to_kind,
|
||||||
|
r.to_name,
|
||||||
|
f.types AS field_types,
|
||||||
|
f.refs_json AS field_refs_json,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY r.config
|
||||||
|
ORDER BY r.from_full_name, r.from_field
|
||||||
|
) AS rn
|
||||||
|
FROM refs r
|
||||||
|
LEFT JOIN objects o
|
||||||
|
ON o.config = r.config AND o.full_name = r.from_full_name
|
||||||
|
LEFT JOIN fields f
|
||||||
|
ON f.object_id = o.id
|
||||||
|
AND (
|
||||||
|
(COALESCE(f.tabular_section, '') = '' AND f.name = r.from_field)
|
||||||
|
OR (f.tabular_section || '.' || f.name = r.from_field)
|
||||||
|
)
|
||||||
|
WHERE {where}
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
FROM matched
|
||||||
|
WHERE rn <= ?
|
||||||
|
ORDER BY config, from_full_name, from_field
|
||||||
|
"""
|
||||||
|
rows = [dict(r) for r in conn.execute(sql, [*params, limit])]
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
types_s = row.get("field_types") or ""
|
||||||
|
refs_raw = row.get("field_refs_json") or "[]"
|
||||||
|
try:
|
||||||
|
refs_list = json.loads(refs_raw) if refs_raw else []
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
refs_list = []
|
||||||
|
type_parts = [t.strip() for t in types_s.split(",") if t.strip()]
|
||||||
|
# составной: несколько cfg:* или несколько ссылок в типе
|
||||||
|
composite = len(refs_list) > 1 or len(type_parts) > 1
|
||||||
|
row["is_composite"] = composite
|
||||||
|
others: list[str] = []
|
||||||
|
target_full = f"{row['to_kind']}.{row['to_name']}"
|
||||||
|
for ref in refs_list:
|
||||||
|
label = f"{ref.get('kind')}.{ref.get('name')}"
|
||||||
|
if label != target_full and label not in others:
|
||||||
|
others.append(label)
|
||||||
|
for t in type_parts:
|
||||||
|
# cfg:DocumentRef.X → DocumentRef.X
|
||||||
|
short = t[4:] if t.startswith("cfg:") else t
|
||||||
|
if short != target_full and short not in others and "Ref." in short:
|
||||||
|
others.append(short)
|
||||||
|
row["other_types"] = others
|
||||||
|
|
||||||
|
return rows, totals
|
||||||
|
|
||||||
|
|
||||||
def get_object(
|
def get_object(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ if str(HERE) not in sys.path:
|
|||||||
sys.path.insert(0, str(HERE))
|
sys.path.insert(0, str(HERE))
|
||||||
|
|
||||||
from index1c.build import build_project_index, default_workers # noqa: E402
|
from index1c.build import build_project_index, default_workers # noqa: E402
|
||||||
|
from index1c import read_version_file # noqa: E402
|
||||||
from index1c.cli import ( # noqa: E402
|
from index1c.cli import ( # noqa: E402
|
||||||
BaseconfError,
|
BaseconfError,
|
||||||
add_baseconf_args,
|
add_baseconf_args,
|
||||||
@@ -175,6 +176,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
"Алиасы baseconf: target→crm3-26, source→crm3-dev"
|
"Алиасы baseconf: target→crm3-26, source→crm3-dev"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
p.add_argument(
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=f"%(prog)s {read_version_file()}",
|
||||||
|
)
|
||||||
add_tool_args(p)
|
add_tool_args(p)
|
||||||
sub = p.add_subparsers(dest="cmd", required=True)
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
|||||||
+163
-8
@@ -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 object ЗаказКлиента -b target
|
||||||
|
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Документы
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ from index1c.cli import ( # noqa: E402
|
|||||||
resolve_project_root,
|
resolve_project_root,
|
||||||
)
|
)
|
||||||
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.search import ( # noqa: E402
|
from index1c.search import ( # noqa: E402
|
||||||
find_refs_to,
|
find_refs_to,
|
||||||
get_module,
|
get_module,
|
||||||
@@ -47,6 +50,11 @@ from index1c.search import ( # noqa: E402
|
|||||||
search_modules,
|
search_modules,
|
||||||
search_objects,
|
search_objects,
|
||||||
)
|
)
|
||||||
|
from index1c.object_view import ( # noqa: E402
|
||||||
|
find_structure_matches,
|
||||||
|
format_object_tree,
|
||||||
|
load_object_structure,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _open_or_exit(root: Path):
|
def _open_or_exit(root: Path):
|
||||||
@@ -95,14 +103,31 @@ def cmd_modules(args: argparse.Namespace) -> int:
|
|||||||
baseconfs = get_baseconfs_for_query(args, conn, root)
|
baseconfs = get_baseconfs_for_query(args, conn, root)
|
||||||
except BaseconfError as e:
|
except BaseconfError as e:
|
||||||
return handle_baseconf_error(e)
|
return handle_baseconf_error(e)
|
||||||
rows = search_modules(conn, args.query, configs=baseconfs, limit=args.limit)
|
names_only = bool(args.names_only)
|
||||||
|
rows = search_modules(
|
||||||
|
conn,
|
||||||
|
args.query,
|
||||||
|
configs=baseconfs,
|
||||||
|
limit=args.limit,
|
||||||
|
names_only=names_only,
|
||||||
|
)
|
||||||
if not rows:
|
if not rows:
|
||||||
print(format_not_found_modules(args.query, baseconfs=baseconfs, conn=conn))
|
print(format_not_found_modules(args.query, baseconfs=baseconfs, conn=conn))
|
||||||
return 2
|
return 2
|
||||||
for r in rows:
|
for r in rows:
|
||||||
print(f"- [{r['config']}] `{r['rel_path']}` ({r['module_kind']}, owner={r['owner']})")
|
syn = r.get("owner_synonym") or ""
|
||||||
if r.get("snip"):
|
if names_only:
|
||||||
print(f" {r['snip']}")
|
# только имя модуля (owner) и синоним объекта-владельца
|
||||||
|
syn_s = f" — {syn}" if syn else ""
|
||||||
|
print(f"- [{r['config']}] `{r['owner']}`{syn_s}")
|
||||||
|
else:
|
||||||
|
syn_s = f", {syn}" if syn else ""
|
||||||
|
print(
|
||||||
|
f"- [{r['config']}] `{r['rel_path']}` "
|
||||||
|
f"({r['module_kind']}, owner={r['owner']}{syn_s})"
|
||||||
|
)
|
||||||
|
if r.get("snip"):
|
||||||
|
print(f" {r['snip']}")
|
||||||
return 0
|
return 0
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -116,15 +141,41 @@ def cmd_refs(args: argparse.Namespace) -> int:
|
|||||||
baseconfs = get_baseconfs_for_query(args, conn, root)
|
baseconfs = get_baseconfs_for_query(args, conn, root)
|
||||||
except BaseconfError as e:
|
except BaseconfError as e:
|
||||||
return handle_baseconf_error(e)
|
return handle_baseconf_error(e)
|
||||||
rows = find_refs_to(conn, args.target, configs=baseconfs, limit=args.limit)
|
rows, totals = find_refs_to(
|
||||||
|
conn, args.target, configs=baseconfs, limit=args.limit
|
||||||
|
)
|
||||||
if not rows:
|
if not rows:
|
||||||
scope = f" в {', '.join(baseconfs)}" if baseconfs else ""
|
scope = f" в {', '.join(baseconfs)}" if baseconfs else ""
|
||||||
print(f"Ссылок на «{args.target}» не найдено{scope}.")
|
print(f"Ссылок на «{args.target}» не найдено{scope}.")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
current_cfg = None
|
||||||
for r in rows:
|
for r in rows:
|
||||||
|
if r["config"] != current_cfg:
|
||||||
|
current_cfg = r["config"]
|
||||||
|
total = totals.get(current_cfg, 0)
|
||||||
|
shown = sum(1 for x in rows if x["config"] == current_cfg)
|
||||||
|
trunc = f", показано {shown}" if shown < total else ""
|
||||||
|
print(f"## [{current_cfg}] — всего ссылок: {total}{trunc}")
|
||||||
|
arrow = f"{r['to_kind']}.{r['to_name']}"
|
||||||
|
if r.get("is_composite"):
|
||||||
|
others = r.get("other_types") or []
|
||||||
|
if others and len(others) <= 4:
|
||||||
|
arrow += f" (среди прочего: {', '.join(others)})"
|
||||||
|
elif others:
|
||||||
|
arrow += f" (среди прочего: {', '.join(others[:3])}, … ещё {len(others) - 3})"
|
||||||
|
else:
|
||||||
|
arrow += " (среди прочего)"
|
||||||
|
print(f"- `{r['from_full_name']}.{r['from_field']}` → {arrow}")
|
||||||
|
|
||||||
|
if len(totals) > 1:
|
||||||
|
grand = sum(totals.values())
|
||||||
|
shown = len(rows)
|
||||||
|
print()
|
||||||
print(
|
print(
|
||||||
f"- [{r['config']}] `{r['from_full_name']}.{r['from_field']}` "
|
f"Итого: {grand} ссылок в {len(totals)} конфигурациях; "
|
||||||
f"→ {r['to_kind']}.{r['to_name']}"
|
f"выведено {shown} (лимит {args.limit} на каждую). "
|
||||||
|
f"См. -b / --limit."
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
finally:
|
finally:
|
||||||
@@ -149,6 +200,76 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_object(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
|
||||||
|
matches = find_structure_matches(
|
||||||
|
conn,
|
||||||
|
args.query,
|
||||||
|
configs=baseconfs,
|
||||||
|
object_type=otype,
|
||||||
|
limit=args.limit,
|
||||||
|
)
|
||||||
|
if not matches:
|
||||||
|
print(format_not_found_object(args.query, baseconfs=baseconfs, conn=conn), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
for i, m in enumerate(matches):
|
||||||
|
if i:
|
||||||
|
print()
|
||||||
|
obj = load_object_structure(m)
|
||||||
|
# подтянуть FillChecking/типы из свежего XML в matched_fields
|
||||||
|
matched = list(m.get("matched_fields") or [])
|
||||||
|
if matched and obj.get("fields"):
|
||||||
|
by_f = {
|
||||||
|
(
|
||||||
|
f.get("tabular_section") or "",
|
||||||
|
f.get("name") or "",
|
||||||
|
f.get("kind") or "",
|
||||||
|
): f
|
||||||
|
for f in obj["fields"]
|
||||||
|
}
|
||||||
|
for mf in matched:
|
||||||
|
key = (
|
||||||
|
mf.get("tabular_section") or "",
|
||||||
|
mf.get("name") or "",
|
||||||
|
mf.get("kind") or "",
|
||||||
|
)
|
||||||
|
src = by_f.get(key)
|
||||||
|
if src:
|
||||||
|
mf["fill_checking"] = src.get("fill_checking") or ""
|
||||||
|
mf["types"] = src.get("types") or mf.get("types")
|
||||||
|
mf["synonym"] = src.get("synonym") or mf.get("synonym")
|
||||||
|
mf["refs"] = src.get("refs") or mf.get("refs")
|
||||||
|
# точное совпадение объекта (score 0) → clear без дерева;
|
||||||
|
# иначе при найденных реквизитах — clear только по ним
|
||||||
|
kind = m.get("match_kind") or "object"
|
||||||
|
if args.clear and matched and m.get("score", 100) > 0:
|
||||||
|
kind = "field"
|
||||||
|
elif args.clear and m.get("score", 100) == 0:
|
||||||
|
kind = "object"
|
||||||
|
print(
|
||||||
|
format_object_tree(
|
||||||
|
obj,
|
||||||
|
clear=bool(args.clear),
|
||||||
|
matched_fields=matched,
|
||||||
|
match_kind=kind,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(matches) > 1:
|
||||||
|
print()
|
||||||
|
print(f"Найдено объектов: {len(matches)}. Уточните -b / --type / --limit.")
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def cmd_module(args: argparse.Namespace) -> int:
|
def cmd_module(args: argparse.Namespace) -> int:
|
||||||
root = resolve_project_root(args)
|
root = resolve_project_root(args)
|
||||||
conn = _open_or_exit(root)
|
conn = _open_or_exit(root)
|
||||||
@@ -177,6 +298,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
"Файл настроек утилиты — --config (-c): см. tools/index/config.example.json"
|
"Файл настроек утилиты — --config (-c): см. tools/index/config.example.json"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
p.add_argument(
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=f"%(prog)s {read_version_file()}",
|
||||||
|
)
|
||||||
add_tool_args(p)
|
add_tool_args(p)
|
||||||
sub = p.add_subparsers(dest="cmd", required=True)
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
@@ -191,12 +317,23 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
m.add_argument("query")
|
m.add_argument("query")
|
||||||
add_baseconf_args(m)
|
add_baseconf_args(m)
|
||||||
m.add_argument("--limit", type=int, default=30)
|
m.add_argument("--limit", type=int, default=30)
|
||||||
|
m.add_argument(
|
||||||
|
"--names-only",
|
||||||
|
"-n",
|
||||||
|
action="store_true",
|
||||||
|
help="Только имена модулей (owner) и синонимы, без пути и фрагментов кода",
|
||||||
|
)
|
||||||
m.set_defaults(func=cmd_modules)
|
m.set_defaults(func=cmd_modules)
|
||||||
|
|
||||||
ref = sub.add_parser("refs", help="Обратный индекс: кто ссылается на объект")
|
ref = sub.add_parser("refs", help="Обратный индекс: кто ссылается на объект")
|
||||||
ref.add_argument("target", help="Catalog.Партнеры | CatalogRef.Партнеры | Партнеры")
|
ref.add_argument("target", help="Catalog.Партнеры | CatalogRef.Партнеры | Партнеры")
|
||||||
add_baseconf_args(ref)
|
add_baseconf_args(ref)
|
||||||
ref.add_argument("--limit", type=int, default=100)
|
ref.add_argument(
|
||||||
|
"--limit",
|
||||||
|
type=int,
|
||||||
|
default=100,
|
||||||
|
help="Макс. строк на каждую конфигурацию 1С (по умолчанию 100)",
|
||||||
|
)
|
||||||
ref.set_defaults(func=cmd_refs)
|
ref.set_defaults(func=cmd_refs)
|
||||||
|
|
||||||
sh = sub.add_parser("show", help="Карточка объекта (JSON: поля, типы, ссылки)")
|
sh = sub.add_parser("show", help="Карточка объекта (JSON: поля, типы, ссылки)")
|
||||||
@@ -204,6 +341,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
add_baseconf_args(sh)
|
add_baseconf_args(sh)
|
||||||
sh.set_defaults(func=cmd_show)
|
sh.set_defaults(func=cmd_show)
|
||||||
|
|
||||||
|
ob = sub.add_parser(
|
||||||
|
"object",
|
||||||
|
help="Структура объекта: дерево реквизитов, обязательность, типы",
|
||||||
|
)
|
||||||
|
ob.add_argument("query", help="Имя/синоним объекта или реквизита")
|
||||||
|
add_baseconf_args(ob)
|
||||||
|
ob.add_argument("--type", help="Ограничить типом: Document, Catalog, …")
|
||||||
|
ob.add_argument("--limit", type=int, default=10, help="Макс. число объектов")
|
||||||
|
ob.add_argument(
|
||||||
|
"--clear",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Краткий вывод: только найденные реквизиты; "
|
||||||
|
"если совпал сам объект — только его имя без дерева"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ob.set_defaults(func=cmd_object)
|
||||||
|
|
||||||
md = sub.add_parser("module", help="Метаданные модуля по пути или owner")
|
md = sub.add_parser("module", help="Метаданные модуля по пути или owner")
|
||||||
md.add_argument("path", help="CommonModules/… или CommonModule.Имя")
|
md.add_argument("path", help="CommonModules/… или CommonModule.Имя")
|
||||||
add_baseconf_args(md)
|
add_baseconf_args(md)
|
||||||
|
|||||||
Reference in New Issue
Block a user