Skip to content

refactor(deletion-retention): add declarative purge policies - #42888

Open
mikebridge wants to merge 5 commits into
apache:masterfrom
mikebridge:sc-115410-purge-policy-registry
Open

refactor(deletion-retention): add declarative purge policies#42888
mikebridge wants to merge 5 commits into
apache:masterfrom
mikebridge:sc-115410-purge-policy-registry

Conversation

@mikebridge

Copy link
Copy Markdown
Contributor

SUMMARY

Replace the hard-coded soft-delete purge cascade with a declarative policy registry for charts, dashboards, and datasets.

The registry classifies discovered dependencies as owned, association, preserved, blocked, version-owned, or listener-driven, then validates policy completeness before executing Core SQL deletes. It also makes persistent after_delete effects explicit through typed listener declarations and derives version-shadow cleanup from policy metadata.

Key safeguards include:

  • a production-root tripwire requiring every scheduled SoftDeleteMixin model to have a purge policy
  • recursive owned/association traversal with deepest-first deletion
  • explicit blockers and preservation rules
  • runtime validation that supported-root deletion listeners are declared
  • SQLite, PostgreSQL, and MySQL statement compilation coverage
  • deterministic SQL-statement budgets and an opt-in fixed-cardinality timing protocol for all three roots

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable; this is a backend retention refactor with no UI changes.

TESTING INSTRUCTIONS

source ~/venv/superset-2/bin/activate
pytest -q \
  tests/unit_tests/commands/deletion_retention/test_purge_policy.py \
  tests/integration_tests/deletion_retention/purge_tests.py \
  tests/integration_tests/deletion_retention/purge_performance_tests.py

changed_files=(${(f)"$(git diff --name-only preset/master...HEAD)"})
pre-commit run --files "${changed_files[@]}"

Expected result: 60 passed, 1 opt-in timing benchmark skipped, and all applicable pre-commit hooks pass.

To run the manual elapsed-time protocol, set SUPERSET_PURGE_BENCHMARK=1 and supply the matching merge-base medians through:

  • SUPERSET_PURGE_BASELINE_CHART_SECONDS
  • SUPERSET_PURGE_BASELINE_DASHBOARD_SECONDS
  • SUPERSET_PURGE_BASELINE_DATASET_SECONDS

ADDITIONAL INFORMATION

  • Has associated issue: SC-115410
  • Required feature flags:
  • 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
  • Introduces new feature or API
  • Removes existing feature or API

@bito-code-review

bito-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #f96b7d

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/commands/deletion_retention/purge_policy.py - 1
    • CWE-400: Recursion Stack Overflow · Line 1143-1166
      The recursive traversal in `_dependency_owner_depth` can hit Python's default recursion limit (1000) on deep ownership chains during purge execution, causing an unrecoverable crash. Convert to iterative with explicit depth cap.
Filtered by Review Rules

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

  • superset/commands/deletion_retention/purge_policy.py - 1
Review Details
  • Files reviewed - 9 · Commit Range: 3dd2b48..3dd2b48
    • superset/commands/deletion_retention/purge_cascade.py
    • superset/commands/deletion_retention/purge_policy.py
    • superset/connectors/sqla/models.py
    • superset/tags/core.py
    • superset/utils/sqlalchemy_events.py
    • tests/integration_tests/deletion_retention/purge_performance_tests.py
    • tests/integration_tests/deletion_retention/purge_tests.py
    • tests/unit_tests/commands/deletion_retention/__init__.py
    • tests/unit_tests/commands/deletion_retention/test_purge_policy.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

@dosubot dosubot Bot added change:backend Requires changing the backend risk:refactor High risk as it involves large refactoring work labels Aug 7, 2026
@netlify

netlify Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 3dd2b48
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a75f4d0a27ad000083a4fc6
😎 Deploy Preview https://deploy-preview-42888--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.

Comment thread superset/commands/deletion_retention/purge_cascade.py Outdated
…licy module

Simplification pass over the declarative purge-policy registry:

- drop the write-only DependencyKey.relationship_aliases field, the
  discovery-time alias merge that populated it, and its identity test;
  nothing ever read the aliases
- drop ExecutionPhase.SNAPSHOT (referenced nowhere) and the unreachable
  LISTENER_EFFECT entry in the classification-to-phase map (synthetic
  listener dependencies always carry an explicit phase)
- replace five single-method Protocol classes with one-line Callable
  aliases; the signatures are all positional, so the protocols bought
  no extra type safety
