Skip to content

feat(soft-delete): deletion-retention purge of soft-deleted entities - #41549

Merged
eschutho merged 10 commits into
apache:masterfrom
mikebridge:sc-111185-deletion-retention
Jul 30, 2026
Merged

feat(soft-delete): deletion-retention purge of soft-deleted entities#41549
eschutho merged 10 commits into
apache:masterfrom
mikebridge:sc-111185-deletion-retention

Conversation

@mikebridge

@mikebridge mikebridge commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

Adds permanent deletion retention for soft-deleted dashboards, charts, and datasets.

  • Registers a daily Celery task, deletion_retention.purge_soft_deleted, scheduled at midnight, including the shipped Docker development Celery configuration.
  • Defaults to a 30-day retention window and dry-run mode for safe rollout.
  • Resolves a per-workspace retention override at task runtime; malformed values fail safely to the configured default.
  • Provides operator CLI commands to inspect/set the window and force-purge an entity by UUID.
  • Preserves the dependency rules used by ordinary deletion: entities with restrictive references are blocked rather than having those references silently removed.
  • Explicitly removes eligible association, tag, permission, and version-history rows after the entity delete is successfully claimed.
  • Uses session-scoped Continuum suppression so purge bookkeeping cannot disable version capture for concurrent requests.
  • Writes a content-free, UUID-keyed audit attempt before each purge, confirms the durable outcome afterward, and reconciles stale pending attempts.
  • Adds an indexed audit table for entity lookup and pending-attempt reconciliation.

The prerequisite soft-delete support for charts, dashboards, and datasets is present on master. This PR adds the scheduled retention pipeline and operator CLI; it does not add a REST API endpoint.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable; this is a backend task, CLI, and database change.

TESTING INSTRUCTIONS

Run the focused unit and integration suite:

pytest -q tests/unit_tests/tasks/test_deletion_retention.py \
  tests/integration_tests/deletion_retention

Expected result: 44 tests pass.

Inspect the effective operator configuration:

superset deletion-retention show-window

The migration was tested from a clean SQLite integration database and through an explicit downgrade/upgrade round trip:

SUPERSET_CONFIG=tests.integration_tests.superset_test_config \
  superset db downgrade 8f3a1b2c4d5e
SUPERSET_CONFIG=tests.integration_tests.superset_test_config \
  superset db upgrade e7d93a524ff6
SUPERSET_CONFIG=tests.integration_tests.superset_test_config \
  superset db heads

The upgraded schema has a UUID primary key plus entity_uuid and (status, created_on) indexes, with one migration head at e7d93a524ff6.

A Docker smoke test was also run against the isolated PostgreSQL stack:

  • Applied the migration from 8f3a1b2c4d5e to e7d93a524ff6.
  • Verified dry-run selected a disposable aged dashboard without deleting it.
  • Verified the live task purged that dashboard with zero cascade failures and produced a confirmed audit result.
  • Dispatched deletion_retention.purge_soft_deleted through Redis to the Docker Celery worker and received the expected safe {"skipped": 1} result while SOFT_DELETE was disabled.

Changed-file pre-commit hooks pass. pre-commit run --all-files was also executed; its remaining failures are pre-existing errors in semantic-layer test typing, frontend Select/DatePicker typing, and SQL Lab MCP complexity, outside this PR's files.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags: SOFT_DELETE
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided: creates one empty audit table and two indexes; no data rewrite or expected downtime
  • Introduces new feature or API
  • Removes existing feature or API

@netlify

netlify Bot commented Jun 29, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 3661d59
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a68875b8f88b90008aefeb8
😎 Deploy Preview https://deploy-preview-41549--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.03426% with 104 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.34%. Comparing base (ecc7f72) to head (5c99638).
⚠️ Report is 59 commits behind head on master.

Files with missing lines Patch % Lines
...erset/commands/deletion_retention/purge_cascade.py 90.13% 10 Missing and 12 partials ⚠️
superset/tasks/deletion_retention.py 86.86% 12 Missing and 6 partials ⚠️
superset/cli/deletion_retention.py 66.66% 15 Missing and 2 partials ⚠️
superset/commands/deletion_retention/audit.py 81.39% 13 Missing and 3 partials ⚠️
...uperset/commands/deletion_retention/force_purge.py 75.38% 11 Missing and 5 partials ⚠️
superset/commands/deletion_retention/window.py 40.00% 13 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #41549      +/-   ##
==========================================
+ Coverage   65.27%   65.34%   +0.07%     
==========================================
  Files        2795     2807      +12     
  Lines      157835   158959    +1124     
  Branches    36080    36234     +154     
==========================================
+ Hits       103024   103869     +845     
- Misses      52818    53048     +230     
- Partials     1993     2042      +49     
Flag Coverage Δ
hive 38.09% <22.67%> (-0.22%) ⬇️
mysql 57.83% <83.03%> (+0.24%) ⬆️
postgres 57.87% <83.03%> (+0.25%) ⬆️
presto 39.98% <22.67%> (-0.24%) ⬇️
python 59.26% <83.03%> (+0.23%) ⬆️
sqlite 57.50% <83.03%> (+0.25%) ⬆️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mikebridge
mikebridge force-pushed the sc-111185-deletion-retention branch from 078d330 to 5c6ffc4 Compare June 30, 2026 15:56
@mikebridge
mikebridge force-pushed the sc-111185-deletion-retention branch from 5c6ffc4 to 06769f6 Compare July 13, 2026 19:04
@mikebridge
mikebridge marked this pull request as ready for review July 13, 2026 19:05
@dosubot dosubot Bot added the change:backend Requires changing the backend label Jul 13, 2026
@mikebridge
mikebridge force-pushed the sc-111185-deletion-retention branch from 06769f6 to dde6b98 Compare July 13, 2026 19:46
Comment thread superset/commands/deletion_retention/audit.py Outdated
Comment thread superset/commands/deletion_retention/force_purge.py Outdated
Comment thread superset/commands/deletion_retention/force_purge.py Outdated
Comment thread superset/commands/deletion_retention/window.py Outdated
Comment thread tests/unit_tests/tasks/test_deletion_retention.py Outdated
Comment thread tests/unit_tests/tasks/test_deletion_retention.py Outdated
Comment thread tests/integration_tests/deletion_retention/_base.py Outdated
Comment thread tests/integration_tests/deletion_retention/_base.py Outdated
Comment thread superset/commands/deletion_retention/purge_cascade.py Outdated
Comment thread superset/commands/deletion_retention/purge_cascade.py Outdated
Comment thread superset/commands/deletion_retention/purge_cascade.py
Comment thread superset/migrations/versions/2026-06-23_13-00_e7d93a524ff6_add_purge_audit_log.py Outdated
Comment thread superset/migrations/versions/2026-06-23_13-00_e7d93a524ff6_add_purge_audit_log.py Outdated
Comment thread superset/migrations/versions/2026-06-23_13-00_e7d93a524ff6_add_purge_audit_log.py Outdated
Comment thread tests/integration_tests/deletion_retention/window_tests.py Outdated
Comment thread tests/unit_tests/tasks/test_deletion_retention.py Outdated
Comment thread tests/unit_tests/tasks/test_deletion_retention.py
Comment thread superset/commands/deletion_retention/force_purge.py Outdated
Comment thread superset/commands/deletion_retention/window.py Outdated
Comment thread superset/commands/deletion_retention/purge_cascade.py Outdated
Comment thread superset/migrations/versions/2026-06-23_13-00_e7d93a524ff6_add_purge_audit_log.py Outdated
@mikebridge
mikebridge force-pushed the sc-111185-deletion-retention branch 4 times, most recently from 05ca6de to 2725c6e Compare July 13, 2026 22:17

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #faba71

