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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user