Found while implementing #16091 (domain:cli), which repaired the character-column WIDTH divergence between os generate migration and driver-sql. This is the same producer pair and the same "the generator should reproduce the platform" question, but a different column property — nullability and default rather than type/width — with a different governing rule (ADR-0113), so it was deliberately kept out of that diff and filed instead.
⛔ Not graded or prioritised here. No labels applied, no assignee — this is for triage.
Driven, not read
Measured on a private PostgreSQL 16.13 cluster, all three producers run from ONE object and their columns read back out of information_schema.columns: driver-sql through its own initObjects, os generate migration --format sql through db.raw of the emitted DDL, and os generate migration (typescript, the default format) by importing the emitted module and calling up(db).
field driver sqlgen sqlgen==tsgen verdict
f_plain null=YES default=- null=YES default=- yes agree
f_required null=YES default=- null=NO default=- yes DIVERGED
f_storage_notnull null=NO default=- null=YES default=- yes DIVERGED
f_required_and_st null=NO default=- null=NO default=- yes agree
f_default null=YES default='hello'::text null=YES default=- yes DIVERGED
f_default_required null=YES default='hello'::text null=NO default=- yes DIVERGED
diverged: 4 of 6
Both generators agree with each other and disagree with the platform in the same way, so this is one defect with two producers rather than two defects.
Three causes
1. required is read as a column constraint, and the driver stopped reading it that way.
createColumn's tail says so in its own words:
ADR-0113: the physical NOT NULL comes from the EXPLICIT storage constraint, not from required — required is the write-time contract enforced by the record validator at the engine seam, and binding the DDL to it made every post-deploy tightening a destructive migration.
The line itself is if ((field as { storage?: { notNull?: boolean } }).storage?.notNull) col.notNullable();. Both generators still key on required: generateMigrationSql appends ' NOT NULL' when fieldDef.required, and generateMigrationTs emits .notNullable() for the same. So a generated table constrains a column the platform leaves open — which is exactly the shape #15521 ruled against for the audit-stamp columns and corrected there, one column class to the left.
2. storage.notNull is never read at all. The reverse direction, and the quieter one: an author who declares the constraint the platform actually honours gets a NULLABLE column from both generators. ADR-0113's own conversion (field-required-notnull-explicit) writes storage.notNull into pre-protocol-17 sources, so this is not a hypothetical spelling.
3. defaultValue produces no column DEFAULT in either generator. SqlDriver.applyDeclaredColumnDefault is the single place a defaultValue becomes DDL, and it emits one for any scalar, non-runtime-token value — measured above as default='hello'::text. Neither generator emits anything, so a row inserted out of band into a generated table gets NULL where the platform's own table would have supplied the declared value.
Why it was not repaired under #16091
That card's ruled class is the character column — which type and how wide — and its four repaired members are all answers createColumn's type switch gives. Nullability and default are decided AFTER that switch, by two different helpers, under an ADR that made the required question a settled decision rather than a value to correct. Repairing them there would have widened the verification surface past the card's own class and past its pin.
What a fix has to answer first
⛔ Not answered here. #15521's ruling ("the generator follows the driver") reaches the audit columns and, via #16091, the character columns; whether it reaches this pair is the same question one property over, and the answer for required in particular is not free — a scaffold that emits no NOT NULL for a field the author marked required will read to that author as the scaffold losing their declaration, even though the write seam still enforces it. That is a decision about what a scaffold is for, and it looks like the same decision ADR-0113 already took for the driver.
Reproducing
The probe is one object with the six fields above, driven through all three producers into a live PostgreSQL 16.13 and read back from information_schema.columns. A postgresql-16 server package is present in the standard dev container, so a private cluster can be stood up with initdb plus pg_ctl without any external service.
Found while implementing #16091 (
domain:cli), which repaired the character-column WIDTH divergence betweenos generate migrationanddriver-sql. This is the same producer pair and the same "the generator should reproduce the platform" question, but a different column property — nullability and default rather than type/width — with a different governing rule (ADR-0113), so it was deliberately kept out of that diff and filed instead.⛔ Not graded or prioritised here. No labels applied, no assignee — this is for triage.
Driven, not read
Measured on a private PostgreSQL 16.13 cluster, all three producers run from ONE object and their columns read back out of
information_schema.columns:driver-sqlthrough its owninitObjects,os generate migration --format sqlthroughdb.rawof the emitted DDL, andos generate migration(typescript, the default format) by importing the emitted module and callingup(db).Both generators agree with each other and disagree with the platform in the same way, so this is one defect with two producers rather than two defects.
Three causes
1.
requiredis read as a column constraint, and the driver stopped reading it that way.createColumn's tail says so in its own words:The line itself is
if ((field as { storage?: { notNull?: boolean } }).storage?.notNull) col.notNullable();. Both generators still key onrequired:generateMigrationSqlappends' NOT NULL'whenfieldDef.required, andgenerateMigrationTsemits.notNullable()for the same. So a generated table constrains a column the platform leaves open — which is exactly the shape #15521 ruled against for the audit-stamp columns and corrected there, one column class to the left.2.
storage.notNullis never read at all. The reverse direction, and the quieter one: an author who declares the constraint the platform actually honours gets a NULLABLE column from both generators. ADR-0113's own conversion (field-required-notnull-explicit) writesstorage.notNullinto pre-protocol-17 sources, so this is not a hypothetical spelling.3.
defaultValueproduces no column DEFAULT in either generator.SqlDriver.applyDeclaredColumnDefaultis the single place adefaultValuebecomes DDL, and it emits one for any scalar, non-runtime-token value — measured above asdefault='hello'::text. Neither generator emits anything, so a row inserted out of band into a generated table gets NULL where the platform's own table would have supplied the declared value.Why it was not repaired under #16091
That card's ruled class is the character column — which type and how wide — and its four repaired members are all answers
createColumn's type switch gives. Nullability and default are decided AFTER that switch, by two different helpers, under an ADR that made therequiredquestion a settled decision rather than a value to correct. Repairing them there would have widened the verification surface past the card's own class and past its pin.What a fix has to answer first
⛔ Not answered here. #15521's ruling ("the generator follows the driver") reaches the audit columns and, via #16091, the character columns; whether it reaches this pair is the same question one property over, and the answer for
requiredin particular is not free — a scaffold that emits noNOT NULLfor a field the author marked required will read to that author as the scaffold losing their declaration, even though the write seam still enforces it. That is a decision about what a scaffold is for, and it looks like the same decision ADR-0113 already took for the driver.Reproducing
The probe is one object with the six fields above, driven through all three producers into a live PostgreSQL 16.13 and read back from
information_schema.columns. Apostgresql-16server package is present in the standard dev container, so a private cluster can be stood up withinitdbpluspg_ctlwithout any external service.