Skip to content

Generated CRUD ignores field.timestamp @autoSet: onCreate|onUpdate: every adopter hand-stamps now() on created_at/updated_at #203

Description

@dmealing

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:

  1. Duplicated now() stamping in every writable repository.
  2. A latent lost-update-style bug: a full-row update(model) rewrites created_at (a write-once column) from whatever stale value the model carries, unless the hand-written code remembers to special-case it.
  3. 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:

  • insert stamps every onCreate and onUpdate column with now() — the model's value is ignored (a fresh row's updated_at equals its created_at).
  • update(model) stamps onUpdate columns with now() and skips onCreate columns entirely — it never rewrites created_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) { … } stamps onUpdate before running the caller's partial-update block, so a partial update still bumps updated_at.
  • insertPreserving(model) — an escape hatch that writes the @autoSet columns verbatim from the model, for import / restore / replication paths that must keep the original timestamps. Only emitted for entities that actually declare @autoSet fields.
  • The generated column mapper carries stampAutoSet: Boolean = true / includeOnCreate: Boolean = true flags, 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions