v8.0.0-rc.18
Raw SQL in a contract is now always a sql literal or value, a full-text index can span several weighted fields and is searched from the ORM by naming it, and middleware observes each statement and transaction as an activity. PSL gains mixins, the SQL ORM gains whereUnique and firstOrThrow, and MongoDB migrations carry out --rename.
The upgrade recipes for this hop: the app recipe and the extension recipe. Each breaking change below names the change ids to look for in them.
Breaking changes
-
Raw SQL in a contract is a
sqlliteral. In PSL,@@index(where:),@@index(expression:),@@fullTextIndex(where:),@@check(expression:)and a policy'susingandwithChecktake asqlliteral, and a quoted string is refused withPSL_VALUE_TYPE_INCOMPATIBLE. The app recipe ships a script that rewrites every.prismafile. In a TypeScript contract the same fields take asqlvalue, and a string does not compile; migration files keep strings. A text with shared indentation, blank lines at its ends or CRLF line breaks is stored as its canonical text, which changes the storage hash once. Seeraw-sql-is-a-sql-literal,builder-raw-sql-is-a-sql-value,psl-storage-hash-may-change-onceandts-storage-hash-may-change-oncein the app recipe. (#30550, #30558)Before:
@@index([email], where: "(archived_at IS NULL)", name: "users_email_active")After:
@@index([email], where: sql`(archived_at IS NULL)`, name: "users_email_active") -
Middleware hooks are activity hooks. The runtime runs every statement and transaction as an activity.
beforeQueryandbeforeExecutebecomebeforeEncode;interceptQueryandinterceptExecutebecomeintercept.onQueryandintercept.onExecute;afterQueryandafterExecutebecomeactivityEnded;afterTransactionbecomesactivityFinalized.RuntimeMiddlewareContextis nowActivityContext, andctx.planExecutionIdis replaced byactivity.id.activityEndedalso fires for a statement the caller stopped reading and for one that failed before it ran, so a metrics middleware checksresult.outcome. Seemiddleware-hooks-are-activity-hooksandevery-started-statement-is-ended-and-finalizedin the app recipe, andstatement-runners-replace-middleware-runnersin the extension recipe. (#30703)Before:
async afterQuery(plan, result, ctx) { if (result.latencyMs > 250) ctx.log.warn({ sql: plan.sql, rowCount: result.rowCount }); },
After:
async beforeEncode(plan, ctx) { ctx.state(statementSql)['sql'] = plan.sql; }, async activityEnded(activity, result, ctx) { if (result.kind !== 'statement' || result.latencyMs <= 250) return; ctx.log.warn({ sql: ctx.state(statementSql)['sql'], ...(result.returns === 'rows' ? { rowCount: result.rowCount } : {}), }); },
-
A full-text index is stored as data.
@@fullTextIndexandfullTextIndexstore an index of typefullTextwith its columns, weight groups and language, not a SQL expression. The schema source does not change, but the index's generated name and the storage hash do: re-emit the contract and runprisma migration plan, which renames the index in place. Avoiddb updatefor a large index, because it drops and rebuilds it. Seere-emit-contracts-with-a-full-text-indexin the app recipe andrender-full-text-index-document-takes-a-definitionin the extension recipe. (#30562)Before:
{ "expression": "to_tsvector('english', \"title\")", "name": "post_title_search_724b05e5", "type": "gin" }After:
{ "columns": ["title"], "name": "post_title_search_e0dd1131", "options": { "language": "english", "weightGroups": [["title"]] }, "type": "fullText" } -
A base model lists its variants by discriminator value. In
contract.jsonandcontract.d.ts, a polymorphic base'svariantsis keyed by discriminator value, and each entry names the variant model and its namespace. A contract in the previous form is refused withCONTRACT.VALIDATION_FAILED. Re-emit the contract and rewritevariantsin the snapshots undermigrations/snapshots/; the storage hash does not change. Seere-emit-contracts-with-polymorphic-modelsandread-variants-by-discriminator-valuein the app recipe. (#30696)Before:
"variants": { "Bug": { "value": "bug" }, "Feature": { "value": "feature" } }
After:
"variants": { "bug": { "model": "Bug", "namespace": "public" }, "feature": { "model": "Feature", "namespace": "public" } }
-
The SQL ORM client accepts only field names. Every name passed to the SQL ORM client must be a field of the model, or of the variant the collection is narrowed to; any other name, including a column name that differs from its field name, throws
ORM.FIELD_UNKNOWN. Rows no longer carry a column no field maps. Seeorm-refuses-names-that-are-not-fieldsin the app recipe. (#30667)Before:
await db.orm.Post.where({ user_id: 1 }).all();
After:
await db.orm.Post.where({ userId: 1 }).all();
-
The query fragment types are renamed, and a fragment is made from the model's root collection.
Fragmentis nowQueryFragmentandFieldFragmentis nowDeclaredFieldsFragment.collection.fragment(body)throwsORM.ARGUMENT_INVALIDon a collection with chained calls, which it used to ignore; chain on the collection directly, or move the calls into the fragment's body. Seefragment-types-are-renamedandfragment-is-made-from-the-root-collectionin the app recipe. (#30652, #30673)Before:
const recentSummary = db.orm.public.Post.orderBy((p) => p.id.desc()).fragment((posts) => posts.select('id', 'title'), );
After:
const recentSummary = db.orm.public.Post.fragment((posts) => posts.orderBy((p) => p.id.desc()).select('id', 'title'), );
-
migration planasks before a lossy type change, and writes placeholders only when asked. On Postgres and SQLite, a type change that can lose values fails withCLI.CONSENT_REQUIREDwhere nobody can answer, until--convert <Model.field>(write the conversion yourself) or--delete <Model.field>(accept the database's cast) answers it. A new required field gets a temporary value in existing rows; pass--backfill <Model.field>to fill them yourself. In a hand-written Postgresmigration.ts,alterColumnTypetakestypeandusingdirectly instead of anoptionsobject. Seelossy-type-changes-ask,placeholders-only-when-askedandpostgres-alter-column-type-plain-optionsin the app recipe. (#30695) -
Removing a field from a MongoDB model asks first. The plan now removes the field from every document that holds it, and
migration plananddb updateask about it; add--delete <Model>.<field>, or--renameif the field was renamed, to scripts and CI. A MongoDB plan's preview writesdb.getCollection("users")where it wrotedb.users, andnode migration.tsuses the config that owns the migration. Seemongo-removed-field-asks,mongo-preview-get-collectionandmigration-ts-finds-its-configin the app recipe. (#30691) -
A Prisma 7 schema's
@ignoreand@@ignoreobjects stay in the contract.prisma7Schema()keeps their columns, tables, keys and indexes in storage, so a schema that uses them gets a new storage hash, and a schema that loaded only because they were dropped can be refused. Emit the contract, then sign each database, or plan a migration first if Prisma 8 already manages your migrations. Seeprisma7-ignore-keeps-storagein the app recipe. (#30672) -
Top-level PSL declarations belong to the default namespace. A top-level declaration and one inside
namespace public { … }on Postgres share one set of names, so the same name declared both ways isPSL_DUPLICATE_DECLARATION, and a top-level declaration cannot be named like the default namespace (public, orunboundon SQLite and MongoDB). Seepsl-default-namespace-duplicate-declarationandpsl-default-namespace-name-is-reservedin the app recipe. (#30700) -
Stricter types for TypeScript contracts and the SQL ORM. A contract built with
defineContracttypes its namespaces with exactly the keys it has at runtime, and its ORM rows with each field's codec type instead ofunknown. A model with no fields has no keys, so reading or writing one fails to compile; generic update code types its data asScalarUpdateInput.conflictOn,connectanddisconnectno longer acceptnullfor a nullable unique column, andprepared.allandprepared.aggregatemust be called on thepreparedobject. Seetypescript-contract-namespace-keys,model-with-no-fields-has-no-keys,generic-update-data-uses-update-input-type,unique-criterion-rejects-nullandprepared-all-and-aggregate-need-their-receiverin the app recipe. (#30663, #30705, #30670) -
db.nativeEnumslists native enum types only. A domain enum (enumblock orenumType()) is no longer listed there; read it throughdb.enums, which holds its values as a query returns them. Seenative-enums-leave-out-domain-enumsin the app recipe. (#30661) -
A MongoDB enum member must be written as its codec stores it. A member that your own codec stores as a value it reads back differently is refused, with a message that says what to write. The built-in MongoDB codecs are not affected. See
mongo-enum-member-written-as-storedin the app recipe. (#30711) -
Extension authors: SQL ORM collection state, MongoDB migration operations and the PSL symbol table changed.
CollectionStaterequirestables, built withcreateCollectionTables, and names a variant by its discriminator value. The MongoDB DDL alphabet gainsRenameCollectionCommand, andMongoDataTransformOperationis nowMongoDmlOperation.buildSymbolTablerequiresdefaultNamespaceId, and the symbol table places top-level declarations in its default namespace. Seecollection-state-carries-tables,sql-orm-state-names-a-variant-by-value,mongo-ddl-rename-collection,mongo-dml-operationsandsymbol-table-has-no-top-levelin the extension recipe. (#30646, #30696, #30691, #30700)
Features
- PSL mixins: declare shared fields, attributes or enum members once with
model mixin,enum mixinor the mixin form of another block, and include them with+Name. The language server completes, navigates to, hovers, renames and colours them. (#30675, #30694, #30679, #30681) - A Postgres full-text index can cover several fields in weight groups, strongest first:
@@fullTextIndex([[title, subtitle], body], name: "post_search"). (#30562) - ORM
whereandorderBycallbacks receive{ fns, indexes }as a second argument, so a query can search a weighted full-text index by naming it:fns.fullTextMatches(indexes.post_search, q)andfns.fullTextRank(indexes.post_search, q). (#30676) whereUniqueon SQL ORM collections addresses at most one record by a unique key; a many-record call after it, such asall()orlimit(), is a compile error. (#30670)firstOrThrow()on SQL and MongoDB ORM collections and on the SQL prepared view reads one row withLIMIT 1and rejects withRUNTIME.NO_ROWSwhen nothing matches. (#30644)- On MongoDB,
migration plananddb updatecarry out--rename: a model rename renames its collection and keeps its documents, indexes and validator, and a field rename renames the field in every document. (#30691) postgres(),postgresServerless(),sqlite()andsupabase()take alogoption, so lint warnings, contract-marker warnings and errors swallowed from middleware hooks reach your logger. (#30707)- A polymorphic base can have variants in other namespaces:
@@base(a.Task, "chore"). (#30696) - The
sqltag interpolates othersqlvalues, so predicates compose assql`${owner} AND deleted_at IS NULL`instead of joined strings. (#30558) prisma migration planwrites a contract's raw SQL intomigration.tsassqlvalues, as a TypeScript contract writes it. (#30677)- The PSL language server renames symbols, and go-to-definition, hover, find references and rename work on the entity name inside a type-constructor argument, such as
OrderStatusinpg.enum(OrderStatus). (#30633, #30659)
Fixes
prisma db verify --strictpasses on a database Prisma 7 built, andprisma contract inferno longer prints a model for Prisma 7's_prisma_migrationstable. (#30671)- A repeated
db updateon a MongoDB database with a time-series collection no longer fails with "Cannot change immutable collection option 'timeseries'". (#30691) - The
budgets()middleware enforcesmaxRowson the rows a query returns; the check never ran before, so a read over the limit now fails withBUDGET.ROWS_EXCEEDED. (#30703) update({})resolves the first matching row andupdateAll({})yields the matching rows, where they returnednulland no rows. (#30680)- Two enum members are refused as duplicates only when their stored values are equal, so dates a millisecond apart and distinct object members are accepted. (#30661)
- A migration that makes a field required while rows have no value for it fails with a summary that counts those rows and names the fix. (#30695)