The previous @has_access decorator on ActivityDebugView.show was
redirecting authenticated-but-not-authorized requests to the home
page (FAB's access-denied default), which is what the user was seeing
instead of the React app. Then @login_required was tried briefly but
crashed because flask-login's redirect endpoint isn't wired up under
this app's auth backend.
Simplest fix: no decorator on the shell route. The shell page exposes
no data of its own — the React component fires API calls to
/api/v1/{resource}/{uuid}/activity/ which gate access via
raise_for_ownership on the path entity. Anonymous users would see
the React UI with 401 errors inline, which is correct UX for a debug
tool.
Verified: GET /activity-debug/dashboard/<uuid>/ → 200 + SPA shell HTML.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The React route at /activity-debug/{resource}/{uuid} was returning
a JSON 404 on fresh page-load because Superset's SPA model doesn't
use a Flask catch-all — every React-router path needs a corresponding
Flask view that calls render_app_template(). Adding the missing
piece.
* superset/views/activity_debug.py — new ActivityDebugView (uses
BaseSupersetView). Single @expose("/<resource>/<uuid>/") returning
the React shell; the in-app router (superset-frontend/src/views/
routes.tsx) handles the actual page rendering. @has_access keeps
it gated by login.
* superset/initialization/__init__.py — appbuilder.add_view_no_menu
registration alongside the other SPA shell views.
Throwaway by design; both edits tagged with "Throwaway: sc-107283"
comments for easy removal when the activity-view feature ships.
Verified: GET /activity-debug/dashboard/<uuid>/ returns 302 to
/login when unauthenticated (correct), 200 with the SPA shell when
logged in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds /activity-debug/{dashboard,chart,dataset}/{uuid} for visual
verification of the sc-107283 activity-view endpoints. Lets you eyeball
the response shape without building a curl/jq habit:
* Controls for include / page / page_size / since / until
* Per-record cards showing entity_kind chip, source badge, kind tag,
tombstone affordance, issued_at, changed_by, summary, entity_uuid,
version_uuid, path, from/to values, impact
* Deleted entities render with strikethrough + "deleted" tag
* Soft-deleted state surfaces as an orange tag (currently always null
until sc-103157 lands deleted_at)
**Throwaway by design**: when the activity-view feature ships, delete
the lazy import + route entry from superset-frontend/src/views/routes.tsx
and remove the superset-frontend/src/pages/ActivityDebug/ directory.
Both edits are tagged with "Throwaway: sc-107283" comments so they're
easy to find.
The component uses @superset-ui/core/components (Card, Tag, Radio, Input,
Space, Typography, Loading, Empty) per CLAUDE.md conventions. TypeScript
clean, ruff/eslint/oxlint clean, no any types. Uses a native HTML
<select> for page_size — the @superset-ui Select wrapper's stricter prop
types weren't worth fighting for a debug tool.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Breaking change to the (still-unmerged) activity-view JSON contract:
``entity_kind`` now emits ``"dashboard"`` / ``"chart"`` / ``"dataset"``
instead of the Python class names ``"Dashboard"`` / ``"Slice"`` /
``"SqlaTable"``. The class names are developer-facing artifacts that
leaked the model layer (e.g. ``"Slice"`` predates the UI rename to
"chart"; ``"SqlaTable"`` predates "dataset"). User-facing JSON should
speak user language.
Implementation: a new ``_USER_FACING_KIND`` map translates at JSON
serialization time only (in ``_decorate_records``). Internal code keeps
the Python class-name form (``model_cls.__name__``) for dispatch — the
existing ``_NAME_COLUMN``, ``_NOT_FOUND_EXC``, ``_API_KIND_LABEL``,
``_can_read``, ``_compute_impact``, etc. all key off class names and
are unchanged. The translation happens at the single ``record["entity_kind"]
= ...`` assignment.
Schema validator ``ACTIVITY_ENTITY_KINDS`` updated to the new tuple.
Integration tests' response-shape assertions renamed via bulk sed; unit
tests testing internal helpers are unchanged (they operate on internal
api_kind / class names).
UPDATING.md example payload updated. Spec updates (spec.md, data-model.md,
contracts/activity-view.yaml) committed separately to the spec repo.
Full suite: 66 unit + 35 integration + 1 xfailed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T037 — Instrument the five phases of get_activity with stats_logger
timing metrics under the `superset.activity_view.<kind>.<phase>_ms`
key convention (plan §D-17). Wrap each phase with a stats_timing
context manager:
* superset.activity_view.<kind>.relationship_resolution_ms (_resolve_scope)
* superset.activity_view.<kind>.fetch_ms (_fetch_change_records)
* superset.activity_view.<kind>.visibility_filter_ms
* superset.activity_view.<kind>.denormalize_ms
* superset.activity_view.<kind>.decorate_ms
`<kind>` is the lowercased path-entity model class name (dashboard /
slice / sqlatable), enabling per-endpoint-family Grafana panels.
T038 — Add request- and response-shape attributes via the existing
counter/gauge interface:
* superset.activity_view.<kind>.requests.include_<value> (incr)
* superset.activity_view.<kind>.requests.has_since_filter_<true|false> (incr)
* superset.activity_view.<kind>.page_size (gauge)
* superset.activity_view.<kind>.record_count (gauge — post-visibility-filter)
* superset.activity_view.<kind>.related_entity_count.charts (gauge)
* superset.activity_view.<kind>.related_entity_count.datasets (gauge)
Confirmed no PII: entity names, diff content, user identifiers — none
flow into the metric layer. Only counts and shape tags.
T050 — Cross-coupling sanity test (unit-scope): asserts
_METRIC_PREFIX == "superset.activity_view" so a code review catches
accidental drift from sc-103156's eventual "superset.versioning.*"
sibling namespace. Both endpoint families belong to one Grafana panel
under "superset.versioning.* OR superset.activity_view.*".
Full suite: 66 unit + 35 integration + 1 xfailed (T044 sc-103156
restore-kind dependency, unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* T042 test_activity_marks_hard_deleted_chart_with_tombstone (D-15)
Edit a chart, hard-delete it via db.session.delete(chart) + commit,
hit dashboard activity. Asserts tombstoned record has
entity_deleted=True, entity_uuid=None, entity_name preserved from
the last shadow row. The fixture's _cleanup tolerates missing slices
(uses Slice.id.in_(slice_ids) which silently skips).
* T044 test_activity_surfaces_dashboard_restore_event (AV-015) — xfail
AV-015 requires sc-103156's restore code to emit a synthetic
kind="restore" change record with to_value carrying the source
version_uuid + label. sc-103156's restore_version() currently does
not — the diff capture surfaces the field changes as kind="field".
The activity layer correctly passes through whatever kind sc-103156
emits; the test will start passing once the upstream emission lands.
Marked strict=True so the day sc-103156 emits, this fails-to-fail
and we know to revisit.
* T051 test_activity_excludes_records_after_retention_prune (AV-010)
Edit a chart (creates a new version_transaction), backdate that
transaction's issued_at past the 30-day retention cutoff, run
_prune_old_versions_impl(retention_days=30). Assert the prune
removed ≥1 transaction AND the activity endpoint's filtered count
decreased.
* T039 test_activity_pagination_is_deterministic_and_disjoint
(SC-AV-002 pragmatic interpretation). The spec's stricter "no
skip/duplicate under concurrent writes" is unprovable with offset
pagination — new top-inserted records shift every later page by
one. Cursor pagination would solve this (deferred per plan §D-10).
Under offset, the testable guarantees are: (a) same request fired
twice produces identical pages; (b) page N and page N+1 are
disjoint under one request round. Both come from the
(issued_at DESC, transaction_id DESC, sequence DESC) sort.
Full activity-view suite: 35 passed, 1 xfailed in 46s. The xfail is
T044 with the documented sc-103156 dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three of the Phase 6 polish tasks landed together:
* T040 test_activity_ordering_is_stable_by_issued_at_then_transaction_id
— AV-006 deterministic-ordering contract. Iterates adjacent pairs in
the response and asserts (issued_at, transaction_id) is monotonically
non-increasing. Catches any sort-stability regression — random
ordering would fail the pairwise check almost certainly.
* T041 test_activity_page_size_caps_returned_records_at_200 — pairs
with the existing pagination_clamps test. The former confirms
?page_size=500 doesn't 400; this one confirms the response is
bounded to ≤200 records as the OpenAPI contract guarantees.
* T053 verification: every fixture-mutating test in
activity_view_tests.py was already following the
try/finally + rollback convention (sweep verified 30 tests; zero
non-conformant). No remediation needed; documenting the
sweep result in the commit message.
Full activity-view suite: 32/32 integration + 65/65 unit tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two structural changes from the second SQLAlchemy review. No behaviour
change; full activity-view suite (70 unit + 30 integration) still
green.
* Tidy 1 (Warning #2): switch _batch_chart_counts and
_batch_datasets_used_by_charts from positional row-unpack to
.mappings()-keyed access. Column-rename-safe — a future edit that
reorders columns in the SELECT can't silently misassign values
during the Python loop. Matches the existing convention in
_select_change_rows_for_kinds.
* Tidy 2 (Suggestion #1): docstring on _seed_activity_history
explaining why it intentionally commits without rollback (unlike
the T053 convention in activity_view_tests.py). The seed IS the
setup, not the unit under test — the endpoint reads a realistic
post-commit state of the shadow tables.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two doc-shaped tasks landed together because they're both about making
the activity-view endpoints discoverable by external consumers.
T047 — UPDATING.md: new section under the existing entity-version-history
entry documenting the three activity-view endpoints (dashboard / chart /
dataset), their query params (since / until / include / page / page_size),
the response shape with all DTO fields, the silent permission filter
(AV-008), tombstone behaviour (D-15), and the no-feature-flag / no-
new-tables impact statement. Mirrors the depth of the sc-103156
versioning section above it.
T048 satisfied by the UPDATING.md entry: the activity-view feature
adds no new config keys (no SUPERSET_* env vars, no feature flag), and
the per-endpoint API reference is auto-generated from the YAML
docstrings via FAB's OpenAPI integration. The `/swagger/v1` page picks
up the activity endpoints automatically — verified by the new tests
below. sc-103156 followed the same pattern (UPDATING.md only, no
standalone config doc).
T049 — Three new tests in TestActivityOpenApiSpec verify FAB's OpenAPI
generation includes the activity endpoints with the right shape:
* test_three_activity_paths_appear_in_openapi — the three
/<uuid_str>/activity/ paths are surfaced in /api/v1/_openapi.
* test_activity_endpoints_document_query_params — since / until /
include / page / page_size are all declared, and include's enum is
exactly {"self", "related", "all"}.
* test_activity_endpoints_declare_200_response — 200 + 400/401/403/404
are all declared response codes.
base_api_tests.py::TestOpenApiSpec::test_open_api_spec already
validates the full spec's structural correctness on every CI run, so
malformed YAML in the activity-view docstrings would have been caught
upstream. The new tests add activity-specific assertions about the
endpoints' presence and parameter shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced by T045's perf test (which failed at p95 4228ms vs 1500ms
budget) and an ad-hoc per-phase profile that pinpointed _resolve_scope
as accounting for ~1926ms of the wall clock.
The previous _resolve_dashboard_scope called _datasets_used_by_chart
once per slice on the dashboard. On a fixture with ~12 charts that's
12 sequential SQL round-trips for the same shape of query, each
returning a small result set. The new _batch_datasets_used_by_charts
takes the full set of slice_ids and returns
{slice_id: [(dataset_id, window), ...]} in one query; the singular
form _datasets_used_by_chart now forwards to the batch (used only by
_resolve_chart_scope which has a single slice).
Impact (measured on the same fixture):
* Wall-clock per request: 3955ms → 2257ms (43% faster)
* Total query count per request: 18,376 → 29 (driven by the
identity-map populating during the warmup; the genuine count for
the activity layer alone is ~13 SQL queries)
* T045 p95 over 50 runs: 4228ms → 2429ms
The remaining gap to the 1500ms budget is dominated by Python work
inside _fetch_change_records: post-filter and sort over ~26K
accumulated test-environment records. The spec's target scale is
"25 charts × 3 dataset windows" — the SQLite test environment has
accumulated far more state from prior test runs. T046 (index
migration) is deferred because the remaining bottleneck is Python-
side, not SQL-side; production data on Postgres with retention
pruning will not have this shape. Document and revisit when a
production-representative perf measurement is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two opt-in tests (SUPERSET_PERF_VALIDATION=1 to run, skipped otherwise)
under the existing PerfValidationTests class:
* test_av_sc001_activity_endpoint_p95_under_1500ms — seeds dense
history on the birth_names dashboard (~12 chart edits, 5 dataset
edits, 1 dashboard edit), then hits /api/v1/dashboard/<uuid>/activity/
50 times after warmup and asserts p95 < 1500ms per SC-AV-001.
* test_av_sc003_save_path_p95_unaffected_by_activity_view — under the
same fixture, runs 50 dashboard saves and re-measures the SC-004
version-capture overhead (50ms p95 budget). Confirms the activity-
view branch hasn't added a new save-path coupling that regresses
sc-103156's save-path budget. PASSES on the current branch — read-
only feature, no new save-path code paths.
Seed fixture differs from the spec's "25 charts × 3 dataset windows
each" target — the birth_names fixture has ~12 charts on a single
dataset, so we approximate by editing many charts plus the dataset
multiple times. A bespoke multi-dataset fixture would let us measure
against the spec's exact scale; that's future work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced by the SQLAlchemy review (Warning #1) and required before
T045's p95 perf budget can be met on dashboards with many historical
dataset edits.
Before: _decorate_records fired one COUNT(DISTINCT slice_id) query
per related dataset record via _compute_impact → _count_dashboard_
charts_pointing_at_dataset_at_tx. For a dashboard activity stream
with N dataset-edit records, that's N round-trips to compute impacts.
After: _decorate_records collects the distinct (dataset_id, target_tx)
pairs once via _collect_impact_pairs, fires a single batched query
via _batch_chart_counts that pulls the (slice, dataset, validity-
window) state for every relevant slice, and filters by validity in
Python per pair. The result is a {(dataset_id, target_tx): count}
mapping consumed by the new pure helper _impact_for_record per row.
Round-trip count drops from O(N records) to O(1) for the impact
calculation. The SQL stays small and dialect-portable — same per-kind
IN-clause + Python validity filter pattern as _fetch_change_records.
Trade-off documented in _batch_chart_counts' docstring: the SELECT
pulls (slice_id, datasource_id, two validity-window pairs) for every
slice ever on the dashboard whose dataset matches one of the
requested dataset_ids. For a busy dashboard with 100 slices and 50
versions each, that's ~5000 rows into Python vs N small COUNT scans.
For N > ~5 (which is typical) the batch wins.
Removed:
* _compute_impact (replaced by _impact_for_record + _collect_impact_pairs)
* _count_dashboard_charts_pointing_at_dataset_at_tx (replaced by
_batch_chart_counts)
Test changes: the three _compute_impact unit tests (no-impact paths)
become six _impact_for_record tests (positive count + four no-impact
paths + zero-count → None). Five new _collect_impact_pairs tests
cover dashboard/chart/dataset path branching plus dedupe and empty.
Full suite: 27/27 integration + 65/65 unit (was 57; +8 from the
restructure). No semantic regression on either side of the cut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No-op docstring change. The function fires one SELECT per entity_kind
with entity_id IN (...) instead of one query with tuple_(entity_kind,
entity_id).in_(...). The latter is supported by Postgres but its SQL
emission across MySQL and SQLite is uneven (some dialects emit literal
ROW expressions, some emit a transformed multi-column form, some don't
support it at all in older versions). Three round-trips per request is
the correct trade-off given Superset's multi-dialect requirement.
Documenting now so a future reader doesn't reach for the tuple-IN
"optimisation" and silently break MySQL or SQLite. Surfaced by the
SQLAlchemy review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the dataset activity stream. Per AV-004 datasets have no
transitive layer in V2: dataset activity = dataset's own edits only,
regardless of whether charts use the dataset. The chart/dashboard
endpoints surface dataset edits in their *related* streams (already
shipped); the dataset endpoint stays narrowly self-only.
* T033 DatasetRestApi.activity — third application of the shared
resolve_endpoint_path_entity + parse_activity_query_params pattern.
Endpoint body is ~10 lines of real logic, same shape as T016 and
T028 but with SqlaTable as the path entity. The activity orchestrator
already short-circuits the related-entity resolution for datasets
(_resolve_scope returns [] for related when path_kind="SqlaTable"),
so the dataset endpoint inherits the V2 semantics for free.
* T034 TestDatasetActivityView class added to activity_view_tests.py.
* T035 test_dataset_activity_excludes_chart_edits — AS-1 of US3 /
AV-004 verbatim. Edit a chart that uses the dataset; assert the
chart edit does NOT appear in the dataset's activity stream
(datasets are read-only upstream of charts in V2).
* T036 test_dataset_activity_includes_dataset_self_edits — confirms
the positive path: editing the dataset's own description surfaces
a SqlaTable/self record.
* New test_dataset_activity_related_only_returns_empty — AV-004 in
contract form: ?include=related on a dataset returns an empty
result list and count=0 because there are no related entities to
draw from.
Plus the standard boundary tests (404 for unknown UUID, 400 for
malformed UUID / invalid include, 403 for non-owner, 200 envelope
shape).
Full activity-view suite: 27/27 integration tests + 57/57 unit tests
green. All three endpoint families live.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the chart cross-entity activity stream (US2). Builds on the
resolve_endpoint_path_entity helper from the prior tidy commit
(1d60513079), so the new endpoint is a small composition of: resolve
path entity → parse params → get_activity(Slice, ...) → serialize.
* T028 ChartRestApi.activity — @expose("/<uuid_str>/activity/") on the
chart blueprint. Decorator stack mirrors list_versions; registered
in include_route_methods. The MODEL_API_RW_METHOD_PERMISSION_MAP
entry "activity": "write" added in Phase 3 already covers this
endpoint family (the map is class-permission-agnostic).
* T029 TestChartActivityView class added to activity_view_tests.py,
following the same patterns as TestDashboardActivityView.
* T030 test_chart_activity_includes_dataset_edit_as_related — Given
a chart pointing at the birth_names dataset, When the dataset's
description is edited, Then the chart activity stream surfaces a
SqlaTable/related record (AS-1 of US2).
* T032 test_chart_activity_excludes_sibling_dashboards — Given the
chart is on a dashboard, When the dashboard's title is edited,
Then NO Dashboard records appear in the chart's activity. Per the
spec's Relationship Traversal section: charts don't see sideways
to dashboards they happen to be on.
* T031 (datasource-switch attribution) deferred — needs a second
dataset fixture which the birth_names environment doesn't provide
cleanly. Will land with a multi-dataset test harness in a follow-up.
Additional coverage: 404 for unknown UUID, 400 for malformed UUID,
400 for invalid include, 403 for non-owner, 200 envelope smoke,
chart-self-edit appears as source=self, include=self filter test.
Full suite: 19/19 integration tests + 57/57 unit tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the change easy, then make the easy change. Phase 4 (T028 chart
/activity/ endpoint, T033 dataset endpoint) duplicates the UUID-parse
+ find_active_by_uuid + raise_for_ownership dance from the existing
DashboardRestApi.activity. Extracting now keeps the next two endpoints
single-line affairs.
* New PathEntityResponseError carries a pre-built error Response.
Subclassing Exception (not ValueError) so the endpoint's catch is
precise and the standard hierarchy stays unpolluted.
* New resolve_endpoint_path_entity(api, model_cls, uuid_str) runs the
three-step preflight and raises PathEntityResponseError with the
right 400/404/403 response carried inside.
* DashboardRestApi.activity refactored to use the helper. 12 lines of
imports + try/except boilerplate collapse to a single try/except
block that maps every preflight failure to its response.
No behaviour change: 10/10 dashboard activity integration tests still
green. Pure structural change, per Beck's tidy-first discipline (the
T028 endpoint code follows in the next commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five structural changes — no behaviour change — applied as one commit
per the tidy-first discipline. All from the clean-code review of
e4070a4716.
* Tidy 1 — Fix _fetch_change_records sort key. Was calling .timestamp()
on issued_at with an `else 0` fallback; the fallback was dead defense
(column is non-null per sc-103156 schema) and .timestamp() introduces
non-determinism on tz-naive datetimes. Sort on the datetimes directly.
* Tidy 2 — parse_activity_query_params now raises ActivityParamsError
(subclass of ValueError) instead of returning (Optional[dict],
Optional[str]). The tuple was forcing every caller into a defensive
`if error or params is None: return self.response_400(message=error
or "Invalid query parameters")`. The new shape — `try: params =
parse(...); except ActivityParamsError as exc: return
response_400(str(exc))` — is shorter, type-safe, and the contract
is enforced at the boundary.
* Tidy 3 — Test helper now uses Flask client's query_string= parameter
instead of f-string concatenation. Handles URL-encoding correctly
for the day a test passes a value containing & / = / + / etc.
* Tidy 4 — get_activity pipeline collapses to a single rolling
`records` variable instead of the mid-stream `raw / visible_raw /
enriched / visible` naming. Each function call's name documents
what the step does; no intermediate variable names needed.
* Tidy 5 — Extracted four per-parameter parsers: _parse_optional_iso,
_parse_include, _parse_page, _parse_page_size. The "parse one
parameter" concept now has a name. Cost: four small helpers (each
10-15 lines, one job). Benefit: parse_activity_query_params is a
10-line table-driven dispatcher.
Test changes: parser unit tests now use pytest.raises(ActivityParamsError)
instead of unpacking the (params, error) tuple. Added one test
confirming ActivityParamsError subclasses ValueError so the standard
library exception hierarchy still catches it. Total unit tests: 57.
Integration tests still 10/10 green.
Deferred per the review: the UUID-parse + entity-find + ownership-
check dance is duplicated between activity / list_versions / get_version
and will grow to T028 / T033. Refactor when T028 lands — three real
callers > one prospective one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the dashboard cross-entity activity stream (US1 MVP).
Closes T015-T018, T023-T024, T026 from sc-107283 tasks.md. T019-T022
(complex fixture choreography) and T025 (RBAC fixture for restricted
chart access) deferred to a follow-up; T025's logical coverage is
provided by the new unit tests for _can_read kind dispatch.
* T015 Rename _dashboard_related_scope → _resolve_dashboard_scope and
_chart_related_scope → _resolve_chart_scope, parallel to _resolve_scope
/ _resolve_path_entity. Aligns the codebase with the task spec names.
* T016 New activity(uuid_str) method on DashboardRestApi:
@expose("/<uuid_str>/activity/"), @protect + @safe + @statsd_metrics
+ @event_logger. Same row-level ownership check pattern as
/versions/. Registered in include_route_methods and mapped to "write"
in MODEL_API_RW_METHOD_PERMISSION_MAP so the can_write Dashboard
permission gates access (Alpha-non-owner gets 403 from
raise_for_ownership, not 404 from missing permission).
* New parse_activity_query_params helper in activity.py — shared parser
for since/until/include/page/page_size used by all three endpoint
families. Tolerates the Z suffix Python <3.11 fromisoformat rejects.
Silently clamps page_size to the contract max instead of rejecting.
Two correctness/scale bugs found and fixed under integration-test load:
* SQLite SQLITE_MAX_EXPR_DEPTH (1000) was tripping on dashboards with
many slices × many historical attachment windows. _fetch_change_records
used to emit one OR-clause per (entity_kind, entity_id, window) tuple;
now it issues one SELECT per kind with entity_id IN (...) and filters
by exact windows in Python via _row_within_any_window. SQL shape is
proportional to the number of kinds (≤3); the per-entity window
precision is preserved.
* _merge_entity_windows now unions overlapping/touching windows within
each entity via _union_windows. Sequential fixture loads create many
redundant Continuum shadow rows; without merging, the unfiltered
windows still produce many OR branches downstream.
* Pipeline-ordering fix in get_activity: visibility filter runs BEFORE
decoration. Decoration strips entity_id (not in API contract) and the
filter needs it — and dropping invisible records early also avoids
paying for name lookup + tombstone probes on records the requester
can't see (AV-008's silent-filter contract).
Tests:
* tests/integration_tests/versioning/activity_view_tests.py — 10 tests
for TestDashboardActivityView covering: 404 for unknown UUID, 400 for
malformed UUID / invalid include / invalid since, 403 for non-owner,
200 envelope smoke, chart-edit-appears-as-related, include=self
filter, include=related filter, page_size clamping.
* tests/unit_tests/versioning/test_activity.py — grew from 30 to 56
tests. New coverage: parse_activity_query_params (7 cases), _can_read
per-kind dispatch (4 cases — covers T025 at unit scope), _union_windows
(9 parametrized cases), _merge_entity_windows window-union case,
_row_within_any_window (6 cases).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five structural changes — no behaviour change — applied as one commit
per the tidy-first discipline (separate from the prior feat commits
that landed the primitives and orchestrator):
* Add tests/unit_tests/versioning/test_activity.py — 30 tests for the
pure-function helpers: _intersect_windows (11 boundary cases),
_resolve_scope branching (per kind + per include mode), entity-
window merging, AV-012 summary headlines, changed_by projection,
kind-translation round-trip, _can_read fall-through, _compute_impact
no-impact paths. Runs in <500ms, no DB, no Flask. The DB-touching
helpers wait for the integration suite (Phase 3+).
* Hoist _datasets_used_by_chart(slice_id) out of the inner attachment-
window loop in _dashboard_related_scope. Previously a chart with N
attachment windows fired N queries for identical data; now once per
slice. Removes a perf cliff at the historical-rich case the spec
explicitly calls out (US1 AS-3).
* Delete the unused requesting_user parameter from
_filter_records_by_visibility and from get_activity. It was never
threaded through to the security manager calls (which read the user
from Flask-Login implicitly) — speculative future-shape that the
reader had to mentally trace through. If CLI/Celery bypass becomes
necessary, add it then with a real call site.
* Delete the unused module-level logger. No observability lands here
until T037/T038; reintroduce when those tasks add real logger calls.
* Fix the file docstring — it claimed _resolve_version_tables was
reused; it isn't. Trimmed to (find_active_by_uuid,
derive_version_uuid).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes Phase 2 of sc-107283. The activity-view query layer now has
one public entry point — get_activity(model_cls, entity_uuid, ...) —
plus the supporting visibility filter and DTO decorator.
* T011 _filter_records_by_visibility — silent permission filter per
AV-008. Batch-resolves live entities per kind, dispatches
can_access_dashboard / can_access_chart / can_access_datasource.
Tombstoned entities pass through (the decorator handles deleted
messaging; no navigable entity_uuid is exposed). _can_read helper
isolates the per-kind predicate dispatch.
* T012 _decorate_records — synthesizes the full ActivityRecord shape:
entity_kind (API form), entity_uuid (live lookup; null for
tombstones), entity_deleted, entity_deletion_state, source ("self"
vs "related"), summary (AV-012 headline), impact (via T010),
version_uuid (derive_version_uuid), changed_by (User projection).
Strips internal-only columns (entity_id, sequence, raw user cols)
from the final shape.
* T013 get_activity — top-level orchestrator. Branches by include
("self"/"related"/"all") via _resolve_scope; for related,
_dashboard_related_scope walks charts-on-dashboard then datasets-
used-by-chart and clips chart-on-dataset windows by attachment
windows. _chart_related_scope skips the dashboard hop. Datasets
return empty related scope per AV-004. _merge_entity_windows
collapses repeated (kind, id) entries to keep the OR-clause in
_fetch_change_records compact. Pagination is post-visibility-filter
per AV-008; defaults page_size=25, clamps to 200.
Tasks T004-T014 in tasks.md updated to [X]. The next phase opens user
story implementation (T015 dashboard scope helper + T016 endpoint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the foundational query layer for the cross-entity activity view
in superset/versioning/activity.py. Pure helpers + single-purpose
shadow-table queries; no public entry point yet (T011-T013 follow).
* T004 _resolve_path_entity — UUID lookup via find_active_by_uuid;
typed 404 dispatch by model class (DashboardNotFoundError /
ChartNotFoundError / DatasetNotFoundError).
* T005 _charts_attached_to_dashboard — (slice_id, window) tuples from
dashboard_slices_version, op=2 rows excluded.
* T006 _datasets_used_by_chart — (datasource_id, window) tuples from
slices_version, filtered to datasource_type='table'.
* T007 _intersect_windows — pure half-open window intersection;
end=None acts as positive infinity. Unit-verified.
* T008 _fetch_change_records — workhorse query joining version_changes
+ version_transaction + ab_user, OR-clause over (kind, id, [windows]),
ordered by (issued_at DESC, transaction_id DESC, sequence DESC) per
AV-006. No DB-level pagination here — AV-008's silent permission
filter means count is computed after the visibility filter, so
pagination happens in T013.
* T009 _denormalize_entity_names — batch shadow lookup keyed by
(api_kind, entity_id, transaction_id); validity-strategy predicate.
Helper _resolve_names_for_kind extracted to keep cyclomatic
complexity under ruff's C901 threshold.
* T010 _compute_impact — dashboard+dataset path yields
{"charts": N}; all other path/related shapes yield None per
data-model.md §"impact computation".
* T014 _check_entity_tombstones — live-row existence + soft-delete
state probe; tolerates the pre-sc-103157 absence of a deleted_at
column via hasattr-style introspection on Table.c.
Kind translation isolated at the module boundary: version_changes
stores lowercase ("chart"/"dashboard"/"dataset"); the ActivityRecord
DTO returns class names ("Slice"/"Dashboard"/"SqlaTable"). The
_TABLE_KIND_TO_API / _API_KIND_TO_TABLE mappings translate at the
two crossings.
Phase 2 orchestration layer (T011-T013) lands in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply clean-code review polish on top of the Phase 1 scaffold before
Phase 2 builds on this surface:
* Replace magic-string field descriptions with ``validate.OneOf`` on
``entity_kind``, ``source``, ``entity_deletion_state``, and ``kind``.
Canonical lists hoisted to module-level tuples (``ACTIVITY_*``).
* Type ``path`` as ``fields.List(fields.String())`` and ``impact`` as
a new ``ActivityImpactSchema`` — replaces two ``fields.Raw`` ``Any``s
whose shape was previously documented only in prose.
* Resolve the ``ActivityChangedBySchema`` docstring contradiction —
the schema deliberately omits ``username`` so it is no longer
honest to call it "identical shape to VersionChangedBySchema".
* Fix the ``activity.py`` module docstring: one polymorphic
``get_activity`` function, not three entry points.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* T001 — new ``superset/versioning/activity.py`` module with ASF
header and docstring describing the cross-entity activity-view query
layer (companion to ``queries.py``). Empty otherwise; Phase 2 fills
it with the shared query helpers.
* T002 — empty test file
``tests/integration_tests/versioning/activity_view_tests.py`` with
ASF header, module docstring naming the three test classes that will
follow, and a ``SupersetTestCase`` import. Phase 3+ fills it with
test classes.
* T003 — three Marshmallow schemas added to
``superset/versioning/schemas.py``:
* ``ActivityChangedBySchema`` — User subset on each record.
* ``ActivityRecordSchema`` — one change record (per data-model.md).
* ``ActivityResponseSchema`` — envelope ``{result, count}``.
Fields mirror data-model.md §"``ActivityRecord`` DTO" line-for-line;
metadata descriptions are FAB/Swagger-ready.
No new endpoints, no listeners, no DB writes — pure scaffolding. The
three schemas are unused until Phase 3 (T016) wires the first endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Squashes the three migrations that captured the versioning schema as
the feature was developed:
* ``56cd24c07170 add_entity_version_history_tables`` — version_transaction
+ dashboards_version / slices_version / tables_version parent shadows.
* ``e1f3c5a7b9d0 add_version_changes_table`` — the field-level diff log.
* ``f7a2b3c4d5e6 spike_add_child_continuum_shadow_tables`` —
table_columns_version / sql_metrics_version / dashboard_slices_version
child shadows.
Combined into a single migration ``2026-05-28_19-50_56cd24c07170_
add_versioning_tables.py`` that creates the same 8 tables in dependency
order (sequence + version_transaction first, parent shadows, version_
changes, child shadows last) and drops them in reverse on downgrade.
Why:
* All three migrations represent one logical feature ("enable
versioning") — applying any subset leaves a broken intermediate
state.
* The third migration was literally named "spike_..."; the iterative
shape was a development artifact, not a design intent.
* Downstream operators get one migration to apply / reverse and one
file to review.
* The dev DB was rolled back through the three downgrades, the new
migration was applied via ``superset db upgrade``, and column-count
spot checks confirmed byte-identical schema (4/9/21/26/35/19/15/5).
The ``revision`` hash is reused from the original first migration
(``56cd24c07170``) for chain continuity. ``down_revision`` stays
``2bee73611e32`` (sc-105349 composite-PK tip), unchanged from the
original chain's head.
Net: -70 lines (605 deleted, 535 added), same schema, one Alembic step
instead of three.
The diff engine now recurses into nested dicts inside JSON-blob fields
(``json_metadata``, ``Slice.params`` non-list sub-keys, layout node
``meta``) via a shared ``_recursive_leaf_diff`` helper, emitting one
``ChangeRecord`` per atomic leaf change rather than one record carrying
the whole top-level sub-tree on both sides.
Concretely, a dashboard header text change from "VERSION 2!" to
"HEADER!" used to emit a single record at path ``["edit", "header",
"HEADER-id-1"]`` with the entire layout node duplicated on both sides
of the diff. It now emits a single record at path ``["edit", "header",
"HEADER-id-1", "text"]`` with just the changed string as ``from_value``
/ ``to_value``. Multi-leaf edits produce one record per leaf.
Each call site passes its own depth cap via a named constant:
* ``_LAYOUT_META_DIFF_DEPTH = 3`` — layout meta is presentation state
(text, sizes, colors), shallow by nature.
* ``_JSON_METADATA_DIFF_DEPTH = 6`` — native filter configuration can
go five levels deep (``defaultDataMask.filterState.value``).
* ``_SLICE_PARAMS_DIFF_DEPTH = 6`` — adhoc filter sub-queries and pivot
options can be similarly deep.
A cap is a usefulness bound (granularity that's meaningful in a
timeline), not a safety bound. Cap-on-dict-vs-dict emits a debug log so
production tuning can see when a cap is too tight for typical data.
Lists are treated as opaque leaves: positional paths break under
reorder. Lists with stable identity (adhoc filters, metrics, dataset
columns) already have natural-key walkers (``_diff_list_by_natural_key``)
that emit per-element records with the right identity; those are
unchanged.
Backward compatibility:
* Top-level scalar fields, child-collection records (column/metric),
M2M slice membership, and layout add/remove/move records all keep
their existing path/from/to shape. The change is scoped to JSON-blob
recursion.
* All 56 existing diff unit tests pass without modification. 13 new
tests cover the Shape B behaviour (helper directly, type mismatches,
opaque list policy, depth cap, nested ``json_metadata``, layout
edit, ``Slice.params`` deep unknown key, cross-node aggregation).
* Existing integration tests in ``change_records_tests.py`` assert on
scalar-field changes only — unaffected.
Restoration is unaffected: the restore path reads from Continuum
shadow tables, not from ``version_changes``. This change is purely
about audit-log granularity.
Spec coverage: tasks.md T047d (added in spec repo). Rationale: plan.md
"Key Design Decision: Leaf-level recursive diff for JSON-blob fields
(Shape B)". Conventions: data-model.md "Leaf-level recursion".
The ``_pin_audit_columns`` helper added in 1b0083bc03 relies on a
SA-version-dependent behavior: calling ``attributes.flag_modified(parent,
"changed_by_fk")`` causes the next UPDATE statement to use the
in-memory value rather than invoking the column's ``onupdate=callable``.
This is the mechanism that prevents a stale ``g.user.id`` from being
written into the parent's ``changed_by_fk`` when the synthetic
flag-flush triggers an UPDATE during an autoflush at a time when the
test user has already been deleted from ``ab_user`` — the cascade that
broke CI in sc-103156 T062.
Four unit tests, no app context required (uses a minimal in-memory
SQLite engine + declarative_base):
* ``test_flag_modified_suppresses_onupdate_callable``: the positive
invariant — the in-memory value lands in the DB and ``onupdate`` is
NOT called. If SA ever changes this semantic, the test fails.
* ``test_onupdate_does_fire_without_flag_modified``: negative sanity
check — confirms the test setup is realistic. Without
``flag_modified``, ``onupdate`` fires as expected.
* ``test_pin_audit_columns_skips_missing_attribute``: the helper
tolerates parents without audit columns (e.g., non-AuditMixin
models). No raise.
* ``test_pin_audit_columns_tolerates_invalid_request_error``: SA
raises ``InvalidRequestError`` from ``flag_modified`` when the
attribute is unloaded in instance state (the freshly-constructed
``session.new`` case). The helper must catch and skip — this is the
production-path safety net mentioned in the docstring.
Uses ``expire_on_commit=False`` for the positive invariant test so the
column stays loaded; the production-path case where the attribute is
expired is the ``InvalidRequestError`` branch covered separately.
Closes review item W2 from the sqlalchemy-review pass on
sc-103156-versioning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four unit tests pin the behavior the validator added in 46f138326b:
* Valid UUID-format strings get coerced to ``uuid.UUID`` instances —
the primary contract that makes importers, test fixtures, and ad-hoc
``Model(uuid=...)`` construction see a consistent type.
* Already-``UUID`` values pass through unchanged (the validator must
not re-wrap; ``is`` identity check pins this).
* Non-UUID-shaped strings pass through unchanged. This keeps test
mocks that use placeholder strings (e.g.
``tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py::test_dashboard_schema``
with ``"dashboard-uuid-7"``) working. Without this contract, those
tests would break under the validator and need to migrate to
``uuid.uuid4()``. If the contract is ever tightened to raise, this
test catches the regression and signals the migration cost.
* ``None`` passes through unchanged.
Closes review item S3 from the sqlalchemy-review pass on
sc-103156-versioning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``ChartRestApi.list_versions`` (and the dashboard / dataset siblings)
resolves the entity via ``VersionDAO.find_active_by_uuid`` to run the
``raise_for_ownership`` check from T056, then calls
``VersionDAO.list_versions(model_cls, entity_uuid)`` — which internally
runs the SAME ``find_active_by_uuid`` query a second time. The lookup
filters on ``uuid`` (a unique non-PK column), so it isn't served from
SQLAlchemy's identity map — every call is a real SELECT.
Add an optional ``entity=`` keyword to ``list_versions``,
``get_version``, and ``resolve_version_uuid`` so callers that have
already resolved the path entity can skip the redundant lookup. The
public signature stays backward-compatible (kwarg-only).
Threading ``entity`` through ``get_version`` also avoids a third
redundant lookup inside the ``resolve_version_uuid`` call it makes.
Net: 6 fewer SELECT queries on the ``/versions/`` and
``/versions/<uuid>/`` hot paths (1 per call × 6 endpoints). All
``find_active_by_uuid`` callsites in the API layer were already
present from T056; this commit just plumbs them through.
Closes review item W3 from the sqlalchemy-review pass on
sc-103156-versioning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reset session state (``rollback`` + ``expire_all``) before and after each
test in this class. Defensive hygiene against the Postgres-only multi-
test cascade documented in spec task T062 — a previous test in the
full-suite ordering can leave Continuum's shadow-table session attributes
in a state where the restore command's ``@transaction`` boundary raises
mid-flush and the API returns 422 "Dataset could not be updated."
Note: this *doesn't* fix the cascade itself (the polluter is writing to
the DB, not just the session; reproducer remains 3 failures on Postgres
full-suite ordering). The root cause is queued as T062. This change just
guarantees that whatever state this class' tests inherit, they start
from a clean session view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The version-history integration tests follow the codebase's dominant
pattern: a trailing ``# Cleanup`` block at the end of each test that
restores the fixture entity's mutated scalar fields (``description``,
``slice_name``, ``dashboard_title``). The pattern is brittle — when any
intermediate assertion fails, the cleanup is never reached and the
fixture stays polluted with the test's last edit, cascading state into
later tests in the same suite run.
This bit hard in CI's full-suite Postgres ordering: a failure in
``test_restore_applies_scalar_field`` left ``description="restore-test
v2"`` on the birth_names dataset, polluting downstream tests including
the RLS virtual-dataset cascade. The same shape is latent in the chart
and dashboard files.
Convert every mutating test in the three version-history files to wrap
the mutation block in ``try:`` and the restoration in ``finally:``,
with a ``db.session.rollback()`` at the top of the finally to clear any
half-flushed state left by a 422 response. Re-query the entity by id
inside the finally so the restore writes against a live object rather
than a stale handle the failing path may have expired or detached.
Pure hygiene improvement — doesn't fix the underlying Postgres failure
chain (still triggered by ``model_tests.py``'s interaction with later
tests in the full-suite ordering), but prevents this set of tests from
amplifying any future cascade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ``@validates("uuid")`` coercion added in 46f138326b now ensures
``UUIDMixin.uuid`` always returns a ``uuid.UUID`` instance after
assignment, regardless of input type. This is the right semantic
(matches what ``UUIDType`` would emit on a fresh DB read) but it
broke three pre-existing tests that compared the attribute against
string literals — those assertions only passed before because the
post-INSERT refresh path either didn't run or ran inconsistently for
the model under test.
Update those three assertions to compare ``UUID`` to ``UUID`` so the
test contract matches the now-consistent attribute shape:
* ``test_import_database`` and ``test_import_database_no_creds`` —
``database.uuid`` now matches the coerced ``UUID`` literal.
* ``test_load_parquet_table_sets_uuid_on_new_table`` — ``tbl.uuid``
same treatment.
Also harden the validator to pass non-UUID-shaped strings through
unchanged (instead of raising), so test mocks that use placeholder
strings like ``"dashboard-uuid-7"`` keep working — the SQL bind layer
will surface a clearer error if such a value is ever written to the
DB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``sqlalchemy_utils.UUIDType`` only coerces in ``process_bind_param`` /
``process_result_value`` — string-to-UUID conversion happens on SQL
write / read, not on Python attribute assignment. Importers and ad-hoc
construction (e.g., ``SqlMetric(uuid="00000000-...")`` in the
``test_import_dataset`` unit test) leave the in-memory attribute as a
``str`` until the next DB round-trip refreshes it.
For non-versioned mappers a post-INSERT attribute expiration usually
refreshes the value before the caller reads it. With SQLAlchemy-
Continuum versioning on a child mapper (``TableColumn``, ``SqlMetric``),
the expire-on-INSERT behaviour shifts enough that the assertion
``metric.uuid == uuid.UUID(...)`` fails with a string-vs-UUID inequality.
Bisection confirmed: removing ``__versioned__`` from either child
restores the implicit coercion path; adding it reproduces the failure.
Rather than chase the Continuum/SA flush interaction, add a defensive
``@validates("uuid")`` to ``UUIDMixin`` so the attribute is always a
``UUID`` at runtime regardless of where the value came from. This
aligns the in-memory shape with what ``UUIDType`` would emit on a
fresh DB read, and the coercion is the same one the SQL layer would
apply on the next bind anyway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``_force_parent_dirty_on_child_change`` calls ``flag_modified`` on a
versioned parent so Continuum records a parent shadow row whenever a
versioned child changes. The resulting UPDATE on ``tables`` /
``dashboards`` / ``slices`` inherits ``AuditMixin``'s
``onupdate=get_user_id`` on ``changed_by_fk`` and ``onupdate=datetime.now``
on ``changed_on`` — SQLAlchemy calls ``get_user_id()`` at flush time
and stamps whoever ``g.user`` resolves to.
When the flush is autoflush-triggered during a *previous* test's
teardown (or the fixture cleanup between two tests), ``g.user`` can
still point at a user that's already been deleted from ``ab_user``.
The parent UPDATE then fails the FK to ``ab_user``, surfacing as
``IntegrityError: FOREIGN KEY constraint failed`` /
``ForeignKeyViolation``. In CI's full-suite ordering this fires from
two distinct places — ``test_rls_filter_alters_no_role_user_birth_names_query``
teardown and ``TestDatasetRestoreApi::test_restore_applies_scalar_field``
mid-restore (the latter returning 422 "Dataset could not be updated.").
Fix: also ``flag_modified`` ``changed_by_fk`` and ``changed_on`` on
the parent. The columns now have dirty attribute history, so
SQLAlchemy uses the in-memory (previously-committed, valid) values
instead of invoking ``onupdate``. The synthetic parent UPDATE carries
the existing audit values; the FK violation goes away. The behaviour
on a real user-driven save is unchanged — those code paths go through
the normal write path with a live ``g.user`` and the audit columns
update as before.
Extracted ``_pin_audit_columns`` helper to keep the caller under
ruff's ``C901`` complexity ceiling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's ruff-format would collapse the two-clause if expression onto a
single line — local ruff at v0.9.7 reformats the same way. Pure
formatting, no behavior change. Counterpart of ``6de8811873``.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same fix as ``7caf9862be`` for the dashboard restore test. CI's
integration DB accumulates Continuum shadow rows across fixture teardown
cycles, and ``TestDatasetRestoreApi::test_restore_applies_scalar_field``
picks the first row whose ``description`` matches the original. In CI
that match can land on an ``operation_type=2`` (DELETE) row from a prior
test run; Continuum's ``revert()`` then restores the dataset to its
deleted state and the restore API returns 422 "Dataset could not be
updated."
Filter ``operation_type != 2`` so the test only ever targets INSERT or
UPDATE shadow rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SIP's *Authorization layering* commitment promises that all nine
version endpoints enforce both model-level access (``@protect()``) AND
row-level access (``security_manager.raise_for_ownership(entity)``).
Restore endpoints already do this via ``BaseRestoreVersionCommand.validate``,
but ``list_versions`` and ``get_version`` (six endpoints across chart,
dashboard, and dataset) skipped the row-level check — a non-owning user
with ``can_write`` on the model could list or read the shadow rows of
any entity they could reach by UUID.
Each affected handler now looks up the entity via
``VersionDAO.find_active_by_uuid`` (which already runs inside the DAO),
returns 404 if it's gone, then calls ``raise_for_ownership`` and maps a
``SupersetSecurityException`` to 403. Admins bypass automatically via
the existing FAB mechanism. The redundant lookup hits the session
identity map on the second call, so the cost is one PK read.
Tests:
- Add ``test_list_versions_denies_non_owner`` (chart, dashboard, dataset)
and ``test_restore_denies_non_owner`` (chart) — Alpha has ``can_write``
on each model but isn't an owner of the admin-owned fixture, so the
row-level branch is the only thing that can reject. The dataset row-
level test is alongside the existing model-level Gamma test, which
only exercises the ``@protect()`` denial path.
- Unskip the three placeholder tests on chart and dashboard that were
blocked waiting for this gap to close (``@pytest.mark.skip(reason="...
no built-in no-write user to exercise the 403 branch...")``).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pairs with ``25f1922b2d temp(versioning): demo version-history dropdowns``,
which adds ``uuid`` to the dashboard-list page's ``select_columns`` so the
per-row VersionHistoryDropdown can call ``/api/v1/dashboard/<uuid>/versions/``.
``DashboardList.test.tsx`` asserts the list-fetch URL via
``toMatchInlineSnapshot``, so the snapshot needs the new column. Revert
this entry along with the dropdown component (same lifecycle as the temp
parent commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The integration SQLite DB accumulates Continuum shadow rows across
fixture teardown cycles. ``TestDashboardRestoreApi::test_restore_applies_scalar_field``
picks the first version row whose snapshot matches the original title,
and that match can land on an ``operation_type=2`` (DELETE) row left
over from a prior test run. Continuum's ``revert()`` then dutifully
restores the entity to its deleted state — the dashboard row is gone
after the restore returns 200, and the post-restore re-query fails
with ``NoResultFound``.
Filter ``operation_type != 2`` so the test only ever targets
INSERT or UPDATE shadow rows, which is also what a real user would
pick from the version-history dropdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous fix (9c2391d1b4) gated the force-parent-dirty hook on
is_modified(child) for ALL session collections (dirty/new/deleted).
That was over-restrictive: is_modified checks attribute history,
and deletion is a state transition with no attribute history —
so deleted children evaluated as not-modified and the parent
wasn't flagged. The change-records listener then didn't see the
deletion and no removal record was emitted.
Symptom: test_restore_emits_full_child_diff_in_one_transaction
failed expecting a column-removed change record after a restore
that removed the column; instead only the parent's scalar fields
appeared in observed paths.
Refine: apply the is_modified filter ONLY to persistent rows in
session.dirty. session.new (creation) and session.deleted
(removal) are always real content changes by virtue of their
session-collection membership — no is_modified check needed (and
in deletion's case, the check returns the wrong answer).
Pre-commit (previous) flagged I001 unsorted-imports on the
backward-compat façade. Two queries imports merged into one
block (the aliased ``derive_version_uuid as _derive_version_uuid``
moves inline rather than living in its own block), and the
restore-side names sorted: ``_RESTORE_RELATIONS``,
``_stamp_audit_fields_for_restore``, ``restore_version``.
Pure mechanical reformatting; no behaviour change.
_force_parent_dirty_on_child_change was firing whenever ANY
TableColumn or SqlMetric of the parent appeared in
session.dirty / new / deleted — even when the child was there for
non-content reasons:
- Lazy-load side effects when a relationship is touched
- M2M relationship-cascade artifacts (e.g. RLS setUp doing
rls_entry.tables.extend([dataset]) triggers cascade behavior
that pulls children into the session)
- AuditMixin auto-bumps from earlier code paths
- Reverter side passes during restore
Force-touching the parent in those cases produced an incidental
UPDATE tables SET description=…, changed_on=…, changed_by_fk=…
whose changed_by_fk value or autoflush ordering tripped FK
integrity on some dialects. Symptoms:
- test_rls_filter_alters_no_role_user_birth_names_query → FK
IntegrityError on autoflush during a query
- test_restore_applies_scalar_field → 422 "Dataset could not be
updated" during restore
Fix: gate on Continuum's is_modified(child), which returns True
only when a non-excluded versioned column on the child has
SQLAlchemy attribute-history changes. New objects (session.new)
and genuinely-modified rows still flag the parent; phantom-dirty
rows do not.
The intended hook semantics — "child edit forces a parent shadow
row" — are preserved: a column-description edit through the
dataset API still triggers is_modified True, still flags the
parent. See test_dataset_column_edit_creates_parent_version.
flag_modified(parent, "uuid") was producing FK integrity failures via
the column's BLOB/BINARY round-trip: SQLAlchemy logs the param as
``<memory at 0x…>`` and the UUID round-trip doesn't always match the
in-memory value byte-for-byte. Symptom: in scenarios where the parent
is already going to flush (Reverter applying historical state during
restore, RLS test triggering autoflush during a query), our added
``uuid`` UPDATE column tripped the FK check.
Pick ``description`` instead — plain Text column on all three
versioned parent classes (Dashboard, Slice, SqlaTable), no
TypeDecorator, no marshaling layer. Flagging it round-trips its
current value safely. Fallback chain ``description → uuid → col_keys[0]``
keeps the original deterministic-pick property for forks/subclasses
that excluded ``description``.
Should unblock test_restore_applies_scalar_field and the
test_rls_filter_alters_no_role_user_birth_names_query autoflush
error.
- factory.py: TID251 banned ``import json``; switch to
``from superset.utils import json`` (project convention).
- factory.py: ruff format reflow on _matches_previous_version.
- version_restore.py: ruff format collapse on restore_version call.
CI was pinning a different ruff version than my local uvx default;
re-ran against ruff==0.9.7 (the version in requirements/development.txt)
which surfaced these.
The previous attempt (d0520f6766) was too aggressive: skipping when
parent is in session.dirty/new/deleted bypassed the
persistent-and-clean case the hook EXISTS for. Some upstream code
paths put the dataset in session.dirty *before* this listener fires
(API controllers touching audit fields, etc.), so the
session-membership pre-check made us silently no-op on the very
scenario the hook needs to handle. CI symptom:
test_dataset_column_edit_creates_parent_version showed before=317,
after=317 (parent shadow not written).
Restore the unconditional flag_modified and catch the specific
InvalidRequestError that fires only for the session.new case
(uuid default callable hasn't populated state yet). Other states
fall through to the original behavior:
- persistent + clean → flag_modified succeeds, parent goes dirty,
Continuum picks it up, SkipUnmodifiedPlugin keeps the row via
_has_dirty_versioned_children. ✓
- persistent + dirty → flag_modified is harmless (already dirty).
- session.new → InvalidRequestError, skip (parent INSERTs anyway).
- session.deleted → flag_modified may or may not raise; if it does,
we skip; if not, the delete dominates.
Should unblock test_dataset_column_edit_creates_parent_version,
test_get_version_returns_historical_snapshot_with_children, and
test_restore_with_column_edits_reverts_columns.
- ruff: import sort + E501 reflow on the parent-state guard in
baseline.py
- ruff format: function-signature collapse and join-chain reflow in
queries.py
- auto-walrus: two ``entity_kind = …; if … is not None:`` patterns
in queries.py converted to assignment-expressions
When one ORM flush touches multiple versioned entities (dashboard +
slice + dataset all save at tx=X), each gets a shadow row sharing
that tx. If only the dashboard is later edited at tx=Y, the
dashboard row at tx=X is closed (end_tx=Y) while slice/dataset rows
stay live at tx=X. Retention then preserves tx=X (slice/dataset are
live there) and prunes tx=Y. The dashboard's closed row at tx=X
survives step 1, then its end_transaction_id=Y trips the FK when
step 2 deletes version_transaction row Y.
Fix: extend the shadow-row delete to also match end_transaction_id
IN tx_ids. Live rows have end_tx=NULL so they're never matched by
either predicate. Closed rows that touch a pruned tx at either
endpoint are pruned together — consistent with retention semantics
(any tx in the row's lifespan is gone, so the row's chain is broken
anyway).
Unblocks test_retention_prunes_old_rows on sqlite, mysql, postgres.
The force-parent-dirty listener was calling attributes.flag_modified
on every parent reachable from a dirty child — including parents
themselves in session.new (e.g. brand-new SqlaTable + brand-new
TableColumns from POST /api/v1/dataset/). flag_modified rejects
unloaded attributes, and a session.new SqlaTable's uuid (default=uuid4
fires at flush time) is unloaded until then. CI caught this with
InvalidRequestError cascading into 422s across dataset creation /
upload / Playwright dataset specs.
The hook is only needed for the persistent-and-clean case (child
edited, parent's own scalars untouched, dropdown otherwise empty).
Anything in session.new will flush anyway; anything in session.dirty
is already flagged; session.deleted shouldn't be touched. Short-
circuit before the flag_modified call.
Unblocks test-sqlite, test-mysql, test-postgres (previous), and
playwright dataset specs.
Three small follow-ups surfaced by aminghadersohi's review of the
SoftDeleteMixin PR (#39977) that apply equally here:
- H1: cache _child_to_parent_registry() with functools.cache. Called
twice per save flush; mapping depends only on import-time model
classes, so unbounded cache is the right shape (no invalidation).
- M5: tighten _CHILD_BASELINE_HANDLERS type from dict[str, Any] to
dict[str, Callable[[Session, Any, int], None]] via a named alias.
Mypy now catches a future broken handler signature.
- M3/M4: explain the inline-import pattern once in the module
docstrings of baseline.py and changes.py. Both modules use
pylint disable=import-outside-toplevel uniformly because they
load during init_versioning() before mappers are configured;
the per-callsite "why" comments would just repeat the same
reason. Module-level explanation + a hint to comment unusual
cases is the cleaner shape.
M6 (listener placement) doesn't apply — init_versioning() already
runs inside init_app_in_ctx(). M8 (loose OpenAPI schema in
*/api.py docstrings) is real but its own change.
Extends the existing docstring note ("the orphan is swept by retention")
with the reasoning behind not cleaning it up in the same flush. The
inline-delete is appealing in principle but would couple this plugin
to the change-records listener's buffer state via the ON DELETE
CASCADE on ``version_changes.transaction_id``: both listeners would
have to agree that the flush produced nothing before the version_transaction
row could be dropped safely. The orphan's ~40-byte storage cost +
retention's correct-by-construction handling (orphans have no parent
shadow, so they're never in the "preserve" set) make the coordination
overhead not worth it.
Captures the design decision in the file where the next reader will
look for it.
Pure file shuffle, zero behaviour change. Reorders ``baseline.py`` so it
reads top-down by level of abstraction (newspaper-article rule): the
public entry point at the top, supporting helpers descending below.
Before: 14 private helpers, then ``register_baseline_listener`` at the
bottom. A reader opening the file met the leaf builders first and had
to accumulate context before finding the call site.
After (top-down):
- Entry point: ``register_baseline_listener`` + inner ``capture_baseline``
- High-level helpers used by ``capture_baseline``:
``_force_parent_dirty_on_child_change``,
``_collect_parents_to_baseline``,
``_child_to_parent_registry``,
``_version_table_for``,
``_shadow_row_count``,
``_insert_baseline_and_children``
- Mid-level builders:
``_insert_baseline_row``,
``_baseline_children_for_parent``
- Per-entity child handlers + their dispatch table:
``_baseline_dataset_children``,
``_baseline_dashboard_children``,
``_CHILD_BASELINE_HANDLERS``
- Leaf builders:
``_insert_child_baseline_rows``,
``_baseline_attached_slices``,
``_insert_synthetic_slice_baseline``
Three section-divider comments mark the abstraction levels. The
``_CHILD_BASELINE_HANDLERS`` dict literal stays after its referenced
handlers (module-level literals evaluate at import time and need names
already bound); a comment now flags this constraint.
Function bodies are byte-for-byte unchanged; ``git log -L`` on any
function shows only its relocation. 96 unit tests pass.
baseline.py:_insert_baseline_row and changes.py:_read_pre_state both
issued the same "read a single row through ``session.connection()``
inside ``with session.no_autoflush:``" pattern. Same five-line block,
same intent ("read the pre-flush state without triggering the in-flight
edit's flush").
Promoted to ``superset.versioning.utils.read_row_outside_flush(session,
table, entity_id)``. Companion to ``single_flush_scope`` — they sit
next to each other in utils.py and frame the two directions of the
"don't autoflush mid-listener" pattern.
Returns ``dict[str, Any]`` (or ``None``) so callers can't accidentally
hold a cursor-bound ``RowMapping`` past the listener boundary. Both
call sites get shorter by ~5 lines.
Also picks up Decimal stringification in the changes.py docstring
update (was listed in the W4 commit but the docstring still said
"(datetime, UUID, bytes)" — now matches the implementation).
Behaviour unchanged. 96 unit tests pass.
After the SRP split (8c9cf36) put both functions in the same module
~150 lines apart, their overlap became visible: same JOIN of
version_table → version_transaction → ab_user, same baseline-first
ordering, same user-row → ``changed_by`` projection, same lookup
``_ENTITY_KIND_BY_CLASS_NAME.get(model_cls.__name__)``. About 30 lines
of duplication.
Five small helpers extracted at the module top:
- ``_resolve_version_tables(model_cls)`` returns ``(ver_tbl, tx_tbl, user_tbl)``
- ``_version_with_tx_user_join(ver_tbl, tx_tbl, user_tbl)`` builds the join
- ``_baseline_first_ordering(ver_tbl)`` returns the order-by tuple
- ``_user_select_cols(user_tbl)`` returns the user-column list with
``user_id`` as the stable label (normalises the prior asymmetry
where ``list_versions`` labelled it ``user_id`` and ``get_version``
labelled it ``_user_id`` to dodge a column-name collision — the
``user_id`` label collides with neither)
- ``_changed_by_from_row(row)`` projects user columns onto the API shape
- ``_entity_kind_for(model_cls)`` resolves the change-records taxonomy lookup
Both call sites get shorter and read what they do (build query / project
user / build row) rather than how. Behavior unchanged; no test changes.
Also two small inline tidyings while in the file:
- Replace the ternary
``changes_by_tx = list_change_records_batch(...) if entity_kind else {}``
with an explicit two-line if-statement in both functions. The ternary
buries the decision; the if-statement reads as one thought.
- Inline the one-shot ``meta_cols`` set declaration in ``get_version``
into the ``if col.name in {...}`` check that uses it three lines later.
Net: about 110 lines → about 80 lines across the two functions, plus
a small helper section at the top.
Cleanup pass from the SQLAlchemy + migration code review. Eight items,
all in the "warnings / suggestions" tier — no behaviour change visible
to the API, but each closes a real correctness, perf, or maintainability
concern surfaced in review.
baseline.py
- Delete unused ``_get_user_id`` (W1). The function wrapped a broad
``except Exception: # noqa: S110`` swallow that hid bugs; grep
confirmed no callers anywhere. The legitimate audit-field paths
(``row.get("changed_by_fk")`` etc.) already drive the
``version_transaction.user_id`` write.
- Batch ``_baseline_attached_slices`` from O(N) round-trips to
three queries (W2): one membership SELECT, one existing-shadow
SELECT, one bulk live-row SELECT for the missing ids. The previous
per-slice ``COUNT(*)`` + ``SELECT`` was a measurable first-save
hotspot on dashboards with many charts. Drops the now-unused
``_slice_has_shadow`` helper.
- Pick a stable column name for ``flag_modified`` in
``_force_parent_dirty_on_child_change`` (W3). ``uuid`` is on all
three versioned parent classes and excluded by none, so the
flagged attribute is deterministic across SQLAlchemy versions /
mapper-config orders instead of depending on
``versioned_column_properties(parent)[0]``. Falls back to the
first available column for forks that exclude ``uuid``.
changes.py
- Add ``Decimal`` handling to ``_jsonable`` (W4) — ``json.dumps``
rejects ``Decimal``, so any numeric column (e.g. ``SqlMetric.currency``
contents, or fork/plugin Decimal columns) would crash the bulk
insert. Stringify rather than ``float()`` to preserve precision;
the diff engine compares ``from_value`` / ``to_value`` by string
equality after this coercion so both sides round-trip identically.
queries.py
- Promote the inline ``{0: "baseline", 1: "update", 2: "delete"}``
dict to module-level ``_OP_TYPE_LABELS`` (W7). The literal was
duplicated across ``list_versions`` and ``get_version``; the third
caller is one bug fix away.
- Comment on ``resolve_version_uuid``'s Python-side ``derive_version_uuid``
loop (W8) — no portable SQL form for UUIDv5 across PostgreSQL /
MySQL / SQLite, iteration count is bounded by the retention
window. Flags the place to revisit if retention is ever disabled
(``=0``) on a heavily-edited entity.
migrations/2026-05-01_23-36 (composite-PK)
- Belt-and-braces guard in ``_downgrade_mysql_table`` (W6): asserts
``t.name in AFFECTED_TABLES`` before interpolating into the
backtick-quoted ALTER statements. The invariant was already
structurally implied (callers iterate ``AFFECTED_TABLES``), but
making it load-bearing means a future refactor can't slip an
arbitrary table name through.
(W5 was verified-no-change: grepped ``tests/`` for ``metadata.create_all``
callers that exercise versioning tables; none. The cascade-FK
gap on ``version_changes.transaction_id`` is already documented
in ``tests/integration_tests/versioning/change_records_tests.py:27-32``.)
62 versioning unit tests pass.
VersionDAO carried five distinct concerns under one class — UUID
derivation, version metadata queries, change-record loading,
single-version snapshot retrieval, and restore orchestration. Bob's
"and" test (the clean-code review flagged this as the next structural
fix after the dead-code purge) gives ~600 lines of "queries about
versioned state of one entity AND the workflow that mutates it."
Splits the read and write sides into purpose-built modules:
- ``superset/versioning/queries.py`` — UUID derivation
(``VERSION_UUID_NAMESPACE``, ``derive_version_uuid``) + read-side
helpers (``find_active_by_uuid``, ``current_version_number``,
``current_live_transaction_id``, ``current_live_version_uuid``,
``list_versions``, ``resolve_version_uuid``, ``get_version``,
``list_change_records_batch``). ~475 lines.
- ``superset/versioning/restore.py`` — write-side (``restore_version``,
``_stamp_audit_fields_for_restore``, ``_RESTORE_RELATIONS``).
~140 lines. Depends only on ``queries.find_active_by_uuid`` and
``utils.single_flush_scope``.
- ``superset/daos/version.py`` — collapsed to an ~85-line backward-compat
façade that re-exports both modules under a single ``VersionDAO``
class via ``staticmethod`` aliases. The module also re-exports
``VERSION_UUID_NAMESPACE`` and ``derive_version_uuid`` at module level
so the ~10 existing callers (api.py handlers, command classes, the
ETag emitter, integration tests) don't have to change their imports.
New code is encouraged to import from the sub-modules directly.
The functions themselves are unchanged byte-for-byte aside from
internal call sites being rewritten from ``VersionDAO.foo`` to the bare
function name (since they now live as module-level functions, not
class methods).
One unit-test mock target moved: ``test_restore_version_returns_none_for_unknown_entity``
now patches ``superset.versioning.restore.find_active_by_uuid`` (the
actual call site) instead of ``VersionDAO.find_active_by_uuid`` (which
is now just an alias).
Each of the three modules now has one reason to change. When the
sc-103157 soft-delete pass adds the ``deleted_at IS NULL`` filter to
``find_active_by_uuid``, it touches only ``queries.py``. When a
per-entity-type restore Strategy replaces the string-keyed
``_RESTORE_RELATIONS`` dispatch, it touches only ``restore.py``.
DashboardList demo dropdown previously instructed the user to "Reload
the page to see the change" after a restore. The URL the user
returns to may still carry ``?native_filters_key=…`` /
``permalink_key`` / ``form_data_key`` from a prior session — those
point at server-cached snapshots (in ``key_value`` and the
filter-state cache) captured before the restore. On rehydration the
cached state is merged on top of the restored ``json_metadata``,
masking the rollback (e.g. dashboard-level colour-scheme restore
appears not to take effect).
Replaces the alert + manual reload with a direct ``window.location.href``
navigation to ``/superset/dashboard/<uuid>/`` — drops all URL params,
forcing hydration from the freshly restored DB state.
Also regenerates ``package-lock.json`` to pick up the ``zod 4.4.1 →
4.4.3`` bump that master's ``package.json`` already reflects.
(``temp(versioning)`` prefix per the demo dropdown's status — this
file is not part of V1 scope per ADR-005; the V2 UI SIP owns the
actual restore UI surface.)
Two coupled clean-code review fixes:
(1) Rename ``VersionDAO._find_active_entity_by_uuid`` →
``find_active_by_uuid``. The leading-underscore + three
``# pylint: disable=protected-access`` suppressions in the restore
commands were the smell of a wrongly-private API. The method is a
perfectly reasonable public DAO operation; dropping the underscore
removes the suppressions.
(2) Collapse ``RestoreChartVersionCommand``, ``RestoreDashboardVersionCommand``,
``RestoreDatasetVersionCommand`` onto a shared
``BaseRestoreVersionCommand`` (``superset/commands/version_restore.py``).
The three classes were textbook copy-paste — identical except for
the model class and three exception types. Each subclass now declares
``model_cls`` + ``not_found_exc`` + ``forbidden_exc`` and overrides
``run()`` with one ``@transaction(reraise=<failed_exc>)``-decorated
line delegating to ``self._do_restore()``. ~80 lines per file →
~45 lines per file; one shared workflow instead of three drift sources.
The api.py imports of ``RestoreChartVersionCommand`` /
``RestoreDashboardVersionCommand`` / ``RestoreDatasetVersionCommand`` are
unchanged — public class names preserved.
The full-Continuum spike (ADR-004 revised) replaced the JSON-snapshot
restore path with Continuum's native Reverter and removed the
``dataset_snapshots`` / ``dashboard_snapshots`` tables from the
migration chain. Seven VersionDAO methods and two module-level
helpers that read/wrote those tables stayed in the code anyway and
went unused — dead code that looked live.
Worse, ``VersionDAO.get_version`` still read from
``dataset_snapshots`` in its SqlaTable branch. On any environment
where the snapshot tables don't exist (current production behavior),
``GET /api/v1/dataset/<uuid>/versions/<version_uuid>/`` raised
``OperationalError``. The branch is rewritten to read column and
metric state from Continuum's child shadow tables
(``table_columns_version`` / ``sql_metrics_version``) via the
existing ``_shadow_rows_valid_at`` helper.
Deleted:
- ``_deserialize_snapshot_value`` (module helper)
- ``_coerce_snapshot_list`` (module helper)
- ``RESTORE_EXCLUDE_FIELDS`` (constant — only referenced by deleted code
and a docstring)
- ``VersionDAO._restore_dataset_children``
- ``VersionDAO._parse_slice_ids_json``
- ``VersionDAO._apply_dashboard_slices``
- ``VersionDAO._restore_dashboard_children``
- ``VersionDAO._apply_snapshot_children``
The corresponding ~17 unit tests in
``tests/unit_tests/daos/test_version_dao.py`` are removed alongside.
Stale docstring references in ``versioning/changes.py`` and
``versioning/diff.py`` that pointed at the retired snapshot tables are
also cleaned up.
Also strips an 8-line comment block in ``restore_version`` that
duplicated the docstring of ``_stamp_audit_fields_for_restore``.
Net: −290 lines from ``daos/version.py``; a production-shape bug
fixed; dead code that looked live is gone.
VersionDAO.restore_version previously called Continuum's Reverter
once per relation in a split-revert loop with flush + expire between
calls. That closed an autoflush race in the Reverter when multiple
relations were reverted at once, but split one logical restore across
multiple Continuum transactions — and once the change-records listener
was wired up, the listener's tx-dedup guard skipped the second pass,
silently dropping child-addition records from version_changes. A
restore that re-added a calculated column would render as an empty
"Baseline" entry in the dropdown.
Replaces the split-revert with a single ``target_version.revert(relations=relations)``
call wrapped in a new ``single_flush_scope(db.session)`` context
manager (``superset/versioning/utils.py``). The context manager
suppresses autoflush inside the block and issues one trailing flush
on clean exit; on exception, the trailing flush is skipped so the
session's normal rollback path handles cleanup. Same autoflush window
closed, one Continuum transaction instead of N, the change-records
listener sees the complete shadow state in one after_flush pass.
The wrapper carries the full autoflush-race / cascade-add rationale
in its docstring so the restore_version call site can be a short
6-line block referencing it.
Integration coverage: ``test_restore_emits_full_child_diff_in_one_transaction``.
SQLAlchemy doesn't mark a parent as dirty when only its children
(``TableColumn`` / ``SqlMetric`` on ``SqlaTable``) are modified.
Continuum's UnitOfWork only creates operations for entities in
``session.dirty``, so a column-only edit produces shadow rows in
``table_columns_version`` but no parent shadow row in
``tables_version``. ``VersionDAO.list_versions`` queries the parent
shadow, so the version dropdown is empty for child-only saves —
exactly the failure mode reported when "I edited a column description
but no version appeared."
Extends ``register_baseline_listener`` with a new before-flush hook
``_force_parent_dirty_on_child_change`` that walks the existing
``_child_to_parent_registry`` and ``attributes.flag_modified(parent,
<first non-excluded versioned column>)`` whenever a versioned child
is dirty / new / deleted but the parent's own scalars haven't been
touched. The flag puts the parent in ``session.dirty`` so Continuum's
UoW creates a parent UPDATE operation; the resulting shadow row's
scalar columns mirror the previous version (only the children
actually changed), and the row exists to anchor the transaction in
the parent's version chain.
``SkipUnmodifiedPlugin._is_no_op_update`` is updated in this commit's
predecessor to recognize the "scalars match but children dirty" case
via ``_has_dirty_versioned_children`` so the forced parent UPDATE
isn't skipped.
Integration coverage: ``test_dataset_column_edit_creates_parent_version``.
Continuum's no-op suppression compared post-flush column values
byte-for-byte against the previous live shadow row. For
``Dashboard.json_metadata`` that produced false-positive version rows
on saves where the user authored nothing — the frontend re-stamps
``map_label_colors`` (regenerated from the ``LabelsColorMap``
singleton) on every save, plus ``chart_configuration`` /
``global_chart_configuration`` / ``show_chart_timestamps`` /
``color_namespace`` (derived from the current chart set), so two
consecutive identical saves produce different bytes for the column.
The diff engine already excluded those keys via
``DASHBOARD_JSON_METADATA_AUDIT_KEYS`` when computing change records;
the skip-plugin diverged.
Adds a ``_COLUMN_NORMALIZERS`` registry keyed on
``(class_name, column_name)`` that maps to a per-column normalizer
applied to both pre- and post-image before equating. The first
entry parses ``Dashboard.json_metadata`` as JSON and drops the
audit-key set before comparing. The same registry is the extension
point for analogous transient fields on charts and datasets.
Promotes ``_DASHBOARD_JSON_METADATA_AUDIT_KEYS`` to a public name
(``DASHBOARD_JSON_METADATA_AUDIT_KEYS``) so the skip-plugin can import
it from ``superset.versioning.diff`` without reaching across a
leading-underscore boundary.
Integration coverage: ``test_map_label_colors_only_change_does_not_create_version``.
The v1 import pipeline previously wrote dashboard ↔ chart membership
via raw Core DML (``db.session.execute(delete(dashboard_slices)…)`` +
``db.session.execute(insert(dashboard_slices)…)``). With Continuum's
M2M tracker enabled by the versioning feature, those Core writes
emit malformed shadow INSERTs into ``dashboard_slices_version`` —
the tracker can't see the composite-PK columns through the Core
layer and produces rows with only ``(transaction_id, operation_type)``
populated, triggering a ``NOT NULL`` violation on
``(dashboard_id, slice_id)``.
Rewrites both import paths (``ImportAssetsCommand._import`` in
``commands/importers/v1/assets.py`` and ``ImportDashboardsCommand._import``
in ``commands/dashboard/importers/v1/__init__.py``) to use ORM-level
``dashboard.slices = [...]`` reassignment followed by an explicit
``db.session.flush()``. The explicit flush is necessary to land the
M2M rows before any subsequent autoflush fires an inner-flush event
handler that would reset the relationship change (cf. the SAWarning
``Attribute history events accumulated on N previously clean instances
within inner-flush event handlers have been reset``).
The unit tests previously called ``_import`` directly twice in the same
session — production wraps ``run()`` in ``@transaction`` so each invocation
gets its own DB+Continuum transaction. Added ``db.session.commit()`` between
calls in ``test_import_adds_dashboard_charts``,
``test_import_removes_dashboard_charts``, and
``test_dashboard_import_with_overwrite_replaces_charts`` so the tests
mirror production semantics; otherwise the second call's M2M shadow
inserts conflict with the first call's on
``UNIQUE (dashboard_id, slice_id, transaction_id)``.
Adds debug-only ``VersionHistoryDropdown`` widgets to the chart,
dashboard, and dataset list pages so the version surface can be
exercised from the UI during the spike. Each row's actions column
gets a clock-icon dropdown that fetches ``/api/v1/{resource}/<uuid>/
versions/`` on click, lists the ten most recent versions with a
formatted change-log summary, and offers per-version restore via
``POST .../versions/<uuid>/restore``.
Strings are wrapped in ``t('...')`` with placeholder formatting
(e.g. ``t('Added %(kind)s "%(name)s"', { kind, name })``) so
translators can reorder verbs and nouns rather than concatenating
fragments. ``KIND_LABELS`` is a static map keying English layout
kinds (``chart``, ``row``, ``column``, ``tab``, ``markdown``, etc.)
to ``t(...)``-extractable labels. Empty change lists render as
"Baseline" rather than "No changes recorded" since the empty case
is overwhelmingly the ``operation_type=0`` baseline row.
Locale-aware date rendering: ``new Date(iso).toLocaleString(lang)``
where ``lang`` comes from ``document.documentElement.lang`` (set
by ``src/views/App.tsx`` from the bootstrap ``locale``), so dates
follow the user's chosen Superset locale rather than the browser's.
French translations for the new strings are appended to
``superset/translations/fr/LC_MESSAGES/messages.po`` (Ajouté,
Supprimé, Modifié, Version initiale, kind labels, …). Run
``npm run build-translation`` and ``pybabel compile -l fr`` to
regenerate the JSON / MO packs.
This commit is **demo-only** per ADR-005 (V1 is backend-only). It
is intentionally marked ``temp`` so it can be reverted before the
PR splits — the production V1 ships without UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks in the no-op-suppression behavior implemented by
``SkipUnmodifiedPlugin`` (which lives in ``superset/versioning/factory.py``
shipping with the foundation commit). Five integration tests:
1. Owners-only edit doesn't mint a version row — exercises the
case where every dirty column is an excluded relationship.
2. Re-save with identical scalar values doesn't mint a row —
exercises the json_metadata re-serialise path where
``set_dash_metadata`` rewrites the column to a different byte
sequence with identical parsed content; the plugin must compare
post-flush values against the prior shadow row to detect this.
3. Real scalar change DOES mint a row — guards against the plugin
over-suppressing.
4. Same assertion on a Slice (covers the ``String`` column path on
a different entity type).
5. ``json_metadata`` sub-key edit DOES mint a row — covers the
``MediumText`` column path past the plugin's content-equality
check.
Tests are designed so a column-type change in the parent entities
(e.g. flipping ``json_metadata`` from ``MediumText`` to ``JSON``)
will fail one of these if the plugin's Python ``!=`` comparison
breaks for the new type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Helper module that derives the strong-validator ``ETag`` value from
an entity's current live ``version_uuid`` and attaches it to a
Flask response. Two functions:
- ``set_version_etag(response, version_uuid)`` — direct path used by
PUT handlers that already compute ``new_version_uuid`` (see the
REST API commit two prior). Cheap; no extra query.
- ``set_version_etag_by_uuid(response, model_cls, entity_uuid)`` —
used by version endpoints that operate on ``entity_uuid``; looks
up ``entity_id`` then derives ``version_uuid`` via ``VersionDAO``.
Costs one extra ``SELECT id WHERE uuid = ?``; documented in the
docstring so callers prefer the cheap variant when they have the
id already.
Integration tests cover all three entity types and four endpoint
shapes (entity GET, save PUT, version-list GET, single-version GET)
plus the entity-with-no-versions edge case (header is correctly
absent).
The ETag is wired into the API endpoints in the REST-API commit
(group 3) and the CORS ``expose_headers: ["ETag"]`` ships with the
retention commit (group 4) since both touch ``superset/config.py``.
Locking enforcement (``If-Match`` → 412) is explicitly NOT in this
change — deferred to the follow-up UI SIP per Open Question §7.
``ETag`` is informational in v1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a scheduled Celery task that prunes version history older than
``SUPERSET_VERSION_HISTORY_RETENTION_DAYS`` (default 30; settable
via env var; ``0`` disables retention entirely).
**Task** — ``superset.tasks.version_history_retention.prune_old_versions``:
1. Computes ``cutoff = utcnow() - timedelta(days=N)``.
2. Selects ``version_transaction.id`` rows with ``issued_at <
cutoff`` and filters out any tx whose parent shadow includes a
live row (``end_transaction_id IS NULL``). The live row is the
only preservation rule — closed historical rows including the
baseline (``operation_type=0``) age out. Per-entity minimum-history
floor is an open question tracked in ``future-work.md``.
3. Deletes rows owned by surviving txs in each parent shadow
table (``dashboards_version`` / ``slices_version`` /
``tables_version``).
4. Deletes child-shadow rows for the same transactions
(``table_columns_version`` / ``sql_metrics_version`` /
``dashboard_slices_version``).
5. Drops the surviving ``version_transaction`` rows. The
``version_changes`` rows cascade via the FK from the previous
commit.
Idempotent and safely retried on partial failure.
**Schedule** — ``superset/config.py`` adds the task to the default
``CeleryConfig.beat_schedule`` (nightly at 03:00). Operators who
override ``CeleryConfig`` in their ``superset_config.py`` need to
merge this entry — see UPDATING.md.
Also adds ``"expose_headers": ["ETag"]`` to the default
``CORS_OPTIONS`` so cross-origin browser clients can read the
``ETag`` header introduced in the next commit. (Co-located here
because both touch ``superset/config.py``; the ETag mechanism
itself ships in the next commit.)
**Auto-discovery** — ``superset/tasks/celery_app.py`` adds
``version_history_retention`` to its late-imports so Celery's
auto-discovery picks up the task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes the version surface as three new endpoints per entity type
(chart, dashboard, dataset), each carrying the standard Superset
decorator stack (``@protect()``, ``@safe``, ``@statsd_metrics``,
``@event_logger.log_this_with_context``) so they appear in FAB's
``action_log`` alongside other audited operations.
| Method | Path | Purpose |
|---|---|---|
| GET | ``/api/v1/{resource}/<uuid>/versions/`` | List version history (oldest-first; per entry: ``version_uuid``, ``version_number``, ``transaction_id``, ``operation_type``, ``issued_at``, ``changed_by``, ``changes`` array) |
| GET | ``/api/v1/{resource}/<uuid>/versions/<version_uuid>/`` | Read-only snapshot of the entity at the requested version (scalar fields plus ``columns`` / ``metrics`` for datasets) |
| POST | ``/api/v1/{resource}/<uuid>/versions/<version_uuid>/restore`` | Replay the snapshot onto the live entity via Continuum's ``Reverter`` (non-destructive — produces a new version row stamping the restoring user via the standard save path) |
``<version_uuid>`` is a deterministic ``UUIDv5(entity_uuid,
transaction_id)`` so it's stable across replicas and retention
pruning. Authorisation reuses the resource's existing ``can_write``
permission; workspace admins can list / restore any entity.
**Restore commands** — ``superset/commands/{chart,dashboard,dataset}/
restore_version.py`` wrap ``VersionDAO.restore_version`` in the
standard ``@transaction()`` boundary. The command resolves the
``Reverter`` once per related collection (split-revert pattern, with
``flush + expire`` between calls) so a multi-relation restore
doesn't trip Continuum's autoflush race that would otherwise mark
half the collection as ``state.deleted=True`` mid-revert.
**Save responses** — ``PUT /api/v1/{resource}/<pk>`` is updated to
include ``old_version`` / ``new_version`` (0-based numbers),
``old_transaction_id`` / ``new_transaction_id`` (stable across
pruning), and ``old_version_uuid`` / ``new_version_uuid`` body
fields so callers can correlate a save with its resulting version
row. The ``ETag`` response header in the next commit is built on
top of this, but the body fields stay — they predate the header
and remain useful for clients that don't read response headers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a structured per-field change log alongside the foundational
shadow tables. Each save flush emits zero or more ``version_changes``
rows describing what changed relative to the previous version, with
shape ``[{kind, path, from_value, to_value, sequence}]`` keyed to
``version_transaction.id`` (FR-016 .. FR-021).
**Schema** — ``version_changes`` table, FK to ``version_transaction``
with ``ON DELETE CASCADE`` so retention drops dependent records
without explicit cleanup. Composite unique index on
``(transaction_id, entity_kind, entity_id, sequence)`` so the
listener can write monotonically and downstream readers see a
deterministic order.
**Diff engine** (``superset/versioning/diff.py``) — pure-function
diffing of pre-/post-state pairs:
- ``diff_scalar_fields`` for ordinary columns; emits one record per
changed field with JSON-safe ``from_value`` / ``to_value``.
- ``diff_json_field`` for ``json_metadata`` and ``params``, walking
the parsed structure and emitting per-sub-key records. Honours
an ``exclude_keys`` set
(``_DASHBOARD_JSON_METADATA_AUDIT_KEYS``: ``chart_configuration``,
``global_chart_configuration``, ``map_label_colors``,
``show_chart_timestamps``, ``color_namespace``;
``_CHART_PARAMS_AUDIT_KEYS``) so frontend-stamped sub-keys that
mutate on every save don't dominate the change log (FR-022).
- ``diff_dashboard_layout`` walks ``position_json`` structurally
and emits ``[verb, kind, id]`` records (verbs ``add``, ``remove``,
``move``, ``edit``; kinds from a ``CHART``/``ROW``/``COLUMN``/etc.
→ english map) so a UI can render "Added chart 'Foo'" without
re-parsing JSON. ``HEADER_ID`` is suppressed because it duplicates
the ``dashboard_title`` scalar record.
- ``fold_dashboard_layout_with_chart_changes`` deduplicates layout
records against M2M / chart-membership records by UUID so an
add-and-attach doesn't appear twice.
- ``_values_equivalent`` treats ``None`` and ``""`` as equal; this
matches the save path's habit of normalising nullable strings to
the empty string.
**Listener** — ``superset/versioning/changes.py`` registers a
``before_flush`` listener that captures pre-state for each dirty
entity and an ``after_flush`` listener that runs the diff engine
against the post-state and writes ``version_changes`` rows under
the resolved ``transaction_id``. Tracks processed transaction ids
on ``session.info`` so re-firings within a single transaction
(autoflush triggered by mid-commit queries) don't double-insert and
trip the unique constraint. Reads child rows via raw SELECT against
``table_columns`` / ``sql_metrics`` rather than ``dataset.columns``
because the live collection is stale during the restore path's raw
DELETE+INSERT cycle.
**Endpoint surface** — ``VersionDAO.list_change_records_batch``
batches the lookup across multiple transactions with a single
``WHERE transaction_id IN (...)`` query so the version-list
endpoint avoids N+1 round-trips. ``list_versions`` / ``get_version``
return entries with a populated ``changes`` array (empty for
``operation_type=0`` baseline rows).
**Tests** — ``test_diff.py`` covers the diff engine shape (39
unit cases across scalar, JSON, layout, child-collection, and
fold paths). ``change_records_tests.py`` exercises the listener
end-to-end with realistic save flows. ``perf_validation_tests.py``
is the T044 harness for SC-002/3/4 (list endpoint p95 < 1s,
restore < 3s, save overhead < 50ms).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds SQLAlchemy-Continuum as a dependency and wires it as the
canonical capture mechanism for chart, dashboard, and dataset edits.
**Schema** — three Alembic migrations, leaving the chain at one
foundation revision plus one child-shadow revision:
- ``version_transaction`` (renamed from Continuum's default
``transaction``; SQL-reserved-word workaround) carries the per-save
``user_id`` / ``issued_at`` and is the join target for all shadow
rows. Auto-incrementing PK; user_id has no FK so import / Celery /
CLI saves can write rows without an active Flask user.
- Parent shadow tables for the three entity types:
``dashboards_version``, ``slices_version``, ``tables_version``.
- Child shadow tables for dataset children + dashboard M2M:
``table_columns_version``, ``sql_metrics_version``,
``dashboard_slices_version`` (composite PK on the M2M shadow,
matching the live ``dashboard_slices`` reshape from
sc-105349-composite-association-pks).
**Models** — ``Dashboard``, ``Slice``, ``SqlaTable`` (and dataset
children ``TableColumn`` / ``SqlMetric``) gain ``__versioned__``
class attributes. The exclude lists carry both M2M relationships
(``owners``, ``roles``, ``dashboards``) and the ``AuditMixin``
columns (``changed_on`` / ``created_on`` / ``changed_by_fk`` /
``created_by_fk`` plus ``last_saved_at`` / ``last_saved_by_fk``
on ``Slice``) so auto-bumped audit fields cannot trigger a
version row on their own (FR-025).
**Plugins** — ``superset/versioning/factory.py`` ships three
Continuum plugins:
- ``VersionTransactionFactory`` renames the transaction table and
appends the unconditional ``user_id`` column.
- ``VersioningFlaskPlugin`` sources the acting user from Superset's
``g.user`` rather than ``flask_login.current_user`` (Superset's
JWT auth populates ``g.user`` but leaves ``current_user``
anonymous on API routes).
- ``SkipUnmodifiedPlugin`` filters Continuum's UPDATE operations,
marking content-equivalent re-saves as ``processed=True`` so they
don't mint no-op shadow rows (FR-026; see follow-up commits for
the test). Lives in this commit because it shares the file with
the other plugins.
**Save-path glue** — a ``before_flush`` baseline listener
(``superset/versioning/baseline.py``) inserts an ``operation_type=0``
shadow row the first time a pre-existing entity is saved, including
the slice-baseline-under-dashboard pattern that gives the dashboard
M2M shadow a row to join against. ``UpdateDashboardCommand`` wraps
its body in ``no_autoflush`` so ``process_tab_diff`` /
``process_native_filter_diff`` don't fire intermediate flushes that
would mint extra version rows. ``DatasetDAO.update_columns`` is
rewritten as a natural-key upsert keyed on ``column_name`` so child
edits flow through ORM events Continuum sees.
**DAO** — ``superset/daos/version.py`` exposes the read API used by
the version endpoints in the next commits:
``current_version_number`` (0-based index, unstable under retention
pruning), ``current_live_transaction_id`` (stable across pruning),
``current_live_version_uuid`` (deterministic UUIDv5), plus
``list_versions`` / ``get_version`` / ``restore_version`` and a
batch ``list_change_records_batch`` for N+1 avoidance.
**Initialization** — ``superset/initialization/__init__.py`` wires
``init_versioning()`` after ``make_versioned()`` runs and the
versioned mappers are configured. Registers the baseline listener
plus the change-record listener (the latter's body lives in the
next commit but the registration site is here because it shares
the init function).
**Tests** — version-capture and version-list integration tests for
each entity type, plus a ``VersionDAO`` unit test suite. Retention
test uses a backdated ``issued_at`` so it can drive
``_prune_old_versions_impl`` synchronously.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three improvements from @aminghadersohi's review on
apache/superset#39859:
1. **`fk["name"]` unguarded in ``_downgrade_mysql_table`` re-add loop**
The drop loop gates on ``if fk_name := fk.get("name"):`` but the
re-add loop accessed ``fk["name"]`` unconditionally in an f-string.
MySQL/InnoDB always assigns FK names, so this branch was defensive,
but the asymmetry was confusing. Symmetrized via ``continue`` at the
top of the re-add loop.
2. **``ondelete`` whitelist before raw-SQL interpolation**
The value comes from MySQL's ``information_schema`` (not user
input), but interpolating a reflected string into raw SQL without a
guard left a "what if an unexpected value appears" footgun. Added
``_VALID_ONDELETE_ACTIONS`` (the four SQL-standard actions) and a
``RuntimeError`` when an unexpected value is reflected.
3. **Direct ALTER on PostgreSQL for tables with pre-existing UNIQUE**
``recreate="always"`` is dialect-agnostic — on PostgreSQL it
triggers ``CREATE TABLE AS SELECT → DROP → RENAME`` holding
``ACCESS EXCLUSIVE`` for the full table-copy duration. For a
multi-million-row ``dashboard_slices``, that lock window can be
noticeable. The reflected UNIQUE constraint has a stable name on
PostgreSQL (default ``<table>_<cols>_key`` convention), so dropping
it directly and then running structural change as direct ALTER
avoids the copy entirely.
The reflected UNIQUE name is wrapped in a new
``_drop_redundant_unique_by_name()`` helper. Postgres takes the
direct path; MySQL keeps ``recreate="always"`` because InnoDB binds
FKs to the UNIQUE's underlying index for back-reference (``DROP
CONSTRAINT`` on the UNIQUE there raises ``ERROR 1553``); SQLite
keeps ``recreate="always"`` because unnamed UNIQUEs reflect with
``name=None`` and can't be dropped by name.
Verified end-to-end: downgrade-then-upgrade against MySQL with
~12M total junction rows seeded completes in ~1m 41s (within the
range of the prior measurements).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Justin Park (@justinpark) reported on apache/superset#39859:
MySQLdb.OperationalError: (1832, "Cannot change column 'dashboard_id':
used in a foreign key constraint 'fk_dashboard_roles_dashboard_id_dashboards'")
Root cause: ``batch_op.alter_column(fk1, nullable=False)`` for the six
non-UNIQUE association tables emits ``ALTER COLUMN`` on a column that
participates in an FK constraint. MySQL 8 rejects this with ERROR 1832
when the table has data — even when the change is just ``NULL`` →
``NOT NULL`` and the column is already part of a freshly-added
composite primary key (which InnoDB has just made implicitly NOT NULL
anyway). The error fires on populated tables only; CI's ``test-mysql``
shard runs against empty tables and so didn't catch this, while a
real production-shaped install does.
The ``alter_column`` was only ever needed for SQLite, where composite
``PRIMARY KEY`` does not promote constituent columns to ``NOT NULL``
(a long-standing SQLite quirk — only ``INTEGER PRIMARY KEY`` does).
PostgreSQL and MySQL implicitly promote PK columns to ``NOT NULL`` as
part of ``ADD PRIMARY KEY``, so the explicit step is unnecessary on
both — and on MySQL it's actively broken on populated tables.
Fix: extract the ``alter_column`` pair into a helper
``_enforce_not_null_for_sqlite()`` that no-ops on Postgres and MySQL.
Both branches of the per-table upgrade (the ``recreate="always"`` path
for the two UNIQUE-bearing tables, and the direct-ALTER path for the
other six) now call the helper instead of inlining the
``alter_column``.
Verified end-to-end: downgrade-then-upgrade against MySQL with
~12M total junction rows (10M dashboard_slices + 1M each
slice_user/dashboard_user + 100K dashboard_roles) completes in
1m 39s with no ERROR 1832. The 44 in-memory SQLite tests still pass.
Considered Justin's alternative (drop FKs on MySQL across all eight
tables, unifying the two branches) but rejected as more invasive —
it would require capturing FK metadata and explicitly re-creating
the FKs for the six non-recreate tables, since they don't go through
the ``copy_from`` path that re-creates FKs automatically. The
SQLite-only approach is more targeted: it removes the operation that
MySQL rejects rather than working around the rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the stress-test seed script with an optional duplicate-row
injection step, used to measure the empirical cost of the migration's
``_dedupe_by_min_id`` phase.
Usage: after running the normal seed at a given scale, add
``--dirty-duplicates-pct 5`` (or any non-zero value) to inject that
percentage of duplicate ``(fk1, fk2)`` rows into each non-UNIQUE
junction (slice_user, dashboard_user, dashboard_roles —
dashboard_slices is skipped because its UNIQUE constraint, present
both pre- and post-migration, rejects duplicates).
Pre-condition: requires the DB to be at the pre-migration revision
(33d7e0e21daa). The post-migration composite PK rejects duplicates,
so attempting to inject on the upgraded schema errors out.
Empirical result on MySQL @ 10M dashboard_slices + ~2.1M other
junction rows + 105K injected duplicates (5% on the 3 non-UNIQUE
tables):
Upgrade time: 1m 36s vs clean baseline 1m 37s
→ dedupe cost is within measurement noise; the table-scan that
the migration already performs dominates whether or not
duplicates exist.
This empirically confirms what the cost-model predicted: the
``_dedupe_by_min_id`` GROUP BY scan is the dominant cost of that
phase, and the actual per-duplicate DELETE is negligible.
NULL-FK injection deliberately skipped — would require altering the
six non-UNIQUE FK columns from NOT NULL back to nullable (the
migration's downgrade keeps them NOT NULL by design), which adds
per-backend ALTER complexity for a code path that's structurally
identical in cost shape (DELETE WHERE col IS NULL is the same scan
shape as the dedupe scan).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add ``scripts/seed_junction_load.py``, a backend-agnostic script that
bulk-inserts synthetic parent rows (dashboards, slices, users, roles,
tables, dbs) and many-to-many junction rows for the four largest
association tables targeted by the composite-PK migration:
``dashboard_slices``, ``slice_user``, ``dashboard_user``,
``dashboard_roles``.
Designed for measuring migration runtime at varying scales — run with
a series of size flags (100K / 1M / 5M / 10M for the target table)
and time the migration at each scale to verify the predicted
``O(N log N)`` extrapolation against real numbers.
Properties:
- **Reproducible**: deterministic cross-product walk through parent IDs
produces a stable pair sequence; re-running is replayable.
- **Idempotent**: re-running with the same target is a no-op; with a
higher target, only new rows are added.
- **Backend-agnostic**: connects via Superset's standard ``DATABASE_*``
env vars (or ``SUPERSET__SQLALCHEMY_DATABASE_URI``). Branches on
dialect for ``BINARY(16)`` vs ``UUID`` vs TEXT/BLOB UUID columns.
- **Batched**: bulk INSERT 10K rows per statement.
- **Per-phase timing**: logs elapsed wall time for the parents phase,
the junctions phase as a whole, and per junction-table.
- **Avoidance set**: loads existing junction pairs into a Python set
so re-runs on top of pre-existing data don't collide on the
uniqueness constraint.
Usage (inside the Superset container):
docker exec superset-superset-1 \\
/app/.venv/bin/python /app/scripts/seed_junction_load.py \\
--dashboard-slices 1000000
Defaults target a "large multi-team install" shape: 1M
``dashboard_slices``, 100K each ``slice_user`` / ``dashboard_user``,
10K ``dashboard_roles``. Override per-table via flags.
Tested locally on MySQL (the user's current eval stack):
- 200/100/100/50 row mini-run produced expected counts.
- Re-running at the same target is a no-op (idempotent).
- ``--dry-run`` plans without writing.
Junction tables not yet covered (``sqlatable_user``, ``rls_filter_*``,
``report_schedule_user``) are typically small in production and
require additional parent seeding (RLS filters, report schedules)
that wasn't worth the scope here. Adding them is straightforward by
extending ``JUNCTIONS`` and writing the corresponding parent seeder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix two follow-on issues reported when starting the dev stack with
docker-compose-mysql.yml:
1. ``superset-init`` step 4 (load-examples) fails with
``MySQLdb.OperationalError: (2002, "Can't connect to server on 'db'")``
because the analytics-examples DB connection inherits ``EXAMPLES_PORT=5432``
(Postgres port) from ``docker/.env``. The override flipped
``DATABASE_DIALECT`` to ``mysql+mysqldb`` but left the EXAMPLES_*
group on Postgres defaults, so the URI became
``mysql+mysqldb://examples:examples@db:5432/examples`` — MySQL
container has no listener on 5432.
Fix: add ``EXAMPLES_HOST/PORT/DB/USER/PASSWORD`` and a complete
``SUPERSET__SQLALCHEMY_EXAMPLES_URI`` to the ``mysql-env`` anchor.
2. The Postgres init scripts under
``docker/docker-entrypoint-initdb.d/`` (``cypress-init.sh``,
``examples-init.sh``) get mounted on the MySQL container too —
compose merges volume lists. They invoke ``psql`` which doesn't
exist in the MySQL image, abort with ``psql: command not found``,
and prevent the ``examples`` DB from being created.
Fix: add a MySQL-specific init script
``docker/mysql-init/examples-init.sql`` that creates the
``examples`` database and user, and mount it at
``/docker-entrypoint-initdb.d`` in the override. Compose's
later-takes-precedence rule on duplicate volume targets displaces
the Postgres init dir, so the MySQL container only sees the
MySQL-compatible script.
(Used a plain duplicate-target mount rather than the ``!override``
tag because pre-commit's ``check-yaml`` doesn't recognize Compose's
custom YAML tags.)
Recovery for an existing failed MySQL stack: ``docker compose -f
docker-compose.yml -f docker-compose-mysql.yml down``, then
``docker volume rm superset_db_home_mysql`` (so the new init script
runs on the next fresh boot), then ``up`` again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ``docker-compose-mysql.yml``, a compose-override file that swaps
the default Postgres metadata DB for MySQL 8 with one extra ``-f``
flag:
docker compose -f docker-compose.yml -f docker-compose-mysql.yml up
Useful for evaluating dialect-specific behaviour (e.g., the runtime
cost of DDL migrations on a deployment whose production metadata DB
is MySQL — the question raised by review feedback on this PR).
Mirrors the connection settings used by CI's ``test-mysql`` shard:
``mysql+mysqldb`` dialect, charset ``utf8mb4`` with binary_prefix.
Host port defaults to 13306 (configurable via ``DATABASE_PORT_MYSQL``)
to avoid colliding with a native MySQL install on 3306.
A separate volume (``db_home_mysql``) keeps MySQL data isolated from
the Postgres ``db_home`` volume, so switching between the two with
``-f`` flag toggles doesn't corrupt either side.
The Postgres-specific init scripts under
``docker/docker-entrypoint-initdb.d/`` are not mounted on the MySQL
service (they are postgres-only). Examples / cypress fixtures still
load via ``superset-init``'s post-startup steps, which run
``superset load-examples`` against whichever metadata DB is in use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror of the PostgreSQL diagnostic queries added in 11148779ed,
adapted for MySQL/InnoDB. One important difference: InnoDB rebuilds
the clustered index on every PK change, so all eight tables undergo
a full table rebuild on MySQL — not just the two that go through
the explicit ``recreate="always"`` path. The lock-window estimate
query is updated to cover all eight rather than just two, and the
"migration_path" column makes the rebuild expectation explicit
("direct ALTER (still rebuilds InnoDB clustered index)").
Other notes:
- ``information_schema.TABLES.TABLE_ROWS`` is an InnoDB estimate,
analogous to PostgreSQL's ``reltuples``; documented inline.
- ``KEY_COLUMN_USAGE`` carries both sides of the FK in a single
row on MySQL, so the external-FK pre-flight check is simpler
than the PostgreSQL version (no joins between three views).
- The aggregated dedupe query is portable standard SQL; included
verbatim for copy-paste convenience.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a "Sizing the maintenance window on PostgreSQL" sub-section to the
operator runbook. The simple per-table COUNT/duplicate/NULL queries
that were already there are dialect-portable but only count rows;
operators on PostgreSQL with large deployments need to characterize
the migration's runtime cost before scheduling it.
Adds four diagnostic queries:
- Per-table size, row count (from pg_class.reltuples), and which
migration path each table will take (recreate-rewrite vs direct
ALTER). Sizes the work concretely.
- Aggregated duplicate-row roll-up: dup_groups + total rows_dropped
per table. Replaces eight separate per-table queries with one
consolidated result for audit/dump-before-apply decisions.
- External-FK pre-flight check (the same one the migration runs at
upgrade time and aborts on). Lets operators surface any blocking
external reference ahead of the maintenance window. Should be
empty on a stock install.
- Lock-window estimate for the two full-rewrite tables, using
pg_relation_size and a conservative 100 MB/s rewrite throughput
assumption. The other six use direct ALTER and are dominated by
composite-index build time (seconds for low-millions-of-rows
tables).
Prompted by reviewer feedback on apache/superset#39859 from a large
deployment asking how to size the maintenance window. The original
pre-flight queries are kept for cross-dialect operators (MySQL,
SQLite) since the new queries use PostgreSQL-specific catalog views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI cypress + playwright shards were red with:
ERROR [flask_migrate] Error: Multiple head revisions are present
for given argument 'head'
The recent rebase onto master pulled in
``33d7e0e21daa_add_semantic_layers_and_views.py`` (from PR #37815,
"semantic layer extension"), which had been authored against
``ce6bd21901ab`` as its parent — the same parent our migration
referenced. After the rebase both migrations point at
``ce6bd21901ab``, producing two heads and breaking ``flask db
upgrade head`` for any downstream consumer (CI's Cypress / Playwright
shards spin up a real Superset instance via ``superset db upgrade``,
which is why those shards failed first; the integration shards run
against a precomputed schema and didn't surface this).
Fix: chain our migration after the semantic-layer migration by
pointing ``down_revision`` at ``33d7e0e21daa``. The chain is now
linear:
... → ce6bd21901ab → 33d7e0e21daa (semantic layers)
→ 2bee73611e32 (composite PK, this PR)
Verified with ``superset db heads`` (returns single head
``2bee73611e32``) and the local migration test suite (44 passed,
1 skipped).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Found by running fresh-install + round-trip against a real SQLite DB:
6 of the 8 affected tables had FK columns that were originally
declared nullable. PostgreSQL and MySQL implicitly promote the
constituent columns of an ``ALTER TABLE ... ADD PRIMARY KEY`` to
``NOT NULL``; SQLite does not (it's a long-standing SQLite quirk —
only ``INTEGER PRIMARY KEY`` enforces NOT NULL on a composite-PK
column). Result: a fresh SQLite install would accept
``INSERT INTO dashboard_slices (NULL, 5)`` despite both columns
being part of the composite PK.
Our integration tests previously masked this: the test fixture seeds
columns with ``nullable=False``, so the post-upgrade NOT NULL
assertion passed regardless of whether the migration enforced it.
Fix: add explicit ``batch_op.alter_column(fk, nullable=False)`` for
both FK columns inside the per-table batch_alter_table block. On
PostgreSQL and MySQL this is a no-op (PK already implies NOT NULL);
on SQLite it adds the missing NOT NULL declaration so a fresh
install matches the data-model.md "After" contract.
Verified end-to-end:
- Postgres + MySQL: column shape unchanged (still NOT NULL)
- SQLite fresh install + round-trip: all 8 tables now have NOT NULL
on FK columns, ``INSERT (NULL, 5)`` correctly rejected with
IntegrityError on dashboard_slices, dashboard_user, sqlatable_user
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two MySQL-only failures in the downgrade path, found by running the
full migration history against a fresh MySQL 8 container:
1. ``MySQLdb.OperationalError: (1553, "Cannot drop index 'PRIMARY':
needed in a foreign key constraint")``. InnoDB uses the composite
PK index to back the FK on the leftmost column. The downgrade
tried to drop the composite PK before dropping the FKs, orphaning
the FK's backing index. PostgreSQL and SQLite create separate
indexes for FK columns and don't trip on this.
2. ``Field 'id' doesn't have a default value`` on subsequent INSERT.
``sa.Identity(always=False)`` only emits ``AUTO_INCREMENT`` on
MySQL when the column is created with ``primary_key=True`` — our
portable path adds the column first then creates the PK separately,
so MySQL leaves the column without auto-generation. Existing rows
would all collide on id=0; future inserts fail because no default.
Postgres' ``GENERATED BY DEFAULT AS IDENTITY`` and SQLite's
``INTEGER PRIMARY KEY`` rowid alias don't have this gap.
Fix: extract ``_downgrade_mysql_table()`` that emits the canonical
MySQL idiom — drop FKs, then a single ALTER combining
``DROP PRIMARY KEY, ADD COLUMN id INT NOT NULL AUTO_INCREMENT,
ADD PRIMARY KEY (id)`` (which backfills existing rows with sequential
ids and preserves AUTO_INCREMENT), restore the redundant UNIQUE on
the 2 tables that originally had it, and re-add the FKs with their
original names. Postgres and SQLite keep the existing portable
``batch_alter_table`` path.
Raw SQL is unavoidable for the combined-ALTER form; per the
constitution it's allowed for dialect-specific DDL with no SQLA
equivalent, with triple-quoted strings for legibility.
Verified end-to-end: upgrade → downgrade → upgrade against a fresh
MySQL 8 container with INSERT-without-id sanity check showing the
restored ``id`` column auto-increments correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI test-mysql failed with:
MySQLdb.OperationalError: (1826, "Duplicate foreign key constraint
name 'fk_dashboard_slices_slice_id_slices'")
Root cause: MySQL scopes foreign-key constraint names per-database,
not per-table (PostgreSQL and SQLite scope per-table). The
``batch_alter_table(... recreate="always", copy_from=...)`` path
used for ``dashboard_slices`` and ``report_schedule_user`` builds
``_alembic_tmp_<table>`` carrying the original FK names from
``copy_from`` while the original table still holds those names — MySQL
rejects the temp-table creation with ERROR 1826.
Fix: on MySQL only, drop the original FK constraints by name before
the ``batch_alter_table`` runs. The ``copy_from`` re-creates them on
the rebuilt table with their original names, so the post-migration
shape is unchanged. On PostgreSQL and SQLite the original code path
still runs unchanged.
Local SQLite tests (44 passed, 1 skipped) still pass; CI will validate
on MySQL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Beto's review comments on apache/superset#39859: replace
``sa.text(f"...")`` SQL construction in the three pre-flight helpers
(``_delete_null_fk_rows``, ``_dedupe_by_min_id``, ``_assert_no_duplicates``)
with SQLAlchemy core constructs (``sa.delete``, ``sa.select``,
``sa.func``, ``.subquery()``, ``.notin_()``).
A small ``_table_clause()`` helper builds a lightweight ``TableClause``
exposing the columns the queries reference; the three helpers consume
it. Removes all ``# noqa: S608`` comments — they are no longer needed
because there is no string-interpolated SQL.
Verified the compiled SQL is identical on Postgres, MySQL, and SQLite,
including the MySQL ERROR 1093 workaround (the inner aggregation is
wrapped in a derived table via ``.subquery()``, producing
``... NOT IN (SELECT keep_id FROM (SELECT min(id) ...) AS keep_min)``).
Also drops the redundant ``f`` prefix on the two non-interpolating
lines of the ``_check_no_external_fks_to_id`` error message.
44 migration tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four operator-experience improvements from the second review pass:
1. ``TABLES_WITH_NULLABLE_FKS`` is now explicitly documented as an
informational set that is not consulted at runtime; the comment
explains the previous ``dashboard_roles`` omission was the bug
that motivated the always-run cleanup.
2. ``_delete_null_fk_rows`` docstring updated to match the
"always run" semantics (was still claiming "called only on tables
in TABLES_WITH_NULLABLE_FKS").
3. ``_check_no_external_fks_to_id`` now documents its scope
limitation: ``Inspector.get_table_names()`` returns the default
schema only, so cross-schema FKs in non-standard multi-schema
PostgreSQL deployments would not be caught. The single-schema
case (Superset's documented deployment) is fully covered.
4. ``_dedupe_by_min_id`` now logs a sample of up to 10 discarded
``(fk1, fk2, id)`` tuples at WARN before deletion, so operators
can audit which rows the ``MIN(id)`` policy drops. The keep-
original policy is correct in practice but discards later
re-grants on ownership tables; the sample makes that visible.
5. ``UPDATING.md`` documents the upgrade/downgrade primary-key
name divergence (``pk_<table>`` vs ``<table>_pkey``) so
operators using schema-comparison tools don't mistake it for
migration drift.
No schema or runtime-behaviour changes. All 44 migration tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cleanups from PR review:
1. ``dashboard_roles.dashboard_id`` was created nullable in revision
e11ccdd12658 but was missing from ``TABLES_WITH_NULLABLE_FKS``. A
production database with a stray NULL ``dashboard_id`` row would have
failed the PK-add with a cryptic constraint violation. Fix by running
the NULL-FK cleanup on every affected table — it is a no-op DELETE on
tables whose FK columns are already NOT NULL, and it eliminates the
risk of further drift in the hardcoded set. ``dashboard_roles`` is
added to the documentation set; the runtime now does not consult it.
2. The unit-test parent-table name for ``rls_filter_roles`` and
``rls_filter_tables`` was ``rls_filter`` (does not exist) instead of
the real parent ``row_level_security_filters``. Test passes either
way (the in-memory FK is self-consistent), but the parameter is now
accurate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace synthetic id INTEGER PRIMARY KEY with composite PRIMARY KEY (fk1, fk2)
on the eight pure-junction tables: dashboard_roles, dashboard_slices,
dashboard_user, report_schedule_user, rls_filter_roles, rls_filter_tables,
slice_user, sqlatable_user. The redundant UNIQUE(fk1, fk2) on dashboard_slices
and report_schedule_user is dropped (subsumed by the new PK).
Migration handles dialect quirks: copy_from for tables with pre-existing
UNIQUE (so SQLite's anonymous-constraint reflection doesn't matter), wrapped-
subquery dedupe for MySQL (ERROR 1093), sa.Identity(always=False) on downgrade
to backfill the restored id column without NOT NULL violations, and distinct
PK names per direction (pk_<table> on upgrade, <table>_pkey on downgrade) to
avoid round-trip index-name collisions on Postgres.
ORM Table() definitions updated to match. UPDATING.md entry added with
operator runbook (BI-tool impact, pre-flight inventory queries, dedupe-row-
loss notice, pg_dump workaround, FK-NOT-NULL downgrade asymmetry note).
Tests: 8 schema-shape assertions (post-upgrade), 8 duplicate-rejection unit
tests, 8 distinct-pair sanity tests, 1 round-trip + idempotency test
(in-memory SQLite via Alembic MigrationContext).
Continuum-restore verification against the new shape is out of scope for this
PR; it is the responsibility of the versioning epic (sc-103156).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-04-01 14:27:11 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-04-01 13:30:26 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-04-01 09:34:01 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Joe Li
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com>
For complete documentation on using GitHub Codespaces with Apache Superset, please see:
**[Setting up a Development Environment - GitHub Codespaces](https://superset.apache.org/docs/contributing/development#github-codespaces-cloud-development)**
## Pre-installed Development Environment
When you create a new Codespace from this repository, it automatically:
1.**Creates a Python virtual environment** using `uv venv`
2.**Installs all development dependencies** via `uv pip install -r requirements/development.txt`
3.**Sets up pre-commit hooks** with `pre-commit install`
4.**Activates the virtual environment** automatically in all terminals
The virtual environment is located at `/workspaces/{repository-name}/.venv` and is automatically activated through environment variables set in the devcontainer configuration.
We kindly ask you to include the following information in your report:
- Apache Superset version that you are using
- A sanitized copy of your `superset_config.py` file or any config overrides
-Detailed steps to reproduce the vulnerability
**Submission Standards & AI Policy**
To ensure engineering focus remains on verified risks and to manage high reporting volumes, all reports must meet the following criteria:
-Plain Text Format: In accordance with Apache guidelines, please provide all details in plain text within the email body. Avoid sending PDFs, Word documents, or password-protected archives.
- Mandatory AI Disclosure: If you utilized Large Language Models (LLMs) or AI tools to identify a flaw or assist in writing a report, you must disclose this in your submission so our triage team can contextualize the findings.
- Human-Verified PoC: All submissions must include a manual, step-by-step Proof of Concept (PoC) performed on a supported release. Raw AI outputs, hypothetical chat transcripts, or unverified scanner logs will be closed as Invalid.
We kindly ask you to include the following information in your report to assist our developers in triaging and remediating issues efficiently:
- Version/Commit: The specific version of Apache Superset or the Git commit hash you are using.
- Configuration: A sanitized copy of your `superset_config.py` file or any config overrides.
- Environment: Your deployment method (e.g., Docker Compose, Helm, or source) and relevant OS/Browser details.
- Impacted Component: Identification of the affected area (e.g., Python backend, React frontend, or a specific database connector).
- Expected vs. Actual Behavior: A clear description of the intended system behavior versus the observed vulnerability.
- Detailed Reproduction Steps: Clear, manual steps to reproduce the vulnerability.
**Vulnerability Definition**
Apache Superset considers a security vulnerability to be a demonstrable issue that has meaningful impact on confidentiality, integrity, or availability beyond the intended security model. Low-impact boundary variations or technical edge cases in existing access controls may be classified as hardening improvements rather than vulnerabilities, even if exploitable.
**Out of Scope Vulnerabilities**
To prioritize engineering efforts on genuine architectural risks, the following scenarios are explicitly out of scope and will not be issued a CVE:
- **Attacks requiring Admin privileges**: (e.g., CSS injection, template manipulation, dashboard ownership overrides, or modifying global system settings). Per the CVE vulnerability definition in CNA Operational Rules 4.1, a qualifying vulnerability must allow violation of a security policy. The Admin role is a fully trusted operational boundary defined by Apache Superset's security policy; actions within this boundary do not violate that policy and are therefore considered intended capabilities 'by design,' not vulnerabilities.
- **Brute Force and Rate Limiting**: Reports targeting a lack of resource exhaustion protections, generic rate-limiting, or volumetric Denial of Service (DoS) attempts.
- **Theoretical attack vectors**: Issues without a demonstrable, reproducible exploit path.
- **Non-Exploitable Findings**: Missing security headers, generic banner disclosures, or descriptive error messages that do not lead to a direct, documented exploit.
- **User enumeration**: API responses, timing differences, or error messages that reveal whether user accounts, IDs, dashboards, or datasets exist.
- **Information disclosure (low impact)**: Software version disclosure, generic error messages, stack traces without sensitive data exposure, or system configuration details that don't enable further exploitation.
- **Resource exhaustion requiring authentication**: Denial of Service attacks that require valid user credentials and don't bypass rate limiting or resource controls.
- **Missing security headers**: Without demonstration of a concrete exploit scenario that leverages the missing header.
**Outcome of Reports**
Reports that are deemed out-of-scope for a CVE but represent valid security best practices or hardening opportunities may be converted into public GitHub issues. This allows the community to contribute to the general hardening of the platform even when a specific vulnerability threshold is not met.
Note that Apache Superset is not responsible for any third-party dependencies that may
have security issues. Any vulnerabilities found in third-party dependencies should be
@@ -29,6 +59,13 @@ reported to the maintainers of those projects. Results from security scans of Ap
Superset dependencies found on its official Docker image can be remediated at release time
by extending the image itself.
**Vulnerability Aggregation & CVE Attribution**
In accordance with MITRE CNA Operational Rules (4.1.10, 4.1.11, and 4.2.13), Apache Superset issues CVEs based on the underlying architectural root cause rather than the number of affected endpoints or exploit payloads.
- Aggregation: If multiple exploit vectors stem from the same programmatic failure or shared vulnerable code, they must be aggregated into a single, comprehensive report.
- Independent Fixes: Separate CVEs will only be assigned if the vulnerabilities reside in decoupled architectural modules and can be fixed independently of one another.
Reports that fail to aggregate related findings will be merged during triage to ensure an accurate and defensible CVE record.
**Your responsible disclosure and collaboration are invaluable.**
`❗ @${pull.user.login} Your base branch \`${currentBranch}\` has ` +
'also updated `superset/migrations`.\n' +
'\n' +
'**Please consider rebasing your branch and [resolving potential db migration conflicts](https://github.com/apache/superset/blob/master/CONTRIBUTING.md#merging-db-migrations).**',
'**Please consider rebasing your branch and [resolving potential db migration conflicts](https://superset.apache.org/docs/contributing/development#merging-db-migrations).**',
Looks like your PR contains new `.js` or `.jsx` files:
```
${{steps.check.outputs.js_files_added}}
```
As decided in [SIP-36](https://github.com/apache/superset/issues/9101), all new frontend code should be written in TypeScript. Please convert above files to TypeScript then re-request review.
A modern, enterprise-ready business intelligence web application.
### Documentation
- **[User Guide](https://superset.apache.org/user-docs/)** — For analysts and business users. Explore data, build charts, create dashboards, and connect databases.
- **[Administrator Guide](https://superset.apache.org/admin-docs/)** — Install, configure, and operate Superset. Covers security, scaling, and database drivers.
- **[Developer Guide](https://superset.apache.org/developer-docs/)** — Contribute to Superset or build on its REST API and extension framework.
[**Why Superset?**](#why-superset) |
[**Supported Databases**](#supported-databases) |
[**Installation and Configuration**](#installation-and-configuration) |
Superset can query data from any SQL-speaking datastore or data engine (Presto, Trino, Athena, [and more](https://superset.apache.org/docs/configuration/databases)) that has a Python DB-API driver and a SQLAlchemy dialect.
Superset can query data from any SQL-speaking datastore or data engine (Presto, Trino, Athena, [and more](https://superset.apache.org/docs/databases)) that has a Python DB-API driver and a SQLAlchemy dialect.
Here are some of the major database solutions that are supported:
**A more comprehensive list of supported databases** along with the configuration instructions can be found [here](https://superset.apache.org/docs/configuration/databases).
**A more comprehensive list of supported databases** along with the configuration instructions can be found [here](https://superset.apache.org/docs/databases).
Want to add support for your datastore or data engine? Read more [here](https://superset.apache.org/docs/frequently-asked-questions#does-superset-work-with-insert-database-engine-here) about the technical requirements.
@@ -161,14 +195,14 @@ Try out Superset's [quickstart](https://superset.apache.org/docs/quickstart/) gu
to find resources around contributing along with a detailed guide on
how to set up a development environment.
## Resources
- [Superset "In the Wild"](https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md) - open a PR to add your org to the list!
- [Feature Flags](https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md) - the status of Superset's Feature Flags.
- [Superset "In the Wild"](https://superset.apache.org/inTheWild) - see who's using Superset, and [add your organization](https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.yaml) to the list!
- [Feature Flags](https://superset.apache.org/docs/configuration/feature-flags) - the status of Superset's Feature Flags.
- [Standard Roles](https://github.com/apache/superset/blob/master/RESOURCES/STANDARD_ROLES.md) - How RBAC permissions map to roles.
- [Superset Wiki](https://github.com/apache/superset/wiki) - Tons of additional community resources: best practices, community content and other information.
- [Superset SIPs](https://github.com/orgs/apache/projects/170) - The status of Superset's SIPs (Superset Improvement Proposals) for both consensus and implementation status.
# Part 2: Verify RSA key - this is the same as running `gpg --verify {release}.asc {release}` and comparing the RSA key and email address against the KEYS file # noqa: E501
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Superset Feature Flags
This is a list of the current Superset optional features. See config.py for default values. These features can be turned on/off by setting your preferred values in superset_config.py to True/False respectively
## In Development
These features are considered **unfinished** and should only be used on development environments.
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
- ALERT_REPORT_TABS
- DATE_RANGE_TIMESHIFTS_ENABLED
- ENABLE_ADVANCED_DATA_TYPES
- PRESTO_EXPAND_DATA
- SHARE_QUERIES_VIA_KV_STORE
- TAGGING_SYSTEM
- CHART_PLUGINS_EXPERIMENTAL
## In Testing
These features are **finished** but currently being tested. They are usable, but may still contain some bugs.
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
These features flags currently default to True and **will be removed in a future major release**. For this current release you can turn them off by setting your config to False, but it is advised to remove or set these flags in your local configuration to **True** so that you do not experience any unexpected changes in a future release.
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
## Superset Users in the Wild
Here's a list of organizations, broken down into broad industry categories, that have taken the time to send a PR to let
the world know they are using Apache Superset. If you are a user and want to be recognized,
all you have to do is file a simple PR [like this one](https://github.com/apache/superset/pull/10122) — [just click here](https://github.com/apache/superset/edit/master/RESOURCES/INTHEWILD.md) to do so. If you think
the categorization is inaccurate, please file a PR with your correction as well.
| Permission/role description |Admins have all possible rights, including granting or revoking rights from other users and altering other people’s slices and dashboards.|Alpha users have access to all data sources, but they cannot grant or revoke access from other users. They are also limited to altering the objects that they own. Alpha users can add and alter data sources.|Gamma users have limited access. They can only consume data coming from data sources they have been given access to through another complementary role. They only have access to view the slices and dashboards made from data sources that they have access to. Currently Gamma users are not able to alter or add data sources. We assume that they are mostly content consumers, though they can create slices and dashboards.|The sql_lab role grants access to SQL Lab. Note that while Admin users have access to all databases by default, both Alpha and Gamma users need to be given access on a per database basis.||
| can read on SavedQuery |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can write on SavedQuery |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can read on CssTemplate |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on CssTemplate |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on ReportSchedule |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on ReportSchedule |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on Chart |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on Chart |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on Annotation |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on Annotation |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on Dataset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on Dataset |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on Log|:heavy_check_mark:|O|O|O|
| can write on Log |:heavy_check_mark:|O|O|O|
| can read on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on Database |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can write on Database |:heavy_check_mark:|O|O|O|
| can read on Query |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can this form get on ResetPasswordView |:heavy_check_mark:|O|O|O|
| can this form post on ResetPasswordView |:heavy_check_mark:|O|O|O|
| can this form get on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can this form post on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can this form get on UserInfoEditView |:heavy_check_mark:|O|O|O|
| can this form post on UserInfoEditView |:heavy_check_mark:|O|O|O|
| can show on UserDBModelView |:heavy_check_mark:|O|O|O|
| can edit on UserDBModelView |:heavy_check_mark:|O|O|O|
| can delete on UserDBModelView |:heavy_check_mark:|O|O|O|
| can add on UserDBModelView |:heavy_check_mark:|O|O|O|
| can list on UserDBModelView |:heavy_check_mark:|O|O|O|
| can userinfo on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| resetmypassword on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| resetpasswords on UserDBModelView |:heavy_check_mark:|O|O|O|
| userinfoedit on UserDBModelView |:heavy_check_mark:|O|O|O|
| can show on RoleModelView |:heavy_check_mark:|O|O|O|
| can edit on RoleModelView |:heavy_check_mark:|O|O|O|
| can delete on RoleModelView |:heavy_check_mark:|O|O|O|
| can add on RoleModelView |:heavy_check_mark:|O|O|O|
| can list on RoleModelView |:heavy_check_mark:|O|O|O|
| copyrole on RoleModelView |:heavy_check_mark:|O|O|O|
| can get on OpenApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can show on SwaggerView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can get on MenuApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can list on AsyncEventsRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can invalidate on CacheRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can csv upload on Database |:heavy_check_mark:|O|O|O|
| can excel upload on Database |:heavy_check_mark:|O|O|O|
| can query form data on Api |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can query on Api |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can time range on Api |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can external metadata on Datasource |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can save on Datasource |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get on Datasource |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can my queries on SqlLab |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can log on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can import dashboards on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can schemas on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can sqllab history on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can publish on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can csv on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can slice on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can sync druid source on Superset |:heavy_check_mark:|O|O|O|
| can explore on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can approve on Superset |:heavy_check_mark:|O|O|O|
| can explore json on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can fetch datasource metadata on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can csrf token on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can sqllab on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can select star on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can warm up cache on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can sqllab table viz on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can available domains on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can request access on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can dashboard on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can post on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can expanded on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can delete on TableSchemaView |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can get on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can post on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can delete query on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can migrate query on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can activate on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can delete on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can put on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can read on SecurityRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| menu access on Security |:heavy_check_mark:|O|O|O|
| menu access on List Users |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on List Roles |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Action Log |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Manage|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Annotation Layers |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on CSS Templates |:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Import Dashboards |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Data |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Databases |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Datasets |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Charts |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Dashboards |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on SQL Lab |:heavy_check_mark:|O|O|:heavy_check_mark:|
| menu access on SQL Editor |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| menu access on Saved Queries |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| menu access on Query Search |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| all datasource access on all_datasource_access |:heavy_check_mark:|:heavy_check_mark:|O|O|
| all database access on all_database_access |:heavy_check_mark:|:heavy_check_mark:|O|O|
| all query access on all_query_access |:heavy_check_mark:|O|O|O|
| can write on DynamicPlugin |:heavy_check_mark:|O|O|O|
| can edit on DynamicPlugin |:heavy_check_mark:|O|O|O|
| can list on DynamicPlugin |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can show on DynamicPlugin |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can download on DynamicPlugin |:heavy_check_mark:|O|O|O|
| can add on DynamicPlugin |:heavy_check_mark:|O|O|O|
| can delete on DynamicPlugin |:heavy_check_mark:|O|O|O|
| can external metadata by name on Datasource |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can get value on KV |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can store on KV |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can tagged objects on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can suggestions on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can get on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can post on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can delete on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can edit on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can list on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can show on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can add on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can delete on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| muldelete on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can edit on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can list on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can show on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can add on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can delete on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| muldelete on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can edit on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can list on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can show on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can add on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can delete on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can list on AlertLogModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can show on AlertLogModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can list on AlertObservationModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can show on AlertObservationModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Row Level Security |:heavy_check_mark:|O|O|O|
| menu access on Access requests |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Home |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Plugins |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Dashboard Email Schedules |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Chart Emails |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Alerts |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Alerts & Report |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| menu access on Scan New Datasources |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can share dashboard on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can share chart on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can this form get on ColumnarToDatabaseView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can this form post on ColumnarToDatabaseView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can export on Chart |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on DashboardFilterStateRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on DashboardFilterStateRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on DashboardPermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on DashboardPermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can delete embedded on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can set embedded on Dashboard |:heavy_check_mark:|O|O|O|
| can export on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can get embedded on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can export on Database |:heavy_check_mark:|O|O|O|
| can export on Dataset |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can write on ExploreFormDataRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on ExploreFormDataRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on ExplorePermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on ExplorePermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can export on ImportExportRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can import on ImportExportRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can export on SavedQuery |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
| can dashboard permalink on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can grant guest token on SecurityRestApi |:heavy_check_mark:|O|O|O|
| can read on AdvancedDataType |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on EmbeddedDashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can duplicate on Dataset |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on Explore |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can samples on Datasource |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on AvailableDomains |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can get or create dataset on Dataset |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get column values on Datasource |:heavy_check_mark:|:heavy_check_mark:|O|O|
| can export csv on SQLLab |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can get results on SQLLab |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can execute sql query on SQLLab |:heavy_check_mark:|O|O|:heavy_check_mark:|
| can recent activity on Log |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| Permission/role description |Admins have all possible rights, including granting or revoking rights from other users and altering other people's slices and dashboards.|Alpha users have access to all data sources, but they cannot grant or revoke access from other users. They are also limited to altering the objects that they own. Alpha users can add and alter data sources.|Gamma users have limited access. They can only consume data coming from data sources they have been given access to through another complementary role. They only have access to view the slices and dashboards made from data sources that they have access to. Currently Gamma users are not able to alter or add data sources. We assume that they are mostly content consumers, though they can create slices and dashboards.|Public is the most restrictive built-in role, designed for anonymous/unauthenticated users viewing public dashboards. It provides minimal read-only access for dashboard viewing with interactive filters. Use `PUBLIC_ROLE_LIKE = "Public"` to apply these permissions to anonymous users.|The sql_lab role grants access to SQL Lab. Note that while Admin users have access to all databases by default, both Alpha and Gamma users need to be given access on a per database basis.||
| can read on SavedQuery |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can write on SavedQuery |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can read on CssTemplate |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on CssTemplate |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on ReportSchedule |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can write on ReportSchedule |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on Chart |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on Chart |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on Annotation |:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|O|
| can write on Annotation |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can read on AnnotationLayerRestApi |:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|O|
| can read on Dataset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can write on Dataset|:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can read on Log |:heavy_check_mark:|O|O|O|O|
| can write on Log |:heavy_check_mark:|O|O|O|O|
| can read on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on Database |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can write on Database |:heavy_check_mark:|O|O|O|O|
| can read on Query |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can this form get on ResetPasswordView |:heavy_check_mark:|O|O|O|O|
| can this form post on ResetPasswordView |:heavy_check_mark:|O|O|O|O|
| can this form get on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can this form post on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can this form get on UserInfoEditView |:heavy_check_mark:|O|O|O|O|
| can this form post on UserInfoEditView |:heavy_check_mark:|O|O|O|O|
| can show on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can edit on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can delete on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can add on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can list on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can userinfo on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| resetmypassword on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| resetpasswords on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| userinfoedit on UserDBModelView |:heavy_check_mark:|O|O|O|O|
| can show on RoleModelView |:heavy_check_mark:|O|O|O|O|
| can edit on RoleModelView |:heavy_check_mark:|O|O|O|O|
| can delete on RoleModelView |:heavy_check_mark:|O|O|O|O|
| can add on RoleModelView |:heavy_check_mark:|O|O|O|O|
| can list on RoleModelView |:heavy_check_mark:|O|O|O|O|
| copyrole on RoleModelView |:heavy_check_mark:|O|O|O|O|
| can get on OpenApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can show on SwaggerView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get on MenuApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can list on AsyncEventsRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can invalidate on CacheRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can csv upload on Database |:heavy_check_mark:|O|O|O|O|
| can excel upload on Database |:heavy_check_mark:|O|O|O|O|
| can query form data on Api |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can query on Api |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can time range on Api |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can external metadata on Datasource |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can save on Datasource |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can get on Datasource |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can my queries on SqlLab |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can log on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can import dashboards on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can schemas on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can sqllab history on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can publish on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can csv on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can slice on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can sync druid source on Superset |:heavy_check_mark:|O|O|O|O|
| can explore on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can approve on Superset |:heavy_check_mark:|O|O|O|O|
| can explore json on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can fetch datasource metadata on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can csrf token on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can sqllab on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can select star on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can warm up cache on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can sqllab table viz on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can available domains on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can request access on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can dashboard on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can post on TableSchemaView |:heavy_check_mark:|O|O|O|:heavy_check_mark:|
| can expanded on TableSchemaView |:heavy_check_mark:|O|O|O|:heavy_check_mark:|
| can delete on TableSchemaView |:heavy_check_mark:|O|O|O|:heavy_check_mark:|
| can get on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can post on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can delete query on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can migrate query on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can activate on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can delete on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can put on TabStateView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can read on SecurityRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| menu access on Security |:heavy_check_mark:|O|O|O|O|
| menu access on List Users |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on List Roles |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Action Log|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Manage |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| menu access on Annotation Layers |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on CSS Templates |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| menu access on Import Dashboards |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Data |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Databases |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Datasets |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Charts |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Dashboards |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on SQL Lab |:heavy_check_mark:|O|O|O|:heavy_check_mark:|
| menu access on SQL Editor |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| menu access on Saved Queries |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| menu access on Query Search |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| all datasource access on all_datasource_access |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| all database access on all_database_access |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| all query access on all_query_access |:heavy_check_mark:|O|O|O|O|
| can write on DynamicPlugin |:heavy_check_mark:|O|O|O|O|
| can edit on DynamicPlugin |:heavy_check_mark:|O|O|O|O|
| can list on DynamicPlugin |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can show on DynamicPlugin |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can download on DynamicPlugin |:heavy_check_mark:|O|O|O|O|
| can add on DynamicPlugin |:heavy_check_mark:|O|O|O|O|
| can delete on DynamicPlugin |:heavy_check_mark:|O|O|O|O|
| can external metadata by name on Datasource |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get value on KV |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can store on KV |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can tagged objects on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can suggestions on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can post on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can delete on TagView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can edit on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can list on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can show on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can add on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can delete on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| muldelete on DashboardEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can edit on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can list on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can show on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can add on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can delete on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| muldelete on SliceEmailScheduleView |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can edit on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can list on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can show on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can add on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can delete on AlertModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can list on AlertLogModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can show on AlertLogModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can list on AlertObservationModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can show on AlertObservationModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Row Level Security |:heavy_check_mark:|O|O|O|O|
| menu access on Access requests |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Home |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Plugins |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Dashboard Email Schedules |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Chart Emails |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Alerts |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Alerts & Report |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| menu access on Scan New Datasources |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can share dashboard on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can share chart on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can this form get on ColumnarToDatabaseView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can this form post on ColumnarToDatabaseView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can export on Chart |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can write on DashboardFilterStateRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can read on DashboardFilterStateRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can write on DashboardPermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on DashboardPermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can delete embedded on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can set embedded on Dashboard |:heavy_check_mark:|O|O|O|O|
| can export on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get embedded on Dashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can export on Database |:heavy_check_mark:|O|O|O|O|
| can export on Dataset |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can write on ExploreFormDataRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on ExploreFormDataRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can write on ExplorePermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on ExplorePermalinkRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can export on ImportExportRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can import on ImportExportRestApi |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can export on SavedQuery |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
| can dashboard permalink on Superset |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can grant guest token on SecurityRestApi |:heavy_check_mark:|O|O|O|O|
| can read on AdvancedDataType |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can read on EmbeddedDashboard |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
| can duplicate on Dataset |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can read on Explore |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can samples on Datasource |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can read on AvailableDomains |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
| can get or create dataset on Dataset |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can get column values on Datasource |:heavy_check_mark:|:heavy_check_mark:|O|O|O|
| can export csv on SQLLab |:heavy_check_mark:|O|O|O|:heavy_check_mark:|
| can get results on SQLLab |:heavy_check_mark:|O|O|O|:heavy_check_mark:|
| can execute sql query on SQLLab |:heavy_check_mark:|O|O|O|:heavy_check_mark:|
| can recent activity on Log |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
@@ -23,8 +23,649 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
### Entity version history for charts, dashboards, and datasets
Saves of charts, dashboards, and datasets now automatically produce a version history — browsable and restorable via new API endpoints. No frontend UI in this release; the backend plumbing is the deliverable.
**New endpoints** (per entity type — same pattern for `chart`, `dashboard`, and `dataset`):
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/v1/{resource}/<uuid>/versions/` | List the entity's version history (0-based `version_number`, `version_uuid`, `issued_at`, `changed_by`) |
| `GET` | `/api/v1/{resource}/<uuid>/versions/<version_uuid>/` | Get a single version snapshot (scalar fields at that version; plus `columns` / `metrics` for datasets) |
| `POST` | `/api/v1/{resource}/<uuid>/versions/<version_uuid>/restore` | Restore the entity to the state captured by that version |
`<version_uuid>` is a deterministic `UUIDv5` derived from the entity's UUID and the Continuum transaction id — stable across replicas and retention pruning. Authorisation reuses the resource's existing `can_write` permission; workspace admins can list/restore any entity.
**Version response shape — `changes` array:**
Each entry returned by `GET /api/v1/{resource}/<uuid>/versions/` and `GET .../versions/<version_uuid>/` includes a `changes` array describing what changed relative to the previous version:
The array is empty for baseline (`operation_type=0`) transactions. `kind` enumerates structured record types (`field`, layout-walker records for dashboards, dataset child diffs for `TableColumn` / `SqlMetric`); `path` is a dotted JSON-pointer-style locator; `from_value` / `to_value` are JSON-safe scalars or compact records.
**Save-response and ETag headers:**
- Save responses (`PUT /api/v1/{resource}/<pk>`) include `old_version_uuid` and `new_version_uuid` body fields so the client can correlate a save with its resulting version row.
- All entity GETs (`GET /api/v1/{chart,dashboard,dataset}/<pk>`), version-list GETs, single-version GETs, and save responses emit an `ETag: "<version_uuid>"` header reflecting the entity's current live version. The default `CORS_OPTIONS` now sets `expose_headers: ["ETag"]` so cross-origin browser clients can read the header. **No `If-Match` enforcement in v1** — `ETag` is informational; concurrent-edit detection is deferred to a follow-up SIP.
- **Operators overriding `CORS_OPTIONS` in `superset_config.py` MUST include `"expose_headers": ["ETag"]`** (or merge with the default) for cross-origin clients to read the ETag. A bare `CORS_OPTIONS = {"origins": [...]}` will silently drop the expose-headers default.
**Behaviour changes on save:**
- Every save of a chart, dashboard, or dataset produces one new version row. Rows preserve the full post-save state (scalar fields for all three entity types; `TableColumn` / `SqlMetric` children for datasets; `dashboard_slices` chart membership for dashboards — children versioned via SQLAlchemy-Continuum shadow tables `table_columns_version`, `sql_metrics_version`, and `dashboard_slices_version`).
- First save after an entity already exists in the DB creates a retroactive baseline version so the UI can show "what this looked like before I edited it."
- Tags, owners, and roles are **not** versioned in v1 (ADR-005). A restore leaves those at their live values.
**New config key:**
| Key | Default | Purpose |
|---|---|---|
| `SUPERSET_VERSION_HISTORY_RETENTION_DAYS` | `30` | Versions older than this many days are pruned by a nightly Celery beat task (`superset.tasks.version_history_retention.prune_old_versions`). Each entity's live row (`end_transaction_id IS NULL`) is always preserved; closed historical rows including the baseline age out with the rest. Set to `0` to disable retention entirely. |
**Impact on external integrations:**
- New tables populated on every save — `dashboards_version`, `slices_version`, `tables_version` (parent shadow tables for the three entity types), `table_columns_version`, `sql_metrics_version`, `dashboard_slices_version` (child shadow tables), plus the shared `version_transaction` and `version_changes` tables. External tooling that queries Superset's DB directly will see writes to these tables proportional to save traffic.
- Existing entity endpoints (`GET`/`PUT /api/v1/{chart,dashboard,dataset}/<pk>`) gain an `ETag` response header and the save response gains `old_version_uuid` / `new_version_uuid` body fields. No existing fields are removed or repurposed.
- Version capture is always active — no feature flag.
### Cross-entity activity stream for charts, dashboards, and datasets
A read-only companion to the version-history endpoints (above). Each entity type gains an `/activity/` endpoint that returns a chronological stream of edits — the entity's own edits plus, for dashboards and charts, transitive edits to related entities during their association windows.
**New endpoints** (per entity type):
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/v1/dashboard/<uuid>/activity/` | Dashboard own edits + edits to charts attached during their dashboard window + edits to datasets those charts pointed at during their chart window |
| `GET` | `/api/v1/chart/<uuid>/activity/` | Chart own edits + edits to datasets the chart pointed at during association |
| `GET` | `/api/v1/dataset/<uuid>/activity/` | Dataset own edits only (no transitive layer in V2) |
**Query parameters** (all optional):
| Param | Type | Default | Purpose |
|---|---|---|---|
| `since` | ISO 8601 datetime | — | Lower bound on `issued_at` |
| `until` | ISO 8601 datetime | — | Upper bound on `issued_at` |
| `include` | `self` \| `related` \| `all` | `all` | Filter to only the entity's own edits, only related edits, or both |
`count` is the total record count *after* the silent permission filter (see below), not the raw query size.
**Authorisation:** reuses the resource's existing `can_write` permission. Workspace admins can read any entity's activity stream. The endpoint runs `raise_for_ownership` on the path entity — non-owners get `403`.
**Silent permission filter (AV-008):** records whose source entity the requesting user can't read are silently dropped — no placeholder, no count contribution. The frontend cannot distinguish "no activity" from "you can't see this activity."
**Tombstones (AV-009 / D-15):** when an activity record references a hard-deleted source entity, the record still appears with `entity_deleted: true`, `entity_uuid: null`, and `entity_name` recovered from the last shadow row.
**Impact on external integrations:**
- Pure read-only. No new tables, no new columns, no migrations. Reads sc-103156's shadow tables and the `version_changes` table.
- No new save-path code paths — perf-validation gate confirms the activity-view branch does not regress sc-103156's SC-004 50ms-overhead budget.
- No feature flag; the endpoints are always available once sc-103156's version-history feature is enabled.
### Granular Export Controls
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
When the feature flag is enabled, these permissions are enforced on both the frontend (disabled buttons with tooltips) and backend (403 responses from API endpoints). When disabled, legacy `can_csv` behavior is preserved.
**Migration behavior:** All three new permissions are granted to every role that currently has `can_csv`, preserving existing access. Admins can then selectively revoke individual export permissions from specific roles as needed.
### Deck.gl MapBox viewport and opacity controls are functional
The Deck.gl MapBox chart's **Opacity**, **Default longitude**, **Default latitude**, and **Zoom** controls were previously non-functional — changing them had no effect on the rendered map. These controls are now wired up correctly.
**Behavior change for existing charts:** Previously, the viewport controls had hard-coded default values (`-122.405293`, `37.772123`, zoom `11` — San Francisco) that were stored in each chart's `form_data` but never applied. The map always used `fitBounds` to center on the data. With this fix, those stored values are now respected, which means existing MapBox charts may open centered on the old default coordinates instead of fitting to data bounds.
**To restore fit-to-data behavior:** Open the chart in Explore, clear the **Default longitude**, **Default latitude**, and **Zoom** fields in the Viewport section, and re-save the chart.
### Combined datasource list endpoint
Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to serve datasets and semantic views in one response.
- The endpoint is available to users with at least one of `can_read` on `Dataset` or `SemanticView`.
- Semantic views are included only when the `SEMANTIC_LAYERS` feature flag is enabled.
- The endpoint enforces strict `order_column` validation and returns `400` for invalid sort columns.
### ClickHouse minimum driver version bump
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
### MCP Tool Observability
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
**Two-layer instrumentation:**
1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table)
2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution
SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms
FROM logs
WHERE action LIKE 'mcp.%'
GROUP BY action
ORDER BY avg_ms DESC
LIMIT 20;
-- MCP tool success rate
SELECT
json_extract(curated_payload, '$.tool') as tool,
COUNT(*) as total_calls,
SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) as successful,
ROUND(100.0 * SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate
FROM logs
WHERE action = 'mcp_tool_call'
GROUP BY tool
ORDER BY total_calls DESC;
```
**Security note:** Sensitive parameters (passwords, API keys, tokens) are automatically redacted in logs as `[REDACTED]`.
### Distributed Coordination Backend
A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-based backend for real-time coordination features in Superset. This backend enables:
- **Pub/sub messaging** for real-time event notifications between workers
- **Atomic distributed locking** using Redis SET NX EX (more performant than database-backed locks)
- **Event-based coordination** for background task management
The distributed coordination is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments.
Example configuration in `superset_config.py`:
```python
DISTRIBUTED_COORDINATION_CONFIG = {
"CACHE_TYPE": "RedisCache",
"CACHE_KEY_PREFIX": "signal_",
"CACHE_REDIS_URL": "redis://localhost:6379/1",
"CACHE_DEFAULT_TIMEOUT": 300,
}
```
See `superset/config.py` for complete configuration options.
### WebSocket config for GAQ with Docker
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
- Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward)
- Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example:
- [35621](https://github.com/apache/superset/pull/35621): The default hash algorithm has changed from MD5 to SHA-256 for improved security and FedRAMP compliance. This affects cache keys for thumbnails, dashboard digests, chart digests, and filter option names. Existing cached data will be invalidated upon upgrade. To opt out of this change and maintain backward compatibility, set `HASH_ALGORITHM = "md5"` in your `superset_config.py`.
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
### Breaking Changes
- [37370](https://github.com/apache/superset/pull/37370): The `APP_NAME` configuration variable no longer controls the browser window/tab title or other frontend branding. Application names should now be configured using the theme system with the `brandAppName` token. The `APP_NAME` config is still used for backend contexts (MCP service, logs, etc.) and serves as a fallback if `brandAppName` is not set.
- **Migration:**
```python
# Before (Superset 5.x)
APP_NAME = "My Custom App"
# After (Superset 6.x) - Option 1: Use theme system (recommended)
# After (Superset 6.x) - Option 2: Temporary fallback
# Keep APP_NAME for now (will be used as fallback for brandAppName)
APP_NAME = "My Custom App"
# But you should migrate to THEME_DEFAULT.token.brandAppName
```
- **Note:** For dark mode, set the same tokens in `THEME_DARK` configuration.
- [36317](https://github.com/apache/superset/pull/36317): The `CUSTOM_FONT_URLS` configuration option has been removed. Use the new per-theme `fontUrls` token in `THEME_DEFAULT` or database-managed themes instead.
- **Before:**
```python
CUSTOM_FONT_URLS = [
"https://fonts.example.com/myfont.css",
]
```
- **After:**
```python
THEME_DEFAULT = {
"token": {
"fontUrls": [
"https://fonts.example.com/myfont.css",
],
# ... other tokens
}
}
```
### Composite primary keys on many-to-many association tables
The eight M:N association tables listed below have been changed from a synthetic surrogate `id INTEGER PRIMARY KEY` to a composite `PRIMARY KEY (fk1, fk2)` on the two foreign-key columns. The `id` column is dropped, and the two tables that previously carried a redundant `UNIQUE (fk1, fk2)` constraint have that constraint removed (it is now subsumed by the composite primary key).
**Affected tables and their composite-PK column pairs:**
**Impact on external readers:** Any BI tool, custom report, backup script, or external integration that references these tables by their old surrogate `id` column (e.g., `SELECT id FROM dashboard_slices WHERE …`, `WHERE dashboard_slices.id IN (…)`) will break. Update such queries to project or filter on the FK pair (`dashboard_id, slice_id`) instead. The FK columns themselves are unchanged.
**Pre-flight inventory queries.** Before applying the upgrade, operators are encouraged to run the queries below against their database to assess what the migration will change. Two classes of pre-existing data are not preserved by the migration: duplicate `(fk1, fk2)` rows (the migration keeps `MIN(id)` and deletes the rest) and rows with `NULL` in either FK column (the migration deletes them, since FK columns are promoted to `NOT NULL` for the composite PK). Compliance- or audit-sensitive operators should also `\copy` (Postgres) or `SELECT … INTO OUTFILE` (MySQL) the affected rows for their own records before upgrading.
```sql
-- Duplicate (fk1, fk2) pairs (the migration will keep MIN(id) per group, delete the rest)
SELECT dashboard_id, role_id, COUNT(*) FROM dashboard_roles GROUP BY dashboard_id, role_id HAVING COUNT(*) > 1;
SELECT dashboard_id, slice_id, COUNT(*) FROM dashboard_slices GROUP BY dashboard_id, slice_id HAVING COUNT(*) > 1;
SELECT user_id, dashboard_id, COUNT(*) FROM dashboard_user GROUP BY user_id, dashboard_id HAVING COUNT(*) > 1;
SELECT user_id, report_schedule_id, COUNT(*) FROM report_schedule_user GROUP BY user_id, report_schedule_id HAVING COUNT(*) > 1;
SELECT role_id, rls_filter_id, COUNT(*) FROM rls_filter_roles GROUP BY role_id, rls_filter_id HAVING COUNT(*) > 1;
SELECT table_id, rls_filter_id, COUNT(*) FROM rls_filter_tables GROUP BY table_id, rls_filter_id HAVING COUNT(*) > 1;
SELECT user_id, slice_id, COUNT(*) FROM slice_user GROUP BY user_id, slice_id HAVING COUNT(*) > 1;
SELECT user_id, table_id, COUNT(*) FROM sqlatable_user GROUP BY user_id, table_id HAVING COUNT(*) > 1;
-- Rows with a NULL in either FK (the migration will delete these)
SELECT COUNT(*) FROM dashboard_roles WHERE dashboard_id IS NULL OR role_id IS NULL;
SELECT COUNT(*) FROM dashboard_slices WHERE dashboard_id IS NULL OR slice_id IS NULL;
SELECT COUNT(*) FROM dashboard_user WHERE user_id IS NULL OR dashboard_id IS NULL;
SELECT COUNT(*) FROM report_schedule_user WHERE user_id IS NULL OR report_schedule_id IS NULL;
SELECT COUNT(*) FROM rls_filter_roles WHERE role_id IS NULL OR rls_filter_id IS NULL;
SELECT COUNT(*) FROM rls_filter_tables WHERE table_id IS NULL OR rls_filter_id IS NULL;
SELECT COUNT(*) FROM slice_user WHERE user_id IS NULL OR slice_id IS NULL;
SELECT COUNT(*) FROM sqlatable_user WHERE user_id IS NULL OR table_id IS NULL;
```
**Sizing the maintenance window on PostgreSQL.** The queries above are dialect-portable but only count rows. Operators on PostgreSQL can run the diagnostic queries below to characterize the migration's runtime cost ahead of time: per-table row count and on-disk size, an aggregated duplicate roll-up, the external-FK pre-flight check (the migration runs the same check and aborts if it returns rows), and a rewrite-time estimate for the two tables that go through the slower full-table-rebuild path.
```sql
-- Per-table size, row count, and which migration path each will take.
-- Two tables ("dashboard_slices", "report_schedule_user") have a
-- redundant UNIQUE constraint that the migration drops via a full
-- table rewrite (op.batch_alter_table(recreate="always")). The other
-- six use direct ALTER TABLE, which is much cheaper.
WITH affected(name, has_unique) AS (
VALUES
('dashboard_roles', false),
('dashboard_slices', true),
('dashboard_user', false),
('report_schedule_user', true),
('rls_filter_roles', false),
('rls_filter_tables', false),
('slice_user', false),
('sqlatable_user', false)
)
SELECT
a.name AS table_name,
CASE WHEN a.has_unique THEN 'recreate (full rewrite)'
ELSE 'direct ALTER' END AS migration_path,
c.reltuples::bigint AS estimated_rows,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
pg_size_pretty(pg_relation_size(c.oid)) AS heap_size,
pg_size_pretty(pg_indexes_size(c.oid)) AS index_size
FROM affected a
JOIN pg_class c ON c.relname = a.name AND c.relkind = 'r'
ORDER BY pg_total_relation_size(c.oid) DESC;
```
```sql
-- Aggregated duplicate-row roll-up.
-- "dup_groups" is the number of (fk1, fk2) pairs that appear more
-- than once; "rows_dropped" is the total number of rows the
-- migration will delete during the dedupe pass (it keeps MIN(id) per
-- group and discards the rest).
SELECT 'dashboard_roles' AS t, COUNT(*) AS dup_groups, SUM(c) - COUNT(*) AS rows_dropped
FROM (SELECT COUNT(*) c FROM dashboard_roles GROUP BY dashboard_id, role_id HAVING COUNT(*) > 1) g
UNION ALL SELECT 'dashboard_slices', COUNT(*), SUM(c) - COUNT(*)
FROM (SELECT COUNT(*) c FROM dashboard_slices GROUP BY dashboard_id, slice_id HAVING COUNT(*) > 1) g
UNION ALL SELECT 'dashboard_user', COUNT(*), SUM(c) - COUNT(*)
FROM (SELECT COUNT(*) c FROM dashboard_user GROUP BY user_id, dashboard_id HAVING COUNT(*) > 1) g
UNION ALL SELECT 'report_schedule_user',COUNT(*), SUM(c) - COUNT(*)
FROM (SELECT COUNT(*) c FROM report_schedule_user GROUP BY user_id, report_schedule_id HAVING COUNT(*) > 1) g
UNION ALL SELECT 'rls_filter_roles', COUNT(*), SUM(c) - COUNT(*)
FROM (SELECT COUNT(*) c FROM rls_filter_roles GROUP BY role_id, rls_filter_id HAVING COUNT(*) > 1) g
UNION ALL SELECT 'rls_filter_tables', COUNT(*), SUM(c) - COUNT(*)
FROM (SELECT COUNT(*) c FROM rls_filter_tables GROUP BY table_id, rls_filter_id HAVING COUNT(*) > 1) g
UNION ALL SELECT 'slice_user', COUNT(*), SUM(c) - COUNT(*)
FROM (SELECT COUNT(*) c FROM slice_user GROUP BY user_id, slice_id HAVING COUNT(*) > 1) g
UNION ALL SELECT 'sqlatable_user', COUNT(*), SUM(c) - COUNT(*)
FROM (SELECT COUNT(*) c FROM sqlatable_user GROUP BY user_id, table_id HAVING COUNT(*) > 1) g
ORDER BY rows_dropped DESC NULLS LAST;
```
```sql
-- External-FK pre-flight check.
-- The migration runs the equivalent check at upgrade time and aborts
-- if any external FK references one of the soon-to-be-removed `id`
-- columns. Running it ahead of time lets you discover (and migrate)
-- any such reference before the maintenance window. On a stock
-- Superset install this should return zero rows. (Default schema
-- only; multi-schema deployments need to broaden the lookup.)
SELECT
rc.constraint_name,
kcu.table_schema || '.' || kcu.table_name AS referencing_table,
kcu.column_name AS referencing_column,
ccu.table_name AS referenced_table,
ccu.column_name AS referenced_column
FROM information_schema.referential_constraints rc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_name = rc.constraint_name
AND kcu.constraint_schema = rc.constraint_schema
JOIN information_schema.constraint_column_usage ccu
-- Lock-window estimate for the two full-rewrite tables.
-- recreate="always" takes ACCESS EXCLUSIVE on the table for the full
-- rewrite. Use heap size combined with your hardware's effective
-- write throughput (~100-200 MB/s on commodity SSD; faster on NVMe)
-- to size the maintenance window. The other six tables use direct
-- ALTER and are dominated by composite-index build time, typically
-- seconds for tables in the low millions of rows.
SELECT
c.relname AS table_name,
pg_size_pretty(pg_relation_size(c.oid)) AS heap_size,
pg_relation_size(c.oid) / 1024 / 1024 AS heap_size_mb,
ROUND(pg_relation_size(c.oid) / 1024 / 1024 / 100.0, 1) AS est_rewrite_seconds_at_100mbs
FROM pg_class c
WHERE c.relname IN ('dashboard_slices', 'report_schedule_user');
```
**Sizing the maintenance window on MySQL.** Equivalent diagnostic queries for MySQL/InnoDB. One important difference from PostgreSQL: InnoDB rebuilds the clustered index on every PK change, so *all eight* tables undergo a full table rebuild on MySQL — not just the two that go through the explicit `recreate="always"` path. The lock-window estimate query below therefore covers all eight tables.
```sql
-- Per-table size, row count, and which migration path each will take.
-- TABLE_ROWS is an InnoDB estimate (analogous to PostgreSQL's reltuples);
-- run SELECT COUNT(*) per table for an exact count if needed.
SELECT
TABLE_NAME AS table_name,
CASE WHEN TABLE_NAME IN ('dashboard_slices', 'report_schedule_user')
THEN 'recreate (explicit, drops UNIQUE)'
ELSE 'direct ALTER (still rebuilds InnoDB clustered index)'
**Restoring an old `pg_dump` (or equivalent) against the new schema.** A dump taken before the migration includes `INSERT` statements that populate the now-removed `id` column. Restoring such a dump against the post-migration schema will fail. The supported workaround is to dump only the schema and reference data, then re-create the M:N associations from application data after restore — for example with `pg_dump --exclude-table-data` (or per-table `--exclude-table-data=dashboard_slices` etc.) for the eight junction tables, restore the rest, then run a one-shot script that re-INSERTs `(fk1, fk2)` pairs derived from your application export. Operators who need to restore an old dump verbatim should restore against a pre-migration Superset and then re-run the upgrade.
**Intentional downgrade asymmetry.** The migration's `downgrade()` restores the surrogate `id` column and (for `dashboard_slices` and `report_schedule_user`) the original `UNIQUE (fk1, fk2)` constraint, but it does **not** restore the original `NULL`-allowed state on the FK columns — they remain `NOT NULL`. This is intentional: under SQLAlchemy's `secondary=` semantics, a `NULL` in either FK column of a junction table is meaningless (it cannot participate in either side of the relationship). Operators downgrading are not expected to need this restored. The asymmetry is documented for completeness so that round-trip schema diffs are not mistaken for migration bugs.
**Constraint-name divergence between upgrade and downgrade.** The composite primary key created on upgrade is named `pk_<table>` (Alembic's default for `op.create_primary_key("pk_<table>", ...)`), while the surrogate `id` primary key restored on downgrade is named `<table>_pkey` (PostgreSQL's default convention for `PrimaryKeyConstraint("id")`). The two names alternate so that a round-trip (upgrade → downgrade → upgrade) does not collide on a pre-existing constraint name. Operators using schema-comparison tools (e.g. `pg_diff`, `migra`) against a downgraded database may see this as drift versus a fresh-install schema. It is cosmetic — no application code references either constraint name.
## 6.0.0
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
@@ -41,14 +682,14 @@ Note: Pillow is now a required dependency (previously optional) to support image
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.
There's a migration added that can potentially affect a significant number of existing charts.
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
- [31590](https://github.com/apache/superset/pull/31590) Marks the beginning of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
- [32432](https://github.com/apache/superset/pull/32432) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
## 5.0.0
- [31976](https://github.com/apache/superset/pull/31976) Removed the `DISABLE_LEGACY_DATASOURCE_EDITOR` feature flag. The previous value of the feature flag was `True` and now the feature is permanently removed.
- [31959](https://github.com/apache/superset/pull/32000) Removes CSV_UPLOAD_MAX_SIZE config, use your web server to control file upload size.
- [32000](https://github.com/apache/superset/pull/32000) Removes CSV_UPLOAD_MAX_SIZE config, use your web server to control file upload size.
- [31959](https://github.com/apache/superset/pull/31959) Removes the following endpoints from data uploads: `/api/v1/database/<id>/<file type>_upload` and `/api/v1/database/<file type>_metadata`, in favour of new one (Details on the PR). And simplifies permissions.
- [31844](https://github.com/apache/superset/pull/31844) The `ALERT_REPORTS_EXECUTE_AS` and `THUMBNAILS_EXECUTE_AS` config parameters have been renamed to `ALERT_REPORTS_EXECUTORS` and `THUMBNAILS_EXECUTORS` respectively. A new config flag `CACHE_WARMUP_EXECUTORS` has also been introduced to be able to control which user is used to execute cache warmup tasks. Finally, the config flag `THUMBNAILS_SELENIUM_USER` has been removed. To use a fixed executor for async tasks, use the new `FixedExecutor` class. See the config and docs for more info on setting up different executor profiles.
- [31894](https://github.com/apache/superset/pull/31894) Domain sharding is deprecated in favor of HTTP2. The `SUPERSET_WEBSERVER_DOMAINS` configuration will be removed in the next major version (6.0)
@@ -56,7 +697,7 @@ Note: Pillow is now a required dependency (previously optional) to support image
- [31774](https://github.com/apache/superset/pull/31774): Fixes the spelling of the `USE-ANALAGOUS-COLORS` feature flag. Please update any scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag spelling.
- [31582](https://github.com/apache/superset/pull/31582) Removed the legacy Area, Bar, Event Flow, Heatmap, Histogram, Line, Sankey, and Sankey Loop charts. They were all automatically migrated to their ECharts counterparts with the exception of the Event Flow and Sankey Loop charts which were removed as they were not actively maintained and not widely used. If you were using the Event Flow or Sankey Loop charts, you will need to find an alternative solution.
- [31198](https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName".
- [29798](https://github.com/apache/superset/pull/29798) Since 3.1.0, the intial schedule for an alert or report was mistakenly offset by the specified timezone's relation to UTC. The initial schedule should now begin at the correct time.
- [29798](https://github.com/apache/superset/pull/29798) Since 3.1.0, the initial schedule for an alert or report was mistakenly offset by the specified timezone's relation to UTC. The initial schedule should now begin at the correct time.
- [30021](https://github.com/apache/superset/pull/30021) The `dev` layer in our Dockerfile no long includes firefox binaries, only Chromium to reduce bloat/docker-build-time.
- [30099](https://github.com/apache/superset/pull/30099) Translations are no longer included in the default docker image builds. If your environment requires translations, you'll want to set the docker build arg `BUILD_TRANSLATIONS=true`.
- [31262](https://github.com/apache/superset/pull/31262) NOTE: deprecated `pylint` in favor of `ruff` as our only python linter. Only affect development workflows positively (not the release itself). It should cover most important rules, be much faster, but some things linting rules that were enforced before may not be enforce in the exact same way as before.
@@ -69,7 +710,7 @@ Note: Pillow is now a required dependency (previously optional) to support image
- [25166](https://github.com/apache/superset/pull/25166) Changed the default configuration of `UPLOAD_FOLDER` from `/app/static/uploads/` to `/static/uploads/`. It also removed the unused `IMG_UPLOAD_FOLDER` and `IMG_UPLOAD_URL` configuration options.
- [30284](https://github.com/apache/superset/pull/30284) Deprecated GLOBAL_ASYNC_QUERIES_REDIS_CONFIG in favor of the new GLOBAL_ASYNC_QUERIES_CACHE_BACKEND configuration. To leverage Redis Sentinel, set CACHE_TYPE to RedisSentinelCache, or use RedisCache for standalone Redis
- [31961](https://github.com/apache/superset/pull/31961) Upgraded React from version 16.13.1 to 17.0.2. If you are using custom frontend extensions or plugins, you may need to update them to be compatible with React 17.
- [31260](https://github.com/apache/superset/pull/31260) Docker images now use `uv pip install` instead of `pip install` to manage the python envrionment. Most docker-based deployments will be affected, whether you derive one of the published images, or have custom bootstrap script that install python libraries (drivers)
- [31260](https://github.com/apache/superset/pull/31260) Docker images now use `uv pip install` instead of `pip install` to manage the python environment. Most docker-based deployments will be affected, whether you derive one of the published images, or have custom bootstrap script that install python libraries (drivers)
### Potential Downtime
@@ -146,7 +787,7 @@ Note: Pillow is now a required dependency (previously optional) to support image
- [26462](https://github.com/apache/superset/issues/26462): Removes the Profile feature given that it's not actively maintained and not widely used.
- [26377](https://github.com/apache/superset/pull/26377): Removes the deprecated Redirect API that supported short URLs used before the permalink feature.
- [26329](https://github.com/apache/superset/issues/26329): Removes the deprecated `DASHBOARD_NATIVE_FILTERS` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled.
- [25510](https://github.com/apache/superset/pull/25510): Reenforces that any newly defined Python data format (other than epoch) must adhere to the ISO 8601 standard (enforced by way of validation at the API and database level) after a previous relaxation to include slashes in addition to dashes. From now on when specifying new columns, dataset owners will need to use a SQL expression instead to convert their string columns of the form %Y/%m/%d etc. to a `DATE`, `DATETIME`, etc. type.
- [25510](https://github.com/apache/superset/pull/25510): Reinforces that any newly defined Python data format (other than epoch) must adhere to the ISO 8601 standard (enforced by way of validation at the API and database level) after a previous relaxation to include slashes in addition to dashes. From now on when specifying new columns, dataset owners will need to use a SQL expression instead to convert their string columns of the form %Y/%m/%d etc. to a `DATE`, `DATETIME`, etc. type.
- [26372](https://github.com/apache/superset/issues/26372): Removes the deprecated `GENERIC_CHART_AXES` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled.
@@ -34,8 +34,24 @@ intended for use with local development.
### Local overrides
#### Environment Variables
To override environment variables locally, create a `./docker/.env-local` file (git-ignored). This file will be loaded after `.env` and can override any settings.
#### Python Configuration
In order to override configuration settings locally, simply make a copy of [`./docker/pythonpath_dev/superset_config_local.example`](./pythonpath_dev/superset_config_local.example)
into `./docker/pythonpath_dev/superset_config_docker.py` (gitignored) and fill in your overrides.
into `./docker/pythonpath_dev/superset_config_docker.py` (git-ignored) and fill in your overrides.
#### WebSocket Configuration
To customize the WebSocket server configuration, create `./docker/superset-websocket/config.json` (git-ignored) based on [`./docker/superset-websocket/config.example.json`](./superset-websocket/config.example.json).
Then update the `superset-websocket`.`volumes` config to mount it.
#### Docker Compose Overrides
For advanced Docker Compose customization, create a `docker-compose-override.yml` file (git-ignored) to override or extend services without modifying the main compose file.
### Local packages
@@ -61,6 +77,34 @@ To run the container, simply run: `docker compose up`
After waiting several minutes for Superset initialization to finish, you can open a browser and view [`http://localhost:8088`](http://localhost:8088)
to start your journey.
### Running Multiple Instances
If you need to run multiple Superset instances simultaneously (e.g., different branches or clones), use the make targets which automatically find available ports:
```bash
make up
```
This automatically:
- Generates a unique project name from your directory
- Finds available ports (incrementing from defaults if in use)
WEBDRIVER_BASEURL=f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT','/')}/"# When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
## Core Principle: Stories Are the Single Source of Truth
When working on the Storybook-to-MDX documentation system:
**ALWAYS fix the story first. NEVER add workarounds to the generator.**
## Why This Matters
The generator (`scripts/generate-superset-components.mjs`) should be lightweight - it extracts data from stories and passes it through. When you add special cases to the generator:
- It becomes harder to maintain
- Stories diverge from their docs representation
- Future stories need to know about generator quirks
When you fix stories to match the expected patterns:
- Stories work identically in Storybook and Docs
- The generator stays simple and predictable
- Patterns are consistent and learnable
## Story Patterns for Docs Generation
### Required Structure
```tsx
// Use inline export default (NOT const meta = ...; export default meta)
export default {
title: 'Components/MyComponent',
component: MyComponent,
};
// Name interactive stories with Interactive prefix
export const InteractiveMyComponent: Story = {
args: {
// Default prop values
},
argTypes: {
// Control definitions - MUST be at story level, not meta level
propName: {
control: { type: 'select' },
options: ['a', 'b', 'c'],
description: 'What this prop does',
},
},
};
```
### For Components with Variants (size × style grids)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.