feat(python-codegen): @column field naming, server-default exprs, --entities allowlist - #79
Merged
Merged
Conversation
…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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_modelgenerator + CLI — no metamodel/registry change, so no cross-port conformance reconciliation is needed.Changes
1.
@column-aware Pydantic field naming. The generator emittedfield.nameverbatim, 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 nowfield.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@columnemits byte-identically.2. Server-side default expressions are no longer rendered as Python literal defaults. A string
@defaultmatching the SQL-expression patterns (now,now(),current_timestamp/date/time, anything ending in()such asgen_random_uuid()) is DB-filled — soid: 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 TScolumn-mapper.tsSQL_EXPR_PATTERNSso both ports agree on what counts as an expression.3.
metaobjects gen --entities <names>allowlist. The whole model is still loaded (soextendsbases and@objectRefVOs resolve), but only the named entities are emitted — generate a subset of a shared model without splitting the metadata. Wired throughgenandverify --codegen;run_genalready supportedentity_filter.Tests
New regression tests in
test_entity_model.py(column override, fallback, server-default-expr vs literal) andtest_cli.py(entities allowlist emit + verify in sync). Fulltests/codegensuite green except two pre-existingtest_cli_staleness_nudgefailures unrelated to this change (they fail on cleanorigin/maintoo).Also bumps the Python port to
0.12.0(backward-compatible minor; aligns with the 0.12.x line).