Skip to content

[DMS-1129] Design SQL Server foreign-key pruning for identity-update cascades#1088

Open
samuellugo-ship-it wants to merge 14 commits into
mainfrom
DMS-1129
Open

[DMS-1129] Design SQL Server foreign-key pruning for identity-update cascades#1088
samuellugo-ship-it wants to merge 14 commits into
mainfrom
DMS-1129

Conversation

@samuellugo-ship-it

@samuellugo-ship-it samuellugo-ship-it commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Spike DMS-1129Design foreign key pruning. Design/analysis only; no production code. This PR is documentation: it replaces the SQL Server "every reference composite FK uses ON UPDATE NO ACTION + MssqlIdentityPropagationTrigger" strategy with ODS-style foreign-key pruning, and fixes the flaw ODS has (silently pruning when no edge is safe to prune).

Design note: reference/design/backend-redesign/design-docs/mssql-cascading.md (includes diagrams of the objects, propagation direction, and each pruning/fail-fast decision point).

The approach

  • The cascade graph is analyzed in propagation direction — referenced/parent table → referrer/child table. SQL Server error 1785 forbids a table from appearing more than once in a single UPDATE/DELETE's cascade action tree (the legal graph is a multitree): the conflict is a diamond (a receiver reached by two distinct paths from one origin) or a cycle — not raw cascade in-degree. Direct in-degree > 1 is only a candidate signal: independent parents into one receiver are legal and are never pruned (pruning an independent live edge would only reintroduce error 547). Detection is per-origin duplicate reachability (shared cascade ancestry), not per referenced parent.
  • Cascade-eligibility and liveness use transitive identity mutability (IsAbstract || TransitivelyAllowIdentityUpdates), matching TransitiveIdentityMutabilityPass / ReferenceConstraintPass.
  • Every emitted SQL Server reference FK keeps the full composite key (identity columns restored — this closes the DMS-1002 referential-integrity weakness). Per-edge outcome (MssqlPropagationMode):
    • NativeCascade — an eligible edge (an independent edge, the surviving path of a diamond, or the sole edge into a receiver); ON UPDATE CASCADE.
    • NoPropagation — a reconverging edge of a diamond that is pruned but covered by the surviving path for the same origin and canonical columns; ON UPDATE NO ACTION.
    • ImmutableNoAction — a reference to a genuinely immutable target (not in the cascade graph); ON UPDATE NO ACTION. (So NO ACTION no longer implies "pruned/covered"; the mode is carried explicitly and is not derivable from the action alone.)
  • There is no TriggerFallback and no DocumentId-only FK shape. The retired MssqlIdentityPropagationTrigger is gone for identity-value propagation; native ON UPDATE CASCADE (which follows FKs on child/_ext binding tables) replaces it.
  • Fail derivation fast (with a diagnostic) in two fundamental conditions: (1) a cascade cycle / SCC (or self-loop); and (2) diamonds that cannot be jointly broken — either a single uncovered diamond (a receiver reached twice from one origin whose reconverging edge cannot be covered — the case ODS prunes silently and incorrectly), or globally infeasible overlapping diamonds (each breakable in isolation, but no global survivor assignment satisfies the retained-NativeCascade invariant because overlapping diamonds share edges). Diagnostics name the origin/root(s), receiver(s), distinct paths, candidate/pruned edges, and coverage columns — plus, for the overlapping case, the shared edge/mode conflict and the invariant clause that failed. No permissive fallback remains, so the MetaEd authoring guard (METAED-1667) is load-bearing and the DMS fail-fast is the backstop.
  • Physical FK pruning, unsafe-graph detection, and fail-fast are SQL Server only — a team-decided engine-specific policy (where the two engines can't behave identically, each does what works for it rather than being forced into parity). PostgreSQL keeps full-composite ON UPDATE CASCADE on every eligible edge and is left as-is: never pruned or failed by DMS (it has no SQL Server 1785 DDL restriction; no claim is made about PostgreSQL runtime behavior for a cascade cycle). Cross-engine safety for authors is handled at authoring time by MetaEd (METAED-1667), which flags a cascade graph SQL Server cannot represent — rather than forcing PostgreSQL derivation to reject graphs it can represent.
  • Canonical FK / *_RefKey column order is identity storage columns first, DocumentId last (matches ReferenceConstraintPass and the change-queries /deletes seek-path rationale; local and target lists are positionally aligned). No column-order code change is required.
  • Derived contract DMS-1258 must add: per-edge MssqlPropagationMode (three values above), coverage/carrier diagnostics for NoPropagation edges only, and hard fail-fast derivation errors. The MssqlFkShape axis is dropped (every FK is full composite).

Empirical validation (SQL Server 2022)

A throwaway container and a real populated ODS database (EdFi_Ods_Populated_Template, all writes rolled back) confirmed the behaviors the design rests on:

  • Msg 1785 — a table reached by multiple cascade paths (or a cascade cycle) rejected at DDL time; pruning one edge into the receiver to NO ACTION makes the graph legal.
  • Msg 547 — a NO ACTION composite FK that includes identity columns blocks a parent identity update, and an AFTER trigger cannot rescue it (the FK check precedes the trigger). This is why pruned edges must be covered, and why a DocumentId-only + trigger fallback cannot preserve value-level RI — this design removes that fallback.
  • Msg 2113 — no INSTEAD OF UPDATE on a table with a cascading FK (forecloses the "reorder children-first" alternative).
  • Kept ON UPDATE CASCADE composite FKs propagate identity changes natively; redundant pruning is safe when a key-unified column is covered by a surviving cascade.
  • Real-data note: the stock Ed-Fi cascade cluster is already acyclic, so pruning/fail-fast matter chiefly for key-unified extensions; a single edfi.Session rename cascaded to hundreds of rows in ~1.2 s (identity-update fan-out is the real operational cost).

Changes

  • New: mssql-cascading.md — consolidated design note with a "Resolved design decisions" section and a Mermaid Diagrams section (object map, propagation-direction diamond, safe covered pruning, unsafe fail-fast, cycle/SCC fail-fast, before/after RI improvement).
  • Revised to point at it and drop the old blanket trigger/DocumentId-first claims: overview, strengths-risks, transactions-and-concurrency, key-unification, data-model, ddl-generation, summary, compiled-mapping-set, update-tracking, flattening-reconstitution, the 01-relational-model/07-index-and-trigger-inventory and 09-identity-concurrency/03-identity-propagation epic docs, and the two presentations. Added "superseded" banners to key-unification-children-problem and key-unification-design-change-proposal (their non-root-referrer concern is solved natively by cascade).

Follow-ups

  • DMS-1258 — implement pruning + fail-fast (blocked by this spike). Its ticket wording was corrected off the old "per referenced table" / TriggerFallback framing to the receiver / propagation-direction / three-mode model.
  • METAED-1667 — disallow allow primary key updates when no safe pruning strategy exists (now load-bearing, since there is no permissive fallback).
  • DMS-1128 — superseded by DMS-1258.

Acceptance criteria

  • Analyze ODS implementation and update the design documents with a DMS approach.
  • Decide MSSQL-only vs both → MSSQL-only for pruning, detection, and fail-fast (engine-specific; PostgreSQL left as-is; MetaEd is the cross-engine authoring guard — rationale in the note).
  • Create a MetaEd ticket to disallow allow primary key updates with no safe pruning → METAED-1667.
  • Create a DMS ticket to implement pruning + fail-fast → DMS-1258.
  • Update DMS-1128 with the new approach → superseded by DMS-1258.

🤖 Generated with Claude Code

@samuellugo-ship-it

Copy link
Copy Markdown
Contributor Author

Rebased onto main (picked up #1086).

@samuellugo-ship-it

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in 9c49e3c: rewrote mssql-cascading.md (propagation-direction graph, cycle/SCC + convergence fail-fast, TriggerFallback removed, transitive mutability, portability-guard PG rationale, canonical FK order, MssqlPropagationMode contract + Resolved design decisions) and swept the stale trigger-only/DocumentId-first wording across the design docs. Also corrected the DMS-1258 ticket wording.

@samuellugo-ship-it

Copy link
Copy Markdown
Contributor Author

Round-2 review findings addressed in a3358f9: added ImmutableNoAction as a third MssqlPropagationMode (immutable NO ACTION FKs no longer conflated with pruned/covered), swept remaining direct→transitive mutability and local-FK column-order wording, replaced stale "SQL Server propagation trigger" mentions, and added a Mermaid diagrams section (objects, propagation direction, safe/unsafe pruning, cycle/SCC, before/after). PR body and DMS-1258 updated to the three-mode contract.

@samuellugo-ship-it

Copy link
Copy Markdown
Contributor Author

Addressed the PASS-WITH-NITS finding: the NativeCascade contract definitions in mssql-cascading.md and compiled-mapping-set.md now explicitly enumerate independent, diamond-survivor, and sole edges (matching the algorithm and outcome table). Docs-only, commit 34db8f6.

@samuellugo-ship-it

Copy link
Copy Markdown
Contributor Author

Addressed the re-review findings in 533d21e3 (docs-only; no code changes).

Two things not visible in the diff:

  • PostgreSQL fail-fast policy (the one you asked me to decide): adopted fail derivation on BOTH dialects for a SQL-Server-unrepresentable graph, as an explicit cross-engine portability/authoring policy — not a PostgreSQL correctness requirement (PG alone would accept the graph). This resolves the contradiction with the old "future decision to also fail on PostgreSQL" hedge, which is removed. If you'd prefer the alternative (PG records a diagnostic, only SQL Server fails), say so and I'll flip it.
  • Jira updated via MCP: METAED-1667 rewritten to per-origin duplicate reachability (dropped "one surviving edge per referenced table"); DMS-1127 retargeted off the removed fallback/nested-trigger model to native-cascade update-tracking validation; DMS-1258 aligned to the both-dialect policy + the new global retained-graph invariant.

Also fixed: column order (identity-first, DocumentId-last), precise "origin" definition, global invariant for overlapping diamonds, pass-ordering/metadata home, softened PG cycle wording, abstract-FK qualifier, expanded NoPropagation/fail-fast diagnostics, historical side-doc subsections marked superseded, and swept the "surviving edge" shorthand to accurate wording repo-wide.

samuellugo-ship-it added a commit that referenced this pull request Jul 8, 2026
…h, align stale PG subsection

Docs-only (no code):

- Overlapping diamonds: replace the "pick a local first-fit survivor per
  diamond, then fail if the global invariant breaks" framing in
  mssql-cascading.md with a real deterministic GLOBAL survivor-selection
  contract — a backtracking search over overlapping diamonds in stable order
  that accepts the first complete assignment whose retained NativeCascade graph
  satisfies the invariant (one mode per FK, acyclic, multitree, every
  NoPropagation edge covered by the FINAL graph), and fails fast only when NO
  global assignment satisfies it. This stops the design from falsely rejecting
  schemas that are representable under a different (backtracked) assignment.
- key-unification.md: rewrite the stale "PostgreSQL (no special mitigation
  required)" subsection that still claimed PostgreSQL "supports cycles or
  multiple cascade paths". Now aligned with mssql-cascading.md: full-composite
  ON UPDATE CASCADE on every eligible edge, never physically pruned, derivation
  still fails on BOTH dialects for cycles/SCCs and uncovered diamonds as a
  cross-engine portability policy (not a PostgreSQL correctness need), and no
  claim about PostgreSQL runtime cycle behavior. Kept the key-unification
  mid-cascade / single-writable-column content.

Also updated (not in diff): PR #1088 body bullet 6 (PG "cascades multiple paths
and cycles natively" -> no-1785-restriction + both-dialect portability
fail-fast, no runtime-cycle claim); Jira DMS-1128 supersession banner rewritten
to drop TriggerFallback/MssqlFkShape/DocumentId-only/one-surviving-edge-per-
referenced-table; DMS-1258 AC + tasks aligned to the deterministic global search.

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

