Skip to content

feat(domain)!: reject declared properties under a restricted scope - #424

Open
Tomoscorbin wants to merge 4 commits into
mainfrom
refactor/reject-unmanaged-declared-properties
Open

Tomoscorbin wants to merge 4 commits into
mainfrom
refactor/reject-unmanaged-declared-properties

Conversation

@Tomoscorbin

Copy link
Copy Markdown
Owner

Stacked on #423.

What

A declaration whose scope does not manage properties can no longer declare them. DesiredTable rejects the combination at construction — for DeltaTable users that means import time, with the keys named:

ValueError: Properties are declared (['delta.enableChangeDataFeed']), but this
declaration does not manage properties. Remove them, or widen the scope to
manage properties.

The invariant sits in DesiredTable.__post_init__ next to the matching renamed_from rule, so it also covers custom DesiredTableSource implementations, not just the api path.

Nothing changes on the observed side: a restricted scope still makes no property assertion, so pipeline-owned properties (or values written by a previous fully managed sync) are never compared and never read as drift. The differ's ignore gate stays.

Why

Follow-up to #423's review discussion. DeltaTable(scope="metadata", properties={...}) was accepted, value-validated, and then silently inert — neither deployed nor checked, documented only in the metadata how-to. That representable-but-meaningless state is why the scope model needed a "carried but ignored" story at all. Rejecting it at declaration time defines the state out of existence: everything a declaration states now asserts something.

Breaking change: a restricted-scope declaration that also declares properties now fails loudly at import instead of passing silently. The fix is to delete the inert properties block (no behaviour change) or widen the scope to "full" (if deployment was the actual intent).

One test relied on the old state: test_a_restricted_scope_mirrors_cdf_reserved_column_names carried the CDF property under a restricted scope to show the reserved-column rule stays quiet. The reserved-name rule only fires when the declaration enables CDF, which a restricted scope now cannot do, so the test keeps pinning the mirrored _change_type column without the property. The differ test for carried-but-ignored declared properties is deleted — that state is no longer representable — while its observed-side companion remains.

Gate: 1403 passed, ruff check/format clean, mypy clean (178 files), all 7 import contracts kept.

🤖 Generated with Claude Code

"Unmanaged" meant different things depending on where it was sensed:
the differ skipped unmanaged properties entirely, while validation
compared every other unmanaged aspect and rejected its drift. The
property exception lived in the differ's docstring, and generate
depended on it at a distance.

TableScope now answers a per-aspect ReconciliationMode — MANAGE,
REQUIRE_MATCH, or IGNORE — derived from the existing minimum-scope
ladder plus one fallback table that states what each unmanaged aspect
does instead. The differ's rule becomes generic (an IGNORE aspect is
not compared), UnmanagedAspectDrift becomes the literal REQUIRE_MATCH
interpreter, and manages() survives as "mode is MANAGE" for the sites
that only need the boolean.

No behaviour changes. The DeltaTable scope docstring now states the
property exception it previously omitted: a restricted scope carries
declared properties without deploying or checking them.
The differ's property gate read

    scope.reconciles(TableAspect.PROPERTIES) is ReconciliationMode.IGNORE

which asks the reader to know the whole mode vocabulary just to parse
a guard. TableScope now names the projection, next to manages():
ignores(aspect) — the scope carries the aspect without comparing it.
The differ reads "if the scope ignores properties, nothing to
compare", and the docstrings and architecture doc say "ignore" in
plain English instead of quoting the enum member.

reconciles() stays the single source; both predicates derive from it.
No behaviour changes.
…dicates

reconciles() returned a mode, so every call site had to know the enum
vocabulary to read a guard — unlike manages() and is_within(), where
the verb is the meaning. Validation, the last mode consumer, really
asks "must this aspect match?", so TableScope now answers all three
questions by name: manages(), requires_match(), and ignores().

With no caller left for the raw mode, ReconciliationMode and the
11-row _UNMANAGED_MODE_BY_ASPECT table collapse into the predicates:
ignores() is "properties, when unmanaged"; requires_match() is every
other unmanaged aspect. A new aspect still fails loudly — every
predicate goes through _MINIMUM_SCOPE_BY_ASPECT, which raises KeyError
for an unmapped aspect — and require-match is the safe meaning for an
aspect added without a decision, since drift is refused, never
converged.

No behaviour changes.
A declaration with scope="metadata" (or narrower) could carry
properties that meant nothing: they were value-validated, then never
deployed and never compared. The silence was documented in the
metadata how-to, but nothing told the user their properties block was
inert.

DesiredTable now rejects declared properties when the scope does not
manage them, next to the matching renamed_from rule, so the invariant
also covers custom DesiredTableSource implementations. The error
arrives at declaration time — import, in user modules — naming the
keys to remove.

The differ's ignore gate stays: the live table's properties (pipeline-
owned, or written by a previous fully managed sync) still assert
nothing under a restricted scope and are never read as drift. What
changes is only the declared side: everything a declaration states now
asserts something.

BREAKING CHANGE: a restricted-scope declaration that also declares
properties now fails at construction instead of passing silently.
Remove the properties block, or widen the scope to "full".
Base automatically changed from refactor/reconciliation-modes to main September 2, 2026 15:59

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant