From d0520f67669c0bebc1fec23ed356cfb07fe03e35 Mon Sep 17 00:00:00 2001 From: Mike Bridge Date: Wed, 20 May 2026 16:13:03 -0600 Subject: [PATCH] fix(versioning): skip non-clean parents in force-parent-dirty hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The force-parent-dirty listener was calling attributes.flag_modified on every parent reachable from a dirty child — including parents themselves in session.new (e.g. brand-new SqlaTable + brand-new TableColumns from POST /api/v1/dataset/). flag_modified rejects unloaded attributes, and a session.new SqlaTable's uuid (default=uuid4 fires at flush time) is unloaded until then. CI caught this with InvalidRequestError cascading into 422s across dataset creation / upload / Playwright dataset specs. The hook is only needed for the persistent-and-clean case (child edited, parent's own scalars untouched, dropdown otherwise empty). Anything in session.new will flush anyway; anything in session.dirty is already flagged; session.deleted shouldn't be touched. Short- circuit before the flag_modified call. Unblocks test-sqlite, test-mysql, test-postgres (previous), and playwright dataset specs. --- superset/versioning/baseline.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/superset/versioning/baseline.py b/superset/versioning/baseline.py index 3e8be36afab..9caa0d6f34e 100644 --- a/superset/versioning/baseline.py +++ b/superset/versioning/baseline.py @@ -131,6 +131,15 @@ def _force_parent_dirty_on_child_change(session: Session) -> None: parent = getattr(obj, parent_attr, None) if parent is None or type(parent) is not parent_cls: # noqa: E721 continue + # Only flag *persistent + clean* parents. Anything else is + # either already going to surface in the flush (``session.new`` + # → INSERT, ``session.dirty`` → already flagged) or shouldn't be + # flagged (``session.deleted`` → being removed). Also avoids + # InvalidRequestError from ``attributes.flag_modified`` when a + # brand-new parent's ``uuid`` default (``default=uuid4``) hasn't + # fired yet so the attribute is unloaded in instance state. + if parent in session.new or parent in session.dirty or parent in session.deleted: + continue col_keys = [prop.key for prop in versioned_column_properties(parent)] if not col_keys: continue