v8.0.0-rc.2
This release retires the prisma-next binary in favour of the unified prisma CLI, returns the default aggregates to plain JavaScript numbers with lossless variants beside them, makes CHECK constraints a declared part of the contract, and splits runtime row queries from non-returning writes. Almost every application will need to re-emit its contract and rename its config file, so read the breaking changes before upgrading.
Two upgrade recipes carry the mechanical translations for this hop: the user recipe and the extension-author recipe.
Breaking changes
-
This repository no longer publishes a CLI; the unified
prismaCLI replaces it — nothing published ships aprisma-nextbin anymore.@prisma/orm-toolchainexposes theormcommand family at@prisma/orm-toolchain/cliand no binary, and the database facades forward no launcher. Install@prisma/cli(the prisma-cli distribution, published undernextfor the v8 line) and replaceprisma-next <command>in package scripts and CI with the unified CLI. The config file moves with it:prisma-next.config.tsis deprecated in favour ofprisma.config.ts, and the config value is now engine-shaped, with your existing ORM config nested under anormsection. Both the old filename and the flat shape still load, each printing a deprecation warning on stderr, so the rename and the rewrap can land separately. See the user recipe. (#30005)Before:
// prisma-next.config.ts import { defineConfig } from '@prisma/orm-postgres/config'; export default defineConfig({ contract: './contract.ts', output: './generated' });
After:
// prisma.config.ts import { defineConfig } from '@prisma/cli-engine'; import { defineConfig as ormConfig } from '@prisma/orm-postgres/config'; export default defineConfig({ orm: ormConfig({ contract: './contract.ts', output: './generated' }), });
-
The default aggregates are JavaScript numbers again, with lossless variants beside them —
count(),sum()over an integer column, andavg()over an integer column all returnnumber. In8.0.0-rc.1they returned abigint, abigintor decimal string depending on the column's width, and a decimal string respectively. The lossless results moved to three new operations:countBigInt()returns abigint,sumBigInt()returns abigint, andavgDecimal()returns an exact decimal string (PostgreSQL only — SQLite has no decimal type and contributes none). Acount()or integersum()whose value passes ±(2^53 − 1) now raisesRUNTIME.DECODE_FAILEDrather than returning a rounded number, so move those calls to theBigIntvariants where the magnitude is real. Unchanged:min/max,sum/avgover a float column,sumoverDecimal,sumoverUnboundedInt, and the ORM'shaving(...)operands. The SQL builder's comparison operands do move, becausefns.gt(a, b)types both sides from one codec. The same PR also makes the wide-integer codecs refuse the wrong JavaScript type: aBigIntorUnboundedIntcolumn rejects anumberand aBigIntNumbercolumn rejects abigint, withRUNTIME.ENCODE_FAILEDnaming the type that arrived, where previously a number was accepted and stringified — which let a fractional value reach an integer column unremarked. See the user recipe. (#29930)Before:
const { total } = await db.User.aggregate((a) => ({ total: a.count() })); total === 2n; // bigint const busy = await db.sql.public.user .groupBy('kind') .having((_f, fns) => fns.gt(fns.count(), 1n)); // bigint literal
After:
const { total } = await db.User.aggregate((a) => ({ total: a.count() })); total === 2; // number — countBigInt() returns the bigint const busy = await db.sql.public.user .groupBy('kind') .having((_f, fns) => fns.gt(fns.count(), 1)); // plain number literal
-
Which aggregate methods exist is now the contract's answer — the aggregate methods are no longer declared on the ORM and SQL-builder surfaces outright. Each surface is derived from the operation names in the emitted
contract.d.ts'sAggregateTypesblock, so a target or extension can contribute an operation and it appears under its own name with no client change. PostgreSQL now contributes eight operations and SQLite seven. Re-emit your contract with the CLI'scontract emit: against a contract with noAggregateTypesblock — one authored in code withdefineContract(...)and handed straight to the client, or emitted before8.0.0-rc.1— every aggregate surface resolves toAggregateOperationsUnavailable, an empty type, and each call becomes a compile error. What this release changes is compile-time only — the separate runtime guard introduced in8.0.0-rc.1still stands, rejecting an aggregate whose operation and input codec the composed target does not declare withORM.AGGREGATE_UNSUPPORTEDbefore the query runs. Separately,count(field)now rendersCOUNT(<column>)instead of accepting the argument and discarding it, so a call that got past the types — a@ts-expect-error, acount(x as never), or dynamic dispatch — now counts that field's non-null values rather than rows. See the user recipe and the extension-author recipe. (#29922) -
CHECK constraints are declared in the contract, and introspection now sees all of them — the CHECK shape in
contract.jsonchanged from{ name, column, valueSet }to{ name, prefix, expression }, whereexpressionis the raw SQL predicate andnameis a content-addressed wire name (<prefix>_<8hex>, the convention indexes and RLS policies already use). An old-shape contract is rejected on read, so re-emitting is not optional. Three consequences to plan for. Your first migration plan after upgrading drops each old unsuffixed enum constraint and adds the wire-named one, which needsdestructiveto converge. Every list (many) column gains a declared element-non-null CHECK the planner previously created without declaring. And introspection stopped parsing predicates, so hand-written constraints earlier versions could not see are now visible — and an undeclared check is an extra thatdb verify --strictreports and a destructive-capable plan drops, so read the first plan fordropCheckConstraintoperations naming constraints you wrote yourself, and declare each one you want to keep with@@check(expression: "…", map: "<physical name>"). Two API changes ride along:addCheckConstraintin committed migration files takes anexpressioninstead of acolumn/valuespair, and thetypescriptContractoptions bag now requirescreateNamespacewhenever it passesdefaultControlPolicy. AnenumType()whose codec is numeric now throwsCONTRACT.ENUM_INVALIDwhile the contract is being built rather than failing later at migrate time. See the user recipe and the extension-author recipe. (#29892)Before:
this.addCheckConstraint({ schema, table, constraint, column: 'kind', values: ['admin', 'user'] });
After:
this.addCheckConstraint({ schema, table, constraint, expression: `"kind" IN ('admin', 'user')` });
-
Runtime row queries and non-returning writes are separate calls —
query()streams rows andexecute()resolves{ affectedRows }, which is how a write now reports its affected count without a precedingSELECT. Classify each call site by the result it consumes rather than replacing everyexecute: a select, a returning write, or any plan whose rows are iterated, indexed, or decoded moves toquery, while an insert, update, or delete that returns nothing stays onexecuteand readsaffectedRows. Prepared row consumption moves fromtarget.queryPrepared(prepared, params)toprepared.query(target, params). Runtime middleware splits the same way, intobeforeQuery/interceptQuery/afterQueryandbeforeExecute/interceptExecute/afterExecutewith a sharedbeforeCompile; query interception returns{ rows }and execute interception returns{ stats }. There is no operation discriminator, compatibility alias, or generic fallback hook. On Mongo,db.querystays the static builder and the row-executingdb.executefacade method is gone — build withdb.query, then execute through(await db.runtime()).query(plan). See the user recipe. (#29921) -
rawis a reserved storage namespace — the SQL surface exposes the whole-query raw statement tag asdb.sql.raw, so a storage namespace of that name would be unreachable through the builder while the emitted types still promised its tables. Building the client now raisesORM.NAMESPACE_RESERVEDnaming the namespace. Rename it in your schema, re-emit the contract, and plan the rename against the database as you would any other namespace rename. Onlyrawis reserved. (#29997)Before:
model Event { id String @id @@schema("raw") }
After:
model Event { id String @id @@schema("ingest") }
-
Codec ids are checked where you write them — a codec id in a prepared declaration or in a contract-bound raw fragment is now checked against your contract's codec map, so an id the contract does not carry is a compile error instead of an execution-time
RUNTIME.PARAM_REF_MISSING_CODEC. The usual cause is an unversioned id. Read the correct spelling off your emittedcontract.d.ts— every id it carries now completes at both positions. A raw fragment built through a contract-free lane is unaffected, since it has no map to check against. (#30011)Before:
await db.prepare({ id: 'pg/int4' }, (sql, params) => /* … */); const upper = fns.raw`UPPER(${f.email})`.returns('pg/text');
After:
await db.prepare({ id: 'pg/int4@1' }, (sql, params) => /* … */); const upper = fns.raw`UPPER(${f.email})`.returns('pg/text@1');
-
db updatetakes consent by database name, and--yesno longer grants it — a plan that would destroy data is refused until you type the name of the connected database, and the consent binds to that exact plan by hash.--yesnever grants it; the CLI style guide has always said a blanket confirmation flag must not stand in for a destructive confirmation. Non-interactive runs grant with--confirm <database>. A dry run, or a plan with nothing destructive in it, never asks. Update any CI invocation that relied on-yto apply a destructive plan. (#29986) -
The diagnostic commands exit 4 on findings and 2 on errors —
db verify,db sign, andmigration checknow distinguish "I ran and found problems" (exit 4) from "I could not run" (exit 2). Exit 1 is reserved for a bug in the CLI itself, and exit 0 still means the check ran and found nothing.db verifyanddb signpreviously exited 1 on findings, andmigration checkexited 2. Scripts that test for any non-zero exit are unaffected; scripts that match a specific code must be updated. (#29984) -
Four
migration statusflags are retired —--graph,--all,--limit, and--refmoved to their own commands. An old invocation now gets a typedCLI.COMMAND_MOVEDerror naming the replacement rather than failing as an unknown flag. (#29982) -
Prepared statements split by their declared result —
runtime.prepare()returns one of two handles chosen from the plan the callback builds: a rows plan gives thePreparedStatementyou already have, consumed with.query(target, params), while a plan whose declared result is an affected-row count gives aPreparedExecution, consumed with.execute(target, params). This matters to extension authors: a facade that redeclaresprepare()changes its return type toPreparedForwith no logic change, and a scope that installs the prepared-query bridge must also install the execute bridge orprepared.executethrows on the bridge invariant. See the extension-author recipe. (#30006)
Features
- Whole-query raw SQL replaces the classic
$queryRaw/$executeRawuse case. A whole statement is authored with the same tagged template the fragment mechanism already used, terminated with.returnsRow(rowSpec)for decoded, typed rows or.affectedCount()for a mutation count, and built into an ordinary query plan that flows through the existing lowering, codec, guardrail, and execution machinery — no new query lane and no new execution surface. Row-returning raw queries interpolate into other raw templates as subqueries, which gives CTEs, including data-modifying ones, for free. (#29997) @@check(expression: "…")declares a CHECK constraint in the schema, andcontract inferadopts the ones your database already has. Usename:for a wire-name prefix, so the physical constraint isname_<8hex>hashed over the predicate and compared by name — which means Postgres reprinting the expression never causes drift. Usemap:to adopt a constraint under its existing physical name, comparing the predicate byte-for-byte. Pulling a database now emits@@checkfor every live check Prisma Next did not derive, so a hand-written constraint is declared from the first pull instead of reading as an undeclared extra. (#29972)@noCheckopts a column out of the CHECK constraints Prisma Next derives for it, per kind:@noChecksuppresses all of them,@noCheck(membership)keeps the element-non-null check on a list column while dropping the membership check, and@noCheck(elementNotNull)does the reverse. The TypeScript builder equivalent is.noCheck(...).contract inferemits the attribute too, so a pulled schema passesdb verify --schema-onlyimmediately instead of needing one migration first. (#29928)- Two new column types make integer representation a per-column choice without changing the lossless
BigIntdefault.BigIntNumberreads and writes as a JavaScriptnumber, throwing outside ±(2^53 − 1) instead of rounding.UnboundedIntuses PostgreSQL unconstrainednumericstorage and round-trips integral values as exactbigintvalues at arbitrary magnitude. PostgreSQL contributes both; SQLite contributesBigIntNumber. (#29902) - The minimum supported PostgreSQL version drops from 17 to 15, the oldest version CI has been exercising all along.
initscaffolds and the--probe-dbwarning threshold follow the new floor. The reasoning is recorded in ADR 244. (#29971) - Renaming a model or column whose CHECK constraint content is unchanged now plans a single
ALTER TABLE … RENAME CONSTRAINT, classedwidening, instead of a drop plus an add. A cosmetic rename no longer needs a destructive-capable plan or a full table revalidation. (#29894) - Errors carry typed next actions. A failure that has a remedy now ships it as structured data —
nextActions, each naming a command to run — raised at the site that holds the arguments rather than spelled out in English prose a caller would have to parse. The binary name is templated at the raise site and substituted when rendered, so the suggestion stays correct as the CLI is renamed. (#29977) - CLI failures report their real error code.
envelope.codeis the stable surface consumers branch on, and a dozen failures previously reportedCONTRACT.VERIFY_FAILEDwhile hiding the true code in metadata. Every construction site now declares its code explicitly, fourteen new codes were added for the failures that had none, and the generic error path gainedcausesupport. (#29919) - Config loading reports diagnostics per section instead of throwing on the first problem it finds. A command fails only when a section it actually reads is broken, so a malformed
formattersection no longer blocksdb init. Each diagnostic is tagged with the config section and field it concerns. (#29936)
Fixes
initno longer fails at its contract-emit step against the published packages. The step now runs the scaffolded project's own CLI binary as a subprocess rather than loading the new config in-process with the running CLI's bundled loader, and its failure message carries the child's stderr so a real cause is visible. The schema-path prompt also shows its default as placeholder text instead of looking blank until a keypress. (#30018)contract emitpicks the import specifier for the emittedcontract.d.tsby reading the nearestpackage.jsonabove the file it is writing, rather than falling back to the process working directory. Running the command from the wrong directory previously wrote an unresolvable internal specifier into the generated file. (#29981)- Synthesized foreign-key-backing index name prefixes are truncated to fit PostgreSQL's 63-byte identifier limit, so a mapped explicit join table no longer fails
contract emitbefore the content hash can be appended. User-authored over-budget index prefixes still fail loudly. (#30025) - A raw row spec column named
__proto__is now refused loudly instead of silently vanishing — bracket assignment onto an object literal hit the inherited setter, so the key never became an own property and the record was quietly re-parented.constructorandprototypecreate ordinary own properties and round-trip faithfully. (#30014) - The PostgreSQL direct driver no longer ends a caller's transaction. A driver-level read issued while its connection held an open transaction reported no transaction in progress, took the cursor portal-protection path, and wrapped itself in
BEGIN/COMMIT— and thatCOMMITended the caller's transaction, so later statements ran autocommit andROLLBACKundid nothing. The driver and its connection now share the transaction-open flag. (#29920) - ORM mutation reloads encode
Bytesidentities through the column codec, so a repeated upsert keyed on aBytescolumn no longer raisesORM.MUTATION_ROW_MISSING. Every unbound literal entering a select through raw collection state now becomes a typed parameter. (#29910)
New contributors
- @EmaToplek made their first contribution in #29903