Skip to content

schema, server: a default is the value its option says, in the row as well as the request - #8

Merged
lesomnus merged 1 commit into
mainfrom
schema-defaults
Oct 17, 2026
Merged

lesomnus merged 1 commit into
mainfrom
schema-defaults

Conversation

@lesomnus

Copy link
Copy Markdown
Member

What was wrong

(orm.field).default was honoured in neither place that reads it.

  • The server's Add wrote the type's zero value into a request that left the field out, whatever the option said. default: "7" gave 0.
  • The schema wrote nothing. A default makes a field optional, and optional is nullable in ent. So a column added to a table that already had rows left those rows NULL. That reads back as the zero value in Go, but behaves differently in SQL: = '' and <> 'x' both pass NULL by.
  • A field with no default became NOT NULL with none, and the same ADD COLUMN is refused outright on a table with rows. One deployment found this: its init container would not finish after three such columns came between two of its releases.

What this does

internal/work/default.go is now the single place both generators ask what a default means:

field type default may say server Add column
string anything, as written the value .Default(value)
bool true, false, empty for false the value .Default(value)
integer, float a decimal number, empty for 0 the value .Default(value)
enum a value's name, its number, empty for 0 the value .Default(value)
bytes empty []byte{} none (ent writes a []byte default into DDL as [])
uuid key, time, message empty minted, now, nil none

A value that the field's type cannot read is refused when the code is generated, rather than silently becoming the zero value.

For a schema that uses default today

  • A scalar field that already declares default: "" gains a column DEFAULT. ent applies it at the next migration; on SQLite it does so by rebuilding the table.
  • A non-empty default that used to be ignored now takes effect. If the value cannot be read, generation stops and names the field.

Tests

  • TestADefaultIsWhatTheOptionSays: an Add that leaves the new non-zero-default fields out gets the declared values (7, true, "nobody", LEVEL_HARD by name, 0.5). A request that explicitly says zero keeps zero.
  • TestADefaultedColumnComesToATableThatHasRows: drops the columns from a table that has a row, migrates, and reads the row back in SQL. The row holds the declared values, and '' rather than NULL for an empty-string default.
  • TestADefaultSaysAValueOrIsRefused: the parser, including every refusal.
  • Mutation check: with the schema's Default removed, or the server's value replaced by zero, the matching test fails.
  • go test ./... passes in the root module, runtime, and internal/apptest, on SQLite and on Postgres 17 (APPTEST_POSTGRES).

🤖 Generated with Claude Code

… well as the request

`(orm.field).default` was read in one place, and not read there either.
The server wrote the zero value into an Add that left a field out
whatever the option said -- `default: "7"` gave 0 -- and the schema wrote
nothing at all. A default makes a field optional and optional is nullable
in ent, so a column added to a table that already had rows left them
NULL: the zero value read back into Go, and nothing like it in SQL. A
field with no default was NOT NULL with none, and the same ADD COLUMN was
refused outright -- which is how a deployment found this: its init
container would not finish after three such columns came between two of
its releases.

`work.Default` is the one answer both places ask now. A value -- a string
as written, a number in decimal, a bool, an enum by name or number, empty
for the zero value -- is what the server writes and the column's DEFAULT
(`.Default(v)`), so a row that predates the column holds it. Bytes are
server-only, since ent writes a `[]byte` default into DDL as `[]`. A key
is still minted and a time is still now. A default the type cannot read
is refused at generation rather than read as the zero value.

For a schema that already declares `default: ""` on a scalar field, the
column gains a DEFAULT, which ent applies at the next migration (on
SQLite by rebuilding the table). A non-empty default that used to be
ignored is now the value.

`TestADefaultIsWhatTheOptionSays` and
`TestADefaultedColumnComesToATableThatHasRows` -- which drops the columns
off a table with rows and has the migration bring them back -- run on
SQLite and, under APPTEST_POSTGRES, on Postgres 17;
`TestADefaultSaysAValueOrIsRefused` is the parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lesomnus
lesomnus merged commit 693e48f into main Oct 17, 2026
2 checks passed
lesomnus added a commit to lesomnus/roster that referenced this pull request Oct 17, 2026
…arries it

