Skip to content

Commit cc4b4fd

Browse files
dmealingclaude
andauthored
fix: ADR-0039 — resolving accessors are the default (own*() breaks extends) — all 5 ports (#138)
* fix(metadata): ADR-0039 — resolving accessors are the default (TS reference) extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray live on the parent, reachable only through the resolving accessors. Reading a field/node's effective property or member set through an own-only accessor silently dropped everything inherited via extends — corrupting codegen, runtime, effective serialization, and schema. Flip own→resolving everywhere except the sanctioned emit-declared-here cases (codegen subclass-emit ownFields, own-mode canonical serializer, overlay/super-resolution walks) and the deliberately own-only @dbColumnType (physical, never inherited). New: MetaData.resolvedIsArray() — the resolving accessor for the native isArray flag (isArray is a boolean property, not an attr, so it has no attrs()-based resolution path). All codegen/runtime/effective-serializer reads of a node's array-ness now route through it. Fixed helpers + sites: - meta-field.ts getters (objectRef/column/default/defaultValue/maxLength/ precision/scale/unique/isRequired) → attr() resolving. - meta-identity.ts + meta-relationship.ts getters → resolving. - naming.resolveColumnName, serializer-json effective isArray, object-serializer, validation-passes (field.object @objectref + field.map + flattened-array), migrate-ts expected-schema (@storage/@unique/@objectRef/@default/@autoSet/ @maxLength/@precision/@scale/@localTime/isArray; @dbColumnType kept own) + referential-actions.isRequired, runtime-ts object-manager PK lookup, type-coercer jsonb (@storage resolve, @dbColumnType own), extract-object (@required/@values/@enumAlias/@normalize/isArray), codegen-ts inferred-types/ zod-validators/payload/projection/output-parser/fr010/template-data/ docs-data-builder/column-mapper, codegen-ts-react form-file. Conformance fixture (fails on pre-fix code): abstract field.string(isArray+ @maxlength) / field.decimal(@precision/@scale) / field.object(@objectref+@storage +isArray) each extended by a concrete field, plus a BaseEntity(uuid PK+timestamp) extended by a concrete entity. The effective serializer + validation own-reads both broke pre-fix (ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF + effective isArray mismatch). Plus a codegen gate asserting the Drizzle schema honors every inherited value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n * fix(csharp): ADR-0039 — resolving accessors are the default (C# port) extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray live on the parent, reachable only through the resolving accessors. Reading a field/node's effective property or member set through an own-only accessor silently dropped everything inherited via extends — corrupting codegen (EF entities/DbContext/routes/payload/extract), effective serialization, and loader validation. Flip own→resolving everywhere except the sanctioned emit-declared-here cases (codegen subclass-emit OwnFields/OwnChildren, the own-mode canonical serializer, overlay/super-resolution walks, projection OwnSources) and the deliberately own-only @dbColumnType (physical, never inherited). Mirrors the TS reference (d62aab5). New: MetaData.ResolvedIsArray() — the resolving accessor for the native isArray flag (isArray is a bool property, not an attr, so it has no Attrs()-based resolution path); walks own→super, own true wins, cycle-guarded, cached. All codegen/effective-serializer reads of a node's array-ness route through it. Fixed helpers + sites: - MetaField getters (ObjectRef/ValueType/Storage/DbColumn/Default/DefaultValue/ MaxLength/Precision/Scale/Unique + the @required half of IsRequired) → resolving Attr(); DbColumnType + EnumValues KEPT own (commented). - MetaSource + MetaRelationship getters → resolving (reconciled with the already- resolving MetaIdentity). - SerializerJson effective-mode isArray → ResolvedIsArray (own mode keeps IsArray); ValidationPasses field.object @objectref + flattened-array isArray + field.map @valueType/@objectref → resolving. - Codegen: CSharpNaming (IsRequired/IsLocalTime/RequiresSystemNet), EntityGenerator/ DbContextGenerator/RoutesGenerator isArray, PayloadCodegen (@objectref + isArray), ExtractObject (@required/@values/@enumAlias/@normalize/@default/@coerceDefault/ @xmlText/isArray), TemplateData, Fr010FieldMapping (dropped the dead OwnAttr ("isArray") clause; @required/@values/@default/@coerceDefault/@normalize/@xmlText → resolving). Gate: the shared cross-port fixture fixtures/conformance/extends-abstract-field-inheritance now passes in C# (effective serializer + loader), and a new C# codegen honor-inheritance test asserts the generated Contact entity honors inherited isArray/@maxLength/ @precision/@objectRef/@storage + the BaseEntity UUID PK. No golden changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n * fix(metadata): ADR-0039 — resolving accessors are the default (Python port) extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray live on the parent, reachable only through the resolving accessors. Reading an effective property or member set through an own-only accessor silently dropped everything inherited via extends. Python's naming is INVERTED vs TS/Java — attr(name) is OWN-ONLY, attrs().get(name) resolves — so this port both fixes the own reads and makes the resolving form the obvious/default-named call. Naming (the durable hardening): a full attr()->own_attr() rename was too invasive (148 call sites, many legitimately own-only in the loader-validation of authored declarations, plus the own-mode serializer/merge which already use own_meta_attrs/ own_children — NOT attr()). Instead: - added MetaData.get_meta_attr(name) — the RESOLVING attr read (own + inherited via the super chain); the default read to prefer for any effective property. - added MetaData.resolved_is_array() — isArray is a native boolean property (not an attr), so it resolves the native flag through the super chain OR the loaded @isarray attr. Mirrors the TS resolvedIsArray(). - re-documented attr() LOUDLY as OWN-ONLY, calling out the TS/Java inversion and naming the only sanctioned own uses (own-mode serialize, overlay/merge + super walks, validating the own declaration, @dbColumnType). Fixed effective-property reads (own -> resolving): - meta_field.object_ref: attr()->get_meta_attr (unifies with codegen attrs().get @objectref — both now resolve). - codegen fr010_field_mapping: is_array->resolved_is_array; coerceDefault/default/ normalize (_attr_string) -> get_meta_attr. - codegen type_map.field_is_array + template_codegen/template_data (isArray/ required/maxLength) -> resolving. - codegen fr019_shared_enum.is_provided (@provided) -> get_meta_attr. - runtime object_manager: identity @fields (PK lookup), @localTime, @column, field-is-array -> resolving; @dbColumnType kept own-only (commented — the one deliberately never-inherited attribute). - loader validation_passes: field.object @objectRef/@storage/isArray + field.map @valueType/@objectref + effective-children iteration -> resolving (a concrete field.object inheriting @objectref from an abstract parent no longer falsely trips ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF). The own-declaration validators (@values / @default / @dbColumnType membership) stay own-only, commented. - serializer_json: EFFECTIVE mode now resolves isArray through the super chain; own mode still emits the authored flag (round-trips the extends tree). KEPT own-only (each commented): the sanctioned emit-declared-here case (EntityModelGenerator.own_fields, pinned by test_entity_model.py — a Contact subclass extends the generated BaseEntity and does NOT re-emit inherited id/ createdAt); the own-mode canonical serializer; overlay/merge + TPH discriminator super-resolution walks; the loader's validate-the-own-declaration passes; and the physical @dbColumnType. The shared cross-port fixture fixtures/conformance/extends-abstract-field- inheritance (unchanged) now passes — the effective serializer previously dropped the inherited isArray:true on tags/addresses. New Python codegen gate (test_extends_abstract_field_inheritance.py) asserts a concrete field extending an abstract array/decimal/object field, and an entity inheriting fields via BaseEntity, generate correctly (list[str] / list[Address] / Decimal / subclass- emit). Existing goldens unchanged for non-abstract-extends inputs (full non- integration suite green except 2 pre-existing, unrelated staleness-nudge failures). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n * fix(jvm): ADR-0039 — resolving accessors are the default (Java + Kotlin port) extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray live on the parent, reachable only through the resolving accessors. Reading a field/node's effective property or member set through an own-only accessor silently dropped everything inherited via extends — corrupting codegen (an entity inheriting its source.rdb emitted NOTHING), effective serialization, and loader validation. Mirrors the TS reference. The isArray resolver - MetaField.isArrayType() now FULLY resolves: it walks the extends super chain for the native isArray flag (resolvedNativeIsArray) as well as the @isarray attr, so a concrete field extending an abstract array field is array-typed. isArray() stays the own-only raw flag; all codegen array reads route through isArrayType(). Metadata core - MetaObject.getSources() no-arg now RESOLVES (getSources(true)), matching getMetaFields()/getIdentities()/getRelationships(). The declaration-layer one-primary-source validation keeps own-only via explicit getSources(false). - ValidationPhase: field.object @objectRef/@storage and field.map @valueType/@objectref reads flipped own→resolving (fixed the shared fixture's false ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF for a field.object extending an abstract AddressBag). @dbColumnType stays own-only (commented). - CanonicalJsonSerializer: effective mode now inlines inherited array-ness (getNativeIsArray resolves via isArrayType in effective mode; own mode keeps the raw flag so the authored form round-trips). Java codegen (codegen-spring) - SpringTypeMapper / SpringPayloadGenerator / OutputFormatSpecEmitter: raw field.isArray() → resolving isArrayType(); OutputFormatSpecEmitter @enumDoc/@example/@Instruction flipped own→resolving (consistent with the already-resolving @values/@required). Kotlin codegen (codegen-kotlin) - New resolving source lookup KotlinGenUtil.firstRdbSource/hasRdbSource (getSources(true)); routed ALL 8 generators' own-only entity.children.filterIsInstance<RdbSource>() reads through it — an entity inheriting its source.rdb via extends now emits its table/controller/etc. - KotlinTypeMapper: intAttr (@precision/@scale) and stringAttr default (@valueType) flipped own→resolving (consistent with stringMaxLength). - KotlinExtractSchemaEmitter / KotlinOutputFormatSpecEmitter: raw .isArray → isArrayType(); @enumDoc/@example/@Instruction → resolving. - KotlinEntityGenerator.validator(): raw field.children → getChildren(MetaValidator, true). Kept own-only (each commented): @dbColumnType (never inherited by policy); one-primary-source validation (declaration-layer); @isAbstract/@discriminator/ @provided/@kind/template-ref reads; origin.* reads (origin never inherits). Tests - New KotlinAbstractFieldExtendsInheritanceTest gates the two ADR-0039 shapes: a concrete entity inheriting its source.rdb via extends emits an Exposed table, and concrete fields extending an abstract array field / abstract decimal field inherit isArray / @precision+@scale. Verified it FAILS on pre-fix code. - Shared fixtures/conformance/extends-abstract-field-inheritance now passes in the Java conformance runner (loader + effective serializer). Fixture unchanged. All green: Java metadata+codegen-spring 158, Java conformance 394, Kotlin codegen 266, Kotlin integration (Testcontainers) 92, Java integration (Testcontainers) 86. No existing goldens/snapshots changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n --------- Co-authored-by: Doug Mealing <noreply@anthropic.com>
1 parent 999269c commit cc4b4fd

74 files changed

Lines changed: 1390 additions & 294 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{
6+
"field.string": {
7+
"name": "Tags",
8+
"package": "acme",
9+
"abstract": true,
10+
"isArray": true,
11+
"@maxLength": 40
12+
}
13+
},
14+
{
15+
"field.decimal": {
16+
"name": "Money",
17+
"package": "acme",
18+
"abstract": true,
19+
"@precision": 12,
20+
"@scale": 2
21+
}
22+
},
23+
{
24+
"object.value": {
25+
"name": "Address",
26+
"children": [
27+
{
28+
"field.string": {
29+
"name": "street"
30+
}
31+
},
32+
{
33+
"field.string": {
34+
"name": "city"
35+
}
36+
}
37+
]
38+
}
39+
},
40+
{
41+
"field.object": {
42+
"name": "AddressBag",
43+
"package": "acme",
44+
"abstract": true,
45+
"isArray": true,
46+
"@objectRef": "acme::Address",
47+
"@storage": "jsonb"
48+
}
49+
},
50+
{
51+
"object.entity": {
52+
"name": "BaseEntity",
53+
"abstract": true,
54+
"children": [
55+
{
56+
"field.uuid": {
57+
"name": "id"
58+
}
59+
},
60+
{
61+
"field.timestamp": {
62+
"name": "createdAt"
63+
}
64+
},
65+
{
66+
"identity.primary": {
67+
"name": "id",
68+
"@fields": [
69+
"id"
70+
],
71+
"@generation": "uuid"
72+
}
73+
}
74+
]
75+
}
76+
},
77+
{
78+
"object.entity": {
79+
"name": "Contact",
80+
"extends": "acme::BaseEntity",
81+
"children": [
82+
{
83+
"field.uuid": {
84+
"name": "id"
85+
}
86+
},
87+
{
88+
"field.timestamp": {
89+
"name": "createdAt"
90+
}
91+
},
92+
{
93+
"identity.primary": {
94+
"name": "id",
95+
"@fields": [
96+
"id"
97+
],
98+
"@generation": "uuid"
99+
}
100+
},
101+
{
102+
"source.rdb": {
103+
"@table": "contacts"
104+
}
105+
},
106+
{
107+
"field.string": {
108+
"name": "name",
109+
"@required": true
110+
}
111+
},
112+
{
113+
"field.string": {
114+
"name": "tags",
115+
"extends": "acme::Tags",
116+
"isArray": true,
117+
"@maxLength": 40
118+
}
119+
},
120+
{
121+
"field.decimal": {
122+
"name": "balance",
123+
"extends": "acme::Money",
124+
"@precision": 12,
125+
"@scale": 2
126+
}
127+
},
128+
{
129+
"field.object": {
130+
"name": "addresses",
131+
"extends": "acme::AddressBag",
132+
"isArray": true,
133+
"@objectRef": "acme::Address",
134+
"@storage": "jsonb"
135+
}
136+
}
137+
]
138+
}
139+
}
140+
]
141+
}
142+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{
6+
"field.string": {
7+
"name": "Tags",
8+
"package": "acme",
9+
"abstract": true,
10+
"isArray": true,
11+
"@maxLength": 40
12+
}
13+
},
14+
{
15+
"field.decimal": {
16+
"name": "Money",
17+
"package": "acme",
18+
"abstract": true,
19+
"@precision": 12,
20+
"@scale": 2
21+
}
22+
},
23+
{
24+
"object.value": {
25+
"name": "Address",
26+
"children": [
27+
{
28+
"field.string": {
29+
"name": "street"
30+
}
31+
},
32+
{
33+
"field.string": {
34+
"name": "city"
35+
}
36+
}
37+
]
38+
}
39+
},
40+
{
41+
"field.object": {
42+
"name": "AddressBag",
43+
"package": "acme",
44+
"abstract": true,
45+
"isArray": true,
46+
"@objectRef": "acme::Address",
47+
"@storage": "jsonb"
48+
}
49+
},
50+
{
51+
"object.entity": {
52+
"name": "BaseEntity",
53+
"abstract": true,
54+
"children": [
55+
{
56+
"field.uuid": {
57+
"name": "id"
58+
}
59+
},
60+
{
61+
"field.timestamp": {
62+
"name": "createdAt"
63+
}
64+
},
65+
{
66+
"identity.primary": {
67+
"name": "id",
68+
"@fields": [
69+
"id"
70+
],
71+
"@generation": "uuid"
72+
}
73+
}
74+
]
75+
}
76+
},
77+
{
78+
"object.entity": {
79+
"name": "Contact",
80+
"extends": "acme::BaseEntity",
81+
"children": [
82+
{
83+
"source.rdb": {
84+
"@table": "contacts"
85+
}
86+
},
87+
{
88+
"field.string": {
89+
"name": "name",
90+
"@required": true
91+
}
92+
},
93+
{
94+
"field.string": {
95+
"name": "tags",
96+
"extends": "acme::Tags"
97+
}
98+
},
99+
{
100+
"field.decimal": {
101+
"name": "balance",
102+
"extends": "acme::Money"
103+
}
104+
},
105+
{
106+
"field.object": {
107+
"name": "addresses",
108+
"extends": "acme::AddressBag"
109+
}
110+
}
111+
]
112+
}
113+
}
114+
]
115+
}
116+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{
6+
"field.string": {
7+
"name": "Tags",
8+
"abstract": true,
9+
"isArray": true,
10+
"@maxLength": 40
11+
}
12+
},
13+
{
14+
"field.decimal": {
15+
"name": "Money",
16+
"abstract": true,
17+
"@precision": 12,
18+
"@scale": 2
19+
}
20+
},
21+
{
22+
"object.value": {
23+
"name": "Address",
24+
"children": [
25+
{ "field.string": { "name": "street" } },
26+
{ "field.string": { "name": "city" } }
27+
]
28+
}
29+
},
30+
{
31+
"field.object": {
32+
"name": "AddressBag",
33+
"abstract": true,
34+
"isArray": true,
35+
"@objectRef": "acme::Address",
36+
"@storage": "jsonb"
37+
}
38+
},
39+
{
40+
"object.entity": {
41+
"name": "BaseEntity",
42+
"abstract": true,
43+
"children": [
44+
{
45+
"field.uuid": {
46+
"name": "id"
47+
}
48+
},
49+
{
50+
"field.timestamp": {
51+
"name": "createdAt"
52+
}
53+
},
54+
{
55+
"identity.primary": {
56+
"name": "id",
57+
"@fields": "id",
58+
"@generation": "uuid"
59+
}
60+
}
61+
]
62+
}
63+
},
64+
{
65+
"object.entity": {
66+
"name": "Contact",
67+
"extends": "acme::BaseEntity",
68+
"children": [
69+
{
70+
"source.rdb": {
71+
"@table": "contacts"
72+
}
73+
},
74+
{
75+
"field.string": {
76+
"name": "name",
77+
"@required": true
78+
}
79+
},
80+
{
81+
"field.string": {
82+
"name": "tags",
83+
"extends": "acme::Tags"
84+
}
85+
},
86+
{
87+
"field.decimal": {
88+
"name": "balance",
89+
"extends": "acme::Money"
90+
}
91+
},
92+
{
93+
"field.object": {
94+
"name": "addresses",
95+
"extends": "acme::AddressBag"
96+
}
97+
}
98+
]
99+
}
100+
}
101+
]
102+
}
103+
}

0 commit comments

Comments
 (0)