Actionable Suggestions - 3
  • tests/unit_tests/tasks/test_deletion_retention.py - 1
  • superset/tasks/deletion_retention.py - 1
  • tests/integration_tests/deletion_retention/purge_tests.py - 1
Additional Suggestions - 11
  • superset/commands/deletion_retention/purge_cascade.py - 6
    • Missing unit tests for _dataset_permission_name · Line 375-384
      The new `_dataset_permission_name` function (lines 375–384) has no unit tests. Per BITO.md rule [11730], this helper that constructs a permission string from entity attributes must be tested to ensure it produces the correct identifier used by _cleanup_dataset_permission.
    • Missing unit tests for _cleanup_dataset_permission · Line 387-403
      The new `_cleanup_dataset_permission` function (lines 387–403) has no unit tests. Per BITO.md rule [11730], this permission-cleanup mutation that calls `security_manager._delete_pvm_on_sqla_event` with raw SQLAlchemy connection must be tested to ensure the PVM is correctly removed and no exceptions are raised.
    • Missing unit tests for _entity_version_targets · Line 405-434
      The new `_entity_version_targets` function (lines 405–434) has no unit tests. Per BITO.md rule [11730], this helper that determines which version shadow tables are scoped to an entity must be tested for each model type (Dashboard, Slice, SqlaTable) to ensure correct table/predicate pairs are returned.
    • Missing unit tests for _delete_version_history · Line 437-501
      The new `_delete_version_history` function (lines 437–501) has no unit tests. Per BITO.md rule [11730], new mutation logic covering version-history deletions and transaction orphan-sweeping must be tested across success paths, error scenarios, and edge cases — particularly the transaction-sharing scenario that exposes the unflushed-DELETE bug.
    • Missing unit tests for _sweep_orphan_transactions · Line 504-534
      The new `_sweep_orphan_transactions` function (lines 504–534) has no unit tests. Per BITO.md rule [11730], this orphan-sweeping logic — which directly deletes `version_transaction` rows — must be tested for both the orphaned and non-orphaned cases.
    • Missing unit tests for _delete_owned_children · Line 353-372
      The new `_delete_owned_children` function (lines 353–372) has no unit tests covering its two code paths (SqlaTable vs non-SqlaTable), the hard-deletion of TableColumn and SqlMetric rows, or the interaction with DB-level CASCADE. Per BITO.md rule [11730], new tools and mutation logic must have dedicated unit tests covering success paths, error handling, and edge cases.
  • superset/cli/deletion_retention.py - 2
    • Missing unit tests · Line 1-98
      BITO.md adaptive rule [11730] calls for comprehensive unit tests for new tools. The underlying `ForcePurgeCommand` and `resolve_retention_window` are tested in `tests/integration_tests/deletion_retention/`, but no CLI-level unit test covers the three click commands in this new module — unlike `thumbnails_test.py` and `guest_token_test.py` in the same directory.
    • Unused variable · Line 31-31
      Remove the unused `logger` declaration at line 31 — it is never referenced in the file. All commands output via `click.echo()`, so no logging call site exists to warrant a module-level logger.
  • tests/integration_tests/deletion_retention/window_tests.py - 1
    • Replace blind exception catch · Line 51-51
      Catching bare `Exception` is overly broad; catch specific exceptions instead.
  • tests/integration_tests/deletion_retention/purge_tests.py - 2
    • Boolean default positional argument issue · Line 52-52
      The `dry_run` parameter is a boolean with a default value used as a positional argument. Consider making it keyword-only to avoid FBT001/FBT002 violations. Multiple similar issues exist across this file (test methods also have boolean positional arguments with defaults).
    • Dynamically typed Any expressions · Line 187-187
      The function `lose_parent_delete` uses `typing.Any` for dynamically typed expressions. Multiple similar issues exist (lines 187, 197). Consider using more specific types where possible.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • tests/integration_tests/deletion_retention/window_tests.py - 1
  • superset/commands/deletion_retention/purge_cascade.py - 1
    • CWE-20: Missing datasource_type filter on dangling chart query · Line 183-183
Review Details
  • Files reviewed - 18 · Commit Range: 05ca6de..05ca6de
    • docker/pythonpath_dev/superset_config.py
    • superset/cli/deletion_retention.py
    • superset/commands/deletion_retention/__init__.py
    • superset/commands/deletion_retention/audit.py
    • superset/commands/deletion_retention/force_purge.py
    • superset/commands/deletion_retention/purge_cascade.py
    • superset/commands/deletion_retention/window.py
    • superset/config.py
    • superset/key_value/types.py
    • superset/migrations/versions/2026-06-23_13-00_e7d93a524ff6_add_purge_audit_log.py
    • superset/tasks/deletion_retention.py
    • tests/integration_tests/deletion_retention/__init__.py
    • tests/integration_tests/deletion_retention/_base.py
    • tests/integration_tests/deletion_retention/audit_tests.py
    • tests/integration_tests/deletion_retention/force_purge_tests.py
    • tests/integration_tests/deletion_retention/purge_tests.py
    • tests/integration_tests/deletion_retention/window_tests.py
    • tests/unit_tests/tasks/test_deletion_retention.py
  • Files skipped - 1
    • UPDATING.md - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread tests/unit_tests/tasks/test_deletion_retention.py
