docs(openspec): aurora-1850-upd-form-cells-subtree — proposal, spec, design
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f0cac30622
commit
feee68d4e5
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-28
|
||||
246
openspec/changes/aurora-1850-upd-form-cells-subtree/design.md
Normal file
246
openspec/changes/aurora-1850-upd-form-cells-subtree/design.md
Normal file
@ -0,0 +1,246 @@
|
||||
# Design: правка ячейки пересчитывает только затронутое поддерево (AURORA-1850, #235)
|
||||
|
||||
Зачем это нужно — в proposal.md, раздел Why. Требования — в specs/sheet-cell-edit/spec.md.
|
||||
|
||||
## Context
|
||||
|
||||
Где мы сейчас: ветка стоит в стеке от `feat/215-aurora-1844-upd-form-cells-full-rows`, миграция 0050.
|
||||
|
||||
- Цепочка вызовов при правке такая. HTTP `PATCH .../cell(s)` или WS `cell_updated` попадает в `SheetRepository.update_cell(s)` (`api/src/repository/sheet_repository.py:70-116`). Оттуда вызывается `v3.upd_form_cell` (`0002_functions.sql:2478`, SQL-обёртка). Далее — `v3.upd_form_cells(..., p_user_id, ...)`: права, `computed_field`, синхронизация сателлитов (`0031_form2_ahr_satellite_sync.sql:904-987`). После неё — `v3.upd_form_cells` без пользователя (`0050_upd_form_cells_full_rows.sql`): проверка строк, `_apply_form_cell`, затем один вызов `v3._form_affected_rows(p_form_id, p_sheet, p_ids, p_direction, p_sections)`.
|
||||
- `_form_affected_rows` (0050) во всех ветках собирает весь лист через `v3.v_form_view` и фильтрует результат. Фильтры по листам:
|
||||
- сателлиты — `data.id ∈ p_ids`;
|
||||
- AHR_LIMIT — путь по `limit_template` (`_limit_template_path_set`);
|
||||
- OTCH9F — то же `(expense_item_id, equipment_name)`;
|
||||
- FORM_4 — ROOT, GROUP по программе и ITEM по проекту;
|
||||
- FORM_1 и FORM_2 AHR/CAP/OPER — агрегаты с `header.expense_item_id ∈ _expense_item_path_set(eid изменённых строк)` плюс INPUT с `line_id ∈ p_ids`;
|
||||
- при `p_sections` ключи исключённых разделов вырезаются (`v_strip`).
|
||||
- Сборка FORM_2 AHR/CAP/OPER идёт по цепочке `v_form_view` → `v_form2_view` (`0033:1-373`, переписана регуляркой в 0037) → `v_form2_sheet_jsonb` (`0015:2534-2804`, LANGUAGE sql, проекция строка в строку) → `v_form2_sheet_sections` (`0033:469-1155`, plpgsql). Внутри `v_form2_sheet_sections`:
|
||||
- `tw` — все узлы дерева статей листа для типа формы;
|
||||
- `page` — все строки формы на листе;
|
||||
- `jp..jq4` и `input_base` — колонки INPUT;
|
||||
- `agg` и `aq_q` — суммы по `expense_item_id` по всем строкам листа;
|
||||
- `tw_agg` и `tw_aq1..4` — суммы узла: `LEFT JOIN agg ON eid = ANY(tw.desc_ids)`, причём `desc_ids` включает сам узел (`0001_initial_schema.py:1063-1065`);
|
||||
- `_sort`: для INPUT это `tree_path || line_id`, для узла — `t.path`.
|
||||
- `sort_order` вычисляется выше по цепочке: `ROW_NUMBER() OVER (ORDER BY _sort_path)` в `v_form2_view` (`0033:21`), для FORM_1 — в ветке `v_form_view` (`0015:1478-1507`). Набор ключей полного листа — все узлы `tw` плюс строки `page ⋈ tw`. Соединение внутреннее, поэтому строка статьи, не привязанной к типу формы, в лист не попадает.
|
||||
- FORM_1 устроена так же: `v_form_view` (ветка AHR/CAP/OPER) → `v_form1_sheet_jsonb` (`0015:2268`) → `v_form1_sheet_sections` (`0033:1723-2470`). Отличие в том, что `page` и все агрегатные CTE фильтруются по `direction` (`bl.direction IS NULL OR = p_direction`), а `tw` — нет.
|
||||
- Сателлитные листы, AHR_LIMIT и OTCH9F собираются прямо в `v_form2_view`. `sort_order` у них такой:
|
||||
- RENT/UTILITY/SECURITY — `ROW_NUMBER` по `id`;
|
||||
- AHR_LIMIT — `limit_template.sort_order`;
|
||||
- OTCH9F — `ROW_NUMBER` по `(expense_item_id, month)`.
|
||||
- `v3.get_form2_ahr_satellite_delta` (0031:784-855, переписана регуляркой в 0037) вызывается в фоне из `notify_ahr_about_satellite_change` (`websocket.py:798-903`). Она собирает `v_form_view(form, 'AHR', NULL, 'Support')` целиком и фильтрует ROOT/GROUP/ITEM/SUB_ITEM по пути, а SUB_SUB_ITEM не берёт. Результат уходит на лист AHR как `cell_updated` или `row_added`.
|
||||
- Где тратится время (`perf/report-data/20261026-s1/explain-manual/summary.md`, `form2_ahr_tw.jit_off`; 2091 строка):
|
||||
- тело `v_form2_sheet_sections` — 240 мс;
|
||||
- основное — 4 × `tw ⋈ aq_q` вложенным циклом (546 узлов × 438 групп, около 100 мс);
|
||||
- соединения `input_base` (около 60 мс);
|
||||
- JIT по умолчанию добавляет ещё 210–230 мс (М12).
|
||||
- Фронт ищет строку ответа по `(row_type, depth, sort_order)` (`cellUtils.js:76`; так же в ветке AURORA-1838). Поэтому неточный `sort_order` молча испортит чужую строку.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Правка FORM_1 и FORM_2 (все редактируемые листы) не собирает лист целиком. Ответ по составу и содержимому байт в байт равен тому, что дала бы полная сборка с фильтром из 0050.
|
||||
- Один источник истины. Суммы, колонки INPUT и `sort_order` считаются тем же кодом, что и GET.
|
||||
- Фоновое обновление АХР после правки сателлита не собирает AHR целиком.
|
||||
|
||||
**Non-Goals:**
|
||||
- FORM_4: остаётся на полной сборке до слияния AURORA-1846.
|
||||
- `add_budget_line` и `del_budget_line`: их ответы по-прежнему строятся полной сборкой.
|
||||
- Ускорение GET листа.
|
||||
- Инкрементальные или материализованные суммы.
|
||||
- Пакетная обработка правок соединения (бывшая часть AURORA-1845).
|
||||
- Кэш пользователя и формы на соединение (AURORA-1845).
|
||||
- Изменения фронта.
|
||||
|
||||
## Решения (согласовано 28.10)
|
||||
|
||||
1. `v3._form_affected_rows` переписывается так, чтобы не собирать весь лист. Существующие построители (`v_form1_sheet_sections`, `v_form2_sheet_sections` и вызывающие их функции) получают параметр фокуса по строкам. NULL означает прежнее поведение. Параллельной копии нет. `sort_order` в ответе точный, то есть глобальный `ROW_NUMBER`.
|
||||
2. Охват — только FORM_1 и FORM_2: AHR/CAP/OPER, сателлиты, AHR_LIMIT и OTCH9F (способ — по дизайну ниже). FORM_4 остаётся на полной сборке до AURORA-1846 (PR #232), критерий для FORM_4 переносится.
|
||||
3. `get_form2_ahr_satellite_delta` переводится на тот же механизм. Тело берётся с мигрированной БД, с учётом 0037.
|
||||
4. Корректность доказывает тест эквивалентности на базе `sheet_contract.py`: каждая строка ответа PATCH побайтно равна строке того же `sort_order` из полного GET. Покрываются FORM_1 и FORM_2 (все листы), первая и последняя строка, очистка, PATCH `/cells` по разным ветвям, `sort_order` после POST и DELETE line.
|
||||
5. Критерий: mean `upd_form_cell` ≤ 50 мс на FORM_2/AHR и 0 вложенных полных сборок листа на правку. Вторая точка снимается после grow (2092 строки).
|
||||
6. AURORA-1845 сужена до кэша на соединение. После 1850 ручной ввод не должен создавать очередь — это проверяется замером coauthor на rate 5.
|
||||
7. Номер миграции — 0051, в стеке после 0050 из 1844. При rebase на 1831 и 1846 номер меняется.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Параметр фокуса и его семантика
|
||||
У `v_form2_sheet_sections`, `v_form2_sheet_jsonb`, `v_form2_view`, `v_form1_sheet_sections` и `v_form1_sheet_jsonb` последним параметром добавляется `p_focus_lines integer[] DEFAULT NULL`. В новой `v_form1_view` (D3) он тоже есть.
|
||||
- `NULL` — полный лист, поведение прежнее.
|
||||
- Непустой массив — только строки фокуса и узлы их путей.
|
||||
- Пустой массив `'{}'` — **пустой результат**, ни в коем случае не полный лист. `array_agg` по пустому набору даёт NULL, поэтому вызывающие обязаны передавать `COALESCE(..., '{}')`. Это проверяется тестом (раздел «Тестирование», п. 6).
|
||||
|
||||
Смысл фокуса для листов AHR/CAP/OPER:
|
||||
- **Узлы фокуса** — `_expense_item_path_set(eid строк фокуса)` ∩ `tw`. eid берутся из `budget_line` **без** фильтра `direction`, как в 0050: правка строки чужого направления в FORM_1 возвращает предков, но не саму строку.
|
||||
- **INPUT фокуса** — строки фокуса, прошедшие обычный фильтр `page` (форма, лист, `direction`, соединение с `tw`).
|
||||
- **Суммы.** `agg` и `aq_q` (у FORM_1 — `agg`, `aq1..aq4`, `atot`) ограничиваются строками с `expense_item_id ∈ ∪ desc_ids(узлы фокуса)`. Сумма узла определяется только его `desc_ids`, которые входят в это объединение, поэтому значение не меняется. `tw_agg` и `tw_aq*` строятся только по узлам фокуса.
|
||||
- `jp..jq4`, `input_base` и `final_input` строятся только по INPUT фокуса.
|
||||
|
||||
Отвергнутые варианты:
|
||||
- (а) Отдельная «лёгкая» функция сумм, копия формул. Отвергнута решением человека: два источника истины.
|
||||
- (б) Фильтр поверх полного `v_form_view`. Это текущее состояние: plpgsql-функция не принимает `WHERE` внутрь.
|
||||
- (в) Ветвление `IF p_focus_lines IS NULL THEN <запрос> ELSE <копия запроса>` внутри построителя. Это та же копия на 700 строк.
|
||||
|
||||
### D2. sort_order считает построитель, по «скелету»
|
||||
Построители `*_sheet_sections` и `*_sheet_jsonb` получают выходную колонку `_sort_rank bigint`. Её значение — `ROW_NUMBER() OVER (ORDER BY _sort, <tie-break D6>)` по **скелету** листа. Скелет — ключи всех узлов `tw` и всех строк `page ⋈ tw` без jsonb и без сумм: ровно тот набор ключей, который даёт полный лист. Ранг считается одинаково в обоих режимах, и строки вывода получают его соединением по ключу.
|
||||
|
||||
`v_form2_view` и `v_form1_view` берут `sort_order` из `_sort_rank` и больше не считают собственный `ROW_NUMBER`.
|
||||
|
||||
Скелет на 2092 строки стоит единицы миллисекунд: сортировка около 2,7 тыс. `int[]`. Это лишняя работа и для GET, но зато порядок считается одним кодом в обоих режимах.
|
||||
|
||||
Отвергнутые варианты:
|
||||
- (а) `ROW_NUMBER` в вызывающих функциях, как сейчас. В режиме фокуса он нумерует подмножество.
|
||||
- (б) Отдельный запрос `count(*) ключей < k` в `_form_affected_rows`. Он дублирует правила набора ключей (`direction`, соединение `page ⋈ tw`).
|
||||
|
||||
### D3. FORM_1: новая `v3.v_form1_view`
|
||||
Ветка FORM_1 AHR/CAP/OPER в `v_form_view` (`0015:1478-1507`, `jsonb_build_object` из 16 ключей) выносится в `v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, p_focus_lines)`. Она симметрична `v_form2_view`. `v_form_view` вызывает её с фокусом NULL, `_form_affected_rows` — с `p_ids`. Сигнатура `v_form_view` не меняется (`CREATE OR REPLACE`).
|
||||
|
||||
Отвергнутый вариант: `_form_affected_rows` сама собирает jsonb поверх `v_form1_sheet_jsonb`. Тогда у проекции строки было бы два места.
|
||||
|
||||
### D4. Сателлиты, AHR_LIMIT, OTCH9F — внутри `v_form2_view` с тем же параметром
|
||||
- **AHR_RENT, AHR_UTILITY, AHR_SECURITY.** Ранг — `ROW_NUMBER() OVER (ORDER BY id)` по всем строкам формы на этом листе: только `id`, без соединения с `vsp` и без jsonb. Затем фильтр `id = ANY(p_focus_lines)`, и jsonb строится только для отобранных строк. Здесь `p_focus_lines` — id деталей (как `p_ids` в 0050).
|
||||
- **AHR_LIMIT.** Вывод фильтруется по `_limit_template_path_set(p_focus_lines)`. `sort_order` = `lt.sort_order` — статичный, от фокуса не зависит. Агрегат `tree`/`leaf_amt`/`agg` остаётся по всему шаблону: шаблон общий и его размер не зависит от числа строк формы. Сужать его можно, но не обязательно (на усмотрение исполнителя). Формула 0037 (`v3._form2_ahr_limit_amount`) сохраняется.
|
||||
- **OTCH9F.** `base` ограничивается разделами `(expense_item_id, equipment_name)` строк фокуса. Окна разделяются по `(budget_form_id, expense_item_id, equipment_name)`, поэтому разделы целые и остатки те же. Ранг — `ROW_NUMBER() OVER (ORDER BY expense_item_id, month, id)` по всем строкам формы (D6) на лёгком скелете с тем же `JOIN expense_item`.
|
||||
- **SMETA и прочие листы** — без изменений: фокус игнорируется, а `_form_affected_rows` для них по-прежнему зовёт полный путь (такие листы не редактируются).
|
||||
|
||||
### D5. `_form_affected_rows` и `get_form2_ahr_satellite_delta`
|
||||
- `_form_affected_rows`, ветки FORM_1 и FORM_2:
|
||||
- AHR/CAP/OPER → `v_form1_view` или `v_form2_view` с `p_focus_lines = p_ids`;
|
||||
- сателлиты, AHR_LIMIT, OTCH9F → `v_form2_view` с фокусом.
|
||||
|
||||
Правила состава ответа (0050) при этом выполняет сам построитель. `v_strip` и `ORDER BY sort_order` остаются. Ветка FORM_4 без изменений. Функция помечается `STABLE`: только чтение, а её вызывает STABLE-функция дельты.
|
||||
- `get_form2_ahr_satellite_delta`: CTE `target_lines` сохраняется **в том виде, как он стоит в БД после 0037**, вместе с сопоставлением `(ei.depth = 3 AND ei.item_id = …) OR ei.num_group_id = …`. Финальный `SELECT` заменяется на `SELECT * FROM v3._form_affected_rows(p_form_id, 'AHR', COALESCE((SELECT array_agg(line_id) FROM target_lines), '{}'), 'Support', NULL)`. CTE `target_path` больше не нужен. Сигнатура и `LANGUAGE sql STABLE` прежние, Python не меняется.
|
||||
- Изменение поведения дельты: теперь в неё входят предки уровня SUB_SUB_ITEM, а раньше фильтр их пропускал (0031:843). После 0033 у AHR есть статьи глубины 4, и их агрегат у соавторов оставался старым. Это исправление по контракту «все предки».
|
||||
|
||||
### D6. Детерминированный порядок при совпадении ключа
|
||||
Ключ INPUT `tree_path || line_id` совпадает с `path` дочернего узла, если `line_id` равен id дочерней статьи, а строка привязана к нелистовой статье. На AHR такое бывает: после 0033 у статей глубины 3 есть дети глубины 4, и строки синхронизации сателлитов привязаны к статьям глубины 3. Сейчас порядок при таком совпадении зависит от плана, а скелет и полный вывод могли бы упорядочить строки по-разному.
|
||||
|
||||
Добавляется tie-break `ORDER BY _sort, line_id NULLS FIRST`: при совпадении узел идёт раньше строки. Для OTCH9F добавляется `id` после `(expense_item_id, month)`. GET меняется только там, где порядок раньше был не определён.
|
||||
|
||||
### D7. JIT выключается в построителях и на пути правки
|
||||
`SET jit = off` ставится как атрибут функций `_form_affected_rows`, `get_form2_ahr_satellite_delta`, `v_form2_sheet_sections` и `v_form1_sheet_sections`. Причины две.
|
||||
1. Запрос с фокусом в custom-плане может оценить стоимость выше `jit_above_cost` и потратить 200+ мс на компиляцию. Это больше всего бюджета в 50 мс.
|
||||
2. План кэшируется один на оператор, общий для GET и правок в одном backend. Дешёвые custom-планы правок снижают среднюю стоимость custom-плана, и после этого GET может перестать переходить на generic-план. Тогда GET будет каждый раз платить JIT (+210–230 мс, М12).
|
||||
|
||||
По М12 JIT на этих запросах только вредит: 534 мс против 240 мс. Атрибут `SET` не мешает: plpgsql не инлайнится.
|
||||
|
||||
Отвергнутые варианты:
|
||||
- (а) `jit = off` только на пути правки. Не закрывает причину 2.
|
||||
- (б) `plan_cache_mode = force_custom_plan`. Добавляет 7–8 мс планирования на каждую правку и усиливает причину 2.
|
||||
- (в) Глобальная настройка Postgres. Вне репозитория, прод не наш.
|
||||
|
||||
### D8. Тела функций в миграции — буквальные, снятые с мигрированной БД
|
||||
Тела `v_form2_view` и `get_form2_ahr_satellite_delta` берутся из `pg_get_functiondef` на БД, мигрированной до 0050, и правятся вручную. Регулярные выражения поверх `pg_get_functiondef`, как в 0037, не используются.
|
||||
|
||||
Отвергнутый вариант — DO-блок с `regexp_replace`. Правка затрагивает десятки мест, а регулярка в 0037 уже показала хрупкость (ветка `CONTINUE`).
|
||||
|
||||
Защита от отката 0037 — тесты из раздела «Тестирование», п. 7.
|
||||
|
||||
### D9. Порядок операторов в миграции 0051
|
||||
Старые сигнатуры удаляются явно, **без CASCADE**. Если обнаружится скрытая зависимость, миграция упадёт, а не удалит лишнее. Порядок:
|
||||
1. `DROP FUNCTION v3.v_form2_view(integer, character varying, text[])`, затем `v3.v_form2_sheet_jsonb(integer, character varying, text[])`, затем `v3.v_form2_sheet_sections(integer, character varying, text[])`.
|
||||
2. Затем `v3.v_form1_sheet_jsonb(integer, character varying, character varying, text[])` и `v3.v_form1_sheet_sections(integer, character varying, character varying, text[])`.
|
||||
3. `CREATE` в порядке «вызываемая раньше вызывающей», потому что тела LANGUAGE sql проверяются при создании:
|
||||
- `*_sheet_sections`;
|
||||
- `*_sheet_jsonb`;
|
||||
- `v_form2_view`, `v_form1_view`;
|
||||
- `v_form_view` (`CREATE OR REPLACE`);
|
||||
- `_form_affected_rows` (`CREATE OR REPLACE`);
|
||||
- `get_form2_ahr_satellite_delta` (`CREATE OR REPLACE`).
|
||||
|
||||
Без DROP функции с DEFAULT-параметрами дали бы неоднозначные перегрузки: вызов `v_form2_view(a, b, c)` из `v_form_view` подходит к обеим.
|
||||
|
||||
Ограничения формата:
|
||||
- `api/alembic/sql_migration.py` пропускает строки, начинающиеся с `--`, и ищет `$tag$` построчно. Открывающий и закрывающий тег должны стоять на разных строках, закрывающий — в начале строки, как в существующих файлах.
|
||||
- SQL совместим с PostgreSQL 15.12: без `any_value`, у подзапросов в FROM есть алиасы.
|
||||
|
||||
Файлы: `api/alembic/versions/0051_upd_form_cells_subtree.py` (`down_revision = "0050"`, `run_sql_file_migration`) и `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql`. `downgrade()` пустой, как во всех миграциях.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Суммы предков в режиме фокуса разойдутся с полной сборкой.** Защита: параметризация вместо копии (D1); побайтная эквивалентность на всех строках тестовых листов («Тестирование», п. 1–2); та же проверка на данных стенда («Замер», шаг 4).
|
||||
- **`sort_order` разойдётся с полным листом, и фронт молча испортит соседнюю строку.** Защита: ранг по скелету одним кодом (D2), tie-break (D6), тесты после POST и DELETE line и тест на совпадение ключа.
|
||||
- **Пустой фокус превратится в полный лист** (NULL из `array_agg`). Последствия: дельта АХР отдаст весь лист, правка снова станет дорогой. Защита: семантика `'{}'` (D1), `COALESCE` в вызывающих, тест п. 6.
|
||||
- **Откатится правка 0037** (формула лимитов, сопоставление статей глубины 4). Защита: D8 и тесты п. 7.
|
||||
- **Регресс GET** из-за общего кэша планов, лишнего скелета или выключенного JIT. Защита: D7; требование «GET листа не меняется»; замер GET до и после со смешанной нагрузкой («Замер», шаг 5).
|
||||
- **Generic-план для запроса с фокусом окажется плохим.** Фильтры фокуса стоят в базовых CTE (`tw`, `page`, `agg`/`aq_q`), которые материализуются, поэтому объём ниже по плану мал при любом плане. Если замер покажет обратное, следующий шаг — `SET plan_cache_mode = force_custom_plan` только на `_form_affected_rows`, с повторным замером.
|
||||
- **Сумма ROOT растёт с размером листа**: агрегат предка суммирует все строки под ним. Числовой `SUM` без jsonb на 2092 строках, по оценке, укладывается в 10–20 мс, а вложенные циклы `tw ⋈ aq_q` (около 100 мс) сужаются до 4–5 узлов. Если вторая точка (2092 строки) превысит 50 мс, исполнитель останавливается и эскалирует с профилем EXPLAIN: инкрементальные суммы — это уже другая задача.
|
||||
- **Права, аудит, синхронизация сателлитов** не затронуты: это слой выше `_form_affected_rows`.
|
||||
- **Зависимости.** AURORA-1844 — шов и контракт, это база ветки. AURORA-1846 нужна только для будущего фокуса FORM_4. Номер миграции может конфликтовать с 1831 и 1846.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
- PR идёт в ветку `feat/215-aurora-1844-upd-form-cells-full-rows` (стек, Gitea #235). После слияния 1844 в `test` ветка перебазируется на `test`.
|
||||
- **Перенумерация.** Если до слияния стека в `test` попадут 1831 (`0050_project_technical_number_lock`) или 1846 (`0050_form4_sheet_sum`), 1844 получит следующий свободный номер, а 1850 — номер после него. При rebase меняются имена файлов, `revision`, `down_revision` и ссылка на SQL-файл в `.py`. Содержимое миграции от номера не зависит: 1846 меняет только `v_form4_sheet_jsonb`, 1831 — `add_project`, пересечений по функциям нет.
|
||||
- Откат: `downgrade()` пустой. Откат — это новая миграция, которая восстанавливает тела из 0050 и 0037 (снять их через `pg_get_functiondef` до применения).
|
||||
- `ARCHITECTURE.md`: в абзац о `_form_affected_rows` добавить, что FORM_1 и FORM_2 собираются построителями с фокусом (`p_focus_lines`), `sort_order` берётся из скелета листа, FORM_4 — полная сборка до AURORA-1846.
|
||||
|
||||
## Тестирование
|
||||
|
||||
Тесты интеграционные: `api/tests/integration/`, новый файл рядом с `test_upd_form_cells_full_rows.py`. Существующие тесты 1844 остаются и должны проходить. Тестовая БД — PG16 (`perf-pg`, отдельная БД `dfip_test`, см. черновик P3 §0). Контроллер проверяет также на 15.12. Время в тестах не проверяется.
|
||||
|
||||
1. **Побайтная эквивалентность на уровне SQL** (новый хелпер в `sheet_contract.py`).
|
||||
- Для набора `p_ids` сравниваются `SELECT row_type, depth, sort_order, data::text FROM v3._form_affected_rows(...)` и эталон.
|
||||
- Эталон — фильтр 0050, применённый к полному `v3.v_form_view` с теми же параметрами: агрегаты по пути плюс INPUT по `line_id`; для сателлитов — по `id`, для AHR_LIMIT — по пути шаблона, для OTCH9F — по разделу. Эталон пишется в коде теста, в БД он не попадает.
|
||||
- Сравнение `data::text` побайтное: jsonb-текст канонический, поэтому видна и разница в масштабе numeric (`30` и `30.00`).
|
||||
- Проверка **на каждой строке INPUT** тестового листа по очереди, плюс пакеты из строк разных ветвей.
|
||||
- Покрытие: FORM_1 AHR (Support и Development), CAP (Support), OPER; FORM_2 AHR, CAP, OPER, AHR_LIMIT, AHR_RENT, AHR_UTILITY, AHR_SECURITY, OTCH9F. Строки OTCH9F вставляются через `db_exec` в `fixed_asset_report` (HTTP-ручки нет), минимум два месяца одного оборудования и второе оборудование той же статьи в том же месяце — так проверяется tie-break.
|
||||
- Тестовые листы содержат строки в нескольких ветвях дерева, чтобы были первая и последняя строки листа и предки без соседей.
|
||||
2. **Эквивалентность через HTTP** (`assert_patch_rows_match_sheet`): PATCH `/cell` и `/cells` на тех же листах, включая очистку (`null`), правку нечисловой колонки и `sections`.
|
||||
3. **`sort_order` после POST и DELETE line**: добавить несколько строк, удалить строку в середине, править строку ниже неё; ответ сравнить с GET.
|
||||
4. **Совпадение ключа сортировки**: в откатываемой сессии вставить `budget_line` с явным `id`, равным id дочерней статьи той статьи, к которой привязана строка. Последовательность при этом не трогается. GET дважды даёт один и тот же порядок, узел идёт перед строкой, ответ на правку совпадает с GET.
|
||||
5. **Дельта АХР**: после правки AHR_RENT (форма 4 фикстуры) результат `SELECT * FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', id)` (через `db_exec` в той же сессии) совпадает со строками GET AHR того же `sort_order`. В нём есть синхронизированная строка INPUT и предки всех уровней, включая SUB_SUB_ITEM, если они есть на пути.
|
||||
6. **Пустой фокус**: `get_form2_ahr_satellite_delta` для несуществующего `source_row_id` возвращает 0 строк. Вызов построителя с `'{}'` тоже даёт 0 строк.
|
||||
7. **Защита правки 0037**:
|
||||
- GET AHR_LIMIT для строки LEAF «- принтеры» (`limit_template.id = 13`, код `204620030`, засеяна в `0013_form2_limit.sql:1707`) после правки `qty_q1` даёт `amount_q1 = qty_q1 × 0.57 × 10900 / 4`;
|
||||
- если в справочнике есть `limit_template` с кодом, равным `num_group_id` статьи AHR глубины 4, правка такой строки AHR_LIMIT даёт непустую дельту АХР; если такого шаблона нет, тест делает `skip` с причиной;
|
||||
- отдельный тест проверяет, что `pg_get_functiondef` новых `v_form2_view` и `get_form2_ahr_satellite_delta` содержит `_form2_ahr_limit_amount` и `ei.num_group_id = lt.expense_item_code` без `ei.depth = 3 AND (`.
|
||||
8. **Атомарность пакета** — существующий `test_bad_column_in_batch_fails_and_applies_nothing` остаётся.
|
||||
|
||||
## Замер до/после
|
||||
|
||||
Замер выполняет контроллер после реализации, не в ночной прогон. Код стенда `perf/` не меняется. Сценарий — шаг `coauthor` (`perf/e2e/scenarios/scroll-coauthor.spec.ts` и бот `perf/perfkit/coauthor.py`), цель `FORM_2/AHR/*`, rate 1 и 5, масштаб 1, CPU 1x. Метрики берутся из `sql/coauthor/sql_top.json`, `summary.md` и строк `db_time` в `perf/.run/api.log`.
|
||||
|
||||
1. **«До», 1154 строки.** Код ветки 1844, `stand.sh reset`, `perfkit.gen.run --scale 1`, `perfkit.gen.verify --scale 1` без grow, затем coauthor (около 7 мин). Ожидаемое значение — около 310 мс на `upd_form_cell` (одна сборка).
|
||||
2. **Снимок GET «до».** Для всех листов FORM_1 и FORM_2 из манифеста сохранить JSON ответа `GET /form/{id}/sheet/{sheet}` (с `direction`, где он есть).
|
||||
3. **Переход на ветку 1850.** `stand.sh migrate`, перезапуск api, снова снимок GET. Ответы должны попарно совпасть (требование «GET листа не меняется»). Различие допустимо только в порядке строк с совпадающим ключом, и такие строки нужно перечислить.
|
||||
4. **Эквивалентность на данных стенда.** psql, только чтение. Для 50 случайных строк FORM_2/AHR и 20 строк каждого из листов CAP, OPER, AHR_RENT и FORM_1/AHR сравнить `v3._form_affected_rows(form, sheet, ARRAY[id], dir, NULL)` с эталонным фильтром по `v3.v_form_view` (тот же запрос, что в тесте п. 1). Расхождений должно быть 0.
|
||||
5. **GET без регресса.** В одном psql-соединении сделать 5 прогревочных вызовов, затем 20 раз чередовать `SELECT count(*) FROM v3.v_form_view(<FORM_2 rf>, 'AHR', NULL, NULL)` и `SELECT count(*) FROM v3.upd_form_cell(...)` в транзакции с ROLLBACK, измерить по `\timing` или `pg_stat_statements`. Средний GET не больше значения на коде 1844 + 10 % (то же измерение на шаге 1).
|
||||
6. **«После», 1154 строки.** coauthor, rate 1 и 5. Критерии:
|
||||
- `mean_exec_time` вызова `upd_form_cell` ≤ 50 мс;
|
||||
- вызовов `v3.v_form_view` с `toplevel = false` — 0;
|
||||
- `calls` у `v3.v_form2_view(p_form_id, p_sheet, p_sections)` равно числу GET листа, а не числу правок;
|
||||
- на rate 5 `(coauthor_echoed + coauthor_errors) / coauthor_sent ≥ 0.95` — очереди нет.
|
||||
7. **«После», 2092 строки.** `perfkit.gen.grow --scale 1` (около 4 мин), затем coauthor. Критерий: `mean_exec_time` ≤ 50 мс и тот же критерий очереди.
|
||||
|
||||
Отчёт: таблица «до» и «после» по шагам 1, 6, 7 с путями к `sql_top.json` и `summary.md`, результаты шагов 3–5. Общее время — около 45 мин. Точка «до» на 2092 строках не снимается (см. решения без обсуждения).
|
||||
|
||||
## Оставлено исполнителю
|
||||
|
||||
- Конкретная перестройка CTE внутри построителей: разделение `tw`/`tw_focus`, `page`/`page_focus`, размещение скелета. Ранг через `ROW_NUMBER` по скелету или эквивалентный способ — главное, одним выражением для обоих режимов.
|
||||
- Вычисление узлов фокуса и `∪ desc_ids` в `DECLARE`/`SELECT INTO` перед `RETURN QUERY`, если это даёт лучший план.
|
||||
- Сужать ли агрегат шаблона AHR_LIMIT.
|
||||
- Имена тестовых хелперов и раскладка тестов по файлам.
|
||||
- Текст абзаца в `ARCHITECTURE.md`.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|
|
||||
| 2026-10-28 | Capability `sheet-cell-edit`. В спецификацию как ADDED вошёл и контракт ответа 1844 (полные строки, явные `null`, `sections`, атомарность пакета) | `openspec/specs/` пуст; без контракта требование о `sort_order` и эквивалентности не к чему привязать | Переименование capability при архивации; пересечение с change 1845 или 1848, если там заведут ту же область |
|
||||
| 2026-10-28 | Параметр `p_focus_lines integer[] DEFAULT NULL` последним во всех построителях; `NULL` — полный лист, `'{}'` — пустой результат | Совместимость существующих вызовов по позиции; явное различие «весь лист» и «ничего» | Если вызывающая сторона передаст NULL вместо `'{}'`, будет полная сборка, правильная по содержимому, но медленная; ловит тест п. 6 |
|
||||
| 2026-10-28 | `sort_order` считает построитель по скелету листа в обоих режимах (новая колонка `_sort_rank`); view перестают считать собственный `ROW_NUMBER` | Один источник порядка для GET и правки, требование точного `sort_order` | +1–3 мс к GET на скелет; при ошибке в скелете меняется и GET, что ловит снимок GET на стенде |
|
||||
| 2026-10-28 | Tie-break `ORDER BY _sort, line_id NULLS FIRST` (узел раньше строки); для OTCH9F — `id` после `(expense_item_id, month)` | Порядок при совпадении ключа сейчас не определён; без tie-break скелет и вывод могут разойтись | Порядок строк в GET меняется только в таких случаях, возможно непривычно для пользователя |
|
||||
| 2026-10-28 | Новая `v3.v_form1_view`: в неё вынесена ветка FORM_1 AHR/CAP/OPER из `v_form_view` | Одна проекция строки FORM_1 для GET и правки | Ещё одна функция в схеме; при ошибке переноса меняется GET FORM_1, что ловят снимок GET и тесты 1844 |
|
||||
| 2026-10-28 | Узлы фокуса берутся из `budget_line` без фильтра `direction`, а INPUT — с фильтром | Так ведёт себя 0050: предки по всем строкам правки, INPUT только видимые в листе | Если правильнее было не возвращать предков для строки чужого направления — лишние, но верные строки в ответе |
|
||||
| 2026-10-28 | Сателлиты: фильтр по `id` и `ROW_NUMBER` по лёгкому скелету; AHR_LIMIT: фильтр вывода по пути шаблона, агрегат шаблона может остаться полным; OTCH9F: `base` по разделам изменённых строк | Размер шаблона AHR_LIMIT не зависит от числа строк формы; окна OTCH9F замкнуты в разделе | AHR_LIMIT остаётся O(размер шаблона) на правку, сейчас это доли мс |
|
||||
| 2026-10-28 | Дельта АХР теперь включает предков уровня SUB_SUB_ITEM | Контракт «все предки»; после 0033 у AHR есть статьи глубины 4, их агрегат у соавторов устаревал | Если фронт АХР не ожидает таких строк, получит лишние строки; `updateCells` сливает по ключу, так что это безопасно |
|
||||
| 2026-10-28 | `SET jit = off` на `_form_affected_rows`, `get_form2_ahr_satellite_delta`, `v_form2_sheet_sections` и `v_form1_sheet_sections` (затрагивает и GET) | JIT съедает бюджет 50 мс и через общий кэш планов может начать срабатывать на каждом GET; по М12 JIT на этих запросах только вредит | Если на проде JIT на GET полезен, GET станет медленнее; ловит шаг 5 замера |
|
||||
| 2026-10-28 | `_form_affected_rows` помечается `STABLE` | Функция только читает, вызывается из STABLE-функции дельты | STABLE-функция видит снимок вызывающего оператора. Сейчас её зовёт отдельный оператор `RETURN QUERY` после `_apply_form_cell`, и правки видны. Если когда-нибудь её вызовут в одном операторе с правкой, ответ будет со старыми значениями; это ловят тесты эквивалентности |
|
||||
| 2026-10-28 | Тела `v_form2_view` и `get_form2_ahr_satellite_delta` в 0051 — буквальные, снятые через `pg_get_functiondef` с БД после 0050, а не регулярка поверх функции | Правка затрагивает десятки мест; регулярка хрупкая | Если тело снято с БД не той версии, откатится 0037; ловят тесты п. 7 |
|
||||
| 2026-10-28 | DROP старых сигнатур без CASCADE в одной миграции, затем CREATE в порядке «вызываемая раньше» | Неоднозначные перегрузки с DEFAULT; скрытая зависимость должна ронять миграцию, а не удалять объекты | Падение миграции при неизвестной зависимости — разбор вручную |
|
||||
| 2026-10-28 | Имя миграции `0051_upd_form_cells_subtree` | Номер из решения человека, имя по change id | Переименование при rebase |
|
||||
| 2026-10-28 | Эталон эквивалентности — фильтр 0050 поверх полного `v_form_view` в коде теста, сравнение `data::text` побайтно, проход по каждой строке INPUT тестового листа | Требование «побайтно»; dict-сравнение в HTTP не видит масштаб numeric; полный проход закрывает первую и последнюю строку без ручного выбора | Тесты дольше на несколько секунд |
|
||||
| 2026-10-28 | Порог «GET не медленнее более чем на 10 %» и замер GET в одном соединении с чередованием правок | Нужен численный критерий регресса; чередование проверяет общий кэш планов | Порог 10 % может быть слишком мягким или жёстким при шуме стенда |
|
||||
| 2026-10-28 | Отсутствие очереди на rate 5 проверяется как `(echoed + errors) / sent ≥ 0.95`, без задержки эха по времени | Бот не пишет задержку эха, а `perf/` сейчас не трогаем; при отсутствии очереди сервер отвечает почти на все отправленные правки | Растущую задержку при почти полном ответе этот критерий не увидит; можно позже добавить в бот p50 и p95 задержки |
|
||||
| 2026-10-28 | Точка «до» на 2092 строках не снимается; ориентир — 1,46 с из шага memory (две сборки) и около 0,73 с после 1844 по оценке | Критерий абсолютный (≤ 50 мс); экономия около 30 мин на `reset`, `gen` и `grow` с кодом 1844 | В отчёте нет измеренной точки «до» на большом листе, только оценка |
|
||||
| 2026-10-28 | Снимок GET всех листов FORM_1 и FORM_2 до и после миграции на стенде и SQL-эквивалентность на 50+ строках стенда | Тестовые данные малы; реальный объём и разброс статей есть только на стенде | +10 мин к замеру |
|
||||
@ -0,0 +1,47 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Правка ячейки на сервере стоит столько же, сколько сборка всего листа. На стенде профилирования (`perf/REPORT.md`, М3; `perf/report-data/20261026-s1/sql/coauthor/sql_top.json`) правка на FORM_2/AHR в 1154 строки занимает 630 мс: `v3.upd_form_cells` дважды собирает лист через `v3.v_form_view`, по 310 мс на сборку. На листе в 2092 строки (после grow) правка занимает 1,46 с. WebSocket-соединение обрабатывает сообщения последовательно, поэтому потолок — около 1,6 правки/с (на 2092 строках около 0,7). Пользователь, который быстро вводит значения, видит, что подтверждения его правок отстают всё сильнее.
|
||||
|
||||
AURORA-1844 (PR #231, база этой ветки) убрала снимок «до» и `_jsonb_diff`. Теперь на правку приходится одна полная сборка. Её вызов вынесен в шов `v3._form_affected_rows`. Но и одна сборка — это около 310 мс, и время по-прежнему растёт с размером листа. Задача AURORA-1850 (Gitea #235, эпик AURORA-1834) убирает и её: правка собирает только затронутое поддерево, то есть изменённые строки и агрегаты их предков.
|
||||
|
||||
Критерий из Jira: правка ячейки на сервере занимает ≤ 50 мс независимо от размера листа, а суммы совпадают с полной сборкой (подтверждается тестом).
|
||||
|
||||
## What Changes
|
||||
|
||||
- `v3._form_affected_rows` для FORM_1 и FORM_2 больше не вызывает `v3.v_form_view` и не собирает лист целиком. Касается листов AHR/CAP/OPER, сателлитов AHR_RENT/AHR_UTILITY/AHR_SECURITY, а также AHR_LIMIT и OTCH9F.
|
||||
- В существующие построители листа (`v3.v_form1_sheet_sections`, `v3.v_form2_sheet_sections` и их вызывающие) добавляется параметр «фокус по строкам». Значение NULL означает прежнее поведение, то есть полный лист. Параллельной копии сборки нет: GET листа и ответ на правку считаются одним кодом.
|
||||
- `sort_order` в ответе на правку остаётся глобальным: он совпадает с номером строки в полном GET того же листа с теми же параметрами.
|
||||
- `v3.get_form2_ahr_satellite_delta` переводится на тот же механизм. Это фоновое обновление листа АХР после правки или добавления строки сателлита, и сейчас оно строит весь AHR. Правка миграции 0037 (сопоставление строк AHR_LIMIT со статьями глубины 4) сохраняется.
|
||||
- FORM_4 остаётся на полной сборке, пока не слита AURORA-1846 (PR #232). Критерий производительности для FORM_4 переносится.
|
||||
- Новая миграция `0051_upd_form_cells_subtree`, номер — в стеке после 0050 из AURORA-1844. Номер может измениться при rebase (см. design.md).
|
||||
- Новые интеграционные тесты эквивалентности сравнивают каждую строку ответа на правку с полным GET.
|
||||
|
||||
**Не меняется:**
|
||||
- контракт HTTP (`PATCH /form/{id}/sheet/{sheet}/cell(s)`) и WebSocket (`cell_updated`): форма строк, состав строк, правила `sections`;
|
||||
- содержимое GET листа. Исключение — детерминированный порядок строк при совпадении ключа сортировки (design.md, D6);
|
||||
- проверки прав (`v3.upd_form_cells` с `p_user_id`, `can_edit`, `computed_field`), синхронизация сателлитов, `v3._apply_form_cell`;
|
||||
- `add_budget_line` и `del_budget_line`: они по-прежнему отдают строки через полный `v_form_view`. Это задел на потом, не в этой задаче;
|
||||
- фронт.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `sheet-cell-edit`: ответ на правку ячеек листа формы (HTTP и WebSocket), то есть состав строк, их содержимое и `sort_order` относительно полного GET; фоновое обновление АХР после правки сателлита; время правки на сервере. Каталог `openspec/specs/` пока пуст, поэтому в спецификацию вместе с новыми требованиями попадает и контракт, установленный AURORA-1844.
|
||||
|
||||
### Modified Capabilities
|
||||
Нет.
|
||||
|
||||
## Impact
|
||||
|
||||
- SQL (`api/alembic/versions/`): новая миграция `0051_*.py` и `sql/0051_*.sql`. Пересоздаются `v3.v_form2_sheet_sections`, `v3.v_form2_sheet_jsonb`, `v3.v_form2_view`, `v3.v_form1_sheet_sections` и `v3.v_form1_sheet_jsonb`: у них меняется сигнатура, поэтому нужен DROP и CREATE. Появляется новая `v3.v_form1_view`. Переписываются `v3.v_form_view` (ветка FORM_1), `v3._form_affected_rows` и `v3.get_form2_ahr_satellite_delta`. Тело `v_form2_view` и `get_form2_ahr_satellite_delta` берётся из БД после миграции 0037.
|
||||
- Python (`api/src`) не меняется: сигнатуры `upd_form_cell(s)` и `get_form2_ahr_satellite_delta` прежние.
|
||||
- Тесты: `api/tests/integration/` — эквивалентность на базе `sheet_contract.py`.
|
||||
- `ARCHITECTURE.md`: абзац о `_form_affected_rows` дополняется описанием фокусной сборки.
|
||||
- Связанные задачи:
|
||||
- AURORA-1844 — шов и контракт полных строк, обязательная предпосылка.
|
||||
- AURORA-1846 — FORM_4. После её слияния — отдельная задача на фокус FORM_4 по разделу, программе и проекту.
|
||||
- AURORA-1845 — сужена до кэша пользователя и формы на соединение. Пакетная обработка — отдельная неприоритетная задача. После 1850 ручной ввод не должен создавать очередь, это проверяется замером.
|
||||
- AURORA-1838 и AURORA-1848 — фронт ищет строку по `(row_type, depth, sort_order)`, поэтому `sort_order` обязан быть точным. Явные `null` в строках ответа нужны для очистки ячеек.
|
||||
- AURORA-1831 и AURORA-1846 занимают номер миграции 0050 в параллельных PR.
|
||||
@ -0,0 +1,114 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Ответ сервера на правку ячеек листа бюджетной формы (HTTP `PATCH /form/{id}/sheet/{sheet}/cell(s)` и WebSocket `cell_updated`): какие строки в нём приходят, какими они должны быть относительно полного GET листа и сколько времени правка может стоить на сервере.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Ответ на правку содержит затронутые строки целиком
|
||||
Ответ на правку ячеек листов AHR/CAP/OPER форм FORM_1, FORM_2 и FORM_4 SHALL содержать каждую изменённую строку INPUT и все её строки-агрегаты, то есть всех предков по дереву статей до ROOT включительно. Для FORM_4 предками считаются только ROOT раздела, GROUP программы и ITEM проекта изменённой строки. Агрегат SHALL входить в ответ, даже если его значения не изменились. Ответ SHALL NOT содержать строку META и строки других ветвей дерева. Поле `data` каждой строки SHALL быть полной строкой в том же виде, что и в GET листа с теми же `direction` и `sections`: пустые значения передаются явным `null`. Если задан `sections`, ключи верхнего уровня у исключённых разделов SHALL отсутствовать в `data`.
|
||||
|
||||
#### Scenario: Правка числовой ячейки возвращает строку и её предков
|
||||
- **WHEN** исполнитель меняет `plan.q1` у строки листа FORM_2/AHR
|
||||
- **THEN** ответ содержит эту строку INPUT и по одной строке-агрегату на каждый уровень её пути в дереве статей
|
||||
- **AND** `data` каждой строки ответа равно `data` строки того же `sort_order` из последующего GET листа
|
||||
|
||||
#### Scenario: Правка нечисловой ячейки всё равно возвращает предков
|
||||
- **WHEN** исполнитель меняет `header.justification` у строки FORM_2/AHR
|
||||
- **THEN** ответ содержит строку INPUT и всех её предков, хотя их суммы не изменились
|
||||
|
||||
#### Scenario: Очистка значения
|
||||
- **WHEN** исполнитель записывает `null` в `plan.q1`
|
||||
- **THEN** в строке INPUT ответа ключ `plan.q1` присутствует со значением `null` (или 0 — так же, как в GET)
|
||||
- **AND** суммы предков в ответе уменьшены на прежнее значение ячейки
|
||||
|
||||
#### Scenario: Правка с фильтром разделов
|
||||
- **WHEN** правка выполняется с `sections=plan`
|
||||
- **THEN** ни одна строка ответа не содержит ключей разделов, отличных от `plan`, а также служебных ключей, которых нет в GET с тем же `sections`
|
||||
|
||||
### Requirement: sort_order в ответе совпадает с полным листом
|
||||
Значения `row_type`, `depth` и `sort_order` каждой строки ответа на правку SHALL совпадать со значениями этой же строки в полном GET листа с теми же параметрами, выполненном сразу после правки. `sort_order` — сквозной номер строки во всём листе, а не номер внутри ответа. Это SHALL выполняться для первой и последней строки листа, после добавления и удаления строк в листе, а также при совпадении ключа сортировки у строки INPUT и узла дерева. Порядок строк в GET при совпадении ключа SHALL быть детерминированным и одинаковым в GET и в ответе на правку.
|
||||
|
||||
#### Scenario: Первая и последняя строка листа
|
||||
- **WHEN** правится первая по порядку строка INPUT листа, а затем последняя
|
||||
- **THEN** у каждой строки обоих ответов есть строка с тем же `(row_type, depth, sort_order)` в GET листа, и её `data` совпадает
|
||||
|
||||
#### Scenario: После добавления и удаления строк
|
||||
- **WHEN** в лист добавлены строки (POST line), одна из строк в середине листа удалена (DELETE line), после чего правится строка, расположенная ниже удалённой
|
||||
- **THEN** `sort_order` каждой строки ответа равен её `sort_order` в GET листа после этих операций
|
||||
|
||||
#### Scenario: Пакет правок по разным ветвям дерева
|
||||
- **WHEN** через `PATCH /cells` одним запросом правятся строки двух разных ветвей дерева статей
|
||||
- **THEN** ответ содержит обе строки INPUT и объединение их предков без дублей, и каждая строка совпадает со строкой GET того же `sort_order`
|
||||
|
||||
#### Scenario: Направление FORM_1
|
||||
- **WHEN** правится строка FORM_1/AHR с `direction=Support`
|
||||
- **THEN** `sort_order` строк ответа равен `sort_order` в GET FORM_1/AHR с `direction=Support`
|
||||
|
||||
### Requirement: Состав ответа на листах сателлитов, AHR_LIMIT и OTCH9F
|
||||
На листах FORM_2 AHR_RENT, AHR_UTILITY и AHR_SECURITY ответ на правку SHALL содержать только изменённые строки. На AHR_LIMIT ответ SHALL содержать изменённые строки LEAF шаблона и их предков по шаблону (GROUP и SECTION). На OTCH9F ответ SHALL содержать все строки (все месяцы) того же сочетания статьи и оборудования, что и изменённая строка: остатки на начало месяца зависят от предыдущих месяцев. Для всех этих листов SHALL выполняться требование о совпадении `data` и `sort_order` с полным GET.
|
||||
|
||||
#### Scenario: Правка строки аренды
|
||||
- **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT
|
||||
- **THEN** ответ содержит ровно одну строку с `data.id` этой строки, и она совпадает со строкой GET AHR_RENT того же `sort_order`
|
||||
|
||||
#### Scenario: Правка количества в лимитах
|
||||
- **WHEN** исполнитель меняет `qty_q1` у строки LEAF листа AHR_LIMIT
|
||||
- **THEN** ответ содержит эту строку и строки её группы и раздела с пересчитанными `amount_q1`, и они совпадают с GET AHR_LIMIT
|
||||
|
||||
#### Scenario: Правка месяца в OTCH9F
|
||||
- **WHEN** исполнитель меняет поступление за месяц у строки OTCH9F
|
||||
- **THEN** ответ содержит строки всех месяцев того же оборудования той же статьи с пересчитанными остатками, и они совпадают с GET OTCH9F
|
||||
|
||||
### Requirement: Обновление АХР после правки сателлита
|
||||
После правки или добавления строки на AHR_RENT, AHR_UTILITY, AHR_SECURITY или AHR_LIMIT формы FORM_2 сервер SHALL отправить подключённым к листу AHR событие (`cell_updated` или `row_added`). Событие SHALL содержать синхронизированные строки AHR и всех их предков, включая уровень SUB_SUB_ITEM. Каждая из этих строк SHALL совпадать со строкой того же `sort_order` полного GET листа AHR. Если синхронизированных строк нет, событие SHALL NOT содержать строк AHR, то есть ответ пуст, а не весь лист. Строки AHR_LIMIT SHALL сопоставляться и со статьями AHR глубины 4 по `num_group_id` (поведение миграции 0037).
|
||||
|
||||
#### Scenario: Правка аренды обновляет АХР
|
||||
- **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT, связанной со строкой AHR
|
||||
- **THEN** строки, которые сервер готовит для события на лист AHR, совпадают со строками GET AHR того же `sort_order`, и среди них есть синхронизированная строка INPUT с новой суммой
|
||||
|
||||
#### Scenario: Источник без синхронизированных строк
|
||||
- **WHEN** запрашивается обновление АХР для строки сателлита, у которой нет связанной строки AHR
|
||||
- **THEN** набор строк для события пуст
|
||||
|
||||
#### Scenario: Лимит на статье глубины 4
|
||||
- **WHEN** правится строка AHR_LIMIT, код статьи которой совпадает с `num_group_id` статьи AHR глубины 4
|
||||
- **THEN** набор строк для события содержит синхронизированную строку AHR этой статьи
|
||||
|
||||
### Requirement: Пакет правок атомарен
|
||||
Если хотя бы одна правка в пакете отклонена (неизвестная колонка, чужая строка, нет прав, окно закрыто), SHALL NOT применяться ни одна правка пакета, и ответ SHALL быть ошибкой.
|
||||
|
||||
#### Scenario: Ошибочная колонка в пакете
|
||||
- **WHEN** `PATCH /cells` содержит одну корректную правку и одну с несуществующей колонкой
|
||||
- **THEN** ответ — ошибка 4xx, и GET листа после запроса совпадает с GET до него
|
||||
|
||||
### Requirement: GET листа не меняется
|
||||
Содержимое GET листов FORM_1 и FORM_2 (строки, `data`, `sort_order`) после изменения SHALL совпадать с содержимым до него на тех же данных. Исключение — порядок строк с совпадающим ключом сортировки: до изменения он не был определён. Время GET FORM_2/AHR SHALL NOT вырасти больше чем на 10 %.
|
||||
|
||||
#### Scenario: Снимок GET до и после миграции
|
||||
- **WHEN** на стенде профилирования снят GET всех листов FORM_1 и FORM_2 из манифеста до применения миграции и после неё
|
||||
- **THEN** ответы попарно совпадают
|
||||
|
||||
#### Scenario: Время GET после смешанной нагрузки
|
||||
- **WHEN** в одном соединении с БД чередуются правки ячеек и GET листа FORM_2/AHR
|
||||
- **THEN** среднее время GET не больше базового + 10 %
|
||||
|
||||
### Requirement: Время правки ячейки на FORM_1 и FORM_2
|
||||
Правка одной ячейки листа FORM_1 или FORM_2 SHALL NOT собирать лист целиком: на правку SHALL приходиться 0 вложенных полных сборок листа. Среднее серверное время `v3.upd_form_cell` на FORM_2/AHR SHALL быть не больше 50 мс как на листе в 1154 строки, так и на листе в 2092 строки. Требование не распространяется на FORM_4 до слияния AURORA-1846.
|
||||
|
||||
#### Scenario: Соавтор на листе масштаба 1
|
||||
- **WHEN** на стенде профилирования (масштаб 1, без grow, FORM_2/AHR 1154 строки) выполняется шаг `coauthor`
|
||||
- **THEN** в `sql/coauthor/sql_top.json` `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс
|
||||
- **AND** вызовов `v3.v_form_view` не верхнего уровня (`toplevel = false`) нет, а число вызовов `v3.v_form2_view(p_form_id, p_sheet, p_sections)` равно числу GET листа и не зависит от числа правок
|
||||
|
||||
#### Scenario: Соавтор на выросшем листе
|
||||
- **WHEN** после `perfkit.gen.grow` (FORM_2/AHR 2092 строки) шаг `coauthor` выполняется повторно
|
||||
- **THEN** `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс
|
||||
|
||||
### Requirement: Ручной ввод без очереди
|
||||
Сервер SHALL успевать подтверждать правки одного соединения с частотой ручного ввода (5 правок/с), не накапливая очередь.
|
||||
|
||||
#### Scenario: Соавтор с частотой 5 правок в секунду
|
||||
- **WHEN** бот соавтора на FORM_2/AHR отправляет 5 правок/с в течение шага `coauthor`
|
||||
- **THEN** число полученных ответов (эхо и ошибки) составляет не меньше 95 % от числа отправленных правок
|
||||
Loading…
x
Reference in New Issue
Block a user