github prisma/orm v8.0.0-rc.18

pre-release2 hours ago

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 sql literal. In PSL, @@index(where:), @@index(expression:), @@fullTextIndex(where:), @@check(expression:) and a policy's using and withCheck take a sql literal, and a quoted string is refused with PSL_VALUE_TYPE_INCOMPATIBLE. The app recipe ships a script that rewrites every .prisma file. In a TypeScript contract the same fields take a sql value, 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. See raw-sql-is-a-sql-literal, builder-raw-sql-is-a-sql-value, psl-storage-hash-may-change-once and ts-storage-hash-may-change-once in 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. beforeQuery and beforeExecute become beforeEncode; interceptQuery and interceptExecute become intercept.onQuery and intercept.onExecute; afterQuery and afterExecute become activityEnded; afterTransaction becomes activityFinalized. RuntimeMiddlewareContext is now ActivityContext, and ctx.planExecutionId is replaced by activity.id. activityEnded also fires for a statement the caller stopped reading and for one that failed before it ran, so a metrics middleware checks result.outcome. See middleware-hooks-are-activity-hooks and every-started-statement-is-ended-and-finalized in the app recipe, and statement-runners-replace-middleware-runners in 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. @@fullTextIndex and fullTextIndex store an index of type fullText with 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 run prisma migration plan, which renames the index in place. Avoid db update for a large index, because it drops and rebuilds it. See re-emit-contracts-with-a-full-text-index in the app recipe and render-full-text-index-document-takes-a-definition in 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.json and contract.d.ts, a polymorphic base's variants is keyed by discriminator value, and each entry names the variant model and its namespace. A contract in the previous form is refused with CONTRACT.VALIDATION_FAILED. Re-emit the contract and rewrite variants in the snapshots under migrations/snapshots/; the storage hash does not change. See re-emit-contracts-with-polymorphic-models and read-variants-by-discriminator-value in 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. See orm-refuses-names-that-are-not-fields in 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. Fragment is now QueryFragment and FieldFragment is now DeclaredFieldsFragment. collection.fragment(body) throws ORM.ARGUMENT_INVALID on a collection with chained calls, which it used to ignore; chain on the collection directly, or move the calls into the fragment's body. See fragment-types-are-renamed and fragment-is-made-from-the-root-collection in 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 plan asks before a lossy type change, and writes placeholders only when asked. On Postgres and SQLite, a type change that can lose values fails with CLI.CONSENT_REQUIRED where 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 Postgres migration.ts, alterColumnType takes type and using directly instead of an options object. See lossy-type-changes-ask, placeholders-only-when-asked and postgres-alter-column-type-plain-options in 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 plan and db update ask about it; add --delete <Model>.<field>, or --rename if the field was renamed, to scripts and CI. A MongoDB plan's preview writes db.getCollection("users") where it wrote db.users, and node migration.ts uses the config that owns the migration. See mongo-removed-field-asks, mongo-preview-get-collection and migration-ts-finds-its-config in the app recipe. (#30691)

  • A Prisma 7 schema's @ignore and @@ignore objects 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. See prisma7-ignore-keeps-storage in 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 is PSL_DUPLICATE_DECLARATION, and a top-level declaration cannot be named like the default namespace (public, or unbound on SQLite and MongoDB). See psl-default-namespace-duplicate-declaration and psl-default-namespace-name-is-reserved in the app recipe. (#30700)

  • Stricter types for TypeScript contracts and the SQL ORM. A contract built with defineContract types its namespaces with exactly the keys it has at runtime, and its ORM rows with each field's codec type instead of unknown. A model with no fields has no keys, so reading or writing one fails to compile; generic update code types its data as ScalarUpdateInput. conflictOn, connect and disconnect no longer accept null for a nullable unique column, and prepared.all and prepared.aggregate must be called on the prepared object. See typescript-contract-namespace-keys, model-with-no-fields-has-no-keys, generic-update-data-uses-update-input-type, unique-criterion-rejects-null and prepared-all-and-aggregate-need-their-receiver in the app recipe. (#30663, #30705, #30670)

  • db.nativeEnums lists native enum types only. A domain enum (enum block or enumType()) is no longer listed there; read it through db.enums, which holds its values as a query returns them. See native-enums-leave-out-domain-enums in 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-stored in the app recipe. (#30711)

  • Extension authors: SQL ORM collection state, MongoDB migration operations and the PSL symbol table changed. CollectionState requires tables, built with createCollectionTables, and names a variant by its discriminator value. The MongoDB DDL alphabet gains RenameCollectionCommand, and MongoDataTransformOperation is now MongoDmlOperation. buildSymbolTable requires defaultNamespaceId, and the symbol table places top-level declarations in its default namespace. See collection-state-carries-tables, sql-orm-state-names-a-variant-by-value, mongo-ddl-rename-collection, mongo-dml-operations and symbol-table-has-no-top-level in the extension recipe. (#30646, #30696, #30691, #30700)

Features

  • PSL mixins: declare shared fields, attributes or enum members once with model mixin, enum mixin or 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 where and orderBy callbacks 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) and fns.fullTextRank(indexes.post_search, q). (#30676)
  • whereUnique on SQL ORM collections addresses at most one record by a unique key; a many-record call after it, such as all() or limit(), is a compile error. (#30670)
  • firstOrThrow() on SQL and MongoDB ORM collections and on the SQL prepared view reads one row with LIMIT 1 and rejects with RUNTIME.NO_ROWS when nothing matches. (#30644)
  • On MongoDB, migration plan and db update carry 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() and supabase() take a log option, 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 sql tag interpolates other sql values, so predicates compose as sql`${owner} AND deleted_at IS NULL` instead of joined strings. (#30558)
  • prisma migration plan writes a contract's raw SQL into migration.ts as sql values, 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 OrderStatus in pg.enum(OrderStatus). (#30633, #30659)

Fixes

  • prisma db verify --strict passes on a database Prisma 7 built, and prisma contract infer no longer prints a model for Prisma 7's _prisma_migrations table. (#30671)
  • A repeated db update on a MongoDB database with a time-series collection no longer fails with "Cannot change immutable collection option 'timeseries'". (#30691)
  • The budgets() middleware enforces maxRows on the rows a query returns; the check never ran before, so a read over the limit now fails with BUDGET.ROWS_EXCEEDED. (#30703)
  • update({}) resolves the first matching row and updateAll({}) yields the matching rows, where they returned null and 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)

Don't miss a new orm release

NewReleases is sending notifications on new releases.