Comment thread superset/tasks/deletion_retention.py
Comment thread tests/integration_tests/deletion_retention/purge_tests.py
@mikebridge

Copy link
Copy Markdown
Contributor Author

Ran a 7-lens local review panel (python / sqlalchemy / committer / preset / clean-code / DDD / tidy-first) before merge, given this PR hard-deletes data. Fixes in ac6890f:

  • Retention cutoff clock: reverted to the naive-local clock soft_delete() uses for deleted_at (PR fix: Do not convert dataset changed_on to UTC #33693 convention) — the naive-UTC cutoff introduced in 91e9a69 shifted the window by the server's TZ offset (early purge west of UTC). Two lenses caught this independently against helpers.py.
  • embedded_dashboards explicit delete (7/7 lenses): the delete-orphan child carrying the public embed UUID leaked on FK-off SQLite; FK-off test extended.
  • Suppression ordering: commit moved outside suppress_purge_association_versions so the trim precedes Continuum's pending-statement execution; integration test pins zero association shadows with capture ON.
  • Chunked IN() lists in the orphan-transaction sweep (SQLite 999-bind limit; a long-history entity previously became permanently unpurgeable).
  • PurgeAuditLog moved to superset/models/ so it registers with Model.metadata at app init — alembic autogenerate would otherwise propose dropping the table; composite index added to the model.
  • Write-ahead audit now fails closed for the scheduled purge (skip + retry next run) while force-purge proceeds with the operator present — resolves the design question flagged on the earlier bot threads; UPDATING.md updated (incl. would_purge upper-bound and force-purge-on-live notes).
  • Conditional-UPDATE finalize + locked reconcile scan (race closure), click.UUID validation on --uuid, and an index on dashboard_slices_version(slice_id).

Deferred with tickets to follow: IntegrityError-vs-policy-block anti-corruption + persisted block reason, blocked-entity audit-row growth/dedup, docs/ page.

@bito-code-review

bito-code-review Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #c69970

Actionable Suggestions - 0
Additional Suggestions - 5
  • superset/cli/deletion_retention.py - 1
    • Missing CLI-level integration tests · Line 73-109
      The new CLI commands (set_window, show_window, force_purge) have no CLI-level integration tests. Tests exist for the underlying ForcePurgeCommand and resolve_retention_window() but not for the click entry points. Without CLI tests, regressions in argument parsing, confirmation prompts, or output formatting will not be caught.
  • docker/pythonpath_dev/superset_config.py - 1
    • Missing feature flag for dev · Line 87-87
      The import is correctly configured, but the scheduled task will silently skip because `SOFT_DELETE` defaults to `False` (config.py:698). Developers deploying this config may not realize the feature gate is off. Consider documenting the required flag or enabling it for dev environments.
  • tests/integration_tests/deletion_retention/window_tests.py - 2
    • Incorrect type annotation dict[str, object] · Line 78-78
      The local variable `result` on line 78 is annotated `dict[str, object]`, but `_purge_impl` (line 261, superset/tasks/deletion_retention.py) and `purge_soft_deleted` (line 261) are both declared to return `dict[str, Any]`. `Any` — not `object` — is the correct type parameter here. This causes a type-checker to flag a type mismatch on every assertion that reads from `result` (e.g., `result["purged"]`). Replace `object` with `Any` and add the necessary import from `typing` if not already present.
    • Replace broad Exception catch · Line 51-51
      The `except Exception` clause catches all exceptions without specific handling. Consider catching a more specific exception type (e.g., `KeyValueDAO.NoEntryFoundError` or a generic `Exception` alongside targeted logging).
  • tests/integration_tests/deletion_retention/force_purge_tests.py - 1
    • Test name mismatch with assertions · Line 97-97
      The test name mentions 'reports_relationship_count' but the test body verifies `removed_dashboard_slices` (a dashboard-chart relationship), not any ReportSchedule field. A reader could incorrectly assume the test covers report-blocking behavior. Rename to match the actual assertion.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset/commands/deletion_retention/window.py - 1
  • tests/integration_tests/deletion_retention/audit_tests.py - 3
  • superset/commands/deletion_retention/force_purge.py - 1
Review Details
  • Files reviewed - 20 · Commit Range: 47655b8..ac6890f
    • docker/pythonpath_dev/superset_config.py
    • superset/cli/deletion_retention.py
    • superset/commands/deletion_retention/__init__.py
    • superset/commands/deletion_retention/audit.py
    • superset/commands/deletion_retention/force_purge.py
    • superset/commands/deletion_retention/purge_cascade.py
    • superset/commands/deletion_retention/window.py
    • superset/config.py
    • superset/key_value/types.py
    • superset/migrations/versions/2026-07-28_09-00_e7d93a524ff6_add_purge_audit_log.py
    • superset/models/__init__.py
    • superset/models/purge_audit_log.py
    • superset/tasks/deletion_retention.py
    • tests/integration_tests/deletion_retention/__init__.py
    • tests/integration_tests/deletion_retention/_base.py
    • tests/integration_tests/deletion_retention/audit_tests.py
    • tests/integration_tests/deletion_retention/force_purge_tests.py
    • tests/integration_tests/deletion_retention/purge_tests.py
    • tests/integration_tests/deletion_retention/window_tests.py
    • tests/unit_tests/tasks/test_deletion_retention.py
  • Files skipped - 1
    • UPDATING.md - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@rusackas

rusackas commented Jul 28, 2026

Copy link
Copy Markdown
Member

Just noting the bito comments tucked away under the accordion menu. Not sure if you want to nab them now, or in a follow-up:

  • superset/cli/deletion_retention.py - 1
    • Missing CLI-level integration tests · Line 73-109
      • The new CLI commands (set_window, show_window, force_purge) have no CLI-level integration tests. Tests exist for the underlying ForcePurgeCommand and resolve_retention_window() but not for the click entry points. Without CLI tests, regressions in argument parsing, confirmation prompts, or output formatting will not be caught.
  • docker/pythonpath_dev/superset_config.py - 1
    • Missing feature flag for dev · Line 87-87
      • The import is correctly configured, but the scheduled task will silently skip because SOFT_DELETE defaults to False (config.py:698). Developers deploying this config may not realize the feature gate is off. Consider documenting the required flag or enabling it for dev environments.
  • tests/integration_tests/deletion_retention/window_tests.py - 2
    • Incorrect type annotation dict[str, object] · Line 78-78
      • The local variable result on line 78 is annotated dict[str, object], but _purge_impl (line 261, superset/tasks/deletion_retention.py) and purge_soft_deleted (line 261) are both declared to return dict[str, Any]. Any — not object — is the correct type parameter here. This causes a type-checker to flag a type mismatch on every assertion that reads from result (e.g., result["purged"]). Replace object with Any and add the necessary import from typing if not already present.
    • Replace broad Exception catch · Line 51-51
      • The except Exception clause catches all exceptions without specific handling. Consider catching a more specific exception type (e.g., KeyValueDAO.NoEntryFoundError or a generic Exception alongside targeted logging).
  • tests/integration_tests/deletion_retention/force_purge_tests.py - 1
    • Test name mismatch with assertions · Line 97-97
      • The test name mentions 'reports_relationship_count' but the test body verifies removed_dashboard_slices (a dashboard-chart relationship), not any ReportSchedule field. A reader could incorrectly assume the test covers report-blocking behavior. Rename to match the actual assertion.

…t name

Two nits from the bito suggestions @rusackas surfaced on apache#41549:

- The dev Celery config schedules deletion_retention.purge_soft_deleted
  but SOFT_DELETE is off by default, so the task runs and purges nothing.
  Note the gate where a developer reading the schedule will see it.
- test_force_purge_reports_relationship_count_before_db_cascade asserts on
  removed_dashboard_slices; 'reports' reads as ReportSchedule in this
  codebase, so name the test after what it actually counts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikebridge

Copy link
Copy Markdown
Contributor Author

Thanks for digging those out of the accordions — two of them were worth taking, and they're in db7ed72c52:

  • Dev config feature gate. You're right that this is a trap: the beat schedule registers deletion_retention.purge_soft_deleted but SOFT_DELETE is off by default, so it runs nightly and purges nothing. I've noted the gate inline at the schedule entry, where someone reading it will actually see it. I stopped short of flipping SOFT_DELETE: True in the dev FEATURE_FLAGS — that changes deletion behaviour for everyone using the dev compose stack, which feels like its own decision rather than a drive-by in this PR. Happy to do it if you'd rather.
  • Test name. Renamed to test_force_purge_counts_removed_dashboard_slices_before_db_cascade. Bito's stated reason was off (the "reports" in the old name was the verb, not ReportSchedule), but it lands on a fair point regardless — "reports" is a loaded noun in this codebase and the name read ambiguously.

Two I'd push back on:

  • except Exception in window_tests.py. That's in tearDown, wrapping a KeyValueDAO.delete_entry with a rollback, and it already carries # pylint: disable=broad-except. Broad catching is the correct shape there — narrowing it would let a teardown failure mask the actual test failure, which is the opposite of what you want when a purge test goes wrong.
  • dict[str, object]dict[str, Any]. The suggestion's rationale doesn't hold up: it claims this "causes a type-checker to flag a type mismatch on every assertion," but == is defined on object and mypy is green on this branch. Any would be marginally more consistent with the declared return type, so I'm not against it — I just didn't want to churn a line on a premise that isn't true. Say the word if you'd like it changed.

CLI-level tests — agreed, and taking it as a follow-up. The gap is real: set_window, show_window, and force_purge are tested at the command layer but not through the click entry points, so argument parsing, the confirmation prompt, and output formatting are uncovered. That's meaningful surface for an operator-facing destructive command. It's also a chunk of new test infrastructure on a PR that's already large, and this branch has been through a rebase and a review round since your approval, so I'd rather not reopen it for test scaffolding. Filing it alongside the other two follow-ups from this PR.

One thing worth flagging since you're here: your approval is from 07-13, and the head has moved a fair bit since — a 7-lens review round hardened the purge (cutoff-clock semantics reverted to match soft_delete()'s naive-local stamp, explicit embedded_dashboards deletion, suppression ordering, chunked IN (), PurgeAuditLog moved under superset/models/ so it registers with Model.metadata, and fail-closed write-ahead auditing for the scheduled path). CI is green on the current head. No obligation, but if you'd rather re-read it before it merges, now's the moment.

