Skip to content

Commit e2b32f8

Browse files
dmealingclaude
andcommitted
docs: #368 — correct the Kotlin claim, add the rule-(e) zero-candidate gap, state the composite fallback
Three corrections to the public record. I2 — CHANGELOG and PR-BODY both said "Python and Java do not implement this correlation and are unaffected". That was FALSE for the Java tree's Kotlin Exposed generator, which had the same first-match-on-target defect (fixed in the preceding commit). Both now name TypeScript migrate-ts, the C# port and the Kotlin generator, and say only Python is unaffected. I3 — the ledger ruled that rule (e)'s ZERO-candidate gap goes in the limitation list and it never landed. Added: the gate covers a @Cardinality: one relationship only when its holder declares at least one identity.reference at the target, because `candidates.length <= 1` skips zero as well as one. The user-visible consequence is stated plainly — an inverted shape (relationship on one entity, both FKs on the far side) loads clean and codegen then SILENTLY DROPS the relation. M5 — the composite-reference limitation was understated: it does not refuse. Rule (e) accepts a @sourceRefField matching any candidate's first column and the ladder's .find() returns the FIRST candidate, so such a model loads clean and resolves to whichever composite reference is declared first. Also recorded in CHANGELOG, both user-visible: Java's relationship validation moved from eager-throw-on-first-violation to collect-all-findings (a change to Java loader OUTPUT), and adopters with the affected shapes will see FK referential-action diffs on their next `meta migrate`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC
1 parent 53edf3f commit e2b32f8

2 files changed

Lines changed: 39 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,13 @@ here.**
9696
even when the hop explicitly named the other reference, and the docs-site link graph
9797
drew the wrong edge. All four produce a join that typechecks, emits correct DDL, and
9898
passes `meta verify` — the only symptom is wrong rows. The referential-actions
99-
correlation (TypeScript's `migrate-ts` and the C# port; Python and Java do not
100-
implement this correlation and are untouched) had the same defect one level over:
101-
every FK past the first silently inherited the *first* relationship's `@onDelete` /
102-
`@onUpdate` instead of its own, so a model mixing `restrict` and `cascade` across two
103-
references to the same target emitted the wrong action on whichever FK wasn't
104-
examined first.
99+
correlation (TypeScript's `migrate-ts`, the C# port, and the JVM tree's Kotlin
100+
Exposed table generator; Python does not implement this correlation) had the same
101+
defect one level over: every FK past the first silently inherited the *first*
102+
relationship's `@onDelete` / `@onUpdate` instead of its own, so a model mixing
103+
`restrict` and `cascade` across two references to the same target emitted the wrong
104+
action on whichever FK wasn't examined first. All three are now correlated by
105+
inverting the same ladder, and fail closed rather than guessing.
105106

106107
Resolution is now explicit and identical across TypeScript, Python, C# and Java: an
107108
ambiguous `@cardinality: one` reference set resolves by a ladder — the sole
@@ -112,11 +113,25 @@ here.**
112113
Amendment 1). `@sourceRefField` is now legal on a `@cardinality: one` relationship —
113114
it previously failed to load there as an M:N-only attribute. Two limits are
114115
documented rather than fixed here: the ladder matches a candidate's first FK field
115-
only, so two composite references sharing a first column stay indistinguishable, and
116-
the loader gate covers `@cardinality: one` relationships only — a `many`-cardinality
117-
relationship, or a bare `identity.reference` pair with no relationship wrapper, still
118-
reaches codegen unvalidated. See
119-
[`docs/features/relationships.md`](docs/features/relationships.md).
116+
only, so two composite references sharing a first column stay indistinguishable (and
117+
resolve to the first, rather than being refused); the loader gate covers
118+
`@cardinality: one` relationships only — a `many`-cardinality relationship, or a bare
119+
`identity.reference` pair with no relationship wrapper, still reaches codegen
120+
unvalidated; and the gate needs at least one candidate on the holder, so an inverted
121+
shape with both FKs on the far side loads clean and codegen then silently drops the
122+
relation. See [`docs/features/relationships.md`](docs/features/relationships.md).
123+
124+
Two consequences worth stating outright. **Java's relationship validation changed
125+
from eager-throw-on-first-violation to collect-all-findings** —
126+
`validateRelationshipsM2M` now returns a `List<MetaDataException>` rather than
127+
throwing, and the new rule-(e) pass collects the same way, so a Java loader run
128+
reports every broken relationship where it previously reported only the first; that
129+
is a change to Java loader OUTPUT, not an internal refactor. And **adopters with the
130+
affected shapes will see FK referential-action diffs on their next `meta migrate`**:
131+
a second FK to the same target now resolves its OWN `@onDelete` / `@onUpdate`
132+
instead of inheriting the first relationship's, so the generated `ON DELETE` /
133+
`ON UPDATE` (and a Kotlin Exposed table's `ReferenceOption` arguments) legitimately
134+
change.
120135

121136
No vocabulary was added, removed or retyped — the only change to
122137
`expected-registry.json` corrects `@sourceRefField`'s own description, which no

‎docs/features/relationships.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,23 @@ Resolution follows a ladder, checked in order:
209209
**Limitations, documented rather than fixed:**
210210

211211
- The ladder matches a candidate's **first** FK field only, so two composite
212-
references sharing a first column are indistinguishable from each other. The load
213-
error still renders each candidate's full field tuple (`name(fieldA, fieldB)`) so the
214-
ambiguity is visible even where `@sourceRefField` cannot resolve it.
212+
references sharing a first column are indistinguishable from each other — and this
213+
does **not** refuse. Rule (e) accepts a `@sourceRefField` that matches *any*
214+
candidate's first column (`candidates.some(c => c.fields[0] === declared)`), and the
215+
ladder's `.find()` then returns the **first** such candidate. The model loads clean
216+
and resolves to whichever composite reference is declared first, which may not be the
217+
one meant. Where an error *is* raised, it still renders each candidate's full field
218+
tuple (`name(fieldA, fieldB)`) so the collision is at least visible.
215219
- The load-time gate covers `@cardinality: one` relationships only. A
216220
`many`-cardinality relationship, and a bare `identity.reference` pair with no
217221
relationship wrapper at all, reach codegen unvalidated — an ambiguous reference set
218222
in either shape is not caught at load.
223+
- The load-time gate covers a `@cardinality: one` relationship only when its **holder
224+
declares at least one `identity.reference` at the target**. Rule (e)'s
225+
`candidates.length <= 1` skip (`validation-passes.ts`, and its three ports) skips
226+
**zero** as well as one, so an inverted shape — the relationship on one entity, both
227+
FKs on the far side — loads clean, and codegen then **silently drops the relation**:
228+
no join, no error, no diagnostic.
219229
- A projection's `@via` hop (above) resolves the identical ambiguity for the hop it
220230
names, but `origin.first`'s own `@via` is never consulted for its base↔child
221231
correlation — an ambiguous target there has no `@via`-based fix; the only escape is

0 commit comments

Comments
 (0)