npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aventara/prisma7-adapter

v1.0.0-rc.12

Published

The Prisma 7 adapter for Aventara.

Downloads

1,923

Readme

@aventara/prisma7-adapter

The Prisma 7 Adapter for Aventara. It discovers what a Prisma schema and its generated client can do, compiles that into an Aventara AvAdapterModel, and executes validated Aventara operations against a Prisma client the application owns.

This README is for the person setting the adapter up in an application. It covers what the adapter does, what it does not offer, and the two operations whose guarantee is weaker than their names suggest. Everything you need is in the body of this file, not in footnotes.

  • Status: release candidate. Two engines are proven, PostgreSQL and SQLite. Every other Prisma provider is unmeasured, and this file says so where it matters.
  • Peer dependencies: @prisma/client ^7.10.1 (required); prisma, @prisma/adapter-better-sqlite3 and @prisma/adapter-pg ^7.10.1 and typescript >=5.7.0 <7 (optional). The error map targets 7.10.0, and the adapter is bound to Prisma 7's error surface; see Prisma 7 only.

The application owns the Prisma client, and a driver adapter is required

createPrismaAdapter takes a client that is already constructed. It never constructs one, never reads a connection string, and never reads an environment variable. The application owns the client's lifetime, its pool, its logging, its extensions and its $disconnect.

Prisma 7 reaches a database through a driver adapter, so the client you hand over must have been built with one. There is no fallback path, in Prisma or here.

import { PrismaPg } from "@prisma/adapter-pg";
import { createFramework } from "@aventara/core";
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
import { PrismaClient } from "./generated/client.js";          // your generated client
import { discovery } from "./generated/aventara.artifact.js";   // the generate step's output

const client = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});

const adapter = await createPrismaAdapter({
  client,
  discovery,
  provider: "postgresql",                 // declared, never sniffed
  driver: "@prisma/adapter-pg",           // declared, never sniffed
  // Adapter-owned, and not part of Aventara's public configuration:
  transactionOptions: { timeout: 10_000, isolationLevel: "Serializable" },
});

const framework = await createFramework({ api: "/api", adapter });

The caller declares provider and driver. The adapter does not inspect client internals to guess them: the driver decides whether transactions are advertised at all, and a wrong guess would be worse than a declaration that is checked.

createPrismaAdapter is async. Construction verifies the discovery artifact, and the digest it checks is a crypto.subtle promise, so every check runs once, here, before an adapter exists.

describePrismaAdapter: the same facts, with no client

describePrismaAdapter({ discovery, provider, driver }) returns the adapter's passive facts: the model, the driver's transaction capability, the referential actions in Aventara's words and the computed fields the generated types were written with. It constructs nothing and connects to nothing, so aventara-build (@aventara/build) compiles the Contracts from it at build time. Spread into createPrismaAdapter with the client, it is the adapter's options, and the adapter states exactly the facts the description does:

// src/aventara.adapter.ts
export default describePrismaAdapter({
  discovery,
  provider: "postgresql",
  driver: "@prisma/adapter-pg",
});

// where the framework is created
import description from "./aventara.adapter.js";
const adapter = await createPrismaAdapter({ ...description, client });

createPrismaAdapter still verifies the artifact and checks the client against it; the description does neither, because it has no client to check.

maxWait, timeout and isolationLevel are adapter-owned

They are provider concepts. Core's Adapter.transaction(work) interface carries no options and will not grow any, so these live on createPrismaAdapter({ transactionOptions }) and are not Aventara public configuration. Prisma's own defaults apply when they are absent: maxWait 2000 ms, timeout 5000 ms.

The supported isolation levels come from the declared provider. PostgreSQL admits ReadUncommitted, ReadCommitted, RepeatableRead and Serializable; SQLite admits Serializable only. Asking for a level the provider does not support fails the transaction; see P2028.


The generate step, and the artifact it emits

Discovery runs once, at generate time, and writes a plain-data artifact. At runtime the adapter loads that artifact, verifies it, and re-derives nothing.

The step ships with the package, as a binary and as a second export:

prisma generate                           # your client, as usual

npx aventara-prisma7-generate \
  --schema   prisma/schema.prisma \
  --client   src/generated \
  --provider postgresql \
  --driver   @prisma/adapter-pg \
  --out      src/discovery.artifact.ts \
  --config   src/aventara.config.ts

--provider and --driver are declared, never sniffed. The provider is recorded in the artifact, and the step refuses a --provider that the schema's datasource block contradicts (provider = "sqlite" with --provider postgresql), naming the flag to pass. The driver decides AvAdapterModel.transactions, a driver fact the schema cannot state. Declare the driver the application will actually construct the adapter with. The two cannot drift silently, but they can disagree, and core refuses construction when they do.

The step runs the Prisma 7 CLI itself: @prisma/prisma7 (bin prisma7, the Prisma 7 CLI under its own name) when it is installed, otherwise prisma only when its version is in this package's declared prisma range. npm i prisma now installs a Prisma 8 release candidate, so a project that has Prisma 8 installed keeps @prisma/prisma7 beside it. With neither usable, the step refuses in one sentence naming both. TypeScript 6 is supported; TypeScript 7, which has no classic compiler API, is refused in one sentence.

--config is optional. It names your Aventara configuration module (src/aventara.config.ts). The step neither reads nor runs it: aventara-build (@aventara/build), which aventara:prepare runs right after this step, reads the configuration's TypeScript type and writes which fields its compute.create and compute.update stages fill, per layer, to computed-fields.generated.ts beside the artifact (the directory the configuration imports its Resource tree from). The artifact imports that sibling file: computedFields (also discovery.computedFields) and its type, which the model's type carries. A variant's guards, pipes and filters, which run before write compute, may then leave exactly those fields out of a create; every other required field stays required. On a fresh install the file is not there until aventara-build has run. Without --config, or when the configuration is not a literal where a compute stage is written (aventara-build prints where), the artifact states nothing and those stages may leave any create field out. When the framework starts, it compares the statement with the configuration and warns (AVENTARA_STALE_GENERATED_TYPES) if you changed a compute stage without running aventara:prepare again.

--out must not be inside --client: Prisma's next prisma generate clears that directory and would delete the artifact. The step refuses such an --out, and one that names a directory, in one sentence before Prisma runs.

The same step can run from code, for a project that already has a build script:

import { generatePrismaDiscoveryArtifact } from "@aventara/prisma7-adapter/generate";

await generatePrismaDiscoveryArtifact({
  schema: "prisma/schema.prisma",
  clientDirectory: "src/generated",
  provider: "postgresql",
  driver: "@prisma/adapter-pg",
  artifactFile: "src/discovery.artifact.ts",
});

The two entries have different dependencies. @aventara/prisma7-adapter depends on @aventara/core and your generated client. @aventara/prisma7-adapter/generate also needs typescript and prisma, which are declared as optional peer dependencies. They are not installed for you, and nothing reachable from the runtime entry imports either, so no compiler and no Prisma tooling enters a deployed runtime. If you call the generate entry, have both in your project's devDependencies; a project that generates a Prisma client already does.

The step reads two sources at the same time, the schema.prisma text and the generated client's own public types, and cross-checks them against each other. It emits a TypeScript module exporting one discovery value with eight members:

  • the canonical AvAdapterModel (scalars, enums, Resources with fields, identifiers, available operations and create requirements, plus the transactions capability and the referenceWrites limit);
  • the Prisma addressing each Resource is reached by;
  • the resolved referential actions per Resource;
  • a record of the generated client it was read from (each model and its field references);
  • a modelHash;
  • the prismaVersion and database provider it was generated against;
  • the artifact's format (regenerate_with_aventara_prepare).

Each Resource's create requirements are what a create cannot do without, read off the question marks of the generated create inputs: required lists the columns with no default (["title"]), and oneOf each required relation with the fields that hold its reference ([["author", "authorId"]]: write author, or authorId, not both). parentSupplies says which of them a nested create gets from its parent ({ "Author": { "posts": ["author", "authorId"] } }). They are part of the model, so they reach every Contract and the generated client: a create that leaves one out is a type error, and at runtime, after your pipes and computed behaviours have run, it is answered A2004 with a V1000 issue at its path (["arguments", "data", "title"], or ["arguments", "data", "author"] for a required relation) before the database can refuse it.

The module also exports the model's type by name, AvModel (typeof model is that type). A project that emits declarations (Nest's declaration: true) writes that name wherever an exported declaration holds the model, such as the scaffold's aventaraConfig(), an AppFramework alias or defineResource<typeof model>(), so those declarations stay under a kilobyte at any schema size. Before the name, each mention wrote the whole model out, about 0.5 MB over 50 models.

modelHash digests the model and nothing else. The addressing table, the referential actions and the client record sit beside it and outside the hash. A database-side rename or a foreign-key action changes no semantics; inside the hash, either would move the Contract identity and force every generated client to be regenerated for nothing.

On the runtime path there is no TypeScript program, no PSL parser and no Prisma tooling. The loader reads no file: the artifact arrives as a value you import. The adapter reads one: the installed @prisma/client's package.json, for its version.

At construction, before the application listens, the adapter checks three things and fails hard with an instruction to regenerate if any is wrong:

  1. the artifact matches itself: the modelHash over its own canonicalized model;

  2. the installed @prisma/client major is the one the artifact was generated against. The version is read from @prisma/client/package.json, resolved from the adapter's own location and then from the working directory. Where it cannot be read (a runtime without Node's module loader, a bundle deployed without @prisma/client), the adapter starts and emits a warning, code AVENTARA_PRISMA_VERSION_UNCHECKED, saying the majors were not compared; installedPrismaVersion overrides the version read;

  3. the Prisma client you pass is the one the artifact was generated from: the same models, and the same field references (prisma.<model>.fields) on each. A schema change followed by prisma generate but not by the generate step (a model or field added, removed or renamed) is refused in one sentence naming what differs:

    AvPrismaDiscoveryArtifactError: The Prisma client has changed since the discovery artifact was generated (model `Comment` is in the client and not in the artifact): run `npm run aventara:prepare` (the adapter's `aventara-prisma7-generate`) to regenerate it, then restart.

    A value with no recognisable model delegate (a test double, a deep mock) is not compared.

The client check does not see a change that leaves every model and field name in place, such as a type, a default or an attribute. Run the generate step in the same build that runs prisma generate (a project set up by aventara does, from postinstall and aventara:prepare), and deploy the artifact with the schema and client it came from.


What is advertised, what is not, and why

Operation availability is derived, never assumed. Every omission is decided in one place, from three facts: the generated delegate's own method set, the Resource's identifiers, and the driver's transaction capability.

For a typical schema the compiled Contracts advertise fifteen distinct family.variant operations, identically on all three Contract scopes. delete.many is not one of them.

delete.many is not offered: use delete.count

Prisma 7.10.0 has no deleteManyAndReturn. delete.many owes its caller the deleted rows, and no provider call returns them, so the variant is omitted from every compiled Contract instead of being emulated. Core refuses a request for it with A2002 UNKNOWN_OPERATION before the adapter is reached, and no statement is issued.

To delete a filtered set, use delete.count. It answers a number, including zero.

Provider-gated capabilities are read from the client's own types, never from a provider table

No module in this package branches on provider. A capability is advertised when the generated client's own public types admit it. The provider name is not even in scope where capabilities are derived, so if (provider === …) is a compile error. A provider table would be a second source of truth, and it would drift one Prisma release at a time.

On PostgreSQL and SQLite, read from the generated types:

| Capability | PostgreSQL | SQLite | Read from | |---|---|---|---| | Scalar list fields | advertised | not advertised: the connector rejects a list of primitives at schema validation | the schema's own validation plus the generated field set | | JSON path filter | advertised | omitted: the operand is a JSONPath string here and string[] there, and one protocol name cannot carry both | the written operand shape on JsonFilter.path | | Case-insensitive mode, JSON ordering operators, field-reference in/notIn operands | as the generated filter types admit | as the generated filter types admit | <Kind>Filter's own members | | Explicit null ordering | advertised | advertised | SortOrderInput's admission, per field | | create.many / update.many (row-returning bulk writes) | advertised | advertised | createManyAndReturn / updateManyAndReturn on the delegate | | Transactions | "interactive" | "interactive" | the driver, not the provider (see below) |

Nothing beyond PostgreSQL and SQLite is measured. Cloudflare D1 is SQLite through @prisma/adapter-d1: it reads the same generated client, so its capabilities are SQLite's, and it is measured in process through Miniflare's D1 binding, where it differs from SQLite in two ways (no transactions, and a bigint within ±(2^53 − 1); see below). MySQL, SQL Server, MongoDB and CockroachDB were not generated against. Because every capability is read from the client you actually generated, a provider that lacks something advertises less instead of failing at runtime, but this release makes no claim about those providers.

update.many and create.many cannot project a to-many relation

On update.many and create.many, Prisma's row-returning bulk writes reach to-one relations only. The generated *SelectUpdateManyAndReturn / *IncludeUpdateManyAndReturn admit the scalars plus a to-one relation and carry no to-many relation and no $count, where the unrestricted *Include carries both.

A to-one projection on update.many works. A projection reaching a to-many relation or $count on these two variants is refused before any statement runs: A2004 (a validation error) with the issue V1008, and nothing is written. The Contract advertises each relation's projection per position, and on these two variants the position the bulk result is read in does not include to-many relations.

Keep update.many/create.many projections to scalars and to-one relations, and read to-many relations with a separate find. The adapter does not fill the gap with a follow-up read, which would add a statement and a new consistency window to a bulk write.


update.first and delete.first are framework-manufactured conveniences

Read this section before using either.

Aventara's first-style write takes a general filter, lets the ordering decide which record is "first", limits the effect to that one record, and returns the affected record, all in one call. ORMs almost never offer this, because SQL splits the ingredients between dialects: UPDATE … ORDER BY … LIMIT is a MySQL/MariaDB extension while RETURNING is PostgreSQL/SQLite/MSSQL, so whichever dialect grants the limit denies the returning. Prisma 7.10.0's operation set has no updateFirst and no deleteFirst at all.

The framework builds these two operations out of two statements, inside one adapter-internal transaction:

  1. findFirst with the caller's where and orderBy, selecting the Resource's identifier. The orderBy selects which record; ordering and single-record selection are natural on a read. No match answers A1001 NO_MATCH with null data, and nothing is written.
  2. update / delete narrowed to the identifier the read returned, with the caller's original predicate re-asserted alongside it, carrying the caller's own projection, so the record the contract owes comes back from the write itself. Nothing extra is read, and nothing is stripped.

They are therefore non-atomic, and they have a defined conflict outcome. The Contract's own vocabulary calls them manufactured, non-atomic variants, and neither is ever described as atomic.

What re-assertion closes, which race that is, and the residual it leaves

Re-asserting the caller's predicate on the write closes the wrong-record race. Without it, the write would go by identifier alone and could change a record that no longer matches what the caller asked for. With it, the write can only reach a record the read selected. A record that left the predicate in between is detected instead of changed: it answers A2014 STALE_SELECTION (409) with nothing written, and a retry may succeed.

The published guarantee is: the record written is one the read selected, or nothing is written and the caller is told.

Re-assertion does not close stale firstness, and nothing can. Between the read and the write another writer can insert or update a record that sorts ahead of the selected one and still matches the original predicate. Neither statement sees it (the read never saw it, and the re-asserted write succeeds against the record it was given), so update.first can change a record that is no longer the first ordered match and report ordinary success. Nothing detects or reports this, and no re-assertion can, because the missing fact is about a record that was never read.

This is a documented, permitted property of a framework-manufactured variant. Serializable isolation does not fix it. Core's Adapter.transaction(work) interface carries no isolation parameter, so an adapter cannot set a level for a manufactured variant or for a plan, and a provider transaction's level is fixed before its first statement, so a per-operation escalation cannot be expressed at all. SQLite admits Serializable only, and the residual persists there regardless.

If your operation's correctness depends on the record still being the first ordered match at the moment of the write, do not use update.first/delete.first. Select the record yourself and use update.unique/delete.unique on an identifier, inside a transaction plan whose other operations make the invariant you need checkable.

Wherever this README says a race is closed, it names which race.

Why the write is by identifier and not by Prisma's limit

Prisma does offer a top-level limit on updateMany, deleteMany and updateManyAndReturn in 7.10.0, on every model on PostgreSQL and SQLite. Aventara exposes what the ORM offers, so this is not a choice to hide it.

limit cannot express this operation. None of those three argument types carries an orderBy, so limit: 1 affects an arbitrary matching row, and two identical calls can affect different rows. The identifier from the ordered read is what makes the write deterministic.

Aventara does not currently expose an arbitrary affected-row cap. If it ever does, its name will make clear that it is not an ordered window.


Errors

Branch on the code, never on the message text

Every failure this adapter originates carries framework-authored text and nothing read off the Prisma error. No table name, constraint name, column name, SQL fragment, connection string, file path or stack reaches a caller. The normalizer never reads the provider's message, so there is no provider string in scope to forward. This package's own conformance gate sweeps the whole serialized envelope of every failing path for provider markers.

So branch on the code. Message text is framework prose, may change, and says less than the provider did.

The three conflict codes

Three different situations each have their own code.

| Code | Means | Retry? | |---|---|---| | A2008 CONFLICT | The operation would violate a uniqueness or reference constraint the Resource declares. Raised from Prisma's P2002 and P2003. | No. Re-running the same request unchanged fails the same way. | | A2013 CONCURRENT_MODIFICATION | Another operation modified the same state concurrently, so this operation was not applied and nothing was written. Raised from Prisma's P2034. | Yes, the same request may succeed. The framework performs no automatic retry. | | A2014 STALE_SELECTION | The state this operation had selected changed before its effect could be applied, so nothing was written. Raised by the adapter's update.first and delete.first. | Yes, the same request may succeed. No automatic retry. |

A2008 was narrowed to the constraint case, not repurposed. If you previously treated A2008 as "any conflict", the two situations that left it now have their own codes.

A2008's narrowed meaning lives in the specification, not in the wire text. The emitted message stays "The operation conflicts with the current state of the resource." The words "uniqueness" and "constraint" are the provider's own words for this failure, and the sanitization rule bans them from a provider-originated envelope because it cannot tell a framework-authored "constraint" from Prisma's. So this README can say A2008 means a uniqueness or reference constraint, but the envelope does not say so, and nothing should be parsed out of it.

A write conflict and a deadlock share A2013. Both arrive as P2034 with identical meta keys and differ only in a provider-owned code (40001 versus 40P01) that never crosses the boundary. They cannot be told apart here, so they get one outcome.

None of the three carries cause.issues

A2008, A2013 and A2014 pair with no validation code, and cause.issues is always absent on them. A conflict is not a Framework Validation Layer failure: validation codes determine the outcome code and roll up only to the validation outcomes, so a conflict validation code would have no legal rollup. Do not write a caller that expects issues on a conflict.

P2034 is never retried automatically

Neither Prisma nor this adapter retries a write conflict, and the adapter will not grow a retry. A transaction plan's operations run developer-authored pipeline callbacks, and re-running a plan re-runs those callbacks, including after hooks whose external side effects are not rollback-safe (see below). The caller makes a retry, after deciding its own operations are safe to repeat.

An after hook's external side effect inside a plan runs before the commit

Inside a transaction plan, a pipeline after hook runs inside the provider transaction, and therefore before the provider commits. If a later operation fails, the provider rolls back: the row is absent but the external effect has already happened. An email was sent, a queue message published or a cache written, for a record that does not exist.

The specification describes this behaviour, and this package's conformance gate asserts it against a real provider commit. The duty falls on the hook author: an after hook SHOULD be observational or idempotent, and MUST NOT be relied on to undo a mutation. If an effect must not happen unless the plan commits, do not perform it in an after hook.

P2028 covers seven situations, and three of them are separable

P2028 is Prisma 7's transaction code, and seven internal classes share it. The adapter answers A3002 TRANSACTION_ERROR wherever it is met, and never reads the message text to decide anything.

What can be told apart from the error's structure, measured on PostgreSQL and SQLite:

| Situation | Code | meta | |---|---|---| | Transaction-timeout expiry | P2028 | { operation, timeout, timeTaken }: the expiry is identifiable | | maxWait acquisition timeout | P2028 | empty: identifiable by that emptiness, and by nothing finer | | Valid-but-unsupported isolation level (e.g. Snapshot on either engine) | P2028 | { isolationLevel } | | An isolation level outside Prisma's own set (e.g. "Bogus") | P2023 | { providedIsolationLevel }, a different code entirely |

The other four classes (transaction not found, already committed, already rolled back, and an internal consistency failure) carry no meta at all and differ only in their message text. Reading error text to tell outcomes apart is not allowed here, so those four cannot be told apart, and the acquisition timeout cannot be told apart from them. That limit is Prisma's, and it is recorded here so nobody later works around it by parsing messages.

A P2028 that ends the transaction rejects out of $transaction and becomes A3002. A P2028 raised by a delegate call (or a raw statement) inside the callback, such as a step that runs after the timeout passed, is in the closed code table and also becomes A3002, attributed to that operation (cause.operation). In rc.1 that second case was unclassified and answered A3001.

Everything unrecognised becomes A3001

The code table contains only codes that were induced against a real engine on a real schema. A Prisma code the table does not contain is left to core's adapter boundary, which answers A3001 ADAPTER_ERROR with core's own text and sends the raw error to the diagnostics sink. Adding a row later is additive; a guessed row, once shipped, cannot be taken back.

Prisma 7 only

The code table's keys are P-codes, which are Prisma 7.x's error surface. Prisma 8 (a release candidate in October 2026) is a rewrite: no @prisma/client, no prisma generate, no driver adapters, and dotted NAMESPACE.SUBCODE errors with no P-codes yet. A Prisma 8 adapter will therefore be a new adapter package, @aventara/prisma8-adapter, instead of a re-map of this one. A project keeping Prisma 7 beside a Prisma 8 install runs the Prisma 7 CLI as @prisma/prisma7 (bin prisma7), which the generate step prefers. The bound is enforced: the package's own suite reads the installed @prisma/client's version and fails if the major is not 7, and the discovery artifact records the major it was generated against.


Transactions

Adapter.transaction is mounted exactly when the declared driver supports an interactive transaction. It is one client.$transaction(async (tx) => …) per Aventara transaction plan:

  • It uses the callback form only, never the array form. An Aventara plan is sequential by contract, so operation n may consume operation n − 1's result, and the second call's arguments do not exist until the first has answered. The array form takes every call up front, so it cannot carry a plan at all, and the interface this adapter holds cannot even represent it.
  • The root client is unreachable from inside the callback: the executor core is handed closes over the transaction client and nothing else.
  • The value the callback threw is rethrown by identity. The adapter adds no try, no catch and no wrapper, because core's transaction interface requires that, and wrapping would turn every plan failure into an integrity failure.

A first-style variant inside a plan reuses the plan's transaction instead of opening a nested one, so its two statements roll back with the plan, not independently.

Prisma swallows its own rollback failures: the rollback's rejection reaches an internal debug logger and never the caller. This adapter therefore never claims a rollback succeeded, because it cannot observe that it did. A commit failure is different: it rejects like a callback throw.

transactions is a driver fact, and it reaches the ClientContract hash

@prisma/adapter-d1 declares transactions: "none": Prisma's client refuses an interactive $transaction on D1 itself. Prisma still opens its own implicit transaction for a nested write (a $create under a relation, a $connect), and the D1 driver runs it as single statements, so a nested write is not atomic on D1: when a later statement fails, the rows the earlier ones wrote are kept. @prisma/adapter-pg and @prisma/adapter-better-sqlite3 declare "interactive", observed by running one.

Plan for the consequence. The transactions capability is part of the ClientContract hash, and a generated client fails hard on a hash mismatch. So one schema, one framework configuration and two deployment drivers produce two ClientContracts, two hashes and two generated clients, and a client generated against a "none" deployment will not run against an "interactive" one, or the reverse.

Generate your client against the deployment's own driver, and treat the driver as part of the deployment's identity, not as a connection detail. If you deploy the same application over D1 and over PostgreSQL, those are two deployments with two generated clients.

An artifact and a driver cannot drift silently either: an artifact that says "interactive" loaded by an adapter whose declared driver says "none" makes framework construction fail with a capability mismatch.


Referential actions

The artifact carries the referential actions, the adapter states them to core, and core refuses a transaction plan that depends on rows a cascade removed.

The generate step resolves each relation edge's referential actions, including the defaults Prisma applies when the PSL argument is absent. It cross-checks them while reading the schema, and emits them as the artifact's referentialActions member:

discovery.referentialActions;
// {
//   Shop:  { owningEdges: [] },
//   Item:  { owningEdges: [{ field: "shop", target: "Shop",
//                           cardinality: "one", referenceFields: ["shopId"],
//                           onDelete: "cascade", onUpdate: "cascade" }] },
//   Clerk: { owningEdges: [{ field: "shop", target: "Shop",
//                           cardinality: "one", referenceFields: ["shopId"],
//                           onDelete: "set-null", onUpdate: "cascade" }] },
// }

Read it in one direction. onDelete is declared on the child and fires when the parent is deleted, so an entry under Resource R says deleting a target row does onDelete to rows of R. To answer "what does deleting a Shop do?", scan every Resource's owningEdges for target === "Shop" and read the enclosing Resource key: above, that is Item (cascade) and Clerk (set-null). Both ends of that sentence are Resource names and nothing in the shape tells them apart, so reading it backwards produces no error, only a plan that reasons backwards.

referenceFields are the edge's @relation(fields: […]), the scalars that hold the reference (the relation field's referenceFields in the model; its references are the target's fields). A record that writes both a relation and one of its reference fields (shopId and shop: { $connect: … }, whether the relation is required or optional) is refused before the database is reached: A2004 with a V1012 issue at the relation, message Set "shopId" or "shop", not both. The reference fields are also what a required relation may be written through instead of the relation itself.

Prisma takes one record's relations either all as relation writes or all through their reference fields (its checked and unchecked inputs), so every artifact's model states referenceWrites: "uniform", and every Contract compiled from it carries the same member. A record that writes shopId beside another relation's write (supplier: { $connect: … }) is refused before the database is reached: A2004 with a V1012 issue at the relation write. A reference field beside a back-reference (a relation that holds no reference on that side, such as a shop's items) mixes nothing and is accepted.

Only owning edges appear. PSL forbids the non-owning side of a relation from declaring an action, so a back-reference's answer is its partner's and has no entry of its own. Both actions are resolved: an absent PSL argument means Prisma's effective default (set-null where every foreign-key scalar is optional, restrict otherwise, cascade on update), on both PostgreSQL and SQLite. An action spelling the reader cannot name stops the generate step before it reaches the artifact.

Neither the model nor the compiled Contracts carry a referential action or a reference field. The edges sit beside the model and outside modelHash, so they move no Contract value and no hash. createPrismaAdapter states them to core through the adapter's optional referentialActions member, translated one to one into core's five words (mirroring Prisma's and SQL's actions, direction unchanged):

| Prisma action | core's word | deleting a target row does this to rows of the enclosing Resource | |---|---|---| | cascade | cascade | deletes them, and the closure recurses into them | | restrict | restrict | refuses the delete while they reference it | | no-action | noAction | refuses the delete while they reference it; identical to restrict on both engines, because NoAction is not DEFERRABLE on either | | set-null | setNull | keeps them, the reference set to null | | set-default | setDefault | keeps them, the reference set to its column default |

Only cascade drives core behaviour (the V1019 closure below). restrict, noAction, setNull and setDefault are carried as informational adapter facts: the adapter states what the schema says instead of reducing it to what core currently reads.

onUpdate is not carried, because core plans the delete closure only.

Core builds the transitive closure of cascade edges once, at framework construction. The transaction planner refuses a step that depends on rows an earlier step's cascade removed (its own Resource, or one its arguments reach through a relation) with A2004/V1019 "Transaction step depends on a row removed by an earlier step's cascade", before any provider statement is issued. For example, with Metric.author (onDelete: Cascade) as the one cascade edge, deleting an Author and then reading its Metric rows is refused. Deleting an Author and then reading Post (Restrict), or a Category and then Post (SetNull), is not. The refusal is per Resource, since a plan cannot know which rows a delete will match, and it is plan-scoped: a single delete that cascades runs.


Raw SQL: developer statements only, always bound

Aventara never writes raw SQL itself. Every standard operation goes through Prisma's typed delegate surface. $queryRawUnsafe, $executeRaw, $executeRawUnsafe, findRaw, aggregateRaw and $runCommandRaw appear nowhere in this package, and no raw action is representable in the plan the translator builds; a test pins that plan to the closed set of thirteen Prisma actions the adapter may issue.

Developer SQL reaches the database two ways, both written as a tagged template: framework.appTx.$sql, a step of an application transaction, and a pipeline stage's ctx.$sql, run at once.

const post = framework.appTx.Post.create.one({ data: { title, authorId }, select: ["id"] });
const job = framework.appTx.$sql<{ msg_id: bigint }>`
  select pgmq.send('jobs', jsonb_build_object('postId', ${post.$ref("id")}))`;
const [created, rows] = await framework.transaction([post, job]);
// rows is { msg_id: bigint }[]

The adapter runs it with $queryRaw in its tagged-template form. Inside a transaction (an appTx.$sql step, or a ctx.$sql in a step, a forced call or a ctx.$transaction) it runs on the interactive transaction client, so the statement commits or rolls back with the transaction. A ctx.$sql made outside any transaction runs on the base client as a plain statement: no BEGIN is sent around it, so it needs no interactive transaction and works with @prisma/adapter-d1 too. The adapter declares sqlStatements with every driver. Every interpolated value is a bound parameter, never SQL text, so the text is static and a table or column name cannot come from a value. What it binds, measured on both engines with 7.10.0:

  • a JSON value (a plain object or array) is bound as its JSON text: cast it, ${value}::jsonb on PostgreSQL, or read it with json_extract(${value}, '$.k') on SQLite;
  • an Aventara AvDecimal is bound as its exact decimal text; a Date, bigint, Uint8Array and the primitives are bound as Prisma binds them;
  • a bare placeholder is the database's to type: PostgreSQL reads select ${1} as text, so cast where the type matters.

The rows come back as $queryRaw reads them, except that a PostgreSQL numeric becomes Aventara's AvDecimal. Prisma 7.10.0 cannot read a SQLite BLOB column out of a raw query; such a statement fails with A3001.