@bito-code-review

bito-code-review Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #3f2fed

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: ac6890f..db7ed72
    • docker/pythonpath_dev/superset_config.py
    • tests/integration_tests/deletion_retention/force_purge_tests.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@rusackas

Copy link
Copy Markdown
Member

Indeed, the review is/was old, but still LGTM in general.

Still open things that are not blockers, but I'm curious how/when you want to track or handle them (is there an overarching tracking issue, perhaps?):

  • the CLI commands still aren't directly unit-tested and probably ought to be
  • the M:N tripwire I flagged last time seems to be unsolved

@mikebridge

Copy link
Copy Markdown
Contributor Author

Thanks for re-confirming — and yes, both are now tracked. There wasn't an overarching issue for them before, which was a fair thing to call out; there is now:

  • CLI coverage — the set_window / show_window / force_purge entry points. Filed with CliRunner as the suggested approach and the force_purge confirmation prompt called out as the part that actually matters, since it's the destructive operator-facing path.
  • The M:N tripwire — filed as the shape we discussed in July rather than just "add tests": a declarative per-entity purge registry (deletion validators, owned children and association tables, listener-equivalent cleanup, version-shadow ownership predicates, relationships deliberately preserved or treated as blockers), paired with contract tests that introspect mapped relationships and FKs and fail when a chart, dashboard, or dataset gains an owned or cascading relationship with no explicit purge policy.

That second one is the interesting one, and I've noted on the ticket that it should land before the cascade grows to more entity types — the registry is much cheaper to introduce while the branch count is still small. The explicit cascade itself stays: it's what keeps purge consistent when SQLite FK enforcement is off, preserves ordinary deletion blockers, avoids traversing large relationship graphs, and handles Continuum transactions being shared across entities. The registry just moves the knowledge out of reviewers' heads and into something CI can fail on.

Both are on the internal versioning epic. Happy to mirror the tripwire one as a GitHub issue if you'd rather it were visible to the community — it's arguably a general Superset maintainability concern rather than a Preset one, so say the word and I'll open it.

…ansactions

A shadow row references version_transaction through either end of its
lifespan: transaction_id (created at) and, once a later edit closes it,
end_transaction_id (closed at). Both are foreign keys. The orphan sweep
only counted the created-at side, so a transaction still referenced
through a survivor's closing endpoint was judged orphaned and deleted,
failing the foreign key.

Transactions spanning entities are the normal case, not an edge case:
one flush saving a dashboard, chart and dataset gives all three a shadow
row at the same transaction. Editing any one of them closes its row
there and anchors the others' end pointers, so the hazard appears as
soon as two entities are ever saved together and one is edited again.

The failure was also misreported. cascade_hard_delete catches
IntegrityError alongside PurgeBlockedError, so the operator was told the
entity was blocked by existing deletion rules -- a deliberate policy
decision -- when the cascade was simply incomplete. That masquerade is
tracked separately as sc-115342; this commit removes the instance of it
that made versioned entities unpurgeable.

version_history_retention hit the identical trap and documents it on
_delete_for_transactions; the reasoning there applies verbatim here.

Found by the recently-archived archive-listing test on apache#41550, which
began exercising this path for the first time once its fixtures were
repaired. apache#41549's own suite had no test that purged an entity carrying
version history through the API, which is why CI was green.
forge_version_row now takes an optional end_tx_id so the regression test
can build the shape; the test fails without the production change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread superset/tasks/deletion_retention.py Outdated
Comment thread superset/tasks/deletion_retention.py
Comment thread superset/commands/deletion_retention/force_purge.py Outdated
mikebridge pushed a commit to mikebridge/superset that referenced this pull request Jul 29, 2026
…t the chart

"View version history" was gated on can_overwrite alone, which
hydrateExplore computes purely as "the current user is among the
slice's editors". A chart with no explicit editors yields false for
everyone, so on a fresh install -- where every seeded and example chart
has an empty editors list -- the menu item was absent for all users,
administrators included. The feature was effectively invisible until
someone hand-edited a chart's editors.

The server disagrees with that gate. security_manager.is_editor returns
True for admins outright, and unions extra_editors (granted by a
deployment's EXTRA_EDITORS_RESOLVER) into the editor set before
comparing. So the UI was hiding an action the API would have allowed,
for two distinct groups of users.

SaveModal already worked around the same gap with a broader predicate,
which is why an admin could save over a chart but not see its history.
That predicate is now a shared helper both call sites use, so the two
cannot drift apart again:

  canOverwrite || admin || in editors || in extra_editors,
  and never for an externally managed slice

Two things fixed while extracting it. SaveModal's isCurrentUserOwner
fallback checked slice.owners, which the owners -> editors rename left
behind: Slice.data ships editors, extra_editors and viewers, and no
owners at all, so that branch could never fire. It is dropped rather
than carried into the shared helper. And extra_editors was not consulted
anywhere in the frontend despite being in the payload for exactly this
purpose.

Unit tests mocked redux with can_overwrite: true, so the gate was never
exercised against what hydrateExplore actually computes. The new helper
tests cover the real shapes, starting with an admin on a chart whose
editors list is empty.

Reported from local testing against an integration branch of apache#42469 +
apache#41549 + apache#41551 + apache#41550.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikebridge

Copy link
Copy Markdown
Contributor Author

Heads-up that 9c73e69faf landed after your re-approval — flagging it rather than merging over it silently, because it's a behavioural fix and it's squarely in the area you were worried about.

The orphan-transaction sweep counted a version_transaction as referenced only through transaction_id, never end_transaction_id. Both are foreign keys: a shadow row points at the transaction that created it and, once a later edit closes it, at the one that ended it. Transactions spanning entities are the normal case — one flush saving a dashboard, chart and dataset gives all three a row at the same transaction — so as soon as any one of them is edited again, the others' end pointers keep that transaction alive. The sweep judged it orphaned, the delete failed the FK, and purge could not remove any entity with version history.

The way it failed is the worse half. cascade_hard_delete catches IntegrityError alongside PurgeBlockedError, so the operator was told the entity was "blocked by existing deletion rules" — a deliberate policy decision — when the cascade was simply incomplete. It looked like intended behaviour. That masquerade is filed separately as an internal follow-up; this commit just removes the instance of it.

This is the maintenance risk you named on 07-13, arriving through a column rather than a missing table. Worth noting how it surfaced: this PR's own suite never purged an entity carrying version history through the API, so CI was honestly green while the feature's core operation was broken for exactly the case it exists to serve. It only appeared because a test on the recovery-UI PR started exercising the path today. There's now a regression test here that fails against the old code. version_history_retention had already hit the identical trap and documents it on _delete_for_transactions — the reasoning transfers verbatim, which is a decent argument for the introspection-based tripwire we discussed rather than relying on either of us spotting it.

CI is green on the new head. No obligation to re-read — the change is ~10 lines plus a test — but you're welcome to, and I'd rather you heard it from the PR than found it later.

mikebridge pushed a commit to mikebridge/superset that referenced this pull request Jul 29, 2026
…t the chart

"View version history" was gated on can_overwrite alone, which
hydrateExplore computes purely as "the current user is among the
slice's editors". A chart with no explicit editors yields false for
everyone, so on a fresh install -- where every seeded and example chart
has an empty editors list -- the menu item was absent for all users,
administrators included. The feature was effectively invisible until
someone hand-edited a chart's editors.

The server disagrees with that gate. security_manager.is_editor returns
True for admins outright, and unions extra_editors (granted by a
deployment's EXTRA_EDITORS_RESOLVER) into the editor set before
comparing. So the UI was hiding an action the API would have allowed,
for two distinct groups of users.

SaveModal already worked around the same gap with a broader predicate,
which is why an admin could save over a chart but not see its history.
That predicate is now a shared helper both call sites use, so the two
cannot drift apart again:

  canOverwrite || admin || in editors || in extra_editors,
  and never for an externally managed slice

Two things fixed while extracting it. SaveModal's isCurrentUserOwner
fallback checked slice.owners, which the owners -> editors rename left
behind: Slice.data ships editors, extra_editors and viewers, and no
owners at all, so that branch could never fire. It is dropped rather
than carried into the shared helper. And extra_editors was not consulted
anywhere in the frontend despite being in the payload for exactly this
purpose.

Unit tests mocked redux with can_overwrite: true, so the gate was never
exercised against what hydrateExplore actually computes. The new helper
tests cover the real shapes, starting with an admin on a chart whose
editors list is empty.

Reported from local testing against an integration branch of apache#42469 +
apache#41549 + apache#41551 + apache#41550.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #91f117

Actionable Suggestions - 0
Review Details
  • Files reviewed - 3 · Commit Range: db7ed72..9c73e69
    • superset/commands/deletion_retention/purge_cascade.py
    • tests/integration_tests/deletion_retention/_base.py
    • tests/integration_tests/deletion_retention/purge_tests.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@rusackas

Copy link
Copy Markdown
Member

@mikebridge there are some fresh bot findings with "critical" reports... can you assess and resolve those (whether applying the fix or refuting them) before merging?

mikebridge pushed a commit to mikebridge/superset that referenced this pull request Jul 30, 2026
…t the chart

"View version history" was gated on can_overwrite alone, which
hydrateExplore computes purely as "the current user is among the
slice's editors". A chart with no explicit editors yields false for
everyone, so on a fresh install -- where every seeded and example chart
has an empty editors list -- the menu item was absent for all users,
administrators included. The feature was effectively invisible until
someone hand-edited a chart's editors.

The server disagrees with that gate. security_manager.is_editor returns
True for admins outright, and unions extra_editors (granted by a
deployment's EXTRA_EDITORS_RESOLVER) into the editor set before
comparing. So the UI was hiding an action the API would have allowed,
for two distinct groups of users.

SaveModal already worked around the same gap with a broader predicate,
which is why an admin could save over a chart but not see its history.
That predicate is now a shared helper both call sites use, so the two
cannot drift apart again:

  canOverwrite || admin || in editors || in extra_editors,
  and never for an externally managed slice

Two things fixed while extracting it. SaveModal's isCurrentUserOwner
fallback checked slice.owners, which the owners -> editors rename left
behind: Slice.data ships editors, extra_editors and viewers, and no
owners at all, so that branch could never fire. It is dropped rather
than carried into the shared helper. And extra_editors was not consulted
anywhere in the frontend despite being in the payload for exactly this
purpose.

Unit tests mocked redux with can_overwrite: true, so the gate was never
exercised against what hydrateExplore actually computes. The new helper
tests cover the real shapes, starting with an admin on a chart whose
editors list is empty.

Reported from local testing against an integration branch of apache#42469 +
apache#41549 + apache#41551 + apache#41550.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…in identity

Three findings from the codeant pass.

Force-purge resolved a bare UUID by scanning every soft-delete model and
taking the first match. UUID uniqueness is only enforced per table, and
the import APIs accept caller-supplied UUIDs, so that search can be
genuinely ambiguous -- and an operator running a compliance deletion
would have no way to say which entity they meant. It now collects every
match and refuses when there is more than one, naming the types so the
caller can disambiguate. A new model_cls argument lets a caller that
already knows the type pin resolution to it, which is what any caller
acting for an end user must do: their authorization was checked against
one specific entity. require_archived is added alongside it, because the
cascade runs with enforce_window=False here and so skips its own
deleted_at check.

Dry runs called audit.reconcile_pending(), which finalizes stale pending
rows -- a durable write. An operator sizing up a rollout was altering the
audit record they were inspecting. Reconciliation is now skipped when
dry_run is set.

The retention task snapshots an entity's uuid, writes the audit row, then
re-resolves by integer id. It did not check that the row it got back was
the row it described. Worth being precise about the severity: the
cascade's conditional claim re-checks deleted_at under lock, so a live
entity could never have been purged this way. What could drift is
attribution -- if an id were reused between the two resolutions (SQLite
recycles rowids; sequences do not) and the new occupant were itself
eligible, the purge would proceed while the audit named the previous
object. An audit row that identifies the wrong entity is worse than a
skipped purge, so identity is now verified before the cascade.

Each test fails against the previous code: the dry-run assertion sees
reconcile called once, and the ambiguity tests cannot import a symbol
that did not exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikebridge

Copy link
Copy Markdown
Contributor Author

@rusackas assessed all three — two are real and fixed, and the third is real but not at the severity it was given. 2cc0d92abe.

Ambiguous UUID resolution in force-purge (Critical — valid, fixed). _resolve() scanned every soft-delete model and returned the first match. UUID uniqueness is only enforced per table and the import APIs accept caller-supplied UUIDs, so the search can genuinely be ambiguous — and an operator running a compliance deletion had no way to express which entity they meant. It now collects every match and refuses when there is more than one, naming the types so the caller can disambiguate.

A model_cls argument goes alongside it, so a caller that already knows the type can pin resolution to it rather than relying on the search being unambiguous. That matters for #41550, which adds the first end-user-facing caller: its authorization is checked against one specific entity, so resolution must not be free to wander to another. Worth noting the flaw only becomes reachable by an end user in that PR — here the callers are the CLI and the retention task, both operator-grade.

Dry runs finalizing audit rows (Major — valid, fixed). audit.reconcile_pending() ran unconditionally, and it finalizes stale pending records, which is a durable write. An operator sizing up a rollout was mutating the audit record they were inspecting. Now skipped when dry_run is set, with the real path unchanged and both pinned by test.

Identity re-check in the retention task (Critical — valid, but the severity overstates it). The finding is correct that _purge_one snapshots a uuid, writes the audit row, re-resolves by integer id, and never checks it got the same row back. I've added the check.

But the destructive half of the claim doesn't hold. cascade_hard_delete runs with enforce_window=True here, and its conditional claim re-checks deleted_at IS NOT NULL AND deleted_at < cutoff under SELECT ... FOR UPDATE, then repeats those predicates on the DELETE itself. A live entity that inherited a recycled id could never have been purged by this path — it would fail the claim and raise PurgeRaceLostError.

What could actually drift is attribution: if an id were reused between the two resolutions and the new occupant were itself soft-deleted and past the cutoff, the purge would proceed while the audit row named the previous object. That needs id reuse, which SQLite does with rowids but Postgres and MySQL sequences do not. So: a real hole in the audit trail, not "concurrent purge can delete the wrong entity". Fixed regardless — an audit row that identifies the wrong entity is worse than a skipped purge, and the check is one comparison.

Each new test fails against the previous code: the dry-run assertion sees reconcile called once, and the ambiguity tests can't import a symbol that didn't exist. 50 tests green across the retention suites; pre-commit clean.

@mikebridge

Copy link
Copy Markdown
Contributor Author

Correction to an earlier comment of mine on this PR.

I wrote that require_archived closed the window between authorization and purge. That was wrong, and I want it on the record before anyone reviews on the strength of it.

ForcePurgeCommand runs the cascade with enforce_window=False, and in the code as it stands on this branch both the SELECT ... FOR UPDATE claim and the conditional DELETE add deleted_at predicates only under enforce_window. So require_archived filtered at resolution time and nowhere else: a restore committing after the resolve could still have its live row hard-deleted. It narrowed the race rather than closing it.

The fix — carrying the predicate into both statements — is on the follow-up branch for #41550, along with a control run showing the regression test fails without it. Two of the tests I pointed at as covering this were also passing for the wrong reason (one exited at a pragma: no cover guard without ever constructing ForcePurgeCommand); those are repaired there too.

Nothing here changes what this PR does — flagging it because my earlier description of it was inaccurate.

…e CLI

Two follow-ups from the review pass, both on the operator path.

The ambiguity refusal had no way out. ForcePurgeCommand raises
AmbiguousPurgeTargetError telling the operator to "pass the entity type to
disambiguate", but the CLI -- its only reachable caller, since the command's
other users pin model_cls -- exposed no such option and did not catch the
exception. The operator got a raw traceback, and got it *after* answering the
irreversible confirmation prompt: precisely the failure mode the type=click.UUID
validation a few lines above was added to prevent. force-purge now takes
--type/-t and reports the refusal as a ClickException naming the flag.

The identity guard in _purge_one had no test. Deleting the
entity_uuid(entity) != entity_uuid_value check left every test green, so
nothing pinned the invariant that an audit row must name the entity actually
purged. Covered by driving a uuid change between the audit write and the
re-check, and asserting the cascade does not run and the audit row is failed.

Controls: each new test fails against the pre-fix source (CLI reverted to the
bare ForcePurgeCommand(str(uuid)).run() with no handler; guard replaced by a
constant-false branch) and passes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikebridge

Copy link
Copy Markdown
Contributor Author

Pushed one more commit (2cdfe31) closing two gaps a review pass found in the codeant fixes. Both are on the operator path; no change to the retention task, the migration, or the cascade semantics.

The ambiguity refusal had no escape hatch. ForcePurgeCommand now refuses a UUID that matches rows in more than one table rather than guessing — but the message says "pass the entity type to disambiguate" and the CLI had no such option, nor a handler for the exception. The operator got a raw traceback, and got it after answering the irreversible confirmation prompt — the exact failure mode the type=click.UUID validation a few lines above was added to prevent. force-purge now takes --type/-t and reports the refusal as a clean ClickException.

The identity guard in _purge_one had no test. Deleting the entity_uuid(entity) != entity_uuid_value check left the whole suite green, so nothing pinned the invariant that an audit row names the entity actually purged. Now covered.

Each new test has a verified control: it fails against the pre-fix source and passes with the fix. Reverting the CLI to the bare ForcePurgeCommand(str(uuid)).run() fails both CLI tests; replacing the guard with a constant-false branch fails the identity test.

@eschutho @rusackas — re-request below since this touches the CLI you'd already approved. Happy to split it into a follow-up PR instead if you'd rather not re-review; the tradeoff is that master would briefly ship a force-purge that can traceback on an ambiguous UUID.

…its id

Closes the id-reuse race rather than narrowing it. The retention task
snapshots an entity's uuid, writes a write-ahead audit row, then re-resolves
by id and cascades. An id freed and reissued in that gap would be purged
while the audit row named the original -- an audit trail that identifies the
wrong object.

_purge_one already re-checked the uuid before cascading, but that check sits
above the row lock, so it narrowed the window instead of closing it. The
locked claim and the conditional delete now carry the uuid too, via
_identity_predicates, so a reused id matches no row at all. Both callers get
this: force-purge resolved by uuid to begin with, and the predicate simply
restates what it already established.

Extracted as a helper rather than inlined because cascade_hard_delete sits
exactly at the C901 limit; the helper adds no branch to it and gives the
claim and the delete one definition of identity instead of two.

Falls back to the id alone for a model with no uuid column.

Control: the new test fails with the uuid predicate removed (the stranger is
destroyed) and passes with it. Two earlier drafts of that test passed in both
directions -- one committed inside the cascade's savepoint so the row survived
on a broken transaction, the other detached the entity and died on
DetachedInstanceError -- so the assertion now desynchronizes the in-memory
snapshot from the stored row with set_committed_value and calls the cascade
directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #c21dfd

Actionable Suggestions - 0
Additional Suggestions - 3
  • superset/tasks/deletion_retention.py - 2
    • Missing test for dry-run guard · Line 109-118
      `_reconcile_unless_dry_run` is unreferenced in any test file. With `SOFT_DELETE_PURGE_DRY_RUN` defaulting to `True`, this is the hot path — no coverage means the dry-run guard can silently regress.
    • Missing unit test for id-reuse guard · Line 243-259
      Add an isolated unit test for the UUID-mismatch guard by mocking `entity_uuid` to return different values on consecutive `db.session.get` calls, asserting that `cascade_hard_delete` is not called, `audit.fail` is called with the record ID, and the returned purged/would/blocked/failures tuple matches expectations. Ensure `MagicMock` is imported from `unittest.mock`.
  • tests/integration_tests/deletion_retention/force_purge_tests.py - 1
    • Assertion checks wrong exception type · Line 208-208
      Use `assert isinstance(result.exception, click.ClickException)` instead of `assert not isinstance(result.exception, AmbiguousPurgeTargetError)`, while preserving the `assert result.exit_code != 0` check.
Review Details
  • Files reviewed - 6 · Commit Range: 9c73e69..5c99638
    • superset/commands/deletion_retention/force_purge.py
    • superset/tasks/deletion_retention.py
    • tests/integration_tests/deletion_retention/force_purge_tests.py
    • tests/integration_tests/deletion_retention/purge_tests.py
    • superset/cli/deletion_retention.py
    • superset/commands/deletion_retention/purge_cascade.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@eschutho
eschutho merged commit 59a5ae0 into apache:master Jul 30, 2026
60 checks passed
mikebridge pushed a commit to mikebridge/superset that referenced this pull request Jul 31, 2026
…-positive tests

The review pass found that three tests added for the restore/purge race and
the bootstrap window did not exercise the code they named. Each is fixed and
each now has a verified control run: it fails against the pre-fix source and
passes against the fix.

1. require_archived reaches the cascade, not just resolution.

   ForcePurgeCommand ran the cascade with enforce_window=False, and both the
   locked claim and the conditional delete added deleted_at predicates only
   under enforce_window. require_archived therefore narrowed the window
   between authorization and purge without closing it -- a restore committing
   after the resolve could still have its live row hard-deleted. The flag is
   now carried into both statements.

   An earlier commit message on this branch and a PR comment on apache#41549 both
   claimed this race was already closed. It was not; that claim was wrong.

2. test_force_purge_refuses_a_row_restored_after_resolution was green either
   way. run() resolves twice and only the second result reaches the cascade,
   so restoring during the first call made the second resolve return None
   under require_archived and the command exited at its not-found guard
   without running the cascade at all. The restore now lands after the final
   resolve. Control: fails with the cascade predicates disabled.

3. test_purge_of_restored_chart_reports_not_found never constructed
   ForcePurgeCommand. It patched PurgeArchivedCommand.validate, whose return
   value is discarded -- validate reports through self._model -- so run() exited
   at the "cannot happen" guard marked pragma: no cover and returned 404 from
   a defensive branch. It now hooks _resolve and lets the real validate run.
   Control: fails with require_archived=False.

4. The four bootstrap window tests shared one cache entry.
   cached_common_bootstrap_data is memoized for 60s on (user_id, locale) alone
   and the branches under test live inside the memoized body, so the first
   test's payload was served to the rest. They now call the .uncached body
   directly rather than clearing the cache, which would tie the assertions to
   a reachable Redis. Control: all four fail against the pre-fix bootstrap,
   including the phantom-key test.

Extracting _eligibility_predicates keeps cascade_hard_delete under the C901
limit and gives the claim and the delete one shared definition of eligibility
instead of two parallel branch chains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request Jul 31, 2026
…t the chart

"View version history" was gated on can_overwrite alone, which
hydrateExplore computes purely as "the current user is among the
slice's editors". A chart with no explicit editors yields false for
everyone, so on a fresh install -- where every seeded and example chart
has an empty editors list -- the menu item was absent for all users,
administrators included. The feature was effectively invisible until
someone hand-edited a chart's editors.

The server disagrees with that gate. security_manager.is_editor returns
True for admins outright, and unions extra_editors (granted by a
deployment's EXTRA_EDITORS_RESOLVER) into the editor set before
comparing. So the UI was hiding an action the API would have allowed,
for two distinct groups of users.

SaveModal already worked around the same gap with a broader predicate,
which is why an admin could save over a chart but not see its history.
That predicate is now a shared helper both call sites use, so the two
cannot drift apart again:

  canOverwrite || admin || in editors || in extra_editors,
  and never for an externally managed slice

Two things fixed while extracting it. SaveModal's isCurrentUserOwner
fallback checked slice.owners, which the owners -> editors rename left
behind: Slice.data ships editors, extra_editors and viewers, and no
owners at all, so that branch could never fire. It is dropped rather
than carried into the shared helper. And extra_editors was not consulted
anywhere in the frontend despite being in the payload for exactly this
purpose.

Unit tests mocked redux with can_overwrite: true, so the gate was never
exercised against what hydrateExplore actually computes. The new helper
tests cover the real shapes, starting with an admin on a chart whose
editors list is empty.

Reported from local testing against an integration branch of apache#42469 +
apache#41549 + apache#41551 + apache#41550.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request Aug 3, 2026
…t the chart

"View version history" was gated on can_overwrite alone, which
hydrateExplore computes purely as "the current user is among the
slice's editors". A chart with no explicit editors yields false for
everyone, so on a fresh install -- where every seeded and example chart
has an empty editors list -- the menu item was absent for all users,
administrators included. The feature was effectively invisible until
someone hand-edited a chart's editors.

The server disagrees with that gate. security_manager.is_editor returns
True for admins outright, and unions extra_editors (granted by a
deployment's EXTRA_EDITORS_RESOLVER) into the editor set before
comparing. So the UI was hiding an action the API would have allowed,
for two distinct groups of users.

SaveModal already worked around the same gap with a broader predicate,
which is why an admin could save over a chart but not see its history.
That predicate is now a shared helper both call sites use, so the two
cannot drift apart again:

  canOverwrite || admin || in editors || in extra_editors,
  and never for an externally managed slice

Two things fixed while extracting it. SaveModal's isCurrentUserOwner
fallback checked slice.owners, which the owners -> editors rename left
behind: Slice.data ships editors, extra_editors and viewers, and no
owners at all, so that branch could never fire. It is dropped rather
than carried into the shared helper. And extra_editors was not consulted
anywhere in the frontend despite being in the payload for exactly this
purpose.

Unit tests mocked redux with can_overwrite: true, so the gate was never
exercised against what hydrateExplore actually computes. The new helper
tests cover the real shapes, starting with an admin on a chart whose
editors list is empty.

Reported from local testing against an integration branch of apache#42469 +
apache#41549 + apache#41551 + apache#41550.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:backend Requires changing the backend risk:db-migration PRs that require a DB migration size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants