perf(svod2): ускорен серверный поиск свода 2 без изменения результата
Поиск в v3.svod2_lines строил jsonb header || contract_summary || contract_detail на каждую строку, разворачивал его в jsonb_each и прогонял каждое значение через svod2_casefold, а числа через svod2_js_number_text. Теперь: - разбор поисковой строки (числа возможны, нет U+0001, нужные пары таблицы регистра) делается один раз в CTE q; раньше при подстановке SQL-функции выражение нормализации p_search вычислялось на каждое значение каждой строки; - значения берутся напрямую из колонок, без jsonb; склеиваются и проверяются одним strpos через разделитель U+0001 (со строкой поиска, где есть U+0001, значения проверяются по одному); - регистр данных приводится только по парам таблицы, чей результат есть в строке поиска; - числа в текст JS переводятся, только если все символы строки возможны в записи числа, и только если по остальным значениям совпадения нет; нули без обращения к plpgsql; - v_svod2_go и v_svod2_rf с SET jit = off (JIT плана добавлял сотни миллисекунд). Тесты: поиск svod2_lines равен прежнему поиску по jsonb (эталонный SQL в тесте) на обоих листах для 55 строк, включая числа, экспоненту, NaN и Infinity, стык значений, U+0001, ненормализованные строки; состав колонок contract_* закреплён тестом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
952627c653
commit
c8e957b13d
@ -1,15 +1,40 @@
|
|||||||
-- AURORA-1843, миграция 0053, часть 1: нормализаторы поиска как в JS и компактная проекция строки свода 2.
|
-- AURORA-1843, миграция 0053, часть 1: нормализаторы поиска как в JS и компактная проекция строки свода 2.
|
||||||
|
|
||||||
-- Регистр без зависимости от локали: явная таблица translate (ASCII, кириллица, Ґ, латиница-1 без U+00D7).
|
-- Регистр без зависимости от локали: явная таблица translate (ASCII, кириллица, Ґ, латиница-1 без U+00D7).
|
||||||
|
-- Таблица в двух IMMUTABLE-функциях (свернутся в константы при планировании): svod2_lines берёт из неё
|
||||||
|
-- только пары, нужные для поиска.
|
||||||
|
CREATE OR REPLACE FUNCTION v3.svod2_casefold_from()
|
||||||
|
RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $function$
|
||||||
|
SELECT E'\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u0419\u041A\u041B\u041C\u041D\u041E\u041F\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\u042A\u042B\u042C\u042D\u042E\u042F\u0400\u0401\u0402\u0403\u0404\u0405\u0406\u0407\u0408\u0409\u040A\u040B\u040C\u040D\u040E\u040F\u0490\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE'
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.svod2_casefold_to()
|
||||||
|
RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $function$
|
||||||
|
SELECT E'\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\u0439\u043A\u043B\u043C\u043D\u043E\u043F\u0440\u0441\u0442\u0443\u0444\u0445\u0446\u0447\u0448\u0449\u044A\u044B\u044C\u044D\u044E\u044F\u0450\u0451\u0452\u0453\u0454\u0455\u0456\u0457\u0458\u0459\u045A\u045B\u045C\u045D\u045E\u045F\u0491\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE'
|
||||||
|
$function$;
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.svod2_casefold(x text)
|
CREATE OR REPLACE FUNCTION v3.svod2_casefold(x text)
|
||||||
RETURNS text
|
RETURNS text
|
||||||
LANGUAGE sql IMMUTABLE
|
LANGUAGE sql IMMUTABLE
|
||||||
AS $function$
|
AS $function$
|
||||||
SELECT translate(
|
SELECT translate(x, v3.svod2_casefold_from(), v3.svod2_casefold_to())
|
||||||
x,
|
$function$;
|
||||||
E'\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u0419\u041A\u041B\u041C\u041D\u041E\u041F\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\u042A\u042B\u042C\u042D\u042E\u042F\u0400\u0401\u0402\u0403\u0404\u0405\u0406\u0407\u0408\u0409\u040A\u040B\u040C\u040D\u040E\u040F\u0490\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE',
|
|
||||||
E'\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u0430\u0431\u0432\u0433\u0434\u0435\u0436\u0437\u0438\u0439\u043A\u043B\u043C\u043D\u043E\u043F\u0440\u0441\u0442\u0443\u0444\u0445\u0446\u0447\u0448\u0449\u044A\u044B\u044C\u044D\u044E\u044F\u0450\u0451\u0452\u0453\u0454\u0455\u0456\u0457\u0458\u0459\u045A\u045B\u045C\u045D\u045E\u045F\u0491\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE'
|
-- Регистр данных для поиска по уже нормализованной строке p_search: достаточно заменить символы, чьё
|
||||||
)
|
-- приведение даёт символ из p_search (остальные не могут совпасть ни с одним символом p_search), а при
|
||||||
|
-- p_full (p_search не нормализована) берётся полная таблица. Результат strpos(.., p_search) тот же,
|
||||||
|
-- что у svod2_casefold, но без прохода по всей таблице на каждый символ значения.
|
||||||
|
CREATE OR REPLACE FUNCTION v3.svod2_casefold_for_search(x text, p_full boolean, p_from text, p_to text)
|
||||||
|
RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $function$
|
||||||
|
SELECT CASE WHEN p_full THEN v3.svod2_casefold(x)
|
||||||
|
WHEN p_from IS NULL THEN x
|
||||||
|
ELSE translate(x, p_from, p_to) END
|
||||||
$function$;
|
$function$;
|
||||||
|
|
||||||
-- Обрезка как String.prototype.trim: ECMAScript WhiteSpace и LineTerminator.
|
-- Обрезка как String.prototype.trim: ECMAScript WhiteSpace и LineTerminator.
|
||||||
@ -240,9 +265,39 @@ $function$;
|
|||||||
DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[], integer, integer, varchar, varchar);
|
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);
|
DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[], integer, integer, varchar, varchar);
|
||||||
|
|
||||||
|
-- Число как в поиске по экрану: ноль без обращения к plpgsql (большинство сумм - нули), иначе запись JS.
|
||||||
|
CREATE OR REPLACE FUNCTION v3.svod2_num_text(n numeric)
|
||||||
|
RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $function$
|
||||||
|
SELECT CASE WHEN n IS NULL THEN NULL
|
||||||
|
WHEN n = 0 THEN '0'
|
||||||
|
ELSE v3.svod2_js_number_text(n::float8) END
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
-- Есть ли в значениях подстрока p_s (нормализованная). Одна склейка и один strpos: искомая строка без
|
||||||
|
-- U+0001 не может перекрыть границу значений, ложного совпадения на стыке нет (U+0001 внутри самих
|
||||||
|
-- значений безвреден). Со строкой, где есть U+0001, каждое значение проверяется отдельно, как matchesSearch.
|
||||||
|
CREATE OR REPLACE FUNCTION v3.svod2_vals_match(
|
||||||
|
vals text[], p_s text, p_sep_ok boolean, p_full boolean, p_from text, p_to text)
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE sql IMMUTABLE
|
||||||
|
AS $function$
|
||||||
|
SELECT CASE WHEN p_sep_ok
|
||||||
|
THEN strpos(v3.svod2_casefold_for_search(array_to_string(vals, E'\u0001'), p_full, p_from, p_to), p_s) > 0
|
||||||
|
ELSE EXISTS (SELECT 1 FROM unnest(vals) AS u(x)
|
||||||
|
WHERE strpos(v3.svod2_casefold_for_search(u.x, p_full, p_from, p_to), p_s) > 0)
|
||||||
|
END
|
||||||
|
$function$;
|
||||||
|
|
||||||
-- Общий отбор строк свода 2 для страниц и вариантов фильтров. Условия отбора как в 0049,
|
-- Общий отбор строк свода 2 для страниц и вариантов фильтров. Условия отбора как в 0049,
|
||||||
-- форма выбирается по p_sheet; p_search (уже нормализованный) отсекает строки без совпадения
|
-- форма выбирается по p_sheet; p_search (уже нормализованный) отсекает строки без совпадения
|
||||||
-- по полям header, contract_summary и contract_detail, как matchesSearch экрана.
|
-- по значениям header, contract_summary и contract_detail, как matchesSearch экрана.
|
||||||
|
-- Значения берутся напрямую из колонок, без jsonb. Набор повторяет ключи прежнего
|
||||||
|
-- header || contract_summary || contract_detail (при совпадении ключей побеждает последний):
|
||||||
|
-- у contract_summary остаются поля, которых нет у contract_detail, а counterparty, reference,
|
||||||
|
-- deadline и comment берутся только из contract_detail (отсутствие строки contract_detail
|
||||||
|
-- затирает их у contract_summary значением null). Состав колонок сверяет тест.
|
||||||
CREATE OR REPLACE FUNCTION v3.svod2_lines(
|
CREATE OR REPLACE FUNCTION v3.svod2_lines(
|
||||||
p_sheet varchar,
|
p_sheet varchar,
|
||||||
p_year integer,
|
p_year integer,
|
||||||
@ -256,10 +311,22 @@ RETURNS TABLE(
|
|||||||
)
|
)
|
||||||
LANGUAGE sql STABLE
|
LANGUAGE sql STABLE
|
||||||
AS $function$
|
AS $function$
|
||||||
WITH empty_sections AS MATERIALIZED (
|
WITH q AS MATERIALIZED (
|
||||||
SELECT
|
-- Разбор поисковой строки один раз на запрос (при подстановке SQL-функции в вызывающий запрос
|
||||||
to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS contract_summary,
|
-- выражение p_search иначе вычислялось бы на каждое значение).
|
||||||
to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS contract_detail
|
-- num_ok: все символы строки возможны в записи числа JS (цифры, . - + e, Infinity, NaN);
|
||||||
|
-- иначе ни одно число не может совпасть, и числа в текст не переводятся;
|
||||||
|
-- sep_ok: в строке нет U+0001, разделителя при склейке значений строки свода;
|
||||||
|
-- fold_*: пары таблицы регистра, чей результат есть в строке (см. svod2_casefold_for_search).
|
||||||
|
SELECT p_search AS s,
|
||||||
|
p_search ~ '^[0-9.+eEinftyaINFTYA-]*$' AS num_ok,
|
||||||
|
strpos(p_search, E'\u0001') = 0 AS sep_ok,
|
||||||
|
v3.svod2_casefold(p_search) <> p_search AS fold_full,
|
||||||
|
m.f AS fold_from, m.t AS fold_to
|
||||||
|
FROM (SELECT string_agg(c.f, '' ORDER BY c.i) AS f, string_agg(c.t, '' ORDER BY c.i) AS t
|
||||||
|
FROM unnest(regexp_split_to_array(v3.svod2_casefold_from(), ''),
|
||||||
|
regexp_split_to_array(v3.svod2_casefold_to(), '')) WITH ORDINALITY AS c(f, t, i)
|
||||||
|
WHERE strpos(p_search, c.t) > 0) m
|
||||||
), eligible_lines AS (
|
), eligible_lines AS (
|
||||||
-- svod2_lines eligible: begin
|
-- svod2_lines eligible: begin
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
@ -380,56 +447,52 @@ WITH empty_sections AS MATERIALIZED (
|
|||||||
SELECT el.line_id, el.form_type_code, el.form_id, el.org_unit_id, el.org_name,
|
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
|
el.section_code, el.item_id, el.smeta_type, el.smeta_direction
|
||||||
FROM eligible_lines el
|
FROM eligible_lines el
|
||||||
WHERE p_search IS NULL OR EXISTS (
|
CROSS JOIN q
|
||||||
|
WHERE q.s IS NULL OR EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM v3.budget_line bl
|
FROM v3.budget_line bl
|
||||||
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
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_summary cs ON cs.line_id = bl.id
|
||||||
LEFT JOIN v3.contract_detail cd ON cd.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
|
LEFT JOIN v3.ckk ck ON ck.line_id = bl.id
|
||||||
CROSS JOIN empty_sections e
|
|
||||||
CROSS JOIN LATERAL (
|
CROSS JOIN LATERAL (
|
||||||
SELECT
|
SELECT
|
||||||
jsonb_build_object(
|
ARRAY[ei.section_code, ei.item_id, ei.num_group_id, COALESCE(bl.name, ei.name),
|
||||||
'section_code', ei.section_code, 'item_id', ei.item_id,
|
bl.justification, bl.internal_order, bl.direction, el.smeta_direction,
|
||||||
'num_group', ei.num_group_id, 'name', COALESCE(bl.name, ei.name),
|
el.form_type_code, el.org_name, el.smeta_type]::text[] AS strs,
|
||||||
'expense_item_id', ei.id, 'justification', bl.justification,
|
CASE WHEN q.num_ok
|
||||||
'internal_order', bl.internal_order, 'vsp_id', bl.vsp_id,
|
THEN ARRAY[ei.id::text, bl.vsp_id::text, bl.project_id::text,
|
||||||
'project_id', bl.project_id, 'direction', bl.direction,
|
el.form_id::text, el.org_unit_id::text]
|
||||||
'smeta_direction', CASE bl.direction
|
END AS ints,
|
||||||
WHEN 'Support' THEN 'Поддержка'
|
CASE WHEN has_contract.v
|
||||||
WHEN 'Development' THEN 'Развитие'
|
THEN ARRAY[cs.centralized_flag, cd.counterparty, cd.reference, cd.addenda, cd.subject,
|
||||||
ELSE NULL
|
cd.currency, cd.rf_schedule, cd.vat_rate, cd.deadline, cd.payment_scheme,
|
||||||
END,
|
cd.act, cd.comment, to_jsonb(cd.contract_date) #>> '{}']::text[]
|
||||||
'source_form', el.form_type_code, 'source_id', el.form_id,
|
END AS rest_s,
|
||||||
'source_org_unit_id', el.org_unit_id, 'org_name', el.org_name,
|
CASE WHEN has_contract.v AND q.num_ok
|
||||||
'smeta_type', el.smeta_type) AS header_j,
|
THEN ARRAY[cs.total_amount, cs.future_payments_y1, cs.future_payments_y2, cs.other_ssp_amount,
|
||||||
COALESCE(to_jsonb(cs) - 'line_id', e.contract_summary) AS cs_j,
|
cd.ceiling_amount, cd.exchange_rate, cd.amount_foreign,
|
||||||
CASE WHEN p_sheet = 'RF'
|
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q1, ck.expenses_q1) ELSE cd.expenses_q1 END,
|
||||||
THEN COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail) || jsonb_build_object(
|
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q2, ck.expenses_q2) ELSE cd.expenses_q2 END,
|
||||||
'expenses_q1', COALESCE(cd.expenses_q1, ck.expenses_q1),
|
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q3, ck.expenses_q3) ELSE cd.expenses_q3 END,
|
||||||
'expenses_q2', COALESCE(cd.expenses_q2, ck.expenses_q2),
|
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q4, ck.expenses_q4) ELSE cd.expenses_q4 END]::numeric[]
|
||||||
'expenses_q3', COALESCE(cd.expenses_q3, ck.expenses_q3),
|
END AS rest_n
|
||||||
'expenses_q4', COALESCE(cd.expenses_q4, ck.expenses_q4))
|
FROM (SELECT cs.line_id IS NOT NULL OR cd.line_id IS NOT NULL
|
||||||
ELSE COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail)
|
OR (p_sheet = 'RF' AND ck.line_id IS NOT NULL) AS v) has_contract
|
||||||
END AS cd_j
|
|
||||||
) s
|
) s
|
||||||
CROSS JOIN LATERAL jsonb_each(s.header_j || s.cs_j || s.cd_j) kv
|
|
||||||
WHERE bl.id = el.line_id
|
WHERE bl.id = el.line_id
|
||||||
AND strpos(
|
AND CASE
|
||||||
v3.svod2_casefold(
|
WHEN v3.svod2_vals_match(s.strs || s.ints || s.rest_s, q.s, q.sep_ok,
|
||||||
CASE jsonb_typeof(kv.value)
|
q.fold_full, q.fold_from, q.fold_to) THEN true
|
||||||
WHEN 'string' THEN kv.value #>> '{}'
|
-- числа переводятся в текст только если по остальным значениям совпадения нет
|
||||||
WHEN 'number' THEN v3.svod2_js_number_text((kv.value #>> '{}')::float8)
|
WHEN s.rest_n IS NOT NULL THEN v3.svod2_vals_match(
|
||||||
WHEN 'boolean' THEN kv.value #>> '{}'
|
ARRAY(SELECT v3.svod2_num_text(n) FROM unnest(s.rest_n) AS u(n)),
|
||||||
WHEN 'null' THEN ''
|
q.s, q.sep_ok, q.fold_full, q.fold_from, q.fold_to)
|
||||||
ELSE kv.value::text
|
ELSE false
|
||||||
END),
|
END
|
||||||
p_search) > 0
|
|
||||||
)
|
)
|
||||||
$function$;
|
$function$;
|
||||||
|
|
||||||
|
|
||||||
-- Страница свода 2 для ГО: поиск и три фильтра на сервере, total_count - число строк до LIMIT.
|
-- Страница свода 2 для ГО: поиск и три фильтра на сервере, total_count - число строк до LIMIT.
|
||||||
CREATE OR REPLACE FUNCTION v3.v_svod2_go(
|
CREATE OR REPLACE FUNCTION v3.v_svod2_go(
|
||||||
p_year integer,
|
p_year integer,
|
||||||
@ -449,7 +512,8 @@ RETURNS TABLE(
|
|||||||
org_name varchar, smeta_type varchar,
|
org_name varchar, smeta_type varchar,
|
||||||
source_line_id integer, sort_order bigint, data jsonb, total_count bigint
|
source_line_id integer, sort_order bigint, data jsonb, total_count bigint
|
||||||
)
|
)
|
||||||
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog'
|
-- jit off: оценка стоимости большого плана включает JIT-компиляцию на сотни миллисекунд, а выигрыша нет.
|
||||||
|
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog' SET jit TO off
|
||||||
AS $function$
|
AS $function$
|
||||||
WITH empty_sections AS MATERIALIZED (
|
WITH empty_sections AS MATERIALIZED (
|
||||||
SELECT
|
SELECT
|
||||||
@ -667,7 +731,8 @@ RETURNS TABLE(
|
|||||||
org_name varchar, smeta_type varchar,
|
org_name varchar, smeta_type varchar,
|
||||||
source_line_id integer, sort_order bigint, data jsonb, total_count bigint
|
source_line_id integer, sort_order bigint, data jsonb, total_count bigint
|
||||||
)
|
)
|
||||||
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog'
|
-- jit off: оценка стоимости большого плана включает JIT-компиляцию на сотни миллисекунд, а выигрыша нет.
|
||||||
|
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog' SET jit TO off
|
||||||
AS $function$
|
AS $function$
|
||||||
WITH empty_sections AS MATERIALIZED (
|
WITH empty_sections AS MATERIALIZED (
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@ -132,3 +132,137 @@ def test_facets_search_subplan_is_not_correlated(svod2_sql):
|
|||||||
outputs = [l for l in plan.splitlines() if "Output:" in l and "SubPlan" in l]
|
outputs = [l for l in plan.splitlines() if "Output:" in l and "SubPlan" in l]
|
||||||
assert any("hashed SubPlan" in l for l in outputs), plan
|
assert any("hashed SubPlan" in l for l in outputs), plan
|
||||||
assert not any(re.search(r"(?<!hashed )SubPlan \d+", l) for l in outputs), plan
|
assert not any(re.search(r"(?<!hashed )SubPlan \d+", l) for l in outputs), plan
|
||||||
|
|
||||||
|
|
||||||
|
# --- Поиск в svod2_lines (ускорение AURORA-1843): результат равен прежнему поиску по jsonb ---
|
||||||
|
|
||||||
|
# Прежний поиск дословно: значения header + contract_summary + contract_detail через jsonb_each,
|
||||||
|
# регистр svod2_casefold по каждому значению, числа через svod2_js_number_text.
|
||||||
|
REFERENCE_SEARCH_SQL = """
|
||||||
|
SELECT l.line_id
|
||||||
|
FROM v3.svod2_lines(:sh, 2031, NULL, NULL) l
|
||||||
|
WHERE 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 LATERAL (
|
||||||
|
SELECT
|
||||||
|
to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS cs0,
|
||||||
|
to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS cd0
|
||||||
|
) 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', l.form_type_code, 'source_id', l.form_id,
|
||||||
|
'source_org_unit_id', l.org_unit_id, 'org_name', l.org_name,
|
||||||
|
'smeta_type', l.smeta_type) AS header_j,
|
||||||
|
COALESCE(to_jsonb(cs) - 'line_id', e.cs0) AS cs_j,
|
||||||
|
CASE WHEN :sh = 'RF'
|
||||||
|
THEN COALESCE(to_jsonb(cd) - 'line_id', e.cd0) || 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.cd0)
|
||||||
|
END AS cd_j
|
||||||
|
) s
|
||||||
|
CROSS JOIN LATERAL jsonb_each(s.header_j || s.cs_j || s.cd_j) kv
|
||||||
|
WHERE bl.id = l.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),
|
||||||
|
:s) > 0
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Данные для границ ускорения: соседние значения строки свода (name и justification идут подряд при склейке),
|
||||||
|
# U+0001 внутри значения, числа с экспонентой и отрицательные, ckk без contract_detail, дата.
|
||||||
|
SEARCH_EXTRA_DATA = [
|
||||||
|
"UPDATE v3.budget_line SET name = 'ab', justification = 'cd' WHERE id = 910001",
|
||||||
|
"UPDATE v3.budget_line SET internal_order = E'x\\u0001y' WHERE id = 910004",
|
||||||
|
"INSERT INTO v3.contract_detail (line_id, counterparty, reference, ceiling_amount, expenses_q1, expenses_q2, "
|
||||||
|
"exchange_rate, amount_foreign, contract_date, act) VALUES "
|
||||||
|
"(910007, 'ЗАО Эй', 'Ref-А', 15000000000000000000000000, 12.5, 0, 0.0000001, -0.0000001234, '2031-03-04', 'Infinity')",
|
||||||
|
"UPDATE v3.contract_detail SET exchange_rate = 'NaN', amount_foreign = '-Infinity' WHERE line_id = 910002",
|
||||||
|
"INSERT INTO v3.ckk (line_id, ceiling_amount, expenses_q1, expenses_q2) VALUES (910009, 1, 555.5, 6)",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Нормализованные (как приходят от v_svod2_*) и ненормализованные строки; по ветвям ускорения:
|
||||||
|
# только символы числа (числа переводятся в текст) и нет (не переводятся), есть/нет пар таблицы регистра,
|
||||||
|
# U+0001 в строке, стык значений, пустая строка.
|
||||||
|
SEARCH_EQUIV = [
|
||||||
|
"", "аренд", "ё", "ёлк", "зао", "эй", "ref-а", "ооо", "д-", "zzzz", "а",
|
||||||
|
"1", "0", "-", ".", "e", "+", "e+25", "1.5e+25", "1e-7", "-1.234e-7", "-1", "555.5", "777.25", "12.5", "1234.5",
|
||||||
|
"infinity", "nan", "a", "nt", "inf", "2031-03-04", "03-04", "12",
|
||||||
|
"abcd", "ab", "cd", "bc", "abc", "ab\u0001cd", "x\u0001y", "x", "y", "\u0001", "y\u0001", "\u0001x", "аренд\u0001",
|
||||||
|
"АРЕНД", "Ё", "E", "Infinity", "ЭЙ", "Ab", "REF",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_columns_are_those_known_to_svod2_lines(svod2_sql):
|
||||||
|
# svod2_lines перечисляет значения для поиска явно (по колонкам, без jsonb). Новая колонка в
|
||||||
|
# contract_summary/contract_detail не попадёт в поиск, пока её не добавят в svod2_lines: тест просит это сделать.
|
||||||
|
# Из contract_summary в поиск идут только колонки, которых нет в contract_detail: прежний
|
||||||
|
# header || contract_summary || contract_detail затирал одноимённые.
|
||||||
|
def columns(table: str) -> set[str]:
|
||||||
|
return {r[0] for r in svod2_sql(
|
||||||
|
"SELECT column_name FROM information_schema.columns "
|
||||||
|
"WHERE table_schema = 'v3' AND table_name = :t", {"t": table})}
|
||||||
|
|
||||||
|
assert columns("contract_summary") == {
|
||||||
|
"line_id", "total_amount", "counterparty", "reference", "deadline", "comment",
|
||||||
|
"future_payments_y1", "future_payments_y2", "other_ssp_amount", "centralized_flag"}
|
||||||
|
assert columns("contract_detail") == {
|
||||||
|
"line_id", "counterparty", "reference", "addenda", "subject", "currency", "ceiling_amount",
|
||||||
|
"expenses_q1", "expenses_q2", "expenses_q3", "expenses_q4", "rf_schedule", "vat_rate",
|
||||||
|
"exchange_rate", "amount_foreign", "deadline", "payment_scheme", "act", "comment", "contract_date"}
|
||||||
|
# ключи header не пересекаются с колонками contract_*
|
||||||
|
header_keys = {
|
||||||
|
"section_code", "item_id", "num_group", "name", "expense_item_id", "justification",
|
||||||
|
"internal_order", "vsp_id", "project_id", "direction", "smeta_direction", "source_form",
|
||||||
|
"source_id", "source_org_unit_id", "org_name", "smeta_type"}
|
||||||
|
assert not header_keys & (columns("contract_summary") | columns("contract_detail"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sheet", ["GO", "RF"])
|
||||||
|
@pytest.mark.parametrize("search", SEARCH_EQUIV, ids=[repr(s) for s in SEARCH_EQUIV])
|
||||||
|
def test_svod2_lines_search_equals_reference(svod2_sql, svod2_data, sheet, search):
|
||||||
|
for statement in SEARCH_EXTRA_DATA:
|
||||||
|
svod2_sql(statement)
|
||||||
|
got = svod2_sql(
|
||||||
|
"SELECT line_id FROM v3.svod2_lines(:sh, 2031, NULL, :s)", {"sh": sheet, "s": search})
|
||||||
|
expected = svod2_sql(REFERENCE_SEARCH_SQL, {"sh": sheet, "s": search})
|
||||||
|
assert sorted(r[0] for r in got) == sorted(r[0] for r in expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_svod2_lines_search_extra_data_is_searchable(svod2_sql, svod2_data):
|
||||||
|
# Страховка от пустых сравнений: на этих данных поиск что-то находит и что-то отсекает.
|
||||||
|
for statement in SEARCH_EXTRA_DATA:
|
||||||
|
svod2_sql(statement)
|
||||||
|
|
||||||
|
def found(sheet: str, search: str) -> int:
|
||||||
|
return len(svod2_sql(
|
||||||
|
"SELECT line_id FROM v3.svod2_lines(:sh, 2031, NULL, :s)", {"sh": sheet, "s": search}))
|
||||||
|
|
||||||
|
assert found("GO", "ab") == 1 and found("GO", "cd") == 1
|
||||||
|
assert found("GO", "abcd") == 0 and found("GO", "ab\u0001cd") == 0 # стык значений
|
||||||
|
assert found("GO", "x\u0001y") == 1 # U+0001 внутри значения
|
||||||
|
assert found("RF", "e+25") == 1 and found("RF", "-1.234e-7") == 1 # число как в JS
|
||||||
|
assert found("RF", "555.5") == 1 # ckk без contract_detail
|
||||||
|
assert found("RF", "zzzz") == 0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user