Skip to content

feat(charts): soft-delete and restore#40129

Merged
rusackas merged 40 commits into
apache:masterfrom
mikebridge:sc-106189-charts-soft-delete
Jul 6, 2026
Merged

feat(charts): soft-delete and restore#40129
rusackas merged 40 commits into
apache:masterfrom
mikebridge:sc-106189-charts-soft-delete

Conversation

@mikebridge

@mikebridge mikebridge commented May 14, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

Wire charts to the soft-delete infrastructure from #39977 — one of three entity rollouts (charts / dashboards / datasets) decomposing the soft-delete work per SIP-208.

Slice now inherits SoftDeleteMixin. A global do_orm_execute listener excludes soft-deleted rows from every ORM SELECT, including relationship lazy loads (e.g. dashboard.slices). DELETE becomes a soft delete; a new restore endpoint and a chart_deleted_state rison filter let owners/admins enumerate and recover soft-deleted charts; the v1 importer treats re-import of a soft-deleted UUID as an implicit restore-with-update.

Merge-ordering constraint satisfied (2026-07-01). #41166 (the SOFT_DELETE release toggle, default off) is merged to master and this branch is rebased onto it, so everything below is gated by the SOFT_DELETE feature flag — this PR ships dark. With the flag off (the default), delete behavior is unchanged (permanent hard delete).

What this PR ships

