этап 1 · окружение · данные в PostgreSQL · 8 контрольных запросов

Этап 1 — окружение, данные в PostgreSQL, офлайн-тесты

Статус: сделано, перепроверено с чистого листа 24.10.2026. Границы этапа (без ядра, HTTP/MCP, контейнера и вызовов модели) соблюдены — их и не трогали.

Что сделано

  1. Комплект КОРУС распакован, его содержимое лежит прямо в корне репозитория (документы 01–06, ACCESS.md, CLAUDE.md, reference_impl/, demo_stand/, customer_materials/, eval/). Папка с архивом из репозитория убрана.
  2. Первым коммитом записан список того, что не должно попадать в репозиторий: рабочее окружение питона, файлы своей базы PostgreSQL, файл-сокет, журналы, CSV синтетики и посторонние копии DuckDB.
  3. Собрано рабочее окружение: зависимости референсной реализации (duckdb, boto3) плюс psycopg и sqlglot, нужные для работы с PostgreSQL на следующих этапах. Команда одна: bash scripts/setup_env.sh.
  4. Прогнаны офлайн-тесты референса на DuckDB — это точка отсчёта до каких-либо правок кода.
  5. Поднята своя PostgreSQL 16, в неё загружена синтетическая витрина закупок (DDL + 11 файлов данных), и 8 контрольных запросов сверены между DuckDB и PostgreSQL — совпало всё, сверка точная (деньги до копейки, количества до штуки, без округления с допуском).
  6. Добавлен режим самопроверки сверки: он искажает один результат и должен дать «НЕ совпадает» — иначе сверке нельзя доверять. Проверено, что она действительно ловит искажение, включая отдельно оговорённый случай — расхождение ровно в 1 рубль на крупной сумме (~28 млрд).
  7. PostgreSQL после работы остановлена.

Что изменилось с прошлой версии отчёта

Вывод офлайн-тестов референса (DuckDB) — прогон с чистого листа 24.10.2026

Команда:

.venv/bin/python reference_impl/qa/test_handler.py

Итоговая строка реального прогона:

Пройдено 85 из 85

Окружение перед прогоном было пересобрано с нуля (rm -rf .venv && bash scripts/setup_env.sh) — это не повтор старого результата, а свежий прогон на чистом окружении.

Сверка 8 контрольных запросов: PostgreSQL против DuckDB — прогон 24.10.2026

ИНН «Альфа-Снаб Екатеринбург» (используется в запросах 1 и 4), определён запросом по наименованию — 6600370124, одинаковый в обеих базах.

№ Запрос DuckDB PostgreSQL Совпало
1 Сумма закупок по ИНН «Альфа-Снаб Екатеринбург» за 2025: позиций, написаний, сумма без НДС 4858 поз., 3 написания, 1 357 249 732,08 руб. 4858 поз., 3 написания, 1 357 249 732,08 руб. да
2 Сумма закупок БЕ 1100 за 2025: позиций, заказов, сумма без НДС 23 718 поз., 11 778 заказов, 28 076 423 313,02 руб. 23 718 поз., 11 778 заказов, 28 076 423 313,02 руб. да
3 Поставщики класса 001_OIL за 2025: написаний, ИНН, записей без ИНН 31 написание, 18 ИНН, 9 записей без ИНН 31 написание, 18 ИНН, 9 записей без ИНН да
4 Юрлица холдинга «Альфа-Снаб»: уникальных ИНН, записей справочника 6 ИНН, 9 записей 6 ИНН, 9 записей да
5 Дизельное топливо за 2025, тонн: по классу МДМ / по группе материалов 333 421,171 / 338 707,218 333 421,171 / 338 707,218 да
6 Материалы по фрагменту «ШПАЛА»: материалов, групп, классов, без класса 14 / 3 / 3 / 2 14 / 3 / 3 / 2 да
7 Позиции свыше 10 млн руб. за 2026: позиций, БЕ, сумма 1150 поз., 10 БЕ, 34 949 335 628,35 руб. 1150 поз., 10 БЕ, 34 949 335 628,35 руб. да
8 Позиции без класса МДМ, БЕ 7400, 2025: всего, без класса, сумма всего, сумма без класса, доля 15 445 / 4327, 15 015 978 659,21 / 4 190 072 105,89 руб., 28,02 % 15 445 / 4327, 15 015 978 659,21 / 4 190 072 105,89 руб., 28,02 % да

Итоговая строка скрипта сверки: совпадает, код возврата 0.

При переносе запросов на PostgreSQL обойдена известная ловушка из документации комплекта: сумма закупок — поле с плавающей точкой, а округление до копеек на PostgreSQL для такого поля без явного приведения к типу numeric не работает (функция round(double precision, integer) не существует). Во всех запросах округление сделано как ROUND(сумма::numeric, 2).

Самопроверка сверки — в обе стороны (прогон 24.10.2026)

Сверке можно доверять только если она умеет не только подтверждать совпадение, но и ловить расхождение. Проверено явно в обе стороны, оба прогона — свежие, после перезагрузки PostgreSQL.

1. Честный прогон — данные не трогали, ожидается «совпадает»:

.venv/bin/python scripts/compare_pg_duckdb.py

Итоговая строка: «совпадает», код возврата 0. Все 8 запросов — «да» (таблица выше).

2. Прогон с искажением — один результат PostgreSQL намеренно подменён, ожидается «НЕ совпадает»:

.venv/bin/python scripts/compare_pg_duckdb.py --self-check-poison

Искажены два запроса: сумма закупок по ИНН «Альфа-Снаб Екатеринбург» (на 0,01 руб., с 1 357 249 732,08 до 1 357 249 732,09 руб.) и число позиций по БЕ 1100 (на 1 штуку, с 23 718 до 23 719). Итоговая строка: «НЕ совпадает», код возврата 1; оба искажённых запроса в таблице отмечены «НЕТ», остальные шесть — «да» (сверка не «краснит» всё подряд, а точечно находит именно испорченные строки).

Отдельно проверено более крупное и округлое искажение по прямому требованию заказчика — ровно 1 рубль на сумме около 28 млрд (запрос 2: 28 076 423 313,02 → 28 076 423 314,02 руб.) — и оно тоже распознано как «НЕТ»: точность сравнения (Decimal, округление ровно до 2 знаков, равенство без допуска) не теряет расхождение даже на крупной сумме. Подробности и вывод этой отдельной проверки — в reports/archive/etap-4-priemka-chistyi-progon.md, раздел 6.

Вывод: сверка работает в обе стороны — подтверждает совпадение, когда данные верны, и находит расхождение, когда данные испорчены. Если бы самопроверка этого не умела, доверять зелёной галочке «совпадает» на настоящих данных было бы нельзя.

Сколько строк загружено в каждую таблицу

Проверено прямым запросом count(*) к своей PostgreSQL после загрузки — совпадает с числом строк, которое выгрузил export_csv.py из purch_demo.duckdb.

Таблица Строк
dm_purch.order_purch_h2 297 362
dm_purch.request_purch_h2 311 732
dm_purch.mrp_order_h2 288 641
dm_purch.receipt_agg_h2 138 471
dm_purch.mdm_class 1 048
dm_purch.dim_material 3 071
dds_v_purch.dim_contragent_h2 412
dds_v_purch.dim_matgroup_h2 104
dds_v_purch.dim_holding_purch_h2 3
dds_v_purch.dim_comparea_purch_h2 3
dds_v_purch.dim_compcode_sort_ord_h1 10

Роль только на чтение и прогон SQL из кода (дополнительная проверка, прогон 24.10.2026)

