Repository navigation
fix(sqla): avoid redundant schema introspection for virtual datasets - #45681
aminghadersohi wants to merge 31 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #45681 +/- ##
===========================================
- Coverage 84.56% 65.10% -19.47%
===========================================
Files 3068 3060 -8
Lines 204987 204420 -567
Branches 48171 47801 -370
===========================================
- Hits 173340 133079 -40261
- Misses 28346 68259 +39913
+ Partials 3301 3082 -219
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Acceptance run at e0dc5c9 against the merge-base 3c9f08d (base = the three changed files checked out from the merge-base, same stack). Setup: docker-compose-light, Postgres 17 as the analytic DB (60k-row table), one dashboard per dataset with 10 charts and 3 cascading native filters. Connections counted twice and the counts agree exactly: a SQLAlchemy Connections per dashboard load (13 chart-data requests), base → head
Results unchanged: 1,434 base/head request pairs have the same status, cache key, SQL text, rows and Default-schema lookup raising: at base every virtual-dataset chart fails with a 500. At head, schema-set virtual datasets (plain and Jinja) and schema-set query-backed charts return the same data. Schema-NULL ones still fail because they need the lookup. Timing, with a simulated 0.7 s warehouse login per connection: warm load of the schema-set virtual dashboard 2.5–2.8 s → 0.3–0.6 s (median cache-hit request 0.88 s → 0.15 s), the same as the physical dashboard. Cold load 8.5 s → 3.2 s. Pre-existing, outside this diff (not regressions, measured at head):
Not covered: a real Snowflake connection (counts are real Postgres logins, and the login cost is simulated), async SQL Lab (no Celery in the light stack), and screenshots: n/a (not a visual change) |
Run babel_update.sh for the expression RLS validation message. Include upstream re-extraction drift in the regenerated template and language catalogs.
msyavuz
left a comment
There was a problem hiding this comment.
Looks good to me but there are still unattended reviews from @sadpandajoe
…-excess-db-connections # Conflicts: # superset/connectors/sqla/models.py # superset/models/helpers.py # superset/translations/ar/LC_MESSAGES/messages.po # superset/translations/ca/LC_MESSAGES/messages.po # superset/translations/cs/LC_MESSAGES/messages.po # superset/translations/de/LC_MESSAGES/messages.po # superset/translations/en/LC_MESSAGES/messages.po # superset/translations/es/LC_MESSAGES/messages.po # superset/translations/fa/LC_MESSAGES/messages.po # superset/translations/fi/LC_MESSAGES/messages.po # superset/translations/fr/LC_MESSAGES/messages.po # superset/translations/it/LC_MESSAGES/messages.po # superset/translations/ja/LC_MESSAGES/messages.po # superset/translations/ko/LC_MESSAGES/messages.po # superset/translations/lv/LC_MESSAGES/messages.po # superset/translations/messages.pot # superset/translations/mi/LC_MESSAGES/messages.po # superset/translations/nl/LC_MESSAGES/messages.po # superset/translations/pl/LC_MESSAGES/messages.po # superset/translations/pt/LC_MESSAGES/messages.po # superset/translations/pt_BR/LC_MESSAGES/messages.po # superset/translations/ro/LC_MESSAGES/messages.po # superset/translations/ru/LC_MESSAGES/messages.po # superset/translations/sk/LC_MESSAGES/messages.po # superset/translations/sl/LC_MESSAGES/messages.po # superset/translations/sr/LC_MESSAGES/messages.po # superset/translations/sr_Latn/LC_MESSAGES/messages.po # superset/translations/ta/LC_MESSAGES/messages.po # superset/translations/th/LC_MESSAGES/messages.po # superset/translations/tr/LC_MESSAGES/messages.po # superset/translations/uk/LC_MESSAGES/messages.po # superset/translations/zh/LC_MESSAGES/messages.po # superset/translations/zh_TW/LC_MESSAGES/messages.po # tests/unit_tests/sql/parse_tests.py
Expression RLS cache-key collection decided from the template's syntax whether a render was needed. A template joining string parameters with list or dict parameters (and other compositions) rendered a table subquery at execution while collection neither rendered it nor scoped the key to the viewer, so viewers with different RLS shared cache keys. Collection now renders every expression through BaseTemplateProcessor.render_for_discovery: template parameters and plain values are real, the request-only filter readers run, and every other macro or context function, unknown filter/test/global and the non-deterministic built-ins are stubs. Using a stub stops the render without running it and scopes the key to the viewer; filters and tests given a stub are opaque too. The engine macro namespace, which persists between execution renders, is stubbed whole. The rendered SQL goes through the same RLS collector, so no warehouse macro runs before the cache lookup, including for expressions with table subqueries. FROM is rendered the same way to learn filter removals, and a series-limit prequery is collected with its own context.
A discovery stub exposed existing attributes such as `__class__`; the sandbox described them in an undefined that named the stub's type, so a template could tell discovery from execution and render a subquery only at execution. Any attribute access on a stub is now opaque. The series-limit prequery context used the selected columns, while execution passes `groupby or columns` without the timestamp pseudo column; collection now uses the same columns.
An adhoc template whose output names a saved column (arithmetic, conditionals, comparisons, blocks, macros, indexing, string methods) must key on that column's subquery RLS for each viewer, as execution resolves and renders the saved expression.
The cache key for tables read inside an adhoc expression sub-query left out global guest rules, which execution applies there, and the key for a dataset SQL sub-query did not record the rules execution injects only into sub-query reads. Both are now computed by the same helper apply_rls uses, so the key carries exactly the predicates each read gets. Discovery rendering also stubs a custom filter result that is not plain data: a to_datetime result reads the clock, so a template could take a different branch than at execution.
| and effective_columns | ||
| and ( | ||
| (query_obj.get("is_timeseries", True) and not series_columns) | ||
| or series_columns.intersection( |
There was a problem hiding this comment.
This compares the raw groupby strings to series_columns, but execution matches on the rendered label (outer.name in series_column_labels). With groupby=["{{ dim }}"], dim="col1" and series_columns=["col1"], execution runs the series-limit metric while this check sees no overlap, so has_series_groupby is false and a restricted-table sub-query used only in series_limit_metric contributes no predicates to the cache key. Two viewers with different RLS on that table could then share a cached result. Should the labels be compared after the same template rendering execution applies?
There was a problem hiding this comment.
Good catch, confirmed. Grouped execution labels a non-saved string selection with its processed SQL (_process_select_expression: render, then sanitize_clause, then _unwrap_select_clause), and matches that outer.name against series_column_labels. The collector compared the raw string.
Fixed in 98d2d9f: has_series_groupby derives each unknown string's label from the same single render_for_discovery pass used to collect it, then applies sanitize_clause / _unwrap_select_clause as execution does. Fail-closed cases count as overlapping: an opaque render, a label with a subquery (RLS rewrites it per viewer), and engines without SELECT aliases (execution names the column by its SQL there).
Tests (fail before, pass after; SqlaTable and Query):
test_cache_key_collects_series_metric_for_rendered_groupby_labels({{ dim }}withdim=col1,col1;,{{ 'col1' }} ;)test_cache_key_collects_series_metric_without_select_aliasestest_cache_key_collects_series_metric_for_opaque_groupby_label
Siblings checked:
- superset/models/helpers.py:4269 (GROUP BY saved columns, granularity, adhoc columns) — not affected: execution labels them by raw name /
get_column_name, unrendered. - superset/models/helpers.py:4216 (
add_orderby:metric_labels,adhoc_columns_by_label, saved names) — not affected: execution keys onget_metric_name/ adhoclabel/ names, all unrendered. - superset/models/helpers.py:4252 (
need_groupbyORDER BY check) — not affected: same unrendered metric keys as execution. - superset/models/helpers.py:4231 (
add_filter:_get_filter_column, metric names, adhoc labels,removed_filters) — not affected: execution resolves the same raw strings. - superset/models/helpers.py:4406 (series prequery columns) — not affected: built from the same raw selections and re-collected by the same code.
test_raw_string_groupby_cache_keys_are_user_scoped still pins a single render of the source.
Grouped execution labels a non-saved string selection with its rendered, sanitized SQL and compares that label to the series columns. The cache-key collector compared the raw string, so a templated or normalized GROUP BY that matched a series column skipped the series-limit metric's RLS. Derive the label from the single discovery render, and treat opaque renders, subqueries and engines without SELECT aliases as matching.
SUMMARY
Review revision:
2723978ee47c6fe5d34c7db14b0b60997cf59f49.Avoid redundant schema inspection for virtual datasets and unsaved SQL Lab Explore charts while retaining underlying-table and expression-subquery RLS cache contributions.
BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
No UI change.
test_cache_hit_schema_discovery_connection_budgetasserts exactly one discovery connection per cache hit.test_virtual_dataset_avoids_schema_connectionsretains zero connections for cached requests with dynamic configured schemas or statically known connection schemas.test_cache_hit_series_limit_does_not_execute_prequeryasserts no series-limit prequeries on cache hits, including the ExtraCache path.Druid planner tests use SQLite transport; no live external-engine latency claim is made.
TESTING INSTRUCTIONS
Docker, Python 3.11:
PYTHONPATH=.:superset-core/src python -m pytest -q \ tests/unit_tests/connectors/sqla/virtual_dataset_connections_test.py \ tests/unit_tests/db_engine_specs/test_schema_parameters.py \ -m 'not manual and not llm_compliance'dba4383d1c39acf2968a79c793a89b852978cc9d: 26 failed, 7 passed. The same selection passes after the change; it includes two real user objects with different mocked RLS rules and different executed results/cache keys.48254b641bc1861b191138d58ecf0fc2c9de6df5: 213 failed, 57 passed. Passing cases are compatibility/negative controls, not claimed as failure reproducers.Real-LLM checks were not run.
ADDITIONAL INFORMATION
Expression entry points include selected/grouped columns, stored and SQL/SIMPLE metrics, filter columns, WHERE/HAVING, ordering, active series-limit metrics, temporal axes and fetch-value predicates. Parse-failure keys retain per-user isolation. No migrations, new feature flags, or public API/UI changes.