Found rehearsing an upgrade -- a database the deployed release made,
brought up by main -- which no gate here did. The one deployment's init
container would not have finished: three columns came between its two
releases, each NOT NULL and none with a default (`connection.subject_claim`
from #91, `connection.provisions` and `holder.directory` from #92), and
SQLite refuses `ADD COLUMN … NOT NULL` with nothing to put in the rows
already there -- *Cannot add a NOT NULL column with default value NULL*
-- as Postgres does for any table that is not empty.

Each declares its default now, as `(orm.field).default`: the empty
string, false, the empty string -- what every row already reads them as,
since proto3 has no absent scalar. protoc-gen-orm-ent gives a declared
default to the column as well as to the server since
protobuf-orm/protoc-gen-orm-ent#8, which this bumps to; before it a
default was the server's alone, and a defaulted column came to a table
with rows as a nullable one holding NULL -- the zero value in Go, and not
in SQL.

What else moves with it:

- A defaulted field is optional in its `Add`, so `subject_claim`,
  `provisions` and `directory` take pointers in their `*AddRequest`
  builders; the declared connection in `cmd/resources.go` and the tests
  say so.
- Two existing columns gain a DEFAULT from fields that already declared
  one: payday's `audit.domain`, and `tenant.hold`. SQLite applies that by
  rebuilding the table, once, at the first start on this.
- `wasm/schema/schema.sql`, regenerated.

`TestAColumnComesToATableThatHasRows` takes the three columns off tables
with rows and brings them back with plain ent -- on SQLite, and under
PDTEST_POSTGRES on Postgres 17 -- reads the rows in SQL, and asks that
the next migration plan nothing. With the generator before #8 it fails
on the NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lesomnus added a commit to lesomnus/roster that referenced this pull request Oct 17, 2026
…arries it

Found rehearsing an upgrade -- a database the deployed release made,
brought up by main -- which no gate here did. The one deployment's init
container would not have finished: three columns came between its two
releases, each NOT NULL and none with a default (`connection.subject_claim`
from #91, `connection.provisions` and `holder.directory` from #92), and
SQLite refuses `ADD COLUMN … NOT NULL` with nothing to put in the rows
already there -- *Cannot add a NOT NULL column with default value NULL*
-- as Postgres does for any table that is not empty.

Each declares its default now, as `(orm.field).default`: the empty
string, false, the empty string -- what every row already reads them as,
since proto3 has no absent scalar. protoc-gen-orm-ent gives a declared
default to the column as well as to the server since
protobuf-orm/protoc-gen-orm-ent#8, which this bumps to; before it a
default was the server's alone, and a defaulted column came to a table
with rows as a nullable one holding NULL -- the zero value in Go, and not
in SQL.

What else moves with it:

- A defaulted field is optional in its `Add`, so `subject_claim`,
  `provisions` and `directory` take pointers in their `*AddRequest`
  builders; the declared connection in `cmd/resources.go` and the tests
  say so.
- Two existing columns gain a DEFAULT from fields that already declared
  one: payday's `audit.domain`, and `tenant.hold`. SQLite applies that by
  rebuilding the table, once, at the first start on this.
- `buf breaking` checks cardinality as the wire and JSON see it
  (`FIELD_WIRE_JSON_COMPATIBLE_CARDINALITY`) rather than as the generated
  accessor does (`FIELD_SAME_CARDINALITY`): the three requests' fields
  went from implicit to explicit presence, which changes a Go type and
  neither the bytes nor the JSON. Singular to repeated still fails.
- `wasm/schema/schema.sql`, regenerated.

`TestAColumnComesToATableThatHasRows` takes the three columns off tables
with rows and brings them back with plain ent -- on SQLite, and under
PDTEST_POSTGRES on Postgres 17 -- reads the rows in SQL, and asks that
the next migration plan nothing. With the generator before #8 it fails
on the NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lesomnus added a commit to lesomnus/roster that referenced this pull request Oct 17, 2026
…arries it

Found rehearsing an upgrade -- a database the deployed release made,
brought up by main -- which no gate here did. The one deployment's init
container would not have finished: three columns came between its two
releases, each NOT NULL and none with a default (`connection.subject_claim`
from #91, `connection.provisions` and `holder.directory` from #92), and
SQLite refuses `ADD COLUMN … NOT NULL` with nothing to put in the rows
already there -- *Cannot add a NOT NULL column with default value NULL*
-- as Postgres does for any table that is not empty.

Each declares its default now, as `(orm.field).default`: the empty
string, false, the empty string -- what every row already reads them as,
since proto3 has no absent scalar. protoc-gen-orm-ent gives a declared
default to the column as well as to the server since
protobuf-orm/protoc-gen-orm-ent#8, which this bumps to; before it a
default was the server's alone, and a defaulted column came to a table
with rows as a nullable one holding NULL -- the zero value in Go, and not
in SQL.

What else moves with it:

- A defaulted field is optional in its `Add`, so `subject_claim`,
  `provisions` and `directory` take pointers in their `*AddRequest`
  builders; the declared connection in `cmd/resources.go` and the tests
  say so.
- Two existing columns gain a DEFAULT from fields that already declared
  one: payday's `audit.domain`, and `tenant.hold`. SQLite applies that by
  rebuilding the table, once, at the first start on this.
- `buf breaking` checks cardinality as the wire and JSON see it
  (`FIELD_WIRE_JSON_COMPATIBLE_CARDINALITY`) rather than as the generated
  accessor does (`FIELD_SAME_CARDINALITY`): the three requests' fields
  went from implicit to explicit presence, which changes a Go type and
  neither the bytes nor the JSON. Singular to repeated still fails.
- `wasm/schema/schema.sql`, regenerated.

`TestAColumnComesToATableThatHasRows` takes the three columns off tables
with rows and brings them back with plain ent -- on SQLite, and under
PDTEST_POSTGRES on Postgres 17 -- reads the rows in SQL, and asks that
the next migration plan nothing. With the generator before #8 it fails
on the NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lesomnus added a commit to lesomnus/roster that referenced this pull request Oct 17, 2026
…arries it

Found rehearsing an upgrade -- a database the deployed release made,
brought up by main -- which no gate here did. The one deployment's init
container would not have finished: three columns came between its two
releases, each NOT NULL and none with a default (`connection.subject_claim`
from #91, `connection.provisions` and `holder.directory` from #92), and
SQLite refuses `ADD COLUMN … NOT NULL` with nothing to put in the rows
already there -- *Cannot add a NOT NULL column with default value NULL*
-- as Postgres does for any table that is not empty.

Each declares its default now, as `(orm.field).default`: the empty
string, false, the empty string -- what every row already reads them as,
since proto3 has no absent scalar. protoc-gen-orm-ent gives a declared
default to the column as well as to the server since
protobuf-orm/protoc-gen-orm-ent#8, which this bumps to; before it a
default was the server's alone, and a defaulted column came to a table
with rows as a nullable one holding NULL -- the zero value in Go, and not
in SQL.

What else moves with it:

- A defaulted field is optional in its `Add`, so `subject_claim`,
  `provisions` and `directory` take pointers in their `*AddRequest`
  builders; the declared connection in `cmd/resources.go` and the tests
  say so.
- Two existing columns gain a DEFAULT from fields that already declared
  one: payday's `audit.domain`, and `tenant.hold`. SQLite applies that by
  rebuilding the table, once, at the first start on this.
- `buf breaking` checks cardinality as the wire and JSON see it
  (`FIELD_WIRE_JSON_COMPATIBLE_CARDINALITY`) rather than as the generated
  accessor does (`FIELD_SAME_CARDINALITY`): the three requests' fields
  went from implicit to explicit presence, which changes a Go type and
  neither the bytes nor the JSON. Singular to repeated still fails.
- `wasm/schema/schema.sql`, regenerated.

`TestAColumnComesToATableThatHasRows` takes the three columns off tables
with rows and brings them back with plain ent -- on SQLite, and under
PDTEST_POSTGRES on Postgres 17 -- reads the rows in SQL, and asks that
the next migration plan nothing. With the generator before #8 it fails
on the NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lesomnus added a commit that referenced this pull request Oct 17, 2026
The pin was #9's head while it was open; it is master now, which has #9
and #8 beside it. Nothing it generates moved.

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