- cache _validated_purge_policy with lru_cache instead of a hand-rolled
  module-level dict (parameters annotated as bare type: mypy's functools
  stubs reject type[Any] against lru_cache's Hashable bound)
- resolve each dependency table once and check the inbound-FK guard in
  one place, passing the table into _dependency_predicates
- extract _ownership_edge() for the ownership-path lookup previously
  duplicated between _owner_value_select and _dependency_owner_depth
- reduce the callback-typing test to the phase assertion that can
  actually fail; assert callable() on typed dataclass fields is enforced
  by mypy already

Verified: 30/30 unit tests, mypy, ruff, changed-file pre-commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. Capturing the permission name before the row is locked creates a race condition where the permission identifier could become stale if the dataset's table_name or database_id is updated concurrently. To resolve this, the permission name should be captured after the row has been successfully locked using with_for_update().

To implement this, move the permission name capture inside the session.begin_nested() block, after the lock is acquired. Would you like me to implement this fix and check the remaining comments on this PR?

superset/commands/deletion_retention/purge_cascade.py

if session.execute(claim.with_for_update()).scalar_one_or_none() is None:
                raise PurgeRaceLostError

            # Capture permission name here, after the lock is acquired
            permission_name = policy.capture_permission_name(entity, policy)
            policy.validate(session, policy, entity_id)

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.99043% with 92 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.38%. Comparing base (f1411cc) to head (744d413).
⚠️ Report is 9 commits behind head on master.

Files with missing lines Patch % Lines
...perset/commands/deletion_retention/purge_policy.py 75.36% 65 Missing and 19 partials ⚠️
superset/utils/sqlalchemy_events.py 85.36% 2 Missing and 4 partials ⚠️
...erset/commands/deletion_retention/purge_cascade.py 89.47% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42888      +/-   ##
==========================================
+ Coverage   66.36%   66.38%   +0.01%     
==========================================
  Files        2857     2859       +2     
  Lines      161048   161367     +319     
  Branches    37046    37083      +37     
==========================================
+ Hits       106886   107116     +230     
- Misses      52147    52215      +68     
- Partials     2015     2036      +21     
Flag Coverage Δ
hive 38.28% <34.44%> (+0.01%) ⬆️
mysql 57.81% <77.99%> (+0.05%) ⬆️
postgres 57.86% <77.99%> (+0.05%) ⬆️
presto 40.23% <34.44%> (+<0.01%) ⬆️
python 59.25% <77.99%> (+0.05%) ⬆️
sqlite 57.48% <77.99%> (+0.05%) ⬆️
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.

…he permission-capture race

Two fixes for review findings on the purge-policy registry PR:

1. CI: test_real_mapper_graph_has_complete_policy failed in full-suite runs
   with 'Could not locate any simple equality expressions ... on relationship
   Tag.created_by'. Root cause is the 2018 add_implicit_tags migration
   script: its throwaway declarative models inherited FAB's AuditMixin,
   whose created_by/changed_by relationships leave a permanently
   unconfigurable mapper in the global registry once alembic imports the
   script (the unit-test app fixture's pending-migration check does exactly
   that). The first test to trigger configure_mappers() afterwards fails —
   the new coverage test was simply the first caller. The script now
   declares the audit columns directly (identical DDL, no relationships),
   and a regression test imports the script and configures mappers.

2. codeant finding (seconded by bito): the dataset permission name was
   captured from the in-memory entity before the purge claimed and locked
   the row, so a rename or database move committed in that window made the
   cleanup delete a stale permission while orphaning the real one. The
   capture now runs under the row lock and reads table_name/database_name
   from the database, and the callback takes (session, policy, entity_id)
   like every other policy action.

Verified: 146 unit (incl. the previously failing suite-order combination),
26 integration purge tests, mypy, changed-file pre-commit.

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

Copy link
Copy Markdown
Contributor Author

Addressed the automated review feedback:

  • The dataset-permission identity race was fixed in 38272dfae1: permission identity is read from the database after the purge row is locked, and the existing inline thread has been replied to.
  • The ownership-path recursion suggestion was fixed in 744d41340f: both depth ordering and owner-value predicate construction are iterative and detect cycles. Regression coverage exercises a 1,100-edge mapped ownership path through the actual predicate builder, plus a cyclic policy.
  • The filtered “missing visited state propagation” suggestion does not apply to the current implementation: discover_dependencies() and _discover_table_dependencies() both pass their updated visited set into recursive discovery, and test_recursive_discovery_stops_at_owned_cycles covers that behavior.

Fresh local verification after the latest fix: 62 passed, 1 skipped; changed-file pre-commit passed, including MyPy, Ruff, and Pylint; final Python review returned no findings.

@bito-code-review

bito-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #faf318

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/commands/deletion_retention/purge_policy.py - 1
    • Missing test for multi-level ownership iteration · Line 1040-1069
      The new iteration loop in `_owner_value_select` (lines 1040–1069) traces multi-level ownership chains correctly — the logic uses `local_columns` for parent-key selection and `remote_columns` for child-key filtering on each step. However, no test directly exercises `_owner_value_select` with a multi-edge path. The existing `test_dependency_owner_depth_rejects_cycles` only covers `_dependency_owner_depth`. Without a coverage gap here, a regression in the iteration logic (e.g., swapping `local_columns`/`remote_columns` or off-by-one in `path_table_name`) would not be caught.
Filtered by Review Rules

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

  • superset/commands/deletion_retention/purge_policy.py - 1
Review Details
  • Files reviewed - 5 · Commit Range: 3dd2b48..744d413
    • superset/commands/deletion_retention/purge_policy.py
    • tests/unit_tests/commands/deletion_retention/test_purge_policy.py
    • superset/commands/deletion_retention/purge_cascade.py
    • superset/migrations/versions/2018-07-26_11-10_c82ee8a39623_add_implicit_tags.py
    • tests/unit_tests/migrations/test_add_implicit_tags_mapper_isolation.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

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 risk:refactor High risk as it involves large refactoring work size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant