Skip to content

Commit 2b07b51

Browse files
dmealingclaude
andcommitted
docs(changelog): the cross-port physical-name fixes this line landed
Seven entries covering work the four fix commits landed without changelog coverage. Written centrally rather than per-commit because most of them are one cross-port statement, and a reader wants the shape of the divergence, not five per-port notes. The @Schema entry leads and is marked BEHAVIOUR deliberately. It is the only adopter-visible change in the set: generated code that was addressing `public` will now address the schema the model declares, and for an adopter whose `search_path` happened to mask the divergence that is a change on upgrade rather than a fix they will notice as one. The entry says so in those terms, and says what to do instead of naming a config flag that does not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HSCgs7z19w8aGXGiceCohC
1 parent 0e0b566 commit 2b07b51

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,124 @@ an object with a read-only primary source beside a writable one, which `object.e
6363
`object.projection` each refuse and which only ever loaded because `object.base` carries neither
6464
rule.
6565

66+
### Fixed — BEHAVIOUR: `@schema` is honoured by every table, view and callable binding
67+
68+
**A `@schema` object's generated code addressed the wrong schema.** `migrate-ts` creates it
69+
where it says — `CREATE TABLE "sales"."widgets"` — and under ADR-0015 it owns where a table
70+
lives. No code generator in any port read the attribute: the names artifact carried it in
71+
TypeScript, C#, Kotlin and Java, and every binding then dropped it, so generated queries
72+
addressed `public.widgets` while the migration built `sales.widgets`.
73+
74+
Whether that surfaced depended entirely on the adopter's `search_path` — configured one way
75+
the generated code silently read a DIFFERENT table, configured another it worked. **This is
76+
therefore a behaviour change on upgrade for anyone whose `search_path` was masking it**: their
77+
generated code will now address the schema the model declares. Nothing in this repo sets
78+
`search_path`, so there was no layer compensating; three shipped conformance fixtures declare
79+
a non-default `@schema`, so the shape is not theoretical. Pin the old behaviour by removing
80+
`@schema` (and letting the connection resolve the name), not by configuration.
81+
82+
Each port binds it in the form its target expresses, through the constant:
83+
`pgSchema(WidgetNames.schema).table(…)` in TypeScript (a different CALL shape, not a third
84+
argument), `[Table(WidgetNames.Name, Schema = WidgetNames.Schema)]` in C#, and
85+
`Table(WidgetNames.SCHEMA + "." + WidgetNames.NAME)` in Kotlin. Views and stored-proc
86+
wrappers are included — fixing tables alone would leave the two halves disagreeing with each
87+
other as well as with the database. sqlite/d1 are excluded and that is correct rather than a
88+
gap: SQLite has no schema concept and migrate already refuses a non-default `@schema` there.
89+
90+
A write-through entity's **replica view** takes its own schema, not the entity's: the object's
91+
primary source there is the WRITABLE table, so the obvious lookup would have qualified the
92+
view with the write table's schema — a name paired with someone else's schema, which is worse
93+
than no schema at all.
94+
95+
**The gate said `@schema` was unread, and that was false when it was written.** The C# row
96+
asserted "NO generator reads it" while `CallableGenerator` had been reading it and splicing it
97+
into emitted SQL as a spelled literal since before this work stream; the row still read
98+
`dropped` only because the fixture put `@schema` on a plain entity and not on the one
99+
generator that qualified. It is on both now. The TypeScript row's "NO generator anywhere reads
100+
it" was wrong for a different reason — the projection view-DDL builder has always read it.
101+
102+
### Fixed — a jsonb value-object enum was stored as EF's integer ordinal (C#)
103+
104+
A value object stored as owned JSON had no enum conversion on either `.ToJson` arm, so EF Core
105+
8's default applied and a member persisted as its **ordinal** — `{"mood": 0}` — where
106+
TypeScript, Java, Kotlin and Python all store the member SYMBOL. A silent cross-port wire
107+
break, and a positionally fragile one: reordering `@values` silently re-maps already-stored
108+
rows, which is exactly why `@intValueMap` was specified as a map rather than an array.
109+
110+
The conversion is `HasConversion<string>()` unconditionally, deliberately **not** the
111+
`@intValueMap` pair the flattened arm uses: the int map is a COLUMN-storage concern and there
112+
is no column inside a JSON document. It reaches no port's value-object path, so routing it in
113+
here would have swapped one cross-port divergence for another.
114+
115+
### Fixed — three flattened value-object member kinds bound a column no migration creates (C#)
116+
117+
`@storage: flattened` spreads a value object onto the owning table as
118+
`<parent column>_<member column>`, and migrate's `flattenObjectField` iterates the value
119+
object's fields **unconditionally**. The generator named scalars and non-array enums only, so
120+
an array enum and a nested value object fell through to EF's `<Nav>_<Prop>` default and bound
121+
a column the migration never creates (Postgres `42703`). Both are now named. `field.map` is
122+
still not, deliberately — this port configures a top-level `field.map` nowhere either, so
123+
there is no proven mapping to mirror and forcing one risks a broken model build; it now emits
124+
a **gen-time warning** naming the member and the exact column migrate will create, and is
125+
recorded as G9 in the port's `KNOWN_GAPS.md`.
126+
127+
### Fixed — the gate over the gates counted PROSE as coverage, and inverted its own verdict
128+
129+
`scripts/check-no-magic-gate-coverage.sh` matched each shape marker against the whole gate
130+
file, and a javadoc line reading ``contains {@code "object.value"} as a substring`` satisfied
131+
`java:value_object` on its own — so the ledger would have reported "no gap" with both real
132+
declarations deleted. Markers now match with comment-only lines stripped.
133+
134+
The first fix carried a worse bug than the one it fixed: piping the stripper into `grep -q`
135+
under `set -o pipefail` meant a MATCH was reported as a GAP, because `grep -q` closes the pipe
136+
on its first hit and the upstream dies of SIGPIPE. It briefly reported 22 gaps that did not
137+
exist. Same inversion as the pre-release pin detector's `grep | head`, and the more dangerous
138+
half of the pair — a check that inverts is worse than one that merely misses.
139+
140+
### Fixed — four of five gates asserted "the fixture loads clean" against a LAX loader
141+
142+
Every no-magic gate carries the line *"A gate whose fixture the loader would reject proves
143+
nothing"* above an `errors == []` assertion. In TypeScript, C#, Python and Kotlin that
144+
assertion could not fail on an unknown attribute, because the loader was not strict — so it
145+
proved the fixture PARSES, never that it is legal under the sealed registry (ADR-0023), which
146+
is the rule an adopter's model faces. All four now load strict; all four fixtures were already
147+
clean under it, so no fixture changed — only what the gate is entitled to claim.
148+
149+
Kotlin needed an API fix, and that is the durable half: `loadString` was the one loader entry
150+
point with **no options parameter**, while its own siblings `loadUris`/`loadResources` took
151+
`LoaderOptions` precisely so a caller could ask for strict. An inline model is what every
152+
codegen test loads, so the one shape that could never be checked strictly was the one shape
153+
the tests all use. `MetaDataLoader.fromString` and the ktx `loadString` now take options and
154+
pass them through; the existing 3-arg forms delegate with null.
155+
156+
### Fixed — FR-015 was documented as shipping cross-port, and Kotlin bound an unregistered attribute
157+
158+
`AGENTS.md` claimed the projection codegen fan-out including "FR-015 proc-callables" was
159+
"shipped cross-port". It was not: callable generators exist in TypeScript and C# only, **Java
160+
and Python have none at all**, and Kotlin's bound its arguments from `@param`, an attribute
161+
that port invented and no provider registers. Under the sealed registry no metadata carrying
162+
it can load, so the predicate answered false for every field of every loadable model: every
163+
wrapper was silently zero-argument, while an adopter following the documented contract and
164+
declaring `@parameterRef` had their arguments ignored without a word.
165+
166+
Kotlin now binds the registered `@parameterRef`; `@param` is deleted from the repository
167+
rather than merely unread. The claim is corrected, and the Java and Python shortfall is
168+
recorded in each port's `KNOWN_GAPS.md` — building those two generators is feature work and is
169+
additionally blocked on `@exposeAsRoute`, which the originating plan left explicitly undecided.
170+
171+
### Fixed — an index's physical attributes never reached the generated schema (TypeScript)
172+
173+
The two index emitters were reconciled on NAME earlier in this line and still disagreed on
174+
SHAPE. `@expr`, `@using`, `@orders` and `@where` all reach migrate and none reached codegen:
175+
an `@expr`-only index emitted **nothing** (both loops required a non-empty `@fields` while
176+
migrate keys an expression index off `@expr`), `@where` never arrived so a PARTIAL unique index
177+
read as a **fully** unique one, and `@orders` was dropped so a DESC index read as ASC. The last
178+
had a test pinning it — asserting the direction was absent, over a fixture whose own comment
179+
says "customerId + placedAt DESC".
180+
181+
The two near-identical loops are now one function, because a rule written twice is a rule that
182+
can be forgotten twice, which is how these came to be honoured by neither.
183+
66184
### Added — every physical database name has ONE spelling per run, in all five ports
67185

68186
A generated per-object artifact now carries an object's physical database names — the

0 commit comments

Comments
 (0)