v8.0.0-rc.15
In this release, each column in a SQL contract names its data type, such as pg/text, in place of the type name the database prints. One script and prisma db sign upgrade a project. Codecs now check every value a contract stores, and a field's type in the contract matches its column. Custom collection classes keep their methods through the chain, and scopes let you share a piece of a query across models. The typed SQL builder can lock the rows a select reads. Middleware gains an afterTransaction stage, and the cache middleware gains invalidation. A hand-written migration can rename a table with this.renameTable. Lists can hold null elements. The PSL language server adds hover, go to definition and find references.
The upgrade recipes for this hop: the app recipe and the extension recipe. Each breaking change below names the change id to look for in them. Run the data-type-in-contract script (change contract-stores-data-type) before any other step that emits a contract again.
Breaking changes
-
A contract column stores its data type id, not its database type name. A SQL column in
contract.jsonnow storesdataType, for examplepg/int4, where it storednativeType, for exampleint4. A contract in the old format is refused withCONTRACT.VALIDATION_FAILED. Every contract's storage hash changes once. To upgrade, first upgrade every extension that ships migrations, such as pgvector and PostGIS, in the same step. Then run the script from the upgrade recipe in the project root. It rewrites every contract, snapshot, migration, ref,migration.tsandcontract.d.ts. Then runprisma db signagainst every database before you deploy. Until a database is signed,prisma db migraterefuses withMIGRATION.MARKER_MISMATCH.db signnow signs every contract space, and its--jsonresult is{ ok, summary, spaces, advancedRefs }. A column descriptor written by hand dropsnativeType. PostgreSQL parameter casts use the base name, so$1::integeris now$1::int4, and logged SQL and SQL snapshots change to match. Seecontract-stores-data-type,column-descriptors-drop-native-type,sign-databases-after-upgradeandparameter-casts-use-base-namesin the app recipe. (#30576)Before:
"email": { "codecId": "pg/text@1", "nativeType": "text", "nullable": false }
After:
"email": { "codecId": "pg/text@1", "dataType": "pg/text", "nullable": false }
The upgrade:
curl -O https://raw.githubusercontent.com/prisma/orm/v8.0.0-rc.15/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.14-to-8.0.0-rc.15/scripts/data-type-in-contract/data-type-in-contract.ts node data-type-in-contract.ts prisma db sign
-
A list's cardinality is stored as an object. A list field or column in
contract.jsonstoresmany: { elementNullable: false }where it storedmany: true, andmany: trueis refused. For SQL contracts, thedata-type-in-contractscript ofcontract-stores-data-typemakes this change. Emit MongoDB contracts again, and refresh their historical snapshots with the migrations that name them. A contract object written by hand changes the same way. Seereemit-explicit-list-cardinalityandrefresh-historical-list-contractsin the app recipe. (#30051) -
Codecs check every value a contract stores. A TypeScript
.default()orenumTypemember that its column's codec does not take is refused when the contract is built, withCONTRACT.DEFAULT_INVALIDorCONTRACT.ENUM_INVALID. A PSL enum member or literal default that the column's type does not hold is refused bycontract emit. A TypeScript enum member must be written as the column stores it, so an upper-case uuid member is refused with the value to write instead. Emit contracts again: the domain half now carries the type parameters and enum values of fields typed by a named type, of enum lists and of composite type members. Hashes do not change for that. Other changes: atextArray()element is typedstring | null, achar(n)value reads without its padding through.include()too, NaN on SQLite throwsRUNTIME.ENCODE_FAILED, an upper-case or braced uuid default is stored as PostgreSQL writes it, and an attribute on a composite type is refused. Seedomain-types-match-their-columns,codecs-check-stored-json,psl-values-checked-by-codecs,ts-enum-member-written-as-storedand the other entries of the same names in the app recipe. (#30451, #30512) -
setDefaultin amigration.tstakes the column, not SQL text. The adapter now writes every column default and reads it with the column's codec. Amigration.tsthat usesdefaultSqlno longer compiles, and running it stops withMIGRATION.OPERATION_OPTION_REMOVED; itsops.jsonstill applies. On PostgreSQL,db updateanddb migratenow apply a changed default on an existing column. A migration planned by an earlier version still skips it: plan it again before you apply it. Seemigration-ts-column-defaultsandpostgres-changed-default-appliedin the app recipe. (#30451)Before:
this.setDefault({ table: 'user', column: 'role', defaultSql: "DEFAULT 'member'" })
After:
this.setDefault({ table: 'user', column: col('role', 'text', { default: lit('member'), codecRef: { codecId: 'pg/text@1' } }) })
-
A TypeScript contract must list the extensions whose codecs it uses.
defineContractfrom@prisma/orm-postgres/contract-builderor@prisma/orm-sqlite/contract-buildertakes each column's database type from its codec's data type. A contract that uses a pgvector, PostGIS or arktype-json column without listing that extension inextensionsfails withCONTRACT.CODEC_DESCRIPTOR_MISSING. Avector(length)orgeometry({ srid })argument out of range now failsdefineContractwithCONTRACT.TYPE_PARAMS_INVALID, not the helper call. On SQLite, emit again:contract.d.tsgains rows forcharandvarchar. Seets-contract-lists-extension-codecs,column-helpers-raise-type-params-invalidandsqlite-contract-d-ts-char-aggregatesin the app recipe. (#30547) -
A write on a collection that may have no filter does not compile. Chaining methods now return the collection's own class, so a filtered collection is a subtype of an unfiltered one.
update,updateAll,delete,deleteAlland theirAndCountforms on a collection filtered on some code paths only no longer compile.cursoranddistinctOncheck the order on the collection itself, so a cast on the argument no longer gets past the check. In a custom collection class, an override of a chaining method takes athisparameter, and a member namedapplyorscopewith another signature must be renamed. Read a collection's type state and row withCollectionTypeStateOf<C>andCollectionRowOf<C>, and drop explicit type arguments oninclude,distinctanddistinctOn. See the entries fromwrites-on-a-conditional-collection-are-refusedtochaining-methods-take-no-explicit-type-arguments, andscope-is-a-collection-member, in the app recipe. (#30560, #30564)const posts = search ? db.Post.published() : db.Post; await posts.deleteAll(); // now a type error: posts may have no filter
-
Bulk writes refuse a
limit,offset,cursorordistinctthey would ignore.updateAll,updateAndCount,deleteAllanddeleteAndCountchange every row that matches the filter. They used to ignore these, sowhere(...).limit(10).deleteAll()deleted every matching row. They now throwORM.ARGUMENT_INVALID.updatewith a relation callback throws on a collection with an order, limit, offset, cursor ordistinct. Afterlimit(0),updateanddeletechange no row and returnnull. Thefieldexported from@prisma/orm-postgres/contract-buildernow checks.default(...)values against the column type at compile time. Seewrites-refuse-what-they-would-ignoreandimported-postgres-field-checks-defaultsin the app recipe. (#30564)Before:
await db.orm.public.Post.where({ userId }).limit(10).deleteAll();
After:
const ids = (await db.orm.public.Post.where({ userId }).select('id').limit(10).all()).map((p) => p.id); await db.orm.public.Post.where((p) => p.id.in(ids)).deleteAll();
-
variant()takes the discriminator value, not the model name. In the SQL and Mongo ORMs,.variant()takes the value a variant declares in@@base(Task, "bug"). A value the model does not declare, or a call on a model with no discriminator, throwsORM.ARGUMENT_INVALID; before, it returned the collection unchanged and read every variant. A second.variant()call is refused: select each variant from the base collection. A model name that equals a declared value still compiles, so check each call against the contract. Seevariant-takes-discriminator-valuein the app recipe. (#30577)Before:
db.orm.public.Task.variant('Bug');
After:
db.orm.public.Task.variant('bug');
-
The cache middleware's store decides how long an entry lives.
cacheAnnotationno longer takesttl, and every annotated read is now cached, includingcacheAnnotation({}), which used to pass through. The default store keeps an entry for 60 seconds.skipis renamedbypass, and the typeCachePayloadis renamedCacheAnnotationOptions.createCacheMiddlewareno longer takesmaxEntriesorclock: pass them tocreateInMemoryCacheStoreand give that store tocreateCacheMiddleware({ store }). A customCacheStorenow keeps a version per key, withget({ key, meta }),set(entry, value)and a new requiredunset({ keys, meta }). Seecache-annotation-ttl-removed,cache-annotation-skip-renamed-bypass,cache-middleware-store-optionsandcache-store-object-argumentsin the app recipe. (#30530)Before:
const cache = createCacheMiddleware({ maxEntries: 500 }); db.sql.public.user.select('id', 'email').annotate(cacheAnnotation({ ttl: 60_000 }));
After:
const cache = createCacheMiddleware({ store: createInMemoryCacheStore({ maxEntries: 500 }) }); db.sql.public.user.select('id', 'email').annotate(cacheAnnotation({}));
-
prisma7Schemaandcontract inferread dates as text. A Prisma 7DateTimecolumn, and@db.Timestamp,@db.Timestamptz,@db.Dateand@db.Timecolumns, now read and write strings such as"2026-09-14 10:00:00.123", notTemporalvalues, so the application needs noTemporal.contract inferwritesTimestampString(p),TimestamptzString(p),DateStringandTimeString(p)for these columns. Emit again, change code that treats these fields asTemporalvalues, then runprisma db sign. A contract inferred earlier keeps its types untilcontract inferruns again. Seeprisma7-schema-date-types-are-textandcontract-infer-writes-text-date-typesin the app recipe. (#30553)Before:
model User { createdAt Timestamp(3) @default(now()) }
After:
model User { createdAt TimestampString(3) @default(now()) }
-
The
pg.sqlandsqlite.sqltags are removed. Writesql; the stored default does not change. Four PSL diagnostic codes for written values are renamed:PSL_UNKNOWN_DEFAULT_LITERAL_TAGis nowPSL_UNKNOWN_LITERAL_TAG,PSL_INVALID_JSON_LITERALis nowPSL_INVALID_LITERAL,PSL_DEFAULT_TYPE_INCOMPATIBLEis nowPSL_VALUE_TYPE_INCOMPATIBLE(orPSL_DEFAULT_LIST_EXPECTEDfor a single value on a list column), and most cases ofPSL_INVALID_DEFAULT_LITERALare nowPSL_INVALID_LITERAL. Seeprefixed-sql-tags-are-removedanddefault-diagnostic-codes-changedin the app recipe. (#30534)Before:
createdAt DateTime @default(pg.sql`(now() + interval '1 hour')`)After:
createdAt DateTime @default(sql`(now() + interval '1 hour')`) -
A Prisma 6 MongoDB
Intis stored as a BSON long. A contract read withprisma6Schema(...)gives a plainIntfield, andInt @db.Long, the newInt64Numbertype: still anumberin the application, but written as a BSON long, as Prisma 6 does. A document whose field holds a fraction now fails to read withRUNTIME.DECODE_FAILED: repair it, then emit again. ABytes @db.ObjectIdfield reads as a 24-digit hex string and refuses a 12-byteBufferorUint8Array. Seeprisma6-int-written-as-longandprisma6-bytes-objectid-is-hexin the app recipe. (#30521) -
db verify --strictreports unclaimed tables asCONTRACT.SCHEMA_VERIFICATION_FAILED. A database that holds tables no contract declares used to be reported asCONTRACT.MARKER_REQUIRED. The exit code is still 4.CONTRACT.MARKER_REQUIREDnow only means the database has not been signed. A script that matches the old code must match the new one. Seestrict-verify-unclaimed-codein the app recipe. (#30601) -
Some contracts get a new storage hash once.
contract inferwrites abyteadefault as a base64 literal,@default("aGVsbG8="), where it wrote asqlexpression; a contract inferred again gets a new storage hash. An index, check or policy whose SQL holds both--and a line break gets a new name, and the nextmigration plandrops and recreates it. In both cases, sign the database or plan the migration as the recipe says. Seecontract-infer-writes-bytea-default-literalsandsql-with-a-line-comment-gets-a-new-wire-namein the app recipe. (#30603, #30546) -
Mongo validators accept a
nulllist. For a nullable list such asString[]?, the collection validator now admitsnull, so the storage hash changes. Emit the contract again, then plan and apply a migration that updates the validator before you write anulllist. Seemongo-nullable-list-containersin the app recipe. (#30568) -
Extension authors: SQL data types declare their names, and codecs take their data type. Declare a SQL data type with
sqlDataType(id, { params, texts })from@internal/sql-contract/data-type; itstextsreplace codectargetTypes, thenativeType()andexpandNativeTypehooks andnormalizeNativeType. A codec'sparamsSchemais its data type'sparams, andpostgresCodecandsqliteCodectake the data type object. Run thedata-type-in-contractscript on your contract space, raise your peer dependency floor to this release, and publish a--data-typeline for each codec you own. The Postgres runtime driver now returns every column as server text, so a codec'sdecodereceives text. A built-in codec'sdecodeJsonrefuses JSON that is not a stored form of its type.SelectAstOptionstakeslocking, DDL nodes hold SQL asOpaqueSql,RenameCheckConstraintCallis nowRenameConstraintCall,ControlFamilyInstancegainssignSpaces, and the SQLite target dropssqlite/json,sqlite/datetimeandsqlite/bigint. See the extension recipe. (#30547, #30576, #30597, #30451, #30549, #30546, #30331, #30534)Before:
export const pgvectorVector: DataType = dataType('pgvector/vector', { listCast });
After:
export const pgvectorVector = sqlDataType('pgvector/vector', { params: pgvectorVectorParams, texts: [{ text: 'vector({length})', written: true, catalog: true }], listCast, });
Features
-
The typed SQL builder locks the rows a select reads. A select gains
forUpdate(),forNoKeyUpdate(),forShare()andforKeyShare(), each with optionalof,nowaitandskipLocked. The methods exist only where the database supports them, so they are absent on SQLite. Emit the contract again to get the capabilities that enable them (re-emit-for-the-row-locking-capabilities). (#30549)tx.sql.public.job.select('id').where((f, fns) => fns.eq(f.state, 'queued')).limit(1).forUpdate({ skipLocked: true }).build()
-
Scopes.
db.orm.scope(fields, body)defines a scope for any model that has the given fields, such as a soft-delete filter.db.orm.public.Post.scope(body)defines one for a single model, andCollectionRowOfnames the row it produces.orderByFieldturns a sort field from a request into a checked order.apply(fn)runs any scope on a collection. (#30564, #30560) -
Custom collection methods chain. Methods of a custom collection class stay available after
where,orderBy,limit,includeand the other chaining methods:db.Post.where({ userId }).published()compiles. (#30560) -
Middleware gains an
afterTransactionstage. It fires once per query when its effects are final, after the enclosing transaction commits or rolls back, withresult.outcomeset tocommitted,rolled-backorunknown. (#30614) -
The cache middleware can invalidate entries.
cache.invalidate({ keys })removes entries by the key acacheAnnotation({ key })named, andcache.invalidate({ meta })removes entries by themetaa read carried, with a store that indexes it.deriveKeyreplaces how keys are made for reads that name none. (#30530) -
A hand-written migration can rename a table.
this.renameTable({ table: 'userProfile', to: 'UserProfile' })in a migration made withprisma migration newrenames the table and the constraints and indexes named after it, on Postgres and SQLite, and keeps its rows. (#30331) -
List elements can be
null.String?[]allowsnullelements,String[]?allows anulllist, andString?[]?allows both. In TypeScript, write.many({ elementsNullable: true }). (#30051) -
db signsigns every contract space. It verifies the application's space and each extension's, then signs every space that verified in one transaction. (#30576) -
Hover in the PSL language server. Hovering a model, field, type or block shows its declaration and its
///doc comment. Hovering an attribute, argument key, function, constant or contributed type shows its signature and documentation. (#30569, #30591) -
Go to definition and find references in the PSL language server. Both work across every schema file of the project, for models, composite types, named types, blocks, fields and namespaces. (#30578, #30621)
-
Completion in block values. The language server completes the values of a block such as
policy_select, including in lists and function calls, and offersns.for namespaces. A block value can name an entity in another namespace, as intarget = auth.Account. Completion now follows the same name lookup as diagnostics. (#30567, #30545) -
prisma6Schemareads more Prisma 6 MongoDB schemas. It accepts every native type Prisma 6 accepts exceptDateTime @db.Timestamp, and a dotted index path such as@@index([author.name])gets a clear diagnostic in place of a parse error.prisma orm initrecognises a Prisma 6 MongoDB project. (#30521)
Fixes
- The Postgres driver returns every column as server text, so a
jsonbcolumn holding a JSON string such as"standard"reads back as that string. Before, it failed withRUNTIME.DECODE_FAILED, and"123"read as a number. Anintervalreads the text Postgres prints, and a computed boolean in a SQL builder select reads astrue, not't'. (#30597) - A query after
close()fails once withDRIVER.NOT_CONNECTED: Runtime is closed, and work already started finishes. Before, a query returned withoutawaitfrom anawait usingscope failed with a misleading marker error or an unhandled rejection. (#30533) contract inferandprisma7Schemacontracts work on Node withoutTemporal. A texttimestampcolumn filled withnow, including Prisma 7's@updatedAt, receives UTC time on a host outside UTC (text-timestamp-now-is-utc). (#30553)- A
Bytesliteral default and a pgvector list default no longer roll backdb init,db updateanddb migratewithMIGRATION.SCHEMA_VERIFY_FAILED. ABytesdefault may be written as base64 or as Postgres hex. (#30603) - On SQLite, a
now()default stores the same text the application writes, so rows that took the default compare and sort correctly. Existing tables keep their old default. (#30612) - On Postgres, verification accepts a column written with a type alias such as
varcharorint, or without a length, and anumericcolumn takes a negative scale. Library errors no longer include the connection string. (#30451) - A Postgres
setDefaultwith no default, or withautoincrement(), is refused withCONTRACT.DEFAULT_INVALIDbefore it writes an invalid statement. Asql/float@1list on Postgres reads as numbers, not strings. (#30552) - Three Postgres column defaults are read correctly: a
json[]default with an unquoted array element, ajsonb[]default holding a number too large for JavaScript, and an uncast nestedtext[]default. (#30501) - A raw SQL check, index or policy that ends in a
--comment produces valid DDL. (#30546) - A generated
migration.tswrites SQL that holds both quote kinds as a template literal, without backslashes. (#30554) migration statusanddb migrateresolve@contractand@db, and each command's help lists only the reference forms it accepts.db signanddb update --torefuse a reserved reference withMIGRATION.REF_WRONG_GRAMMAR. Without a connection, the suggested retry command keeps your flags. (#30475)- After handing migrations from Prisma 7 to Prisma 8, the first
migration plannames the baseline so it sorts before the change it plans. (#30601) - On MongoDB, a nullable list field accepts an explicit
null. Before, the database validator refused it. (#30568) - An unknown or misspelled type name reports one diagnostic,
PSL_UNRESOLVED_REFERENCE: Cannot find type "…", in both the SQL and Mongo families. A field preset written without a call, such astemporal.createdAt, reportsPSL_PRESET_NOT_CALLED. (#30563, #30538) - A
viewblock is read as model fields only in Prisma 7 and Prisma 6 schemas. In a Prisma 8 schema it is an unsupported block like any other. (#30507)