Copy link
Copy Markdown
Contributor Author

Addressed the FAIL findings in 81b2e66a (docs-only; no code).

  • Overlapping-diamond survivor selection (Major): went with direction A. mssql-cascading.md §5 now specifies a real deterministic global search — backtracking over overlapping diamonds in stable order, accepting the first complete assignment whose retained NativeCascade graph satisfies the invariant (one mode/FK, acyclic, multitree, every NoPropagation edge covered by the final graph), and failing fast only when no global assignment exists. This replaces the "local first-fit + post-hoc fail" that could falsely reject representable schemas.
  • Stale PG subsection (Major): key-unification.md "PostgreSQL (no special mitigation required)" rewritten to match mssql-cascading.md — full-composite CASCADE on eligible edges, never pruned, both-dialect fail-fast as a portability policy, no runtime-cycle claim.

Off-diff:

  • PR body bullet 6 corrected (dropped "PostgreSQL cascades multiple paths and cycles natively").
  • Jira: DMS-1128 supersession banner rewritten to drop TriggerFallback/MssqlFkShape/DocumentId-only/"one surviving edge per referenced table"; DMS-1258 AC + tasks aligned to the deterministic global search (incl. an overlapping-diamond backtrack test case).

Verified the remaining cycles or multiple cascade paths hits are quotes of SQL Server's own 1785 error text, not PG claims; mssql-cascading.md and key-unification.md now agree on the PostgreSQL policy.

