From 719f5688630f69dd41fe59e7a0fc7ecab01142f2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:40:41 +0400 Subject: [PATCH] =?UTF-8?q?perf(api):=20=D1=81=D0=B2=D0=BE=D0=B4=202=20?= =?UTF-8?q?=E2=80=94=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9=20=D0=BE=D1=82=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=20svod2=5Flines,=20=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D1=8B=20=D1=81=20total=5Fcount,=20=D0=B2=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B0=D0=BD=D1=82=D1=8B=20=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B2=20=D0=B2=20SQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../versions/sql/0053_svod2_server_paging.sql | 672 ++++++++++++++++++ api/tests/integration/test_svod2_sql.py | 67 ++ .../unit/test_summary2_related_records_sql.py | 14 +- 3 files changed, 750 insertions(+), 3 deletions(-) diff --git a/api/alembic/versions/sql/0053_svod2_server_paging.sql b/api/alembic/versions/sql/0053_svod2_server_paging.sql index 40c3cac..e30e602 100644 --- a/api/alembic/versions/sql/0053_svod2_server_paging.sql +++ b/api/alembic/versions/sql/0053_svod2_server_paging.sql @@ -235,3 +235,675 @@ AS $function$ ) )) $function$; + +-- Прежние 6-аргументные сигнатуры заменяются: тип результата меняется, перегрузка не нужна. +DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[], integer, integer, varchar, varchar); +DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[], integer, integer, varchar, varchar); + +-- Общий отбор строк свода 2 для страниц и вариантов фильтров. Условия отбора как в 0049, +-- форма выбирается по p_sheet; p_search (уже нормализованный) отсекает строки без совпадения +-- по полям header, contract_summary и contract_detail, как matchesSearch экрана. +CREATE OR REPLACE FUNCTION v3.svod2_lines( + p_sheet varchar, + p_year integer, + p_org_unit_ids integer[], + p_search text +) +RETURNS TABLE( + line_id integer, form_type_code varchar, form_id integer, org_unit_id integer, + org_name varchar, section_code varchar, item_id varchar, + smeta_type varchar, smeta_direction varchar +) +LANGUAGE sql STABLE +AS $function$ +WITH empty_sections AS MATERIALIZED ( + SELECT + to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS contract_summary, + to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS contract_detail +), eligible_lines AS ( + -- svod2_lines eligible: begin + SELECT DISTINCT + bl.id AS line_id, + bf.form_type_code, + bf.id AS form_id, + bf.org_unit_id, + ou.title AS org_name, + ei.section_code, + ei.item_id, + CASE bl.direction + WHEN 'Support' THEN 'Поддержка' + WHEN 'Development' THEN 'Развитие' + ELSE NULL + END::varchar AS smeta_direction, + CASE ei.sheet WHEN 'AHR' THEN 'АХР' WHEN 'CAP' THEN 'КВ' + WHEN 'OPER' THEN 'Операц' END::varchar AS smeta_type + FROM v3.budget_form bf + JOIN v3.budget_line bl ON bl.budget_form_id = bf.id + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id + LEFT JOIN v3.plan presence_plan ON presence_plan.line_id = bl.id + LEFT JOIN v3.reserve presence_reserve ON presence_reserve.line_id = bl.id + LEFT JOIN v3.contract_summary presence_contract_summary ON presence_contract_summary.line_id = bl.id + LEFT JOIN v3.allocation presence_allocation ON presence_allocation.line_id = bl.id + LEFT JOIN v3.collegial_approval presence_collegial_approval ON presence_collegial_approval.line_id = bl.id + LEFT JOIN v3.ckk presence_ckk ON presence_ckk.line_id = bl.id + LEFT JOIN v3.contract_detail presence_contract_detail ON presence_contract_detail.line_id = bl.id + LEFT JOIN v3.sequestration presence_sequestration ON presence_sequestration.line_id = bl.id + LEFT JOIN v3.booking presence_booking ON presence_booking.line_id = bl.id + LEFT JOIN v3.budget_line_quarter presence_budget_line_quarter ON presence_budget_line_quarter.line_id = bl.id + LEFT JOIN v3.rent_detail presence_rent_detail ON presence_rent_detail.line_id = bl.id + LEFT JOIN v3.security_detail presence_security_detail ON presence_security_detail.line_id = bl.id + LEFT JOIN v3.utility_detail presence_utility_detail ON presence_utility_detail.line_id = bl.id + WHERE bf.year = p_year + AND ( (p_sheet = 'GO' AND bf.form_type_code IN ('FORM_1', 'FORM_4')) + OR (p_sheet = 'RF' AND bf.form_type_code = 'FORM_2')) + AND ei.sheet IN ('AHR', 'CAP', 'OPER') + AND (p_org_unit_ids IS NULL OR cardinality(p_org_unit_ids) = 0 + OR bf.org_unit_id = ANY(p_org_unit_ids)) + AND ( + ( + presence_plan.plan_q1 <> 0 + OR presence_plan.plan_q2 <> 0 + OR presence_plan.plan_q3 <> 0 + OR presence_plan.plan_q4 <> 0 + OR presence_plan.comment ~ '[^[:space:]]' + ) + OR presence_reserve.line_id IS NOT NULL + OR presence_contract_summary.line_id IS NOT NULL + OR presence_allocation.line_id IS NOT NULL + OR presence_collegial_approval.line_id IS NOT NULL + OR presence_ckk.line_id IS NOT NULL + OR presence_contract_detail.line_id IS NOT NULL + OR presence_sequestration.line_id IS NOT NULL + OR presence_booking.line_id IS NOT NULL + OR ( + presence_budget_line_quarter.adj_current <> 0 + OR presence_budget_line_quarter.adj_ssp <> 0 + OR presence_budget_line_quarter.adj_rf <> 0 + OR presence_budget_line_quarter.adj_reserve <> 0 + OR presence_budget_line_quarter.adj_comment ~ '[^[:space:]]' + OR presence_budget_line_quarter.target_change <> 0 + OR presence_budget_line_quarter.base_plan_correction <> 0 + OR presence_budget_line_quarter.base_plan_correction_comment ~ '[^[:space:]]' + OR presence_budget_line_quarter.payment_date IS NOT NULL + OR presence_budget_line_quarter.payment_amount <> 0 + OR presence_budget_line_quarter.payment_amount_ho <> 0 + OR presence_budget_line_quarter.payment_amount_rf <> 0 + OR presence_budget_line_quarter.payment_comment ~ '[^[:space:]]' + OR presence_budget_line_quarter.payment_act ~ '[^[:space:]]' + OR presence_budget_line_quarter.actual_m1 <> 0 + OR presence_budget_line_quarter.actual_m2 <> 0 + OR presence_budget_line_quarter.actual_m3 <> 0 + OR presence_budget_line_quarter.actual_spod <> 0 + OR presence_budget_line_quarter.transfer_to_q2 <> 0 + OR presence_budget_line_quarter.transfer_to_q3 <> 0 + OR presence_budget_line_quarter.transfer_to_q4 <> 0 + OR presence_budget_line_quarter.transfer_to_economy <> 0 + OR presence_budget_line_quarter.transfer_delay_acts <> 0 + OR presence_budget_line_quarter.transfer_delay_procurement <> 0 + OR presence_budget_line_quarter.transfer_economy_rf <> 0 + OR presence_budget_line_quarter.transfer_next_comment ~ '[^[:space:]]' + OR presence_budget_line_quarter.transfer_far_comment ~ '[^[:space:]]' + OR presence_budget_line_quarter.booking_amount <> 0 + OR presence_budget_line_quarter.plan_revision_eco_change <> 0 + OR presence_budget_line_quarter.plan_revision_item_adj <> 0 + OR presence_budget_line_quarter.plan_revision_increase <> 0 + OR presence_budget_line_quarter.plan_revision_sequester <> 0 + OR presence_budget_line_quarter.plan_revision_comment ~ '[^[:space:]]' + ) + OR ( + presence_rent_detail.contract_number ~ '[^[:space:]]' + OR presence_rent_detail.contract_end_date IS NOT NULL + OR presence_rent_detail.plan_q1 <> 0 + OR presence_rent_detail.plan_q2 <> 0 + OR presence_rent_detail.plan_q3 <> 0 + OR presence_rent_detail.plan_q4 <> 0 + OR presence_rent_detail.actual_jan <> 0 + OR presence_rent_detail.actual_feb <> 0 + OR presence_rent_detail.actual_mar <> 0 + OR presence_rent_detail.actual_apr <> 0 + OR presence_rent_detail.actual_may <> 0 + OR presence_rent_detail.actual_jun <> 0 + OR presence_rent_detail.actual_jul <> 0 + OR presence_rent_detail.actual_aug <> 0 + OR presence_rent_detail.actual_sep <> 0 + OR presence_rent_detail.actual_oct <> 0 + OR presence_rent_detail.actual_nov <> 0 + OR presence_rent_detail.actual_dec <> 0 + OR presence_rent_detail.comment ~ '[^[:space:]]' + ) + OR presence_security_detail.line_id IS NOT NULL + OR presence_utility_detail.line_id IS NOT NULL + ) + -- svod2_lines eligible: end +) +SELECT el.line_id, el.form_type_code, el.form_id, el.org_unit_id, el.org_name, + el.section_code, el.item_id, el.smeta_type, el.smeta_direction +FROM eligible_lines el +WHERE p_search IS NULL OR EXISTS ( + SELECT 1 + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.contract_summary cs ON cs.line_id = bl.id + LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id + LEFT JOIN v3.ckk ck ON ck.line_id = bl.id + CROSS JOIN empty_sections e + CROSS JOIN LATERAL ( + SELECT + jsonb_build_object( + 'section_code', ei.section_code, 'item_id', ei.item_id, + 'num_group', ei.num_group_id, 'name', COALESCE(bl.name, ei.name), + 'expense_item_id', ei.id, 'justification', bl.justification, + 'internal_order', bl.internal_order, 'vsp_id', bl.vsp_id, + 'project_id', bl.project_id, 'direction', bl.direction, + 'smeta_direction', CASE bl.direction + WHEN 'Support' THEN 'Поддержка' + WHEN 'Development' THEN 'Развитие' + ELSE NULL + END, + 'source_form', el.form_type_code, 'source_id', el.form_id, + 'source_org_unit_id', el.org_unit_id, 'org_name', el.org_name, + 'smeta_type', el.smeta_type) AS header_j, + COALESCE(to_jsonb(cs) - 'line_id', e.contract_summary) AS cs_j, + CASE WHEN p_sheet = 'RF' + THEN COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail) || jsonb_build_object( + 'expenses_q1', COALESCE(cd.expenses_q1, ck.expenses_q1), + 'expenses_q2', COALESCE(cd.expenses_q2, ck.expenses_q2), + 'expenses_q3', COALESCE(cd.expenses_q3, ck.expenses_q3), + 'expenses_q4', COALESCE(cd.expenses_q4, ck.expenses_q4)) + ELSE COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail) + END AS cd_j + ) s + CROSS JOIN LATERAL jsonb_each(s.header_j || s.cs_j || s.cd_j) kv + WHERE bl.id = el.line_id + AND strpos( + v3.svod2_casefold( + CASE jsonb_typeof(kv.value) + WHEN 'string' THEN kv.value #>> '{}' + WHEN 'number' THEN v3.svod2_js_number_text((kv.value #>> '{}')::float8) + WHEN 'boolean' THEN kv.value #>> '{}' + WHEN 'null' THEN '' + ELSE kv.value::text + END), + p_search) > 0 +) +$function$; + + +-- Страница свода 2 для ГО: поиск и три фильтра на сервере, total_count - число строк до LIMIT. +CREATE OR REPLACE FUNCTION v3.v_svod2_go( + p_year integer, + p_org_unit_ids integer[] DEFAULT NULL, + p_offset integer DEFAULT NULL, + p_limit integer DEFAULT NULL, + p_sort_by varchar DEFAULT 'org_name', + p_sort_direction varchar DEFAULT 'asc', + p_search text DEFAULT NULL, + p_smeta_type text DEFAULT NULL, + p_smeta_direction text DEFAULT NULL, + p_org_name text DEFAULT NULL, + p_compact boolean DEFAULT false +) +RETURNS TABLE( + source_form varchar, source_id integer, source_org_unit_id integer, + org_name varchar, smeta_type varchar, + source_line_id integer, sort_order bigint, data jsonb, total_count bigint +) +LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog' +AS $function$ +WITH empty_sections AS MATERIALIZED ( + SELECT + (to_jsonb(jsonb_populate_record(NULL::v3.plan, '{}'::jsonb)) - 'line_id') + || jsonb_build_object('plan_year', NULL) AS plan, + (to_jsonb(jsonb_populate_record(NULL::v3.sequestration, '{}'::jsonb)) + - 'id' - 'line_id' - 'actor') || jsonb_build_object('adj_year', NULL) AS sequestration, + (to_jsonb(jsonb_populate_record(NULL::v3.reserve, '{}'::jsonb)) - 'line_id') + || jsonb_build_object('amount_year', NULL) AS reserve, + jsonb_build_object( + 'approved_q1', NULL, 'approved_q2', NULL, + 'approved_q3', NULL, 'approved_q4', NULL, + 'approved_year', NULL + ) AS approved, + to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS contract_summary, + to_jsonb(jsonb_populate_record(NULL::v3.allocation, '{}'::jsonb)) - 'line_id' AS allocation, + to_jsonb(jsonb_populate_record(NULL::v3.collegial_approval, '{}'::jsonb)) - 'line_id' AS collegial, + to_jsonb(jsonb_populate_record(NULL::v3.ckk, '{}'::jsonb)) - 'line_id' AS ckk, + to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS contract_detail, + to_jsonb(jsonb_populate_record(NULL::v3.booking, '{}'::jsonb)) - 'id' - 'line_id' AS booking, + to_jsonb(jsonb_populate_record(NULL::v3.budget_line_quarter, '{}'::jsonb)) + - 'line_id' - 'quarter' AS quarter +), numbered_lines AS ( + SELECT el.*, + count(*) OVER () AS total_count, + row_number() OVER ( + ORDER BY + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc' + THEN el.org_name END ASC NULLS LAST, + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc' + THEN el.org_name END DESC NULLS LAST, + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc' + THEN el.item_id END ASC NULLS LAST, + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc' + THEN el.item_id END DESC NULLS LAST, + CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'asc' + THEN el.section_code END ASC NULLS LAST, + CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'desc' + THEN el.section_code END DESC NULLS LAST, + el.form_type_code, el.form_id, el.line_id + ) AS sort_order + FROM v3.svod2_lines('GO', p_year, p_org_unit_ids, + NULLIF(v3.svod2_casefold(v3.svod2_js_trim(p_search)), '')) el + WHERE (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '') IS NULL + OR (el.smeta_type IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_type)) + = NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), ''))) + AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '') IS NULL + OR (el.smeta_direction IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_direction)) + = NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), ''))) + AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '') IS NULL + OR (el.org_name IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(el.org_name)) + = NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), ''))) +), page AS MATERIALIZED ( + SELECT * FROM numbered_lines + ORDER BY sort_order + LIMIT p_limit OFFSET COALESCE(p_offset, 0) +), booking_data AS ( + SELECT b.line_id, + jsonb_agg(to_jsonb(b) - 'id' - 'line_id' ORDER BY b.quarter, b.source) AS booking + FROM page pg + JOIN v3.booking b ON b.line_id = pg.line_id + GROUP BY b.line_id +) +SELECT x.source_form, x.source_id, x.source_org_unit_id, x.org_name, x.smeta_type, + x.source_line_id, x.sort_order, + CASE WHEN p_compact THEN v3.svod2_compact(x.full_data) ELSE x.full_data END AS data, + x.total_count +FROM ( + SELECT + pg.form_type_code AS source_form, + pg.form_id AS source_id, + pg.org_unit_id AS source_org_unit_id, + pg.org_name, + pg.smeta_type, + pg.line_id AS source_line_id, + pg.sort_order, + jsonb_build_object( + 'line_id', bl.id, + 'header', jsonb_build_object( + 'section_code', ei.section_code, 'item_id', ei.item_id, + 'num_group', ei.num_group_id, 'name', COALESCE(bl.name, ei.name), + 'expense_item_id', ei.id, 'justification', bl.justification, + 'internal_order', bl.internal_order, 'vsp_id', bl.vsp_id, + 'project_id', bl.project_id, 'direction', bl.direction, + 'smeta_direction', CASE bl.direction + WHEN 'Support' THEN 'Поддержка' + WHEN 'Development' THEN 'Развитие' + ELSE NULL + END, + 'source_form', pg.form_type_code, 'source_id', pg.form_id, + 'source_org_unit_id', pg.org_unit_id, 'org_name', pg.org_name, + 'smeta_type', pg.smeta_type), + 'plan', COALESCE( + (to_jsonb(pl) - 'line_id') || jsonb_build_object( + 'plan_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0) + + COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0)), + e.plan || jsonb_build_object('plan_year', NULL)), + 'sequestration', jsonb_build_object( + 'DFIP', COALESCE( + (to_jsonb(sd) - 'id' - 'line_id' - 'actor') || jsonb_build_object( + 'adj_year', COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0) + + COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0)), + e.sequestration || jsonb_build_object('adj_year', NULL)), + 'SSP_GO', COALESCE( + (to_jsonb(ss) - 'id' - 'line_id' - 'actor') || jsonb_build_object( + 'adj_year', COALESCE(ss.adj_q1, 0) + COALESCE(ss.adj_q2, 0) + + COALESCE(ss.adj_q3, 0) + COALESCE(ss.adj_q4, 0)), + e.sequestration || jsonb_build_object('adj_year', NULL))), + 'reserve', COALESCE( + (to_jsonb(rv) - 'line_id') || jsonb_build_object( + 'amount_year', COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0) + + COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)), + e.reserve || jsonb_build_object('amount_year', NULL)), + 'approved', CASE WHEN pl.line_id IS NULL AND sd.id IS NULL + AND rv.line_id IS NULL + THEN e.approved + ELSE jsonb_build_object( + 'approved_q1', COALESCE(pl.plan_q1, 0) + COALESCE(sd.adj_q1, 0) + + COALESCE(rv.amount_q1, 0), + 'approved_q2', COALESCE(pl.plan_q2, 0) + COALESCE(sd.adj_q2, 0) + + COALESCE(rv.amount_q2, 0), + 'approved_q3', COALESCE(pl.plan_q3, 0) + COALESCE(sd.adj_q3, 0) + + COALESCE(rv.amount_q3, 0), + 'approved_q4', COALESCE(pl.plan_q4, 0) + COALESCE(sd.adj_q4, 0) + + COALESCE(rv.amount_q4, 0), + 'approved_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0) + + COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0) + + COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0) + + COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0) + + COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0) + + COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)) END, + 'contract_summary', COALESCE(to_jsonb(cs) - 'line_id', e.contract_summary), + 'allocation', COALESCE(to_jsonb(al) - 'line_id', e.allocation), + 'collegial', COALESCE(to_jsonb(ca) - 'line_id', e.collegial), + 'ckk', COALESCE(to_jsonb(ck) - 'line_id', e.ckk), + 'contract_detail', COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail), + 'booking', COALESCE(bk.booking, jsonb_build_array(e.booking)), + 'q1', COALESCE(to_jsonb(q1) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object( + 'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1' + THEN CASE WHEN COALESCE(cd.expenses_q1, 0) > 0 THEN cd.expenses_q1 ELSE ck.expenses_q1 END + ELSE COALESCE(q1.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q1 + WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1 + ELSE cd.expenses_q1 END) END), + 'q2', COALESCE(to_jsonb(q2) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object( + 'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1' + THEN CASE WHEN COALESCE(cd.expenses_q2, 0) > 0 THEN cd.expenses_q2 ELSE ck.expenses_q2 END + ELSE COALESCE(q2.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q2 + WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2 + ELSE cd.expenses_q2 END) END, + 'plan_revision_eco_change', COALESCE(q2.target_change, q2.plan_revision_eco_change), + 'plan_revision_item_adj', COALESCE(q2.base_plan_correction, q2.plan_revision_item_adj)), + 'q3', COALESCE(to_jsonb(q3) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object( + 'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1' + THEN CASE WHEN COALESCE(cd.expenses_q3, 0) > 0 THEN cd.expenses_q3 ELSE ck.expenses_q3 END + ELSE COALESCE(q3.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q3 + WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3 + ELSE cd.expenses_q3 END) END, + 'plan_revision_eco_change', COALESCE(q3.target_change, q3.plan_revision_eco_change), + 'plan_revision_item_adj', COALESCE(q3.base_plan_correction, q3.plan_revision_item_adj)), + 'q4', COALESCE(to_jsonb(q4) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object( + 'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1' + THEN CASE WHEN COALESCE(cd.expenses_q4, 0) > 0 THEN cd.expenses_q4 ELSE ck.expenses_q4 END + ELSE COALESCE(q4.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q4 + WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4 + ELSE cd.expenses_q4 END) END, + 'plan_revision_eco_change', COALESCE(q4.target_change, q4.plan_revision_eco_change), + 'plan_revision_item_adj', COALESCE(q4.base_plan_correction, q4.plan_revision_item_adj)) + ) AS full_data, + pg.total_count + FROM page pg + JOIN v3.budget_line bl ON bl.id = pg.line_id + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id + CROSS JOIN empty_sections e + LEFT JOIN v3.plan pl ON pl.line_id = bl.id + LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor = 'DFIP' + LEFT JOIN v3.sequestration ss ON ss.line_id = bl.id AND ss.actor = 'SSP_GO' + LEFT JOIN v3.reserve rv ON rv.line_id = bl.id + LEFT JOIN v3.contract_summary cs ON cs.line_id = bl.id + LEFT JOIN v3.allocation al ON al.line_id = bl.id + LEFT JOIN v3.collegial_approval ca ON ca.line_id = bl.id + LEFT JOIN v3.ckk ck ON ck.line_id = bl.id + LEFT JOIN booking_data bk ON bk.line_id = bl.id + LEFT JOIN v3.budget_line_quarter q1 ON q1.line_id = bl.id AND q1.quarter = 1 + LEFT JOIN v3.budget_line_quarter q2 ON q2.line_id = bl.id AND q2.quarter = 2 + LEFT JOIN v3.budget_line_quarter q3 ON q3.line_id = bl.id AND q3.quarter = 3 + LEFT JOIN v3.budget_line_quarter q4 ON q4.line_id = bl.id AND q4.quarter = 4 +) x +ORDER BY x.sort_order; +$function$; + +-- Страница свода 2 для РФ. +CREATE OR REPLACE FUNCTION v3.v_svod2_rf( + p_year integer, + p_org_unit_ids integer[] DEFAULT NULL, + p_offset integer DEFAULT NULL, + p_limit integer DEFAULT NULL, + p_sort_by varchar DEFAULT 'org_name', + p_sort_direction varchar DEFAULT 'asc', + p_search text DEFAULT NULL, + p_smeta_type text DEFAULT NULL, + p_smeta_direction text DEFAULT NULL, + p_org_name text DEFAULT NULL, + p_compact boolean DEFAULT false +) +RETURNS TABLE( + source_form varchar, source_id integer, source_org_unit_id integer, + org_name varchar, smeta_type varchar, + source_line_id integer, sort_order bigint, data jsonb, total_count bigint +) +LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog' +AS $function$ +WITH empty_sections AS MATERIALIZED ( + SELECT + (to_jsonb(jsonb_populate_record(NULL::v3.plan, '{}'::jsonb)) - 'line_id') + || jsonb_build_object('plan_year', NULL) AS plan, + (to_jsonb(jsonb_populate_record(NULL::v3.sequestration, '{}'::jsonb)) + - 'id' - 'line_id' - 'actor') || jsonb_build_object('adj_year', NULL) AS sequestration, + (to_jsonb(jsonb_populate_record(NULL::v3.reserve, '{}'::jsonb)) - 'line_id') + || jsonb_build_object('amount_year', NULL) AS reserve, + jsonb_build_object( + 'approved_q1', NULL, 'approved_q2', NULL, + 'approved_q3', NULL, 'approved_q4', NULL, + 'approved_year', NULL + ) AS approved, + to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS contract_summary, + to_jsonb(jsonb_populate_record(NULL::v3.allocation, '{}'::jsonb)) - 'line_id' AS allocation, + to_jsonb(jsonb_populate_record(NULL::v3.collegial_approval, '{}'::jsonb)) - 'line_id' AS collegial, + to_jsonb(jsonb_populate_record(NULL::v3.ckk, '{}'::jsonb)) - 'line_id' AS ckk, + to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS contract_detail, + to_jsonb(jsonb_populate_record(NULL::v3.booking, '{}'::jsonb)) - 'id' - 'line_id' AS booking, + to_jsonb(jsonb_populate_record(NULL::v3.budget_line_quarter, '{}'::jsonb)) + - 'line_id' - 'quarter' AS budget_quarter +), numbered_lines AS ( + SELECT el.*, + count(*) OVER () AS total_count, + row_number() OVER ( + ORDER BY + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc' + THEN el.org_name END ASC NULLS LAST, + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc' + THEN el.org_name END DESC NULLS LAST, + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc' + THEN el.item_id END ASC NULLS LAST, + CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc' + THEN el.item_id END DESC NULLS LAST, + CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'asc' + THEN el.section_code END ASC NULLS LAST, + CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'desc' + THEN el.section_code END DESC NULLS LAST, + el.form_type_code, el.form_id, el.line_id + ) AS sort_order + FROM v3.svod2_lines('RF', p_year, p_org_unit_ids, + NULLIF(v3.svod2_casefold(v3.svod2_js_trim(p_search)), '')) el + WHERE (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '') IS NULL + OR (el.smeta_type IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_type)) + = NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), ''))) + AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '') IS NULL + OR (el.smeta_direction IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_direction)) + = NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), ''))) + AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '') IS NULL + OR (el.org_name IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(el.org_name)) + = NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), ''))) +), page AS MATERIALIZED ( + SELECT * FROM numbered_lines + ORDER BY sort_order + LIMIT p_limit OFFSET COALESCE(p_offset, 0) +), booking_data AS ( + SELECT b.line_id, + jsonb_agg(to_jsonb(b) - 'id' - 'line_id' ORDER BY b.quarter, b.source) AS booking + FROM page pg + JOIN v3.booking b ON b.line_id = pg.line_id + GROUP BY b.line_id +) +SELECT x.source_form, x.source_id, x.source_org_unit_id, x.org_name, x.smeta_type, + x.source_line_id, x.sort_order, + CASE WHEN p_compact THEN v3.svod2_compact(x.full_data) ELSE x.full_data END AS data, + x.total_count +FROM ( + SELECT + pg.form_type_code AS source_form, pg.form_id AS source_id, + pg.org_unit_id AS source_org_unit_id, pg.org_name, pg.smeta_type, + pg.line_id AS source_line_id, pg.sort_order, + jsonb_build_object( + 'line_id', bl.id, + 'header', jsonb_build_object( + 'section_code', ei.section_code, 'item_id', ei.item_id, + 'num_group', ei.num_group_id, 'name', COALESCE(bl.name, ei.name), + 'expense_item_id', ei.id, 'justification', bl.justification, + 'internal_order', bl.internal_order, 'vsp_id', bl.vsp_id, + 'project_id', bl.project_id, 'direction', bl.direction, + 'smeta_direction', CASE bl.direction + WHEN 'Support' THEN 'Поддержка' + WHEN 'Development' THEN 'Развитие' + ELSE NULL + END, + 'source_form', pg.form_type_code, 'source_id', pg.form_id, + 'source_org_unit_id', pg.org_unit_id, 'org_name', pg.org_name, + 'smeta_type', pg.smeta_type), + 'plan', COALESCE( + (to_jsonb(pl) - 'line_id') || jsonb_build_object( + 'plan_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0) + + COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0)), + e.plan || jsonb_build_object('plan_year', NULL)), + 'sequestration', jsonb_build_object( + 'DFIP', COALESCE( + (to_jsonb(sd) - 'id' - 'line_id' - 'actor') || jsonb_build_object( + 'adj_year', COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0) + + COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0)), + e.sequestration || jsonb_build_object('adj_year', NULL)), + 'SSP_GO', COALESCE( + (to_jsonb(ss) - 'id' - 'line_id' - 'actor') || jsonb_build_object( + 'adj_year', COALESCE(ss.adj_q1, 0) + COALESCE(ss.adj_q2, 0) + + COALESCE(ss.adj_q3, 0) + COALESCE(ss.adj_q4, 0)), + e.sequestration || jsonb_build_object('adj_year', NULL))), + 'reserve', COALESCE( + (to_jsonb(rv) - 'line_id') || jsonb_build_object( + 'amount_year', COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0) + + COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)), + e.reserve || jsonb_build_object('amount_year', NULL)), + 'approved', CASE WHEN pl.line_id IS NULL AND sd.id IS NULL + AND ss.id IS NULL AND rv.line_id IS NULL + THEN e.approved + ELSE jsonb_build_object( + 'approved_q1', COALESCE(pl.plan_q1, 0) + COALESCE(sd.adj_q1, 0) + + COALESCE(ss.adj_q1, 0) + COALESCE(rv.amount_q1, 0), + 'approved_q2', COALESCE(pl.plan_q2, 0) + COALESCE(sd.adj_q2, 0) + + COALESCE(ss.adj_q2, 0) + COALESCE(rv.amount_q2, 0), + 'approved_q3', COALESCE(pl.plan_q3, 0) + COALESCE(sd.adj_q3, 0) + + COALESCE(ss.adj_q3, 0) + COALESCE(rv.amount_q3, 0), + 'approved_q4', COALESCE(pl.plan_q4, 0) + COALESCE(sd.adj_q4, 0) + + COALESCE(ss.adj_q4, 0) + COALESCE(rv.amount_q4, 0), + 'approved_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0) + + COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0) + + COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0) + + COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0) + + COALESCE(ss.adj_q1, 0) + COALESCE(ss.adj_q2, 0) + + COALESCE(ss.adj_q3, 0) + COALESCE(ss.adj_q4, 0) + + COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0) + + COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)) END, + 'contract_summary', COALESCE(to_jsonb(cs) - 'line_id', e.contract_summary), + 'allocation', COALESCE(to_jsonb(al) - 'line_id', e.allocation), + 'collegial', COALESCE(to_jsonb(ca) - 'line_id', e.collegial), + 'ckk', COALESCE(to_jsonb(ck) - 'line_id', e.ckk), + 'contract_detail', COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail) || jsonb_build_object( + 'expenses_q1', COALESCE(cd.expenses_q1, ck.expenses_q1), + 'expenses_q2', COALESCE(cd.expenses_q2, ck.expenses_q2), + 'expenses_q3', COALESCE(cd.expenses_q3, ck.expenses_q3), + 'expenses_q4', COALESCE(cd.expenses_q4, ck.expenses_q4)), + 'booking', COALESCE(bk.booking, jsonb_build_array(e.booking)), + 'q1', COALESCE(to_jsonb(q1) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object( + 'booking_amount', COALESCE(q1.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q1 + WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1 + ELSE cd.expenses_q1 END)), + 'q2', COALESCE(to_jsonb(q2) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object( + 'booking_amount', COALESCE(q2.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q2 + WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2 + ELSE cd.expenses_q2 END), + 'plan_revision_eco_change', COALESCE(q2.target_change, q2.plan_revision_eco_change), + 'plan_revision_item_adj', COALESCE(q2.base_plan_correction, q2.plan_revision_item_adj)), + 'q3', COALESCE(to_jsonb(q3) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object( + 'booking_amount', COALESCE(q3.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q3 + WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3 + ELSE cd.expenses_q3 END), + 'plan_revision_eco_change', COALESCE(q3.target_change, q3.plan_revision_eco_change), + 'plan_revision_item_adj', COALESCE(q3.base_plan_correction, q3.plan_revision_item_adj)), + 'q4', COALESCE(to_jsonb(q4) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object( + 'booking_amount', COALESCE(q4.booking_amount, + CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q4 + WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4 + ELSE cd.expenses_q4 END), + 'plan_revision_eco_change', COALESCE(q4.target_change, q4.plan_revision_eco_change), + 'plan_revision_item_adj', COALESCE(q4.base_plan_correction, q4.plan_revision_item_adj)) + ) AS full_data, + pg.total_count + FROM page pg + JOIN v3.budget_line bl ON bl.id = pg.line_id + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id + CROSS JOIN empty_sections e + LEFT JOIN v3.plan pl ON pl.line_id = bl.id + LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor = 'DFIP' + LEFT JOIN v3.sequestration ss ON ss.line_id = bl.id AND ss.actor = 'SSP_GO' + LEFT JOIN v3.reserve rv ON rv.line_id = bl.id + LEFT JOIN v3.contract_summary cs ON cs.line_id = bl.id + LEFT JOIN v3.allocation al ON al.line_id = bl.id + LEFT JOIN v3.collegial_approval ca ON ca.line_id = bl.id + LEFT JOIN v3.ckk ck ON ck.line_id = bl.id + LEFT JOIN booking_data bk ON bk.line_id = bl.id + LEFT JOIN v3.budget_line_quarter q1 ON q1.line_id = bl.id AND q1.quarter = 1 + LEFT JOIN v3.budget_line_quarter q2 ON q2.line_id = bl.id AND q2.quarter = 2 + LEFT JOIN v3.budget_line_quarter q3 ON q3.line_id = bl.id AND q3.quarter = 3 + LEFT JOIN v3.budget_line_quarter q4 ON q4.line_id = bl.id AND q4.quarter = 4 +) x +ORDER BY x.sort_order; +$function$; + +-- Варианты фильтров и счётчики: как TanStack, с поиском и фильтрами двух других колонок. +CREATE OR REPLACE FUNCTION v3.v_svod2_facets( + p_sheet varchar, + p_year integer, + p_org_unit_ids integer[], + p_search text, + p_smeta_type text, + p_smeta_direction text, + p_org_name text +) +RETURNS TABLE(kind varchar, value varchar, n bigint) +LANGUAGE sql STABLE +AS $function$ +WITH q AS MATERIALIZED ( + SELECT NULLIF(v3.svod2_casefold(v3.svod2_js_trim(p_search)), '') AS s, + NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '') AS f_type, + NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '') AS f_dir, + NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '') AS f_org +), lines AS MATERIALIZED ( + SELECT l.smeta_type, l.smeta_direction, l.org_name, + (q.s IS NULL OR l.line_id IN ( + SELECT m.line_id FROM v3.svod2_lines(p_sheet, p_year, p_org_unit_ids, q.s) m)) AS hit, + (q.f_type IS NULL OR (l.smeta_type IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(l.smeta_type)) = q.f_type)) AS ok_type, + (q.f_dir IS NULL OR (l.smeta_direction IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(l.smeta_direction)) = q.f_dir)) AS ok_dir, + (q.f_org IS NULL OR (l.org_name IS NOT NULL + AND v3.svod2_js_trim(v3.svod2_casefold(l.org_name)) = q.f_org)) AS ok_org + FROM v3.svod2_lines(p_sheet, p_year, p_org_unit_ids, NULL) l + CROSS JOIN q +) +SELECT 'total'::varchar, NULL::varchar, count(*)::bigint FROM lines +UNION ALL +SELECT 'search_total'::varchar, NULL::varchar, count(*)::bigint FROM lines WHERE hit +UNION ALL +SELECT 'smeta_type'::varchar, g.smeta_type::varchar, count(*)::bigint +FROM lines g WHERE g.hit AND g.ok_dir AND g.ok_org AND g.smeta_type IS NOT NULL +GROUP BY g.smeta_type +UNION ALL +SELECT 'smeta_direction'::varchar, g.smeta_direction::varchar, count(*)::bigint +FROM lines g WHERE g.hit AND g.ok_type AND g.ok_org AND g.smeta_direction IS NOT NULL +GROUP BY g.smeta_direction +UNION ALL +SELECT 'org_name'::varchar, g.org_name::varchar, count(*)::bigint +FROM lines g WHERE g.hit AND g.ok_type AND g.ok_dir AND g.org_name IS NOT NULL +GROUP BY g.org_name +$function$; diff --git a/api/tests/integration/test_svod2_sql.py b/api/tests/integration/test_svod2_sql.py index 4944dc4..b403ae0 100644 --- a/api/tests/integration/test_svod2_sql.py +++ b/api/tests/integration/test_svod2_sql.py @@ -51,3 +51,70 @@ def test_compact_matches_python_projection(svod2_sql, svod2_data, client, admin_ full = legacy_full(client, "RF", auth_headers(admin_tokens))[0]["data"] got = svod2_sql("SELECT v3.svod2_compact(CAST(:d AS jsonb)) AS c", {"d": json.dumps(full)})[0][0] assert got == compact_projection(full) + + +def test_single_signature_without_overloads(svod2_sql): + rows = svod2_sql( + "SELECT proname, pronargs FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace " + "WHERE nspname = 'v3' AND proname IN ('v_svod2_go', 'v_svod2_rf', 'v_svod2_go_export')", + None, + ) + assert sorted((r.proname, r.pronargs) for r in rows) == [ + ("v_svod2_go", 11), ("v_svod2_go_export", 4), ("v_svod2_rf", 11)] + + +@pytest.mark.parametrize("fn", ["v_svod2_go", "v_svod2_rf"]) +def test_page_total_count_and_order(svod2_sql, svod2_data, admin_tokens, fn): + # admin_tokens: запрос к API нужен, чтобы откат сессии клиента сработал (loop_ref задаётся при обращении к get_db) + full = svod2_sql(f"SELECT source_line_id FROM v3.{fn}(2031)", None) + page = svod2_sql( + f"SELECT source_line_id, total_count FROM v3.{fn}(2031, NULL, 0, 2, 'org_name', 'asc', " + "NULL, NULL, NULL, NULL, false)", None) + assert [r.source_line_id for r in page] == [r.source_line_id for r in full][:2] + assert {r.total_count for r in page} == {len(full)} + + +def test_facets_totals(svod2_sql, svod2_data, admin_tokens): + rows = {(r.kind, r.value): r.n for r in svod2_sql( + "SELECT * FROM v3.v_svod2_facets('GO', 2031, NULL, NULL, NULL, NULL, NULL)", None)} + assert rows[("total", None)] == rows[("search_total", None)] == len( + svod2_sql("SELECT 1 FROM v3.v_svod2_go(2031)", None)) + + +SEARCHES = ["", "аренда", "АРЕНДА", "ё", "1234.5", "1234.50", " кв ", "1", "true", "777.25", "нет такого"] + + +@pytest.mark.parametrize("sheet,fn", [("GO", "v_svod2_go"), ("RF", "v_svod2_rf")]) +@pytest.mark.parametrize("search", SEARCHES) +def test_search_matches_screen_oracle(svod2_sql, svod2_data, client, admin_tokens, auth_headers, + sheet, fn, search): + from tests.svod2_oracle import screen_rows + + rows = legacy_full(client, sheet, auth_headers(admin_tokens)) + expected = [r["data"]["line_id"] for r in screen_rows(rows, search)] + got = svod2_sql( + f"SELECT source_line_id, total_count FROM v3.{fn}(2031, NULL, NULL, NULL, 'org_name', 'asc', :s)", + {"s": search or None}, + ) + assert sorted(r.source_line_id for r in got) == sorted(expected) + if got: + assert {r.total_count for r in got} == {len(expected)} + + +@pytest.mark.parametrize("sheet", ["GO", "RF"]) +@pytest.mark.parametrize("search,smeta_type", [("", None), ("1", "кв "), ("аренда", "АХР"), ("", "Операц")]) +def test_facets_match_screen_oracle(svod2_sql, svod2_data, client, admin_tokens, auth_headers, + sheet, search, smeta_type): + from tests.svod2_oracle import screen_facets + + rows = legacy_full(client, sheet, auth_headers(admin_tokens)) + filters = {"smeta_type": smeta_type} if smeta_type else {} + expected = screen_facets(rows, search, filters) + got = svod2_sql( + "SELECT kind, value, n FROM v3.v_svod2_facets(:sh, 2031, NULL, :s, :t, NULL, NULL)", + {"sh": sheet, "s": search or None, "t": smeta_type}, + ) + assert next(r.n for r in got if r.kind == "total") == expected["total"] + assert next(r.n for r in got if r.kind == "search_total") == expected["search_total"] + for col in ("smeta_type", "smeta_direction", "org_name"): + assert {r.value: r.n for r in got if r.kind == col} == expected[col] diff --git a/api/tests/unit/test_summary2_related_records_sql.py b/api/tests/unit/test_summary2_related_records_sql.py index 04bfd39..c403dd5 100644 --- a/api/tests/unit/test_summary2_related_records_sql.py +++ b/api/tests/unit/test_summary2_related_records_sql.py @@ -6,7 +6,9 @@ import unittest from pathlib import Path -SQL_PATH = Path(__file__).resolve().parents[2] / "alembic/versions/sql/0049_summary2_related_records.sql" +SQL_DIR = Path(__file__).resolve().parents[2] / "alembic/versions/sql" +SQL_PATH = SQL_DIR / "0053_svod2_server_paging.sql" +EXPORT_SQL_PATH = SQL_DIR / "0049_summary2_related_records.sql" TABLES = ( "plan", "reserve", "contract_summary", "allocation", "collegial_approval", "ckk", "contract_detail", "sequestration", "booking", "budget_line_quarter", @@ -59,8 +61,14 @@ class Summary2RelatedRecordsSQLTest(unittest.TestCase): self.db.execute(f"CREATE TABLE v3.{table} ({columns})") # Execute the eligibility SELECTs, adapting PostgreSQL casts, arrays and regex syntax. self.queries = [] - for part in SQL_PATH.read_text().split("eligible_lines AS (")[1:]: - query = part.split("), ", 1)[0].replace("::varchar", "") + text = SQL_PATH.read_text() + match = re.search(r"-- svod2_lines eligible: begin\n(.*?)\n\s*-- svod2_lines eligible: end", text, re.S) + self.assertIsNotNone(match) + bodies = [match.group(1).replace("p_sheet", f"'{sheet}'") for sheet in ("GO", "RF")] + # Экспорт ГО по-прежнему из 0049: его отбор эта миграция не меняет. + export = EXPORT_SQL_PATH.read_text().split("eligible_lines AS (")[-1].split("), ", 1)[0] + for query in bodies + [export]: + query = query.replace("::varchar", "") query = re.sub(r"(presence_\w+\.\w+) ~ '\[\^\[:space:\]\]'", r"has_text(\1)", query) query = query.replace("p_year", ":year")