@aventara/prisma7-adapter 0.1.0-pilot.4 → 1.0.0-rc.1
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/CHANGELOG.md +597 -0
- package/README.md +363 -317
- package/dist/discovery/artifact.loader.d.ts +4 -4
- package/dist/discovery/artifact.loader.js +17 -19
- package/dist/discovery/capability-surface.interface.d.ts +8 -1
- package/dist/discovery/capability.deriver.d.ts +6 -6
- package/dist/discovery/capability.deriver.js +6 -3
- package/dist/discovery/client-shape.verifier.d.ts +1 -1
- package/dist/discovery/client-shape.verifier.js +2 -2
- package/dist/discovery/discovery-artifact.interface.d.ts +38 -43
- package/dist/discovery/discovery-artifact.interface.js +1 -1
- package/dist/discovery/enum.mapper.d.ts +2 -2
- package/dist/discovery/field.mapper.d.ts +13 -13
- package/dist/discovery/field.mapper.js +11 -11
- package/dist/discovery/identifier.mapper.d.ts +7 -7
- package/dist/discovery/model.discoverer.d.ts +4 -4
- package/dist/discovery/model.discoverer.js +51 -20
- package/dist/discovery/operations.deriver.d.ts +6 -5
- package/dist/discovery/schema-facts.interface.d.ts +3 -3
- package/dist/discovery/schema.reader.d.ts +2 -2
- package/dist/discovery/schema.reader.js +5 -5
- package/dist/discovery/types.reader.d.ts +2 -2
- package/dist/discovery/types.reader.js +33 -22
- package/dist/discovery/vocabulary.dictionary.d.ts +14 -13
- package/dist/discovery/vocabulary.dictionary.js +1 -1
- package/dist/errors/error.normalizer.d.ts +11 -5
- package/dist/errors/error.normalizer.js +27 -5
- package/dist/errors/prisma-error.reader.d.ts +2 -0
- package/dist/errors/prisma-error.reader.js +1 -0
- package/dist/execution/first-variant.emulator.d.ts +4 -4
- package/dist/execution/first-variant.emulator.js +13 -9
- package/dist/execution/operation.executor.d.ts +9 -8
- package/dist/execution/operation.executor.js +17 -13
- package/dist/execution/result.shaper.d.ts +3 -3
- package/dist/execution/result.shaper.js +7 -7
- package/dist/execution/sql-statement.executor.d.ts +9 -0
- package/dist/execution/sql-statement.executor.js +58 -0
- package/dist/execution/transaction.executor.d.ts +8 -7
- package/dist/execution/transaction.executor.js +9 -5
- package/dist/generate/artifact.emitter.d.ts +5 -7
- package/dist/generate/artifact.emitter.js +11 -22
- package/dist/generate/discovery.cli.d.ts +3 -3
- package/dist/generate/discovery.cli.js +11 -11
- package/dist/generate/existence.gate.d.ts +4 -4
- package/dist/generate/existence.gate.js +4 -4
- package/dist/generate/index.d.ts +5 -5
- package/dist/generate/index.js +2 -2
- package/dist/generate/prisma.generate.d.ts +7 -8
- package/dist/generate/prisma.generate.js +11 -12
- package/dist/index.d.ts +4 -4
- package/dist/prisma-client.interface.d.ts +22 -8
- package/dist/prisma-client.interface.js +7 -0
- package/dist/prisma.adapter.d.ts +12 -12
- package/dist/prisma.adapter.js +12 -3
- package/dist/provider/provider-profile.d.ts +11 -11
- package/dist/translation/mutation.translator.d.ts +8 -8
- package/dist/translation/mutation.translator.js +31 -31
- package/dist/translation/operation.translator.d.ts +7 -6
- package/dist/translation/operation.translator.js +12 -12
- package/dist/translation/order-pagination.translator.d.ts +5 -5
- package/dist/translation/order-pagination.translator.js +16 -16
- package/dist/translation/projection.translator.d.ts +4 -4
- package/dist/translation/projection.translator.js +11 -11
- package/dist/translation/scalar.conversion.d.ts +9 -8
- package/dist/translation/scalar.conversion.js +11 -11
- package/dist/translation/where.translator.d.ts +5 -5
- package/dist/translation/where.translator.js +15 -15
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
# `@aventara/prisma7-adapter`
|
|
2
2
|
|
|
3
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 `
|
|
4
|
+
compiles that into an Aventara `AvAdapterModel`, and executes validated Aventara operations against a
|
|
5
5
|
Prisma client the application owns.
|
|
6
6
|
|
|
7
|
-
This
|
|
8
|
-
does, what it
|
|
9
|
-
|
|
10
|
-
this file; nothing load-bearing is in a footnote.
|
|
7
|
+
This README is for the person setting the adapter up in an application. It covers what the adapter
|
|
8
|
+
does, what it does not offer, and the two operations whose guarantee is weaker than their names
|
|
9
|
+
suggest. Everything you need is in the body of this file, not in footnotes.
|
|
11
10
|
|
|
12
|
-
-
|
|
11
|
+
- Status: release candidate. Two engines are proven, PostgreSQL and SQLite. Every other Prisma provider is
|
|
13
12
|
unmeasured, and this file says so where it matters.
|
|
14
|
-
-
|
|
15
|
-
and `@prisma/adapter-pg` `^7.10.0` and `typescript` `>=5.5.0 <7` (optional).
|
|
16
|
-
|
|
17
|
-
[Prisma 7 only](#prisma-7-only).
|
|
13
|
+
- Peer dependencies: `@prisma/client` `^7.10.0` (required); `prisma`, `@prisma/adapter-better-sqlite3`
|
|
14
|
+
and `@prisma/adapter-pg` `^7.10.0` and `typescript` `>=5.5.0 <7` (optional). The error map targets
|
|
15
|
+
7.10.0, and the adapter is bound to Prisma 7's error surface; see [Prisma 7 only](#prisma-7-only).
|
|
18
16
|
|
|
19
17
|
---
|
|
20
18
|
|
|
21
19
|
## The application owns the Prisma client, and a driver adapter is required
|
|
22
20
|
|
|
23
|
-
`createPrismaAdapter` takes a client that is
|
|
21
|
+
`createPrismaAdapter` takes a client that is already constructed. It never constructs one, never
|
|
24
22
|
reads a connection string, and never reads an environment variable. The application owns the client's
|
|
25
23
|
lifetime, its pool, its logging, its extensions and its `$disconnect`.
|
|
26
24
|
|
|
27
|
-
Prisma 7 reaches a database through a
|
|
25
|
+
Prisma 7 reaches a database through a driver adapter, so the client you hand over must have been
|
|
28
26
|
built with one. There is no fallback path, in Prisma or here.
|
|
29
27
|
|
|
30
28
|
```ts
|
|
@@ -47,31 +45,31 @@ const adapter = await createPrismaAdapter({
|
|
|
47
45
|
transactionOptions: { timeout: 10_000, isolationLevel: "Serializable" },
|
|
48
46
|
});
|
|
49
47
|
|
|
50
|
-
const framework = await createFramework({
|
|
48
|
+
const framework = await createFramework({ api: "/api", adapter });
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
`provider` and `driver
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
The caller declares `provider` and `driver`. The adapter does not inspect client internals to guess
|
|
52
|
+
them: the driver decides whether transactions are advertised at all, and a wrong guess would be worse
|
|
53
|
+
than a declaration that is checked.
|
|
56
54
|
|
|
57
55
|
`createPrismaAdapter` is `async`. Construction verifies the discovery artifact, and the digest it
|
|
58
|
-
checks is a `crypto.subtle` promise
|
|
56
|
+
checks is a `crypto.subtle` promise, so every check runs once, here, before an adapter exists.
|
|
59
57
|
|
|
60
58
|
### `maxWait`, `timeout` and `isolationLevel` are adapter-owned
|
|
61
59
|
|
|
62
60
|
They are provider concepts. Core's `Adapter.transaction(work)` interface carries no options and will
|
|
63
61
|
not grow any, so these live on `createPrismaAdapter({ transactionOptions })` and are not Aventara public
|
|
64
|
-
configuration. Prisma's own defaults
|
|
62
|
+
configuration. Prisma's own defaults apply when they are absent: `maxWait` 2000 ms, `timeout` 5000 ms.
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
The supported isolation levels come from the declared provider. PostgreSQL admits `ReadUncommitted`,
|
|
67
65
|
`ReadCommitted`, `RepeatableRead` and `Serializable`; SQLite admits `Serializable` only. Asking for a
|
|
68
|
-
level the provider does not support fails the transaction
|
|
66
|
+
level the provider does not support fails the transaction; see [`P2028`](#p2028-covers-seven-situations-and-three-of-them-are-separable).
|
|
69
67
|
|
|
70
68
|
---
|
|
71
69
|
|
|
72
70
|
## The generate step, and the artifact it emits
|
|
73
71
|
|
|
74
|
-
Discovery runs
|
|
72
|
+
Discovery runs once, at generate time, and writes a plain-data artifact. At runtime the adapter
|
|
75
73
|
loads that artifact, verifies it, and re-derives nothing.
|
|
76
74
|
|
|
77
75
|
The step ships with the package, as a binary and as a second export:
|
|
@@ -87,23 +85,23 @@ npx aventara-prisma7-generate \
|
|
|
87
85
|
--out src/discovery.artifact.ts
|
|
88
86
|
```
|
|
89
87
|
|
|
90
|
-
`--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact
|
|
91
|
-
driver decides `
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
`--provider` and `--driver` are declared, never sniffed. The provider is recorded in the artifact. The
|
|
89
|
+
driver decides `AvAdapterModel.transactions`, a driver fact the schema cannot state. Declare the driver
|
|
90
|
+
the application will actually construct the adapter with. The two cannot drift silently, but they can
|
|
91
|
+
disagree, and core refuses construction when they do.
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
The step runs the Prisma 7 CLI itself: `@prisma/prisma7` (bin `prisma7`, the Prisma 7 CLI under its own
|
|
94
|
+
name) when it is installed, otherwise `prisma` only when its version is in this package's declared
|
|
95
|
+
`prisma` range. `npm i prisma` now installs a Prisma 8 release candidate, so a project that has Prisma 8
|
|
96
|
+
installed keeps `@prisma/prisma7` beside it. With neither usable, the step refuses in one sentence naming
|
|
97
|
+
both. TypeScript 6 is supported; TypeScript 7, which has no classic compiler API, is refused in one
|
|
98
|
+
sentence.
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
`--out` must not be inside `--client`. The types reader reads every `.ts` file under the client root
|
|
101
|
+
and refuses one that is not generated-client output, so an artifact written beside the client would
|
|
102
|
+
break the next run.
|
|
105
103
|
|
|
106
|
-
The same
|
|
104
|
+
The same step can run from code, for a project that already has a build script:
|
|
107
105
|
|
|
108
106
|
```ts
|
|
109
107
|
import { generatePrismaDiscoveryArtifact } from "@aventara/prisma7-adapter/generate";
|
|
@@ -117,119 +115,124 @@ await generatePrismaDiscoveryArtifact({
|
|
|
117
115
|
});
|
|
118
116
|
```
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
The step reads two sources
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
118
|
+
The two entries have different dependencies. `@aventara/prisma7-adapter` depends on `@aventara/core`
|
|
119
|
+
and your generated client. `@aventara/prisma7-adapter/generate` also needs `typescript` and `prisma`,
|
|
120
|
+
which are declared as optional peer dependencies. They are not installed for you, and nothing reachable
|
|
121
|
+
from the runtime entry imports either, so no compiler and no Prisma tooling enters a deployed runtime.
|
|
122
|
+
If you call the generate entry, have both in your project's `devDependencies`; a project that generates
|
|
123
|
+
a Prisma client already does.
|
|
124
|
+
|
|
125
|
+
The step reads two sources at the same time, the `schema.prisma` text and the generated client's own
|
|
126
|
+
public types, and cross-checks them against each other. It emits a TypeScript module exporting one
|
|
127
|
+
`discovery` value with eight members:
|
|
128
|
+
|
|
129
|
+
- the canonical `AvAdapterModel` (scalars, enums, Resources with fields, identifiers, available
|
|
130
|
+
operations and create requirements, plus the `transactions` capability and the `referenceWrites`
|
|
131
|
+
limit);
|
|
132
|
+
- the Prisma addressing each Resource is reached by;
|
|
133
|
+
- the resolved referential actions per Resource;
|
|
134
|
+
- a record of the generated client it was read from (each model and its field references);
|
|
135
|
+
- a `modelHash`;
|
|
136
|
+
- the `prismaVersion` and database `provider` it was generated against;
|
|
137
|
+
- the artifact's format (`regenerate_with_aventara_prepare`).
|
|
138
|
+
|
|
139
|
+
Each Resource's create requirements are what a create cannot do without, read off the question marks
|
|
140
|
+
of the generated create inputs: `required` lists the columns with no default (`["title"]`), and
|
|
141
|
+
`oneOf` each required relation with the fields that hold its reference (`[["author", "authorId"]]`:
|
|
142
|
+
write `author`, or `authorId`, not both). `parentSupplies` says which of them a nested create gets
|
|
143
|
+
from its parent (`{ "Author": { "posts": ["author", "authorId"] } }`). They are part of the model, so
|
|
144
|
+
they reach every Contract and the generated client: a create that leaves one out is a type error, and
|
|
145
|
+
at runtime, after your pipes and computed behaviours have run, it is answered `A2004` with a `V1000`
|
|
139
146
|
issue at its path (`["arguments", "data", "title"]`, or `["arguments", "data", "author"]` for a
|
|
140
|
-
required relation)
|
|
141
|
-
relation it is created through.
|
|
147
|
+
required relation) before the database can refuse it.
|
|
142
148
|
|
|
143
|
-
`modelHash` digests the
|
|
144
|
-
client record
|
|
145
|
-
|
|
146
|
-
to be regenerated for nothing.
|
|
149
|
+
`modelHash` digests the model and nothing else. The addressing table, the referential actions and the
|
|
150
|
+
client record sit beside it and outside the hash. A database-side rename or a foreign-key action
|
|
151
|
+
changes no semantics; inside the hash, either would move the Contract identity and force every
|
|
152
|
+
generated client to be regenerated for nothing.
|
|
147
153
|
|
|
148
|
-
On the runtime path there is
|
|
149
|
-
|
|
154
|
+
On the runtime path there is no TypeScript program, no PSL parser and no Prisma tooling. The loader
|
|
155
|
+
reads no file: the artifact arrives as a value you import.
|
|
150
156
|
|
|
151
|
-
At construction
|
|
152
|
-
|
|
157
|
+
At construction, before the application listens, the adapter checks three things and fails hard with
|
|
158
|
+
an instruction to regenerate if any is wrong:
|
|
153
159
|
|
|
154
|
-
1. the artifact matches
|
|
155
|
-
2. the installed `@prisma/client`
|
|
156
|
-
3. the Prisma client you pass is the one the artifact was
|
|
160
|
+
1. the artifact matches itself: the `modelHash` over its own canonicalized model;
|
|
161
|
+
2. the installed `@prisma/client` major is the one the artifact was generated against;
|
|
162
|
+
3. the Prisma client you pass is the one the artifact was generated from: the same models, and the
|
|
157
163
|
same field references (`prisma.<model>.fields`) on each. A schema change followed by
|
|
158
|
-
`prisma generate` but not by the generate step
|
|
164
|
+
`prisma generate` but not by the generate step (a model or field added, removed or renamed) is
|
|
159
165
|
refused in one sentence naming what differs:
|
|
160
166
|
|
|
161
167
|
```text
|
|
162
|
-
|
|
168
|
+
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.
|
|
163
169
|
```
|
|
164
170
|
|
|
165
171
|
A value with no recognisable model delegate (a test double, a deep mock) is not compared.
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
a default
|
|
169
|
-
set up by `aventara` does, from `postinstall` and `aventara:prepare`), and deploy the
|
|
170
|
-
schema and client it came from.
|
|
173
|
+
The client check does not see a change that leaves every model and field name in place, such as a
|
|
174
|
+
type, a default or an attribute. Run the generate step in the same build that runs `prisma generate`
|
|
175
|
+
(a project set up by `aventara` does, from `postinstall` and `aventara:prepare`), and deploy the
|
|
176
|
+
artifact with the schema and client it came from.
|
|
171
177
|
|
|
172
178
|
---
|
|
173
179
|
|
|
174
180
|
## What is advertised, what is not, and why
|
|
175
181
|
|
|
176
|
-
Operation availability is
|
|
177
|
-
|
|
182
|
+
Operation availability is derived, never assumed. Every omission is decided in one place, from three
|
|
183
|
+
facts: the generated delegate's own method set, the Resource's identifiers, and the driver's
|
|
178
184
|
transaction capability.
|
|
179
185
|
|
|
180
|
-
For a typical schema the compiled Contracts advertise
|
|
181
|
-
|
|
182
|
-
them.
|
|
186
|
+
For a typical schema the compiled Contracts advertise fifteen distinct `family.variant` operations,
|
|
187
|
+
identically on all three Contract scopes. `delete.many` is not one of them.
|
|
183
188
|
|
|
184
|
-
### `delete.many` is not offered
|
|
189
|
+
### `delete.many` is not offered: use `delete.count`
|
|
185
190
|
|
|
186
|
-
Prisma 7.10.0 has no `deleteManyAndReturn`. `delete.many` owes its caller the deleted rows, and
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
Prisma 7.10.0 has no `deleteManyAndReturn`. `delete.many` owes its caller the deleted rows, and no
|
|
192
|
+
provider call returns them, so the variant is omitted from every compiled Contract instead of being
|
|
193
|
+
emulated. Core refuses a request for it with `A2002 UNKNOWN_OPERATION` before the adapter is reached,
|
|
194
|
+
and no statement is issued.
|
|
190
195
|
|
|
191
|
-
|
|
196
|
+
To delete a filtered set, use `delete.count`. It answers a number, including zero.
|
|
192
197
|
|
|
193
198
|
### Provider-gated capabilities are read from the client's own types, never from a provider table
|
|
194
199
|
|
|
195
|
-
No module in this package branches on `provider`. A capability is advertised when the
|
|
196
|
-
client's own public types admit it
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
release at a time.
|
|
200
|
+
No module in this package branches on `provider`. A capability is advertised when the generated
|
|
201
|
+
client's own public types admit it. The provider name is not even in scope where capabilities are
|
|
202
|
+
derived, so `if (provider === …)` is a compile error. A provider table would be a second source of
|
|
203
|
+
truth, and it would drift one Prisma release at a time.
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
tabulated:
|
|
205
|
+
On PostgreSQL and SQLite, read from the generated types:
|
|
203
206
|
|
|
204
207
|
| Capability | PostgreSQL | SQLite | Read from |
|
|
205
208
|
|---|---|---|---|
|
|
206
|
-
| Scalar list fields | advertised |
|
|
207
|
-
| JSON `path` filter | advertised |
|
|
209
|
+
| 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 |
|
|
210
|
+
| 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` |
|
|
208
211
|
| 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 |
|
|
209
212
|
| Explicit null ordering | advertised | advertised | `SortOrderInput`'s admission, per field |
|
|
210
213
|
| `create.many` / `update.many` (row-returning bulk writes) | advertised | advertised | `createManyAndReturn` / `updateManyAndReturn` on the delegate |
|
|
211
|
-
| Transactions | `"interactive"` | `"interactive"` | the
|
|
214
|
+
| Transactions | `"interactive"` | `"interactive"` | the driver, not the provider (see below) |
|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
Nothing beyond PostgreSQL and SQLite is measured. MySQL, SQL Server, MongoDB, CockroachDB and D1 were
|
|
217
|
+
not generated against. Because every capability is read from the client you actually generated, a
|
|
218
|
+
provider that lacks something advertises less instead of failing at runtime, but this release makes no
|
|
219
|
+
claim about those providers.
|
|
217
220
|
|
|
218
221
|
### `update.many` and `create.many` cannot project a to-many relation
|
|
219
222
|
|
|
220
|
-
On `update.many` and `create.many`, Prisma's row-returning bulk writes reach
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
On `update.many` and `create.many`, Prisma's row-returning bulk writes reach to-one relations only.
|
|
224
|
+
The generated `*SelectUpdateManyAndReturn` / `*IncludeUpdateManyAndReturn` admit the scalars plus a
|
|
225
|
+
to-one relation and carry no to-many relation and no `$count`, where the unrestricted `*Include`
|
|
226
|
+
carries both.
|
|
224
227
|
|
|
225
|
-
A to-one projection on `update.many` works. A projection reaching a
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
A to-one projection on `update.many` works. A projection reaching a to-many relation or `$count` on
|
|
229
|
+
these two variants is refused before any statement runs: `A2004` (a validation error) with the issue
|
|
230
|
+
`V1008`, and nothing is written. The Contract advertises each relation's projection per position, and
|
|
228
231
|
on these two variants the position the bulk result is read in does not include to-many relations.
|
|
229
232
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
Keep `update.many`/`create.many` projections to scalars and to-one relations, and read to-many
|
|
234
|
+
relations with a separate `find`. The adapter does not fill the gap with a follow-up read, which would
|
|
235
|
+
add a statement and a new consistency window to a bulk write.
|
|
233
236
|
|
|
234
237
|
---
|
|
235
238
|
|
|
@@ -237,87 +240,84 @@ with a follow-up read: that would add a statement and a new consistency window t
|
|
|
237
240
|
|
|
238
241
|
Read this section before using either.
|
|
239
242
|
|
|
240
|
-
Aventara's first-style write
|
|
241
|
-
record
|
|
242
|
-
because SQL splits the ingredients
|
|
243
|
-
MySQL/MariaDB extension while `RETURNING` is PostgreSQL/SQLite/MSSQL, so whichever dialect grants the
|
|
244
|
-
limit denies the returning. Prisma 7.10.0's operation set has no `updateFirst` and no `deleteFirst`
|
|
245
|
-
|
|
243
|
+
Aventara's first-style write takes a general filter, lets the ordering decide which record is "first",
|
|
244
|
+
limits the effect to that one record, and returns the affected record, all in one call. ORMs almost
|
|
245
|
+
never offer this, because SQL splits the ingredients between dialects: `UPDATE … ORDER BY … LIMIT` is
|
|
246
|
+
a MySQL/MariaDB extension while `RETURNING` is PostgreSQL/SQLite/MSSQL, so whichever dialect grants the
|
|
247
|
+
limit denies the returning. Prisma 7.10.0's operation set has no `updateFirst` and no `deleteFirst` at
|
|
248
|
+
all.
|
|
246
249
|
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
The framework builds these two operations out of two statements, inside one adapter-internal
|
|
251
|
+
transaction:
|
|
249
252
|
|
|
250
253
|
1. `findFirst` with the caller's `where` and `orderBy`, selecting the Resource's identifier. The
|
|
251
|
-
`orderBy`
|
|
252
|
-
|
|
253
|
-
2. `update` / `delete` narrowed to the identifier the read returned,
|
|
254
|
-
predicate re-asserted alongside it
|
|
255
|
-
contract owes comes back from the write itself. Nothing
|
|
254
|
+
`orderBy` selects which record; ordering and single-record selection are natural on a read. No
|
|
255
|
+
match answers `A1001 NO_MATCH` with `null` data, and nothing is written.
|
|
256
|
+
2. `update` / `delete` narrowed to the identifier the read returned, with the caller's original
|
|
257
|
+
predicate re-asserted alongside it, carrying the caller's own projection, so the record the
|
|
258
|
+
contract owes comes back from the write itself. Nothing extra is read, and nothing is stripped.
|
|
256
259
|
|
|
257
|
-
They are therefore
|
|
258
|
-
|
|
260
|
+
They are therefore non-atomic, and they have a defined conflict outcome. The Contract's own vocabulary
|
|
261
|
+
calls them manufactured, non-atomic variants, and neither is ever described as atomic.
|
|
259
262
|
|
|
260
263
|
### What re-assertion closes, which race that is, and the residual it leaves
|
|
261
264
|
|
|
262
|
-
Re-asserting the caller's predicate on the write closes the
|
|
263
|
-
|
|
264
|
-
for
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
can insert or update a record that sorts
|
|
273
|
-
predicate.
|
|
274
|
-
succeeds against the record it was given
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
This is a
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
instant of the write, do not use `update.first`/`delete.first`.** Select the record yourself and use
|
|
265
|
+
Re-asserting the caller's predicate on the write closes the wrong-record race. Without it, the write
|
|
266
|
+
would go by identifier alone and could change a record that no longer matches what the caller asked
|
|
267
|
+
for. With it, the write can only reach a record the read selected. A record that left the predicate in
|
|
268
|
+
between is detected instead of changed: it answers `A2014 STALE_SELECTION` (409) with nothing written,
|
|
269
|
+
and a retry may succeed.
|
|
270
|
+
|
|
271
|
+
The published guarantee is: *the record written is one the read selected, or nothing is written and
|
|
272
|
+
the caller is told.*
|
|
273
|
+
|
|
274
|
+
Re-assertion does not close stale firstness, and nothing can. Between the read and the write another
|
|
275
|
+
writer can insert or update a record that sorts ahead of the selected one and still matches the
|
|
276
|
+
original predicate. Neither statement sees it (the read never saw it, and the re-asserted write
|
|
277
|
+
succeeds against the record it was given), so `update.first` can change a record that is no longer the
|
|
278
|
+
first ordered match and report ordinary success. Nothing detects or reports this, and no re-assertion
|
|
279
|
+
can, because the missing fact is about a record that was never read.
|
|
280
|
+
|
|
281
|
+
This is a documented, permitted property of a framework-manufactured variant. Serializable isolation
|
|
282
|
+
does not fix it. Core's `Adapter.transaction(work)` interface carries no isolation parameter, so an
|
|
283
|
+
adapter cannot set a level for a manufactured variant or for a plan, and a provider transaction's
|
|
284
|
+
level is fixed before its first statement, so a per-operation escalation cannot be expressed at all.
|
|
285
|
+
SQLite admits `Serializable` only, and the residual persists there regardless.
|
|
286
|
+
|
|
287
|
+
If your operation's correctness depends on the record still being the first ordered match at the
|
|
288
|
+
moment of the write, do not use `update.first`/`delete.first`. Select the record yourself and use
|
|
287
289
|
`update.unique`/`delete.unique` on an identifier, inside a transaction plan whose other operations
|
|
288
290
|
make the invariant you need checkable.
|
|
289
291
|
|
|
290
|
-
Wherever this
|
|
291
|
-
closed" without naming one is wrong.
|
|
292
|
+
Wherever this README says a race is closed, it names which race.
|
|
292
293
|
|
|
293
294
|
### Why the write is by identifier and not by Prisma's `limit`
|
|
294
295
|
|
|
295
|
-
Prisma
|
|
296
|
-
7.10.0
|
|
297
|
-
|
|
296
|
+
Prisma does offer a top-level `limit` on `updateMany`, `deleteMany` and `updateManyAndReturn` in
|
|
297
|
+
7.10.0, on every model on PostgreSQL and SQLite. Aventara exposes what the ORM offers, so this is not
|
|
298
|
+
a choice to hide it.
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
`limit` cannot express this operation. None of those three argument types carries an `orderBy`, so
|
|
301
|
+
`limit: 1` affects an arbitrary matching row, and two identical calls can affect different rows. The
|
|
302
|
+
identifier from the ordered read is what makes the write deterministic.
|
|
302
303
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
ordered window — the hazard belongs in the identifier, not in a sentence beside it.
|
|
304
|
+
Aventara does not currently expose an arbitrary affected-row cap. If it ever does, its name will make
|
|
305
|
+
clear that it is not an ordered window.
|
|
306
306
|
|
|
307
307
|
---
|
|
308
308
|
|
|
309
309
|
## Errors
|
|
310
310
|
|
|
311
|
-
###
|
|
311
|
+
### Branch on the code, never on the message text
|
|
312
312
|
|
|
313
313
|
Every failure this adapter originates carries framework-authored text and nothing read off the Prisma
|
|
314
314
|
error. No table name, constraint name, column name, SQL fragment, connection string, file path or
|
|
315
|
-
stack reaches a caller
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
stack reaches a caller. The normalizer never reads the provider's `message`, so there is no provider
|
|
316
|
+
string in scope to forward. This package's own conformance gate sweeps the whole serialized envelope
|
|
317
|
+
of every failing path for provider markers.
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
So branch on the code. Message text is framework prose, may change, and says less than the provider
|
|
320
|
+
did.
|
|
321
321
|
|
|
322
322
|
### The three conflict codes
|
|
323
323
|
|
|
@@ -325,137 +325,131 @@ Three different situations each have their own code.
|
|
|
325
325
|
|
|
326
326
|
| Code | Means | Retry? |
|
|
327
327
|
|---|---|---|
|
|
328
|
-
| `A2008 CONFLICT` | The operation would violate a
|
|
329
|
-
| `A2013 CONCURRENT_MODIFICATION` | Another operation
|
|
330
|
-
| `A2014 STALE_SELECTION` | The state this operation had
|
|
328
|
+
| `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. |
|
|
329
|
+
| `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. |
|
|
330
|
+
| `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. |
|
|
331
331
|
|
|
332
|
-
`A2008`
|
|
333
|
-
|
|
332
|
+
`A2008` was narrowed to the constraint case, not repurposed. If you previously treated `A2008` as "any
|
|
333
|
+
conflict", the two situations that left it now have their own codes.
|
|
334
334
|
|
|
335
|
-
|
|
335
|
+
`A2008`'s narrowed meaning lives in the specification, not in the wire text. The emitted message stays
|
|
336
|
+
*"The operation conflicts with the current state of the resource."* The words "uniqueness" and
|
|
337
|
+
"constraint" are the provider's own words for this failure, and the sanitization rule bans them from a
|
|
338
|
+
provider-originated envelope because it cannot tell a framework-authored "constraint" from Prisma's.
|
|
339
|
+
So this README can say `A2008` means a uniqueness or reference constraint, but the envelope does not
|
|
340
|
+
say so, and nothing should be parsed out of it.
|
|
336
341
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
provider-originated envelope by the sanitization rule, which cannot tell a framework-authored
|
|
341
|
-
"constraint" from Prisma's. So this README may say `A2008` *means* a uniqueness or reference
|
|
342
|
-
constraint; the envelope does **not** say so, and nothing should be parsed out of it.
|
|
343
|
-
- **A write conflict and a deadlock share `A2013` deliberately.** Both arrive as `P2034` with
|
|
344
|
-
identical `meta` keys, differing only in a provider-owned code (`40001` versus `40P01`) that never
|
|
345
|
-
crosses the boundary. They cannot be told apart here, so one outcome is the honest answer.
|
|
342
|
+
A write conflict and a deadlock share `A2013`. Both arrive as `P2034` with identical `meta` keys and
|
|
343
|
+
differ only in a provider-owned code (`40001` versus `40P01`) that never crosses the boundary. They
|
|
344
|
+
cannot be told apart here, so they get one outcome.
|
|
346
345
|
|
|
347
346
|
### None of the three carries `cause.issues`
|
|
348
347
|
|
|
349
|
-
`A2008`, `A2013` and `A2014` pair with
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
348
|
+
`A2008`, `A2013` and `A2014` pair with no validation code, and `cause.issues` is always absent on
|
|
349
|
+
them. A conflict is not a Framework Validation Layer failure: validation codes determine the outcome
|
|
350
|
+
code and roll up only to the validation outcomes, so a conflict validation code would have no legal
|
|
351
|
+
rollup. Do not write a caller that expects `issues` on a conflict.
|
|
353
352
|
|
|
354
353
|
### `P2034` is never retried automatically
|
|
355
354
|
|
|
356
|
-
Neither Prisma nor this adapter retries a write conflict, and the adapter will not grow
|
|
357
|
-
transaction plan's operations run developer-authored pipeline callbacks, and
|
|
358
|
-
|
|
359
|
-
|
|
355
|
+
Neither Prisma nor this adapter retries a write conflict, and the adapter will not grow a retry. A
|
|
356
|
+
transaction plan's operations run developer-authored pipeline callbacks, and re-running a plan re-runs
|
|
357
|
+
those callbacks, including after hooks whose external side effects are not rollback-safe (see below).
|
|
358
|
+
The caller makes a retry, after deciding its own operations are safe to repeat.
|
|
360
359
|
|
|
361
360
|
### An after hook's external side effect inside a plan runs before the commit
|
|
362
361
|
|
|
363
|
-
|
|
364
|
-
a
|
|
362
|
+
Inside a transaction plan, a pipeline after hook runs inside the provider transaction, and therefore
|
|
363
|
+
before the provider commits. If a later operation fails, the provider rolls back: the row is absent
|
|
364
|
+
but the external effect has already happened. An email was sent, a queue message published or a cache
|
|
365
|
+
written, for a record that does not exist.
|
|
365
366
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
That is not a defect to be fixed by the adapter; it is the shape the specification describes, and it
|
|
372
|
-
is asserted against a real provider commit by this package's conformance gate. **The duty is the
|
|
373
|
-
hook author's:** an after hook SHOULD be observational or idempotent, and MUST NOT be relied on to
|
|
374
|
-
undo a mutation. If an effect must not happen unless the plan commits, do not perform it in an after
|
|
375
|
-
hook.
|
|
367
|
+
The specification describes this behaviour, and this package's conformance gate asserts it against a
|
|
368
|
+
real provider commit. The duty falls on the hook author: an after hook SHOULD be observational or
|
|
369
|
+
idempotent, and MUST NOT be relied on to undo a mutation. If an effect must not happen unless the plan
|
|
370
|
+
commits, do not perform it in an after hook.
|
|
376
371
|
|
|
377
372
|
### `P2028` covers seven situations, and three of them are separable
|
|
378
373
|
|
|
379
|
-
`P2028` is Prisma 7's transaction code and
|
|
380
|
-
|
|
381
|
-
|
|
374
|
+
`P2028` is Prisma 7's transaction code, and seven internal classes share it. The adapter answers
|
|
375
|
+
`A3002 TRANSACTION_ERROR` for the failure that ends the transaction, and never reads the message text
|
|
376
|
+
to decide anything.
|
|
382
377
|
|
|
383
|
-
What
|
|
378
|
+
What can be told apart from the error's structure, measured on PostgreSQL and SQLite:
|
|
384
379
|
|
|
385
380
|
| Situation | Code | `meta` |
|
|
386
381
|
|---|---|---|
|
|
387
|
-
| Transaction-timeout expiry | `P2028` | `{ operation, timeout, timeTaken }
|
|
388
|
-
| `maxWait` acquisition timeout | `P2028` |
|
|
382
|
+
| Transaction-timeout expiry | `P2028` | `{ operation, timeout, timeTaken }`: the expiry is identifiable |
|
|
383
|
+
| `maxWait` acquisition timeout | `P2028` | empty: identifiable by that emptiness, and by nothing finer |
|
|
389
384
|
| Valid-but-unsupported isolation level (e.g. `Snapshot` on either engine) | `P2028` | `{ isolationLevel }` |
|
|
390
|
-
| An isolation level outside Prisma's own set (e.g. `"Bogus"`) |
|
|
385
|
+
| An isolation level outside Prisma's own set (e.g. `"Bogus"`) | `P2023` | `{ providedIsolationLevel }`, a different code entirely |
|
|
391
386
|
|
|
392
|
-
The
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
387
|
+
The other four classes (transaction not found, already committed, already rolled back, and an internal
|
|
388
|
+
consistency failure) carry no `meta` at all and differ only in their message text. Reading error text
|
|
389
|
+
to tell outcomes apart is not allowed here, so those four cannot be told apart, and the acquisition
|
|
390
|
+
timeout cannot be told apart from them. That limit is Prisma's, and it is recorded here so nobody later
|
|
391
|
+
works around it by parsing messages.
|
|
397
392
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
393
|
+
A `P2028` that ends the transaction rejects out of `$transaction` and becomes `A3002`. A `P2028`
|
|
394
|
+
raised by a delegate call inside the callback is not in the closed code table, so it becomes `A3001`
|
|
395
|
+
attributed to that operation.
|
|
401
396
|
|
|
402
397
|
### Everything unrecognised becomes `A3001`
|
|
403
398
|
|
|
404
|
-
The code table contains only codes that were
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
399
|
+
The code table contains only codes that were induced against a real engine on a real schema. A Prisma
|
|
400
|
+
code the table does not contain is left to core's adapter boundary, which answers `A3001 ADAPTER_ERROR`
|
|
401
|
+
with core's own text and sends the raw error to the diagnostics sink. Adding a row later is additive;
|
|
402
|
+
a guessed row, once shipped, cannot be taken back.
|
|
408
403
|
|
|
409
404
|
### Prisma 7 only
|
|
410
405
|
|
|
411
|
-
The code table's keys are P-codes, which are
|
|
412
|
-
is a rewrite: no `@prisma/client`, no `prisma generate`, no driver adapters, dotted
|
|
413
|
-
P-codes yet. A Prisma 8 adapter
|
|
414
|
-
this one. A project keeping Prisma 7 beside
|
|
415
|
-
`prisma7`), which the generate step
|
|
416
|
-
bound is enforced
|
|
417
|
-
|
|
418
|
-
|
|
406
|
+
The code table's keys are P-codes, which are Prisma 7.x's error surface. Prisma 8 (a release candidate
|
|
407
|
+
in October 2026) is a rewrite: no `@prisma/client`, no `prisma generate`, no driver adapters, and dotted
|
|
408
|
+
`NAMESPACE.SUBCODE` errors with no P-codes yet. A Prisma 8 adapter will therefore be a new adapter
|
|
409
|
+
package, `@aventara/prisma8-adapter`, instead of a re-map of this one. A project keeping Prisma 7 beside
|
|
410
|
+
a Prisma 8 install runs the Prisma 7 CLI as `@prisma/prisma7` (bin `prisma7`), which the generate step
|
|
411
|
+
prefers. The bound is enforced: the package's own suite reads the installed `@prisma/client`'s version
|
|
412
|
+
and fails if the major is not `7`, and the discovery artifact records the major it was generated
|
|
413
|
+
against.
|
|
419
414
|
|
|
420
415
|
---
|
|
421
416
|
|
|
422
417
|
## Transactions
|
|
423
418
|
|
|
424
|
-
`Adapter.transaction` is mounted
|
|
425
|
-
transaction
|
|
419
|
+
`Adapter.transaction` is mounted exactly when the declared driver supports an interactive
|
|
420
|
+
transaction. It is one `client.$transaction(async (tx) => …)` per Aventara transaction plan:
|
|
426
421
|
|
|
427
|
-
- the
|
|
428
|
-
operation *n* may consume operation *n − 1*'s result and the second call's arguments do not exist
|
|
429
|
-
until the first has answered. The array form takes every call up front, so it cannot
|
|
430
|
-
|
|
431
|
-
-
|
|
432
|
-
|
|
433
|
-
-
|
|
434
|
-
|
|
435
|
-
an integrity failure.
|
|
422
|
+
- It uses the callback form only, never the array form. An Aventara plan is sequential by contract, so
|
|
423
|
+
operation *n* may consume operation *n − 1*'s result, and the second call's arguments do not exist
|
|
424
|
+
until the first has answered. The array form takes every call up front, so it cannot carry a plan at
|
|
425
|
+
all, and the interface this adapter holds cannot even represent it.
|
|
426
|
+
- The root client is unreachable from inside the callback: the executor core is handed closes over the
|
|
427
|
+
transaction client and nothing else.
|
|
428
|
+
- The value the callback threw is rethrown by identity. The adapter adds no `try`, no `catch` and no
|
|
429
|
+
wrapper, because core's transaction interface requires that, and wrapping would turn every plan
|
|
430
|
+
failure into an integrity failure.
|
|
436
431
|
|
|
437
|
-
A first-style variant inside a plan
|
|
438
|
-
|
|
432
|
+
A first-style variant inside a plan reuses the plan's transaction instead of opening a nested one, so
|
|
433
|
+
its two statements roll back with the plan, not independently.
|
|
439
434
|
|
|
440
|
-
|
|
441
|
-
|
|
435
|
+
Prisma swallows its own rollback failures: the rollback's rejection reaches an internal debug logger
|
|
436
|
+
and never the caller. This adapter therefore never claims a rollback succeeded, because it cannot
|
|
442
437
|
observe that it did. A commit failure is different: it rejects like a callback throw.
|
|
443
438
|
|
|
444
439
|
### `transactions` is a driver fact, and it reaches the ClientContract hash
|
|
445
440
|
|
|
446
|
-
`@prisma/adapter-d1` declares `transactions: "none"
|
|
447
|
-
|
|
448
|
-
|
|
441
|
+
`@prisma/adapter-d1` declares `transactions: "none"`: Cloudflare D1 rejects interactive transactions,
|
|
442
|
+
and Prisma says so itself. `@prisma/adapter-pg` and `@prisma/adapter-better-sqlite3` declare
|
|
443
|
+
`"interactive"`, observed by running one.
|
|
449
444
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
generated
|
|
454
|
-
`"interactive"` one, or the reverse.
|
|
445
|
+
Plan for the consequence. The `transactions` capability is part of the ClientContract hash, and a
|
|
446
|
+
generated client fails hard on a hash mismatch. So one schema, one framework configuration and two
|
|
447
|
+
deployment drivers produce two ClientContracts, two hashes and two generated clients, and a client
|
|
448
|
+
generated against a `"none"` deployment will not run against an `"interactive"` one, or the reverse.
|
|
455
449
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
450
|
+
Generate your client against the deployment's own driver, and treat the driver as part of the
|
|
451
|
+
deployment's identity, not as a connection detail. If you deploy the same application over D1 and over
|
|
452
|
+
PostgreSQL, those are two deployments with two generated clients.
|
|
459
453
|
|
|
460
454
|
An artifact and a driver cannot drift silently either: an artifact that says `"interactive"` loaded by
|
|
461
455
|
an adapter whose declared driver says `"none"` makes framework construction fail with a capability
|
|
@@ -465,111 +459,163 @@ mismatch.
|
|
|
465
459
|
|
|
466
460
|
## Referential actions
|
|
467
461
|
|
|
468
|
-
|
|
469
|
-
depends on rows a cascade removed
|
|
462
|
+
The artifact carries the referential actions, the adapter states them to core, and core refuses a
|
|
463
|
+
transaction plan that depends on rows a cascade removed.
|
|
470
464
|
|
|
471
|
-
The generate step resolves each relation edge's referential actions
|
|
472
|
-
applies when the PSL argument is absent
|
|
473
|
-
the artifact's `referentialActions` member:
|
|
465
|
+
The generate step resolves each relation edge's referential actions, including the defaults Prisma
|
|
466
|
+
applies when the PSL argument is absent. It cross-checks them while reading the schema, and emits them
|
|
467
|
+
as the artifact's `referentialActions` member:
|
|
474
468
|
|
|
475
469
|
```ts
|
|
476
470
|
discovery.referentialActions;
|
|
477
471
|
// {
|
|
478
472
|
// Shop: { owningEdges: [] },
|
|
479
473
|
// Item: { owningEdges: [{ field: "shop", target: "Shop",
|
|
480
|
-
// cardinality: "one",
|
|
474
|
+
// cardinality: "one", references: ["shopId"],
|
|
481
475
|
// onDelete: "cascade", onUpdate: "cascade" }] },
|
|
482
476
|
// Clerk: { owningEdges: [{ field: "shop", target: "Shop",
|
|
483
|
-
// cardinality: "one",
|
|
477
|
+
// cardinality: "one", references: ["shopId"],
|
|
484
478
|
// onDelete: "set-null", onUpdate: "cascade" }] },
|
|
485
479
|
// }
|
|
486
480
|
```
|
|
487
481
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
482
|
+
Read it in one direction. `onDelete` is declared on the child and fires when the parent is deleted, so
|
|
483
|
+
an entry under Resource `R` says *deleting a `target` row does `onDelete` to rows of `R`*. To answer
|
|
484
|
+
*"what does deleting a `Shop` do?"*, scan every Resource's `owningEdges` for `target === "Shop"` and
|
|
485
|
+
read the enclosing Resource key: above, that is `Item` (cascade) and `Clerk` (set-null). Both ends of
|
|
486
|
+
that sentence are Resource names and nothing in the shape tells them apart, so reading it backwards
|
|
487
|
+
produces no error, only a plan that reasons backwards.
|
|
488
|
+
|
|
489
|
+
`references` are the edge's `@relation(fields: […])`, the scalars that hold the reference. A record
|
|
490
|
+
that writes both a relation and one of its reference fields (`shopId` and `shop: { $connect: … }`,
|
|
491
|
+
whether the relation is required or optional) is refused before the database is reached: `A2004` with
|
|
492
|
+
a `V1012` issue at the relation, message `Set "shopId" or "shop", not both.` The reference fields are
|
|
493
|
+
also what a required relation may be written through instead of the relation itself.
|
|
494
|
+
|
|
495
|
+
Prisma takes one record's relations either all as relation writes or all through their reference
|
|
496
|
+
fields (its checked and unchecked inputs), so every artifact's model states `referenceWrites: "uniform"`,
|
|
497
|
+
and every Contract compiled from it carries the same member. A record that writes `shopId` beside
|
|
498
|
+
another relation's write (`supplier: { $connect: … }`) is refused before the database is reached:
|
|
499
|
+
`A2004` with a `V1012` issue at the relation write. A reference field beside a back-reference (a
|
|
500
|
+
relation that holds no reference on that side, such as a shop's `items`) mixes nothing and is
|
|
501
|
+
accepted.
|
|
502
|
+
|
|
503
|
+
Only owning edges appear. PSL forbids the non-owning side of a relation from declaring an action, so a
|
|
504
|
+
back-reference's answer is its partner's and has no entry of its own. Both actions are resolved: an
|
|
505
|
+
absent PSL argument means Prisma's effective default (`set-null` where every foreign-key scalar is
|
|
506
|
+
optional, `restrict` otherwise, `cascade` on update), on both PostgreSQL and SQLite. An action spelling
|
|
507
|
+
the reader cannot name stops the generate step before it reaches the artifact.
|
|
508
|
+
|
|
509
|
+
Neither the model nor the compiled Contracts carry a referential action or a reference field. The edges
|
|
510
|
+
sit beside the model and outside `modelHash`, so they move no Contract value and no hash.
|
|
511
|
+
`createPrismaAdapter` states them to core through the adapter's optional `referentialActions` member,
|
|
512
|
+
translated one to one into core's five words (mirroring Prisma's and SQL's actions, direction
|
|
513
|
+
unchanged):
|
|
506
514
|
|
|
507
515
|
| Prisma action | core's word | deleting a `target` row does this to rows of the enclosing Resource |
|
|
508
516
|
|---|---|---|
|
|
509
517
|
| `cascade` | `cascade` | deletes them, and the closure recurses into them |
|
|
510
518
|
| `restrict` | `restrict` | refuses the delete while they reference it |
|
|
511
|
-
| `no-action` | `noAction` | refuses the delete while they reference it
|
|
519
|
+
| `no-action` | `noAction` | refuses the delete while they reference it; identical to `restrict` on both engines, because `NoAction` is not `DEFERRABLE` on either |
|
|
512
520
|
| `set-null` | `setNull` | keeps them, the reference set to null |
|
|
513
521
|
| `set-default` | `setDefault` | keeps them, the reference set to its column default |
|
|
514
522
|
|
|
515
523
|
Only `cascade` drives core behaviour (the `V1019` closure below). `restrict`, `noAction`, `setNull` and
|
|
516
|
-
`setDefault` are carried as informational adapter facts: the adapter states
|
|
517
|
-
|
|
524
|
+
`setDefault` are carried as informational adapter facts: the adapter states what the schema says
|
|
525
|
+
instead of reducing it to what core currently reads.
|
|
518
526
|
|
|
519
|
-
`onUpdate` is not carried
|
|
527
|
+
`onUpdate` is not carried, because core plans the delete closure only.
|
|
520
528
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
`
|
|
528
|
-
|
|
529
|
-
will match — and plan-scoped: a single delete that cascades runs.
|
|
529
|
+
Core builds the transitive closure of `cascade` edges once, at framework construction. The transaction
|
|
530
|
+
planner refuses a step that depends on rows an earlier step's cascade removed (its own Resource, or one
|
|
531
|
+
its arguments reach through a relation) with `A2004`/`V1019` *"Transaction step depends on a row
|
|
532
|
+
removed by an earlier step's cascade"*, before any provider statement is issued. For example, with
|
|
533
|
+
`Metric.author` (`onDelete: Cascade`) as the one cascade edge, deleting an `Author` and then reading
|
|
534
|
+
its `Metric` rows is refused. Deleting an `Author` and then reading `Post` (`Restrict`), or a
|
|
535
|
+
`Category` and then `Post` (`SetNull`), is not. The refusal is per Resource, since a plan cannot know
|
|
536
|
+
which rows a delete will match, and it is plan-scoped: a single delete that cascades runs.
|
|
530
537
|
|
|
531
538
|
---
|
|
532
539
|
|
|
533
|
-
##
|
|
540
|
+
## Raw SQL: developer statements only, always bound
|
|
541
|
+
|
|
542
|
+
Aventara never writes raw SQL itself. Every standard operation goes through Prisma's typed delegate
|
|
543
|
+
surface. `$queryRawUnsafe`, `$executeRaw`, `$executeRawUnsafe`, `findRaw`, `aggregateRaw` and
|
|
544
|
+
`$runCommandRaw` appear nowhere in this package, and no raw action is representable in the plan the
|
|
545
|
+
translator builds; a test pins that plan to the closed set of thirteen Prisma actions the adapter may
|
|
546
|
+
issue.
|
|
547
|
+
|
|
548
|
+
Developer SQL reaches the database two ways, both written as a tagged template: `framework.appTx.$sql`,
|
|
549
|
+
a step of an application transaction, and a pipeline stage's `ctx.$sql`, run at once.
|
|
534
550
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
551
|
+
```ts
|
|
552
|
+
const post = framework.appTx.Post.create.one({ data: { title, authorId }, select: ["id"] });
|
|
553
|
+
const job = framework.appTx.$sql<{ msg_id: bigint }>`
|
|
554
|
+
select pgmq.send('jobs', jsonb_build_object('postId', ${post.$ref("id")}))`;
|
|
555
|
+
const [created, rows] = await framework.transaction([post, job]);
|
|
556
|
+
// rows is { msg_id: bigint }[]
|
|
557
|
+
```
|
|
539
558
|
|
|
540
|
-
|
|
541
|
-
|
|
559
|
+
The adapter runs it with `$queryRaw` in its tagged-template form. Inside a transaction (an
|
|
560
|
+
`appTx.$sql` step, or a `ctx.$sql` in a step, a forced call or a `ctx.$transaction`) it runs on the
|
|
561
|
+
interactive transaction client, so the statement commits or rolls back with the transaction. A
|
|
562
|
+
`ctx.$sql` made outside any transaction runs on the base client as a plain statement: no `BEGIN` is
|
|
563
|
+
sent around it, so it needs no interactive transaction and works with `@prisma/adapter-d1` too. The
|
|
564
|
+
adapter declares `sqlStatements` with every driver. Every interpolated value is
|
|
565
|
+
a bound parameter, never SQL text, so the text is static and a table or column name cannot come from
|
|
566
|
+
a value. What it binds, measured on both engines with 7.10.0:
|
|
567
|
+
|
|
568
|
+
- a JSON value (a plain object or array) is bound as its JSON text: cast it, `${value}::jsonb` on
|
|
569
|
+
PostgreSQL, or read it with `json_extract(${value}, '$.k')` on SQLite;
|
|
570
|
+
- an Aventara `AvDecimal` is bound as its exact decimal text; a `Date`, `bigint`, `Uint8Array` and the
|
|
571
|
+
primitives are bound as Prisma binds them;
|
|
572
|
+
- a bare placeholder is the database's to type: PostgreSQL reads `select ${1}` as `text`, so cast where
|
|
573
|
+
the type matters.
|
|
574
|
+
|
|
575
|
+
The rows come back as `$queryRaw` reads them, except that a PostgreSQL `numeric` becomes Aventara's
|
|
576
|
+
`AvDecimal`. Prisma 7.10.0 cannot read a SQLite BLOB column out of a raw query; such a statement fails
|
|
577
|
+
with `A3001`.
|
|
578
|
+
|
|
579
|
+
A raw query's failure is always `P2010` on 7.10.0, a syntax error and a constraint violation alike.
|
|
580
|
+
The adapter tells them apart by the driver adapter's own `cause.kind`, never by message text. A
|
|
581
|
+
uniqueness or foreign-key violation answers `A2008`, as it does through a delegate, and everything
|
|
582
|
+
else (a syntax error, a missing table, a null violation) answers `A3001` with the raw error in
|
|
583
|
+
diagnostics. Either way the whole plan rolls back.
|
|
584
|
+
|
|
585
|
+
`appTx.$sql` exists only in process. `clientTx` has no SQL step, the ClientContract carries nothing of
|
|
586
|
+
it, and no `/_transactions` request can express one. A later step cannot `$ref` a SQL step's rows
|
|
587
|
+
(`A2007`/`V1010`). No guard, pipe, hook, interceptor or filter runs around a SQL step, since it has no
|
|
588
|
+
Resource or operation for one to wrap.
|
|
542
589
|
|
|
543
590
|
---
|
|
544
591
|
|
|
545
|
-
##
|
|
592
|
+
## Other behaviour
|
|
546
593
|
|
|
547
|
-
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
-
|
|
551
|
-
-
|
|
552
|
-
|
|
553
|
-
-
|
|
554
|
-
-
|
|
594
|
+
- `$count` on a paginated relation reports the count before pagination. On both engines, a relation's
|
|
595
|
+
`_count` is unaffected by `take`/`skip`/`cursor` applied to the same relation in the same query. A
|
|
596
|
+
filter on the relation does affect it.
|
|
597
|
+
- `limit`/`offset`/`cursor` are the public names. Prisma's `take`/`skip` never surface.
|
|
598
|
+
- Decimal values are converted in both directions. Prisma's bundled decimal object is not an Aventara
|
|
599
|
+
public type and does not reach a caller as one.
|
|
600
|
+
- Count variants return numbers, including zero. `create.many` and `update.many` return arrays.
|
|
601
|
+
- `find.unique`, `update.unique` and `delete.unique` answer `A2003 NOT_FOUND` on a missing target;
|
|
555
602
|
`find.first`, `update.first` and `delete.first` answer `A1001 NO_MATCH` with `null` data. The
|
|
556
|
-
distinction is
|
|
603
|
+
distinction is core's, not the adapter's.
|
|
557
604
|
|
|
558
605
|
## Not covered
|
|
559
606
|
|
|
560
|
-
Stated so nothing here reads as a claim it is not:
|
|
561
|
-
|
|
562
607
|
- every provider except PostgreSQL and SQLite;
|
|
563
|
-
- implicit many-to-many relations, `type` / `view` / composite blocks and multi-schema
|
|
564
|
-
as a recorded unknown that
|
|
565
|
-
- `@default` forms beyond `uuid()`, `cuid()`, `autoincrement()`, `now()` and `dbgenerated(…)
|
|
608
|
+
- implicit many-to-many relations, `type` / `view` / composite blocks and multi-schema. Each arrives
|
|
609
|
+
as a recorded unknown that stops the generate step instead of mapping silently;
|
|
610
|
+
- `@default` forms beyond `uuid()`, `cuid()`, `autoincrement()`, `now()` and `dbgenerated(…)`. An
|
|
566
611
|
unmatched form is recorded as a defaulted field plus an unknown that fails the generate step, so an
|
|
567
612
|
unconfirmed mapping cannot become a wrong model;
|
|
568
613
|
- whether Neon-HTTP or PlanetScale-HTTP support interactive transactions in Prisma 7. Those drivers
|
|
569
|
-
are
|
|
570
|
-
guess.
|
|
614
|
+
are absent from the driver set instead of declared `"none"`, because an entry with no evidence would
|
|
615
|
+
be a guess.
|
|
571
616
|
|
|
572
617
|
## License
|
|
573
618
|
|
|
574
|
-
PolyForm Shield 1.0.0 with an additional permission
|
|
575
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
619
|
+
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
620
|
+
may not use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
621
|
+
`LICENSE-ADDITIONAL-PERMISSION.md`.
|