Repository navigation
Codegen reads inherited attrs via own-only accessor (ownAttr/attr) — drops inherited @required/@maxLength/@objectRef and (TS) inherited identity @fields #56
Description
Tracking the ownAttr()-should-be-attr() bug class (codegen reading metadata attributes own-only, so attributes inherited via extends are invisible). Includes a confirmed repro (#1) and a full cross-port audit inventory (#2) so this can be fixed as a port-parallel set, not one symptom.
The model that decides bug-vs-not
There are two inheritance models, and own-only is only sometimes wrong:
- Node-level inheritance (the dominant BaseEntity pattern,
Child extends BaseEntityinheriting wholefield/identitynodes): the effective child iterators (fields()/identities()/primaryIdentity()etc.) return the inherited node itself, which carries its own@fields/@generation/@required. Sonode.ownAttr(x)on a node from an effective iterator is correct by construction → TS / C# / Java codegen are largely clean. - Attr-level inheritance (ADR-0029 nested
extends: afield/identitynode whose attrs are inherited via its own super-pointer — e.g. abstract-field reusefield.string label extends Base.label, oridentity.primary extends Base.primarywithout restating@fields):ownAttr(x)returnsundefinedwhileattr(x)returns the inherited value. This is the trap.
Do NOT blanket-convert ownAttr→attr — most ownAttr-on-an-effective-node reads are correct. Fix only the attr-level reads below.
#1 — Confirmed repro (TS)
A node-level extends that inherits an identity's @fields without restating it drops the entity from the PK map (pk-resolver.ts:33 reads primary.ownAttr("@fields"); line 34 continues when empty → no PK, and downstream FK references to it break):
// Loaded model: BaseEntity (abstract) { id, identity.primary @fields:[id] }
// Widget { source.rdb, id, identity.primary extends "test::BaseEntity.primary" } // @fields NOT restated
const widget = result.root.objects().find(o => o.name === "Widget")!;
const primary = widget.ownChildren().find(c => c.type === "identity")!;
expect(primary.attr("fields")).toEqual(["id"]); // effective: sees inherited value ✓
expect(primary.ownAttr("fields")).toBeUndefined(); // own-only: misses it ← root cause
const pkMap = buildPkMap(result.root);
expect([...pkMap.keys()]).toContain("Widget"); // FAILS: pkMap keys = ["BaseEntity"] — Widget droppedToday the corpus's extends-on-identity always restates @fields, so this latent hole is unexercised; it should be gated (the repro converts to a green assertion once the read is attr()).
#2 — Cross-port audit inventory
| Port | Confirmed NEEDS-EFFECTIVE | UNSURE | Highest-impact site |
|---|---|---|---|
| TypeScript | 0 (codegen robust; latent ADR-0029 edge above) | 1 | migrate-ts/expected-schema.ts:98-101 (source @kind own-only) |
| C# | 0 (node-level → correct) | 1 | MetaObjects/Meta/MetaIdentity.cs:19 (@fields; verify ADR-0029 nested-extends edge) |
| Java (codegen-spring) | 0 (single-arg getMetaAttr defaults effective) |
3 (validation) | ValidationPhase.java:1611-1612 (identity @fields own-only in an effective walk) |
| Kotlin | 1 (+4 strong) | 4 | KotlinExposedTableGenerator.kt:487-488 (@objectRef own-only → broken FK/column on inherited object field) |
| Python | 15 (14 codegen + 1 validation) | ~12 | codegen/generators/entity_model.py:119 (@required own-only → wrong column nullability) |
Kotlin confirmed/strong: KotlinTypeMapper.kt:415-417 (@maxLength→255 fallback), KotlinExposedTableGenerator.kt:487-488 + 481-482 (@objectRef, @storage), KotlinEntityGenerator.kt:224-225 (@objectRef), KotlinFilterAllowlistGenerator.kt:170-171 (@filterable — parity gap vs Java's effective SpringFilterAllowlistGenerator).
Python confirmed (move attr()→attrs()): type_map.py:38(@isArray),:69(@objectRef); entity_model.py:60(@maxLength),:113(@objectRef),:119(@required),:120(@default); payload_vo_generator.py:175(@required),:189(@objectRef); extract_delegate_emitter.py:50; render_helper_generator.py:117; fr010_field_mapping.py:37/42/60; filter_allowlist_generator.py:120(@filterable); loader/validation_passes.py:1390/1394(warning). The correct template already exists in-repo: type_map.effective_enum_values (type_map.py:55) uses attrs() for @values with a docstring explaining inheritance — the rest of Python codegen just didn't follow it.
Don't break this: the validation reference-walk is CORRECT-AS-OWN
In all five ports the reference-walk (relationship @objectRef, identity.reference FK derivation, origin.*, template/source validators) intentionally reads own-only — it validates a reference at its declaration site (FR5d requires the error to point at the declaring node; origin.* is contractually non-inheriting; M:N requires the junction to declare its references locally). Do NOT sweep these into attr() or it regresses into double-reporting / wrong error envelopes. The genuinely-suspect validation sites are the checkFilterableFields own-only-over-effective reads (Java ValidationPhase.java:1611-1680, Python validate_discriminator.py:109), flagged UNSURE.
Suggested fix shape
- Python: the big one — convert the ~15 field-attr reads to
attrs()(theeffective_enum_valuespattern). Highest-impact:@requirednullability. - Kotlin: convert the 1 confirmed + 4 field-attr reads; bring
KotlinFilterAllowlistGeneratorto parity with the effective Java generator. - TS / C# / Java: largely correct — verify the ADR-0029 nested-child-
extendsedge for identity/field accessors + resolve thecheckFilterableFieldsown-only-over-effective inconsistency. - Gate it: add the fix(cli): scaffold outDir defaults to src/generated, not ./src/db #1 repro (and a field-level
@required-inherited variant) to the conformance corpus so codegen + a runtime roundtrip exercise inherit-without-restate in every port — TDD red → green.
cc the session already working this — this inventory is the full map so the fix lands as a set.
Activity
dmealing commentedon Jun 21, 2026
Gate/tripwire PR opened: #57 — two RED tests (TS identity-
@fieldsPK-drop + Python@requiredfield-inheritance → Optional). They assert the correct behavior and turn green when this is fixed, then merge as the permanent regression gate. The Python one's generated output (label: str | None = Nonefor an inherited@required:true) is the cleanest repro of the own-only-attr()footgun.test(gate): add Kotlin tripwire for inherit-without-restate (#56)
dmealing commentedon Jun 21, 2026
Status after #55 (verified against the #57 gates on current
main):✅ #55 fixed the TS field-attr reads (column-mapper, drizzle-schema field reads, filter-allowlist, fr010-field-mapping, extractor, etc. —
ownAttr→attr).❌ Still RED — three remaining slices (one gate each in #57):
TS — identity
@fields(pk-resolver.ts:33+:41): stillprimary.ownAttr(IDENTITY_ATTR_FIELDS)/ownAttr(IDENTITY_ATTR_GENERATION). fix(codegen-ts): read field attributes via effective attr(), not ownAttr() #55 did not touchpk-resolver.ts, so a BaseEntity-inherited PK (identity.primary extends Base.primarywithout restating@fields) is still dropped from the PK map → no primary key. Fix:ownAttr→attr(2 lines). (TS field reads are done; this is the one TS spot fix(codegen-ts): read field attributes via effective attr(), not ownAttr() #55 missed.)Python — all ~14 codegen sites still use
field.attr()(own-only in this port). Gate output: an inherited@required:trueemitslabel: str | None = None. Fix:attr()→attrs().get()per the inventory above (the in-repotype_map.effective_enum_valuesis the template). Highest-impact:entity_model.py:119(@required).Kotlin —
KotlinTypeMapper.kt:415-417(@maxLength),KotlinExposedTableGenerator.kt:481-488(@storage/@objectRef),KotlinFilterAllowlistGenerator.kt:170-171(@filterable) still own-only. Gate output: inherited@maxLength=80emitsvarchar("label", 255). Fix: own→effective; bringKotlinFilterAllowlistGeneratorto parity with the effective Java generator.Each gate in #57 turns green as its port is fixed; #57 merges when all three are green.
fix(codegen): read inherited attributes via the effective accessor, n…
fix(codegen): read inherited attrs via effective accessor across TS/P…
fix(validation): inherited-attr edges in identity accessors + filtera…
dmealing commentedon Jun 21, 2026
Fixed across all five ports by #57 and #58, each with regression gates. The forward-looking config-driven evolution remains tracked in #51.