tml-2912: unify SQL check-constraint mechanism + drift-manage array element checks#902
tml-2912: unify SQL check-constraint mechanism + drift-manage array element checks#902SevInf wants to merge 18 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
283c5ba to
d4c5290
Compare
ec19c95 to
d7a9d4b
Compare
bf74745 to
f9eb395
Compare
d7a9d4b to
fbeb7a1
Compare
…slice 2 D1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…ce 2 D2) Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…pability (slice 2 D3) Thread a merged CapabilityMatrix (built via mergeCapabilityMatrices over [target, adapter, ...extensionPacks], the same merge enrichContract performs) from the ControlStack through ContractSourceContext into the PSL interpreter, and reject a scalar-list field whose target does not report sql.scalarList with PSL_SCALAR_LIST_UNSUPPORTED_TARGET. An absent matrix means do not gate, so non-adapter authoring paths stay valid; the CLI emit path always populates it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
… D1b) Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…r PSL lists (slice 2 D4) Part A (FR13, AC8 PSL half): every native scalar-array column gets a deterministic CHECK (array_position(col, NULL) IS NULL) constraint named <table>_<column>_elem_not_null, emitted at the migration/DDL layer in the postgres issue-planner createTable path via a new CheckExpressionConstraint DDL node. Introspection-based verify skips non-enum check predicates, so the constraint round-trips without false drift and is invisible to infer. Part B (FR14, AC9): PSL array-typed @default lowering. @default([]) -> empty literal array (DEFAULT *{}*); @default(["a","b"]) -> literal array encoded element-wise against the element codec (build-contract encodeColumnDefault is now many-aware). A scalar literal default on a list field is rejected at authoring time with PSL_LIST_DEFAULT_NOT_ARRAY (closes slice-1 D2 carry-in). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…ice 2 D5) AC1: a `posts.tags String[]` schema authored in PSL emits a native array storage column (pg/text@1, many:true, not jsonb), migrates onto a fresh Postgres database as text[] with no manual edits, and round-trips through `contract infer` back to a `tags String[]` PSL field. AC2/NFR1: DateTime[]/Bytes[]/Decimal[] list fields authored in PSL (not hand-built typed contracts) insert and select rows whose decoded element values deep-equal the originals, proving per-element codec application through the whole authored path. NFR4: the same list schema yields matching observable semantics on SQL and Mongo — both author cleanly, both generate a ReadonlyArray<...> domain type over the string element codec, and list element values round-trip. The live Mongo decode runs on mongodb-memory-server (CI; nixos binary download is local env-noise). Also hardens the introspection-side array-literal parser (parseArrayLiteralBody in default-normalizer.ts) so a quoted element containing a comma or an escaped quote round-trips through the migration-diff layer without spurious drift (slice-1 carry-in). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…ing test (rebase on #864) Main #864 made SqlNamespace abstract and createNamespace required on InterpretPslDocumentToSqlContractInput. Thread createTestSqlNamespace through the three D3 capability-gating interpret calls, matching the sibling interpreter tests updated by that refactor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Respond to reviewer feedback on the scalar-list authoring path:
- Simplify the `many` emission to the file's own
`...(rf.many ? { many: true as const } : {})` idiom; `many` stays
omitted (never `false`) for non-list fields, keeping canonical
emission byte-stable.
- Consume the parser's structured expression AST for list-field
`@default([...])` instead of re-parsing a stringified form. The
PSL parser now decodes each attribute-arg expression into a
`ResolvedExpr` discriminated union (string/number/boolean/array/
object/call/identifier) exposed on `ResolvedAttributeArg`, and the
SQL column resolver reads it directly. Deletes the hand-rolled
`splitTopLevelArrayElements` tokenizer and the array-literal string
parsing; array/scalar/function-default behavior is preserved,
including quoted elements containing commas.
- Make the adapter capability matrix required end-to-end so the
scalar-list gate has no test-only undefined branch and fails closed:
an empty matrix rejects scalar lists. `ContractSourceContext`,
`InterpretPslDocumentToSqlContractInput`, and the internal
resolution inputs now require `capabilities`; production already
threads it via the control stack. Test call sites pass an explicit
matrix; the former "does not gate when no matrix is threaded" case
becomes a fail-closed empty-matrix rejection.
- Drop transient milestone codes from the scalar-list integration
test comments and titles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…#870 The PR touches one packages/3-extensions/ test file (threading the now- required capabilities matrix into a test ContractSourceContext), which trips the check:upgrade-coverage per-pr-declaration gate. Record it as an incidental substrate diff: the affected types are framework-internal contract-emission seams the control stack always populates; no extension-author API changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…teArg (slice 2 N1) Replace the decoded ResolvedExpr union with the parser ExpressionAst node directly on ResolvedAttributeArg, per code-owner review. List-default parsing narrows via the AST classes (ArrayLiteralAst/StringLiteralExprAst/...) instead of a bespoke decoded shape. The stringified value is kept, as it is still consumed by scalar/function-call/relation-name/authoring-arg parsing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…nd ORM client Add a sibling to the DateTime[]/Bytes[]/Decimal[] roundtrip covering the plain scalar element types: author `tags String[]`/`scores Int[]` in PSL, migrate onto real Postgres, insert, and SELECT back, asserting element-wise decode for pg/text@1 and pg/int4@1. Add an ORM-client read-back test: over the same PSL-authored contract, read the native array columns through orm().<ns>.<model>.select(...).all(), proving the ORM projects and decodes scalar many:true columns as JS arrays (not just to-many relations). The PSL path yields a generic Contract<SqlStorage>, so accessors are index signatures; the narrow row-type array inference is covered by the emitted contract path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…ntract Replace the runtime-authored, generically-typed ORM scalar-list read test with a strongly-typed write->read round-trip over an emitted contract fixture. The previous test authored the contract at runtime via authorSqlContractFromPsl, yielding a widened Contract<SqlStorage> whose ORM namespace/model accessors were index signatures (bracket access) and whose row type was loose; it also seeded via a raw InsertAst rather than the ORM. New coverage: - Add fixture test/sql-orm-client/fixtures/scalar-lists/ (Item with tags String[] / scores Int[]) emitted to a committed contract.json + contract.d.ts. Storage columns are native arrays (pg/text@1 / pg/int4@1, many: true) and the generated types render each field as ReadonlyArray<...>. - Wire the fixture's config into the integration `emit` script so it is deterministically regenerable and covered by fixtures:check. - Rewrite orm-list-read.integration.test.ts to migrate the emitted contract onto a real Postgres database, seed a row through the typed ORM (db.public.Item .create with tags/scores arrays), read it back through dotted strongly-typed accessors, assert the whole row shape, and assert at the type level that the read row infers tags: ReadonlyArray<string> and scores: ReadonlyArray<number>. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
Remove two inline test comments that merely restated the field types / assertion already visible in the code (no rationale carried). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
… migration) Slice 2 lowered PSL scalar lists to native Postgres arrays, flipping the telemetry-backend extensions/flags fields from jsonb to text[] and drifting the emitted storageHash away from the committed migration and production DB. Author both fields as Json so they emit as pg/jsonb@1 / jsonb again, restoring the pre-slice-2 storageHash without touching the committed migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…d usage Remove the capability-matrix field JSDocs (and the scalar-list gate comment) that described how the field is produced (control stack / enrichContract) and consumed (authoring-time gating) elsewhere. The `capabilities: CapabilityMatrix` field and the gate diagnostic are self-documenting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
f9eb395 to
31880a9
Compare
…nage element checks
Collapse the two parallel check-constraint mechanisms into one and make the
scalar-array element-non-null CHECK drift-managed like enum value-set checks,
closing the L10 strict-verify gap.
- Discriminate SqlCheckConstraintIR(Input) into valueSet | expression leaves
(abstract base + two frozen subclasses + `sqlCheckConstraintIR` factory).
- Payload-discriminate AddCheckConstraintCall / addCheckConstraint /
migration descriptor: `{kind:'valueSet',column,values} | {kind:'expression',expression}`.
- Add one shared `projectContractChecks` + a single element-non-null
name/predicate canonicalizer in the family layer; wire it into the verifier,
expected-IR builder, and planner so all three agree byte-for-byte.
- Recognize `array_position(col, NULL) IS NULL` in parseCheckConstraintDef and
re-canonicalize (handles the stored NULL::type cast + quoting) so drift is
compared canonical-to-canonical.
- Generalize verifyCheckConstraints (expression equality, cross-kind mismatch,
per-kind messages) and route element checks through the diff →
AddCheckConstraintCall (expression) instead of inline CREATE TABLE synthesis.
- Remove the now-dead CheckExpressionConstraint path (DDL union, factory,
renderers, SQLite throws), leaving one check-constraint mechanism.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…0.15
Unifying the check-constraint mechanisms changed the migration builder call
`this.addCheckConstraint({column, values})` to a discriminated
`{payload: {kind: "valueSet", column, values}}` (and adds a `kind: "expression"`
form). Record the consumer transform + detection so the upgrade skill can migrate
hand-preserved migration files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
…tion CHECK recognizer The Shape 3 array_position element-non-null recognizer used a cast fragment `(?:::[^\s)]+)?` that stopped at the first whitespace, so multi-word Postgres type names (e.g. `timestamp with time zone` for DateTime[]) failed to match and the column dropped out of drift management, breaking schema verification after migrate. Anchor the cast with a non-greedy `(?:::.+?)?` up to the closing `) IS NULL`; the cast is discarded and the predicate re-canonicalized, so the wider match is safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Serhii Tatarintsev <tatarintsev@prisma.io>
fbeb7a1 to
95f4968
Compare
What & why
Reviewing #870, the maintainer flagged (
relational-core/.../ddl-types.ts:212) that the scalar-array element-non-null CHECK introduced a second, parallel check-constraint mechanism (CheckExpressionConstraint) alongside the existing enum value-set checks. This PR unifies them into one mechanism and makes the element-non-null checks drift-managed, exactly like enum value-set checks.Stacked on #870 — base is
scalar-lists-slice2, so this diff is only the unification. Merge #870 first.Decision (Option A)
One check-constraint concept with a discriminated payload:
SqlCheckConstraintIR/ the migrationaddCheckConstraintop carryvalueSet ({column, permittedValues})orexpression ({expression}).projectContractCheckshelper feeds the three lock-step sites (verify / expected-IR / planner); it projects anarray_position("<col>", NULL) IS NULLexpression check for everymany: truecolumn.AddCheckConstraintCallpath the enum checks use, and are introspected + compared on verify.CheckExpressionConstraint(the parallel type, inline at CREATE TABLE) is deleted — one mechanism remains.This also closes the strict-verify gap the earlier slice deferred: element checks were previously never introspected/diffed, so
--strictverify false-flagged them. They now round-trip; a migrate → introspect → strict re-verify integration test asserts zero drift.Value-set verification semantics are unchanged from
main— the= ANY (ARRAY…)/IN (…)parsing and set-based comparison are preserved; the discriminant only adds the expression variant on top.Breaking change
The migration builder call changes from
this.addCheckConstraint({ column, values })to a discriminated{ payload: { kind: 'valueSet', column, values } }(and gains akind: 'expression'form). Re-generation emits the new shape; hand-preserved migration files need the edit. Recorded as an upgrade instruction (migration-addcheckconstraint-payload) with detection so the upgrade skill can migrate consumers.Verification
fixtures:checkbyte-clean · cast-ratchet delta 0 ·lint:packagesclean ·check:upgrade-coveragecleantimestamp with time zone) parse + strict re-verify round-trip🤖 Generated with Claude Code