A raw query's failure is always P2010 on 7.10.0, a syntax error and a constraint violation alike. The adapter tells them apart by the driver adapter's own cause.kind, never by message text. A uniqueness or foreign-key violation answers A2008, as it does through a delegate, and everything else (a syntax error, a missing table, a null violation) answers A3001 with the raw error in diagnostics. Either way the whole plan rolls back.

appTx.$sql exists only in process. clientTx has no SQL step, the ClientContract carries nothing of it, and no /_transactions request can express one. A later step cannot $ref a SQL step's rows (A2007/V1010). No guard, pipe, hook, interceptor or filter runs around a SQL step, since it has no Resource or operation for one to wrap.


Other behaviour

  • $count on a paginated relation reports the count before pagination. On both engines, a relation's _count is unaffected by take/skip/cursor applied to the same relation in the same query. A filter on the relation does affect it.
  • limit/offset/cursor are the public names. Prisma's take/skip never surface.
  • Decimal values are converted in both directions. Prisma's bundled decimal object is not an Aventara public type and does not reach a caller as one.
  • Values and filter operands reach Prisma as given: a contains, startsWith or endsWith operand is the engine's LIKE pattern (% and _ included), and a column stores, rounds or widens a value as the engine does. A value PostgreSQL refuses (too long for its column, out of its range, U+0000 in text, a LIKE pattern ending in \) answers A2004 with one issue at ["arguments"] (V1004 or V1001), not A3001.
  • On D1, a BigInt value must lie within ±(2^53 − 1) (±9,007,199,254,740,991), not the full signed 64-bit range: the D1 driver binds a bigint as a JavaScript number and refuses one beyond that before the statement runs. A value beyond it, written, compared in a filter or used in an $increment, answers A2004 with one V1004 issue at the value (["arguments", "data", "views"]), and the statement that carries it writes nothing. A ctx.$sql statement binding such a value answers A3001: its SQL is yours.
  • A record written through a relation cannot write that relation, or its reference fields, back: Prisma's nested input for it leaves them out (a post created in author.posts has no author or authorId; a category created in a post's category has no posts). The artifact states each relation's other end (inverse, paired by relation name), the fields that hold its reference (referenceFields, from @relation(fields: …)) and the target's fields they hold, pair by pair (references, from @relation(references: …), which @aventara/core keeps in the Core Contract only, for a policy engine), and no relation writable at the "loopback" position, so @aventara/core refuses such a write before any guard or SQL, A2004 with a V1012 issue at the field written back, and the types do not offer it. Where a layer hides the other end, Prisma's client refuses the write instead and the adapter answers that refusal the same way.
  • Count variants return numbers, including zero. create.many and update.many return arrays.
  • find.unique, update.unique and delete.unique answer A2003 NOT_FOUND on a missing target; find.first, update.first and delete.first answer A1001 NO_MATCH with null data. The distinction is core's, not the adapter's.

Not covered

  • every provider except PostgreSQL and SQLite;
  • implicit many-to-many relations, type / view / composite blocks and multi-schema. Each arrives as a recorded unknown that stops the generate step instead of mapping silently;
  • @default forms beyond uuid(), cuid(), autoincrement(), now() and dbgenerated(…). An unmatched form is recorded as a defaulted field plus an unknown that fails the generate step, so an unconfirmed mapping cannot become a wrong model;
  • whether Neon-HTTP or PlanetScale-HTTP support interactive transactions in Prisma 7. Those drivers are absent from the driver set instead of declared "none", because an entry with no evidence would be a guess.

License

PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you may not use it to build a product that competes with Aventara itself. See LICENSE and LICENSE-ADDITIONAL-PERMISSION.md.