fix(api): свод 2 — варианты фильтров без коррелированного подзапроса поиска, откат засева в teardown

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-28 21:50:04 +04:00
parent 719f568863
commit e81df1d71e
3 changed files with 23 additions and 3 deletions

View File

@ -879,9 +879,12 @@ WITH q AS MATERIALIZED (
NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '') AS f_dir, 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 NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '') AS f_org
), lines AS MATERIALIZED ( ), lines AS MATERIALIZED (
-- Подзапрос IN не должен ссылаться на столбцы q/l (иначе он коррелированный и считается
-- на каждую строку): параметр нормализуется заново, тогда это hashed SubPlan, один раз.
SELECT l.smeta_type, l.smeta_direction, l.org_name, SELECT l.smeta_type, l.smeta_direction, l.org_name,
(q.s IS NULL OR l.line_id IN ( (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, SELECT m.line_id FROM v3.svod2_lines(p_sheet, p_year, p_org_unit_ids,
NULLIF(v3.svod2_casefold(v3.svod2_js_trim(p_search)), '')) m)) AS hit,
(q.f_type IS NULL OR (l.smeta_type IS NOT NULL (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, 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 (q.f_dir IS NULL OR (l.smeta_direction IS NOT NULL

View File

@ -123,7 +123,7 @@ def svod2_data(client, svod2_sql) -> dict:
# id строки contract_detail: contract_detail.line_id = 910002, технический id, # id строки contract_detail: contract_detail.line_id = 910002, технический id,
# который попадает в шапку строки, — expense_item_id этой строки # который попадает в шапку строки, — expense_item_id этой строки
cd_id = svod2_sql("SELECT expense_item_id FROM v3.budget_line WHERE id = 910002")[0][0] cd_id = svod2_sql("SELECT expense_item_id FROM v3.budget_line WHERE id = 910002")[0][0]
return { yield {
"year": SVOD2_YEAR, "year": SVOD2_YEAR,
"org_a": 910001, "org_a": 910001,
"org_b": 910002, "org_b": 910002,
@ -132,3 +132,6 @@ def svod2_data(client, svod2_sql) -> dict:
"cd_id": cd_id, "cd_id": cd_id,
"ckk_only_value": "777.25", "ckk_only_value": "777.25",
} }
# откат, пока portal открыт: иначе при отсутствии запросов к API транзакция засева
# остаётся висеть и блокирует следующий засев
client.portal.call(client.db_session.rollback)

View File

@ -94,7 +94,7 @@ def test_search_matches_screen_oracle(svod2_sql, svod2_data, client, admin_token
expected = [r["data"]["line_id"] for r in screen_rows(rows, search)] expected = [r["data"]["line_id"] for r in screen_rows(rows, search)]
got = svod2_sql( got = svod2_sql(
f"SELECT source_line_id, total_count FROM v3.{fn}(2031, NULL, NULL, NULL, 'org_name', 'asc', :s)", f"SELECT source_line_id, total_count FROM v3.{fn}(2031, NULL, NULL, NULL, 'org_name', 'asc', :s)",
{"s": search or None}, {"s": search},
) )
assert sorted(r.source_line_id for r in got) == sorted(expected) assert sorted(r.source_line_id for r in got) == sorted(expected)
if got: if got:
@ -118,3 +118,17 @@ def test_facets_match_screen_oracle(svod2_sql, svod2_data, client, admin_tokens,
assert next(r.n for r in got if r.kind == "search_total") == expected["search_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"): for col in ("smeta_type", "smeta_direction", "org_name"):
assert {r.value: r.n for r in got if r.kind == col} == expected[col] assert {r.value: r.n for r in got if r.kind == col} == expected[col]
def test_facets_search_subplan_is_not_correlated(svod2_sql):
import re
plan = "\n".join(r[0] for r in svod2_sql(
"EXPLAIN (VERBOSE, COSTS OFF) "
"SELECT * FROM v3.v_svod2_facets('GO', 2031, NULL, 'аренда', NULL, NULL, NULL)", None))
# IN по svod2_lines должен быть hashed SubPlan (считается один раз); коррелированный
# считался бы на каждую строку. Не-hashed SubPlan в Filter внутри svod2_lines (EXISTS поиска)
# законен, поэтому смотрим только на выражения в Output.
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