Skip to content

feat: ADR-0038 Wave 4 — reverse navigation via explicit FK finders (all 5 ports)#133

Merged
dmealing merged 6 commits into
mainfrom
feat/wave4-reverse-finders
Jun 30, 2026
Merged

feat: ADR-0038 Wave 4 — reverse navigation via explicit FK finders (all 5 ports)#133
dmealing merged 6 commits into
mainfrom
feat/wave4-reverse-finders

Conversation

@dmealing

Copy link
Copy Markdown
Member

Implements ADR-0038 / ADR-0036 Wave 4: reverse 1:N navigation as explicit FK query methods, not lazy collections. find<Source>By<FkField>(id) + batched …In(ids) — one indexed query, no N+1, framework-free, idiomatic per port (Spring repository finder / EF query method / Exposed DSL fn / Python query fn).

  • Dissolves the same-pair collision + @reverseName: names derive from source + FK field (drop trailing Id) → unique by construction. GameSession→Scene ×3 → three distinct finders everywhere.
  • Fixes the TS collision bug: the buggy lazy Drizzle reverse many() (silent overwrite) removed; finders replace it.
  • Zero new metamodel vocabulary — pure codegen from existing relationship/identity.reference metadata; registry-conformance unaffected.
  • Confirmed adopter need (a JVM adopter hand-rolls exactly these SceneRepository finders).

Shared reverse-finders-same-pair conformance fixture (same-pair ×3) gates the metadata cross-port; each port asserts its finder names. TS 52d76f15 · C# f6ec48ea · Py 18dc6c81 · JVM af83751b.

🤖 Generated with Claude Code

claude added 6 commits June 30, 2026 18:22
Implement the TS reference for ADR-0038 / ADR-0036 Wave 4: generate reverse
1:N navigation as explicit, typed FK query methods instead of lazy Drizzle
relations() collections, and fix the same-pair reverse-nav collision bug.

For each FK an entity E holds (identity.reference), E's queries module now
gains a finder pair, named by the FK FIELD (PascalCased, trailing `Id`
dropped) so it is unique within E by construction:

  find<EPlural>By<FkField>(value)    → SELECT … WHERE fk = ?    (single, indexed)
  find<EPlural>By<FkField>In(values) → SELECT … WHERE fk IN (…) (batched, anti-N+1)

The same-pair case (GameSession with 3 FKs to Scene) yields 3 distinct
finders — findGameSessionsByCurrentScene /
…ByLastOpeningNarrativeScene / …ByTransitioningFromScene — no collision.
Both finders are plain, framework-free, single-query reads returning E[].

Collision fix: the lazy reverse many() entries in relation-resolver
(named from the SOURCE entity, so two FKs same source→target silently
overwrote) are REMOVED — the explicit finders are the primitive per
ADR-0038. Forward one() relations and M:N many(junction) navigation are
unchanged.

- naming.ts: reverseFinderFnName / reverseFinderInFnName / reverseFinderFkSegment
  (the cross-port naming SSOT).
- queries.ts: reverseFksFor + renderReverseFinderFns; wired into queries-file.
- reference/queries.ts (ADR-0034 scaffold-and-own copy) kept byte-identical.
- New fixtures/conformance/reverse-finders-same-pair (3 FKs to Scene + a
  string-PK FK to Player) gates the metadata cross-port; reverse-finders.test.ts
  asserts the generated finder names incl. the same-pair case.
- Regenerated golden + package-layout snapshots (additive finders; reverse
  many() blocks dropped). Registry-conformance unaffected (no new vocab).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
…-0038)

Implement the C# port of ADR-0038 / ADR-0036 Wave 4: generate reverse 1:N
navigation as explicit, typed FK query methods on the referencing entity's
query surface — NOT lazy EF Core collections.

For each FK an entity E holds (an identity.reference whose @fields names the
local FK column), the C# entity generator now emits a per-entity
<E>Queries static class with a finder pair, named by the FK FIELD
(PascalCased, single trailing `Id` dropped — the cross-port SSOT) so it is
unique within E by construction:

  Task<List<E>> Find<EPlural>By<FkField>(db, value)   → Where(e => e.<Fk> == value)
  Task<List<E>> Find<EPlural>By<FkField>In(db, values) → Where(e => values.Contains(e.<Fk>))

Both are framework-free, single-query LINQ over the generated DbSet
(AsNoTracking + Where + ToListAsync), the batched IN variant empty-guarded
for the anti-N+1 "selectin" shape. No lazy navigation, no open-session
requirement. The FK value type is the target entity's PK type with the FK
property's nullability (an optional self-FK like Node.parentId → long?).

The same-pair case (GameSession with 3 FKs to Scene) yields 3 distinct
finders — FindGameSessionsByCurrentScene /
…ByLastOpeningNarrativeScene / …ByTransitioningFromScene — no collision.

C# emits no lazy reverse 1:N collection today (only M:N junction nav via
@through, which is unchanged), so nothing to remove.

- CSharpNaming: QueriesClassName / ReverseFinderFkSegment / ReverseFinderName
  / ReverseFinderInName (the cross-port naming SSOT, idiomatic-C# PascalCase).
- ReverseFinders.cs: ReverseFk descriptor + ReverseFinderBuilder
  (For + Generate); wired into EntityGenerator (concrete, non-TPH-subtype,
  non-projection entities with ≥1 FK).
- ReverseFinderCodegenTests loads the shared
  fixtures/conformance/reverse-finders-same-pair metadata and asserts the
  three distinct GameSession finders + the single/batched shapes + FK value
  types + no collision + no lazy reverse collection.
- Regenerated the committed integration fixtures (the fitness corpus has FKs
  → new <E>Queries.g.cs; integration project compiles). Shared fixture and
  registry-conformance untouched (finders ride the existing `entity`
  generator — no new vocab, no new generator name).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
…-0038)

Implement the Python port of ADR-0038 / ADR-0036 Wave 4: generate reverse 1:N
navigation as explicit, typed FK query finders on the repository Protocol seam,
NOT lazy ORM collections — matching the TS reference and the cross-port contract.

For each FK an entity E holds (an identity.reference over an FK field referencing
T), E's repository Protocol now gains a finder pair, named by the FK FIELD
(snake_cased, single trailing `_id` dropped) so it is unique within E by
construction:

  find_<e_plural>_by_<fk_segment>(value)     → single,  WHERE fk = ?
  find_<e_plural>_by_<fk_segment>_in(values) → batched, WHERE fk IN (…) (anti-N+1)

Both return list[Any] (the E rows matching a given T id) — a plain, framework-free
single query the consumer implements, NOT a lazy relationship() (no open session,
no N+1). The same-pair case (GameSession with 3 FKs to Scene) yields 3 distinct
finders — find_game_sessions_by_current_scene /
…_by_last_opening_narrative_scene / …_by_transitioning_from_scene — no collision
and no naming attribute.

The Python port only ever emitted M:N navigation (no lazy reverse 1:N
collections), so there is nothing to remove; M:N traversal is unchanged.

- apidocs/naming.py: reverse_finder_fk_segment / reverse_finder_fn /
  reverse_finder_in_fn (the cross-port naming SSOT, idiomatic snake_case).
- router_generator.py: reverse_fks_for (derives FKs from identity.reference) +
  _emit_reverse_finders extension hook, wired into the repository Protocol; the
  existing _emit_repository_protocol override seam is left signature-stable.
- test_reverse_finders.py loads the shared fixtures/conformance/
  reverse-finders-same-pair corpus and asserts the 3 distinct GameSession finders
  + single/batched shapes + no lazy reverse collection.

Shared fixture + goldens untouched; registry-conformance unaffected (no new vocab).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
…FK finders (ADR-0038)

Implement the JVM ports (Java + Kotlin) of ADR-0038 / ADR-0036 Wave 4: generate
reverse 1:N navigation as explicit, typed FK finder methods instead of lazy
collections (no `@OneToMany`, no Exposed `referrersOn`). For each FK an entity E
holds (an `identity.reference`), E's query surface gains a finder PAIR returning
the E rows matching a given target id:

  single:  WHERE <fkColumn> = ?    -> all E rows for one target
  batched: WHERE <fkColumn> IN (…) -> anti-N+1 load of many parents

The cross-port INVARIANT is the FK-FIELD DERIVATION (PascalCase, drop a single
trailing `Id`), unique within E by construction — so the shared
`reverse-finders-same-pair` fixture's GameSession → Scene ×3 yields THREE DISTINCT
finders (findByCurrentScene / findByLastOpeningNarrativeScene /
findByTransitioningFromScene), never colliding, with NO `@reverseName` vocabulary.
The method name is idiomatic per port: a Spring Data repository finder
`findBy<FkField>` (Java; entity implied by the repo) and an Exposed DSL query
function `<E>Table.findBy<FkField>(…): Query` (Kotlin). The value type tracks the
FK column's own type (Long for Scene, String for the string-PK Player). M:N
navigation is unchanged.

- Java: SpringNaming.reverseFinderFkSegment/Name/InName SSOT;
  SpringRepositoryGenerator emits a single+batched finder per single-field
  identity.reference (value type via SpringTypeMapper).
- Kotlin: KotlinNaming reverse-finder SSOT; KotlinRelationsGenerator now also
  emits `<E>Relations.kt` for FK-holding entities (eq/inList resolve inside the
  Exposed where{} receiver — no new imports, M:N-only files stay byte-identical).
- Neither JVM port ever emitted lazy reverse collections, so this is purely
  additive (nothing to remove). Registry-conformance unaffected (no new vocab).
- Java SpringReverseFinderCodegenTest (6) + Kotlin KotlinReverseFinderCodegenTest
  (7) load the shared fixture and assert the three distinct GameSession finders +
  single/batched shapes + no collision + not-lazy.

Verified: codegen-spring 158, codegen-kotlin 265, metadata conformance 392 — all
green (the shared reverse-finders-same-pair fixture loads in the JVM conformance
runner).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
@dmealing
dmealing merged commit 86acb61 into main Jun 30, 2026
18 checks passed
@dmealing
dmealing deleted the feat/wave4-reverse-finders branch June 30, 2026 22:49
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.

2 participants