From e81df1d71e9a298915160cd87746eb6ee146c16e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:50:04 +0400 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D1=81=D0=B2=D0=BE=D0=B4=202=20?= =?UTF-8?q?=E2=80=94=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD=D1=82=D1=8B=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=BE=D0=B2=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BB=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=BF=D0=BE=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0,=20=D0=BE=D1=82=D0=BA=D0=B0=D1=82=20=D0=B7?= =?UTF-8?q?=D0=B0=D1=81=D0=B5=D0=B2=D0=B0=20=D0=B2=20teardown?= 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 | 5 ++++- api/tests/integration/conftest.py | 5 ++++- api/tests/integration/test_svod2_sql.py | 16 +++++++++++++++- 3 files changed, 23 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 e30e602..d2162c2 100644 --- a/api/alembic/versions/sql/0053_svod2_server_paging.sql +++ b/api/alembic/versions/sql/0053_svod2_server_paging.sql @@ -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_org_name)), '') AS f_org ), lines AS MATERIALIZED ( + -- Подзапрос IN не должен ссылаться на столбцы q/l (иначе он коррелированный и считается + -- на каждую строку): параметр нормализуется заново, тогда это hashed SubPlan, один раз. 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, + 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 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 diff --git a/api/tests/integration/conftest.py b/api/tests/integration/conftest.py index 6879c10..da6ba76 100644 --- a/api/tests/integration/conftest.py +++ b/api/tests/integration/conftest.py @@ -123,7 +123,7 @@ def svod2_data(client, svod2_sql) -> dict: # id строки contract_detail: contract_detail.line_id = 910002, технический id, # который попадает в шапку строки, — expense_item_id этой строки cd_id = svod2_sql("SELECT expense_item_id FROM v3.budget_line WHERE id = 910002")[0][0] - return { + yield { "year": SVOD2_YEAR, "org_a": 910001, "org_b": 910002, @@ -132,3 +132,6 @@ def svod2_data(client, svod2_sql) -> dict: "cd_id": cd_id, "ckk_only_value": "777.25", } + # откат, пока portal открыт: иначе при отсутствии запросов к API транзакция засева + # остаётся висеть и блокирует следующий засев + client.portal.call(client.db_session.rollback) diff --git a/api/tests/integration/test_svod2_sql.py b/api/tests/integration/test_svod2_sql.py index b403ae0..c51da20 100644 --- a/api/tests/integration/test_svod2_sql.py +++ b/api/tests/integration/test_svod2_sql.py @@ -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)] 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}, + {"s": search}, ) assert sorted(r.source_line_id for r in got) == sorted(expected) 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"] for col in ("smeta_type", "smeta_direction", "org_name"): 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"(?