samuellugo-ship-it added a commit that referenced this pull request Jul 8, 2026
…ocs; column-order stragglers

Docs-only (no code). Propagates the round-7 two-condition / global-search model
into the remaining normative surfaces and fixes two stragglers:

- Derived-model contract (mssql-cascading.md) + compiled-mapping-set.md: the
  fail-fast error contract now covers BOTH flavors of the second condition — a
  single uncovered diamond, AND globally infeasible overlapping diamonds (no
  global assignment satisfies the retained-NativeCascade invariant), with
  diagnostics for the overlapping case: involved diamonds, origins/roots,
  receivers, the shared edge/mode conflict, and the invariant clause that failed.
- key-unification.md normative SQL Server rules: replaced "for each diamond, keep
  deterministic winner" (local first-fit) with covered candidate breaks feeding
  the deterministic GLOBAL survivor selection in mssql-cascading.md §5; fail-fast
  only when no complete assignment satisfies the invariant. Kept the
  key-unification point (covered pruning when paths share unified canonical cols).
- mssql-cascading.md dialect-scope: "PostgreSQL alone would create and run such a
  graph" -> "has no 1785 DDL restriction and would accept it at create time" +
  explicit no-runtime-cycle-claim (no runtime behavior asserted for cycles).
- data-model.md:974,981: column order "DocumentId + storage identity" ->
  identity storage columns first, DocumentId last (aligns with data-model.md:584/593).

