Skip to content

fix(codegen-kotlin): fold TPH discriminator subtypes into the base (no dead per-subtype artifacts)#180

Merged
dmealing merged 1 commit into
mainfrom
fix/kotlin-tph-subtype-fold-codegen
Jul 6, 2026
Merged

fix(codegen-kotlin): fold TPH discriminator subtypes into the base (no dead per-subtype artifacts)#180
dmealing merged 1 commit into
mainfrom
fix/kotlin-tph-subtype-fold-codegen

Conversation

@dmealing

@dmealing dmealing commented Jul 6, 2026

Copy link
Copy Markdown
Member

What

A TPH (table-per-hierarchy) discriminator base in the Kotlin port already emitted the union table + data class + discriminator enum + polymorphic controller, and the controller generator skipped subtypes — but five other generation paths still emitted dead per-subtype artifacts for each subtype, some of them non-compiling:

Generator Dead/wrong per-subtype output
KotlinExposedTableGenerator <Sub>Table mapping the same physical table with a partial column set; and buildGlobalFkMap fanned a base's inherited to-many composition into phantom per-subtype inverse FKs → references a folded-away <Sub>Table (compile break)
KotlinEntityGenerator dead <Sub> data class + a spurious re-emit of the inherited discriminator enum
KotlinFilterAllowlistGenerator dead <Sub>FilterAllowlist
KotlinValidatorGenerator registry entry importing the missing <Sub>Table (compile break)
KotlinRelationsGenerator <Sub>Relations referencing the missing <Sub>Table (compile break)

Fix

Every entity-iterating generator now skips KotlinTphPlan.isTphSubtype (mirroring the controller's existing skip). Because a subtype folds into the base, KotlinEntityGenerator also now emits the enum class for any subtype-only field.enum it folds into the base union (owner = base) — else the union references an enum no file defines.

Brings Kotlin in line with the Java (codegen-spring) port, which already folds TPH subtypes into the base.

Gating

  • Snapshot — the entity-with-tph fixture gains a to-many composition (Auth.lines→AuthLine) + validator/relations generators; byte-gates the five skips + the buildGlobalFkMap fold (reverting any one re-emits a per-subtype artifact).
  • Unit compile — a new KotlinOutputCompilesTest case compile-proves the folded-subtype-enum path.
  • Integration compile — a new TphFullSuiteCompilesTest compiles the full generator suite (Exposed + Spring on the classpath) over a discriminator base owning a composition — the compile gate the byte-compare can't give (this is what would have caught the buildGlobalFkMap break).

All green: codegen-kotlin suite 271/0, TphFullSuiteCompilesTest 1/0, TphGeneratedApiContractConformanceTest 5/0.

Scoped out

A pre-existing, non-TPH bug — the generated Kotlin controller emits non-compiling enum-filter code for @filterable field.enum columns (wrong enum name + String→enum cast) — is filed as #179; this PR keeps its snapshot fixture's subtype enum off the controller surface.

Two rounds of code-review + code-simplify informed this change (the second round found the two compile-breaks above).

🤖 Generated with Claude Code

…no dead per-subtype artifacts

A TPH (table-per-hierarchy) discriminator base already emitted the union table +
data class + discriminator enum + polymorphic controller, and the controller
generator skipped subtypes — but five other generation paths still emitted dead,
partly non-compiling per-subtype artifacts for each subtype:

- KotlinExposedTableGenerator emitted <Sub>Table mapping the SAME physical table
  with a partial column set (a footgun), AND its back-FK inference
  (buildGlobalFkMap) fanned a base's inherited to-many composition into phantom
  per-subtype inverse FKs (<Sub>Table.id references) — a compile break once the
  tables are folded away.
- KotlinEntityGenerator emitted a dead <Sub> data class that re-emitted the
  inherited discriminator enum under a spurious name.
- KotlinFilterAllowlistGenerator / KotlinValidatorGenerator / KotlinRelationsGenerator
  emitted dead <Sub> allowlists / registry entries / relation helpers, the latter
  two referencing the now-missing <Sub>Table (compile breaks).

Fix: every entity-iterating generator now skips KotlinTphPlan.isTphSubtype (mirroring
the controller's existing skip). Because a subtype folds into the base, the base
union data class must also emit the enum class for any subtype-only field.enum it
folds in (KotlinEntityGenerator now emits enum files over collectSubtypeFields,
owner = base) — else the union references an enum no file defines.

This brings Kotlin into line with the Java (codegen-spring) port, which already
folds TPH subtypes into the base.

Gated:
- Expanded entity-with-tph snapshot fixture (a to-many composition Auth.lines→AuthLine
  + validator/relations generators) byte-gates the five structural skips + the
  buildGlobalFkMap fold — reverting any one re-emits a per-subtype artifact.
- New KotlinOutputCompilesTest case compile-proves the folded-subtype-enum path
  (a subtype field.enum → the base union compiles).
- New integration TphFullSuiteCompilesTest compiles the FULL generator suite
  (Exposed + Spring on the classpath) over a discriminator base owning a
  composition — the compile gate the snapshot byte-compare can't provide.

Scoped out: a pre-existing, non-TPH bug where the generated Kotlin controller emits
non-compiling enum-filter code for @filterable field.enum columns (wrong enum name +
String→enum cast) — filed as #179; this change keeps its snapshot fixture's subtype
enum off the controller surface.

Two rounds of code review + simplify informed this change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew1XfYSbEAezxjs9opynAe
@dmealing
dmealing merged commit d2a87af into main Jul 6, 2026
1 check passed
@dmealing
dmealing deleted the fix/kotlin-tph-subtype-fold-codegen branch July 6, 2026 17:23
dmealing added a commit that referenced this pull request Jul 6, 2026
…num columns (#179) (#181)

An Exposed enum column is typed `Column<Enum>`, but the generated Spring controller
emitted `Table.col eq (p.value as <BareEnum>)` for a @filterable field.enum — an
unresolved un-prefixed enum type name AND a String→enum cast (the filter-value coercer
produces a String). So any entity with a filterable enum column produced a
non-compiling controller. Pre-existing; surfaced by the TPH subtype-fold work (#180),
which is the first filterable enum to reach a Kotlin controller.

Fix: filter an enum column by its STORED STRING via `CAST(col AS text)`
(`col.castTo<String>(TextColumnType())`) — matching every other port's string-band
enum-filter semantics (FilterOps grants enum the string ops eq/ne/in/like/isNull; the
column stores the member name as VARCHAR). columnElementType returns "String" for an
enum (the predicate value type the coercer already produces); the WHERE arm wraps the
column in the cast for eq/ne/in/like (isNull stays on the raw column); the castTo /
TextColumnType imports are added conditionally — on BOTH the vanilla and the TPH
controller import blocks — only when a filterable enum column is present, so enum-free
controllers stay byte-identical.

Gated:
- EnumFilterControllerRunTest compiles the generated controller AND runs eq/ne/in/like
  filters against H2 (PostgreSQL mode) over MockMvc, asserting the right rows.
- TphFullSuiteCompilesTest gains a non-discriminator @filterable enum on a subtype
  (folds into the union), compile-gating the TPH controller's enum-import block.

Two rounds of code review + simplify informed this change (the second caught the TPH
import block, which shares emitFilterPipeline but has its own import set).


Claude-Session: https://claude.ai/code/session_01Ew1XfYSbEAezxjs9opynAe

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dmealing added a commit that referenced this pull request Jul 6, 2026
…filter controller (#179) (Maven Central)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew1XfYSbEAezxjs9opynAe
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.

1 participant