@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.
Files changed (86) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +564 -0
  4. package/dist/discovery/artifact.loader.d.ts +26 -0
  5. package/dist/discovery/artifact.loader.js +124 -0
  6. package/dist/discovery/capability-surface.interface.d.ts +191 -0
  7. package/dist/discovery/capability-surface.interface.js +13 -0
  8. package/dist/discovery/capability.deriver.d.ts +92 -0
  9. package/dist/discovery/capability.deriver.js +350 -0
  10. package/dist/discovery/discovery-artifact.interface.d.ts +210 -0
  11. package/dist/discovery/discovery-artifact.interface.js +1 -0
  12. package/dist/discovery/enum.mapper.d.ts +59 -0
  13. package/dist/discovery/enum.mapper.js +32 -0
  14. package/dist/discovery/field.mapper.d.ts +244 -0
  15. package/dist/discovery/field.mapper.js +387 -0
  16. package/dist/discovery/identifier.mapper.d.ts +188 -0
  17. package/dist/discovery/identifier.mapper.js +132 -0
  18. package/dist/discovery/model.discoverer.d.ts +108 -0
  19. package/dist/discovery/model.discoverer.js +341 -0
  20. package/dist/discovery/operations.deriver.d.ts +75 -0
  21. package/dist/discovery/operations.deriver.js +92 -0
  22. package/dist/discovery/scalar.mapper.d.ts +81 -0
  23. package/dist/discovery/scalar.mapper.js +51 -0
  24. package/dist/discovery/schema-facts.interface.d.ts +174 -0
  25. package/dist/discovery/schema-facts.interface.js +11 -0
  26. package/dist/discovery/schema.reader.d.ts +162 -0
  27. package/dist/discovery/schema.reader.js +692 -0
  28. package/dist/discovery/types.reader.d.ts +24 -0
  29. package/dist/discovery/types.reader.js +617 -0
  30. package/dist/discovery/vocabulary.dictionary.d.ts +258 -0
  31. package/dist/discovery/vocabulary.dictionary.js +429 -0
  32. package/dist/errors/error.normalizer.d.ts +250 -0
  33. package/dist/errors/error.normalizer.js +135 -0
  34. package/dist/errors/prisma-error.reader.d.ts +144 -0
  35. package/dist/errors/prisma-error.reader.js +167 -0
  36. package/dist/execution/first-variant.emulator.d.ts +64 -0
  37. package/dist/execution/first-variant.emulator.js +205 -0
  38. package/dist/execution/operation.executor.d.ts +87 -0
  39. package/dist/execution/operation.executor.js +210 -0
  40. package/dist/execution/result.shaper.d.ts +103 -0
  41. package/dist/execution/result.shaper.js +141 -0
  42. package/dist/execution/transaction.executor.d.ts +147 -0
  43. package/dist/execution/transaction.executor.js +13 -0
  44. package/dist/generate/artifact.emitter.d.ts +50 -0
  45. package/dist/generate/artifact.emitter.js +105 -0
  46. package/dist/generate/declared-support.reader.d.ts +33 -0
  47. package/dist/generate/declared-support.reader.js +74 -0
  48. package/dist/generate/discovery.bin.d.ts +2 -0
  49. package/dist/generate/discovery.bin.js +15 -0
  50. package/dist/generate/discovery.cli.d.ts +24 -0
  51. package/dist/generate/discovery.cli.js +163 -0
  52. package/dist/generate/existence.gate.d.ts +73 -0
  53. package/dist/generate/existence.gate.js +91 -0
  54. package/dist/generate/index.d.ts +81 -0
  55. package/dist/generate/index.js +65 -0
  56. package/dist/generate/node-version.guard.d.ts +8 -0
  57. package/dist/generate/node-version.guard.js +59 -0
  58. package/dist/generate/prisma-cli.resolver.d.ts +48 -0
  59. package/dist/generate/prisma-cli.resolver.js +95 -0
  60. package/dist/generate/prisma.generate.d.ts +164 -0
  61. package/dist/generate/prisma.generate.js +278 -0
  62. package/dist/generate/reader-crosscheck.d.ts +156 -0
  63. package/dist/generate/reader-crosscheck.js +202 -0
  64. package/dist/index.d.ts +5 -0
  65. package/dist/index.js +1 -0
  66. package/dist/prisma-client.interface.d.ts +48 -0
  67. package/dist/prisma-client.interface.js +9 -0
  68. package/dist/prisma.adapter.d.ts +55 -0
  69. package/dist/prisma.adapter.js +105 -0
  70. package/dist/provider/provider-profile.d.ts +53 -0
  71. package/dist/provider/provider-profile.js +91 -0
  72. package/dist/translation/identifier.addressing.d.ts +80 -0
  73. package/dist/translation/identifier.addressing.js +30 -0
  74. package/dist/translation/mutation.translator.d.ts +115 -0
  75. package/dist/translation/mutation.translator.js +580 -0
  76. package/dist/translation/operation.translator.d.ts +273 -0
  77. package/dist/translation/operation.translator.js +377 -0
  78. package/dist/translation/order-pagination.translator.d.ts +86 -0
  79. package/dist/translation/order-pagination.translator.js +159 -0
  80. package/dist/translation/projection.translator.d.ts +115 -0
  81. package/dist/translation/projection.translator.js +151 -0
  82. package/dist/translation/scalar.conversion.d.ts +101 -0
  83. package/dist/translation/scalar.conversion.js +221 -0
  84. package/dist/translation/where.translator.d.ts +79 -0
  85. package/dist/translation/where.translator.js +254 -0
  86. 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>;