perf(sheet): upd_form_cells возвращает затронутые строки целиком, без снимка до и _jsonb_diff (AURORA-1844)
Выборка строк вынесена в v3._form_affected_rows; FORM_4 отбирает ROOT, GROUP и ITEM только изменённых строк; при sections убираются ключи исключённых разделов. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2a287480b6
commit
6855e9163a
@ -2,7 +2,7 @@
|
||||
|
||||
Монорепозиторий: `api/` (FastAPI) отдаёт REST, WebSocket и собранный фронт; `web/` — SPA на React. Основная бизнес-логика — PL/pgSQL-функции схемы `v3` в Postgres. Python-слой проверяет роли, валидирует запрос, вызывает функцию и маппит её ошибки.
|
||||
|
||||
Поток редактирования ячейки: `RealtimeTable` → WebSocket `/api/v1/ws/form/{id}/sheet/{sheet}` (`cell_edit_start` — блокировка, `cell_updated`) → `SheetService` → `v3.upd_form_cell(s)`. Функция проверяет роль и окно этапа и пишет аудит `v3.log_event`, после чего изменение рассылается остальным подключённым.
|
||||
Поток редактирования ячейки: `RealtimeTable` → WebSocket `/api/v1/ws/form/{id}/sheet/{sheet}` (`cell_edit_start` — блокировка, `cell_updated`) → `SheetService` → `v3.upd_form_cell(s)`. Функция проверяет роль и окно этапа и пишет аудит `v3.log_event`, после чего изменение рассылается остальным подключённым. Ответ `upd_form_cells` (HTTP и `cell_updated`) — полные строки (`data` как в GET листа, с явными `null`): изменённые INPUT и все их предки-агрегаты, в том числе неизменившиеся; при `sections` ключи исключённых разделов убираются. Выборку делает `v3._form_affected_rows` (миграция 0050).
|
||||
|
||||
## Модули
|
||||
|
||||
|
||||
15
api/alembic/versions/0050_upd_form_cells_full_rows.py
Normal file
15
api/alembic/versions/0050_upd_form_cells_full_rows.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0050"
|
||||
down_revision = "0049"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0050_upd_form_cells_full_rows.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
194
api/alembic/versions/sql/0050_upd_form_cells_full_rows.sql
Normal file
194
api/alembic/versions/sql/0050_upd_form_cells_full_rows.sql
Normal file
@ -0,0 +1,194 @@
|
||||
-- AURORA-1844: v3.upd_form_cells возвращает затронутые строки целиком.
|
||||
-- Убран снимок «до» и _jsonb_diff: после применения правок выполняется один
|
||||
-- вызов v_form_view (внутри v3._form_affected_rows), ответ — полные строки
|
||||
-- изменённых INPUT и всех их предков-агрегатов (включая неизменившиеся).
|
||||
-- Обёртка с p_user_id (0031) и upd_form_cell не меняются.
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3._form_affected_rows(p_form_id integer, p_sheet character varying, p_ids integer[], p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
|
||||
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_form_type VARCHAR;
|
||||
v_strip TEXT[] := ARRAY[]::TEXT[];
|
||||
v_lc_path INT[];
|
||||
v_eid_set INT[];
|
||||
v_ei_path INT[];
|
||||
BEGIN
|
||||
SELECT bf.form_type_code INTO v_form_type
|
||||
FROM v3.budget_form bf
|
||||
WHERE bf.id = p_form_id;
|
||||
|
||||
-- Ключи верхнего уровня исключённых разделов (при p_sections они приходят как null
|
||||
-- и затёрли бы данные клиента). Вложенные null не трогаем: это очистка ячейки.
|
||||
IF p_sections IS NOT NULL THEN
|
||||
SELECT COALESCE(array_agg(k), ARRAY[]::TEXT[])
|
||||
INTO v_strip
|
||||
FROM unnest(ARRAY['plan','seq_dfip','seq_ssp','approved','contract','booking',
|
||||
'contract_summary','allocation','collegial','ckk',
|
||||
'sequestration','reserve','contract_detail',
|
||||
'q1','q2','q3','q4','totals',
|
||||
'fact_q1','fact_q2','fact_q3','fact_q4']) AS k
|
||||
WHERE NOT (k = ANY(p_sections));
|
||||
END IF;
|
||||
|
||||
IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
|
||||
RETURN QUERY
|
||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(p_ids)
|
||||
ORDER BY v.sort_order;
|
||||
|
||||
ELSIF p_sheet = 'AHR_LIMIT' THEN
|
||||
v_lc_path := v3._limit_template_path_set(p_ids);
|
||||
RETURN QUERY
|
||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||
WHERE (v.data->>'id')::INT = ANY(v_lc_path)
|
||||
ORDER BY v.sort_order;
|
||||
|
||||
ELSIF p_sheet = 'OTCH9F' THEN
|
||||
RETURN QUERY
|
||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM v3.fixed_asset_report far
|
||||
WHERE far.id = ANY(p_ids)
|
||||
AND far.expense_item_id = (v.data->>'expense_item_id')::INT
|
||||
AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'))
|
||||
ORDER BY v.sort_order;
|
||||
|
||||
ELSE
|
||||
SELECT array_agg(DISTINCT bl.expense_item_id)
|
||||
INTO v_eid_set
|
||||
FROM v3.budget_line bl
|
||||
WHERE bl.id = ANY(p_ids);
|
||||
v_ei_path := v3._expense_item_path_set(v_eid_set);
|
||||
|
||||
IF v_form_type = 'FORM_4' THEN
|
||||
-- ROOT раздела, GROUP по program_id и ITEM по project_id изменённых строк
|
||||
-- (у GROUP/ITEM FORM_4 expense_item_id = id раздела, поэтому фильтр
|
||||
-- по expense_item_id захватил бы все программы и проекты раздела).
|
||||
RETURN QUERY
|
||||
WITH vv AS MATERIALIZED (
|
||||
SELECT f.row_type, f.depth, f.sort_order, f.data
|
||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) f
|
||||
WHERE f.row_type <> 'META'
|
||||
),
|
||||
chg AS (
|
||||
SELECT DISTINCT (x.data->'header'->>'program_id')::INT AS program_id,
|
||||
(x.data->'header'->>'project_id')::INT AS project_id
|
||||
FROM vv x
|
||||
WHERE x.row_type = 'INPUT' AND (x.data->>'line_id')::INT = ANY(p_ids)
|
||||
)
|
||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||
FROM vv v
|
||||
WHERE (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(p_ids))
|
||||
OR (v.row_type = 'ROOT'
|
||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
|
||||
OR (v.row_type = 'GROUP'
|
||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
|
||||
AND (v.data->'header'->>'program_id')::INT IN (SELECT c.program_id FROM chg c))
|
||||
OR (v.row_type = 'ITEM'
|
||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
|
||||
AND (v.data->'header'->>'project_id')::INT IN (SELECT c.project_id FROM chg c))
|
||||
ORDER BY v.sort_order;
|
||||
ELSE
|
||||
RETURN QUERY
|
||||
SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip
|
||||
FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v
|
||||
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_SUB_ITEM')
|
||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
|
||||
OR (v.row_type = 'INPUT'
|
||||
AND (v.data->>'line_id')::INT = ANY(p_ids))
|
||||
ORDER BY v.sort_order;
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.upd_form_cells(p_form_id integer, p_sheet character varying, p_changes jsonb, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[])
|
||||
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_change JSONB;
|
||||
v_line_id INT;
|
||||
v_column TEXT;
|
||||
v_changed_lines INT[] := ARRAY[]::INT[];
|
||||
BEGIN
|
||||
IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
|
||||
RAISE EXCEPTION 'p_changes must be a JSONB array';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN
|
||||
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
|
||||
END IF;
|
||||
|
||||
FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
|
||||
LOOP
|
||||
v_line_id := (v_change->>'line_id')::INT;
|
||||
v_column := v_change->>'column';
|
||||
|
||||
IF v_line_id IS NULL OR v_column IS NULL THEN
|
||||
RAISE EXCEPTION 'change must have line_id and column: %', v_change;
|
||||
END IF;
|
||||
|
||||
IF p_sheet = 'AHR_LIMIT' THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.limit_template lt
|
||||
WHERE lt.id = v_line_id AND lt.row_type = 'LEAF') THEN
|
||||
RAISE EXCEPTION 'limit_template.id=% не существует или не LEAF', v_line_id;
|
||||
END IF;
|
||||
ELSIF p_sheet = 'OTCH9F' THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.fixed_asset_report
|
||||
WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
|
||||
RAISE EXCEPTION 'fixed_asset_report.id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||
END IF;
|
||||
ELSIF p_sheet = 'AHR_RENT' THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.rent_detail rd
|
||||
JOIN v3.budget_line bl ON bl.id = rd.line_id
|
||||
WHERE rd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
||||
RAISE EXCEPTION 'rent_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||
END IF;
|
||||
ELSIF p_sheet = 'AHR_UTILITY' THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.utility_detail ud
|
||||
JOIN v3.budget_line bl ON bl.id = ud.line_id
|
||||
WHERE ud.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
||||
RAISE EXCEPTION 'utility_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||
END IF;
|
||||
ELSIF p_sheet = 'AHR_SECURITY' THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.security_detail sd
|
||||
JOIN v3.budget_line bl ON bl.id = sd.line_id
|
||||
WHERE sd.id = v_line_id AND bl.budget_form_id = p_form_id) THEN
|
||||
RAISE EXCEPTION 'security_detail id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||
END IF;
|
||||
ELSE
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.budget_line
|
||||
WHERE id = v_line_id AND budget_form_id = p_form_id) THEN
|
||||
RAISE EXCEPTION 'budget_line id=% не принадлежит budget_form id=%', v_line_id, p_form_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF NOT (v_line_id = ANY(v_changed_lines)) THEN
|
||||
v_changed_lines := v_changed_lines || v_line_id;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
|
||||
LOOP
|
||||
PERFORM v3._apply_form_cell(
|
||||
(v_change->>'line_id')::INT,
|
||||
v_change->>'column',
|
||||
v_change->'value',
|
||||
p_sheet,
|
||||
p_form_id);
|
||||
END LOOP;
|
||||
|
||||
RETURN QUERY
|
||||
SELECT r.row_type, r.depth, r.sort_order, r.data
|
||||
FROM v3._form_affected_rows(p_form_id, p_sheet, v_changed_lines, p_direction, p_sections) r
|
||||
ORDER BY r.sort_order;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
Loading…
x
Reference in New Issue
Block a user