turbine-orm 0.40.1 → 0.41.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 (63) hide show
  1. package/README.md +22 -4
  2. package/dist/cjs/cli/config.js +3 -0
  3. package/dist/cjs/cli/index.js +179 -0
  4. package/dist/cjs/cli/prisma-report.js +216 -0
  5. package/dist/cjs/cli/prisma-resolve.js +335 -0
  6. package/dist/cjs/cli/prisma-schema.js +484 -0
  7. package/dist/cjs/client.js +1 -0
  8. package/dist/cjs/generate.js +279 -22
  9. package/dist/cjs/index.js +3 -2
  10. package/dist/cjs/introspect.js +203 -26
  11. package/dist/cjs/mssql.js +9 -10
  12. package/dist/cjs/mysql.js +3 -9
  13. package/dist/cjs/powdb-introspect.js +5 -10
  14. package/dist/cjs/powql.js +13 -0
  15. package/dist/cjs/prisma-compat.js +1147 -0
  16. package/dist/cjs/query/aggregates.js +67 -7
  17. package/dist/cjs/query/builder.js +388 -17
  18. package/dist/cjs/query/compound-unique.js +0 -0
  19. package/dist/cjs/query/relations.js +7 -5
  20. package/dist/cjs/query/warn-registry.js +98 -0
  21. package/dist/cjs/query/writes.js +13 -5
  22. package/dist/cjs/schema.js +47 -0
  23. package/dist/cjs/sqlite.js +4 -9
  24. package/dist/cli/config.d.ts +26 -0
  25. package/dist/cli/config.js +3 -0
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +180 -1
  28. package/dist/cli/prisma-report.d.ts +19 -0
  29. package/dist/cli/prisma-report.js +211 -0
  30. package/dist/cli/prisma-resolve.d.ts +87 -0
  31. package/dist/cli/prisma-resolve.js +330 -0
  32. package/dist/cli/prisma-schema.d.ts +116 -0
  33. package/dist/cli/prisma-schema.js +479 -0
  34. package/dist/cli/ui.d.ts +1 -1
  35. package/dist/client.d.ts +18 -2
  36. package/dist/client.js +1 -0
  37. package/dist/generate.d.ts +80 -1
  38. package/dist/generate.js +277 -25
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +1 -1
  41. package/dist/introspect.d.ts +92 -2
  42. package/dist/introspect.js +198 -26
  43. package/dist/mssql.js +10 -11
  44. package/dist/mysql.js +4 -10
  45. package/dist/powdb-introspect.js +5 -10
  46. package/dist/powql.js +13 -0
  47. package/dist/prisma-compat.d.ts +281 -0
  48. package/dist/prisma-compat.js +1143 -0
  49. package/dist/query/aggregates.js +67 -7
  50. package/dist/query/builder.d.ts +77 -4
  51. package/dist/query/builder.js +390 -19
  52. package/dist/query/compound-unique.d.ts +49 -0
  53. package/dist/query/compound-unique.js +0 -0
  54. package/dist/query/deferred.d.ts +18 -0
  55. package/dist/query/relations.js +7 -5
  56. package/dist/query/types.d.ts +70 -9
  57. package/dist/query/warn-registry.d.ts +57 -0
  58. package/dist/query/warn-registry.js +92 -0
  59. package/dist/query/writes.js +13 -5
  60. package/dist/schema.d.ts +75 -0
  61. package/dist/schema.js +46 -0
  62. package/dist/sqlite.js +5 -10
  63. package/package.json +6 -1
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Hand-rolled `schema.prisma` subset parser (zero dependencies).
3
+ *
4
+ * Powers `turbine migrate-from-prisma`. It parses ONLY the constructs the
5
+ * name-mapper needs (models, enums, views, fields, `@map`/`@@map`, relations
6
+ * including implicit m2m junctions, `@@unique` including named selectors, and
7
+ * `@@id`) and is deliberately LENIENT everywhere else: any attribute, block, or
8
+ * token it does not recognize is skipped and recorded as a warning, never a
9
+ * fatal error. The live DATABASE is the authority for resolution, so a partial
10
+ * parse is still useful.
11
+ *
12
+ * It is a pure leaf like `cli/destructive.ts` - it reads a string and returns
13
+ * data, touches no filesystem, database, or process state, and imports nothing
14
+ * from the rest of the package.
15
+ *
16
+ * Where it MUST understand a construct (an unterminated block/string, a broken
17
+ * `@@id`/`@@unique`/`@@map`/`@relation`) it throws {@link PrismaParseError} with
18
+ * a 1-based line number.
19
+ */
20
+ /** A parsed attribute argument: positional or `key: value`, string/array/raw. */
21
+ export interface PrismaAttrArg {
22
+ /** Named-argument key (e.g. `fields`, `references`, `name`, `map`). Absent for positional args. */
23
+ key?: string;
24
+ /** `'string'` (unquoted literal), `'array'` (list of idents/strings), or `'raw'` (bare token). */
25
+ kind: 'string' | 'array' | 'raw';
26
+ /** Scalar value for string/raw kinds. */
27
+ value?: string;
28
+ /** Element list for the array kind (strings unquoted). */
29
+ items?: string[];
30
+ }
31
+ /** A field- or block-level attribute (`@map(...)`, `@@unique(...)`, ...). */
32
+ export interface PrismaAttr {
33
+ /** Attribute name without the leading `@`/`@@` (e.g. `map`, `relation`, `id`, `unique`). */
34
+ name: string;
35
+ /** Parsed argument list (empty when the attribute took no parens). */
36
+ args: PrismaAttrArg[];
37
+ /** True for a block attribute (`@@name`), false for a field attribute (`@name`). */
38
+ block: boolean;
39
+ /** 1-based source line the attribute was found on. */
40
+ line: number;
41
+ }
42
+ /** A parsed field line inside a model/view/type block. */
43
+ export interface PrismaField {
44
+ /** Field name as declared (the Prisma API name). */
45
+ name: string;
46
+ /** Base type with `[]` / `?` stripped (a scalar, enum, or model name). */
47
+ type: string;
48
+ /** Trailing `?` - the field is optional/nullable. */
49
+ optional: boolean;
50
+ /** Trailing `[]` - the field is a list. */
51
+ isList: boolean;
52
+ /** Field attributes in source order. */
53
+ attrs: PrismaAttr[];
54
+ /** 1-based source line. */
55
+ line: number;
56
+ }
57
+ /** A compound key derived from a `@@id` / `@@unique` block attribute. */
58
+ export interface PrismaCompoundKey {
59
+ /** Prisma field names participating, in declared order. */
60
+ fields: string[];
61
+ /** Explicit `name:` selector, else undefined (caller derives the underscore-join). */
62
+ name?: string;
63
+ /** Explicit `map:` constraint name, if any. */
64
+ map?: string;
65
+ /** `'id'` (from `@@id`) or `'unique'` (from `@@unique`). */
66
+ kind: 'id' | 'unique';
67
+ /** 1-based source line of the block attribute. */
68
+ line: number;
69
+ }
70
+ /** A parsed `model` / `view` / `type` block. */
71
+ export interface PrismaModel {
72
+ name: string;
73
+ kind: 'model' | 'view' | 'type';
74
+ /** `@@map("...")` target table name, if present. */
75
+ map?: string;
76
+ fields: PrismaField[];
77
+ /** Compound keys from `@@id` and `@@unique`. */
78
+ compoundKeys: PrismaCompoundKey[];
79
+ /** Every block attribute (`@@index`, `@@schema`, ...), recorded verbatim. */
80
+ blockAttrs: PrismaAttr[];
81
+ /** 1-based source line of the block header. */
82
+ line: number;
83
+ }
84
+ /** A parsed `enum` block. */
85
+ export interface PrismaEnum {
86
+ name: string;
87
+ /** Value names in declared order. */
88
+ values: string[];
89
+ /** `@@map("...")` target enum-type name, if present. */
90
+ map?: string;
91
+ line: number;
92
+ }
93
+ /** The full parse result. */
94
+ export interface PrismaSchemaAst {
95
+ models: PrismaModel[];
96
+ enums: PrismaEnum[];
97
+ /** Non-fatal notes: skipped/unknown blocks and attributes. */
98
+ warnings: string[];
99
+ }
100
+ /** Thrown for a malformed construct the parser must understand. Carries a line number. */
101
+ export declare class PrismaParseError extends Error {
102
+ readonly line: number;
103
+ constructor(message: string, line: number);
104
+ }
105
+ /**
106
+ * Parse a `schema.prisma` source string into a {@link PrismaSchemaAst}.
107
+ *
108
+ * Understands: model / view / type / enum blocks; field lines with `@map`,
109
+ * `@id`, `@unique`, `@default`, `@updatedAt`, `@ignore`, `@relation`; and block
110
+ * attributes `@@map`, `@@id`, `@@unique`, `@@index`, `@@schema`. Unknown
111
+ * attributes and blocks are skipped into {@link PrismaSchemaAst.warnings}.
112
+ *
113
+ * @throws {@link PrismaParseError} on an unterminated block/paren/string or a
114
+ * structurally broken `@@id` / `@@unique` / `@@map`.
115
+ */
116
+ export declare function parsePrismaSchema(source: string): PrismaSchemaAst;
@@ -0,0 +1,479 @@
1
+ /**
2
+ * Hand-rolled `schema.prisma` subset parser (zero dependencies).
3
+ *
4
+ * Powers `turbine migrate-from-prisma`. It parses ONLY the constructs the
5
+ * name-mapper needs (models, enums, views, fields, `@map`/`@@map`, relations
6
+ * including implicit m2m junctions, `@@unique` including named selectors, and
7
+ * `@@id`) and is deliberately LENIENT everywhere else: any attribute, block, or
8
+ * token it does not recognize is skipped and recorded as a warning, never a
9
+ * fatal error. The live DATABASE is the authority for resolution, so a partial
10
+ * parse is still useful.
11
+ *
12
+ * It is a pure leaf like `cli/destructive.ts` - it reads a string and returns
13
+ * data, touches no filesystem, database, or process state, and imports nothing
14
+ * from the rest of the package.
15
+ *
16
+ * Where it MUST understand a construct (an unterminated block/string, a broken
17
+ * `@@id`/`@@unique`/`@@map`/`@relation`) it throws {@link PrismaParseError} with
18
+ * a 1-based line number.
19
+ */
20
+ // ---------------------------------------------------------------------------
21
+ // Error
22
+ // ---------------------------------------------------------------------------
23
+ /** Thrown for a malformed construct the parser must understand. Carries a line number. */
24
+ export class PrismaParseError extends Error {
25
+ line;
26
+ constructor(message, line) {
27
+ super(`schema.prisma line ${line}: ${message}`);
28
+ this.name = 'PrismaParseError';
29
+ this.line = line;
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Comment stripping (string-aware, offset-preserving)
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * Blank out `//` line comments (including `///` doc comments) with spaces,
37
+ * preserving every newline and byte offset so line numbers stay exact. A `//`
38
+ * inside a double-quoted string literal is left intact (e.g.
39
+ * `@default("http://x")`). Prisma has no block-comment syntax.
40
+ */
41
+ function stripComments(src) {
42
+ let out = '';
43
+ let i = 0;
44
+ let inString = false;
45
+ while (i < src.length) {
46
+ const ch = src[i];
47
+ if (inString) {
48
+ out += ch;
49
+ if (ch === '\\' && i + 1 < src.length) {
50
+ out += src[i + 1];
51
+ i += 2;
52
+ continue;
53
+ }
54
+ if (ch === '"')
55
+ inString = false;
56
+ i++;
57
+ continue;
58
+ }
59
+ if (ch === '"') {
60
+ inString = true;
61
+ out += ch;
62
+ i++;
63
+ continue;
64
+ }
65
+ if (ch === '/' && src[i + 1] === '/') {
66
+ // Blank to end of line, keeping the newline.
67
+ while (i < src.length && src[i] !== '\n') {
68
+ out += ' ';
69
+ i++;
70
+ }
71
+ continue;
72
+ }
73
+ out += ch;
74
+ i++;
75
+ }
76
+ return out;
77
+ }
78
+ /** 1-based line number for a character offset. */
79
+ function lineAt(src, offset) {
80
+ let line = 1;
81
+ for (let i = 0; i < offset && i < src.length; i++) {
82
+ if (src[i] === '\n')
83
+ line++;
84
+ }
85
+ return line;
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Attribute-argument tokenizer
89
+ // ---------------------------------------------------------------------------
90
+ /** Unquote a `"..."` literal, resolving the escapes Prisma supports. */
91
+ function unquote(raw) {
92
+ const s = raw.trim();
93
+ if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
94
+ return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\').replace(/\\n/g, '\n').replace(/\\t/g, '\t');
95
+ }
96
+ return s;
97
+ }
98
+ /**
99
+ * Split a balanced attribute-argument body on top-level commas, respecting
100
+ * nested `(...)`, `[...]`, and `"..."`. Returns the raw comma-separated pieces.
101
+ */
102
+ function splitTopLevel(body) {
103
+ const parts = [];
104
+ let depth = 0;
105
+ let inString = false;
106
+ let cur = '';
107
+ for (let i = 0; i < body.length; i++) {
108
+ const ch = body[i];
109
+ if (inString) {
110
+ cur += ch;
111
+ if (ch === '\\' && i + 1 < body.length) {
112
+ cur += body[i + 1];
113
+ i++;
114
+ continue;
115
+ }
116
+ if (ch === '"')
117
+ inString = false;
118
+ continue;
119
+ }
120
+ if (ch === '"') {
121
+ inString = true;
122
+ cur += ch;
123
+ continue;
124
+ }
125
+ if (ch === '(' || ch === '[' || ch === '{')
126
+ depth++;
127
+ else if (ch === ')' || ch === ']' || ch === '}')
128
+ depth--;
129
+ if (ch === ',' && depth === 0) {
130
+ parts.push(cur);
131
+ cur = '';
132
+ continue;
133
+ }
134
+ cur += ch;
135
+ }
136
+ if (cur.trim() !== '')
137
+ parts.push(cur);
138
+ return parts;
139
+ }
140
+ /** Index of the first top-level `:` (outside strings/brackets), or -1. */
141
+ function topLevelColon(s) {
142
+ let depth = 0;
143
+ let inString = false;
144
+ for (let i = 0; i < s.length; i++) {
145
+ const ch = s[i];
146
+ if (inString) {
147
+ if (ch === '\\') {
148
+ i++;
149
+ continue;
150
+ }
151
+ if (ch === '"')
152
+ inString = false;
153
+ continue;
154
+ }
155
+ if (ch === '"')
156
+ inString = true;
157
+ else if (ch === '(' || ch === '[' || ch === '{')
158
+ depth++;
159
+ else if (ch === ')' || ch === ']' || ch === '}')
160
+ depth--;
161
+ else if (ch === ':' && depth === 0)
162
+ return i;
163
+ }
164
+ return -1;
165
+ }
166
+ /** Parse one argument piece into a {@link PrismaAttrArg}. */
167
+ function parseArg(piece) {
168
+ const trimmed = piece.trim();
169
+ // Named arg? `key: value` where the colon is at top level (not inside a
170
+ // string). A bare `http://x` string literal is already inside quotes, so a
171
+ // top-level colon before a quote/bracket is a named key.
172
+ let key;
173
+ let rest = trimmed;
174
+ const colonIdx = topLevelColon(trimmed);
175
+ if (colonIdx !== -1) {
176
+ const maybeKey = trimmed.slice(0, colonIdx).trim();
177
+ if (/^[a-zA-Z_]\w*$/.test(maybeKey)) {
178
+ key = maybeKey;
179
+ rest = trimmed.slice(colonIdx + 1).trim();
180
+ }
181
+ }
182
+ if (rest.startsWith('[')) {
183
+ const inner = rest.slice(1, rest.lastIndexOf(']'));
184
+ const items = splitTopLevel(inner)
185
+ .map((el) => unquote(el.trim()))
186
+ .filter((el) => el !== '');
187
+ return { key, kind: 'array', items };
188
+ }
189
+ if (rest.startsWith('"')) {
190
+ return { key, kind: 'string', value: unquote(rest) };
191
+ }
192
+ return { key, kind: 'raw', value: rest };
193
+ }
194
+ /** Find the index of the `)` matching the `(` at `open`, respecting strings/nesting. */
195
+ function matchParen(s, open) {
196
+ let depth = 0;
197
+ let inString = false;
198
+ for (let i = open; i < s.length; i++) {
199
+ const ch = s[i];
200
+ if (inString) {
201
+ if (ch === '\\') {
202
+ i++;
203
+ continue;
204
+ }
205
+ if (ch === '"')
206
+ inString = false;
207
+ continue;
208
+ }
209
+ if (ch === '"')
210
+ inString = true;
211
+ else if (ch === '(')
212
+ depth++;
213
+ else if (ch === ')') {
214
+ depth--;
215
+ if (depth === 0)
216
+ return i;
217
+ }
218
+ }
219
+ return -1;
220
+ }
221
+ /**
222
+ * Scan a fragment (the tail of a field line, or a block-attribute line) for
223
+ * attributes. Starts at each `@`, reads the attribute name, and, when followed
224
+ * by `(`, captures the balanced parenthesized body.
225
+ */
226
+ function parseAttributes(fragment, line) {
227
+ const attrs = [];
228
+ let i = 0;
229
+ while (i < fragment.length) {
230
+ if (fragment[i] !== '@') {
231
+ i++;
232
+ continue;
233
+ }
234
+ const block = fragment[i + 1] === '@';
235
+ let j = i + (block ? 2 : 1);
236
+ const nameStart = j;
237
+ while (j < fragment.length && /[\w.]/.test(fragment[j]))
238
+ j++;
239
+ const rawName = fragment.slice(nameStart, j);
240
+ if (rawName === '') {
241
+ i = j + 1;
242
+ continue;
243
+ }
244
+ let args = [];
245
+ // Skip spaces between the name and an optional '('.
246
+ let k = j;
247
+ while (k < fragment.length && (fragment[k] === ' ' || fragment[k] === '\t'))
248
+ k++;
249
+ if (fragment[k] === '(') {
250
+ const close = matchParen(fragment, k);
251
+ if (close === -1) {
252
+ throw new PrismaParseError(`unterminated "(" in attribute @${block ? '@' : ''}${rawName}`, line);
253
+ }
254
+ const body = fragment.slice(k + 1, close);
255
+ args = splitTopLevel(body).map(parseArg);
256
+ j = close + 1;
257
+ }
258
+ else {
259
+ j = k;
260
+ }
261
+ // `@db.VarChar(255)` etc. - keep only the head so `db` is the recorded name.
262
+ attrs.push({ name: rawName.split('.')[0], args, block, line });
263
+ i = j;
264
+ }
265
+ return attrs;
266
+ }
267
+ const BLOCK_KEYWORDS = new Set(['model', 'view', 'type', 'enum', 'datasource', 'generator']);
268
+ /** Find the `}` matching the `{` at `open`, respecting strings. */
269
+ function matchBrace(s, open) {
270
+ let depth = 0;
271
+ let inString = false;
272
+ for (let i = open; i < s.length; i++) {
273
+ const ch = s[i];
274
+ if (inString) {
275
+ if (ch === '\\') {
276
+ i++;
277
+ continue;
278
+ }
279
+ if (ch === '"')
280
+ inString = false;
281
+ continue;
282
+ }
283
+ if (ch === '"')
284
+ inString = true;
285
+ else if (ch === '{')
286
+ depth++;
287
+ else if (ch === '}') {
288
+ depth--;
289
+ if (depth === 0)
290
+ return i;
291
+ }
292
+ }
293
+ return -1;
294
+ }
295
+ /** Scan the top level for `keyword Name { ... }` blocks via brace matching. */
296
+ function scanBlocks(src) {
297
+ const blocks = [];
298
+ const headerRe = /(^|\n)[ \t]*([a-zA-Z]+)[ \t]+([A-Za-z_]\w*)[ \t]*\{/g;
299
+ let m;
300
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
301
+ while ((m = headerRe.exec(src)) !== null) {
302
+ const keyword = m[2];
303
+ if (!BLOCK_KEYWORDS.has(keyword))
304
+ continue;
305
+ const braceOpen = src.indexOf('{', m.index);
306
+ const close = matchBrace(src, braceOpen);
307
+ const headerLine = lineAt(src, m.index + m[1].length);
308
+ if (close === -1) {
309
+ throw new PrismaParseError(`unterminated "{" for ${keyword} ${m[3]}`, headerLine);
310
+ }
311
+ blocks.push({
312
+ keyword,
313
+ name: m[3],
314
+ body: src.slice(braceOpen + 1, close),
315
+ headerLine,
316
+ bodyOffset: braceOpen + 1,
317
+ });
318
+ headerRe.lastIndex = close + 1;
319
+ }
320
+ return blocks;
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // Body parsing
324
+ // ---------------------------------------------------------------------------
325
+ /**
326
+ * Split a block body into logical lines, keeping each line's absolute source
327
+ * offset so we can report exact line numbers. Prisma fields and block
328
+ * attributes are single-line.
329
+ */
330
+ function bodyLines(body, bodyOffset, src) {
331
+ const out = [];
332
+ let offset = 0;
333
+ for (const rawLine of body.split('\n')) {
334
+ const text = rawLine.trim();
335
+ if (text !== '')
336
+ out.push({ text, line: lineAt(src, bodyOffset + offset) });
337
+ offset += rawLine.length + 1; // + newline
338
+ }
339
+ return out;
340
+ }
341
+ /** Turn a `@@id` / `@@unique` attribute into a {@link PrismaCompoundKey}. */
342
+ function parseCompoundKey(attr, line) {
343
+ const kind = attr.name === 'id' ? 'id' : 'unique';
344
+ // Field list is the first array-kind arg (positional `[a, b]`) or a
345
+ // `fields: [a, b]` named arg.
346
+ const fieldsArg = attr.args.find((a) => a.kind === 'array' && (a.key === undefined || a.key === 'fields'));
347
+ if (!fieldsArg?.items || fieldsArg.items.length === 0) {
348
+ throw new PrismaParseError(`@@${attr.name} requires a field list, e.g. @@${attr.name}([a, b])`, line);
349
+ }
350
+ const nameArg = attr.args.find((a) => a.key === 'name');
351
+ const mapArg = attr.args.find((a) => a.key === 'map');
352
+ return {
353
+ fields: fieldsArg.items,
354
+ name: nameArg?.kind === 'string' ? nameArg.value : undefined,
355
+ map: mapArg?.kind === 'string' ? mapArg.value : undefined,
356
+ kind,
357
+ line,
358
+ };
359
+ }
360
+ function truncate(s, n = 60) {
361
+ return s.length > n ? `${s.slice(0, n)}...` : s;
362
+ }
363
+ /** Parse a single field declaration line. Returns null for a non-field line. */
364
+ function parseFieldLine(text, line, warnings) {
365
+ // First token = field name, second token = type. Both are simple words; the
366
+ // type may carry a trailing `[]` and/or `?`.
367
+ const m = text.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)(\[\])?(\?)?/);
368
+ if (!m) {
369
+ // Not a field (e.g. a stray token); skip leniently.
370
+ warnings.push(`Skipped unrecognized line ${line}: "${truncate(text)}"`);
371
+ return null;
372
+ }
373
+ const name = m[1];
374
+ const type = m[2];
375
+ const isList = m[3] === '[]';
376
+ const optional = m[4] === '?';
377
+ const rest = text.slice(m[0].length);
378
+ const attrs = parseAttributes(rest, line);
379
+ return { name, type, optional, isList, attrs, line };
380
+ }
381
+ function parseModelBody(block, kind, src, warnings) {
382
+ const model = {
383
+ name: block.name,
384
+ kind,
385
+ fields: [],
386
+ compoundKeys: [],
387
+ blockAttrs: [],
388
+ line: block.headerLine,
389
+ };
390
+ for (const { text, line } of bodyLines(block.body, block.bodyOffset, src)) {
391
+ if (text.startsWith('@@')) {
392
+ const attrs = parseAttributes(text, line);
393
+ for (const attr of attrs) {
394
+ model.blockAttrs.push(attr);
395
+ if (attr.name === 'map') {
396
+ const arg = attr.args.find((a) => a.key === undefined || a.key === 'name');
397
+ if (arg?.kind !== 'string' || !arg.value) {
398
+ throw new PrismaParseError(`@@map requires a quoted table name`, line);
399
+ }
400
+ model.map = arg.value;
401
+ }
402
+ else if (attr.name === 'id' || attr.name === 'unique') {
403
+ model.compoundKeys.push(parseCompoundKey(attr, line));
404
+ }
405
+ // @@index, @@schema, and anything else: recorded in blockAttrs, unused.
406
+ }
407
+ continue;
408
+ }
409
+ // A field line: `name Type[modifiers] @attr @attr(...)`.
410
+ const field = parseFieldLine(text, line, warnings);
411
+ if (field)
412
+ model.fields.push(field);
413
+ }
414
+ return model;
415
+ }
416
+ function parseEnumBody(block, src) {
417
+ const en = { name: block.name, values: [], line: block.headerLine };
418
+ for (const { text, line } of bodyLines(block.body, block.bodyOffset, src)) {
419
+ if (text.startsWith('@@')) {
420
+ for (const attr of parseAttributes(text, line)) {
421
+ if (attr.name === 'map') {
422
+ const arg = attr.args.find((a) => a.key === undefined);
423
+ if (arg?.kind === 'string' && arg.value)
424
+ en.map = arg.value;
425
+ }
426
+ }
427
+ continue;
428
+ }
429
+ const m = text.match(/^([A-Za-z_]\w*)/);
430
+ if (m)
431
+ en.values.push(m[1]);
432
+ }
433
+ return en;
434
+ }
435
+ // ---------------------------------------------------------------------------
436
+ // Entry point
437
+ // ---------------------------------------------------------------------------
438
+ /**
439
+ * Parse a `schema.prisma` source string into a {@link PrismaSchemaAst}.
440
+ *
441
+ * Understands: model / view / type / enum blocks; field lines with `@map`,
442
+ * `@id`, `@unique`, `@default`, `@updatedAt`, `@ignore`, `@relation`; and block
443
+ * attributes `@@map`, `@@id`, `@@unique`, `@@index`, `@@schema`. Unknown
444
+ * attributes and blocks are skipped into {@link PrismaSchemaAst.warnings}.
445
+ *
446
+ * @throws {@link PrismaParseError} on an unterminated block/paren/string or a
447
+ * structurally broken `@@id` / `@@unique` / `@@map`.
448
+ */
449
+ export function parsePrismaSchema(source) {
450
+ const src = stripComments(source);
451
+ const ast = { models: [], enums: [], warnings: [] };
452
+ for (const block of scanBlocks(src)) {
453
+ switch (block.keyword) {
454
+ case 'model':
455
+ ast.models.push(parseModelBody(block, 'model', src, ast.warnings));
456
+ break;
457
+ case 'view':
458
+ ast.models.push(parseModelBody(block, 'view', src, ast.warnings));
459
+ break;
460
+ case 'type':
461
+ // Composite/embedded types (MongoDB) are not tables. Parse leniently so
462
+ // relation fields typed as such a model still resolve, but record a note.
463
+ ast.models.push(parseModelBody(block, 'type', src, ast.warnings));
464
+ ast.warnings.push(`Block "type ${block.name}" parsed but not resolved (composite types are not tables).`);
465
+ break;
466
+ case 'enum':
467
+ ast.enums.push(parseEnumBody(block, src));
468
+ break;
469
+ case 'datasource':
470
+ case 'generator':
471
+ // Configuration blocks - irrelevant to name mapping.
472
+ break;
473
+ default:
474
+ ast.warnings.push(`Skipped unsupported block "${block.keyword} ${block.name}".`);
475
+ break;
476
+ }
477
+ }
478
+ return ast;
479
+ }
package/dist/cli/ui.d.ts CHANGED
@@ -33,7 +33,7 @@ export declare const symbols: {
33
33
  readonly info: "i" | "ℹ";
34
34
  readonly warning: "⚠" | "!";
35
35
  readonly dot: "." | "∙";
36
- readonly line: "" | "-";
36
+ readonly line: "-" | "";
37
37
  readonly vertLine: "|" | "│";
38
38
  readonly topLeft: "╭" | "+";
39
39
  readonly topRight: "+" | "╮";
package/dist/client.d.ts CHANGED
@@ -187,13 +187,29 @@ export interface TurbineConfig {
187
187
  * - `'batched'`: run the base query, then one flat follow-up query per
188
188
  * relation (`WHERE fk = ANY($1)`), stitching children client-side. Wins
189
189
  * when child FK columns are unindexed or result sets are large.
190
+ * - `'auto'` (the SQL-engine default since 0.41): per relation, use `'join'`
191
+ * unless the introspected metadata PROVES the probe columns are unindexed,
192
+ * in which case that relation falls back to the batched loader. Needs
193
+ * DB-backed index metadata (a generated / introspected client); a
194
+ * code-first `defineSchema`-only client behaves exactly like `'join'`.
195
+ * Output shape is identical to `'join'`. See {@link RelationLoadStrategy}.
190
196
  *
191
197
  * Precedence: per-query `relationLoadStrategy` arg > this config > the engine
192
- * default. On SQL engines the default is `'join'`; on PowDB the default is the
198
+ * default. On SQL engines the default is `'auto'`; on PowDB the default is the
193
199
  * batched loaders (an ineligible relation falls back to them silently even
194
- * under `'join'`).
200
+ * under `'join'`; `'auto'` resolves to PowDB's own default).
195
201
  */
196
202
  relationLoadStrategy?: RelationLoadStrategy;
203
+ /**
204
+ * When `true`, every to-many `with` relation with no explicit `orderBy` is
205
+ * loaded ordered by the target table's primary key ascending, so unordered
206
+ * child arrays come back deterministically (json_agg / the batched loaders
207
+ * otherwise leave that order engine-dependent, and `'auto'` fallback can change
208
+ * it). An explicit per-relation `orderBy` always wins; a per-query
209
+ * `stableRelationOrder` overrides this. Default `false` (SQL is byte-identical
210
+ * when off). SQL engines only. See {@link RelationLoadStrategy}.
211
+ */
212
+ stableRelationOrder?: boolean;
197
213
  /**
198
214
  * How nested-relation subqueries encode each row's JSON.
199
215
  *
package/dist/client.js CHANGED
@@ -362,6 +362,7 @@ export class TurbineClient {
362
362
  warnOnUnlimited: config.warnOnUnlimited,
363
363
  utcTimestamps: config.utcTimestamps,
364
364
  relationLoadStrategy: config.relationLoadStrategy,
365
+ stableRelationOrder: config.stableRelationOrder,
365
366
  jsonEncoding: config.jsonEncoding,
366
367
  globalFilters: config.globalFilters,
367
368
  preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),