Merge #241: ускоренный серверный поиск свода 2 (c8e957b)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-29 14:28:29 +04:00
commit eb7efa2d34
2 changed files with 248 additions and 49 deletions

View File

@ -1,15 +1,40 @@
-- AURORA-1843, миграция 0053, часть 1: нормализаторы поиска как в JS и компактная проекция строки свода 2.
-- Регистр без зависимости от локали: явная таблица 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)
RETURNS text
LANGUAGE sql IMMUTABLE
AS $function$
SELECT translate(
x,
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'
)
SELECT translate(x, v3.svod2_casefold_from(), v3.svod2_casefold_to())
$function$;
-- Регистр данных для поиска по уже нормализованной строке 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$;
-- Обрезка как 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_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,
-- форма выбирается по 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(
p_sheet varchar,
p_year integer,
@ -256,10 +311,22 @@ RETURNS TABLE(
)
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
WITH q AS MATERIALIZED (
-- Разбор поисковой строки один раз на запрос (при подстановке SQL-функции в вызывающий запрос
-- выражение p_search иначе вычислялось бы на каждое значение).
-- 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 (
-- svod2_lines eligible: begin
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,
el.section_code, el.item_id, el.smeta_type, el.smeta_direction
FROM eligible_lines el
WHERE p_search IS NULL OR EXISTS (
CROSS JOIN q
WHERE q.s 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
ARRAY[ei.section_code, ei.item_id, ei.num_group_id, COALESCE(bl.name, ei.name),
bl.justification, bl.internal_order, bl.direction, el.smeta_direction,
el.form_type_code, el.org_name, el.smeta_type]::text[] AS strs,
CASE WHEN q.num_ok
THEN ARRAY[ei.id::text, bl.vsp_id::text, bl.project_id::text,
el.form_id::text, el.org_unit_id::text]
END AS ints,
CASE WHEN has_contract.v
THEN ARRAY[cs.centralized_flag, cd.counterparty, cd.reference, cd.addenda, cd.subject,
cd.currency, cd.rf_schedule, cd.vat_rate, cd.deadline, cd.payment_scheme,
cd.act, cd.comment, to_jsonb(cd.contract_date) #>> '{}']::text[]
END AS rest_s,
CASE WHEN has_contract.v AND q.num_ok
THEN ARRAY[cs.total_amount, cs.future_payments_y1, cs.future_payments_y2, cs.other_ssp_amount,
cd.ceiling_amount, cd.exchange_rate, cd.amount_foreign,
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q1, ck.expenses_q1) ELSE cd.expenses_q1 END,
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q2, ck.expenses_q2) ELSE cd.expenses_q2 END,
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q3, ck.expenses_q3) ELSE cd.expenses_q3 END,
CASE WHEN p_sheet = 'RF' THEN COALESCE(cd.expenses_q4, ck.expenses_q4) ELSE cd.expenses_q4 END]::numeric[]
END AS rest_n
FROM (SELECT cs.line_id IS NOT NULL OR cd.line_id IS NOT NULL
OR (p_sheet = 'RF' AND ck.line_id IS NOT NULL) AS v) has_contract
) 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
AND CASE
WHEN v3.svod2_vals_match(s.strs || s.ints || s.rest_s, q.s, q.sep_ok,
q.fold_full, q.fold_from, q.fold_to) THEN true
-- числа переводятся в текст только если по остальным значениям совпадения нет
WHEN s.rest_n IS NOT NULL THEN v3.svod2_vals_match(
ARRAY(SELECT v3.svod2_num_text(n) FROM unnest(s.rest_n) AS u(n)),
q.s, q.sep_ok, q.fold_full, q.fold_from, q.fold_to)
ELSE false
END
)
$function$;
-- Страница свода 2 для ГО: поиск и три фильтра на сервере, total_count - число строк до LIMIT.
CREATE OR REPLACE FUNCTION v3.v_svod2_go(
p_year integer,
@ -449,7 +512,8 @@ RETURNS TABLE(
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'
-- jit off: оценка стоимости большого плана включает JIT-компиляцию на сотни миллисекунд, а выигрыша нет.
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog' SET jit TO off
AS $function$
WITH empty_sections AS MATERIALIZED (
SELECT
@ -667,7 +731,8 @@ RETURNS TABLE(
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'
-- jit off: оценка стоимости большого плана включает JIT-компиляцию на сотни миллисекунд, а выигрыша нет.
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog' SET jit TO off
AS $function$
WITH empty_sections AS MATERIALIZED (
SELECT

View File

@ -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]
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
# --- Поиск в 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