@aventara/prisma7-adapter 0.1.0-pilot.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +564 -0
- package/dist/discovery/artifact.loader.d.ts +26 -0
- package/dist/discovery/artifact.loader.js +124 -0
- package/dist/discovery/capability-surface.interface.d.ts +191 -0
- package/dist/discovery/capability-surface.interface.js +13 -0
- package/dist/discovery/capability.deriver.d.ts +92 -0
- package/dist/discovery/capability.deriver.js +350 -0
- package/dist/discovery/discovery-artifact.interface.d.ts +210 -0
- package/dist/discovery/discovery-artifact.interface.js +1 -0
- package/dist/discovery/enum.mapper.d.ts +59 -0
- package/dist/discovery/enum.mapper.js +32 -0
- package/dist/discovery/field.mapper.d.ts +244 -0
- package/dist/discovery/field.mapper.js +387 -0
- package/dist/discovery/identifier.mapper.d.ts +188 -0
- package/dist/discovery/identifier.mapper.js +132 -0
- package/dist/discovery/model.discoverer.d.ts +108 -0
- package/dist/discovery/model.discoverer.js +341 -0
- package/dist/discovery/operations.deriver.d.ts +75 -0
- package/dist/discovery/operations.deriver.js +92 -0
- package/dist/discovery/scalar.mapper.d.ts +81 -0
- package/dist/discovery/scalar.mapper.js +51 -0
- package/dist/discovery/schema-facts.interface.d.ts +174 -0
- package/dist/discovery/schema-facts.interface.js +11 -0
- package/dist/discovery/schema.reader.d.ts +162 -0
- package/dist/discovery/schema.reader.js +692 -0
- package/dist/discovery/types.reader.d.ts +24 -0
- package/dist/discovery/types.reader.js +617 -0
- package/dist/discovery/vocabulary.dictionary.d.ts +258 -0
- package/dist/discovery/vocabulary.dictionary.js +429 -0
- package/dist/errors/error.normalizer.d.ts +250 -0
- package/dist/errors/error.normalizer.js +135 -0
- package/dist/errors/prisma-error.reader.d.ts +144 -0
- package/dist/errors/prisma-error.reader.js +167 -0
- package/dist/execution/first-variant.emulator.d.ts +64 -0
- package/dist/execution/first-variant.emulator.js +205 -0
- package/dist/execution/operation.executor.d.ts +87 -0
- package/dist/execution/operation.executor.js +210 -0
- package/dist/execution/result.shaper.d.ts +103 -0
- package/dist/execution/result.shaper.js +141 -0
- package/dist/execution/transaction.executor.d.ts +147 -0
- package/dist/execution/transaction.executor.js +13 -0
- package/dist/generate/artifact.emitter.d.ts +50 -0
- package/dist/generate/artifact.emitter.js +105 -0
- package/dist/generate/declared-support.reader.d.ts +33 -0
- package/dist/generate/declared-support.reader.js +74 -0
- package/dist/generate/discovery.bin.d.ts +2 -0
- package/dist/generate/discovery.bin.js +15 -0
- package/dist/generate/discovery.cli.d.ts +24 -0
- package/dist/generate/discovery.cli.js +163 -0
- package/dist/generate/existence.gate.d.ts +73 -0
- package/dist/generate/existence.gate.js +91 -0
- package/dist/generate/index.d.ts +81 -0
- package/dist/generate/index.js +65 -0
- package/dist/generate/node-version.guard.d.ts +8 -0
- package/dist/generate/node-version.guard.js +59 -0
- package/dist/generate/prisma-cli.resolver.d.ts +48 -0
- package/dist/generate/prisma-cli.resolver.js +95 -0
- package/dist/generate/prisma.generate.d.ts +164 -0
- package/dist/generate/prisma.generate.js +278 -0
- package/dist/generate/reader-crosscheck.d.ts +156 -0
- package/dist/generate/reader-crosscheck.js +202 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/prisma-client.interface.d.ts +48 -0
- package/dist/prisma-client.interface.js +9 -0
- package/dist/prisma.adapter.d.ts +55 -0
- package/dist/prisma.adapter.js +105 -0
- package/dist/provider/provider-profile.d.ts +53 -0
- package/dist/provider/provider-profile.js +91 -0
- package/dist/translation/identifier.addressing.d.ts +80 -0
- package/dist/translation/identifier.addressing.js +30 -0
- package/dist/translation/mutation.translator.d.ts +115 -0
- package/dist/translation/mutation.translator.js +580 -0
- package/dist/translation/operation.translator.d.ts +273 -0
- package/dist/translation/operation.translator.js +377 -0
- package/dist/translation/order-pagination.translator.d.ts +86 -0
- package/dist/translation/order-pagination.translator.js +159 -0
- package/dist/translation/projection.translator.d.ts +115 -0
- package/dist/translation/projection.translator.js +151 -0
- package/dist/translation/scalar.conversion.d.ts +101 -0
- package/dist/translation/scalar.conversion.js +221 -0
- package/dist/translation/where.translator.d.ts +79 -0
- package/dist/translation/where.translator.js +254 -0
- package/package.json +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# `@aventara/prisma7-adapter`
|
|
2
|
+
|
|
3
|
+
The Prisma 7 Adapter for Aventara. It discovers what a Prisma schema and its generated client can do,
|
|
4
|
+
compiles that into an Aventara `AdapterModel`, and executes validated Aventara operations against a
|
|
5
|
+
Prisma client the application owns.
|
|
6
|
+
|
|
7
|
+
This document is written for the person wiring it into an application. It states what the adapter
|
|
8
|
+
does, what it deliberately does **not** offer, and the two places where a correct Aventara operation
|
|
9
|
+
has a weaker guarantee than its name suggests. Everything a consumer has to know is in the body of
|
|
10
|
+
this file; nothing load-bearing is in a footnote.
|
|
11
|
+
|
|
12
|
+
- **Status:** pilot. Two engines are proven — PostgreSQL and SQLite. Every other Prisma provider is
|
|
13
|
+
unmeasured, and this file says so where it matters.
|
|
14
|
+
- **Peer dependencies:** `@prisma/client` `^7.10.0` (required); `prisma`, `@prisma/adapter-better-sqlite3`
|
|
15
|
+
and `@prisma/adapter-pg` `^7.10.0` and `typescript` `>=5.5.0 <7` (optional). 7.10.0 is the version the
|
|
16
|
+
error map was measured on; the adapter is bound to Prisma **7**'s error surface; see
|
|
17
|
+
[Prisma 7 only](#prisma-7-only).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## The application owns the Prisma client, and a driver adapter is required
|
|
22
|
+
|
|
23
|
+
`createPrismaAdapter` takes a client that is **already constructed**. It never constructs one, never
|
|
24
|
+
reads a connection string, and never reads an environment variable. The application owns the client's
|
|
25
|
+
lifetime, its pool, its logging, its extensions and its `$disconnect`.
|
|
26
|
+
|
|
27
|
+
Prisma 7 reaches a database through a **driver adapter**, so the client you hand over must have been
|
|
28
|
+
built with one. There is no fallback path, in Prisma or here.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
32
|
+
import { createFramework } from "@aventara/core";
|
|
33
|
+
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
|
|
34
|
+
import { PrismaClient } from "./generated/client.js"; // your generated client
|
|
35
|
+
import { discovery } from "./generated/aventara.artifact.js"; // the generate step's output
|
|
36
|
+
|
|
37
|
+
const client = new PrismaClient({
|
|
38
|
+
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const adapter = await createPrismaAdapter({
|
|
42
|
+
client,
|
|
43
|
+
discovery,
|
|
44
|
+
provider: "postgresql", // declared, never sniffed
|
|
45
|
+
driver: "@prisma/adapter-pg", // declared, never sniffed
|
|
46
|
+
// Adapter-owned, and not part of Aventara's public configuration:
|
|
47
|
+
transactionOptions: { timeout: 10_000, isolationLevel: "Serializable" },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const framework = await createFramework({ entrypoint: "/api", adapter });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`provider` and `driver` are **declared by the caller**. The adapter does not inspect client internals
|
|
54
|
+
to guess them, because a guess that is wrong is worse than a declaration that is checked: the driver
|
|
55
|
+
decides whether transactions are advertised at all.
|
|
56
|
+
|
|
57
|
+
`createPrismaAdapter` is `async`. Construction verifies the discovery artifact, and the digest it
|
|
58
|
+
checks is a `crypto.subtle` promise — so every fence fires once, here, before an adapter exists.
|
|
59
|
+
|
|
60
|
+
### `maxWait`, `timeout` and `isolationLevel` are adapter-owned
|
|
61
|
+
|
|
62
|
+
They are provider concepts. Core's `Adapter.transaction(work)` seam carries no options and must not
|
|
63
|
+
grow any, so these live on `createPrismaAdapter({ transactionOptions })` and are not Aventara public
|
|
64
|
+
configuration. Prisma's own defaults stand when they are absent: `maxWait` 2000 ms, `timeout` 5000 ms.
|
|
65
|
+
|
|
66
|
+
Supported isolation levels come from the declared provider: PostgreSQL admits `ReadUncommitted`,
|
|
67
|
+
`ReadCommitted`, `RepeatableRead` and `Serializable`; SQLite admits `Serializable` only. Asking for a
|
|
68
|
+
level the provider does not support fails the transaction — see [`P2028`](#p2028-covers-seven-situations-and-three-of-them-are-separable).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## The generate step, and the artifact it emits
|
|
73
|
+
|
|
74
|
+
Discovery runs **once, at generate time**, and writes a plain-data artifact. At runtime the adapter
|
|
75
|
+
loads that artifact, verifies it, and re-derives nothing.
|
|
76
|
+
|
|
77
|
+
The step ships with the package, as a binary and as a second export:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
prisma generate # your client, as usual
|
|
81
|
+
|
|
82
|
+
npx aventara-prisma7-generate \
|
|
83
|
+
--schema prisma/schema.prisma \
|
|
84
|
+
--client src/generated \
|
|
85
|
+
--provider postgresql \
|
|
86
|
+
--driver @prisma/adapter-pg \
|
|
87
|
+
--out src/discovery.artifact.ts
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact; the
|
|
91
|
+
driver decides `AdapterModel.transactions`, which is a driver fact the schema cannot state. Declare the
|
|
92
|
+
driver the application will actually construct the adapter with — the two cannot drift silently, but
|
|
93
|
+
they can disagree, and core refuses construction when they do.
|
|
94
|
+
|
|
95
|
+
**The toolchain the step runs on.** The step runs the Prisma 7 CLI itself: `@prisma/prisma7` (bin
|
|
96
|
+
`prisma7`, the Prisma 7 CLI under its own name) when it is installed, otherwise `prisma` only when its
|
|
97
|
+
version is in this package's declared `prisma` range. `npm i prisma` now installs a Prisma 8 release
|
|
98
|
+
candidate, so a project that has Prisma 8 installed keeps `@prisma/prisma7` beside it; with neither
|
|
99
|
+
usable the step refuses in one sentence naming both. TypeScript 6 is supported; TypeScript 7, which has
|
|
100
|
+
no classic compiler API, is refused in one sentence.
|
|
101
|
+
|
|
102
|
+
**`--out` must not be inside `--client`.** The types reader reads every `.ts` file under the client
|
|
103
|
+
root and refuses one that is not generated-client output, so an artifact dropped beside the client
|
|
104
|
+
would break the next run.
|
|
105
|
+
|
|
106
|
+
The same thing programmatically, for a project that already has a build script:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { generatePrismaDiscoveryArtifact } from "@aventara/prisma7-adapter/generate";
|
|
110
|
+
|
|
111
|
+
await generatePrismaDiscoveryArtifact({
|
|
112
|
+
schema: "prisma/schema.prisma",
|
|
113
|
+
clientDirectory: "src/generated",
|
|
114
|
+
provider: "postgresql",
|
|
115
|
+
driver: "@prisma/adapter-pg",
|
|
116
|
+
artifactFile: "src/discovery.artifact.ts",
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**The two entries have different dependency shapes, deliberately.** `@aventara/prisma7-adapter`
|
|
121
|
+
depends on `@aventara/core` and your generated client. `@aventara/prisma7-adapter/generate` additionally
|
|
122
|
+
needs `typescript` and `prisma`, which are declared as **optional peer dependencies**: they are not
|
|
123
|
+
installed on your behalf, nothing reachable from the runtime entry imports either, and so no compiler
|
|
124
|
+
and no Prisma tooling enters a deployed runtime. If you call the generate entry, have both in your
|
|
125
|
+
project's `devDependencies` — if you are generating a Prisma client at all, you already do.
|
|
126
|
+
|
|
127
|
+
The step reads two sources in the same instant — the `schema.prisma` text and the generated client's
|
|
128
|
+
own public types — and cross-checks them against each other. What it emits is a TypeScript module
|
|
129
|
+
exporting one `discovery` value with six members: the canonical `AdapterModel` (scalars, enums,
|
|
130
|
+
Resources with fields, identifiers and available operations, plus the `transactions` capability), the
|
|
131
|
+
Prisma addressing each Resource is reached by, the resolved referential actions per Resource, a
|
|
132
|
+
`modelHash`, and the `prismaVersion` it was generated against.
|
|
133
|
+
|
|
134
|
+
`modelHash` digests the **model** and nothing else. The addressing table and the referential actions sit
|
|
135
|
+
beside it and outside the hash: a database-side rename or a foreign-key action carries no semantic
|
|
136
|
+
content, and inside the hash either would move the Contract identity and force every generated client
|
|
137
|
+
to be regenerated for nothing.
|
|
138
|
+
|
|
139
|
+
On the runtime path there is **no** TypeScript program, **no** PSL parser and **no** Prisma tooling.
|
|
140
|
+
The loader reads no file: the artifact arrives as a value you import.
|
|
141
|
+
|
|
142
|
+
At construction the adapter checks exactly two things, and fails hard with an instruction to
|
|
143
|
+
regenerate if either is wrong:
|
|
144
|
+
|
|
145
|
+
1. the artifact matches **itself** — the `modelHash` over its own canonicalized model;
|
|
146
|
+
2. the installed `@prisma/client` **major** is the one the artifact was generated against.
|
|
147
|
+
|
|
148
|
+
### The residual those two checks do not cover
|
|
149
|
+
|
|
150
|
+
Neither check detects an artifact generated from an **older schema or client at the same major**. A
|
|
151
|
+
stale artifact that is internally consistent and still Prisma 7 loads without complaint, and the
|
|
152
|
+
first symptom is an operation the Contract advertises failing against a column that moved.
|
|
153
|
+
|
|
154
|
+
Currency is therefore a **process** guarantee, not a runtime one: the generate step reads the schema
|
|
155
|
+
and the client in one instant, so run it in the same build that runs `prisma generate`, and deploy the
|
|
156
|
+
artifact with the schema and client it came from. Wire the generate step into CI, not into the
|
|
157
|
+
runtime.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## What is advertised, what is not, and why
|
|
162
|
+
|
|
163
|
+
Operation availability is **derived**, never assumed. Every omission is decided in one place, from
|
|
164
|
+
three facts: the generated delegate's own method set, the Resource's identifiers, and the driver's
|
|
165
|
+
transaction capability.
|
|
166
|
+
|
|
167
|
+
Across the pilot's representative schema the compiled Contracts advertise **fifteen** distinct
|
|
168
|
+
`family.variant` operations, identically on all three Contract scopes. `delete.many` is **not** one of
|
|
169
|
+
them.
|
|
170
|
+
|
|
171
|
+
### `delete.many` is not offered — use `delete.count`
|
|
172
|
+
|
|
173
|
+
Prisma 7.10.0 has no `deleteManyAndReturn`. `delete.many` owes its caller the deleted rows, and there
|
|
174
|
+
is no provider call that returns them, so the variant is **omitted from every compiled Contract**
|
|
175
|
+
rather than emulated. A request for it is refused by core with `A2002 UNKNOWN_OPERATION` before the
|
|
176
|
+
adapter is reached — zero statements are issued.
|
|
177
|
+
|
|
178
|
+
**Use `delete.count`** to delete a filtered set. It answers a number, including zero.
|
|
179
|
+
|
|
180
|
+
### Provider-gated capabilities are read from the client's own types, never from a provider table
|
|
181
|
+
|
|
182
|
+
No module in this package branches on `provider`. A capability is advertised when the **generated
|
|
183
|
+
client's own public types admit it**, and the provider name is not even in scope where capabilities
|
|
184
|
+
are derived — so `if (provider === …)` is a compile error rather than a code-review question. That is
|
|
185
|
+
the rule because a provider table is a second source of truth that drifts, silently, one Prisma
|
|
186
|
+
release at a time.
|
|
187
|
+
|
|
188
|
+
Observed consequences on the two pilot engines, all of them read from generated types rather than
|
|
189
|
+
tabulated:
|
|
190
|
+
|
|
191
|
+
| Capability | PostgreSQL | SQLite | Read from |
|
|
192
|
+
|---|---|---|---|
|
|
193
|
+
| 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 |
|
|
194
|
+
| 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` |
|
|
195
|
+
| 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 |
|
|
196
|
+
| Explicit null ordering | advertised | advertised | `SortOrderInput`'s admission, per field |
|
|
197
|
+
| `create.many` / `update.many` (row-returning bulk writes) | advertised | advertised | `createManyAndReturn` / `updateManyAndReturn` on the delegate |
|
|
198
|
+
| Transactions | `"interactive"` | `"interactive"` | the **driver**, not the provider — see below |
|
|
199
|
+
|
|
200
|
+
**Beyond PostgreSQL and SQLite nothing here is measured.** MySQL, SQL Server, MongoDB, CockroachDB
|
|
201
|
+
and D1 were not generated against. Because every capability is read from the client you actually
|
|
202
|
+
generated, a provider that lacks something advertises less rather than failing at runtime — but the
|
|
203
|
+
pilot makes no claim about them.
|
|
204
|
+
|
|
205
|
+
### `update.many` and `create.many` cannot project a to-many relation
|
|
206
|
+
|
|
207
|
+
On `update.many` and `create.many`, Prisma's row-returning bulk writes reach **to-one relations
|
|
208
|
+
only**: the generated `*SelectUpdateManyAndReturn` / `*IncludeUpdateManyAndReturn` admit the scalars
|
|
209
|
+
plus a to-one relation and carry no to-many relation and no `$count`, where the unrestricted
|
|
210
|
+
`*Include` carries both.
|
|
211
|
+
|
|
212
|
+
A to-one projection on `update.many` works. A projection reaching a **to-many** relation or `$count`
|
|
213
|
+
on these two variants answers `A3001` — sanitized, with nothing written. That is a 500 for an
|
|
214
|
+
argument the Contract advertised, and it is a known, open gap rather than a surprise: the Contract has
|
|
215
|
+
no per-variant projection slot in which to say "available on `update.one`, not on `update.many`".
|
|
216
|
+
|
|
217
|
+
**Today:** keep `update.many`/`create.many` projections to scalars and to-one relations; read to-many
|
|
218
|
+
relations with a separate `find`. **Phase 9** adds the per-variant projection capability so the
|
|
219
|
+
Contract stops advertising a projection it cannot serve, and the refusal becomes a caller error
|
|
220
|
+
(`A2004`) instead of a server failure. The adapter deliberately does **not** paper over it with a
|
|
221
|
+
follow-up read: that would add a statement and a new consistency window to a bulk write.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## `update.first` and `delete.first` are framework-manufactured conveniences
|
|
226
|
+
|
|
227
|
+
Read this section before using either.
|
|
228
|
+
|
|
229
|
+
Aventara's first-style write — *general filter · ordering decides "first" · effect limited to one
|
|
230
|
+
record · the affected record is returned, in one call* — is essentially absent across ORM design,
|
|
231
|
+
because SQL splits the ingredients along a dialect fault line: `UPDATE … ORDER BY … LIMIT` is a
|
|
232
|
+
MySQL/MariaDB extension while `RETURNING` is PostgreSQL/SQLite/MSSQL, so whichever dialect grants the
|
|
233
|
+
limit denies the returning. Prisma 7.10.0's operation set has no `updateFirst` and no `deleteFirst`
|
|
234
|
+
at all.
|
|
235
|
+
|
|
236
|
+
So these two are **not** a Prisma-shaped gap an adapter might fill. They are operations the framework
|
|
237
|
+
**manufactures**, out of two statements, inside one adapter-internal transaction:
|
|
238
|
+
|
|
239
|
+
1. `findFirst` with the caller's `where` and `orderBy`, selecting the Resource's identifier. The
|
|
240
|
+
`orderBy` is the selector for *which* record — ordering and single-record selection are natural on
|
|
241
|
+
a read. No match answers `A1001 NO_MATCH` with `null` data, and nothing is written.
|
|
242
|
+
2. `update` / `delete` narrowed to the identifier the read returned, **with the caller's original
|
|
243
|
+
predicate re-asserted alongside it**, carrying the caller's own projection — so the record the
|
|
244
|
+
contract owes comes back from the write itself. Nothing spare is read, and nothing is stripped.
|
|
245
|
+
|
|
246
|
+
They are therefore **non-atomic**, and they have a **defined conflict outcome**. The Contract's own
|
|
247
|
+
vocabulary says so: a *manufactured*, *non-atomic* variant. Neither is ever described as atomic.
|
|
248
|
+
|
|
249
|
+
### What re-assertion closes, which race that is, and the residual it leaves
|
|
250
|
+
|
|
251
|
+
Re-asserting the caller's predicate on the write closes the **wrong-record race**. Without it the
|
|
252
|
+
write is by identifier alone and could mutate a record that no longer matches what the caller asked
|
|
253
|
+
for; with it the write can only ever reach a record the read selected, and a record that left the
|
|
254
|
+
predicate in between is **detected** rather than mis-mutated — it answers
|
|
255
|
+
`A2014 STALE_SELECTION` (409) with nothing written, and a retry may succeed.
|
|
256
|
+
|
|
257
|
+
**The published guarantee, verbatim:** *the record written is one the read selected, or nothing is
|
|
258
|
+
written and the caller is told.*
|
|
259
|
+
|
|
260
|
+
**It does not close stale firstness, and nothing can.** Between the read and the write another writer
|
|
261
|
+
can insert or update a record that sorts *ahead* of the selected one and still matches the original
|
|
262
|
+
predicate. Both statements are blind to it — the read never saw it, and the re-asserted write
|
|
263
|
+
succeeds against the record it was given — so `update.first` can mutate a record that is no longer
|
|
264
|
+
the first ordered match and report **ordinary success**. Nothing detects it, nothing reports it, and
|
|
265
|
+
no re-assertion can, because the missing fact is about a record that was never read.
|
|
266
|
+
|
|
267
|
+
This is a **documented, permitted property** of a framework-manufactured variant, published as
|
|
268
|
+
policy rather than discovered in practice. Serializable isolation is not the remedy: core's
|
|
269
|
+
`Adapter.transaction(work)` seam carries no isolation parameter, so an adapter cannot set a level for
|
|
270
|
+
a manufactured variant or for a plan, and a provider transaction's level is fixed before its first
|
|
271
|
+
statement, so a per-operation escalation is not expressible at all. SQLite admits `Serializable`
|
|
272
|
+
only, and the residual persists there regardless.
|
|
273
|
+
|
|
274
|
+
**If your operation's correctness depends on the record still being the first ordered match at the
|
|
275
|
+
instant of the write, do not use `update.first`/`delete.first`.** Select the record yourself and use
|
|
276
|
+
`update.unique`/`delete.unique` on an identifier, inside a transaction plan whose other operations
|
|
277
|
+
make the invariant you need checkable.
|
|
278
|
+
|
|
279
|
+
Wherever this document says a race is closed, it names which race. A sentence that says "the race is
|
|
280
|
+
closed" without naming one is wrong.
|
|
281
|
+
|
|
282
|
+
### Why the write is by identifier and not by Prisma's `limit`
|
|
283
|
+
|
|
284
|
+
Prisma **does** offer a top-level `limit` on `updateMany`, `deleteMany` and `updateManyAndReturn` in
|
|
285
|
+
7.10.0 — measured, present on every model of both pilot engines. The adapter does not use it, and the
|
|
286
|
+
reason is not concealment: Aventara's doctrine is to **expose** what the ORM offers, not to hide it.
|
|
287
|
+
|
|
288
|
+
The reason is that `limit` **cannot express this operation**. None of those three argument types
|
|
289
|
+
carries an `orderBy`. So `limit: 1` affects an **arbitrary** matching row, and two identical calls can
|
|
290
|
+
affect different ones. The identifier from the ordered read is what makes the write deterministic.
|
|
291
|
+
|
|
292
|
+
So, stated plainly rather than presented as a principle: **Aventara does not currently expose an
|
|
293
|
+
arbitrary affected-row cap.** If it ever does, it will be named so that nobody can mistake it for an
|
|
294
|
+
ordered window — the hazard belongs in the identifier, not in a sentence beside it.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Errors
|
|
299
|
+
|
|
300
|
+
### A caller discriminates by **code**, never by message text
|
|
301
|
+
|
|
302
|
+
Every failure this adapter originates carries framework-authored text and nothing read off the Prisma
|
|
303
|
+
error. No table name, constraint name, column name, SQL fragment, connection string, file path or
|
|
304
|
+
stack reaches a caller — the normalizer never reads the provider's `message` at all, so there is no
|
|
305
|
+
provider string in scope to forward. The whole serialized envelope of every failing path is swept for
|
|
306
|
+
provider markers by this package's own conformance gate.
|
|
307
|
+
|
|
308
|
+
The consequence is a rule, not an inconvenience: **branch on the code.** Message text is framework
|
|
309
|
+
prose, may change, and deliberately says less than the provider did.
|
|
310
|
+
|
|
311
|
+
### The three conflict codes
|
|
312
|
+
|
|
313
|
+
Three situations used to collapse into one `409 A2008`. They no longer do.
|
|
314
|
+
|
|
315
|
+
| Code | Means | Retry? |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `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. |
|
|
318
|
+
| `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. |
|
|
319
|
+
| `A2014 STALE_SELECTION` | The state this operation had **selected changed** before its effect could be applied, so nothing was written. Raised by the first-variant manufacture's own seam. | Yes, the same request may succeed. No automatic retry. |
|
|
320
|
+
|
|
321
|
+
`A2008` **narrowed** to the constraint case; it was not repurposed. If you previously treated `A2008`
|
|
322
|
+
as "any conflict", the two situations that left it now have their own codes.
|
|
323
|
+
|
|
324
|
+
Two consequences worth stating explicitly:
|
|
325
|
+
|
|
326
|
+
- **`A2008`'s narrowed meaning lives in the specification, not in the wire text.** The emitted message
|
|
327
|
+
stays *"The operation conflicts with the current state of the resource."* The words "uniqueness" and
|
|
328
|
+
"constraint" are exactly the provider's own words for this failure and are banned from a
|
|
329
|
+
provider-originated envelope by the sanitization rule, which cannot tell a framework-authored
|
|
330
|
+
"constraint" from Prisma's. So this README may say `A2008` *means* a uniqueness or reference
|
|
331
|
+
constraint; the envelope does **not** say so, and nothing should be parsed out of it.
|
|
332
|
+
- **A write conflict and a deadlock share `A2013` deliberately.** Both arrive as `P2034` with
|
|
333
|
+
identical `meta` keys, differing only in a provider-owned code (`40001` versus `40P01`) that never
|
|
334
|
+
crosses the boundary. They are not separable at this seam, so one outcome is the honest answer.
|
|
335
|
+
|
|
336
|
+
### None of the three carries `cause.issues`
|
|
337
|
+
|
|
338
|
+
`A2008`, `A2013` and `A2014` pair with **no** validation code and carry `cause.issues` **absent** — as
|
|
339
|
+
a rule, not as an accident. A conflict is not a Framework Validation Layer failure: validation codes
|
|
340
|
+
*determine* the outcome code and roll up only to the validation outcomes, so a conflict validation
|
|
341
|
+
code would have no legal rollup. Do not write a caller that expects `issues` on a conflict.
|
|
342
|
+
|
|
343
|
+
### `P2034` is never retried automatically
|
|
344
|
+
|
|
345
|
+
Neither Prisma nor this adapter retries a write conflict, and the adapter will not grow one. A
|
|
346
|
+
transaction plan's operations run developer-authored pipeline callbacks, and **re-running a plan
|
|
347
|
+
re-runs those callbacks** — including after hooks whose external side effects are not rollback-safe
|
|
348
|
+
(see below). A retry is the caller's to make, having decided its own operations are safe to repeat.
|
|
349
|
+
|
|
350
|
+
### An after hook's external side effect inside a plan runs before the commit
|
|
351
|
+
|
|
352
|
+
State this as a pair, because the behaviour alone reads as a bug and the obligation is what makes it
|
|
353
|
+
a property.
|
|
354
|
+
|
|
355
|
+
Inside a transaction plan, a pipeline after hook runs **inside** the provider transaction — therefore
|
|
356
|
+
**before** the provider commits. If a later operation fails, the provider rolls back: the row is
|
|
357
|
+
absent and the external effect has already happened. An email was sent, a queue message published, a
|
|
358
|
+
cache written, for a record that does not exist.
|
|
359
|
+
|
|
360
|
+
That is not a defect to be fixed by the adapter; it is the shape the specification describes, and it
|
|
361
|
+
is asserted against a real provider commit by this package's conformance gate. **The duty is the
|
|
362
|
+
hook author's:** an after hook SHOULD be observational or idempotent, and MUST NOT be relied on to
|
|
363
|
+
undo a mutation. If an effect must not happen unless the plan commits, do not perform it in an after
|
|
364
|
+
hook.
|
|
365
|
+
|
|
366
|
+
### `P2028` covers seven situations, and three of them are separable
|
|
367
|
+
|
|
368
|
+
`P2028` is Prisma 7's transaction code and it is shared by **seven** internal classes. The adapter
|
|
369
|
+
answers `A3002 TRANSACTION_ERROR` for the failure that *ends* the transaction, and never reads the
|
|
370
|
+
message text to decide anything.
|
|
371
|
+
|
|
372
|
+
What is separable **structurally** (measured on both pilot engines, never inferred from docs):
|
|
373
|
+
|
|
374
|
+
| Situation | Code | `meta` |
|
|
375
|
+
|---|---|---|
|
|
376
|
+
| Transaction-timeout expiry | `P2028` | `{ operation, timeout, timeTaken }` — the expiry is identifiable |
|
|
377
|
+
| `maxWait` acquisition timeout | `P2028` | **empty** — identifiable *by* that emptiness, and by nothing finer |
|
|
378
|
+
| Valid-but-unsupported isolation level (e.g. `Snapshot` on either pilot engine) | `P2028` | `{ isolationLevel }` |
|
|
379
|
+
| An isolation level outside Prisma's own set (e.g. `"Bogus"`) | **`P2023`** | `{ providedIsolationLevel }` — a different code entirely |
|
|
380
|
+
|
|
381
|
+
The remaining **four** classes — transaction not found, already committed, already rolled back, and
|
|
382
|
+
an internal consistency failure — carry **no `meta` at all** and differ only in their message text.
|
|
383
|
+
Reading error text to tell outcomes apart is forbidden here, so those four are **inseparable**, and
|
|
384
|
+
the acquisition timeout is inseparable *from them*. That is Prisma's limit, not a gap in this
|
|
385
|
+
adapter, and it is recorded so nobody later closes it with message parsing.
|
|
386
|
+
|
|
387
|
+
One asymmetry worth knowing: a `P2028` that ends the transaction rejects out of `$transaction` and
|
|
388
|
+
becomes `A3002`. A `P2028` raised by a *delegate call inside* the callback is unrecognised by the
|
|
389
|
+
closed code table, so it becomes `A3001` attributed to that operation.
|
|
390
|
+
|
|
391
|
+
### Everything unrecognised becomes `A3001`
|
|
392
|
+
|
|
393
|
+
The code table contains only codes that were **induced** against a real engine on a real schema. A
|
|
394
|
+
Prisma code the table does not contain is deliberately left to core's adapter boundary, which answers
|
|
395
|
+
`A3001 ADAPTER_ERROR` with core's own text and sends the raw error to the diagnostics sink. Adding a
|
|
396
|
+
row later is additive; shipping a guessed one is not recoverable.
|
|
397
|
+
|
|
398
|
+
### Prisma 7 only
|
|
399
|
+
|
|
400
|
+
The code table's keys are P-codes, which are **7.x**'s error surface. Prisma 8 (a release candidate in October 2026)
|
|
401
|
+
is a rewrite: no `@prisma/client`, no `prisma generate`, no driver adapters, dotted `NAMESPACE.SUBCODE` errors with no
|
|
402
|
+
P-codes yet. A Prisma 8 adapter is therefore a **new adapter package**, `@aventara/prisma8-adapter`, not a re-map of
|
|
403
|
+
this one. A project keeping Prisma 7 beside a Prisma 8 install runs the Prisma 7 CLI as `@prisma/prisma7` (bin
|
|
404
|
+
`prisma7`), which the generate step prefers. The
|
|
405
|
+
bound is enforced rather than described: the package's own suite reads the installed
|
|
406
|
+
`@prisma/client`'s version and fails if the major is not `7`, and the discovery artifact records the
|
|
407
|
+
major it was generated against.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Transactions
|
|
412
|
+
|
|
413
|
+
`Adapter.transaction` is mounted **exactly when the declared driver supports an interactive
|
|
414
|
+
transaction**. It is one `client.$transaction(async (tx) => …)` per Aventara transaction plan:
|
|
415
|
+
|
|
416
|
+
- the **callback** form only — never the array form. An Aventara plan is sequential by contract, so
|
|
417
|
+
operation *n* may consume operation *n − 1*'s result and the second call's arguments do not exist
|
|
418
|
+
until the first has answered. The array form takes every call up front, so it cannot honour a plan
|
|
419
|
+
at all, and it is not even representable through the interface this adapter holds;
|
|
420
|
+
- the **root client is unreachable** from inside the callback: the executor core is handed closes over
|
|
421
|
+
the transaction client and nothing else;
|
|
422
|
+
- the value the callback threw is rethrown **by identity**. The adapter adds no `try`, no `catch` and
|
|
423
|
+
no wrapper, because that is what the seam requires and wrapping would turn every plan failure into
|
|
424
|
+
an integrity failure.
|
|
425
|
+
|
|
426
|
+
A first-style variant inside a plan **reuses the plan's transaction** rather than opening a nested
|
|
427
|
+
one, so its two statements roll back *with* the plan rather than independently.
|
|
428
|
+
|
|
429
|
+
**Prisma swallows its own rollback failures** — the rollback's rejection reaches an internal debug
|
|
430
|
+
logger and never the caller. So this adapter never claims a rollback succeeded, because it cannot
|
|
431
|
+
observe that it did. A commit failure is different: it rejects like a callback throw.
|
|
432
|
+
|
|
433
|
+
### `transactions` is a driver fact, and it reaches the ClientContract hash
|
|
434
|
+
|
|
435
|
+
`@prisma/adapter-d1` declares `transactions: "none"` — Cloudflare D1 rejects interactive
|
|
436
|
+
transactions, and Prisma says so itself. `@prisma/adapter-pg` and `@prisma/adapter-better-sqlite3`
|
|
437
|
+
declare `"interactive"`, observed by running one.
|
|
438
|
+
|
|
439
|
+
**This has a consequence consumers must plan for.** The `transactions` capability participates in the
|
|
440
|
+
**ClientContract hash**, and a generated client **fails hard** on a hash mismatch. So one schema, one
|
|
441
|
+
framework configuration and two deployment drivers produce two ClientContracts, two hashes and two
|
|
442
|
+
generated clients — and a client generated against a `"none"` deployment will not run against an
|
|
443
|
+
`"interactive"` one, or the reverse.
|
|
444
|
+
|
|
445
|
+
Practically: **generate your client against the deployment's own driver**, and treat the driver as
|
|
446
|
+
part of the deployment identity rather than as a connection detail. If you deploy the same application
|
|
447
|
+
over D1 and over PostgreSQL, those are two deployments with two generated clients.
|
|
448
|
+
|
|
449
|
+
An artifact and a driver cannot drift silently either: an artifact that says `"interactive"` loaded by
|
|
450
|
+
an adapter whose declared driver says `"none"` makes framework construction fail with a capability
|
|
451
|
+
mismatch.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Referential actions
|
|
456
|
+
|
|
457
|
+
**The artifact carries them, the adapter states them to core, and core refuses a transaction plan that
|
|
458
|
+
depends on rows a cascade removed.**
|
|
459
|
+
|
|
460
|
+
The generate step resolves each relation edge's referential actions — including the defaults Prisma
|
|
461
|
+
applies when the PSL argument is absent — cross-checks them while reading the schema, and emits them as
|
|
462
|
+
the artifact's `referentialActions` member:
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
discovery.referentialActions;
|
|
466
|
+
// {
|
|
467
|
+
// Shop: { owningEdges: [] },
|
|
468
|
+
// Item: { owningEdges: [{ field: "shop", target: "Shop",
|
|
469
|
+
// cardinality: "one",
|
|
470
|
+
// onDelete: "cascade", onUpdate: "cascade" }] },
|
|
471
|
+
// Clerk: { owningEdges: [{ field: "shop", target: "Shop",
|
|
472
|
+
// cardinality: "one",
|
|
473
|
+
// onDelete: "set-null", onUpdate: "cascade" }] },
|
|
474
|
+
// }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Read it in one direction.** `onDelete` is declared on the **child** and fires when the **parent** is
|
|
478
|
+
deleted, so an entry under Resource `R` says *deleting a `target` row does `onDelete` to rows of `R`*.
|
|
479
|
+
To answer *"what does deleting a `Shop` do?"*, scan every Resource's `owningEdges` for
|
|
480
|
+
`target === "Shop"` and read the enclosing Resource key — above, that is `Item` (cascade) and `Clerk`
|
|
481
|
+
(set-null). Both ends of that sentence are Resource names and nothing in the shape tells them apart, so
|
|
482
|
+
the inverted reading produces no error at all, only a plan that reasons backwards.
|
|
483
|
+
|
|
484
|
+
Only **owning** edges appear: PSL forbids the non-owning side of a relation from declaring an action,
|
|
485
|
+
so a back-reference's answer is its partner's and carries no entry of its own. Both actions are
|
|
486
|
+
*resolved* — an absent PSL argument is not a third state but Prisma's effective default (`set-null`
|
|
487
|
+
where every foreign-key scalar is optional, `restrict` otherwise, `cascade` on update), measured on both
|
|
488
|
+
pilot engines. An action spelling the reader cannot name stops the generate step rather than reaching
|
|
489
|
+
the artifact.
|
|
490
|
+
|
|
491
|
+
Neither the model nor the compiled Contracts carry a referential action — the actions are beside the
|
|
492
|
+
model and outside `modelHash`, so they move no Contract value and no hash. `createPrismaAdapter` states
|
|
493
|
+
them to core through the adapter's optional `referentialActions` member, translated one to one into core's
|
|
494
|
+
five words (the architect's, 2026-10-04, mirroring Prisma's and SQL's actions; direction unchanged):
|
|
495
|
+
|
|
496
|
+
| Prisma action | core's word | deleting a `target` row does this to rows of the enclosing Resource |
|
|
497
|
+
|---|---|---|
|
|
498
|
+
| `cascade` | `cascade` | deletes them, and the closure recurses into them |
|
|
499
|
+
| `restrict` | `restrict` | refuses the delete while they reference it |
|
|
500
|
+
| `no-action` | `noAction` | refuses the delete while they reference it — measured identical to `restrict` on both engines, because `NoAction` is not `DEFERRABLE` on either |
|
|
501
|
+
| `set-null` | `setNull` | keeps them, the reference set to null |
|
|
502
|
+
| `set-default` | `setDefault` | keeps them, the reference set to its column default |
|
|
503
|
+
|
|
504
|
+
Only `cascade` drives core behaviour (the `V1019` closure below). `restrict`, `noAction`, `setNull` and
|
|
505
|
+
`setDefault` are carried as informational adapter facts, by the architect's choice: the adapter states
|
|
506
|
+
what the schema says rather than collapsing it into what core currently reads.
|
|
507
|
+
|
|
508
|
+
`onUpdate` is not carried: core plans the **delete** closure only.
|
|
509
|
+
|
|
510
|
+
**Planning-phase refusal is wired (Phase 9 S10).** Core builds the transitive closure of `cascade` edges
|
|
511
|
+
once, at framework construction, and the transaction planner refuses a step that depends on rows an
|
|
512
|
+
earlier step's cascade removed — its own Resource, or one its arguments reach through a relation — with
|
|
513
|
+
`A2004`/`V1019` *"Transaction step depends on a row removed by an earlier step's cascade"*, before any
|
|
514
|
+
provider statement is issued. It is measured on both pilot engines against `Metric.author`
|
|
515
|
+
(`onDelete: Cascade`), the pilot schemas' one cascade edge: deleting an `Author` and then reading its
|
|
516
|
+
`Metric` rows is refused; deleting an `Author` and then reading `Post` (`Restrict`), or a `Category` and
|
|
517
|
+
then `Post` (`SetNull`), is not. The refusal is per Resource — a plan cannot know which rows a delete
|
|
518
|
+
will match — and plan-scoped: a single delete that cascades runs.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## No raw SQL
|
|
523
|
+
|
|
524
|
+
The adapter issues **no raw SQL, ever**. `$queryRaw`, `$executeRaw`, the `…Unsafe` variants,
|
|
525
|
+
`findRaw`, `aggregateRaw` and `$runCommandRaw` appear nowhere in this package, public or internal —
|
|
526
|
+
and a raw action is not representable in the plan the translator builds, which is pinned by a test
|
|
527
|
+
asserting the closed set of thirteen Prisma actions the adapter may issue.
|
|
528
|
+
|
|
529
|
+
This is an architectural rule with no exception attached. Everything the adapter does, it does through
|
|
530
|
+
Prisma's typed delegate surface.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Also worth knowing
|
|
535
|
+
|
|
536
|
+
- **`$count` on a paginated relation reports the pre-pagination count.** Measured on both engines: a
|
|
537
|
+
relation's `_count` is unaffected by `take`/`skip`/`cursor` applied to the same relation in the same
|
|
538
|
+
query. A filter on the relation *does* affect it.
|
|
539
|
+
- **`limit`/`offset`/`cursor` are the public names.** Prisma's `take`/`skip` never surface.
|
|
540
|
+
- **Decimal values are converted in both directions.** Prisma's bundled decimal object is not an
|
|
541
|
+
Aventara public type and does not reach a caller as one.
|
|
542
|
+
- **Count variants return numbers, including zero.** `create.many` and `update.many` return arrays.
|
|
543
|
+
- **`find.unique`, `update.unique` and `delete.unique` answer `A2003 NOT_FOUND`** on a missing target;
|
|
544
|
+
`find.first`, `update.first` and `delete.first` answer `A1001 NO_MATCH` with `null` data. The
|
|
545
|
+
distinction is deliberate and is core's, not the adapter's.
|
|
546
|
+
|
|
547
|
+
## Not covered by the pilot
|
|
548
|
+
|
|
549
|
+
Stated so nothing here reads as a claim it is not:
|
|
550
|
+
|
|
551
|
+
- every provider except PostgreSQL and SQLite;
|
|
552
|
+
- implicit many-to-many relations, `type` / `view` / composite blocks and multi-schema — each arrives
|
|
553
|
+
as a recorded unknown that **stops the generate step** rather than mapping silently;
|
|
554
|
+
- `@default` forms beyond `uuid()`, `cuid()`, `autoincrement()`, `now()` and `dbgenerated(…)` — an
|
|
555
|
+
unmatched form is recorded as a defaulted field plus an unknown that fails the generate step, so an
|
|
556
|
+
unconfirmed mapping cannot become a wrong model;
|
|
557
|
+
- whether Neon-HTTP or PlanetScale-HTTP support interactive transactions in Prisma 7. Those drivers
|
|
558
|
+
are **absent** from the driver set rather than declared `"none"`: an entry with no evidence is a
|
|
559
|
+
guess.
|
|
560
|
+
|
|
561
|
+
## License
|
|
562
|
+
|
|
563
|
+
PolyForm Shield 1.0.0 with an additional permission — free to use, including in commercial applications; you may not
|
|
564
|
+
use it to build a product that competes with Aventara itself. See `LICENSE` and `LICENSE-ADDITIONAL-PERMISSION.md`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PrismaAdapterModel } from "./discovery-artifact.interface.js";
|
|
2
|
+
/** Every artifact failure is this class, and every one of them says how to fix it. */
|
|
3
|
+
export declare class PrismaDiscoveryArtifactError extends Error {
|
|
4
|
+
constructor(reason: string);
|
|
5
|
+
}
|
|
6
|
+
export type PrismaDiscoveryArtifactLoad = {
|
|
7
|
+
readonly artifact: unknown;
|
|
8
|
+
/** The `version` of the `@prisma/client` actually installed beside the caller. */
|
|
9
|
+
readonly installedPrismaVersion: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The identity of a discovered model: one canonicalization, one digest — over
|
|
13
|
+
* the model and **nothing else**. The artifact's `translation` half is Prisma
|
|
14
|
+
* addressing, and hashing it would put a DB-side rename into the Contract
|
|
15
|
+
* identity; its `referentialActions` half is what a delete does below the ORM
|
|
16
|
+
* entrypoint, and hashing it would put a foreign-key action into the Contract
|
|
17
|
+
* identity and into every generated client.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeModelHash(model: PrismaAdapterModel): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Validates an artifact and hands back its model **unchanged**.
|
|
22
|
+
*
|
|
23
|
+
* Three checks, in the order that makes each one's failure legible: shape, then
|
|
24
|
+
* the `prismaVersion` major fence (C-846), then `modelHash`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadDiscoveryArtifact(load: PrismaDiscoveryArtifactLoad): Promise<PrismaAdapterModel>;
|