PR #1088 body fail-fast bullet updated to the two-condition framing (both flavors).

No rejected behavior reintroduced: verified no TriggerFallback / DocumentId-only /
MssqlFkShape / per-referenced-table / raw-in-degree-pruning outside removal/historical
context; no PG runtime-cycle claims; no local-first-fit normative wording remains.

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

Copy link
Copy Markdown
Contributor Author

Open decision for the team — PostgreSQL fail-fast policy. The recurring PG-wording findings trace back to one unratified policy call: should DMS fail derivation on both dialects for a SQL-Server-unrepresentable graph (current design, a portability policy), or only on SQL Server with PostgreSQL recording a diagnostic? I've written up the context, both options with trade-offs, and a recommendation (Option A) as a decision record on DMS-1258 for team ratification: https://edfi.atlassian.net/browse/DMS-1258?focusedCommentId=94649 — the docs, DMS-1258 ACs, and METAED-1667 will be aligned to whichever the team picks.

samuellugo-ship-it added a commit that referenced this pull request Jul 8, 2026
…agate overlapping-diamond fail flavor

Docs-only (no code). The team ratified the open PostgreSQL question with an
engine-specific policy — do NOT force parity between engines:

- Pruning, unsafe-graph detection, and fail-fast are now scoped to SQL Server
  ONLY (they exist for SQL Server's 1785 DDL restriction). PostgreSQL is left
  as-is: full-composite ON UPDATE CASCADE on every eligible edge, never pruned
  or failed by DMS. Cross-engine safety for authors lives in the MetaEd
  authoring guard (METAED-1667). Replaced the "fail derivation on both dialects
  / cross-engine portability policy" (Option A) framing everywhere it appeared:
  mssql-cascading.md (decision 5, dialect-scope section, step 6, DMS-1258
  alignment, intro), key-unification.md PG subsection, transactions:275.
- Removed the last PG runtime-cycle phrasings ("PostgreSQL alone could run it",
  "create and run such a graph"); PG "accepts the DDL at create time" only, with
  an explicit no-runtime-cycle-claim.
- Propagated the "globally infeasible overlapping diamonds" fail flavor into the
  implementation-facing fail-fast shorthands (data-model, ddl-generation,
  overview, summary, transactions, flattening-reconstitution, key-unification,
  epics 07 + 09) so none hides the overlapping case; MetaEd follow-up +
  METAED-1667 include it too.
- Column-order straggler fixed in key-unification-design-change-proposal.md
  (identity storage columns first, DocumentId last).

Jira/PR (not in diff): METAED-1667 + DMS-1258 descriptions aligned to
engine-specific and the overlapping flavor; DMS-1258 decision comment 94649
marked resolved; DMS-1129 AC-bullet-2 decision recorded as a comment; PR #1088
body updated (engine-specific PG policy).

No rejected behavior reintroduced: verified no TriggerFallback / MssqlFkShape /
DocumentId-only / per-referenced-table / raw-in-degree as live; no both-dialect
overstatement; no PG runtime-cycle claims.

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

Copy link
Copy Markdown
Contributor Author

Latest review findings addressed in af3981cd (docs-only). The recurring open item — the PostgreSQL fail-fast policy — is now resolved by team decision: engine-specific.

  • Pruning, unsafe-graph detection, and fail-fast are SQL Server only. PostgreSQL is left as-is (full-composite ON UPDATE CASCADE, never pruned or failed by DMS). Cross-engine safety for authors lives in the MetaEd authoring guard (METAED-1667) — we do not force PostgreSQL to reject graphs it can represent. Replaced the earlier "fail derivation on both dialects / portability policy" (Option A) wording everywhere, and removed the last PG runtime-cycle phrasings.
  • Overlapping-diamond fail flavor ("globally infeasible overlapping diamonds") propagated into the implementation-facing fail-fast shorthands (data-model, ddl-generation, overview, summary, transactions, flattening-reconstitution, key-unification, epics 07 + 09) and the MetaEd guard, so no summary hides it.
  • Column-order straggler fixed in key-unification-design-change-proposal.md.

Off-diff: METAED-1667 + DMS-1258 aligned to engine-specific + the overlapping flavor; the DMS-1258 decision record (comment 94649) is marked resolved; DMS-1129 AC-bullet-2 decision recorded; this PR body updated.

Verified no regressions: no TriggerFallback / MssqlFkShape / DocumentId-only / per-referenced-table / raw-in-degree as live behavior; no both-dialect overstatement; no PG runtime-cycle claims.

samuellugo-ship-it and others added 14 commits July 8, 2026 16:32
…cascades

Spike deliverable (design + analysis; no production code). Replaces the SQL
Server "all reference composite FKs use ON UPDATE NO ACTION + MssqlIdentity-
PropagationTrigger" strategy with ODS-style foreign-key pruning, plus a
coverage classification and fail-fast for the case ODS mis-prunes:

- keep ON UPDATE CASCADE (full composite, identity columns restored) on one
  surviving live edge per referenced table;
- prune redundant edges to ON UPDATE NO ACTION (full composite when covered by
  a surviving cascade, else DocumentId-only + MssqlIdentityPropagationTrigger);
- fail derivation fast when a table has >= 2 uncovered live cascade paths.

Physical FK-shape pruning is SQL Server only; PostgreSQL keeps full-composite
ON UPDATE CASCADE. The unsafe-graph detection is dialect-agnostic.

Add consolidated design note design-docs/mssql-cascading.md (the path DMS-1128
referenced but that was absent on main) and revise overview, strengths-risks,
transactions-and-concurrency, key-unification, data-model, ddl-generation,
summary, and epics/09-identity-concurrency/03-identity-propagation to point at
it. Implementation is tracked by DMS-1258; MetaEd authoring guard by
METAED-1667; DMS-1128 is superseded by DMS-1258.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrites the DMS-1129 cascade design to be internally consistent and
implementation-ready for DMS-1258:

- Model the cascade graph in propagation direction (referenced/parent ->
  referrer/child); analyze SQL Server 1785 at the cascade receiver
  (child reached by multiple cascade paths), not the referenced parent.
- Add explicit cycle/SCC fail-fast handling.
- Remove TriggerFallback: every SQL Server reference FK keeps the full
  composite key; a pruned edge is covered (NO ACTION) or fail-fast. This
  also removes the DocumentId-only shape that reintroduced the DMS-1002
  stale-identity race and retires the identity-value propagation trigger.
- Use transitive identity mutability consistently for eligibility and
  coverage classification.
- Reframe cross-dialect detection as a SQL Server portability/authoring
  guard; PostgreSQL keeps full-composite ON UPDATE CASCADE (unchanged).
- Fix FK/RefKey column order to identity parts first, DocumentId last
  (matches ReferenceConstraintPass and the change-queries *_RefKey
  rationale); correct the concrete DDL examples in data-model.md.
- Specify the derived contract DMS-1258 must add (MssqlPropagationMode
  {NativeCascade, NoPropagation}; drop MssqlFkShape); add a "Resolved
  design decisions" section and a note that the DMS-1258 ticket wording
  ("per referenced table") must be corrected before implementation.

Sweeps stale trigger-only / DocumentId-first wording out of overview,
summary, strengths-risks, transactions-and-concurrency, key-unification,
data-model, ddl-generation, compiled-mapping-set, update-tracking, the
index/trigger-inventory and identity-propagation epics, and the two
presentations; adds superseded banners to the two key-unification
children/cross-scope analysis docs. Docs-only; no production code change.

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

Round-2 review fixes on the DMS-1129 design docs:

- Derived contract now models genuinely immutable SQL Server NO ACTION FKs.
  Adds a third MssqlPropagationMode value, ImmutableNoAction, so NO ACTION no
  longer implies NoPropagation (it is either a pruned+covered edge or an
  immutable reference). Classification is total for SQL Server reference FKs;
  coverage/carrier diagnostics apply only to NoPropagation. Updated the
  outcome table, contract, migration, and DMS-1258-correction sections in
  mssql-cascading.md and the contract note in compiled-mapping-set.md.
- Replaced remaining direct-mutability wording with transitive identity
  mutability (IsAbstract || TransitivelyAllowIdentityUpdates) in data-model.md
  and flattening-reconstitution.md (cascade-eligibility contexts only;
  direct-flag scenarios left intact).
- Fixed local FK column order to identity-parts-first / DocumentId-last in
  ddl-generation.md and key-unification.md, with explicit positional-alignment
  notes; corrected a DocumentId-first FK example in key-unification-children-problem.md.
- Replaced stale "SQL Server propagation triggers" wording in strengths-risks.md
  (native cascade on surviving edges; maintenance triggers unaffected) and the
  "multi-row propagation-trigger updates" note in summary.md.
- Added a Diagrams section to mssql-cascading.md (Mermaid): object/relationship
  map, propagation-direction diamond, safe covered pruning, unsafe fail-fast,
  cycle/SCC fail-fast, and before/after RI improvement; cross-referenced from
  overview.md and summary.md.

Docs-only; no production code change.

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

Addresses the PR re-review blocker: the design over-approximated SQL Server
error 1785 as raw cascade in-degree > 1. SQL Server 1785 forbids a table from
appearing more than once in a single UPDATE/DELETE's cascade action tree — a
diamond (two distinct paths from one origin) or a cycle — i.e. the legal graph
is a multitree. Reworked mssql-cascading.md accordingly:

- Detection is now per-origin duplicate reachability (shared cascade ancestry),
  not in-degree. Independent parents into one receiver are legal and are never
  pruned; in-degree > 1 is only a candidate signal. Pruning an independent live
  edge would reintroduce error 547, so it is explicitly disallowed.
- Coverage is defined per originating tree/canonical columns; NoPropagation
  applies only to a covered reconverging edge of a diamond. Uncovered diamonds
  and cycles/SCCs fail fast. Diagnostics now carry origin/root, receiver, the
  distinct paths, candidate/pruned edges, and coverage columns.
- Reworked the diagrams: added "legal independent parents" and "do not prune an
  independent edge (547)"; reframed the illegal-convergence, covered-pruning,
  and fail-fast diagrams around same-origin diamonds; kept cycle/SCC and
  before/after. Updated decisions, probe-scope note, algorithm steps, contract,
  ODS comparison, migration, and DMS-1258/MetaEd follow-ups.

Swept the same conceptual drift out of the secondary docs (the "one surviving
edge per receiver" phrasing implied a forest, not a multitree): overview,
summary, data-model, transactions-and-concurrency, key-unification,
ddl-generation, update-tracking, strengths-risks, compiled-mapping-set,
flattening-reconstitution, the two key-unification analysis banners, the
index/trigger-inventory and identity-propagation epics, and the presentation.

Also: transitive-mutability wording in key-unification.md (was "baseline
allowIdentityUpdates rule") and the relational-model presentation label
(was "based on allowIdentityUpdates"). Docs-only; no production code change.

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

The MssqlPropagationMode.NativeCascade contract definitions in
mssql-cascading.md and compiled-mapping-set.md described the mode only as
the "surviving"/"sole" edge, which left legal independent multi-parent
CASCADE edges ambiguous in a total classification. Restate both to
enumerate all three cascade-eligible-and-not-pruned cases (independent
edge, diamond survivor, sole edge), matching the algorithm and outcome
table. Docs-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ant/pass-ordering, doc+Jira sweeps

Docs-only cleanup resolving the re-review findings (no code changes):

- Column order: key-unification.md referenced-key UNIQUE rule now orders identity
  storage columns first, DocumentId last, positionally aligned with the local FK
  (was DocumentId-first, contradicting the canonical order).
- PostgreSQL fail-fast policy: resolved the ambiguity/contradiction. Derivation now
  fails on BOTH dialects for a SQL-Server-unrepresentable graph as an explicit
  cross-engine portability/authoring policy (not a PostgreSQL correctness need);
  removed the stale "future decision to also fail on PostgreSQL" hedge.
- "origin" defined precisely: any cascade-graph ancestor (transitive mutability),
  not only AllowIdentityUpdates roots — 1785 is a static DDL check over the whole
  graph and 547 trips on any referenced-key change.
- Global invariant added for the retained NativeCascade graph (one mode per FK,
  acyclic, multitree, NoPropagation covered by the FINAL graph) so overlapping
  diamonds are validated globally, not per local snapshot.
- Pass ordering + metadata storage pinned relative to TransitiveIdentityMutabilityPass
  and ReferenceConstraintPass.ResolveOnUpdate.
- PostgreSQL cycle wording softened to "no 1785 DDL restriction" (no unproven
  cyclic-runtime claim) in mssql-cascading.md and transactions-and-concurrency.md.
- Abstract identity-table FK text qualified with SQL Server pruning/fail-fast.
- Historical side-doc subsections (children-problem, design-change-proposal) marked
  inline as superseded/non-actionable trigger work.
- DMS-1258 "must be corrected" note retired to a past-tense "DMS-1258 alignment"
  section; stale correction framing removed.
- compiled-mapping-set NoPropagation + fail-fast diagnostics expanded to match
  mssql-cascading.md; "prunes to the surviving edge" and the "on the surviving edge"
  shorthands swept to accurate "eligible edges" / diamond-scoped wording.

Jira (edited via MCP): METAED-1667 rewritten to per-origin duplicate reachability
(dropping "one surviving edge per referenced table"); DMS-1127 retargeted from the
removed fallback/nested-trigger model to native-cascade update-tracking validation;
DMS-1258 aligned to the both-dialect fail-fast policy and global invariant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h, align stale PG subsection

Docs-only (no code):

- Overlapping diamonds: replace the "pick a local first-fit survivor per
  diamond, then fail if the global invariant breaks" framing in
  mssql-cascading.md with a real deterministic GLOBAL survivor-selection
  contract — a backtracking search over overlapping diamonds in stable order
  that accepts the first complete assignment whose retained NativeCascade graph
  satisfies the invariant (one mode per FK, acyclic, multitree, every
  NoPropagation edge covered by the FINAL graph), and fails fast only when NO
  global assignment satisfies it. This stops the design from falsely rejecting
  schemas that are representable under a different (backtracked) assignment.
- key-unification.md: rewrite the stale "PostgreSQL (no special mitigation
  required)" subsection that still claimed PostgreSQL "supports cycles or
  multiple cascade paths". Now aligned with mssql-cascading.md: full-composite
  ON UPDATE CASCADE on every eligible edge, never physically pruned, derivation
  still fails on BOTH dialects for cycles/SCCs and uncovered diamonds as a
  cross-engine portability policy (not a PostgreSQL correctness need), and no
  claim about PostgreSQL runtime cycle behavior. Kept the key-unification
  mid-cascade / single-writable-column content.

Also updated (not in diff): PR #1088 body bullet 6 (PG "cascades multiple paths
and cycles natively" -> no-1785-restriction + both-dialect portability
fail-fast, no runtime-cycle claim); Jira DMS-1128 supersession banner rewritten
to drop TriggerFallback/MssqlFkShape/DocumentId-only/one-surviving-edge-per-
referenced-table; DMS-1258 AC + tasks aligned to the deterministic global search.

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

Regression audit of the round-6 global survivor-search change found two
self-contradictions in mssql-cascading.md (docs-only, no code):

- Step 4 stated "Uncovered -> fail fast" as an absolute per-edge rule, which
  contradicted step 5's global backtracking (coverage is a property of the
  (edge, chosen survivor) pair; an uncovered edge makes that candidate break
  inadmissible and the search tries another survivor). Reworded step 4 so
  fail-fast is reached only when no admissible break exists under any survivor.
