fix(maintenance): survive sqlparse token cap on huge virtual dataset SQL

Discovery of virtual datasets now works, but a runtime blocker remained: any
virtual dataset whose SQL exceeds sqlparse's MAX_GROUPING_TOKENS (10000 tokens)
raised SQLParseError 'Maximum number of tokens exceeded (10000)' from
extract_tables_from_sql_span, which is called unguarded in the scan loop — one
oversized virtual dataset aborted the whole maintenance preview/start.

- extract_tables_from_sql_span now wraps sqlparse.parse + token walk in
  try/except and falls back to regex-only extraction (keeping all schema.table
  matches) instead of raising, so huge SQL no longer fails the scan.
- Tier-1 virtual filter uses value "" (not None) so the sql is_not_null filter
  passes Superset's rison schema instead of always falling back to a full scan.

Tests: huge-SQL fallback (extractor) and huge-virtual-dataset scan resilience
(scanner). ADR-0020 updated with Decision 3.
This commit is contained in:
2026-08-03 23:48:01 +07:00
parent 52e909a2da
commit 02a97bfc9c
6 changed files with 133 additions and 28 deletions

View File

@@ -52,7 +52,7 @@
```
Tier 1 (предпочтительно, маленький результат):
GET /dataset/?q={"columns":["id","table_name","schema","sql"],
"filters":[{"col":"sql","opr":"is_not_null","value":None}]}
"filters":[{"col":"sql","opr":"is_not_null","value":""}]}
Tier 2 (fallback, клиентский скан), если Tier 1 отклонён/упал:
GET /dataset/?q={"columns":["id","table_name","schema","sql"]}
@@ -85,6 +85,14 @@ Tier 2 (fallback, клиентский скан), если Tier 1 отклонё
обнаружение виртуальности строится на `sql`, а не на флаге. Клиентская проверка
`is_virtual` в цикле сопоставления уже опирается на непустой `sql`.
### Примечание по `value` в фильтре
`value: None` в rison-фильтре невалидно (схема требует number/string/boolean/array),
Superset отклоняет его HTTP 400. Для `is_not_null` значение игнорируется оператором,
поэтому используется `value: ""` — валидная строка, которую `col.isnot(None)` не
использует. Это позволяет Tier 1 работать (маленький серверный фильтр), а не каждый
раз откатываться к полному клиентскому скану.
---
## Decision 2: AsyncAPIClient.request поднимает не-2xx ответы
@@ -103,6 +111,35 @@ Tier 2 (fallback, клиентский скан), если Tier 1 отклонё
---
## Decision 3: sqlparse token-limit fallback при извлечении таблиц из SQL
### Проблема
После починки обнаружения виртуальных датасетов runtime-сбой сместился в извлечение
таблиц. `sql_table_extractor.extract_tables_from_sql_span()` вызывает
`sqlparse.parse(sql_text)`; для SQL, у которого больше `MAX_GROUPING_TOKENS = 10000`
токенов, sqlparse поднимает `SQLParseError: "Maximum number of tokens exceeded (10000)"`.
В `find_affected_dashboards` вызов `extract_tables_from_sql(ds_sql)` в цикле по всем
виртуальным датасетам **не был обёрнут в try/except**, поэтому один гигантский
виртуальный датасет срывал всё обнаружение (`preview-dashboards` → 502,
`start_maintenance` → discovery failed). Это и есть «капа слишком маленькая» из логов.
### Решение
`extract_tables_from_sql_span()` оборачивает `sqlparse.parse` + обход токенов в
`try/except`. При ошибке (в т.ч. token-limit) список строковых литералов остаётся
пустым → `is_in_string()` возвращает False → сохраняются **все** regex-совпадения
`schema.table`. Это best-effort сопоставление (допускает возможные false positives из
строковых литералов) вместо жёсткого отказа всего скана.
### Отклонённая альтернатива
Поднимать `MAX_GROUPING_TOKENS` в `sqlparse` (монакий-патч или правка константы) —
хрупко и влияет на производительность парсинга для всех датасетов. Обход только
конкретного сбоя безопаснее.
---
## Последствия
1. **Виртуальные датасеты теперь обнаруживаются.** Физические + виртуальные совпадения
@@ -119,11 +156,17 @@ Tier 2 (fallback, клиентский скан), если Tier 1 отклонё
pagination cap (best-effort: при капе виртуальное совпадение пропускается, физическое
сохраняется).
4. **Проверяемость.** Корневая причина подтверждена по исходникам Superset
4. **Гигантские виртуальные датасеты не валят discovery.** `extract_tables_from_sql_span`
при token-limit sqlparse (10000) откатывается к regex-only, а не бросает исключение.
Один большой виртуальный датасет больше не срывает `preview-dashboards`/`start_maintenance`.
Покрыто тестами: `tests/test_sql_table_extractor.py::test_huge_sql_falls_back_to_regex_instead_of_raising`
и `tests/services/maintenance/test_dashboard_scanner.py::test_huge_virtual_dataset_sql_does_not_fail_discovery`.
5. **Проверяемость.** Корневая причина подтверждена по исходникам Superset
(`superset/datasets/api.py: search_columns` и `list_columns`; `is_sqllab_view`
модельная колонка в `connectors/sqla/models.py`). Механизм покрыт тестами в
`backend/tests/services/maintenance/test_dashboard_scanner.py` (включая end-to-end с
реальным `sql_table_extractor` и реальным SQL из продакшн-сценария) и
`backend/tests/test_core/test_async_network.py`.
модельная колонка в `connectors/sqla/models.py`) и по `sqlparse` (`MAX_GROUPING_TOKENS`).
Механизм покрыт тестами в `backend/tests/services/maintenance/test_dashboard_scanner.py`
(включая end-to-end с реальным `sql_table_extractor` и реальным SQL из продакшн-сценария),
`backend/tests/test_sql_table_extractor.py` и `backend/tests/test_core/test_async_network.py`.
# [/DEF:Doc.Adr.ADR0020:ADR]