v8.0.0-rc.12
This release adds Postgres full-text search, prepared ORM reads and aggregates, schemas split across several files, and a way for a Prisma 7 project to use its existing schema.prisma as the Prisma 8 contract source. It also changes how a schema is written: a model without @@map now names its table exactly as written, every schema file needs // use prisma-8 as its first line, dbgenerated(...) is replaced by sql tagged literals, and a column default must be a value its column's data type accepts. The toolchain moves to @prisma/cli-engine@0.6.1, which no longer exports defineConfig.
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.
Breaking changes
-
The engine peer moves to
@prisma/cli-engine@0.6.1, andprisma.config.tsmust importdefinePrismaConfig.@prisma/orm-toolchainpeers the engine at an exact version, and this release peers 0.6.1 (up from 0.4.0). Projects assembled by theprismaCLI resolve the engine automatically; a project that pins@prisma/cli-engineitself must move the pin to0.6.1. The engine no longer exports the deprecateddefineConfigalias, so a config file that importsdefineConfigfrom@prisma/cli-enginefails to load until it importsdefinePrismaConfig. ThedefineConfighelper from a product package such as@prisma/orm-postgres/configkeeps its name. The new engine also changes how config files are read. It collects everyprisma.config.tsfrom the current directory (or from the file passed to--config) up to the repository root, which is the first directory with a.gitentry, and merges them key by key, with the nearest file winning; a project under a stray parent config now inherits its values, so remove that file or addparent: falseto the project's config. A relative path such ascontractormigrations.dirnow resolves from the directory of the config file that wrote it, not from the working directory. Under theprismaCLI, a malformedormfield is reported asCLI.CONFIG_FIELD_INVALID, naming the field and the file, insideCLI.CONFIG_SECTION_INVALID, where it used to beCONFIG.VALIDATION_FAILED. Seeengine-pin-moves-to-0-6-1,config-paths-resolve-from-declaring-fileanddefine-config-becomes-define-prisma-configin the app recipe. (#30372, #30129, prisma/prisma-cli#233, prisma/prisma-cli#279, prisma/prisma-cli#280, prisma/prisma-cli#284)Before:
import { defineConfig } from '@prisma/cli-engine'; export default defineConfig({ ... });
After:
import { definePrismaConfig } from '@prisma/cli-engine'; export default definePrismaConfig({ ... });
-
A PSL model without
@@mapnames its table exactly as written.model UserProfileused to read and write the table"userProfile". It now uses"UserProfile", and Mongo collections follow the same rule. Before you plan a migration, run theadd-model-mapscript from the upgrade recipe over every.prismafile, including thecontract.prismacopies undermigrations/. It adds@@map("<current table name>")to each model that has none, so the emitted contract, the storage hash and the database stay the same. Run it once, and only on a schema written for an earlier release. If you plan without it,migration plan,db updateandmigratestop withMIGRATION.TABLE_NAME_CASE_CHANGEDinstead of dropping the table and creating an empty one. Mongo has no planner, so an unmapped model reads an empty collection without any error; run the script before you deploy.contract inferfollows the same rule, so a table already named"UserProfile"now infers without@@mapand verifies clean. Seepsl-model-names-table-verbatimin the app recipe and the extension recipe. (#30317, #30321)Before:
model UserProfile { id Int @id email String }
After:
model UserProfile { id Int @id email String @@map("userProfile") }
-
Every PSL schema file needs
// use prisma-8as its first line.contract emitnow reads only the files that carry this header, which is how a schema split across several files knows its members (see Features). A file without it is left out of the contract without a warning, and when no file has it, emit fails withPSL_NO_OPTED_IN_SCHEMA_FILES. The older// use prisma-nextheader still counts.orm initalready writes the header, and the upgrade recipe has a script that adds it to every file that lacks it. Seepsl-schema-requires-use-prisma-8-directivein the app recipe. (#30379) -
dbgenerated(...)is removed, and a raw SQL default is written as asqltagged literal.@default(dbgenerated("..."))now fails withPSL_UNKNOWN_DEFAULT_FUNCTION, and the message names the replacement. Writenow()andautoincrement()as the named functions, a JSON value as ajsonliteral, an enum member or a text value as a quoted string, and any other SQL as@default(sql`...`).sql`now()`andsql`autoincrement()`are refused.contract inferprints raw defaults in the new form. The JSON and enum rewrites change the default incontract.jsonfrom an expression to a literal, so the storage hash moves; the live default already matches, so no migration is needed. In the TypeScript contract builder,.defaultSql('...')is deprecated and will be removed in 8.0.0: write.default(now()),.default(autoincrement())or.default(sql`...`)instead. A Prisma 7 schema read throughprisma7Schemakeeps itsdbgenerated. Seedbgenerated-removed-from-pslanddefault-sql-method-deprecatedin the app recipe and the extension recipe. (#30325, #30380, #30347)Before:
id String @id @default(dbgenerated("gen_random_uuid()")) createdAt DateTime @default(dbgenerated("now()")) expiresAt DateTime @default(dbgenerated("(now() + '00:03:00'::interval)"))
After:
id String @id @default(sql`gen_random_uuid()`) createdAt DateTime @default(now()) expiresAt DateTime @default(sql`(now() + '00:03:00'::interval)`)
-
A written default must be a value its column's data type accepts. Every value written in PSL now has a data type, decided by how it is written, and a column takes it only if the column's type accepts that type. A quoted string therefore no longer works as a JSON, decimal or float default. Write a JSON default as a
jsonliteral, a decimal as a bare number, andNaNandInfinitywithout quotes. A list default on a column that holds one JSON value is onejsonliteral, such as@default(json`[1, 2]`). A refused default fails withPSL_DEFAULT_TYPE_INCOMPATIBLEand names the types the column accepts. Numbers now keep every digit: aDecimalorNumericdefault emits as decimal text ("1.50"), and aBigIntdefault larger than 2^53 now emits instead of failing. A contract with such a default gets a new storage hash, so re-emit it and runprisma db sign. The same applies to aBigIntNumbercolumn (pg/int8number@1orsqlite/bigintnumber@1) with a literal default, which now stores digit text.contract inferprints each default in a formcontract emitreads back. Seea-json-default-is-a-json-tag,a-decimal-default-is-written-unquoted,a-float-non-finite-default-is-written-bare,a-json-list-default-is-one-json-literal,number-valued-64-bit-columns-store-their-default-as-digit-textandpsl-number-defaults-keep-digitsin the app recipe. (#30350, #30287)Before:
meta Jsonb @default("{}") price Decimal @default("1.50") ratio Float @default("NaN")
After:
meta Jsonb @default(json`{}`) price Decimal @default(1.50) ratio Float @default(NaN)
-
Creation timestamp presets use the application clock.
temporal.createdAt()andtemporal.createdAtString(), and the matchingfield.temporal.*helpers, no longer declare a database default. The ORM sets the value on create, from the same clock as the matchingupdatedAtpreset. Re-emit the contract and apply a migration that removes the old database defaults. After that, code that inserts rows with raw SQL must supply the timestamp itself. To keep a database-generated value, use an explicit timestamp type with@default(now()). A preset backed by Temporal now needs a globalTemporalbefore writes as well as reads. Seeclient-generated-created-at-presetsin the app recipe. (#30330) -
Re-emit Postgres contracts: the query operation types moved from the adapter to the target. The emitted
contract.d.tsnow importsQueryOperationTypesfrom@prisma/orm-postgres/target/operation-types. The old subpath,@prisma/orm-postgres/adapter/operation-types, is gone, so acontract.d.tsemitted by an earlier release stops type-checking until you runprisma contract emit. Change any import of the old subpath in your own code the same way.contract.jsondoes not change. Seere-emit-the-contract-for-the-moved-query-operation-typesin the app recipe. (#30348) -
preparecallbacks on the Postgres and SQLite clients receive only the params. The callback no longer gets a SQL builder as its first argument; use the client's own.sqlproperty instead. Calls to.query(target, params)do not change. Seeparams-only-sql-facade-preparein the app recipe. (#30260)Before:
const query = await db.prepare({ id: 'pg/int4@1' }, (sql, params) => sql.public.users.select('id').where((f, fns) => fns.eq(f.id, params.id)).build(), );
After:
const query = await db.prepare({ id: 'pg/int4@1' }, (params) => db.sql.public.users.select('id').where((f, fns) => fns.eq(f.id, params.id)).build(), );
-
Native Postgres enum columns no longer offer text operations. Postgres has no
LIKE,ILIKEor text search for an enum type, solikeandilikeon a native enum column always failed when the query ran. They are now type errors, the new full-text operations do not accept such a column, and@@fullTextIndexon it is refused when the contract is built. Compare the column witheqorininstead. An enum stored as text (@@type("pg/text@1")) keeps every text operation. Seenative-enum-columns-have-no-text-operationsin the app recipe. (#30390) -
The Postgres target decodes list columns. Enum list columns now read back as arrays on every path, including
create()results, without a cast in the SQL. Two values change. An element of a fixed-scale numeric list reads the way Postgres prints it: anumeric(30,10)[]element written as1.5reads as"1.5000000000". A row read directly through the lower-level Postgres driver returns a list column as raw Postgres array text, such as'{a,b}'. ORM and SQL builder reads still return JavaScript arrays. Update assertions and snapshots that pin those values. Seepostgres-target-owned-list-framingin the app recipe. (#30235) -
migration newpicks its starting point the waymigration plandoes, and three error codes are removed. Without--from,migration newused to build on the newest migration. It now starts from thedbref, or from an empty database when there are no migrations, and otherwise refuses withMIGRATION.PLAN_ORIGIN_UNKNOWN. Adbref on an empty migration graph is refused with a pointer tomigration plan, which writes the baseline. Pass--fromin scripts that relied on the old default. The CLI no longer looks for a single newest migration, so a migration history with two branches now reports the real error, such asMIGRATION.HASH_NOT_IN_GRAPH.MIGRATION.AMBIGUOUS_TARGET,MIGRATION.NO_TARGETandMIGRATION.NO_INITIAL_MIGRATIONare removed, andgraphTipandgraphTipHashare no longer in the JSONmetaof the errors that carried them. Seemigration-new-defaults-to-the-db-refandmigration-tip-error-codes-removedin the app recipe. (#30389) -
The Supabase extension's contract changed, so re-sign databases that use it.
@prisma/orm-extension-supabasenow declares the two nullable list columns it used to leave out (storage.buckets.allowed_mime_typesandstorage.objects.path_tokens), the 43 check constraints of its reference Supabase build, its native enum defaults as member values, and its JSON defaults asjsonliterals. Its storage hash changes, so runprisma db signagainst every database signed with the previous version; if you re-emit your own contract, do that first. Your owncontract.jsondoes not change. If your Supabase build's check constraints differ from the reference build (supabase/postgres 17.6.1.106),db verifynow reports the missing ones. Seesupabase-contract-declares-nullable-list-columnsandsupabase-contract-regenerated-from-the-reference-fixturein the extension recipe. (#30318, #30346, #30380) -
Changes for extension authors. These affect packages built on
@prisma/orm-framework, the@prisma/orm-family-*packages and@prisma/orm-toolchain. Each item names its change id in the extension recipe, except the last.- Every codec descriptor names the data type it represents in a required
dataType, and a pack registers its data types, with their casts, throughdataTypeson its component metadata. Casts replaceliteralTypesand each codec's list of accepted shapes,decodeJsontakes only the data type's canonical form, and PSL support for a data type is an authoring entry underauthoring.dataTypes. Seeevery-codec-descriptor-names-a-data-typeand the entries that follow it. (#30350) - A codec without params sets
paramsSchematoundefined, andvoidParamsSchemais removed (codec-without-params-has-no-params-schema). (#30372) - An extension that pins
@prisma/cli-enginemoves the pin to0.6.1, and a config section'svalidatereceives a secondprovenanceargument (engine-pin-moves-to-0-6-1). (#30372) emit()from@prisma/orm-toolchain/emitterrequires adeserializeContractoption and writescontract.d.tsin the order ofcontract.json(emit-requires-deserialize-contract). (#30319)QueryOperationTypesmoves from the Postgres adapter to the Postgres target (query-operation-types-move-to-the-postgres-target). (#30348)SqlLoweringSpecloses its unusedstrategyfield; delete it from operation descriptors (sql-lowering-spec-drops-strategy). (#30373)pg/enum@1no longer has thetextualtrait, so an operation declared ontextualno longer attaches to native enum columns (native-enum-codec-is-not-textual). (#30390)- For prepared queries, an expression's codec moves to
returnType.codec, ORM preparation uses the sharedPreparabletype,PreparedParamRefkeeps its declared nullability, and the limit and offset inCollectionStateandGroupPagingStatecan be expressions (expression-codec-on-return-type,shared-preparable-envelope,preserve-prepared-reference-nullability,preserve-orm-pagination-expressions,preserve-grouped-orm-pagination-expressions). (#30260, #30309) - A Postgres codec used for list columns receives each element as raw text (
postgres-list-element-codecs-receive-raw-strings). (#30235) parseRawDefaultis no longer exported fromfamily/psl-infer; importparsePostgresDefaultfrom@prisma/orm-postgres/target/default-normalizer(psl-infer-raw-default-parser-is-target-owned). (#30287)- Code that runs several inserts for one logical create passes one
defaultValueCacheto all of them (share-create-default-cache-across-inserts). (#30330) - The PSL parser API changed.
fieldAttribute,modelAttributeandblockAttributerequiredocumentation, andidentifier(name)takes{ documentation }as a second argument.entityRef()takes a selector, such asentityRef({ kind: 'model' }), and returns the declaration it resolved; useidentifier()for a name that is not checked.parse()requires a file name as its second argument, and the interpreter input takes adocumentslist in place ofdocument. Seepsl-attribute-specs-are-documented,psl-entity-ref-takes-a-selectorandpsl-parse-takes-a-file-namein the extension recipe. (#30312, #30344, #30335, #30379)
- Every codec descriptor names the data type it represents in a required
Features
-
Postgres full-text search. Text columns gain
fullTextMatches,fullTextRankandfullTextHeadlinein the ORM and the SQL builder. The query argument is atsquery, built withwebsearchToTsquery,plaintoTsquery,phrasetoTsqueryortoTsqueryfrom@prisma/orm-postgres/target/full-text, or with thetsquerytemplate tag, which turns each interpolated value into one quoted term so user input cannot add operators. A bare string is a type error.@@fullTextIndex([field])in PSL, orfullTextIndex(cols.field)in the TypeScript contract builder, creates the GIN index these queries use. Give the index and the operation the samelanguage; otherwise Postgres does not use the index.examples/prisma-8-demosearches post titles end to end. (#30348, #30386, #30376)model Post { id Int @id title String @@fullTextIndex([title], name: "post_title_search") }
import { websearchToTsquery } from '@prisma/orm-postgres/target/full-text'; const q = websearchToTsquery(input); const posts = await db.orm.public.Post.select('id', 'title') .where((p) => p.title.fullTextMatches(q)) .orderBy((p) => p.title.fullTextRank(q).desc()) .all();
-
A Prisma 7 schema as the contract source, on Postgres.
prisma7Schema('prisma/schema.prisma')from@prisma/orm-postgres/configreads a Prisma 7 schema directly, so Prisma 8 can run beside Prisma 7 on the database Prisma 7 migrates.contract emitanddb signwork as usual; run both again after each Prisma 7 migration. A construct Prisma 8 cannot describe exactly, such as aview, is an error that names the line and a Prisma 7 edit that removes it.prisma orm init --from-prisma7-schema prisma/schema.prismasets this up, and a plainprisma orm initin a Prisma 7 project offers to. It checks that Prisma 8 can read the schema before it changes anything, keeps Prisma 7 installed as@prisma/prisma7with its config renamed toprisma7.config.tsand its scripts pointed atprisma7, and writes the Prisma 8 config and client undersrc/prisma/. It does not touchprisma/or the database. (#30287, #30291)import { definePrismaConfig } from 'prisma/config'; import { defineConfig as ormConfig, prisma7Schema } from '@prisma/orm-postgres/config'; export default definePrismaConfig({ orm: ormConfig({ contract: prisma7Schema('prisma/schema.prisma'), db: { connection: process.env['DATABASE_URL']! }, }), });
-
Schemas split across several files. The
contractoption accepts a glob such as'./prisma/**/*.prisma'. Every matching file that starts with// use prisma-8becomes part of one schema, and a new file joins it on the next emit without a config change. The default output goes in the glob's fixed directory (./prisma/contract.json), andorm formatformats every file. Namespace blocks with the same name in one file now merge into one namespace. (#30379, #30343) -
Prepared ORM reads and aggregates. Inside
db.prepare(...), an ORM query can end in.prepared.all(),.prepared.first()or.prepared.aggregate(...), on ordinary and grouped collections. The query is built once. Each.query(target, params)call runs it with new values against the runtime, connection or transaction you pass, and returns the same result shape as the ordinary call. Reading included relations also does less work per row. (#30260, #30309, #30289, #30284)const byId = await db.prepare({ id: 'pg/int4@1' }, (p) => db.orm.public.User.select('id').prepared.first({ id: p.id }), ); await byId.query(runtime, { id: 2 }); // { id: 2 }
-
createAllandcreateAndCountcan skip rows that collide with a unique constraint. Pass{ onConflict: 'skip' }, and optionallyconflictOn: ['email']to name the constraint.createAllreturns only the rows the database wrote, andcreateAndCountcounts only those. Postgres and SQLite support it; multi-table inheritance variants refuse it. Re-emit your contract before you use it: the option needs two new capabilities that a contract from an earlier release does not list (re-emit-for-the-insert-conflict-skip-capabilitiesin the app recipe). (#30365) -
JavaScript
Datetimestamps on Postgres.TimestamptzJsDate(p)in PSL,field.temporal.timestamptzJsDate()in TypeScript, and thecreatedAtJsDate()andupdatedAtJsDate()presets read and writeDatevalues, with no Temporal polyfill. ADatekeeps milliseconds only. (#30288) -
Editor support for attribute arguments. The language server shows signature help for attribute arguments, completes values inside nested arguments (lists, records, function calls and field references), names the placeholders in its snippets, and suggests only scalar fields where an attribute expects one. (#30312, #30266, #30329)
-
Each finding when a contract source fails to load.
CONTRACT.SOURCE_LOAD_FAILEDcarries adiagnosticsarray, with one entry per finding giving its code, its summary and, where known, its file and line. The terminal prints them.meta.diagnosticsandmeta.issuesare unchanged. (#30287)
Fixes
- A command that reads a migration snapshot now checks that the file's content still matches the hash it is filed under, and stops with
MIGRATION.CONTRACT_SNAPSHOT_CONTENT_MISMATCHif the file was edited.migration checkreports the same problem asMIGRATION.CHECK_SNAPSHOT_CONTENT_MISMATCH. Before, an edited snapshot could makemigration planreport no changes. This covers SQL targets; Mongo snapshots are not checked yet. (#30086) migration planwarns when planning from thedbref would branch the migration history, and asks for consent before it writes a baseline with destructive operations, the waydb updatedoes (in scripts, pass--no-interactive --confirm <directory>).migration new --fromnow refuses a hash on an empty migrations directory, and a hash prefix that matches more than one migration, instead of ignoring them. (#30084)db init,db update,db sign,migrate,migration planandmigration newno longer needcontract.d.tson disk. They render the snapshot's types fromcontract.json, and refuse withCONTRACT.TYPES_RENDER_FAILEDbefore writing to the database if that fails. Before, a missingcontract.d.tsletdb initchange the database and then exit on a file error without setting the ref. Apackage.jsonthat depends on both@prisma/orm-postgresand@prisma/orm-mongois now reported asCLI.PROJECT_MANIFEST_INVALID. (#30293, #30298)contract emitwritescontract.d.tsin the order ofcontract.json, so your next emit reorders the models, fields and relations in that file and changes nothing else. (#30298, #30319)contract inferprints a nullable Postgres list column asType[]?instead of as a required list. (#30313)db verifyon Postgres reads more default forms as values: negative and cast numbers, enum values cast to a type in another schema,timestampvalues without a time zone, andARRAY[...]lists. Columns reported as different for these now verify clean. Introspection now reads with fixed session settings (TimeZone = UTC, ISO dates), so a contract inferred from a server outside UTC may show one difference in atimestamptzvalue inside a check constraint or index predicate; re-emit and re-sign once. (#30287)- A "now" value that the ORM generates for a
timestampcolumn without a time zone, such astemporal.timestamp(onUpdate: now), no longer fails at write time. (#30287) createAndCountreturns the number of rows the database inserted, not the length of the input array. (#30365)- The TypeScript contract builder reports a type error at
defineContractwhen a model's ids, uniques, indexes or foreign keys share a name. The check existed but never fired, so a contract that reuses a name now fails to type-check. (#30373, #30387) - In the TypeScript contract builder, a foreign key to a model in another contract space whose
.sql()stage is a function is now an authoring error (CONTRACT.FOREIGN_KEY_INVALID). Before, it produced aREFERENCESclause to a guessed lowercase table name. Give the target model a static.sql({ table: '...' }). (#30323) @@base(...)can name a model declared later in the file. An argument that names no model, or names something that is not a model, is reported at the argument asPSL_INVALID_ATTRIBUTE_SYNTAX;PSL_BASE_TARGET_NOT_FOUNDis removed. (#30344)--confirmnow works in an interactive terminal, and a command that prompted exits when it finishes instead of waiting for a key press. (prisma/prisma-cli#283)- The bundled
prisma-8agent skill: its upgrade references name the published@prisma/orm-*packages, its CI guidance deploys with onedb migratecommand, and its migration reference says thatmigration newrefuses adbref on an empty migration graph. (#30283, #30382, #30391)