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.
- package/README.md +22 -4
- package/dist/cjs/cli/config.js +3 -0
- package/dist/cjs/cli/index.js +179 -0
- package/dist/cjs/cli/prisma-report.js +216 -0
- package/dist/cjs/cli/prisma-resolve.js +335 -0
- package/dist/cjs/cli/prisma-schema.js +484 -0
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/generate.js +279 -22
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/introspect.js +203 -26
- package/dist/cjs/mssql.js +9 -10
- package/dist/cjs/mysql.js +3 -9
- package/dist/cjs/powdb-introspect.js +5 -10
- package/dist/cjs/powql.js +13 -0
- package/dist/cjs/prisma-compat.js +1147 -0
- package/dist/cjs/query/aggregates.js +67 -7
- package/dist/cjs/query/builder.js +388 -17
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/relations.js +7 -5
- package/dist/cjs/query/warn-registry.js +98 -0
- package/dist/cjs/query/writes.js +13 -5
- package/dist/cjs/schema.js +47 -0
- package/dist/cjs/sqlite.js +4 -9
- package/dist/cli/config.d.ts +26 -0
- package/dist/cli/config.js +3 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +180 -1
- package/dist/cli/prisma-report.d.ts +19 -0
- package/dist/cli/prisma-report.js +211 -0
- package/dist/cli/prisma-resolve.d.ts +87 -0
- package/dist/cli/prisma-resolve.js +330 -0
- package/dist/cli/prisma-schema.d.ts +116 -0
- package/dist/cli/prisma-schema.js +479 -0
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +18 -2
- package/dist/client.js +1 -0
- package/dist/generate.d.ts +80 -1
- package/dist/generate.js +277 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +92 -2
- package/dist/introspect.js +198 -26
- package/dist/mssql.js +10 -11
- package/dist/mysql.js +4 -10
- package/dist/powdb-introspect.js +5 -10
- package/dist/powql.js +13 -0
- package/dist/prisma-compat.d.ts +281 -0
- package/dist/prisma-compat.js +1143 -0
- package/dist/query/aggregates.js +67 -7
- package/dist/query/builder.d.ts +77 -4
- package/dist/query/builder.js +390 -19
- package/dist/query/compound-unique.d.ts +49 -0
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/deferred.d.ts +18 -0
- package/dist/query/relations.js +7 -5
- package/dist/query/types.d.ts +70 -9
- package/dist/query/warn-registry.d.ts +57 -0
- package/dist/query/warn-registry.js +92 -0
- package/dist/query/writes.js +13 -5
- package/dist/schema.d.ts +75 -0
- package/dist/schema.js +46 -0
- package/dist/sqlite.js +5 -10
- 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 `'
|
|
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),
|