Сверх минимума этапа 1 дополнительно поднята роль purch_ro — у неё есть только SELECT на схемы витрины (dm_purch, dds_v_purch, dm_v_purch) и нет прав на запись. Проверено 7 разных попыток записи под этой ролью (INSERT/UPDATE/DELETE/CREATE/DROP/TRUNCATE, в таблицу и во вью) — все 7 отклонены сервером. Под той же ролью выполнены вживую все 17 фиксированных SQL-запросов, встречающихся в коде шлюза (справочники резолвера + все запросы шаблонной справки по контрагенту): 11 выполнились и вернули результат, 6 — нет, все по одной и той же причине — функция округления суммы (ROUND(SUM(...), 2)) в коде справки написана для DuckDB и не существует в PostgreSQL для чисел с плавающей точкой без явного приведения к numeric. Это дефект диалекта в самом коде purch_card.py, а не в загрузке данных или в правах роли (этим же способом доступа справочники и первый запрос справки читаются без проблем); чинить код ядра на этапе 1 не входило в задачу — правка (привести 6 мест к ::numeric) нужна на этапе переноса ядра в свой пакет сервиса, находка зафиксирована, чтобы не потерять её. Подробный протокол, полный список запросов и точный текст ошибок — reports/etap-1-shag2-rol-chteniya.md.

Требования заказчика этапа 1 — по пунктам (протокол приёмки)

№ Требование Проверено Результат
1 Комплект в корне репозитория, .gitignore первым коммитом Содержимое корня репозитория, история коммитов Выполнено
2 Прочитать документы 01–06, ACCESS.md Документы прочитаны Выполнено
3 Окружение: venv + зависимости + psycopg, sqlglot Собрано с нуля, без ошибок Выполнено
4 Офлайн-тесты референса на DuckDB — 85 из 85 reference_impl/qa/test_handler.py, прогон с чистого листа Пройдено 85 из 85
5 PostgreSQL по ACCESS.md, загрузка синтетики по 02 (раздел 5), сверка 8 запросов до копейки и штуки, скриптом scripts/load_postgres.sh, scripts/compare_pg_duckdb.py Все 8 — «да», совпадает, код 0
5а Сверка точная, самопроверка ловит искажения, в т.ч. 1 рубль на ~28 млрд --self-check-poison и отдельная проверка на 1 рубль «НЕ совпадает», код 1; 1 рубль на 28 млрд распознан
6 Остановить PostgreSQL в конце pg_ctl ... stop server stopped

Полный протокол с построчным выводом команд этого прогона — reports/archive/etap-4-priemka-chistyi-progon.md.

Как повторить с нуля

rm -rf .venv
bash scripts/setup_env.sh
.venv/bin/python reference_impl/qa/test_handler.py
bash scripts/load_postgres.sh
.venv/bin/python scripts/compare_pg_duckdb.py
.venv/bin/python scripts/compare_pg_duckdb.py --self-check-poison
.venv/bin/python scripts/check_ro_role.py
PATH=/usr/lib/postgresql/16/bin:$PATH pg_ctl -D .pgdata stop

Что не получилось и почему

На этом этапе (1 из 5) не проверялось, отвечает ли ассистент на реальные вопросы о закупках — это требует вызова модели и запуска ядра сервиса (классификатор, генерация SQL, сводка), а заказчик прямо исключил вызовы модели из объёма этапа 1. Эта проверка относится к этапам 6–7 (регрессия на модели, живая приёмка) и будет сделана там.

Больше ничего из объёма этапа 1 не сорвалось: офлайн-тесты 85 из 85, загрузка и сверка PostgreSQL — все 8 запросов совпали, самопроверка и проверка на 1 рубль/28 млрд подтверждены, PostgreSQL остановлена. Дополнительно (сверх минимума) проверена роль только на чтение — не получилось выполнить без ошибки 6 из 17 запросов кода справки по контрагенту из-за диалектного дефекта округления, не относящегося к этому этапу (подробности выше и в reports/etap-1-shag2-rol-chteniya.md); это находка для этапа переноса ядра, а не незачёт этапа 1.


Страница собрана из файла reports/etap-1.md рабочей копии проекта скриптом scripts/render_report.py.