Repository navigation
Generated CRUD ignores field.timestamp @autoSet: onCreate|onUpdate: every adopter hand-stamps now() on created_at/updated_at #203
Description
Summary
field.timestamp accepts an @autoSet: onCreate | onUpdate marker — the canonical way to declare "the CRUD layer owns this timestamp, the caller does not." But no port's code generator emits the stamping logic for it. The generated insert/update leave the column unset (or force the caller to supply a value), so every adopter re-implements now() stamping by hand in a hand-written repository — which defeats the purpose of generated CRUD and reintroduces exactly the drift codegen exists to remove.
In one adopter, this single un-honored declaration was the root cause behind ~32 of 62 hand-written repositories (the large majority of the hand-written repository code by line count). @autoSet is declared once on a base entity's createdAt/updatedAt and inherited by nearly every entity, so honoring it in codegen collapses that entire class of hand-written boilerplate.
Why this belongs in core codegen
created_at / updated_at stamping is pattern-derivable from the metadata (@autoSet: onCreate|onUpdate on a field.timestamp) — exactly like FK finders and CRUD, which codegen already owns. It's the same "if it's derivable from metadata, generate it, don't hand-write it" principle. Leaving it to each adopter guarantees three recurring failures:
- Duplicated
now()stamping in every writable repository. - A latent lost-update-style bug: a full-row
update(model)rewritescreated_at(a write-once column) from whatever stale value the model carries, unless the hand-written code remembers to special-case it. - Inconsistent behavior across ports (a lagging port silently stamps differently, or not at all).
Reference implementation (one adopter's Kotlin/Exposed generator)
Sharing the contract an adopter settled on, in case it's a useful starting point. For a field.timestamp @autoSet: onCreate|onUpdate:
insertstamps everyonCreateandonUpdatecolumn withnow()— the model's value is ignored (a fresh row'supdated_atequals itscreated_at).update(model)stampsonUpdatecolumns withnow()and skipsonCreatecolumns entirely — it never rewritescreated_at. (Omitting this special-case is the latent bug in FR-019: shared + @provided enums (all five ports) + typecheck-gate repair #2 above.)patch(id) { … }stampsonUpdatebefore running the caller's partial-update block, so a partial update still bumpsupdated_at.insertPreserving(model)— an escape hatch that writes the@autoSetcolumns verbatim from the model, for import / restore / replication paths that must keep the original timestamps. Only emitted for entities that actually declare@autoSetfields.- The generated column mapper carries
stampAutoSet: Boolean = true/includeOnCreate: Boolean = trueflags, so the base CRUD and the escape hatch share one definition of the column list rather than duplicating it.
The now() expression is keyed off the column's temporal type, not the parameter's, so this generalizes to any temporal type, not just Instant.
Proposal
Make @autoSet stamping core codegen vocabulary: the generated insert / update / patch honor onCreate / onUpdate on field.timestamp (ideally any temporal type), with an insertPreserving-style verbatim escape hatch, emitted consistently across all ports so a lagging port can't silently diverge on stamping behavior.
Related
Same "pattern-derivable ⇒ generate it" theme as #198 (no generated partial/patch update). Distinct from #197 (identity PKs) which was retired as invalid — this one is a genuine cross-port codegen gap that every adopter currently pays for by hand.
Activity
docs(#203): implementation plan for @autoset CRUD stamping (cross-port)
feat(#203): Python ObjectManager @autoset CRUD stamping
feat(#203): C# generated-routes @autoset CRUD stamping
feat(#203): Java Spring @autoset CRUD stamping
feat(#203): Kotlin Exposed repository @autoset CRUD stamping
feat(#203): TS insertPreserving escape hatch (@autoset verbatim insert)
docs(features): generated mutations — partial patch + @autoset stampi…
dmealing commentedon Jul 23, 2026
Closed — generated CRUD honors `field.timestamp @autoset: onCreate|onUpdate` across all five ports.
What shipped (per-port, each with its own tests)
Contract (per the issue): create stamps every `onCreate` and `onUpdate` column with `now()` (model value ignored — a fresh row's `updatedAt` equals its `createdAt`); update / patch stamps `onUpdate` and skips `onCreate` (never rewrites `createdAt` — the latent lost-update bug); `insertPreserving` writes the `@autoSet` columns verbatim (import/restore), emitted only for entities that declare `@autoSet` fields. Documented in `docs/features/generated-mutations.md` (`d8d8e89d`).
Gated per port (TS `auto-set-zod.test.ts` / `auto-set-insert-preserving.test.ts` / `metadata/auto-set.test.ts`, and the equivalent per-port suites). The one remaining item — a shared cross-port api-contract parity gate (plan step 3) — is additional cross-port-drift assurance on top of the per-port gating, tracked as #229.
feat(codegen-kotlin): generated Spring controller stamps @autoset (AD…
feat(python-codegen): generated FastAPI router stamps @autoset (ADR-0…
test(api-contract): cross-port @autoset stamping gate (ADR-0045, #203/#…
fix(python-codegen): PATCH cannot mutate a write-once @autoset column…
docs(spec): TPH + @autoset controller stamping — cross-port design (#203
docs(plan): TPH + @autoset controller stamping implementation plan (#203
feat(codegen-kotlin): TPH per-subtype controller stamps @autoset (ADR…
feat(python-codegen): TPH per-subtype router stamps @autoset (ADR-0045,
feat(codegen-spring): TPH per-subtype controller stamps @autoset (ADR…
fix(codegen-spring): align TPH @autoset controller gate to base (revi…
test(api-contract): cross-port TPH @autoset stamping gate (ADR-0045, #…
docs(spec): document subtype-own @autoset non-goal + resolved risks (…
fix(codegen-spring): vanilla <Entity>Patch excludes write-once @autoset…
fix(integration-tests): vanilla generated-lane repo double matches se…
chore(release): PyPI 0.19.4 · Maven Central 7.11.3 — TPH @autoset sta…
docs(plan): amend the TS int-backed-enum persistence plan for the post-…
test(integration): TPH + int-backed enums against real Postgres (Task 9)