@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
@@ -0,0 +1,692 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export class PrismaSchemaReaderError extends Error {
4
+ name = "PrismaSchemaReaderError";
5
+ }
6
+ /** Field attributes this reader models. Anything else is recorded, not dropped. */
7
+ const FIELD_ATTRIBUTES = new Set([
8
+ "@id",
9
+ "@unique",
10
+ "@updatedAt",
11
+ "@default",
12
+ "@relation",
13
+ "@ignore",
14
+ ]);
15
+ /** Block attributes this reader models. */
16
+ const BLOCK_ATTRIBUTES = new Set(["@@id", "@@unique", "@@ignore"]);
17
+ /** `@relation` arguments this reader models. A positional argument is the name. */
18
+ const RELATION_ARGUMENTS = new Set([
19
+ "name",
20
+ "fields",
21
+ "references",
22
+ "onDelete",
23
+ "onUpdate",
24
+ ]);
25
+ /**
26
+ * Bucket 2 at `@relation`'s argument level. `map:` names the foreign-key
27
+ * CONSTRAINT — measured at 7.10.0, `@relation(…, map: "beta_alpha_fk")` renders
28
+ * `ALTER TABLE "B" ADD CONSTRAINT "beta_alpha_fk" FOREIGN KEY …`, and that name
29
+ * appears nowhere else. It fails the surface test and the reason behind it
30
+ * alike, which is what separates it from `onDelete`/`onUpdate` on the same
31
+ * attribute.
32
+ */
33
+ const DISCARDED_RELATION_ARGUMENTS = new Set(["map"]);
34
+ /**
35
+ * PSL's referential-action spellings, renamed into Aventara's words. PSL writes
36
+ * the SQL-standard set in PascalCase, so this is a rename, not a translation,
37
+ * and no Prisma spelling travels with the fact.
38
+ *
39
+ * A sixth spelling is bucket 3 rather than a guess: an action this reader cannot
40
+ * name is an action a planner would silently mis-plan around.
41
+ */
42
+ const REFERENTIAL_ACTIONS = {
43
+ Cascade: "cascade",
44
+ Restrict: "restrict",
45
+ NoAction: "no-action",
46
+ SetNull: "set-null",
47
+ SetDefault: "set-default",
48
+ };
49
+ /**
50
+ * **Bucket 2 — recognised and deliberately discarded.** DB-naming and
51
+ * physical-layout concerns with no Aventara vocabulary at all.
52
+ *
53
+ * The list is explicit on purpose: the discard is then a decision a reader can
54
+ * see and audit, not an omission. Bucket 1 is `FIELD_ATTRIBUTES`,
55
+ * `BLOCK_ATTRIBUTES`, `@db.*` and `RELATION_ARGUMENTS`; bucket 3 is everything
56
+ * on neither list, which is recorded in `unhandled` and stops the build.
57
+ */
58
+ const DISCARDED_ATTRIBUTES = new Set(["@map", "@@map", "@@index"]);
59
+ /**
60
+ * Bucket 2 at argument level: the named arguments of a **handled** block
61
+ * attribute that this reader discards. `map:`'s index name is the only one —
62
+ * measured at 7.10.0, it reaches nothing but the 🛑 `internal/class.ts`
63
+ * inline-schema blob, so no value of it can ever be observed through Aventara.
64
+ *
65
+ * `name:` was on this list and is **not** any more (2026-09-29): a custom name
66
+ * REPLACES the positional compound alias in `WhereUniqueInput` and in the
67
+ * `AtLeast` key union, so it is a public-API fact, it is modelled
68
+ * ({@link PrismaSchemaBlockIdentifier}), and this list is where discarding it
69
+ * silently broke addressing for every named identifier.
70
+ *
71
+ * An argument on neither this list nor `@@id`/`@@unique`'s modelled set is
72
+ * bucket 3.
73
+ */
74
+ const DISCARDED_BLOCK_ATTRIBUTE_ARGUMENTS = new Set(["map"]);
75
+ /** The named arguments of `@@id`/`@@unique` this reader models, besides the list. */
76
+ const BLOCK_IDENTIFIER_ARGUMENTS = new Set(["name"]);
77
+ /**
78
+ * The argument name under which `@@id`/`@@unique` may carry its member list.
79
+ *
80
+ * PSL accepts the SAME list positionally or named — `@@unique([b, a])` and
81
+ * `@@unique(fields: [b, a])` are one constraint, and a generated client keys both
82
+ * `b_a` (verified with `prisma validate` and a generated client at 7.10.0,
83
+ * 2026-09-29). Which spelling is used is therefore not a fact about the schema,
84
+ * and no argument here is located by POSITION.
85
+ */
86
+ const BLOCK_IDENTIFIER_LIST_ARGUMENT = "fields";
87
+ /** A member this reader can locate: a field name and nothing else. */
88
+ const PLAIN_NAME = /^[A-Za-z_]\w*$/;
89
+ const BLOCK_KEYWORDS = new Set([
90
+ "model",
91
+ "enum",
92
+ "generator",
93
+ "datasource",
94
+ "type",
95
+ "view",
96
+ ]);
97
+ /**
98
+ * Reads a schema file, or a schema **folder** (Prisma 7 accepts either).
99
+ * A folder is read as readdir-plus-merge in filename order.
100
+ */
101
+ export async function readPrismaSchema(location) {
102
+ const stats = await stat(location).catch((cause) => {
103
+ throw new PrismaSchemaReaderError(`the Prisma schema at ${location} could not be read`, { cause });
104
+ });
105
+ const files = stats.isDirectory()
106
+ ? (await readdir(location))
107
+ .filter((entry) => entry.endsWith(".prisma"))
108
+ .sort()
109
+ .map((entry) => path.join(location, entry))
110
+ : [location];
111
+ if (files.length === 0) {
112
+ throw new PrismaSchemaReaderError(`the Prisma schema folder at ${location} contains no .prisma file`);
113
+ }
114
+ const sources = [];
115
+ for (const file of files) {
116
+ sources.push({ file, text: await readFile(file, "utf8") });
117
+ }
118
+ return readPrismaSchemaSources(sources);
119
+ }
120
+ /**
121
+ * The reader proper, over text already in hand. Merging several sources is
122
+ * concatenation: blocks are file-local and references are by name.
123
+ */
124
+ export function readPrismaSchemaSources(sources) {
125
+ const enums = [];
126
+ const models = [];
127
+ const unhandled = [];
128
+ for (const source of sources) {
129
+ readOneSource(source, enums, models, unhandled);
130
+ }
131
+ return { enums, models, unhandled };
132
+ }
133
+ function readOneSource(source, enums, models, unhandled) {
134
+ let open = null;
135
+ const lines = source.text.split("\n");
136
+ // Relations whose action argument this reader could not NAME. They are
137
+ // recorded in `unhandled` and must not then be handed a default: a resolved
138
+ // action is a claim about what a delete does, and the reader has just said it
139
+ // does not know. Kept beside the facts rather than on them, so the public
140
+ // shape carries no "unreadable" state that no consumer can ever observe (a
141
+ // non-empty `unhandled` fails the generate step first).
142
+ const unreadableActions = new Set();
143
+ for (const [index, raw] of lines.entries()) {
144
+ const site = { file: source.file, line: index + 1 };
145
+ // Comments go first, and go away string-aware: `"a, b // not a comment"`
146
+ // carries a `//` that is data, not a comment.
147
+ const line = stripComment(raw).trim();
148
+ if (line.length === 0) {
149
+ continue;
150
+ }
151
+ const record = (kind, spelling, text) => {
152
+ unhandled.push({
153
+ kind,
154
+ spelling,
155
+ text,
156
+ block: open === null ? null : open.name,
157
+ site,
158
+ });
159
+ };
160
+ if (open === null) {
161
+ open = openBlock(line, site, record);
162
+ continue;
163
+ }
164
+ if (line === "}") {
165
+ if (open.kind === "enum") {
166
+ enums.push({ name: open.name, values: open.values, site: open.site });
167
+ }
168
+ else if (open.kind === "model") {
169
+ models.push({
170
+ name: open.name,
171
+ // Defaults resolve HERE and not on the field line: the default
172
+ // turns on the foreign key's optionality, and a `@relation` may
173
+ // name a scalar declared below it.
174
+ fields: resolvePrismaReferentialDefaults(open.fields, unreadableActions),
175
+ blockId: open.blockId,
176
+ blockUniques: open.blockUniques,
177
+ ignored: open.ignored,
178
+ site: open.site,
179
+ });
180
+ }
181
+ open = null;
182
+ continue;
183
+ }
184
+ if (open.kind === "skip") {
185
+ continue;
186
+ }
187
+ if (open.kind === "enum") {
188
+ // An enum body carries members and block attributes. `@@map` on the enum
189
+ // is bucket 2 like every other `@@map`; anything else `@@` is bucket 3.
190
+ if (line.startsWith("@@")) {
191
+ for (const attribute of splitAttributes(line)) {
192
+ const name = attributeName(attribute);
193
+ if (!DISCARDED_ATTRIBUTES.has(name)) {
194
+ record("block-attribute", name, attribute);
195
+ }
196
+ }
197
+ continue;
198
+ }
199
+ // A member is a name, optionally followed by attributes. A member-level
200
+ // `@map` falls into bucket 2 and is no longer a special case, so the
201
+ // attributes are located only far enough to find where the name ends.
202
+ const attributes = splitAttributes(line);
203
+ const name = line.slice(0, firstTopLevelAt(line) ?? line.length).trim();
204
+ if (!/^[A-Za-z_]\w*$/.test(name)) {
205
+ record("line", "enum-member", line);
206
+ continue;
207
+ }
208
+ for (const attribute of attributes) {
209
+ const attributeName_ = attributeName(attribute);
210
+ if (!DISCARDED_ATTRIBUTES.has(attributeName_)) {
211
+ record("field-attribute", attributeName_, attribute);
212
+ }
213
+ }
214
+ open.values.push(name);
215
+ continue;
216
+ }
217
+ if (line.startsWith("@@")) {
218
+ readBlockAttributeLine(line, open, record);
219
+ continue;
220
+ }
221
+ const field = readFieldLine(line, site, record, unreadableActions);
222
+ if (field !== null) {
223
+ open.fields.push(field);
224
+ }
225
+ }
226
+ if (open !== null) {
227
+ throw new PrismaSchemaReaderError(`${source.file} ends inside the block "${open.name}"`);
228
+ }
229
+ }
230
+ function openBlock(line, site, record) {
231
+ const match = /^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\{/.exec(line);
232
+ const keyword = match?.[1];
233
+ const name = match?.[2];
234
+ if (keyword === undefined || name === undefined) {
235
+ record("line", "top-level", line);
236
+ return null;
237
+ }
238
+ if (!BLOCK_KEYWORDS.has(keyword)) {
239
+ record("block", keyword, line);
240
+ return { kind: "skip", name };
241
+ }
242
+ if (keyword === "model") {
243
+ return {
244
+ kind: "model",
245
+ name,
246
+ site,
247
+ fields: [],
248
+ blockId: null,
249
+ blockUniques: [],
250
+ ignored: false,
251
+ };
252
+ }
253
+ if (keyword === "enum") {
254
+ return { kind: "enum", name, site, values: [] };
255
+ }
256
+ if (keyword === "type" || keyword === "view") {
257
+ // Modelling these would be evaluating, not locating (see the header).
258
+ record("block", keyword, line);
259
+ }
260
+ return { kind: "skip", name };
261
+ }
262
+ function readBlockAttributeLine(line, open, record) {
263
+ const attributes = splitAttributes(line);
264
+ if (attributes.length !== 1) {
265
+ // `prisma validate` rejects two block attributes on one line with P1012
266
+ // (case 3 in the header), so this is unreachable through the real tool.
267
+ // It is recorded rather than assumed away.
268
+ record("line", "block-attribute-line", line);
269
+ return;
270
+ }
271
+ const attribute = attributes[0];
272
+ const name = attributeName(attribute);
273
+ if (DISCARDED_ATTRIBUTES.has(name)) {
274
+ // Bucket 2. `@@map` and `@@index` are DB-naming and physical-layout only.
275
+ return;
276
+ }
277
+ if (!BLOCK_ATTRIBUTES.has(name)) {
278
+ record("block-attribute", name, attribute);
279
+ return;
280
+ }
281
+ if (name === "@@ignore") {
282
+ open.ignored = true;
283
+ return;
284
+ }
285
+ // Every argument is classified BY NAME, never by position. PSL writes the
286
+ // member list either positionally or as `fields:`, and lets the named
287
+ // arguments come in any order; all eight spellings of `@@unique` and all five
288
+ // of `@@id` are accepted by `prisma validate` 7.10.0 (measured 2026-09-29).
289
+ // Reading the list as "the first argument" is what made the reader take
290
+ // `@@unique(fields: [b, a])` as one member called `fields: [b, a]`, silently,
291
+ // and the generate step then refused a schema Prisma accepts.
292
+ //
293
+ // The other arguments are classified, not skipped: `name:` is modelled (its
294
+ // value is an `AtLeast` key), `map:` is bucket 2 (invisible on the 🟢
295
+ // surface), and an argument spelling this reader has never seen is bucket 3
296
+ // and stops the build.
297
+ //
298
+ // Member order follows THE ATTRIBUTE'S LIST, never the field declaration
299
+ // order: `@@unique([y, x])` is `["y", "x"]` even where `x` is declared first.
300
+ let members = null;
301
+ let unlocatable = false;
302
+ let alias = null;
303
+ for (const argument of splitTopLevel(argumentText(attribute) ?? "", ",")) {
304
+ const keyed = /^([A-Za-z_]\w*)\s*:\s*([\s\S]*)$/.exec(argument);
305
+ const key = keyed?.[1];
306
+ const value = keyed?.[2] ?? argument;
307
+ if (key === undefined || key === BLOCK_IDENTIFIER_LIST_ARGUMENT) {
308
+ // The positional argument IS the member list, and `fields:` is that
309
+ // same list named; PSL rejects a declaration carrying both.
310
+ const located = listMembers(value);
311
+ if (located === null) {
312
+ unlocatable = true;
313
+ continue;
314
+ }
315
+ members = located;
316
+ continue;
317
+ }
318
+ if (BLOCK_IDENTIFIER_ARGUMENTS.has(key)) {
319
+ // Located and unquoted, never evaluated: PSL writes it as a string
320
+ // literal and Prisma spells the key with exactly those characters.
321
+ alias = unquote(value);
322
+ continue;
323
+ }
324
+ if (!DISCARDED_BLOCK_ATTRIBUTE_ARGUMENTS.has(key)) {
325
+ record("attribute-argument", key, argument);
326
+ }
327
+ }
328
+ if (members === null || unlocatable) {
329
+ // Bucket 3, and deliberately not a guess: a member spelled `a(sort: Desc)`
330
+ // — valid PSL at 7.10.0 — cannot be reduced to a field name without
331
+ // EVALUATING it, which this reader does not do. Recorded, so the generate
332
+ // step stops loudly rather than emitting an identifier over a column no
333
+ // `AdapterFieldMap` carries.
334
+ record("block-attribute", name, attribute);
335
+ return;
336
+ }
337
+ const identifier = { members, name: alias };
338
+ if (name === "@@id") {
339
+ open.blockId = identifier;
340
+ }
341
+ else {
342
+ open.blockUniques.push(identifier);
343
+ }
344
+ }
345
+ function readFieldLine(line, site, record, unreadableActions) {
346
+ const attributeStart = firstTopLevelAt(line);
347
+ const declaration = (attributeStart === null ? line : line.slice(0, attributeStart)).trim();
348
+ const tokens = declaration.split(/\s+/).filter((t) => t.length > 0);
349
+ const name = tokens[0];
350
+ const written = tokens[1];
351
+ if (tokens.length !== 2 || name === undefined || written === undefined) {
352
+ // `prisma validate` rejects a split field declaration with P1012 (case 4 in
353
+ // the header), so this is unreachable through the real tool.
354
+ record("line", "field-declaration", line);
355
+ return null;
356
+ }
357
+ const list = written.endsWith("[]");
358
+ const optional = written.endsWith("?");
359
+ const type = written.replace(/(\[\]|\?)$/, "");
360
+ let id = false;
361
+ let unique = false;
362
+ let updatedAt = false;
363
+ let ignored = false;
364
+ let defaultArgument = null;
365
+ let relation = null;
366
+ const native = [];
367
+ for (const attribute of attributeStart === null
368
+ ? []
369
+ : splitAttributes(line.slice(attributeStart))) {
370
+ const attributeName_ = attributeName(attribute);
371
+ if (attributeName_.startsWith("@db.")) {
372
+ // A hint only, kept exactly as written.
373
+ native.push(attribute);
374
+ continue;
375
+ }
376
+ if (DISCARDED_ATTRIBUTES.has(attributeName_)) {
377
+ // Bucket 2. A field-level `@map` names a column; Aventara has no
378
+ // vocabulary for a column name, and recording it here would fail the
379
+ // generate step on most real Prisma schemas.
380
+ continue;
381
+ }
382
+ if (!FIELD_ATTRIBUTES.has(attributeName_)) {
383
+ record("field-attribute", attributeName_, attribute);
384
+ continue;
385
+ }
386
+ switch (attributeName_) {
387
+ case "@id":
388
+ id = true;
389
+ break;
390
+ case "@unique":
391
+ unique = true;
392
+ break;
393
+ case "@updatedAt":
394
+ updatedAt = true;
395
+ break;
396
+ case "@ignore":
397
+ ignored = true;
398
+ break;
399
+ case "@default":
400
+ // RAW TEXT. Matched against a table downstream, never parsed here.
401
+ defaultArgument = argumentText(attribute);
402
+ break;
403
+ default:
404
+ relation = readRelationAttribute(attribute, record, unreadableActions);
405
+ break;
406
+ }
407
+ }
408
+ return {
409
+ name,
410
+ type,
411
+ list,
412
+ optional,
413
+ id,
414
+ unique,
415
+ updatedAt,
416
+ defaultArgument,
417
+ relation,
418
+ native,
419
+ ignored,
420
+ site,
421
+ };
422
+ }
423
+ function readRelationAttribute(attribute, record, unreadableActions) {
424
+ let name = null;
425
+ let fields = null;
426
+ let references = null;
427
+ let onDelete = null;
428
+ let onUpdate = null;
429
+ let unreadableAction = false;
430
+ for (const argument of splitTopLevel(argumentText(attribute) ?? "", ",")) {
431
+ const keyed = /^([A-Za-z_]\w*)\s*:\s*([\s\S]*)$/.exec(argument);
432
+ if (keyed === null) {
433
+ // A positional argument is the relation name — the ONLY evidence
434
+ // pairing a back-reference with its forward reference.
435
+ name = unquote(argument);
436
+ continue;
437
+ }
438
+ const key = keyed[1];
439
+ const value = keyed[2].trim();
440
+ if (DISCARDED_RELATION_ARGUMENTS.has(key)) {
441
+ // Bucket 2: a foreign-key constraint name, and nothing else.
442
+ continue;
443
+ }
444
+ if (!RELATION_ARGUMENTS.has(key)) {
445
+ record("attribute-argument", key, argument);
446
+ continue;
447
+ }
448
+ if (key === "name") {
449
+ name = unquote(value);
450
+ continue;
451
+ }
452
+ if (key === "onDelete" || key === "onUpdate") {
453
+ // Located, then renamed through a closed table. An enum member is
454
+ // written bare in PSL, so this is a lookup and not an evaluation.
455
+ const action = REFERENTIAL_ACTIONS[value];
456
+ if (action === undefined) {
457
+ record("attribute-argument", key, argument);
458
+ unreadableAction = true;
459
+ continue;
460
+ }
461
+ if (key === "onDelete") {
462
+ onDelete = action;
463
+ }
464
+ else {
465
+ onUpdate = action;
466
+ }
467
+ continue;
468
+ }
469
+ // `fields:`/`references:` are located the same way the block attributes'
470
+ // list is, and by the same rule: a member this reader cannot reduce to a
471
+ // field name without evaluating it is bucket 3, not a guess.
472
+ const located = listMembers(value);
473
+ if (located === null) {
474
+ record("attribute-argument", key, argument);
475
+ continue;
476
+ }
477
+ if (key === "fields") {
478
+ fields = located;
479
+ }
480
+ else {
481
+ references = located;
482
+ }
483
+ }
484
+ const relation = {
485
+ name,
486
+ fields,
487
+ references,
488
+ onDelete,
489
+ onUpdate,
490
+ };
491
+ if (unreadableAction) {
492
+ unreadableActions.add(relation);
493
+ }
494
+ return relation;
495
+ }
496
+ /**
497
+ * Absent `onDelete`/`onUpdate` replaced by Prisma's **effective** default, on the
498
+ * owning edges of one model.
499
+ *
500
+ * An absent action and the explicitly-written default are one fact about what a
501
+ * delete does, so there is no third state to carry. What the default IS was
502
+ * measured at 7.10.0 with
503
+ * `prisma migrate diff --from-empty --to-schema <file> --script`, identical on
504
+ * `postgresql` and `sqlite` (2026-09-29):
505
+ *
506
+ * ```text
507
+ * alphaId String alpha Alpha → ON DELETE RESTRICT ON UPDATE CASCADE
508
+ * alphaId String? alpha Alpha? → ON DELETE SET NULL ON UPDATE CASCADE
509
+ * aId String, aK String? a A? → ON DELETE RESTRICT ON UPDATE CASCADE
510
+ * ```
511
+ *
512
+ * The third line is why the discriminant is stated as it is. The plain reading —
513
+ * "a required relation restricts, an optional one nulls" — is wrong: the
514
+ * relation field's own `?` does not decide it. `SET NULL` appears exactly when
515
+ * **every** scalar in `fields:` is optional, which is the only case where the
516
+ * database can actually write the null. A compound key with one required member
517
+ * restricts, and so does an optional relation field over a required foreign key.
518
+ *
519
+ * A foreign key naming no sibling is left non-optional here, which resolves to
520
+ * `restrict`; `field.mapper.ts` throws on that schema before any consumer reads
521
+ * the action.
522
+ */
523
+ function resolvePrismaReferentialDefaults(fields, unreadableActions) {
524
+ const optionalByName = new Map(fields.map((field) => [field.name, field.optional]));
525
+ return fields.map((field) => {
526
+ const relation = field.relation;
527
+ // `fields:` is what makes an edge the owning side, and PSL forbids the
528
+ // other side from carrying an action at all — so a back-reference keeps
529
+ // `null`, which reads as "ask the other edge".
530
+ if (relation === null || relation.fields === null) {
531
+ return field;
532
+ }
533
+ if (unreadableActions.has(relation)) {
534
+ return field;
535
+ }
536
+ const nullable = relation.fields.length > 0 &&
537
+ relation.fields.every((name) => optionalByName.get(name) === true);
538
+ return {
539
+ ...field,
540
+ relation: {
541
+ ...relation,
542
+ onDelete: relation.onDelete ?? (nullable ? "set-null" : "restrict"),
543
+ onUpdate: relation.onUpdate ?? "cascade",
544
+ },
545
+ };
546
+ });
547
+ }
548
+ /* ------------------------------------------------------------------ *
549
+ * Locating primitives. Every one of these finds where a token is; not *
550
+ * one of them decides what a token means. *
551
+ * ------------------------------------------------------------------ */
552
+ /** Removes a `//` comment, string-aware: a `//` inside a string literal is data. */
553
+ function stripComment(line) {
554
+ let inString = false;
555
+ for (let i = 0; i < line.length; i += 1) {
556
+ const ch = line[i];
557
+ if (inString) {
558
+ if (ch === "\\") {
559
+ i += 1;
560
+ }
561
+ else if (ch === '"') {
562
+ inString = false;
563
+ }
564
+ continue;
565
+ }
566
+ if (ch === '"') {
567
+ inString = true;
568
+ continue;
569
+ }
570
+ if (ch === "/" && line[i + 1] === "/") {
571
+ return line.slice(0, i);
572
+ }
573
+ }
574
+ return line;
575
+ }
576
+ /** The index of the first attribute `@` outside strings and brackets, or null. */
577
+ function firstTopLevelAt(text) {
578
+ for (const index of topLevelAtIndexes(text)) {
579
+ return index;
580
+ }
581
+ return null;
582
+ }
583
+ function* topLevelAtIndexes(text) {
584
+ let depth = 0;
585
+ let inString = false;
586
+ for (let i = 0; i < text.length; i += 1) {
587
+ const ch = text[i];
588
+ if (inString) {
589
+ if (ch === "\\") {
590
+ i += 1;
591
+ }
592
+ else if (ch === '"') {
593
+ inString = false;
594
+ }
595
+ continue;
596
+ }
597
+ if (ch === '"') {
598
+ inString = true;
599
+ }
600
+ else if (ch === "(" || ch === "[") {
601
+ depth += 1;
602
+ }
603
+ else if (ch === ")" || ch === "]") {
604
+ depth -= 1;
605
+ }
606
+ else if (ch === "@" && depth === 0 && text[i - 1] !== "@") {
607
+ // `@@` is one marker, not two attributes.
608
+ yield i;
609
+ }
610
+ }
611
+ }
612
+ /** Splits `@a(1) @@b([x])` into whole attributes, strings and brackets respected. */
613
+ function splitAttributes(text) {
614
+ const starts = [...topLevelAtIndexes(text)];
615
+ return starts
616
+ .map((start, position) => text.slice(start, starts[position + 1] ?? text.length).trim())
617
+ .filter((attribute) => attribute.length > 0);
618
+ }
619
+ /** `@db.Decimal(10, 2)` → `@db.Decimal`; `@@unique([y, x])` → `@@unique`. */
620
+ function attributeName(attribute) {
621
+ return /^@@?[A-Za-z_][\w.]*/.exec(attribute)?.[0] ?? attribute;
622
+ }
623
+ /** The text between an attribute's outermost parentheses, or null when it has none. */
624
+ function argumentText(attribute) {
625
+ const open = attribute.indexOf("(");
626
+ if (open < 0 || !attribute.trimEnd().endsWith(")")) {
627
+ return null;
628
+ }
629
+ return attribute
630
+ .trimEnd()
631
+ .slice(open + 1, -1)
632
+ .trim();
633
+ }
634
+ /**
635
+ * A `[…]` argument's members in written order, or **null** when the value is not
636
+ * a list of plain field names.
637
+ *
638
+ * Null is the locator's honest answer, and the caller turns it into an
639
+ * `unhandled` entry. Two shapes reach it, both valid PSL at 7.10.0: a member
640
+ * carrying an argument of its own (`@@unique([a(sort: Desc)])`), and a value that
641
+ * is not a list at all. Either one would have to be EVALUATED to yield a field
642
+ * name, and a reader that guesses here emits an identifier over a column that
643
+ * does not exist — which is exactly how the named-list defect stayed silent.
644
+ */
645
+ function listMembers(value) {
646
+ const trimmed = value.trim();
647
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
648
+ return null;
649
+ }
650
+ const members = splitTopLevel(trimmed.slice(1, -1), ",");
651
+ return members.every((member) => PLAIN_NAME.test(member)) ? members : null;
652
+ }
653
+ /** Splits on a separator at bracket depth 0, outside string literals. */
654
+ function splitTopLevel(text, separator) {
655
+ const out = [];
656
+ let depth = 0;
657
+ let inString = false;
658
+ let start = 0;
659
+ for (let i = 0; i < text.length; i += 1) {
660
+ const ch = text[i];
661
+ if (inString) {
662
+ if (ch === "\\") {
663
+ i += 1;
664
+ }
665
+ else if (ch === '"') {
666
+ inString = false;
667
+ }
668
+ continue;
669
+ }
670
+ if (ch === '"') {
671
+ inString = true;
672
+ }
673
+ else if (ch === "(" || ch === "[") {
674
+ depth += 1;
675
+ }
676
+ else if (ch === ")" || ch === "]") {
677
+ depth -= 1;
678
+ }
679
+ else if (ch === separator && depth === 0) {
680
+ out.push(text.slice(start, i).trim());
681
+ start = i + 1;
682
+ }
683
+ }
684
+ out.push(text.slice(start).trim());
685
+ return out.filter((piece) => piece.length > 0);
686
+ }
687
+ function unquote(value) {
688
+ const trimmed = value.trim();
689
+ return trimmed.startsWith('"') && trimmed.endsWith('"')
690
+ ? trimmed.slice(1, -1)
691
+ : trimmed;
692
+ }