Migrationsuperset/migrations/versions/2026-05-08_12-00_7c4a8d09ca37_add_deleted_at_to_slices.py

  • revision = 7c4a8d09ca37, down_revision = 9e1f3b8c4d2a (master's current head, from the dashboards soft-delete migration). Adds a nullable deleted_at column and the ix_slices_deleted_at index to slices. Reversible; uses superset.migrations.shared.utils helpers.

Model + DAO

  • Slice inherits SoftDeleteMixin. The infrastructure listener filters Slice queries, including relationship loads via with_loader_criteria.
  • DeleteChartCommand routes through BaseDAO.delete(), which detects the mixin and dispatches to soft_delete() — no source change to the delete command.

Import pipelinesuperset/commands/chart/importers/v1/utils.py

  • Re-importing a UUID that matches a soft-deleted chart is an implicit restore-and-update. An owner/admin with can_write gets deleted_at cleared and the upload applied via UPDATE, preserving the PK and out-of-archive references (dashboard_slices junctions, report.chart_id, tag rows). Non-owners get ImportFailedError. Callers without can_write get ImportFailedError rather than silently receiving the soft-deleted row. Full matrix in the import_chart docstring.

API CHANGES

POST /api/v1/chart/<uuid>/restore (new)

  • Method / path: POST /api/v1/chart/<uuid>/restore (UUID path param, format: uuid).
  • Permission: can_write on Chart (method_permission_name["restore"] = "write", class_permission_name = "Chart") plus resource-level ownership via security_manager.raise_for_ownership (admins pass).
  • Request body: none.
  • Responses:
    • 200{ "message": "OK" }; deleted_at cleared, chart returns to active state.
    • 403 — caller has can_write but is not an owner/admin (ChartForbiddenError).
    • 404 — no chart with that UUID, or the chart exists but is not soft-deleted / not visible to the caller (ChartNotFoundError).
    • 422 — restore aborted by an underlying DB error (ChartRestoreFailedError).
    • 401 / 500 — standard.

chart_deleted_state rison list filter (new) — on GET /api/v1/chart/

  • Values: include (live + soft-deleted), only (soft-deleted only); absent / any other value → live rows only.
  • Non-admins only see soft-deleted charts they own (the same audience that can restore them).
  • When include/only is used, each result row is augmented with a deleted_at field.

DELETE /api/v1/chart/<pk> and bulk DELETE /api/v1/chart/ (behavior change)

  • Now soft-delete: set deleted_at, preserve the row, hide it from list/detail/lookup and relationship loads. Recoverable via the restore endpoint. Existing alert/report protection still blocks deletion of charts with active reports.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

No UI changes — REST endpoints only. A frontend "Restore" affordance is a follow-up.

Endpoint Before After (flag on)
DELETE /api/v1/chart/<pk> Hard-deletes the row Sets deleted_at; row hidden from default queries
GET /api/v1/chart/?...&chart_deleted_state=only Filter ignored Returns soft-deleted charts with deleted_at populated
GET /api/v1/chart/?...&chart_deleted_state=include Filter ignored Returns live + soft-deleted; deleted_at populated on deleted rows
POST /api/v1/chart/<uuid>/restore 404 (no endpoint) Clears deleted_at on the soft-deleted row

TESTING INSTRUCTIONS

Automated

pytest tests/unit_tests/commands/chart/restore_test.py -v
pytest tests/integration_tests/charts/soft_delete_tests.py -v

QA / manual — enable the SOFT_DELETE feature flag first (it is on master via #41166, default off); then:

  1. Soft-delete hides the chart. As an owner, DELETE /api/v1/chart/<pk> (or delete in the UI). Confirm 200, then GET /api/v1/chart/<pk>404 and the chart no longer appears in GET /api/v1/chart/. Confirm the slices row still exists in the DB with deleted_at set.
  2. Relationship hiding. Open a dashboard that embedded the chart; the slot shows the missing-chart placeholder (no cascade).
  3. Enumerate soft-deleted. GET /api/v1/chart/?q=(filters:!((col:id,opr:chart_deleted_state,value:only))) → the chart appears with a populated deleted_at. Repeat with value:include → both live and soft-deleted rows.
  4. Owner scoping. As a non-admin who does NOT own the chart, repeat step 3 → the soft-deleted chart is NOT listed.
  5. Restore. As the owner (or admin), POST /api/v1/chart/<uuid>/restore200 {"message":"OK"}, then GET /api/v1/chart/<pk> returns the chart and it reappears in default lists; dashboard slot renders again.
  6. Restore negative — ownership. As a user with can_write but no ownership, restore someone else's soft-deleted chart → 403.
  7. Restore negative — not found / already live. Restore a random UUID → 404; restore a live (non-soft-deleted) UUID → 404.
  8. Importer restore. Soft-delete a chart, then re-import its YAML (same UUID) as owner/admin with can_write → restored in place (same PK, dashboards reattached). Re-import as a non-owner → ImportFailedError; as a caller without can_writeImportFailedError.
  9. Flag off. Disable SOFT_DELETE and confirm DELETE hard-deletes as before and the restore endpoint / chart_deleted_state filter are inert.

ADDITIONAL INFORMATION

  • Has associated issue: SIP-208 #39464
  • Required feature flags: SOFT_DELETE (merged in feat(soft-delete): gate soft delete behind a temporary SOFT_DELETE release toggle #41166, default off — this PR ships dark)
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible (nullable column + index; downgrade reverses both)
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided — additive: a nullable deleted_at column with no default (instant on Postgres 11+ / MySQL 8+) plus a single-column index ix_slices_deleted_at. The index build is the only meaningful cost (non-blocking online on MySQL InnoDB; a few seconds on a large slices table on Postgres — no CONCURRENTLY, so writes to slices may briefly queue during the build; reads unaffected).
  • Introduces new feature or API — soft-delete behavior (gated by SOFT_DELETE), the chart_deleted_state rison filter, POST /api/v1/chart/<uuid>/restore, and a deleted_at field on list responses when the deleted-state filter is used
  • Removes existing feature or API

Behavior changes worth flagging

  • DELETE /api/v1/chart/<pk> is soft when SOFT_DELETE is on. Consumers expecting permanent removal will find the chart recoverable. The row is hidden from the default API after DELETE, but direct slices table queries still see it.
  • No cascade. Dashboards referencing a soft-deleted chart render the existing missing-chart placeholder. Soft-delete does not cascade per SIP-208 v1.

Depends on

@github-actions github-actions Bot added risk:db-migration PRs that require a DB migration api Related to the REST API labels May 14, 2026
@netlify

netlify Bot commented May 14, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 4d8b965
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a4bc9790f1ac60008d34dd7
😎 Deploy Preview https://deploy-preview-40129--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 May 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.37838% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.35%. Comparing base (1575b83) to head (4d8b965).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
superset/commands/chart/importers/v1/utils.py 40.00% 7 Missing and 2 partials ⚠️
superset/commands/report/execute.py 0.00% 3 Missing and 3 partials ⚠️
superset/charts/api.py 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #40129      +/-   ##
==========================================
- Coverage   64.72%   59.35%   -5.37%     
==========================================
  Files        2686      822    -1864     
  Lines      148651    70487   -78164     
  Branches    34309     8838   -25471     
==========================================
- Hits        96213    41840   -54373     
+ Misses      50676    26880   -23796     
- Partials     1762     1767       +5     
Flag Coverage Δ
hive 39.20% <44.59%> (+<0.01%) ⬆️
javascript ?
mysql 57.88% <78.37%> (+0.02%) ⬆️
postgres 57.94% <78.37%> (+0.02%) ⬆️
presto 40.74% <44.59%> (+<0.01%) ⬆️
python 59.33% <78.37%> (+0.02%) ⬆️
sqlite 57.52% <78.37%> (+0.02%) ⬆️
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-106189-charts-soft-delete branch from e53c17e to 7230227 Compare May 18, 2026 17:03
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 20, 2026
Eight items from the 2026-05-20 review on the infrastructure PR:

* **H1 — subclass walk cached via ``__init_subclass__``.** The previous
  ``_all_soft_delete_subclasses()`` walked ``SoftDeleteMixin.__subclasses__()``
  on every primary ORM SELECT. With one adopter today the cost is
  negligible, but the listener fires on every query in the app and the
  walk grows linearly with each adopted entity. ``SoftDeleteMixin`` now
  maintains a sorted ``_registered_subclasses`` list updated by
  ``__init_subclass__`` at class-definition time; the listener reads
  the cached list with no walk. Sort happens once per registration
  (rare event) rather than per query.

* **M3 — ``SoftDeleteMixin`` import in ``BaseDAO.delete`` moved to
  top.** ``SKIP_VISIBILITY_FILTER_CLASSES`` from the same module was
  already top-level; ``SoftDeleteMixin`` had no real reason to be
  inline. Cleaner.

* **M4 — pylint-disable in ``raise_for_ownership`` documented.** A
  top-level import here actually does cause a circular
  (security ↔ models.core ↔ superset's lazy
  ``feature_flag_manager``). Kept the inline import but added a
  comment naming the cycle so future maintainers don't try to "fix"
  it and re-trip the issue.

* **M5 — ``BaseDeletedStateFilter.model`` typed as
  ``ClassVar[type[SoftDeleteMixin]]``.** Previously ``Any``, which
  meant a subclass accidentally binding ``model`` to a
  non-soft-deletable entity would crash at runtime on
  ``.deleted_at`` rather than failing mypy.

* **M6 — listener placement documented.** ``setup_soft_delete_listener``
  is called outside the ``with app_context()`` block because the
  ``do_orm_execute`` hook attaches to the ``Session`` class
  (process-wide), not to a Session instance, so it doesn't need
  Flask app context. Added an explaining comment in
  ``init_app_in_ctx``'s caller; ``setup_db()`` already ran earlier
  so the Session import is initialised.

* **M7 — unit tests for ``BaseDeletedStateFilter`` +
  ``SoftDeleteApiMixin``.** New
  ``tests/unit_tests/views/test_soft_delete_filter.py`` (10 tests):
  filter no-op / include / only / case-insensitivity / unknown value;
  mixin no-op-without-flag / inject-with-flag / consume-flag-on-call;
  ``_serialize_deleted_at`` None / datetime handling.

* **Nit — explicit ``orig_resource is None`` guard in
  ``raise_for_ownership``.** Previously fell through to
  ``hasattr(None, "owners")`` returning ``False`` → ``owners = []`` →
  generic ownership error. Now raises the proper "resource removed
  before ownership could be verified" error so the cause is clear in
  the rare race-with-hard-delete case.

* **Nit — unit tests for ``find_existing_for_import`` /
  ``clear_soft_deleted_for_import``.** New
  ``tests/unit_tests/commands/importers/v1/test_find_existing_for_import.py``
  (5 tests): no-match-returns-None, live-row-as-is,
  soft-deleted-row-as-is (pure lookup, no side effect),
  ``clear`` hard-deletes via ORM (listener-firing path), and the
  composed find-then-clear contract that's the documented caller
  sequence.

Deferred with explanation in the PR-comment reply (not in this
commit):

* **H2 — non-restorable / GDPR hook**: V2 concern. Designing the
  interface without a concrete use case risks getting it wrong;
  follow-up ticket if compliance work is ever scoped.

* **M8 — ``deleted_at`` schema mutation**: documented in the entity-
  rollout PR bodies (apache#40128/apache#40129/apache#40130) rather than blocking
  infrastructure.

40 unit tests pass (was 26; added 14 covering filter, mixin, importer
helpers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mikebridge
mikebridge force-pushed the sc-106189-charts-soft-delete branch 5 times, most recently from c5bcec6 to 625b2bb Compare May 20, 2026 22:36
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 20, 2026
The listener's loader_criteria was passing a concrete SQL expression
(``cls.where_not_deleted()``) which rendered as the raw table name
(``slices.deleted_at``) regardless of how the table was aliased in
the statement. When the same soft-deletable model appeared under an
alias in a JOIN — e.g., FAB's outer/inner reconstruction or
``report_schedule.chart`` aliasing ``slices AS chart`` — the JOIN's
ON clause produced ``Unknown column 'slices.deleted_at' in 'on
clause'`` and broke the entire query.

Surfaced as CI failures on the entity-rollout PRs (apache#40128 / apache#40129 /
apache#40130) — specifically pre-existing report-schedule tests that join
to ``slices AS chart`` started failing once ``Slice`` inherited
``SoftDeleteMixin``. The infrastructure PR itself doesn't exercise
this because no entity has the mixin yet.

Fix: switch the criteria to a lambda (the form Bayer's canonical
pattern uses). SQLAlchemy invokes the lambda per occurrence and
adapts the column reference to the alias at each site. The lambda's
body is trivial (``c.deleted_at.is_(None)``) so the
``DeferredLambdaElement`` parser handles it without issue — we
originally moved AWAY from a lambda because of an
``if cls in bypass_classes`` conditional that DeferredLambdaElement
mis-parsed as a SQL ``IN`` operator, but the conditional now lives
outside the lambda (in the per-class iteration), so the lambda body
itself is clean.

Test added:
test_listener_adapts_criteria_to_aliased_table_in_joins —
reproduces the aliased-join shape using the synthetic
``_SoftDeletableChild`` model. Without the fix, this query raises
``OperationalError: no such column: ..._soft_deletable_child.deleted_at``
because the criteria renders as the raw table name. With the fix,
the query runs cleanly.

45 unit tests pass (was 44 + 1 regression test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mikebridge
mikebridge force-pushed the sc-106189-charts-soft-delete branch from 625b2bb to 7f017e0 Compare May 20, 2026 22:39
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 21, 2026
Bayer's canonical pattern excludes ``is_relationship_load`` events on
the assumption that ``with_loader_criteria(propagate_to_loaders=True)``
(the default) carries criteria from the parent statement to the
relationship loaders.

That works for the simple case where the parent statement references
the target class. It does NOT reliably work when the parent statement
references a *different* class and the criteria targets the
relationship's target only. The Superset case that surfaces this:

* A ``Dashboard`` SELECT fires. The listener attaches
  ``with_loader_criteria(Slice, lambda, propagate_to_loaders=True)``.
  ``Slice`` never appears in the Dashboard statement.
* ``dashboard.slices`` lazy-loads via the many-to-many through
  ``dashboard_slices``. The criteria targeting ``Slice`` does not
  reach this lazy load.
* The relationship load returns soft-deleted charts the listener
  was supposed to exclude.

Surfaced as the failing
``test_restore_chart_reattaches_to_dashboards`` integration test on
the charts-rollout PR (apache#40129): the soft-deleted chart was still in
``dashboard.slices``.

Fix: drop the ``is_relationship_load`` exclusion. The listener now
fires on relationship loads too and re-attaches the criteria
directly. The resulting WHERE clause has
``deleted_at IS NULL`` once (when propagation didn't work) or twice
(when it did) — both are idempotent and correct. Bayer's concern
was about "redundant clauses," not correctness, and a tiny SQL
redundancy is the right trade for closing a real visibility leak.

``is_column_load`` exclusion is kept — those events fire for
attribute refreshes on already-loaded objects, not entity loads,
and attaching loader criteria there would be both pointless and
potentially harmful.

The predicate is renamed
``_is_primary_user_select`` → ``_should_attach_soft_delete_criteria``
to reflect the broader scope; docstring updated to explain the
deliberate inclusion of relationship loads and the
redundancy-vs-correctness trade-off.

899 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mikebridge
mikebridge force-pushed the sc-106189-charts-soft-delete branch from 7f017e0 to c5832d0 Compare May 21, 2026 15:44
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 25, 2026
Eight items from the 2026-05-20 review on the infrastructure PR:

* **H1 — subclass walk cached via ``__init_subclass__``.** The previous
  ``_all_soft_delete_subclasses()`` walked ``SoftDeleteMixin.__subclasses__()``
  on every primary ORM SELECT. With one adopter today the cost is
  negligible, but the listener fires on every query in the app and the
  walk grows linearly with each adopted entity. ``SoftDeleteMixin`` now
  maintains a sorted ``_registered_subclasses`` list updated by
  ``__init_subclass__`` at class-definition time; the listener reads
  the cached list with no walk. Sort happens once per registration
  (rare event) rather than per query.

* **M3 — ``SoftDeleteMixin`` import in ``BaseDAO.delete`` moved to
  top.** ``SKIP_VISIBILITY_FILTER_CLASSES`` from the same module was
  already top-level; ``SoftDeleteMixin`` had no real reason to be
  inline. Cleaner.

* **M4 — pylint-disable in ``raise_for_ownership`` documented.** A
  top-level import here actually does cause a circular
  (security ↔ models.core ↔ superset's lazy
  ``feature_flag_manager``). Kept the inline import but added a
  comment naming the cycle so future maintainers don't try to "fix"
  it and re-trip the issue.

* **M5 — ``BaseDeletedStateFilter.model`` typed as
  ``ClassVar[type[SoftDeleteMixin]]``.** Previously ``Any``, which
  meant a subclass accidentally binding ``model`` to a
  non-soft-deletable entity would crash at runtime on
  ``.deleted_at`` rather than failing mypy.

* **M6 — listener placement documented.** ``setup_soft_delete_listener``
  is called outside the ``with app_context()`` block because the
  ``do_orm_execute`` hook attaches to the ``Session`` class
  (process-wide), not to a Session instance, so it doesn't need
  Flask app context. Added an explaining comment in
  ``init_app_in_ctx``'s caller; ``setup_db()`` already ran earlier
  so the Session import is initialised.

* **M7 — unit tests for ``BaseDeletedStateFilter`` +
  ``SoftDeleteApiMixin``.** New
  ``tests/unit_tests/views/test_soft_delete_filter.py`` (10 tests):
  filter no-op / include / only / case-insensitivity / unknown value;
  mixin no-op-without-flag / inject-with-flag / consume-flag-on-call;
  ``_serialize_deleted_at`` None / datetime handling.

* **Nit — explicit ``orig_resource is None`` guard in
  ``raise_for_ownership``.** Previously fell through to
  ``hasattr(None, "owners")`` returning ``False`` → ``owners = []`` →
  generic ownership error. Now raises the proper "resource removed
  before ownership could be verified" error so the cause is clear in
  the rare race-with-hard-delete case.

* **Nit — unit tests for ``find_existing_for_import`` /
  ``clear_soft_deleted_for_import``.** New
  ``tests/unit_tests/commands/importers/v1/test_find_existing_for_import.py``
  (5 tests): no-match-returns-None, live-row-as-is,
  soft-deleted-row-as-is (pure lookup, no side effect),
  ``clear`` hard-deletes via ORM (listener-firing path), and the
  composed find-then-clear contract that's the documented caller
  sequence.

Deferred with explanation in the PR-comment reply (not in this
commit):

* **H2 — non-restorable / GDPR hook**: V2 concern. Designing the
  interface without a concrete use case risks getting it wrong;
  follow-up ticket if compliance work is ever scoped.

* **M8 — ``deleted_at`` schema mutation**: documented in the entity-
  rollout PR bodies (apache#40128/apache#40129/apache#40130) rather than blocking
  infrastructure.

40 unit tests pass (was 26; added 14 covering filter, mixin, importer
helpers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 25, 2026
The listener's loader_criteria was passing a concrete SQL expression
(``cls.where_not_deleted()``) which rendered as the raw table name
(``slices.deleted_at``) regardless of how the table was aliased in
the statement. When the same soft-deletable model appeared under an
alias in a JOIN — e.g., FAB's outer/inner reconstruction or
``report_schedule.chart`` aliasing ``slices AS chart`` — the JOIN's
ON clause produced ``Unknown column 'slices.deleted_at' in 'on
clause'`` and broke the entire query.

Surfaced as CI failures on the entity-rollout PRs (apache#40128 / apache#40129 /
apache#40130) — specifically pre-existing report-schedule tests that join
to ``slices AS chart`` started failing once ``Slice`` inherited
``SoftDeleteMixin``. The infrastructure PR itself doesn't exercise
this because no entity has the mixin yet.

Fix: switch the criteria to a lambda (the form Bayer's canonical
pattern uses). SQLAlchemy invokes the lambda per occurrence and
adapts the column reference to the alias at each site. The lambda's
body is trivial (``c.deleted_at.is_(None)``) so the
``DeferredLambdaElement`` parser handles it without issue — we
originally moved AWAY from a lambda because of an
``if cls in bypass_classes`` conditional that DeferredLambdaElement
mis-parsed as a SQL ``IN`` operator, but the conditional now lives
outside the lambda (in the per-class iteration), so the lambda body
itself is clean.

Test added:
test_listener_adapts_criteria_to_aliased_table_in_joins —
reproduces the aliased-join shape using the synthetic
``_SoftDeletableChild`` model. Without the fix, this query raises
``OperationalError: no such column: ..._soft_deletable_child.deleted_at``
because the criteria renders as the raw table name. With the fix,
the query runs cleanly.

45 unit tests pass (was 44 + 1 regression test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 25, 2026
Bayer's canonical pattern excludes ``is_relationship_load`` events on
the assumption that ``with_loader_criteria(propagate_to_loaders=True)``
(the default) carries criteria from the parent statement to the
relationship loaders.

That works for the simple case where the parent statement references
the target class. It does NOT reliably work when the parent statement
references a *different* class and the criteria targets the
relationship's target only. The Superset case that surfaces this:

* A ``Dashboard`` SELECT fires. The listener attaches
  ``with_loader_criteria(Slice, lambda, propagate_to_loaders=True)``.
  ``Slice`` never appears in the Dashboard statement.
* ``dashboard.slices`` lazy-loads via the many-to-many through
  ``dashboard_slices``. The criteria targeting ``Slice`` does not
  reach this lazy load.
* The relationship load returns soft-deleted charts the listener
  was supposed to exclude.

Surfaced as the failing
``test_restore_chart_reattaches_to_dashboards`` integration test on
the charts-rollout PR (apache#40129): the soft-deleted chart was still in
``dashboard.slices``.

Fix: drop the ``is_relationship_load`` exclusion. The listener now
fires on relationship loads too and re-attaches the criteria
directly. The resulting WHERE clause has
``deleted_at IS NULL`` once (when propagation didn't work) or twice
(when it did) — both are idempotent and correct. Bayer's concern
was about "redundant clauses," not correctness, and a tiny SQL
redundancy is the right trade for closing a real visibility leak.

``is_column_load`` exclusion is kept — those events fire for
attribute refreshes on already-loaded objects, not entity loads,
and attaching loader criteria there would be both pointless and
potentially harmful.

The predicate is renamed
``_is_primary_user_select`` → ``_should_attach_soft_delete_criteria``
to reflect the broader scope; docstring updated to explain the
deliberate inclusion of relationship loads and the
redundancy-vs-correctness trade-off.

899 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mikebridge
mikebridge force-pushed the sc-106189-charts-soft-delete branch 3 times, most recently from 3721e6f to 27aa3e3 Compare May 28, 2026 19:59
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 28, 2026
Eight items from the 2026-05-20 review on the infrastructure PR:

* **H1 — subclass walk cached via ``__init_subclass__``.** The previous
  ``_all_soft_delete_subclasses()`` walked ``SoftDeleteMixin.__subclasses__()``
  on every primary ORM SELECT. With one adopter today the cost is
  negligible, but the listener fires on every query in the app and the
  walk grows linearly with each adopted entity. ``SoftDeleteMixin`` now
  maintains a sorted ``_registered_subclasses`` list updated by
  ``__init_subclass__`` at class-definition time; the listener reads
  the cached list with no walk. Sort happens once per registration
  (rare event) rather than per query.

* **M3 — ``SoftDeleteMixin`` import in ``BaseDAO.delete`` moved to
  top.** ``SKIP_VISIBILITY_FILTER_CLASSES`` from the same module was
  already top-level; ``SoftDeleteMixin`` had no real reason to be
  inline. Cleaner.

* **M4 — pylint-disable in ``raise_for_ownership`` documented.** A
  top-level import here actually does cause a circular
  (security ↔ models.core ↔ superset's lazy
  ``feature_flag_manager``). Kept the inline import but added a
  comment naming the cycle so future maintainers don't try to "fix"
  it and re-trip the issue.

* **M5 — ``BaseDeletedStateFilter.model`` typed as
  ``ClassVar[type[SoftDeleteMixin]]``.** Previously ``Any``, which
  meant a subclass accidentally binding ``model`` to a
  non-soft-deletable entity would crash at runtime on
  ``.deleted_at`` rather than failing mypy.

* **M6 — listener placement documented.** ``setup_soft_delete_listener``
  is called outside the ``with app_context()`` block because the
  ``do_orm_execute`` hook attaches to the ``Session`` class
  (process-wide), not to a Session instance, so it doesn't need
  Flask app context. Added an explaining comment in
  ``init_app_in_ctx``'s caller; ``setup_db()`` already ran earlier
  so the Session import is initialised.

* **M7 — unit tests for ``BaseDeletedStateFilter`` +
  ``SoftDeleteApiMixin``.** New
  ``tests/unit_tests/views/test_soft_delete_filter.py`` (10 tests):
  filter no-op / include / only / case-insensitivity / unknown value;
  mixin no-op-without-flag / inject-with-flag / consume-flag-on-call;
  ``_serialize_deleted_at`` None / datetime handling.

* **Nit — explicit ``orig_resource is None`` guard in
  ``raise_for_ownership``.** Previously fell through to
  ``hasattr(None, "owners")`` returning ``False`` → ``owners = []`` →
  generic ownership error. Now raises the proper "resource removed
  before ownership could be verified" error so the cause is clear in
  the rare race-with-hard-delete case.

* **Nit — unit tests for ``find_existing_for_import`` /
  ``clear_soft_deleted_for_import``.** New
  ``tests/unit_tests/commands/importers/v1/test_find_existing_for_import.py``
  (5 tests): no-match-returns-None, live-row-as-is,
  soft-deleted-row-as-is (pure lookup, no side effect),
  ``clear`` hard-deletes via ORM (listener-firing path), and the
  composed find-then-clear contract that's the documented caller
  sequence.

Deferred with explanation in the PR-comment reply (not in this
commit):

* **H2 — non-restorable / GDPR hook**: V2 concern. Designing the
  interface without a concrete use case risks getting it wrong;
  follow-up ticket if compliance work is ever scoped.

* **M8 — ``deleted_at`` schema mutation**: documented in the entity-
  rollout PR bodies (apache#40128/apache#40129/apache#40130) rather than blocking
  infrastructure.

40 unit tests pass (was 26; added 14 covering filter, mixin, importer
helpers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 28, 2026
The listener's loader_criteria was passing a concrete SQL expression
(``cls.where_not_deleted()``) which rendered as the raw table name
(``slices.deleted_at``) regardless of how the table was aliased in
the statement. When the same soft-deletable model appeared under an
alias in a JOIN — e.g., FAB's outer/inner reconstruction or
``report_schedule.chart`` aliasing ``slices AS chart`` — the JOIN's
ON clause produced ``Unknown column 'slices.deleted_at' in 'on
clause'`` and broke the entire query.

Surfaced as CI failures on the entity-rollout PRs (apache#40128 / apache#40129 /
apache#40130) — specifically pre-existing report-schedule tests that join
to ``slices AS chart`` started failing once ``Slice`` inherited
``SoftDeleteMixin``. The infrastructure PR itself doesn't exercise
this because no entity has the mixin yet.

Fix: switch the criteria to a lambda (the form Bayer's canonical
pattern uses). SQLAlchemy invokes the lambda per occurrence and
adapts the column reference to the alias at each site. The lambda's
body is trivial (``c.deleted_at.is_(None)``) so the
``DeferredLambdaElement`` parser handles it without issue — we
originally moved AWAY from a lambda because of an
``if cls in bypass_classes`` conditional that DeferredLambdaElement
mis-parsed as a SQL ``IN`` operator, but the conditional now lives
outside the lambda (in the per-class iteration), so the lambda body
itself is clean.

Test added:
test_listener_adapts_criteria_to_aliased_table_in_joins —
reproduces the aliased-join shape using the synthetic
``_SoftDeletableChild`` model. Without the fix, this query raises
``OperationalError: no such column: ..._soft_deletable_child.deleted_at``
because the criteria renders as the raw table name. With the fix,
the query runs cleanly.

45 unit tests pass (was 44 + 1 regression test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 28, 2026
Bayer's canonical pattern excludes ``is_relationship_load`` events on
the assumption that ``with_loader_criteria(propagate_to_loaders=True)``
(the default) carries criteria from the parent statement to the
relationship loaders.

That works for the simple case where the parent statement references
the target class. It does NOT reliably work when the parent statement
references a *different* class and the criteria targets the
relationship's target only. The Superset case that surfaces this:

* A ``Dashboard`` SELECT fires. The listener attaches
  ``with_loader_criteria(Slice, lambda, propagate_to_loaders=True)``.
  ``Slice`` never appears in the Dashboard statement.
* ``dashboard.slices`` lazy-loads via the many-to-many through
  ``dashboard_slices``. The criteria targeting ``Slice`` does not
  reach this lazy load.
* The relationship load returns soft-deleted charts the listener
  was supposed to exclude.

Surfaced as the failing
``test_restore_chart_reattaches_to_dashboards`` integration test on
the charts-rollout PR (apache#40129): the soft-deleted chart was still in
``dashboard.slices``.

Fix: drop the ``is_relationship_load`` exclusion. The listener now
fires on relationship loads too and re-attaches the criteria
directly. The resulting WHERE clause has
``deleted_at IS NULL`` once (when propagation didn't work) or twice
(when it did) — both are idempotent and correct. Bayer's concern
was about "redundant clauses," not correctness, and a tiny SQL
redundancy is the right trade for closing a real visibility leak.

``is_column_load`` exclusion is kept — those events fire for
attribute refreshes on already-loaded objects, not entity loads,
and attaching loader criteria there would be both pointless and
potentially harmful.

The predicate is renamed
``_is_primary_user_select`` → ``_should_attach_soft_delete_criteria``
to reflect the broader scope; docstring updated to explain the
deliberate inclusion of relationship loads and the
redundancy-vs-correctness trade-off.

899 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mikebridge
mikebridge force-pushed the sc-106189-charts-soft-delete branch from 27aa3e3 to 6ea8518 Compare May 28, 2026 20:08
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 28, 2026
…orter text, migration warning

Three doc-only corrections to the charts soft-delete entry:

1. Rison filter values were wrong: the entry advertised
   chart_deleted_state=(deleted|active), but the implementation
   (BaseDeletedStateFilter.apply in superset/views/filters.py:185-189)
   only accepts include / only — other values silently no-op.

2. Importer description was stale (described the pre-Option-C raise
   behavior). Rewrote to reflect the implicit restore-and-update Option
   C now does: owner gets the chart back in place, non-owners and
   callers without can_write get ImportFailedError. Reflects the
   permission matrix in the in-tree docstring.

3. Added a migration-safety note following the precedent set by PR
   apache#27392 ("Adds an index... may cause downtime on large deployments")
   and PR apache#27119. Index creation runs inline (no CONCURRENTLY, per
   Superset convention) and briefly blocks reads on the slices table
   during the build on Postgres; MySQL InnoDB builds it online.

Sourced from the SQLAlchemy review on PR apache#40129.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mikebridge pushed a commit to mikebridge/superset that referenced this pull request May 28, 2026
…test

Two SQLAlchemy-review follow-ups:

1. **Explicit flush on restore-and-update path.** The implicit-restore
   flow in import_chart relied on autoflush to push the deleted_at =
   NULL update before Slice.import_from_dict's internal query-by-uuid
   lookup ran. Without it, the listener would have filtered the row
   out of the lookup, import_from_dict would have taken the "new
   object" branch, and the insert would have hit the uuid unique
   constraint at flush time. Adding db.session.flush() after clearing
   deleted_at makes the contract independent of autoflush — robust
   against any caller that disables autoflush on the session, and
   easier for the next reader to reason about. The inline comment is
   rewritten to describe the actual mechanism (the previous wording
   incorrectly claimed config["id"] = existing.id "routes
   import_from_dict through UPDATE", but ImportExportMixin strips
   non-export_fields keys from config — so config["id"] is in fact
   defensive and the real bind happens via the uuid uniqueness
   lookup inside import_from_dict).

2. **Non-admin owner restore integration test.** The existing
   test_restore_chart_reattaches_to_dashboards uses ADMIN_USERNAME,
   so the FAB security wiring on the owner path is only exercised
   via unit-test mocks. Added test_restore_chart_by_non_admin_owner
   to log in as ALPHA_USERNAME, soft-delete and then restore an
   alpha-owned chart, and assert the row's deleted_at is cleared.
   Catches regressions that would break the can_write + ownership
   check on the non-admin path.

Both items raised by the SQLAlchemy lens review on PR apache#40129.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mike Bridge and others added 23 commits July 6, 2026 09:26
… all .po

`pybabel update` fuzzy-matches the new exception string against the existing
similar "Chart could not be created."/"updated." entries in every catalog,
which trips the translation-regression CI check (each language ticks up by
one fuzzy entry).

Pre-seeding an empty `msgstr ""` entry for the new msgid keeps pybabel from
inventing a fuzzy match — when it sees the entry already exists it leaves it
alone. Translators can fill in the real string later through the normal
translation flow.

Affects all 26 active catalogs.
…sent

Adds first test coverage for migration ``7c4a8d09ca37_add_deleted_at_to_slices``:

* End-to-end exercise of ``upgrade()`` and ``downgrade()`` against an
  in-memory SQLite engine with a real Alembic ``Operations`` context,
  not just the underlying helper functions.
* Downgrade-with-soft-deleted-rows-present is pinned: rows that had
  ``deleted_at`` set before downgrade survive the migration and become
  indistinguishable from live rows. Matches the "Rollback note" in
  ``UPDATING.md``; makes the contract regression-proof.
* Idempotency on both directions is covered.

The test uses a minimal pre-seeded ``slices`` stub (id + slice_name)
since the migration only touches ``deleted_at`` and its index.
Superset is on SQLAlchemy 1.4; ``Connection`` doesn't have a ``commit()``
method until 2.0. The implicit transaction over the single ``with
engine.connect() as conn:`` block keeps reads-after-writes visible
within the same connection.
The command does no logging; remove the dead 'import logging' and
'logger = logging.getLogger(__name__)' (codeant), matching the datasets
restore command which already had this cleaned up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Extract the repeated 'find chart by uuid -> set deleted_at -> flush' block
  in the soft-delete import tests into a _soft_delete_existing_chart helper
  (bito) and make its timestamp timezone-aware.
- Add 'Chart could not be restored.' to messages.pot (the .po placeholders
  were already seeded in all languages; the template was missing it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An active chart re-imported with overwrite=True but no can_write wrongly
entered the restore-without-permission branch and raised the deleted-chart
restore error, instead of falling through to returning the existing row
(the pre-soft-delete overwrite-without-permission behaviour). Gate Case B
on is_soft_deleted so only genuine restores are blocked. Add regression
test for the active overwrite-without-can_write path.

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

Two pre-merge fixes surfaced by reviewing charts against current master:

- ChartDeletedStateFilter now restricts soft-deleted rows to the restore
  audience (owners/admins), mirroring DashboardDeletedStateFilter and
  RestoreChartCommand's raise_for_ownership — a read-access non-owner can no
  longer enumerate soft-deleted charts they could never restore. Live rows keep
  normal ChartFilter visibility. Adds integration coverage (owner sees own
  deleted; gamma with datasource access does not).

- Re-point the add_deleted_at_to_slices migration's down_revision from
  33d7e0e21daa to 31dae2559c05 (master's current head) so it no longer creates
  an alembic multi-head when merged onto / rebased atop current master. Will be
  re-pointed again onto the dashboards migration during the merge sequence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Formatting-only; the file predates this pass and the pinned ruff-format
flagged it in CI. No logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror of the dashboards split: import_chart no longer fuses overwrite and
restore behind needs_mutation. A soft-deleted match is a RESTORE (own can_write
gate + ownership check + in-place deleted_at clear); an alive match is an
OVERWRITE (return existing unless overwrite+can_write, then ownership check).
Behaviour unchanged; importer matrix tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parallel to the dashboards/datasets restore-failure tests: assert that an
error raised inside RestoreChartCommand.run surfaces as a clean 422 via
the ChartRestoreFailedError handler rather than an unhandled 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- importer: use existing.restore() instead of writing deleted_at directly
  (speak the domain operation; same SQL, future-proof if restore() grows)
- filters: make the deleted-state docstring merge-order-proof instead of
  naming sibling filter classes that may not exist in the tree yet

Mirrors the datasets commit ebda9ad.

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

Multi-lens review-panel findings (sqlalchemy + committer + DDD lenses):

- DashboardDAO.set_dash_metadata: bypass the soft-delete visibility
  filter when resolving incoming chart ids. The assignment rebuilds
  dashboard.slices wholesale, so with Slice now carrying SoftDeleteMixin
  a save during a member chart's soft-deleted window silently severed
  the dashboard_slices junction (breaking the documented restore-
  reattach contract) and nulled the chart's uuid in position_json. New
  unit test pins it (verified red without the fix, green with).

- importer: include the chart name and uuid in both soft-deleted-match
  error messages, so a failed multi-chart dashboard-bundle import names
  the culprit.

- api: DELETE/bulk-delete OpenAPI docstrings now describe soft-delete
  semantics and the restore path.

- UPDATING.md: note bulk delete is also soft; note the deleted-state
  filter is owner-scoped for non-admins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…master head

Master advanced past 31dae2559c05 (now 78a40c08b4be); point the
soft-delete migration at the current head so it forms a single linear
head against master. Branch/merge sequence unchanged.
Per @aminghadersohi review: the five `from superset...` imports were
inside the test body without a circular-import justification. They have
no top-level side effects (db is a proxy; the models register on
metadata at import, and create_all runs after import either way), so
they move cleanly to the module-level import block.

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

Was chained off 78a40c08b4be (old head); master advanced via apache#39859, so
re-point onto 2bee73611e32 to keep a single migration head.
Soft-delete is gated behind the temporary SOFT_DELETE rollout flag (off by
default since apache#41166). Decorate each soft-delete test with
@with_feature_flags(SOFT_DELETE=True) so they exercise the gated behaviour
once rebased onto the gated master.
Review finding: pre-soft-delete, report_schedule.chart_id's FK made a
dangling target chart structurally impossible. With soft delete the only
protection is DeleteChartCommand's validation, which a validate/commit race
or a SOFT_DELETE flag toggle can slip past — and the executor's
`if self._report_schedule.chart:` then silently falls through to the
dashboard branch (chart is a visibility-filtered relationship that loads
None) and fails opaquely.

Add a guard in _get_url — the single choke point every content path
(screenshots, CSV, embedded data, notification) funnels through — raising a
dedicated ReportScheduleTargetChartDeletedError inside the state-machine
envelope, so the ERROR log row and owner notification carry an actionable
message. Also rename the delete-blocked test: find_by_chart_ids matches any
report row (active or paused), not just active ones.
Review finding (verified by reproduction): the bypass covered only the
incoming slice-resolution query. Assigning dashboard.slices makes the
unit of work diff the new collection against the existing one, which it
lazy-loads at that moment THROUGH the visibility listener — so a trashed
member was missing from the baseline, treated as net-new, and INSERTed
into dashboard_slices where its junction row still exists (soft delete
never removes junction rows). Since apache#39859 gave dashboard_slices a
composite primary key, every save of a dashboard containing a
soft-deleted chart raised IntegrityError (500) with SOFT_DELETE on; on
pre-apache#39859 schemas it silently wrote a duplicate junction row.

Wrap both the resolution query and the assignment in a session-scoped
skip_visibility_filter(Slice) so the baseline load sees the trashed
member and the diff is a no-op for it.

The existing unit test masked this: its fixture left dashboard.slices
in-session (flush-only, flag off), so the filtered baseline reload never
happened. Hardened to the production shape — SOFT_DELETE enabled,
collection expired (as with a fresh find_by_id load in the PUT flow),
and a post-call flush so the diff's SQL reaches the composite-PK
junction. The hardened test fails with the exact IntegrityError against
the previous code and passes with the fix.
…andard

Review panel finding (unanimous, 7 of 8 lenses): the charts UPDATING.md
section and the delete/bulk_delete/import OpenAPI docstrings described
soft delete as unconditional, but SOFT_DELETE defaults to False — on a
default deployment DELETE hard-deletes permanently. Port the adopted
dashboards (apache#40128) wording:

- UPDATING.md now leads with the flag gate + default, documents the
  flag-toggle caveat (soft-deleted rows reappear when the flag is turned
  off; restore and the deleted-state filter deliberately stay live), and
  qualifies the behavior paragraph.
- Charts-specific additions: migration-downgrade consequence (dropping
  deleted_at silently activates not-yet-restored trash — reconcile before
  downgrading) and operational notes (reports targeting a soft-deleted
  chart fail with an explicit error; dashboards preserve trashed-member
  junctions across saves).
- The importer paragraph and import_ docstring state the deliberate
  restore-with-update-without-overwrite asymmetry, matching apache#40128.
- delete/bulk_delete docstrings + summaries qualified with the flag.

Also drop the whitespace-only hunk in commands/chart/delete.py flagged by
five review lenses as diff noise.
…filter normalization

Two cross-entity consistency findings from the review panel:

1. The chart-target guard in _get_url had no dashboard twin: a report
   whose visibility-filtered dashboard relationship loads None while
   dashboard_id is set would fall into ``dashboard.id`` on None — an
   opaque AttributeError instead of an actionable error. Add
   ReportScheduleTargetDashboardDeletedError and guard both entry points
   (_get_url, and get_dashboard_urls which the async command enters
   directly for the permalink pre-commit). Dashboard soft delete lands in
   the sibling rollout, so the guard is inert until then — but the report
   error vocabulary stays parallel across entities from day one. Both
   guards are pinned by tests that fail without them.

2. Hoist the deleted-state token normalization into
   BaseDeletedStateFilter._normalize (single source of truth, identical
   text to the sibling dashboards PR so the merge is clean) and have
   ChartDeletedStateFilter reuse it instead of re-deriving the expression
   the helper exists to eliminate. Also reword the filter docstring to be
   timeless (no reference to unlanded rollouts).
… head

apache#40128 (dashboards soft-delete) merged with migration 9e1f3b8c4d2a chaining
off the previous head, so this migration re-points onto it to keep a single
alembic head. Rebased onto post-apache#40128 master (0 conflicts; the shared
BaseDeletedStateFilter._normalize addition merged cleanly by design —
identical text on both branches).
…alogs

Per @rusackas's pre-merge review: the two new messages in
superset/commands/report/exceptions.py (report targets a deleted chart /
dashboard) were _()-wrapped but missing from messages.pot and every
messages.po. Added to all 30 catalogs with empty msgstrs, matching how
the branch carries "Chart could not be restored."; every catalog
validated to parse and contain byte-exact msgids.

Also adds the reviewer-endorsed docstrings to the migration unit tests
(helpers and the two schema tests) and type annotations on the module
constants.
… head

Master's migration head moved to b4a3f2e1d0c9 (apache#40527) after the last
rebase; chaining off it restores the single-head invariant (per
@rusackas's review note). No schema change.
@mikebridge

Copy link
Copy Markdown
Contributor Author

Thanks @rusackas — done in 4d8b9653a3: rebased onto current master and the migration is re-pointed onto b4a3f2e1d0c9 (#40527's migration, the current single head — verified by graph scan; the docstring's Revises: line is updated too). CI is running on the new head.

For future rounds this is now guarded locally: a pre-push hook fails my pushes if the branch's migration graph has more than one head, so a stale down_revision can't reach CI again — though it can't help when master's head moves after the push, as happened here. All the more reason to merge while it's green 🙂

@rusackas rusackas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM — all human review threads are settled (89 of 100 threads resolved; everything from @aminghadersohi, @richardfogaca, and my earlier pass is closed out, and Richard approved). CI is fully green and the whole thing ships dark behind SOFT_DELETE.

I dug into the one substantive bot thread (the error-envelope concern on get_dashboard_urls): technically observant but not blocking. That pre-commit call site raising outside the state machine predates this PR... without the guard, the same site would AttributeError opaquely instead, so the guard strictly improves the failure. The dashboard case can only fire via the validate/commit race anyway, since DeleteDashboardCommand blocks deletion with dependent reports, and the chart guard (this PR's actual subject) lives in _get_url, inside the envelope. The remaining open bot threads are style/type-hint noise and empty msgstr nags, and new strings landing untranslated is our normal flow... the i18n backfill sweeps pick those up.

@bito-code-review

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped – PR Already Merged

Bito scheduled an automatic review for this pull request, but the review was skipped because this PR was merged before the review could be run.
No action is needed if you didn't intend to review it. To get a review, you can type /review in a comment and save it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API change:backend Requires changing the backend i18n:brazilian i18n:chinese Translation related to Chinese language i18n:czech i18n:dutch i18n:french Translation related to French language i18n:italian Translation related to Italian language i18n:japanese Translation related to Japanese language i18n:korean Translation related to Korean language i18n:latvian i18n:persian i18n:portuguese i18n:russian Translation related to Russian language i18n:slovak i18n:spanish Translation related to Spanish language i18n:traditional-chinese i18n:ukrainian i18n Namespace | Anything related to localization risk:db-migration PRs that require a DB migration size/XXL viz:charts Namespace | Anything related to viz types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants