Skip to content

feat(python-codegen): @column field naming, server-default exprs, --entities allowlist - #79

Merged
dmealing merged 2 commits into
mainfrom
feat/py-codegen-column-server-default-entities
Jun 26, 2026
Merged

feat(python-codegen): @column field naming, server-default exprs, --entities allowlist#79
dmealing merged 2 commits into
mainfrom
feat/py-codegen-column-server-default-entities

Conversation

@dmealing

Copy link
Copy Markdown
Member

Motivation

Surfaced by generating Python record models from the same entity metadata that drives the TypeScript Drizzle layer (one cross-language source of truth). Three consumer-facing gaps blocked that, all in the Python port's entity_model generator + CLI — no metamodel/registry change, so no cross-port conformance reconciliation is needed.

Changes

1. @column-aware Pydantic field naming. The generator emitted field.name verbatim, so an entity authored camelCase for the TS port (callPurpose + @column: call_purpose) produced camelCase Python fields that don't match the DB columns. The Pydantic field name is now field.column ?? field.name — one entity feeds both languages idiomatically (camelCase TS property, snake_case Python field = the physical column). Backward-compatible: a model with no @column emits byte-identically.

2. Server-side default expressions are no longer rendered as Python literal defaults. A string @default matching the SQL-expression patterns (now, now(), current_timestamp/date/time, anything ending in () such as gen_random_uuid()) is DB-filled — so id: uuid.UUID = "gen_random_uuid()" was invalid. Such a field now carries no Python default (keeps its required/optional shape). Literal defaults (bool/int/str/enum) are unchanged. Mirrors the TS column-mapper.ts SQL_EXPR_PATTERNS so both ports agree on what counts as an expression.

3. metaobjects gen --entities <names> allowlist. The whole model is still loaded (so extends bases and @objectRef VOs resolve), but only the named entities are emitted — generate a subset of a shared model without splitting the metadata. Wired through gen and verify --codegen; run_gen already supported entity_filter.

Tests

New regression tests in test_entity_model.py (column override, fallback, server-default-expr vs literal) and test_cli.py (entities allowlist emit + verify in sync). Full tests/codegen suite green except two pre-existing test_cli_staleness_nudge failures unrelated to this change (they fail on clean origin/main too).

Also bumps the Python port to 0.12.0 (backward-compatible minor; aligns with the 0.12.x line).

dmealing and others added 2 commits June 26, 2026 05:26
…ntities allowlist

Three consumer-facing gaps surfaced by generating Python record models from the SAME
entity metadata that drives the TypeScript Drizzle layer:

1. @column-aware Pydantic field naming. The entity/value generator emitted `field.name`
   verbatim, so a model authored camelCase for the TS port (`callPurpose` +
   `@column: call_purpose`) produced camelCase Python fields that don't match the DB
   columns. The Pydantic field name is now `@column` when present, else `field.name` —
   one entity feeds both languages idiomatically (camelCase TS property, snake_case
   Python field = the physical column). Backward-compatible: no @column emits identically.

2. Server-side default EXPRESSIONS are no longer rendered as Python literal defaults.
   A string @default matching the SQL-expression patterns (now, now(),
   current_timestamp/date/time, anything ending in `()` such as gen_random_uuid()) is
   DB-filled, so emitting `id: uuid.UUID = "gen_random_uuid()"` was wrong. Such a field
   now carries no Python default (keeps its required/optional shape); literal defaults
   (bool/int/str/enum) are unchanged. Mirrors the TS column-mapper's SQL_EXPR_PATTERNS
   so both ports agree on what counts as an expression.

3. `metaobjects gen --entities <names>` allowlist. The whole model is still loaded (so
   `extends` bases and @objectref VOs resolve), but only the named entities are emitted
   — generate a subset of a shared model without splitting the metadata. Wired through
   `gen` and `verify --codegen` (run_gen already supported entity_filter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Minor bump for the backward-compatible codegen features in the previous commit
(@column field naming, server-default expressions, --entities allowlist) and to
align the Python port with the 0.12.x line. pyproject + uv.lock self-version only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dmealing
dmealing merged commit 8352079 into main Jun 26, 2026
29 checks passed
@dmealing
dmealing deleted the feat/py-codegen-column-server-default-entities branch June 26, 2026 12:57
dmealing added a commit that referenced this pull request Aug 29, 2026
…d name

The generated `<Entity>` read model named its Pydantic field
`@column or field.name`. Everything else in the port keys by `field.name`:
the FR-036 `<Entity>Create` / `<Entity>Patch` models, the generated router
(which stamps `dto["createdAt"]`), and the runtime — `ObjectManager`
builds `{_column_of(f): f.name}` on every read and every RETURNING clause,
commented "for cross-port row-shape parity". So one generated module
disagreed with itself about what a field is called.

The read model's stated reason — "the Python model field IS the column, so
it binds straight to the row" — was never true of that runtime, which
hands back field-keyed rows by design. The rationale it shipped under
(#79) was different and narrower: idiomatic snake_case for a Python
consumer, which held only because the example's `@column` happened to be
the snake_case of its field name.

It also leaked a free-form name. `@column` is an arbitrary override, so
`callPurpose` + `@column: purpose_code` emitted a Pydantic field
`purpose_code` — neither the wire name nor anything derived from the field
name, just the DB column surfacing in the API shape.

Idiomatic snake_case is a real goal and this is the wrong lever for it:
`columnNamingStrategy` applied to `field.name` is the right one, and this
port could not select it either. That is the next commit.

The replaced test is rewritten rather than deleted — it pinned the
behaviour, so it has to state the opposite and say why — and its `@column`
is deliberately no longer the snake_case of the field name, since that
coincidence is what made the old behaviour look correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NynrRND6ZUwGvq3ZUTCfxG
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