- Step 6 said derivation fails in "exactly two situations" but the global-search
  rewrite implied a third (overlapping diamonds with no consistent global
  assignment). Reframed step 6 as TWO fundamental conditions — cycle/SCC, and
  "diamonds that cannot be jointly broken" — with the single-uncovered-diamond
  and overlapping-infeasible cases as two flavors of the second. This keeps the
  count consistent with compiled-mapping-set.md and DMS-1258 (both say two),
  avoiding a further propagation/churn round. Outcome table row updated to match.

No oscillation found in the git history: every prior concept (1785 model,
TriggerFallback removal, surviving-edge sweep, PG policy) moved in a single
monotonic transition, not A->B->A.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ocs; column-order stragglers

Docs-only (no code). Propagates the round-7 two-condition / global-search model
into the remaining normative surfaces and fixes two stragglers:

- Derived-model contract (mssql-cascading.md) + compiled-mapping-set.md: the
  fail-fast error contract now covers BOTH flavors of the second condition — a
  single uncovered diamond, AND globally infeasible overlapping diamonds (no
  global assignment satisfies the retained-NativeCascade invariant), with
  diagnostics for the overlapping case: involved diamonds, origins/roots,
  receivers, the shared edge/mode conflict, and the invariant clause that failed.
- key-unification.md normative SQL Server rules: replaced "for each diamond, keep
  deterministic winner" (local first-fit) with covered candidate breaks feeding
  the deterministic GLOBAL survivor selection in mssql-cascading.md §5; fail-fast
  only when no complete assignment satisfies the invariant. Kept the
  key-unification point (covered pruning when paths share unified canonical cols).
- mssql-cascading.md dialect-scope: "PostgreSQL alone would create and run such a
  graph" -> "has no 1785 DDL restriction and would accept it at create time" +
  explicit no-runtime-cycle-claim (no runtime behavior asserted for cycles).
- data-model.md:974,981: column order "DocumentId + storage identity" ->
  identity storage columns first, DocumentId last (aligns with data-model.md:584/593).

PR #1088 body fail-fast bullet updated to the two-condition framing (both flavors).

No rejected behavior reintroduced: verified no TriggerFallback / DocumentId-only /
MssqlFkShape / per-referenced-table / raw-in-degree-pruning outside removal/historical
context; no PG runtime-cycle claims; no local-first-fit normative wording remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agate overlapping-diamond fail flavor

Docs-only (no code). The team ratified the open PostgreSQL question with an
engine-specific policy — do NOT force parity between engines:

- Pruning, unsafe-graph detection, and fail-fast are now scoped to SQL Server
  ONLY (they exist for SQL Server's 1785 DDL restriction). PostgreSQL is left
  as-is: full-composite ON UPDATE CASCADE on every eligible edge, never pruned
  or failed by DMS. Cross-engine safety for authors lives in the MetaEd
  authoring guard (METAED-1667). Replaced the "fail derivation on both dialects
  / cross-engine portability policy" (Option A) framing everywhere it appeared:
  mssql-cascading.md (decision 5, dialect-scope section, step 6, DMS-1258
  alignment, intro), key-unification.md PG subsection, transactions:275.
- Removed the last PG runtime-cycle phrasings ("PostgreSQL alone could run it",
  "create and run such a graph"); PG "accepts the DDL at create time" only, with
  an explicit no-runtime-cycle-claim.
- Propagated the "globally infeasible overlapping diamonds" fail flavor into the
  implementation-facing fail-fast shorthands (data-model, ddl-generation,
  overview, summary, transactions, flattening-reconstitution, key-unification,
  epics 07 + 09) so none hides the overlapping case; MetaEd follow-up +
  METAED-1667 include it too.
- Column-order straggler fixed in key-unification-design-change-proposal.md
  (identity storage columns first, DocumentId last).

Jira/PR (not in diff): METAED-1667 + DMS-1258 descriptions aligned to
engine-specific and the overlapping flavor; DMS-1258 decision comment 94649
marked resolved; DMS-1129 AC-bullet-2 decision recorded as a comment; PR #1088
body updated (engine-specific PG policy).

No rejected behavior reintroduced: verified no TriggerFallback / MssqlFkShape /
DocumentId-only / per-referenced-table / raw-in-degree as live; no both-dialect
overstatement; no PG runtime-cycle claims.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing everywhere; engine-specific banner

Docs-only (no code). Finishes propagating the settled fail-fast model into the
last shorthand sites the prior rounds missed:

- strengths-risks.md: "fail derivation fast on an uncovered diamond" -> full
  condition (cascade cycle/SCC/self-loop, or diamonds that cannot be jointly
  broken: single uncovered, or globally infeasible overlapping).
- key-unification.md DDL checklist: same full-condition wording, tied to the
  retained-NativeCascade global invariant.
- key-unification-children-problem.md banner: dropped "largely like PostgreSQL";
  now says SQL-Server-only pruning/fail-fast, PostgreSQL never pruned/failed by
  DMS, MetaEd is the cross-engine authoring guard; includes overlapping diamonds.
- key-unification-design-change-proposal.md banner + both presentation decks:
  same alignment (caught by the whole-tree validation sweep, not just the listed
  files, so they don't reintroduce stale narrowing).
- relational-model presentation: abstract-identity-table node clarified to
  "PK DocumentId, identity cols, Discriminator last" (was a false-positive for the
  FK/RefKey-order sweep; it is the table PK layout, not FK order).

Jira: DMS-1128 "Current authoritative design" updated to engine-specific
(SQL-Server-only pruning/detection/fail-fast; PostgreSQL never pruned/failed;
MetaEd cross-engine authoring guard; overlapping diamonds in the no-safe-pruning
condition). Still "superseded, do not implement from this ticket".

Validation sweep clean: no stale fail-fast narrowing; no both-dialect fail-fast;
no PG runtime-cycle claims (only the explicit no-claim statement remains); no
stale FK/RefKey order; no TriggerFallback/MssqlFkShape/DocumentId-only/
per-referenced-table/raw-in-degree as live behavior (remaining hits are
historical/removal/correction); global-overlap wording present across 14 files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to the full two-condition model

Docs-only (no code). Two implementation-facing summaries still narrowed fail-fast
to "a cascade cycle, or a diamond whose reconverging edge cannot be covered" /
"hard derivation errors for cascade cycles and uncovered diamonds", omitting the
globally-infeasible-overlapping-diamonds case and contradicting the contract
(§ derived-model contract) and algorithm (§ 5-6):

- "Design: pruning with a safety classification" intro (~386)
- "Migration from the current code" bullet (~738)

Both now read: a cascade cycle/SCC/self-loop, or diamonds that cannot be jointly
broken (a single uncovered diamond, or globally infeasible overlapping diamonds
where no global survivor assignment satisfies the retained-NativeCascade
invariant).

No retired concepts reintroduced; PostgreSQL remains engine-specific (never
pruned or failed by DMS). Verification sweep clean: no narrowed fail-fast
wording; forbidden-term hits are historical/removal/correction only; the sole
PG-runtime match is the explicit no-claim statement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Docs-only. summary.md:32 said "SQL Server + PostgreSQL parity is required",
which is too broad and could push DMS-1258 back toward both-dialect
fail-fast/pruning. Reworded to the overview.md:25 meaning: both engines must be
supported (design implementable on both), shared behavior where practical and
explicit engine-specific behavior where they diverge — parity is NOT forced;
SQL Server cascade pruning/detection/fail-fast are SQL-Server-only, PostgreSQL
is left as-is (full-composite ON UPDATE CASCADE, never pruned or failed by DMS).

Also softened the identical bare "PostgreSQL + SQL Server parity" bullet in
extensions.md:47 (extension binding tables participate in the SQL-Server-only
cascade pruning, so a blanket parity claim there is likewise incompatible).

No cascade-policy change; no reintroduction of both-dialect fail-fast,
PostgreSQL pruning/fail-fast, TriggerFallback, MssqlFkShape, DocumentId-only,
per-referenced-table or raw-in-degree pruning, or PG runtime-cycle claims.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Docs-only. overview.md principle #5 label was still the broad "SQL Server +
PostgreSQL parity" shorthand that was removed from summary.md/extensions.md.
Renamed to "Cross-engine support" and clarified the body: implementable on both
PostgreSQL and SQL Server, shared behavior where practical, explicit
engine-specific behavior where they diverge. Target-platform sub-bullet
unchanged. No cascade-policy change; no reintroduction of forced parity,
both-dialect fail-fast, PostgreSQL pruning/fail-fast, or any retired concept.

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

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main (docs-only, no conflicts).

@samuellugo-ship-it samuellugo-ship-it marked this pull request as ready for review July 8, 2026 23:41
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