You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/08/25 20:00:32 UTC
(superset) branch master updated: docs(versioning): fix post-flip doc and comment drift (#43493)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new b89da3e9fca docs(versioning): fix post-flip doc and comment drift (#43493)
b89da3e9fca is described below
commit b89da3e9fca79f6e9ea06c338d9a37880afa1053
Author: Mike Bridge <mi...@preset.io>
AuthorDate: Tue Aug 25 14:00:25 2026 -0600
docs(versioning): fix post-flip doc and comment drift (#43493)
Co-authored-by: Mike Bridge <mi...@ext.preset.io>
Co-authored-by: Claude Fable 5 <no...@anthropic.com>
---
UPDATING.md | 27 +++++++++++++---------
docs/docs/using-superset/version-history.mdx | 24 +++++++++----------
docs/static/feature-flags.json | 24 +++++++++----------
superset/commands/version_restore.py | 4 ++--
superset/config.py | 22 ++++++------------
superset/initialization/__init__.py | 13 +++--------
.../commands/importers/v1/import_command_test.py | 9 ++++----
7 files changed, 56 insertions(+), 67 deletions(-)
diff --git a/UPDATING.md b/UPDATING.md
index 5145b362cb0..cc2a0fadde1 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -184,10 +184,12 @@ misrepresents the entity as unchanged.
- **Storage growth.** Capture writes shadow rows per save, so the metadata
database grows with edit volume. The `version_history.prune_old_versions`
beat task removes rows whose transaction is older than
- `SUPERSET_VERSION_HISTORY_RETENTION_DAYS` (default 30). A deployment that
- replaces `CELERY_CONFIG` rather than inheriting it must carry both the
- `superset.tasks.version_history_retention` import and the beat entry; a
- startup warning names whichever is absent.
+ `SUPERSET_VERSION_HISTORY_RETENTION_DAYS` (default 30).
+- **Check a replaced `CELERY_CONFIG`.** Carry both the
+ `superset.tasks.version_history_retention` import and the
+ `version_history.prune_old_versions` beat entry; see
+ [Version-history retention (pruning)](#version-history-retention-pruning) for
+ the startup-warning behavior.
- **`PUT` responses change shape.** Entity updates now return populated
`old_version_uuid` / `new_version_uuid` fields and an `ETag` header, which
were null or absent while capture was off.
@@ -196,7 +198,10 @@ misrepresents the entity as unchanged.
kill-switch — not removed with the rollout toggles. Setting it to a falsy value
stops capture within a restart, without a revert-and-redeploy. Unlike the
soft-delete toggle, turning it off is a clean stop: existing version rows remain
-readable and no entity state is altered.
+readable and no entity state is altered. Restore is unavailable (404) while
+capture is off. A full rollback also sets
+`FEATURE_FLAGS = {"VERSION_HISTORY": False}` to hide the panel — capture off
+with the panel left on shows an empty or stale history.
### Scheduled report execution now enforces one application deadline
@@ -658,9 +663,9 @@ ALTER TABLE tagged_object DROP CONSTRAINT <constraint_name>;
ALTER TABLE tagged_object DROP FOREIGN KEY <constraint_name>;
```
-### Entity version-history infrastructure (gated off by default)
+### Entity version-history infrastructure
-Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}/<uuid>/versions/` endpoints. This ships **inert**: a new config flag `ENABLE_VERSIONING_CAPTURE` defaults to `False`, so no save writes any version rows and the endpoints return empty. It is an operational kill-switch (a release toggle that becomes a permanent ops switch), not a feature flag — set it to `True` to e [...]
+Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}/<uuid>/versions/` endpoints. Capture is governed by the `ENABLE_VERSIONING_CAPTURE` config value — an operational kill-switch (a release toggle that became a permanent ops switch), not a feature flag; see "Version history is on by default" above for the shipped default. With capture off, no save writes version rows [...]
A few save- and import-path internals change **unconditionally** (independent of the flag), because the versioned mappers must behave correctly whether or not capture is enabled:
@@ -681,7 +686,7 @@ A read-only companion to the version-history endpoints: each entity type gains a
| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) |
| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) |
-Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream is empty unless version capture is on (`ENABLE_VERSIONING_CAPTURE`).
+Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream reflects captured history; with capture off it remains readable but stops accruing new entries.
### Version-history retention (pruning)
@@ -897,7 +902,7 @@ The migration is transactional (all-or-nothing) and idempotent — it can be saf
### Soft delete and restore for datasets
-**The soft-delete behavior in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dataset/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
+**The soft-delete behavior in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `True`** (`@lifecycle: testing`), so on a default deployment `DELETE /api/v1/dataset/<id>` uses the recoverable soft-delete behavior described below. Setting `SOFT_DELETE` to `False` restores legacy permanent hard-delete behavior for subsequent deletes.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If datasets are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live datasets in all lists, lookups, and relationship loads (including charts that reference them). The `POST /<uuid>/restore` endpoint and the `dataset_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on w [...]
@@ -927,7 +932,7 @@ With the flag enabled: `DELETE /api/v1/dataset/<id>` no longer hard-deletes the
### Soft delete and restore for charts
-**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/chart/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
+**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `True`** (`@lifecycle: testing`), so on a default deployment `DELETE /api/v1/chart/<id>` uses the recoverable soft-delete behavior described below. Setting `SOFT_DELETE` to `False` restores legacy permanent hard-delete behavior for subsequent deletes.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If charts are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live charts in all lists, lookups, and relationship loads (including dashboards that contained them). The `POST /<uuid>/restore` endpoint and the `chart_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on win [...]
@@ -951,7 +956,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
### Soft delete and restore for dashboards
-**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dashboard/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
+**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `True`** (`@lifecycle: testing`), so on a default deployment `DELETE /api/v1/dashboard/<id>` uses the recoverable soft-delete behavior described below. Setting `SOFT_DELETE` to `False` restores legacy permanent hard-delete behavior for subsequent deletes.
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If dashboards are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live dashboards in all lists and lookups (including slug lookups — if a soft-deleted dashboard's slug was reused while the flag was on, both rows become visible with the same slug). The `POST /<uuid>/restore` endpoint and the `dashboard_deleted_state` list filter remain fu [...]
diff --git a/docs/docs/using-superset/version-history.mdx b/docs/docs/using-superset/version-history.mdx
index 59490e7b984..6625ca3a080 100644
--- a/docs/docs/using-superset/version-history.mdx
+++ b/docs/docs/using-superset/version-history.mdx
@@ -15,29 +15,29 @@ description of what changed — "Chart renamed to Q3 Revenue", "Added filter on
'Region'" — rather than a raw diff. You can search the history and filter it
down to changes on the entity itself or on the things it depends on.
-## Enabling it
-
-Two switches are involved, and both matter.
+## Enabling and disabling it
| Setting | Type | Effect |
| --- | --- | --- |
| `VERSION_HISTORY` | Feature flag | Shows the version history UI |
| `ENABLE_VERSIONING_CAPTURE` | Config value | Records versions as entities are saved |
+Both default to on. To turn the feature off:
+
```python
# superset_config.py
-FEATURE_FLAGS = {"VERSION_HISTORY": True}
-ENABLE_VERSIONING_CAPTURE = True
+FEATURE_FLAGS = {"VERSION_HISTORY": False}
+ENABLE_VERSIONING_CAPTURE = False
```
-Both default to off. They are separate because capture is the expensive half:
-an operator may want to start recording history before exposing the UI, so that
-there is something to show when they do.
+Restart Superset and its workers for the capture change to take effect. Existing
+history remains readable while capture is off, but **Restore** is unavailable
+(404).
-Turning the UI on without capture gives a panel that reports "No history yet"
-and never fills, so enable capture first — or at the same time. History only
-accrues from the moment capture is switched on; earlier edits are not
-reconstructed.
+Disable them together: capture off with the UI left on gives a panel that
+stops filling — an empty or stale history misrepresents the entity as
+unchanged. History only accrues while capture is on; edits made while it was
+off are not reconstructed.
## Viewing history
diff --git a/docs/static/feature-flags.json b/docs/static/feature-flags.json
index d4cbb2d8d49..f1efcbe4838 100644
--- a/docs/static/feature-flags.json
+++ b/docs/static/feature-flags.json
@@ -93,12 +93,6 @@
"lifecycle": "development",
"description": "Enable semantic layers and show semantic views alongside datasets"
},
- {
- "name": "SOFT_DELETE",
- "default": true,
- "lifecycle": "development",
- "description": "Temporary rollout / kill-switch gate for soft delete (off = legacy hard delete). An emergency stop, not a clean rollback: flipping ON->OFF resurrects already-soft-deleted rows. Retained through this release as the move-back lever; removed (along with its two gate points \u2014 BaseDAO.delete routing and the do_orm_execute visibility listener) once post-flip confidence is established."
- },
{
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
"default": false,
@@ -110,12 +104,6 @@
"default": false,
"lifecycle": "development",
"description": "Enables the tagging system for organizing assets"
- },
- {
- "name": "VERSION_HISTORY",
- "default": true,
- "lifecycle": "development",
- "description": "Enables the version history panel on Explore and Dashboard pages. History only accrues while ``ENABLE_VERSIONING_CAPTURE`` is also on; with capture off the panel renders but stays empty, so the two ship with matching defaults and should be changed together."
}
],
"testing": [
@@ -233,6 +221,12 @@
"lifecycle": "testing",
"description": "Apply RLS rules to SQL Lab queries. Requires query parsing/manipulation. May break queries or allow RLS bypass. Use with care!"
},
+ {
+ "name": "SOFT_DELETE",
+ "default": true,
+ "lifecycle": "testing",
+ "description": "Temporary rollout / kill-switch gate for soft delete (off = legacy hard delete). An emergency stop, not a clean rollback: flipping ON->OFF resurrects already-soft-deleted rows. Retained through this release as the move-back lever; removed (along with its two gate points \u2014 BaseDAO.delete routing and the do_orm_execute visibility listener) once post-flip confidence is established."
+ },
{
"name": "SSH_TUNNELING",
"default": false,
@@ -245,6 +239,12 @@
"default": false,
"lifecycle": "testing",
"description": "Use analogous colors in charts"
+ },
+ {
+ "name": "VERSION_HISTORY",
+ "default": true,
+ "lifecycle": "testing",
+ "description": "Enables the version history panel on Explore and Dashboard pages. History only accrues while ``ENABLE_VERSIONING_CAPTURE`` is also on; with capture off the panel renders empty or stale history, so the two ship with matching defaults and should be changed together."
}
],
"stable": [
diff --git a/superset/commands/version_restore.py b/superset/commands/version_restore.py
index 93bb7cb3a62..7a79a40dde8 100644
--- a/superset/commands/version_restore.py
+++ b/superset/commands/version_restore.py
@@ -138,8 +138,8 @@ class BaseRestoreVersionCommand(BaseCommand):
# With capture off, Continuum's write listeners are detached: a
# revert would mutate the live entity with NO new version row —
# a destructive, untracked write. The whole restore surface is
- # therefore inert under the kill-switch, matching the read-side
- # convention (404, indistinguishable from "no such version").
+ # therefore inert under the kill-switch (404, indistinguishable from
+ # "no such version"). Existing history remains readable.
if not capture_enabled():
raise self.not_found_exc()
entity = find_active_by_uuid(self.model_cls, self._uuid)
diff --git a/superset/config.py b/superset/config.py
index 0e8ee06b8eb..bd108ff7f37 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -708,7 +708,7 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
# the move-back lever; removed (along with its two gate points —
# BaseDAO.delete routing and the do_orm_execute visibility listener) once
# post-flip confidence is established.
- # @lifecycle: development
+ # @lifecycle: testing
"SOFT_DELETE": True,
# Enable semantic layers and show semantic views alongside datasets
# @lifecycle: development
@@ -742,9 +742,9 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
"TAGGING_SYSTEM": False,
# Enables the version history panel on Explore and Dashboard pages.
# History only accrues while ``ENABLE_VERSIONING_CAPTURE`` is also on;
- # with capture off the panel renders but stays empty, so the two ship
- # with matching defaults and should be changed together.
- # @lifecycle: development
+ # with capture off the panel renders empty or stale history, so the two
+ # ship with matching defaults and should be changed together.
+ # @lifecycle: testing
"VERSION_HISTORY": True,
# =================================================================
# IN TESTING
@@ -1694,17 +1694,9 @@ DATETIME_FORMAT_DETECTION_SAMPLE_SIZE = 1000
# The limit for the Superset Meta DB when the feature flag ENABLE_SUPERSET_META_DB is on
SUPERSET_META_DB_LIMIT: int | None = 1000
-# Master switch for entity-version-history capture. Capture is enabled by
-# default, so saves write shadow rows and a ``version_transaction`` /
-# ``version_changes`` record. Set this to a falsy value in
-# ``superset_config.py`` (or via the environment variable of the same name) to
-# disable the before-flush listeners while keeping the /versions/ endpoints
-# available read-only.
-# Capture ships on. It is an operational escape hatch — set the environment
-# variable to a falsy value when a versioning-induced regression needs a
-# 30-second recovery instead of revert-and-redeploy — not a feature flag,
-# and it remains permanently as the kill-switch rather than being removed
-# with the rollout toggles.
+# Master switch for entity-version-history capture. A falsy value disables
+# version writes while keeping existing history available read-only through the
+# ``/versions/`` endpoints; Restore is unavailable while capture is disabled.
ENABLE_VERSIONING_CAPTURE: bool = utils.parse_boolean_string(
os.environ.get("ENABLE_VERSIONING_CAPTURE", "true")
)
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index c37734bb58c..b1812bb770c 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -791,16 +791,9 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
Must be called after all versioned model classes have been imported so
that VERSIONED_MODELS can be populated and configure_mappers() has run.
- ``ENABLE_VERSIONING_CAPTURE`` (ships default ``False``) gates the two
- before-flush listener registrations. The flag is operational, not
- feature: with it off the infrastructure is inert (no save writes
- shadow rows); flipping it on activates capture. The switch also lets
- an operator who observes a versioning-induced regression (e.g. a
- save-path slowdown attributable to the change-record listener)
- disable capture in ``superset_config.py`` and restart workers — a
- 30-second recovery instead of revert-and-redeploy. Shadow tables
- already created by the migration stay; they just stop accumulating
- new rows.
+ ``ENABLE_VERSIONING_CAPTURE`` gates the baseline and change-record
+ listener registrations. When disabled, initialization also detaches
+ SQLAlchemy-Continuum's write listeners.
The fallback here is ``False`` so that any app-factory path that
does not load ``superset.config`` (some test factories, embedded
diff --git a/tests/unit_tests/dashboards/commands/importers/v1/import_command_test.py b/tests/unit_tests/dashboards/commands/importers/v1/import_command_test.py
index b047ae98927..f6879a52dfd 100644
--- a/tests/unit_tests/dashboards/commands/importers/v1/import_command_test.py
+++ b/tests/unit_tests/dashboards/commands/importers/v1/import_command_test.py
@@ -68,11 +68,10 @@ def test_dashboard_import_with_overwrite_replaces_charts(
}
ImportDashboardsCommand._import(initial_configs, overwrite=True)
# Commit between imports, as production does: ``run()`` carries
- # ``@transaction()``, so two imports are two transactions. Calling the
- # private ``_import`` twice without committing puts both in one Continuum
- # transaction, where adding and removing the same association collides on
- # ``dashboard_slices_version``'s (dashboard_id, slice_id, transaction_id)
- # key — an artifact of the test's shortcut, not a reachable state.
+ # ``@transaction()``, so two imports are two transactions. Without this the
+ # add and the remove of one association share a Continuum transaction and
+ # collide on ``dashboard_slices_version``'s composite key — an artifact of
+ # the test's shortcut, not a reachable production state.
db.session.commit()
# Verify initial state: 2 charts associated with the dashboard