Repository navigation
TML-3537: Changing a value object or a variant is a migration, and Postgres checks value object and discriminator columns - #30869
Open
wmadden-electric wants to merge 105 commits into
Open
wmadden-electric wants to merge 105 commits into
wmadden-electric wants to merge 105 commits into
Conversation
…ch briefs Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…he storage changed When the storage hash changes but the planner plans nothing, migration plan writes the package with an empty ops.json and names the storage entities that changed (changedStorageEntities in --json). db migrate and db update move the marker. Only a first migration with no operations is still refused. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…dinate EntityCoordinate moves to @internal/contract, so the foundation can use it. ValueObjectFieldType is EntityCoordinate<'storage', 'valueObject'> with kind 'valueObject'; ValueSetRef extends EntityCoordinate. The domain plane no longer has valueObjects. Domain validation checks each value object reference against storage.namespaces.<ns>.entries.valueObject and refuses the old domain key. valueObjectAt and storageValueObjectsAtDefaultNamespace replace domainValueObjectsAtDefaultNamespace. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ueObject A StorageValueObject entity kind with its schema; a member typed by an enum refers to the storage value set. A domain field type refers to the value object by its storage coordinate. The domain namespace schema drops valueObjects, and an old contract that has it is refused with a message that says to re-emit. The editor JSON schema is regenerated. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…alueObject A MongoValueObject entity kind with its schema; a member typed by an enum refers to the storage value set. A domain field type refers to the value object by its storage coordinate. The domain namespace schema and the contract serializer refuse the old domain valueObjects key with a message that says to re-emit. Readers of a field's value object name use entityName. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…sets into storage The SQL build writes each value object to entries.valueObject of the default namespace before the storage hash, and fields and nested members refer to it by its storage coordinate; an enum member refers to the storage value set. A model node now carries its discriminator, variants and base, so the build writes the <Base>.<field> value set and points the discriminator column at it. An enum-typed discriminator keeps the enum's value set, and an enum named like the discriminator's value set is refused. SQL PSL pairs variants before the build instead of patching them in afterwards. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The Mongo builder writes value objects to entries.valueObject and buildMongoStorage hashes them. field.valueObject, Mongo PSL and the Prisma 6 reader write the storage coordinate. A value object member typed by an enum refers to the storage value set, and the validator refusals count such enums. The Mongo target serializer carries every entries kind through deserialize and serialize. The derived validators do not change. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…hanged The no-operations sentence is appended to the existing summary, so an automatic baseline keeps its baseline text and a plan keeps its extension migration directories. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ed when no entity did Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…no operations Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…re-emitted Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…espace without entries Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ue set ContractValueObjectField types valueSet as EntityCoordinate<'storage', 'valueSet'>, so a value object cannot refer to the domain plane by type. The SQL and Mongo builders write members and model fields through separate functions. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…e-set check as deferred Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…s reference names FieldTypesFromDomain takes the storage namespaces as a last type parameter with a default, so a contract without value objects types its fields as before. ValueObjectTypesFromStorage replaces ValueObjectTypesFromDomain and is keyed by storage namespace. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The SQL and Mongo emitters write entries.valueObject in the storage namespaces type. A value object field type is its storage coordinate. The domain valueObjects line and the top-level valueObjects block are gone. A value object member's override reads the storage value set it names. The Mongo builder types value objects in storage too. A contract without value objects emits the same text as before. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ration order does not change the storage hash Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…e already has a value set of that name The refusal checks the base table's storage namespace, not every enum, and uses the colon form of the other enumType messages. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…espaces are refused Every value object is stored in the default storage namespace and models refer to composite types by bare name, so the second one silently replaced the first. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…en storage is a class instance Real families hydrate storage into a class, which isPlainRecord rejects, so every such plan said the change was outside the storage entities. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…orrected reason for the clash refusal Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… the field's storage reference Run time reads go through valueObjectAt; type-level reads go through the new ContractValueObjectAt. The dead fallback to the flat valueObjects block of contract.d.ts is gone. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The statement resolver still refuses a statement that names a value object, but looks for it under storage entries.valueObject. A statement contract now carries its storage namespaces. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… check accepts for it, and the check reads it through the stack's codec lookup instead of a table in the target Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…valueObjectJsonType Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…names end in _shape Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… _shape check names Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…eck reads every row under a lock, and NOT VALID checks are deferred Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…migration it writes, and decides a migration with no operations from its operations Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…fields of storage value objects, and both family storage validators run it Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…pe names the upgrade script for a stored contract, and the Mongo schema keeps no copy of it Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ts principle stops, and which changes have no operations; the Prisma 6 reader no longer promises @Map on composite fields; small doc fixes Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…and the contract README lists the validate-storage export Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…eview Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…en as; the SQL family resolves each value object member's JSON type, and the check renderer reads only its input jsonType is one of string, number, boolean, object, array or any. The SQL family codecs declare it on their templates, and postgresCodec keeps it. The family reads it when it resolves a value object and refuses a member whose codec declares none with CONTRACT.CODEC_JSON_TYPE_MISSING. The Postgres registries accept a descriptor without it, so an older codec package keeps working where it is not in a value object. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…mespaces'>, and ContractWithDomain stays on functions that read the domain Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…elds and value object members in both families; ContractValueObjectSchema is named after its type; SQL's value object schema leaves out unions and dictionaries by its structure, and its refusal names the value object and the member Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…union and dictionary value object members and checks a domain field's type Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… changedStorageEntities, the words its README and summary use Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…DB value object member leaves it out, and drops dict false Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…nd reads the valueSet slot; ADR 309 describes the codec's JSON type, and its first consequence and the rejected alternative agree with the cases that have no operations Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…e SQL refusal, and the missing jsonType error says why without guessing the codec's age Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…he upgrade script keeps a malformed nullable, and ADR 309 gives the full check name Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…and db update says when it signs a changed contract with no operations and names what changed Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… names the check, table and column, gives the query that finds the rows, and the route that keeps them Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…d apply with --advance-ref db, so the next plan writes nothing; the advice to fix rows re-runs db migrate with its flags Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Contributor
WalkthroughWarning Review details and warnings were omitted to fit the comment limit. |
@prisma/orm-extension-arktype-json
@prisma/orm-extension-middleware-cache
@prisma/orm-extension-paradedb
@prisma/orm-extension-pgvector
@prisma/orm-extension-postgis
@prisma/orm-extension-supabase
@prisma/orm-family-mongo
@prisma/orm-family-sql
@prisma/orm-framework
@prisma/orm-mongo
@prisma/orm-mysql
@prisma/orm-postgres
@prisma/orm-sqlite
@prisma/orm-target-mongo
@prisma/orm-target-mysql
@prisma/orm-target-postgres
@prisma/orm-target-sqlite
@prisma/orm-toolchain
commit: |
Contributor
size-limit report 📦
|
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
This branch has not been deployed
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.
Linked issue
Refs TML-3537. This is the third of four pull requests for the remaining migration statements. The first two (namespaces, enum values) are merged. The fourth adds the value object and variant statements on top of this one.
At a glance
A model stores a value object in one
jsonbcolumn:Before this change,
contract.jsonkeptAddressin the domain plane, and the field named it:Now
Addressis a storage entity, and the field refers to it by its storage coordinate:On a managed Postgres table, the column gets a derived check.
migration planwrites it:Before, renaming
streetchanged only the domain plane. The storage hash stayed the same, andmigration planreported "No changes detected" while every row kept the old key. Now the rename changes the storage hash and the check, so the migration system sees it.Summary
The migration system tells database states apart by the storage hash. A value object's fields and a discriminator's values are stored data, so they now live in storage, where the hash covers them. The decision is recorded in ADR 309.
Decision
This PR ships four things:
storage.namespaces.<ns>.entries.valueObject.<Name>, with the definition they had in the domain plane. A domain field refers to one by storage coordinate. This is the first reference from the domain plane to the storage plane. A contract that still keepsvalueObjectsin the domain is refused with a message that says to re-emit it.<Base>.<field>(for exampleTask.type), holding the variants' values sorted. The base table's discriminator column refers to it, as an enum column refers to its enum's value set. MongoDB writes none, because its collection validator already lists each variant's value.<table>_<column>_shape_<hash>for a value object column, and<table>_<column>_check_<hash>for a discriminator column ("type" IN ('bug', 'feature')). The checks are content-named, so any change to the value object or the variants drops the old check and adds the new one.migration planwrites the migration with no operations and names what changed.db migrateanddb updatemove the marker to the new hash. Before, such a plan was refused.Every contract with value objects or variants is re-emitted, and the
prisma-8-demoinitial migration now creates the two checks. Projects upgrade with a script.Reviewer notes
contract.d.tsand the example migration snapshots changed hash.pnpm fixtures:checkis clean. The source changes are in the families' authoring, emit, ORM and validator code, the Postgres check derivation and the CLI's no-operation plan.jsonpathpredicate, ending inIS TRUE, so an unknown result fails. A required member must be present and of its codec's JSON type, a nullable member may be absent or JSONnull, and no other key is allowed. A value object that contains itself is checked down to where it refers back to itself, as the MongoDB validator does.jsonTypefield ('string' | 'number' | 'boolean' | 'object' | 'array' | 'any'), as MongoDB data types declarebsonTypes. The check reads it.TypeScriptrequires the field. At run time a codec package built before this release still loads; only a value object member that uses such a codec is refused, withCONTRACT.CODEC_JSON_TYPE_MISSING, naming the codec and the member.valueObjectstorage entity kind, with one schema both families use, because domain fields refer to it. ADR 221 says families and packs own entity kinds, so ADR 309 and a note in ADR 221 record the exception.<Base>.<field>in the same namespace. No authoring surface writes that name, so only a hand-written definition reaches the refusal.User.addressas plainjsonb, and the value object type tests read a new PSL fixture. This is recorded inprojects/migration-statements/deferred.md.mongo-demoandretail-storemigration chains faildb migrateon mongodb-memory-server withMIGRATION.SCHEMA_VERIFY_FAILED. They fail the same way onorigin/main, so this PR did not cause it. Filed as TML-3749.How it fits together
packages/1-framework/0-foundation/contract/src/domain-types.tsandpackages/1-framework/0-foundation/contract/src/storage-value-objects.ts.@@discriminatorwith its variants before the build, so the discriminator value set is in the hash. Seepackages/2-sql-family/2-authoring/contract-ts/src/build-contract.tsandpackages/2-sql-family/2-authoring/contract-psl/src/interpreter.ts.contract.d.tstypes a value object field from the storage plane, through a new, required sixth parameter ofFieldTypesFromDomain, so everycontract.d.tschanges. A SQL discriminator field is typed by its variants' values ('bug' | 'feature'). Seepackages/1-framework/0-foundation/contract/src/field-value-types.ts.contract print,contract inferand the statement resolver find a value object through the field's coordinate.contract printshare that derivation. Seepackages/2-sql-family/1-core/contract/src/check-expressions.tsandpackages/3-targets/postgres/1-target/src/core/check-expressions.ts.changedStorageEntitiesin--json). When rows break a new check, the runner reports which check and column, and the CLI gives the query that finds the rows and the way to keep their data. Seepackages/1-framework/3-tooling/cli/src/control-api/operations/migration-plan.tsandpackages/1-framework/3-tooling/cli/src/utils/runner-failure-advice.ts.Behavior changes & evidence
test/integration/test/cli-journeys/value-object-checks.e2e.test.ts(V1),packages/3-targets/postgres/1-target/test/check-expressions.value-object.test.ts.migration.tsbetween the drop of the old check and the add of the new one. Following that advice applies cleanly. Evidence:value-object-checks.e2e.test.ts(V2, V3).value-object-checks.e2e.test.ts(V4).migratemoves the marker. Evidence:value-object-checks.e2e.test.ts(V5),test/integration/test/cli-journeys/plans-with-no-operations.e2e.test.tsand its SQLite and MongoDB twins.test/integration/test/cli-journeys/value-object-validator.mongo.e2e.test.ts.valueObjectsin the domain is refused withCONTRACT.VALIDATION_FAILED, saying to re-emit it. Evidence:packages/1-framework/0-foundation/contract/test/validate-domain.test.ts.Every journey runs through
migration planandmigrate, and throughdb update, and ends withdb verify --schema-onlyclean and a further plan empty.Upgrade
The upgrade instructions are in
upgrade-instructions/pending/value-objects-in-storage/. For an app:node <guide>/scripts/value-objects-in-storage/value-objects-in-storage.ts. It rewrites every contract and snapshot, renames each snapshot directory to its new storage hash, and rewrites the migrations, refs,migration.tsandcontract.d.tsfiles that name an old hash. It prints each<old> -> <new>hash. A second run changes nothing.prisma db sign --db "$DATABASE_URL". A database you could not migrate first is signed at the new hash of its marker's hash withprisma db sign --contract <new hash> --no-advance-ref, then migrated.prisma contract emit.prisma migration plan, and apply it withprisma db migrate --advance-ref db --db "$DATABASE_URL". On MongoDB there is nothing to plan.Adding a check takes a lock on the table while Postgres reads every row, and a later change to the value object replaces the check and reads the rows again. The guide says so.
Extension authors:
contract.storage:contractModelToMongoResultShape,contractFieldToMongoFieldShapeandcompileMongoQuerytake the storage, andMongoValueObjectsis removed;ValueObjectTypesFromDomainis nowValueObjectTypesFromStorage, andFieldTypesFromDomaintakes the storage namespaces as a required sixth argument;jsonType, and the check expression types moved to@prisma/orm-postgres/family-contract/check-expressions.The script and both guides were run from start to finish on copies of
prisma-8-demoandmongo-demowith real migration histories, including a database behind the last migration and a row that breaks the new check. A secondmigration planafterwards writes nothing.Testing performed
value-object-checks.e2e.test.ts(10 cases) andvalue-object-validator.mongo.e2e.test.ts(2 cases).test/integration/test/sql-orm-client/(142 files), the upgrade script's integration test, the print round-trip files.pnpm fixtures:check,pnpm lint:deps,pnpm lint:ratchets,pnpm check:error-reference,pnpm check:upgrade-coverage --mode pr.test --coveragein each touched package.Skill update
skills/prisma-8/references/contract.mdsays value objects are lowered to storage value objects, describes the derived Postgres checks and the discriminator value set, and says a value object change is a migration.Follow-ups
--rename Address.street:Address.line1,--convert Bug,--delete Bug) are the next pull request. It also adds--backfillfor a new required value object member, so rows that lack it can be filled without a hand-written update.orm.Bug.create);orm.Task.variant('bug').createworks. This fails onmaintoo.projects/migration-statements/deferred.md: the SQL TypeScript builder has no value objects; SQL PSL does not namespace composite types; a column's value set reference is not checked against storage; the SQL ORM cannot write aBigIntorBytesmember of a value object.Alternatives considered
db update, which runs no data operations, still could not apply it.?&and-. One strictjsonpathpredicate covers nesting, lists, member types and nullability, and one form for every shape is simpler to derive and to read back.Checklist
git commit -s) per the DCO.TML-NNNN: <sentence-case title>form.Agent: radagast-20