prisma-sql 1.3.0 → 1.5.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/dist/generator.cjs +4658 -0
- package/dist/generator.cjs.map +1 -0
- package/dist/generator.d.mts +1 -0
- package/dist/generator.d.ts +1 -0
- package/dist/generator.js +4656 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.cjs +0 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
|
@@ -0,0 +1,4658 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var generatorHelper = require('@prisma/generator-helper');
|
|
5
|
+
var schemaParser = require('@dee-wan/schema-parser');
|
|
6
|
+
var promises = require('fs/promises');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var internals = require('@prisma/internals');
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __defProps = Object.defineProperties;
|
|
12
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
17
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
18
|
+
var __spreadValues = (a, b) => {
|
|
19
|
+
for (var prop in b || (b = {}))
|
|
20
|
+
if (__hasOwnProp.call(b, prop))
|
|
21
|
+
__defNormalProp(a, prop, b[prop]);
|
|
22
|
+
if (__getOwnPropSymbols)
|
|
23
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
24
|
+
if (__propIsEnum.call(b, prop))
|
|
25
|
+
__defNormalProp(a, prop, b[prop]);
|
|
26
|
+
}
|
|
27
|
+
return a;
|
|
28
|
+
};
|
|
29
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
30
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
31
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
32
|
+
};
|
|
33
|
+
var __async = (__this, __arguments, generator) => {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
var fulfilled = (value) => {
|
|
36
|
+
try {
|
|
37
|
+
step(generator.next(value));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
reject(e);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var rejected = (value) => {
|
|
43
|
+
try {
|
|
44
|
+
step(generator.throw(value));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
reject(e);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
50
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// package.json
|
|
55
|
+
var require_package = __commonJS({
|
|
56
|
+
"package.json"(exports$1, module) {
|
|
57
|
+
module.exports = {
|
|
58
|
+
name: "prisma-sql",
|
|
59
|
+
version: "1.5.0",
|
|
60
|
+
description: "Convert Prisma queries to optimized SQL with type safety. 2-7x faster than Prisma Client.",
|
|
61
|
+
main: "dist/index.cjs",
|
|
62
|
+
module: "dist/index.js",
|
|
63
|
+
types: "dist/index.d.ts",
|
|
64
|
+
exports: {
|
|
65
|
+
".": {
|
|
66
|
+
types: "./dist/index.d.ts",
|
|
67
|
+
import: "./dist/index.js",
|
|
68
|
+
require: "./dist/index.cjs"
|
|
69
|
+
},
|
|
70
|
+
"./generator": {
|
|
71
|
+
types: "./dist/generator.d.ts",
|
|
72
|
+
import: "./dist/generator.js",
|
|
73
|
+
require: "./dist/generator.cjs"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
files: [
|
|
77
|
+
"dist",
|
|
78
|
+
"README.md",
|
|
79
|
+
"LICENSE"
|
|
80
|
+
],
|
|
81
|
+
scripts: {
|
|
82
|
+
build: "tsup",
|
|
83
|
+
test: "vitest",
|
|
84
|
+
"test:coverage": "vitest --coverage",
|
|
85
|
+
"test:e2e": "vitest run tests/e2e",
|
|
86
|
+
bench: "npx tsx tests/helpers/run-benchmarks.ts",
|
|
87
|
+
"prisma:generate": "npx prisma generate --schema=tests/prisma/schema.prisma",
|
|
88
|
+
"prisma:migrate": "npx prisma migrate dev --schema=tests/prisma/schema.prisma",
|
|
89
|
+
"prisma:reset": "npx prisma db push --force-reset --skip-generate --schema=tests/prisma/schema.prisma",
|
|
90
|
+
prepublishOnly: "npm run build",
|
|
91
|
+
"sonar-cli": "sonar-scanner -Dsonar.projectKey=prisma-sql -Dsonar.sources=./src -Dsonar.host.url=http://localhost:9000 -Dsonar.login=sqp_9fe07460d0aa83f711d0edf4f317f05019d0613b",
|
|
92
|
+
sonar: "yarn sonar-cli && npx tsx scripts/sonar.ts"
|
|
93
|
+
},
|
|
94
|
+
keywords: [
|
|
95
|
+
"prisma",
|
|
96
|
+
"sql",
|
|
97
|
+
"query",
|
|
98
|
+
"optimizer",
|
|
99
|
+
"postgresql",
|
|
100
|
+
"sqlite",
|
|
101
|
+
"cloudflare",
|
|
102
|
+
"d1",
|
|
103
|
+
"performance",
|
|
104
|
+
"typescript",
|
|
105
|
+
"generator"
|
|
106
|
+
],
|
|
107
|
+
repository: {
|
|
108
|
+
type: "git",
|
|
109
|
+
url: "https://github.com/multipliedtwice/prisma-to-sql.git"
|
|
110
|
+
},
|
|
111
|
+
bugs: {
|
|
112
|
+
url: "https://github.com/multipliedtwice/prisma-to-sql/issues"
|
|
113
|
+
},
|
|
114
|
+
homepage: "https://github.com/multipliedtwice/prisma-to-sql#readme",
|
|
115
|
+
author: "multipliedtwice <multipliedtwice@gmail.com>",
|
|
116
|
+
license: "MIT",
|
|
117
|
+
dependencies: {
|
|
118
|
+
"@dee-wan/schema-parser": "1.1.0",
|
|
119
|
+
"@prisma/generator-helper": "^7.2.0",
|
|
120
|
+
"@prisma/internals": "^7.2.0"
|
|
121
|
+
},
|
|
122
|
+
devDependencies: {
|
|
123
|
+
"@faker-js/faker": "^10.2.0",
|
|
124
|
+
"@prisma/adapter-better-sqlite3": "^7.2.0",
|
|
125
|
+
"@prisma/adapter-pg": "^7.2.0",
|
|
126
|
+
"@prisma/client": "7.2.0",
|
|
127
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
128
|
+
"@types/node": "^20.0.0",
|
|
129
|
+
"better-sqlite3": "^12.6.2",
|
|
130
|
+
"drizzle-kit": "^0.31.8",
|
|
131
|
+
"drizzle-orm": "^0.45.1",
|
|
132
|
+
postgres: "^3.4.8",
|
|
133
|
+
prisma: "7.2.0",
|
|
134
|
+
tsup: "^8.5.1",
|
|
135
|
+
tsx: "^4.7.0",
|
|
136
|
+
typescript: "^5.3.3",
|
|
137
|
+
vitest: "^4.0.14"
|
|
138
|
+
},
|
|
139
|
+
peerDependencies: {
|
|
140
|
+
"@prisma/client": ">=4.0.0"
|
|
141
|
+
},
|
|
142
|
+
peerDependenciesMeta: {
|
|
143
|
+
"@prisma/client": {
|
|
144
|
+
optional: false
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
engines: {
|
|
148
|
+
node: ">=16.0.0"
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// src/sql-builder-dialect.ts
|
|
155
|
+
var globalDialect = "postgres";
|
|
156
|
+
function setGlobalDialect(dialect) {
|
|
157
|
+
if (dialect !== "postgres" && dialect !== "sqlite") {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Invalid dialect: ${dialect}. Must be 'postgres' or 'sqlite'`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
globalDialect = dialect;
|
|
163
|
+
}
|
|
164
|
+
function getGlobalDialect() {
|
|
165
|
+
return globalDialect;
|
|
166
|
+
}
|
|
167
|
+
function assertNonEmpty(value, name) {
|
|
168
|
+
if (!value || value.trim().length === 0) {
|
|
169
|
+
throw new Error(`${name} is required and cannot be empty`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function arrayContains(column, value, arrayType, dialect) {
|
|
173
|
+
assertNonEmpty(column, "arrayContains column");
|
|
174
|
+
assertNonEmpty(value, "arrayContains value");
|
|
175
|
+
if (dialect === "postgres") {
|
|
176
|
+
return `${column} @> ARRAY[${value}]::${arrayType}`;
|
|
177
|
+
}
|
|
178
|
+
return `EXISTS (SELECT 1 FROM json_each(${column}) WHERE value = ${value})`;
|
|
179
|
+
}
|
|
180
|
+
function arrayOverlaps(column, value, arrayType, dialect) {
|
|
181
|
+
assertNonEmpty(column, "arrayOverlaps column");
|
|
182
|
+
assertNonEmpty(value, "arrayOverlaps value");
|
|
183
|
+
if (dialect === "postgres") {
|
|
184
|
+
return `${column} && ${value}::${arrayType}`;
|
|
185
|
+
}
|
|
186
|
+
return `EXISTS (
|
|
187
|
+
SELECT 1 FROM json_each(${column}) AS col
|
|
188
|
+
JOIN json_each(${value}) AS val
|
|
189
|
+
WHERE col.value = val.value
|
|
190
|
+
)`;
|
|
191
|
+
}
|
|
192
|
+
function arrayContainsAll(column, value, arrayType, dialect) {
|
|
193
|
+
assertNonEmpty(column, "arrayContainsAll column");
|
|
194
|
+
assertNonEmpty(value, "arrayContainsAll value");
|
|
195
|
+
if (dialect === "postgres") {
|
|
196
|
+
return `${column} @> ${value}::${arrayType}`;
|
|
197
|
+
}
|
|
198
|
+
return `NOT EXISTS (
|
|
199
|
+
SELECT 1 FROM json_each(${value}) AS val
|
|
200
|
+
WHERE NOT EXISTS (
|
|
201
|
+
SELECT 1 FROM json_each(${column}) AS col
|
|
202
|
+
WHERE col.value = val.value
|
|
203
|
+
)
|
|
204
|
+
)`;
|
|
205
|
+
}
|
|
206
|
+
function arrayIsEmpty(column, dialect) {
|
|
207
|
+
assertNonEmpty(column, "arrayIsEmpty column");
|
|
208
|
+
if (dialect === "postgres") {
|
|
209
|
+
return `(${column} IS NULL OR array_length(${column}, 1) IS NULL)`;
|
|
210
|
+
}
|
|
211
|
+
return `(${column} IS NULL OR json_array_length(${column}) = 0)`;
|
|
212
|
+
}
|
|
213
|
+
function arrayIsNotEmpty(column, dialect) {
|
|
214
|
+
assertNonEmpty(column, "arrayIsNotEmpty column");
|
|
215
|
+
if (dialect === "postgres") {
|
|
216
|
+
return `(${column} IS NOT NULL AND array_length(${column}, 1) IS NOT NULL)`;
|
|
217
|
+
}
|
|
218
|
+
return `(${column} IS NOT NULL AND json_array_length(${column}) > 0)`;
|
|
219
|
+
}
|
|
220
|
+
function arrayEquals(column, value, arrayType, dialect) {
|
|
221
|
+
assertNonEmpty(column, "arrayEquals column");
|
|
222
|
+
assertNonEmpty(value, "arrayEquals value");
|
|
223
|
+
if (dialect === "postgres") {
|
|
224
|
+
return `${column} = ${value}::${arrayType}`;
|
|
225
|
+
}
|
|
226
|
+
return `json(${column}) = json(${value})`;
|
|
227
|
+
}
|
|
228
|
+
function caseInsensitiveLike(column, pattern, dialect) {
|
|
229
|
+
assertNonEmpty(column, "caseInsensitiveLike column");
|
|
230
|
+
assertNonEmpty(pattern, "caseInsensitiveLike pattern");
|
|
231
|
+
if (dialect === "postgres") {
|
|
232
|
+
return `${column} ILIKE ${pattern}`;
|
|
233
|
+
}
|
|
234
|
+
return `LOWER(${column}) LIKE LOWER(${pattern})`;
|
|
235
|
+
}
|
|
236
|
+
function caseInsensitiveEquals(column, value, dialect) {
|
|
237
|
+
assertNonEmpty(column, "caseInsensitiveEquals column");
|
|
238
|
+
assertNonEmpty(value, "caseInsensitiveEquals value");
|
|
239
|
+
return `LOWER(${column}) = LOWER(${value})`;
|
|
240
|
+
}
|
|
241
|
+
function jsonExtractText(column, path, dialect) {
|
|
242
|
+
assertNonEmpty(column, "jsonExtractText column");
|
|
243
|
+
assertNonEmpty(path, "jsonExtractText path");
|
|
244
|
+
if (dialect === "postgres") {
|
|
245
|
+
const p = String(path).trim();
|
|
246
|
+
const pathExpr = /^\$\d+$/.test(p) ? `${p}::text[]` : p;
|
|
247
|
+
return `${column}#>>${pathExpr}`;
|
|
248
|
+
}
|
|
249
|
+
return `json_extract(${column}, ${path})`;
|
|
250
|
+
}
|
|
251
|
+
function jsonExtractNumeric(column, path, dialect) {
|
|
252
|
+
assertNonEmpty(column, "jsonExtractNumeric column");
|
|
253
|
+
assertNonEmpty(path, "jsonExtractNumeric path");
|
|
254
|
+
if (dialect === "postgres") {
|
|
255
|
+
const p = String(path).trim();
|
|
256
|
+
const pathExpr = /^\$\d+$/.test(p) ? `${p}::text[]` : p;
|
|
257
|
+
return `(${column}#>>${pathExpr})::numeric`;
|
|
258
|
+
}
|
|
259
|
+
return `CAST(json_extract(${column}, ${path}) AS REAL)`;
|
|
260
|
+
}
|
|
261
|
+
function jsonToText(column, dialect) {
|
|
262
|
+
assertNonEmpty(column, "jsonToText column");
|
|
263
|
+
if (dialect === "postgres") {
|
|
264
|
+
return `${column}::text`;
|
|
265
|
+
}
|
|
266
|
+
return column;
|
|
267
|
+
}
|
|
268
|
+
function inArray(column, value, dialect) {
|
|
269
|
+
assertNonEmpty(column, "inArray column");
|
|
270
|
+
assertNonEmpty(value, "inArray value");
|
|
271
|
+
if (dialect === "postgres") {
|
|
272
|
+
return `${column} = ANY(${value})`;
|
|
273
|
+
}
|
|
274
|
+
return `${column} IN (SELECT value FROM json_each(${value}))`;
|
|
275
|
+
}
|
|
276
|
+
function notInArray(column, value, dialect) {
|
|
277
|
+
assertNonEmpty(column, "notInArray column");
|
|
278
|
+
assertNonEmpty(value, "notInArray value");
|
|
279
|
+
if (dialect === "postgres") {
|
|
280
|
+
return `${column} != ALL(${value})`;
|
|
281
|
+
}
|
|
282
|
+
return `${column} NOT IN (SELECT value FROM json_each(${value}))`;
|
|
283
|
+
}
|
|
284
|
+
function getArrayType(prismaType, dialect) {
|
|
285
|
+
if (!prismaType || prismaType.length === 0) {
|
|
286
|
+
return dialect === "sqlite" ? "TEXT" : "text[]";
|
|
287
|
+
}
|
|
288
|
+
if (dialect === "sqlite") {
|
|
289
|
+
return "TEXT";
|
|
290
|
+
}
|
|
291
|
+
const baseType = prismaType.replace(/\[\]|\?/g, "");
|
|
292
|
+
switch (baseType) {
|
|
293
|
+
case "String":
|
|
294
|
+
return "text[]";
|
|
295
|
+
case "Int":
|
|
296
|
+
return "integer[]";
|
|
297
|
+
case "Float":
|
|
298
|
+
return "double precision[]";
|
|
299
|
+
case "Decimal":
|
|
300
|
+
return "numeric[]";
|
|
301
|
+
case "Boolean":
|
|
302
|
+
return "boolean[]";
|
|
303
|
+
case "BigInt":
|
|
304
|
+
return "bigint[]";
|
|
305
|
+
case "DateTime":
|
|
306
|
+
return "timestamptz[]";
|
|
307
|
+
default:
|
|
308
|
+
return "text[]";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function jsonAgg(content, dialect) {
|
|
312
|
+
assertNonEmpty(content, "jsonAgg content");
|
|
313
|
+
if (dialect === "postgres") {
|
|
314
|
+
return `json_agg(${content})`;
|
|
315
|
+
}
|
|
316
|
+
return `json_group_array(${content})`;
|
|
317
|
+
}
|
|
318
|
+
function jsonBuildObject(pairs, dialect) {
|
|
319
|
+
const safePairs = (pairs != null ? pairs : "").trim();
|
|
320
|
+
if (dialect === "postgres") {
|
|
321
|
+
return safePairs.length > 0 ? `json_build_object(${safePairs})` : `json_build_object()`;
|
|
322
|
+
}
|
|
323
|
+
return safePairs.length > 0 ? `json_object(${safePairs})` : `json_object()`;
|
|
324
|
+
}
|
|
325
|
+
function prepareArrayParam(value, dialect) {
|
|
326
|
+
if (!Array.isArray(value)) {
|
|
327
|
+
throw new Error("prepareArrayParam requires array value");
|
|
328
|
+
}
|
|
329
|
+
if (dialect === "postgres") {
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
return JSON.stringify(value);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/builder/shared/constants.ts
|
|
336
|
+
var SQL_SEPARATORS = Object.freeze({
|
|
337
|
+
FIELD_LIST: ", ",
|
|
338
|
+
CONDITION_AND: " AND ",
|
|
339
|
+
CONDITION_OR: " OR ",
|
|
340
|
+
ORDER_BY: ", "
|
|
341
|
+
});
|
|
342
|
+
var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
343
|
+
"select",
|
|
344
|
+
"from",
|
|
345
|
+
"where",
|
|
346
|
+
"and",
|
|
347
|
+
"or",
|
|
348
|
+
"not",
|
|
349
|
+
"in",
|
|
350
|
+
"like",
|
|
351
|
+
"between",
|
|
352
|
+
"order",
|
|
353
|
+
"by",
|
|
354
|
+
"group",
|
|
355
|
+
"having",
|
|
356
|
+
"limit",
|
|
357
|
+
"offset",
|
|
358
|
+
"join",
|
|
359
|
+
"inner",
|
|
360
|
+
"left",
|
|
361
|
+
"right",
|
|
362
|
+
"outer",
|
|
363
|
+
"on",
|
|
364
|
+
"as",
|
|
365
|
+
"table",
|
|
366
|
+
"column",
|
|
367
|
+
"index",
|
|
368
|
+
"user",
|
|
369
|
+
"users",
|
|
370
|
+
"values",
|
|
371
|
+
"update",
|
|
372
|
+
"insert",
|
|
373
|
+
"delete",
|
|
374
|
+
"create",
|
|
375
|
+
"drop",
|
|
376
|
+
"alter",
|
|
377
|
+
"truncate",
|
|
378
|
+
"grant",
|
|
379
|
+
"revoke",
|
|
380
|
+
"exec",
|
|
381
|
+
"execute",
|
|
382
|
+
"union",
|
|
383
|
+
"intersect",
|
|
384
|
+
"except",
|
|
385
|
+
"case",
|
|
386
|
+
"when",
|
|
387
|
+
"then",
|
|
388
|
+
"else",
|
|
389
|
+
"end",
|
|
390
|
+
"null",
|
|
391
|
+
"true",
|
|
392
|
+
"false",
|
|
393
|
+
"is",
|
|
394
|
+
"exists",
|
|
395
|
+
"all",
|
|
396
|
+
"any",
|
|
397
|
+
"some"
|
|
398
|
+
]);
|
|
399
|
+
var SQL_KEYWORDS = SQL_RESERVED_WORDS;
|
|
400
|
+
var DEFAULT_WHERE_CLAUSE = "1=1";
|
|
401
|
+
var SPECIAL_FIELDS = Object.freeze({
|
|
402
|
+
ID: "id"
|
|
403
|
+
});
|
|
404
|
+
var SQL_TEMPLATES = Object.freeze({
|
|
405
|
+
PUBLIC_SCHEMA: "public",
|
|
406
|
+
WHERE: "WHERE",
|
|
407
|
+
SELECT: "SELECT",
|
|
408
|
+
FROM: "FROM",
|
|
409
|
+
ORDER_BY: "ORDER BY",
|
|
410
|
+
GROUP_BY: "GROUP BY",
|
|
411
|
+
HAVING: "HAVING",
|
|
412
|
+
LIMIT: "LIMIT",
|
|
413
|
+
OFFSET: "OFFSET",
|
|
414
|
+
COUNT_ALL: "COUNT(*)",
|
|
415
|
+
AS: "AS",
|
|
416
|
+
DISTINCT_ON: "DISTINCT ON",
|
|
417
|
+
IS_NULL: "IS NULL",
|
|
418
|
+
IS_NOT_NULL: "IS NOT NULL",
|
|
419
|
+
LIKE: "LIKE",
|
|
420
|
+
AND: "AND",
|
|
421
|
+
OR: "OR",
|
|
422
|
+
NOT: "NOT"
|
|
423
|
+
});
|
|
424
|
+
var SCHEMA_PREFIXES = Object.freeze({
|
|
425
|
+
INTERNAL: "@",
|
|
426
|
+
COMMENT: "//"
|
|
427
|
+
});
|
|
428
|
+
var Ops = Object.freeze({
|
|
429
|
+
EQUALS: "equals",
|
|
430
|
+
NOT: "not",
|
|
431
|
+
GT: "gt",
|
|
432
|
+
GTE: "gte",
|
|
433
|
+
LT: "lt",
|
|
434
|
+
LTE: "lte",
|
|
435
|
+
IN: "in",
|
|
436
|
+
NOT_IN: "notIn",
|
|
437
|
+
CONTAINS: "contains",
|
|
438
|
+
STARTS_WITH: "startsWith",
|
|
439
|
+
ENDS_WITH: "endsWith",
|
|
440
|
+
HAS: "has",
|
|
441
|
+
HAS_SOME: "hasSome",
|
|
442
|
+
HAS_EVERY: "hasEvery",
|
|
443
|
+
IS_EMPTY: "isEmpty",
|
|
444
|
+
PATH: "path",
|
|
445
|
+
STRING_CONTAINS: "string_contains",
|
|
446
|
+
STRING_STARTS_WITH: "string_starts_with",
|
|
447
|
+
STRING_ENDS_WITH: "string_ends_with"
|
|
448
|
+
});
|
|
449
|
+
var LogicalOps = Object.freeze({
|
|
450
|
+
AND: "AND",
|
|
451
|
+
OR: "OR",
|
|
452
|
+
NOT: "NOT"
|
|
453
|
+
});
|
|
454
|
+
var RelationFilters = Object.freeze({
|
|
455
|
+
SOME: "some",
|
|
456
|
+
EVERY: "every",
|
|
457
|
+
NONE: "none"
|
|
458
|
+
});
|
|
459
|
+
var Modes = Object.freeze({
|
|
460
|
+
INSENSITIVE: "insensitive",
|
|
461
|
+
DEFAULT: "default"
|
|
462
|
+
});
|
|
463
|
+
var Wildcards = Object.freeze({
|
|
464
|
+
[Ops.CONTAINS]: (v) => `%${v}%`,
|
|
465
|
+
[Ops.STARTS_WITH]: (v) => `${v}%`,
|
|
466
|
+
[Ops.ENDS_WITH]: (v) => `%${v}`
|
|
467
|
+
});
|
|
468
|
+
var REGEX_CACHE = {
|
|
469
|
+
VALID_IDENTIFIER: /^[a-z_][a-z0-9_]*$/
|
|
470
|
+
};
|
|
471
|
+
var LIMITS = Object.freeze({
|
|
472
|
+
MAX_QUERY_DEPTH: 50,
|
|
473
|
+
MAX_ARRAY_SIZE: 1e4,
|
|
474
|
+
MAX_STRING_LENGTH: 1e4
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// src/builder/shared/validators/type-guards.ts
|
|
478
|
+
function isNotNullish(value) {
|
|
479
|
+
return value !== null && value !== void 0;
|
|
480
|
+
}
|
|
481
|
+
function isNonEmptyString(value) {
|
|
482
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
483
|
+
}
|
|
484
|
+
function isEmptyString(value) {
|
|
485
|
+
return typeof value === "string" && value.trim().length === 0;
|
|
486
|
+
}
|
|
487
|
+
function isNonEmptyArray(value) {
|
|
488
|
+
return Array.isArray(value) && value.length > 0;
|
|
489
|
+
}
|
|
490
|
+
function isEmptyArray(value) {
|
|
491
|
+
return Array.isArray(value) && value.length === 0;
|
|
492
|
+
}
|
|
493
|
+
function isPlainObject(val) {
|
|
494
|
+
if (!isNotNullish(val)) return false;
|
|
495
|
+
if (Array.isArray(val)) return false;
|
|
496
|
+
if (typeof val !== "object") return false;
|
|
497
|
+
return Object.prototype.toString.call(val) === "[object Object]";
|
|
498
|
+
}
|
|
499
|
+
function hasProperty(obj, key) {
|
|
500
|
+
return isPlainObject(obj) && key in obj;
|
|
501
|
+
}
|
|
502
|
+
function isArrayType(t) {
|
|
503
|
+
if (!isNotNullish(t)) return false;
|
|
504
|
+
const normalized = t.replace(/\?$/, "");
|
|
505
|
+
return normalized.endsWith("[]");
|
|
506
|
+
}
|
|
507
|
+
function isJsonType(t) {
|
|
508
|
+
return isNotNullish(t) && (t === "Json" || t === "Json?");
|
|
509
|
+
}
|
|
510
|
+
function hasValidContent(sql) {
|
|
511
|
+
return isNotNullish(sql) && sql.trim().length > 0;
|
|
512
|
+
}
|
|
513
|
+
function hasRequiredKeywords(sql) {
|
|
514
|
+
const upper = sql.toUpperCase();
|
|
515
|
+
const hasSelect = upper.includes("SELECT");
|
|
516
|
+
const hasFrom = upper.includes("FROM");
|
|
517
|
+
return hasSelect && hasFrom && upper.indexOf("SELECT") < upper.indexOf("FROM");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/builder/shared/errors.ts
|
|
521
|
+
var SqlBuilderError = class extends Error {
|
|
522
|
+
constructor(message, code, context) {
|
|
523
|
+
super(message);
|
|
524
|
+
this.name = "SqlBuilderError";
|
|
525
|
+
this.code = code;
|
|
526
|
+
this.context = context;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
function createError(message, ctx, code = "VALIDATION_ERROR") {
|
|
530
|
+
const parts = [message];
|
|
531
|
+
if (isNonEmptyArray(ctx.path)) {
|
|
532
|
+
parts.push(`Path: ${ctx.path.join(".")}`);
|
|
533
|
+
}
|
|
534
|
+
if (isNotNullish(ctx.modelName)) {
|
|
535
|
+
parts.push(`Model: ${ctx.modelName}`);
|
|
536
|
+
}
|
|
537
|
+
if (isNonEmptyArray(ctx.availableFields)) {
|
|
538
|
+
parts.push(`Available fields: ${ctx.availableFields.join(", ")}`);
|
|
539
|
+
}
|
|
540
|
+
return new SqlBuilderError(parts.join("\n"), code, ctx);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/builder/shared/model-field-cache.ts
|
|
544
|
+
var SCALAR_SET_CACHE = /* @__PURE__ */ new WeakMap();
|
|
545
|
+
var RELATION_SET_CACHE = /* @__PURE__ */ new WeakMap();
|
|
546
|
+
function getScalarFieldSet(model) {
|
|
547
|
+
const cached = SCALAR_SET_CACHE.get(model);
|
|
548
|
+
if (cached) return cached;
|
|
549
|
+
const s = /* @__PURE__ */ new Set();
|
|
550
|
+
for (const f of model.fields) if (!f.isRelation) s.add(f.name);
|
|
551
|
+
SCALAR_SET_CACHE.set(model, s);
|
|
552
|
+
return s;
|
|
553
|
+
}
|
|
554
|
+
function getRelationFieldSet(model) {
|
|
555
|
+
const cached = RELATION_SET_CACHE.get(model);
|
|
556
|
+
if (cached) return cached;
|
|
557
|
+
const s = /* @__PURE__ */ new Set();
|
|
558
|
+
for (const f of model.fields) if (f.isRelation) s.add(f.name);
|
|
559
|
+
RELATION_SET_CACHE.set(model, s);
|
|
560
|
+
return s;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/builder/shared/validators/sql-validators.ts
|
|
564
|
+
function isValidWhereClause(clause) {
|
|
565
|
+
return isNotNullish(clause) && clause.trim().length > 0 && clause !== DEFAULT_WHERE_CLAUSE;
|
|
566
|
+
}
|
|
567
|
+
function isEmptyWhere(where) {
|
|
568
|
+
if (!isNotNullish(where)) return true;
|
|
569
|
+
return Object.keys(where).length === 0;
|
|
570
|
+
}
|
|
571
|
+
function validateSelectQuery(sql) {
|
|
572
|
+
if (!hasValidContent(sql)) {
|
|
573
|
+
throw new Error("CRITICAL: Generated empty SQL query");
|
|
574
|
+
}
|
|
575
|
+
if (!hasRequiredKeywords(sql)) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`CRITICAL: Invalid SQL structure. SQL: ${sql.substring(0, 100)}...`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function sqlPreview(sql) {
|
|
582
|
+
return `${sql.substring(0, 100)}...`;
|
|
583
|
+
}
|
|
584
|
+
function parseDollarNumber(sql, start, n) {
|
|
585
|
+
let i = start;
|
|
586
|
+
let num = 0;
|
|
587
|
+
let hasDigit = false;
|
|
588
|
+
while (i < n) {
|
|
589
|
+
const c = sql.charCodeAt(i);
|
|
590
|
+
if (c < 48 || c > 57) break;
|
|
591
|
+
hasDigit = true;
|
|
592
|
+
num = num * 10 + (c - 48);
|
|
593
|
+
i++;
|
|
594
|
+
}
|
|
595
|
+
if (!hasDigit || num <= 0) return { next: i, num: 0, ok: false };
|
|
596
|
+
return { next: i, num, ok: true };
|
|
597
|
+
}
|
|
598
|
+
function scanDollarPlaceholders(sql, markUpTo) {
|
|
599
|
+
const seen = new Uint8Array(markUpTo + 1);
|
|
600
|
+
let count = 0;
|
|
601
|
+
let min = Number.POSITIVE_INFINITY;
|
|
602
|
+
let max = 0;
|
|
603
|
+
const n = sql.length;
|
|
604
|
+
let i = 0;
|
|
605
|
+
while (i < n) {
|
|
606
|
+
if (sql.charCodeAt(i) !== 36) {
|
|
607
|
+
i++;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const { next, num, ok } = parseDollarNumber(sql, i + 1, n);
|
|
611
|
+
i = next;
|
|
612
|
+
if (!ok) continue;
|
|
613
|
+
count++;
|
|
614
|
+
if (num < min) min = num;
|
|
615
|
+
if (num > max) max = num;
|
|
616
|
+
if (num <= markUpTo) seen[num] = 1;
|
|
617
|
+
}
|
|
618
|
+
return { count, min, max, seen };
|
|
619
|
+
}
|
|
620
|
+
function assertNoGaps(scan, rangeMin, rangeMax, sql) {
|
|
621
|
+
for (let k = rangeMin; k <= rangeMax; k++) {
|
|
622
|
+
if (scan.seen[k] !== 1) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`CRITICAL: Parameter mismatch - SQL is missing placeholder $${k}. Placeholders must cover ${rangeMin}..${rangeMax} with no gaps. SQL: ${sqlPreview(sql)}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function validateParamConsistency(sql, params) {
|
|
630
|
+
const paramLen = params.length;
|
|
631
|
+
if (paramLen === 0) {
|
|
632
|
+
if (sql.indexOf("$") === -1) return;
|
|
633
|
+
}
|
|
634
|
+
const scan = scanDollarPlaceholders(sql, paramLen);
|
|
635
|
+
if (scan.count === 0) {
|
|
636
|
+
if (paramLen !== 0) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`CRITICAL: Parameter mismatch - SQL has no placeholders but ${paramLen} params provided.`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (scan.max !== paramLen) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`CRITICAL: Parameter mismatch - SQL max placeholder is $${scan.max} but ${paramLen} params provided. This will cause SQL execution to fail. SQL: ${sqlPreview(sql)}`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
assertNoGaps(scan, 1, scan.max, sql);
|
|
649
|
+
}
|
|
650
|
+
function needsQuoting(id) {
|
|
651
|
+
if (!isNonEmptyString(id)) return true;
|
|
652
|
+
const isKeyword = SQL_KEYWORDS.has(id.toLowerCase());
|
|
653
|
+
if (isKeyword) return true;
|
|
654
|
+
const isValidIdentifier = REGEX_CACHE.VALID_IDENTIFIER.test(id);
|
|
655
|
+
return !isValidIdentifier;
|
|
656
|
+
}
|
|
657
|
+
function validateParamConsistencyFragment(sql, params) {
|
|
658
|
+
const paramLen = params.length;
|
|
659
|
+
const scan = scanDollarPlaceholders(sql, paramLen);
|
|
660
|
+
if (scan.max === 0) return;
|
|
661
|
+
if (scan.max > paramLen) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
`CRITICAL: Parameter mismatch - SQL references $${scan.max} but only ${paramLen} params provided. SQL: ${sqlPreview(sql)}`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
assertNoGaps(scan, scan.min, scan.max, sql);
|
|
667
|
+
}
|
|
668
|
+
function assertOrThrow(condition, message) {
|
|
669
|
+
if (!condition) throw new Error(message);
|
|
670
|
+
}
|
|
671
|
+
function dialectPlaceholderPrefix(dialect) {
|
|
672
|
+
return dialect === "sqlite" ? "?" : "$";
|
|
673
|
+
}
|
|
674
|
+
function parseSqlitePlaceholderIndices(sql) {
|
|
675
|
+
const re = /\?(?:(\d+))?/g;
|
|
676
|
+
const indices = [];
|
|
677
|
+
let anonCount = 0;
|
|
678
|
+
let sawNumbered = false;
|
|
679
|
+
let sawAnonymous = false;
|
|
680
|
+
for (const m of sql.matchAll(re)) {
|
|
681
|
+
const n = m[1];
|
|
682
|
+
if (n) {
|
|
683
|
+
sawNumbered = true;
|
|
684
|
+
indices.push(parseInt(n, 10));
|
|
685
|
+
} else {
|
|
686
|
+
sawAnonymous = true;
|
|
687
|
+
anonCount += 1;
|
|
688
|
+
indices.push(anonCount);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return { indices, sawNumbered, sawAnonymous };
|
|
692
|
+
}
|
|
693
|
+
function parseDollarPlaceholderIndices(sql) {
|
|
694
|
+
const re = /\$(\d+)/g;
|
|
695
|
+
const indices = [];
|
|
696
|
+
for (const m of sql.matchAll(re)) indices.push(parseInt(m[1], 10));
|
|
697
|
+
return indices;
|
|
698
|
+
}
|
|
699
|
+
function getPlaceholderIndices(sql, dialect) {
|
|
700
|
+
if (dialect === "sqlite") return parseSqlitePlaceholderIndices(sql);
|
|
701
|
+
return {
|
|
702
|
+
indices: parseDollarPlaceholderIndices(sql),
|
|
703
|
+
sawNumbered: false,
|
|
704
|
+
sawAnonymous: false
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function maxIndex(indices) {
|
|
708
|
+
return indices.length > 0 ? Math.max(...indices) : 0;
|
|
709
|
+
}
|
|
710
|
+
function ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous) {
|
|
711
|
+
assertOrThrow(
|
|
712
|
+
!(sawNumbered && sawAnonymous),
|
|
713
|
+
`CRITICAL: Mixed sqlite placeholders ('?' and '?NNN') are not supported.`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
function ensurePlaceholderMaxMatchesMappingsLength(max, mappingsLength, dialect) {
|
|
717
|
+
assertOrThrow(
|
|
718
|
+
max === mappingsLength,
|
|
719
|
+
`CRITICAL: SQL placeholder max mismatch - max is ${dialectPlaceholderPrefix(dialect)}${max}, but mappings length is ${mappingsLength}.`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
function ensureSequentialPlaceholders(placeholders, max, dialect) {
|
|
723
|
+
const prefix = dialectPlaceholderPrefix(dialect);
|
|
724
|
+
for (let i = 1; i <= max; i++) {
|
|
725
|
+
assertOrThrow(
|
|
726
|
+
placeholders.has(i),
|
|
727
|
+
`CRITICAL: Missing SQL placeholder ${prefix}${i} - placeholders must be sequential 1..${max}.`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function validateMappingIndex(mapping, max) {
|
|
732
|
+
assertOrThrow(
|
|
733
|
+
Number.isInteger(mapping.index) && mapping.index >= 1 && mapping.index <= max,
|
|
734
|
+
`CRITICAL: ParamMapping index ${mapping.index} out of range 1..${max}.`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
function ensureUniqueMappingIndex(mappingIndices, index, dialect) {
|
|
738
|
+
assertOrThrow(
|
|
739
|
+
!mappingIndices.has(index),
|
|
740
|
+
`CRITICAL: Duplicate ParamMapping index ${index} - each placeholder index must map to exactly one ParamMap.`
|
|
741
|
+
);
|
|
742
|
+
mappingIndices.add(index);
|
|
743
|
+
}
|
|
744
|
+
function ensureMappingIndexExistsInSql(placeholders, index) {
|
|
745
|
+
assertOrThrow(
|
|
746
|
+
placeholders.has(index),
|
|
747
|
+
`CRITICAL: ParamMapping index ${index} not found in SQL placeholders.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
function validateMappingValueShape(mapping) {
|
|
751
|
+
assertOrThrow(
|
|
752
|
+
!(mapping.dynamicName !== void 0 && mapping.value !== void 0),
|
|
753
|
+
`CRITICAL: ParamMap ${mapping.index} has both dynamicName and value`
|
|
754
|
+
);
|
|
755
|
+
assertOrThrow(
|
|
756
|
+
!(mapping.dynamicName === void 0 && mapping.value === void 0),
|
|
757
|
+
`CRITICAL: ParamMap ${mapping.index} has neither dynamicName nor value`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
function ensureMappingsCoverAllIndices(mappingIndices, max, dialect) {
|
|
761
|
+
const prefix = dialectPlaceholderPrefix(dialect);
|
|
762
|
+
for (let i = 1; i <= max; i++) {
|
|
763
|
+
assertOrThrow(
|
|
764
|
+
mappingIndices.has(i),
|
|
765
|
+
`CRITICAL: Missing ParamMap for placeholder ${prefix}${i} - mappings must cover 1..${max} with no gaps.`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function validateMappingsAgainstPlaceholders(mappings, placeholders, max, dialect) {
|
|
770
|
+
const mappingIndices = /* @__PURE__ */ new Set();
|
|
771
|
+
for (const mapping of mappings) {
|
|
772
|
+
validateMappingIndex(mapping, max);
|
|
773
|
+
ensureUniqueMappingIndex(mappingIndices, mapping.index);
|
|
774
|
+
ensureMappingIndexExistsInSql(placeholders, mapping.index);
|
|
775
|
+
validateMappingValueShape(mapping);
|
|
776
|
+
}
|
|
777
|
+
ensureMappingsCoverAllIndices(mappingIndices, max, dialect);
|
|
778
|
+
}
|
|
779
|
+
function validateSqlPositions(sql, mappings, dialect) {
|
|
780
|
+
const { indices, sawNumbered, sawAnonymous } = getPlaceholderIndices(
|
|
781
|
+
sql,
|
|
782
|
+
dialect
|
|
783
|
+
);
|
|
784
|
+
if (dialect === "sqlite") {
|
|
785
|
+
ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous);
|
|
786
|
+
}
|
|
787
|
+
const placeholders = new Set(indices);
|
|
788
|
+
if (placeholders.size === 0 && mappings.length === 0) return;
|
|
789
|
+
const max = maxIndex(indices);
|
|
790
|
+
ensurePlaceholderMaxMatchesMappingsLength(max, mappings.length, dialect);
|
|
791
|
+
ensureSequentialPlaceholders(placeholders, max, dialect);
|
|
792
|
+
validateMappingsAgainstPlaceholders(mappings, placeholders, max, dialect);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/builder/shared/sql-utils.ts
|
|
796
|
+
var NUL = String.fromCharCode(0);
|
|
797
|
+
function containsControlChars(s) {
|
|
798
|
+
return s.includes(NUL) || s.includes("\n") || s.includes("\r");
|
|
799
|
+
}
|
|
800
|
+
function containsUnsafeSqlFragmentChars(s) {
|
|
801
|
+
return containsControlChars(s) || s.includes(";");
|
|
802
|
+
}
|
|
803
|
+
function quote(id) {
|
|
804
|
+
if (isEmptyString(id)) {
|
|
805
|
+
throw new Error("quote: identifier is required and cannot be empty");
|
|
806
|
+
}
|
|
807
|
+
if (containsControlChars(id)) {
|
|
808
|
+
throw new Error(
|
|
809
|
+
`quote: identifier contains invalid characters: ${JSON.stringify(id)}`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (needsQuoting(id)) {
|
|
813
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
814
|
+
}
|
|
815
|
+
return id;
|
|
816
|
+
}
|
|
817
|
+
function col(alias, field) {
|
|
818
|
+
if (isEmptyString(alias)) {
|
|
819
|
+
throw new Error("col: alias is required and cannot be empty");
|
|
820
|
+
}
|
|
821
|
+
if (isEmptyString(field)) {
|
|
822
|
+
throw new Error("col: field is required and cannot be empty");
|
|
823
|
+
}
|
|
824
|
+
return `${alias}.${quote(field)}`;
|
|
825
|
+
}
|
|
826
|
+
function sqlStringLiteral(value) {
|
|
827
|
+
if (containsControlChars(value)) {
|
|
828
|
+
throw new Error("sqlStringLiteral: value contains invalid characters");
|
|
829
|
+
}
|
|
830
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
831
|
+
}
|
|
832
|
+
function buildTableReference(schemaName, tableName, dialect) {
|
|
833
|
+
if (isEmptyString(tableName)) {
|
|
834
|
+
throw new Error(
|
|
835
|
+
"buildTableReference: tableName is required and cannot be empty"
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (containsControlChars(tableName)) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
"buildTableReference: tableName contains invalid characters"
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const d = dialect != null ? dialect : "postgres";
|
|
844
|
+
if (d === "sqlite") {
|
|
845
|
+
return quote(tableName);
|
|
846
|
+
}
|
|
847
|
+
if (isEmptyString(schemaName)) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
"buildTableReference: schemaName is required and cannot be empty"
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if (containsControlChars(schemaName)) {
|
|
853
|
+
throw new Error(
|
|
854
|
+
"buildTableReference: schemaName contains invalid characters"
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
const safeSchema = schemaName.replace(/"/g, '""');
|
|
858
|
+
const safeTable = tableName.replace(/"/g, '""');
|
|
859
|
+
return `"${safeSchema}"."${safeTable}"`;
|
|
860
|
+
}
|
|
861
|
+
function assertSafeAlias(alias) {
|
|
862
|
+
const a = String(alias);
|
|
863
|
+
if (a.trim().length === 0) {
|
|
864
|
+
throw new Error("alias is required and cannot be empty");
|
|
865
|
+
}
|
|
866
|
+
if (containsUnsafeSqlFragmentChars(a)) {
|
|
867
|
+
throw new Error(`alias contains unsafe characters: ${JSON.stringify(a)}`);
|
|
868
|
+
}
|
|
869
|
+
if (!/^[A-Za-z_]\w*$/.test(a)) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`alias must be a simple identifier, got: ${JSON.stringify(a)}`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function assertSafeTableRef(tableRef) {
|
|
876
|
+
const t = String(tableRef);
|
|
877
|
+
if (t.trim().length === 0) {
|
|
878
|
+
throw new Error("tableName/tableRef is required and cannot be empty");
|
|
879
|
+
}
|
|
880
|
+
if (containsUnsafeSqlFragmentChars(t)) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
`tableName/tableRef contains unsafe characters: ${JSON.stringify(t)}`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function normalizeKeyList(input) {
|
|
887
|
+
if (!isNotNullish(input)) return [];
|
|
888
|
+
if (Array.isArray(input)) {
|
|
889
|
+
const out = [];
|
|
890
|
+
for (const v of input) {
|
|
891
|
+
const s2 = String(v).trim();
|
|
892
|
+
if (s2.length > 0) out.push(s2);
|
|
893
|
+
}
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
if (typeof input === "string") {
|
|
897
|
+
const raw = input.trim();
|
|
898
|
+
if (raw.length === 0) return [];
|
|
899
|
+
if (raw.includes(",")) {
|
|
900
|
+
return raw.split(",").map((s2) => s2.trim()).filter((s2) => s2.length > 0);
|
|
901
|
+
}
|
|
902
|
+
return [raw];
|
|
903
|
+
}
|
|
904
|
+
const s = String(input).trim();
|
|
905
|
+
return s.length > 0 ? [s] : [];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/builder/joins.ts
|
|
909
|
+
function isRelationField(fieldName, model) {
|
|
910
|
+
return getRelationFieldSet(model).has(fieldName);
|
|
911
|
+
}
|
|
912
|
+
function isValidRelationField(field) {
|
|
913
|
+
if (!isNotNullish(field)) return false;
|
|
914
|
+
if (!field.isRelation) return false;
|
|
915
|
+
if (!isNotNullish(field.relatedModel) || field.relatedModel.trim().length === 0)
|
|
916
|
+
return false;
|
|
917
|
+
const fk = normalizeKeyList(field.foreignKey);
|
|
918
|
+
if (fk.length === 0) return false;
|
|
919
|
+
const refsRaw = field.references;
|
|
920
|
+
const refs = normalizeKeyList(refsRaw);
|
|
921
|
+
if (refs.length === 0) return false;
|
|
922
|
+
if (refs.length !== fk.length) return false;
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
function getReferenceFieldNames(field, foreignKeyCount) {
|
|
926
|
+
const refsRaw = field.references;
|
|
927
|
+
const refs = normalizeKeyList(refsRaw);
|
|
928
|
+
if (refs.length === 0) {
|
|
929
|
+
if (foreignKeyCount === 1) return [SPECIAL_FIELDS.ID];
|
|
930
|
+
return [];
|
|
931
|
+
}
|
|
932
|
+
if (refs.length !== foreignKeyCount) return [];
|
|
933
|
+
return refs;
|
|
934
|
+
}
|
|
935
|
+
function joinCondition(field, parentAlias, childAlias) {
|
|
936
|
+
const fkFields = normalizeKeyList(field.foreignKey);
|
|
937
|
+
if (fkFields.length === 0) {
|
|
938
|
+
throw createError(
|
|
939
|
+
`Relation '${field.name}' is missing foreignKey. This indicates a schema parsing error. Relations must specify fields/references.`,
|
|
940
|
+
{ field: field.name }
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const refFields = getReferenceFieldNames(field, fkFields.length);
|
|
944
|
+
if (refFields.length !== fkFields.length) {
|
|
945
|
+
throw createError(
|
|
946
|
+
`Relation '${field.name}' is missing references (or references count does not match foreignKey count). This is required to support non-id and composite keys.`,
|
|
947
|
+
{ field: field.name }
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const parts = [];
|
|
951
|
+
for (let i = 0; i < fkFields.length; i++) {
|
|
952
|
+
const fk = fkFields[i];
|
|
953
|
+
const ref = refFields[i];
|
|
954
|
+
const left = field.isForeignKeyLocal ? `${childAlias}.${quote(ref)}` : `${childAlias}.${quote(fk)}`;
|
|
955
|
+
const right = field.isForeignKeyLocal ? `${parentAlias}.${quote(fk)}` : `${parentAlias}.${quote(ref)}`;
|
|
956
|
+
parts.push(`${left} = ${right}`);
|
|
957
|
+
}
|
|
958
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
959
|
+
}
|
|
960
|
+
function getModelByName(schemas, name) {
|
|
961
|
+
return schemas.find((m) => m.name === name);
|
|
962
|
+
}
|
|
963
|
+
function buildNotComposite(expr, val, params, dialect, buildOp, separator) {
|
|
964
|
+
const entries = Object.entries(val).filter(
|
|
965
|
+
([k, v]) => k !== "mode" && v !== void 0
|
|
966
|
+
);
|
|
967
|
+
if (entries.length === 0) return "";
|
|
968
|
+
const clauses = [];
|
|
969
|
+
for (const [subOp, subVal] of entries) {
|
|
970
|
+
const sub = buildOp(expr, subOp, subVal, params, dialect);
|
|
971
|
+
if (sub && sub.trim().length > 0) clauses.push(`(${sub})`);
|
|
972
|
+
}
|
|
973
|
+
if (clauses.length === 0) return "";
|
|
974
|
+
if (clauses.length === 1) return `${SQL_TEMPLATES.NOT} ${clauses[0]}`;
|
|
975
|
+
return `${SQL_TEMPLATES.NOT} (${clauses.join(separator)})`;
|
|
976
|
+
}
|
|
977
|
+
function buildScalarOperator(expr, op, val, params, mode, fieldType, dialect) {
|
|
978
|
+
if (val === void 0) return "";
|
|
979
|
+
if (val === null) {
|
|
980
|
+
return handleNullValue(expr, op);
|
|
981
|
+
}
|
|
982
|
+
if (op === Ops.NOT && isPlainObject(val)) {
|
|
983
|
+
return handleNotOperator(expr, val, params, mode, fieldType, dialect);
|
|
984
|
+
}
|
|
985
|
+
if (op === Ops.NOT) {
|
|
986
|
+
const placeholder = params.addAuto(val);
|
|
987
|
+
return `${expr} <> ${placeholder}`;
|
|
988
|
+
}
|
|
989
|
+
if (op === Ops.EQUALS && mode === Modes.INSENSITIVE && isNotNullish(dialect)) {
|
|
990
|
+
const placeholder = params.addAuto(val);
|
|
991
|
+
return caseInsensitiveEquals(expr, placeholder);
|
|
992
|
+
}
|
|
993
|
+
const STRING_LIKE_OPS = /* @__PURE__ */ new Set([
|
|
994
|
+
Ops.CONTAINS,
|
|
995
|
+
Ops.STARTS_WITH,
|
|
996
|
+
Ops.ENDS_WITH
|
|
997
|
+
]);
|
|
998
|
+
if (STRING_LIKE_OPS.has(op)) {
|
|
999
|
+
if (!isNotNullish(dialect)) {
|
|
1000
|
+
throw createError(`Like operators require a SQL dialect`, {
|
|
1001
|
+
operator: op
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
return handleLikeOperator(expr, op, val, params, mode, dialect);
|
|
1005
|
+
}
|
|
1006
|
+
if (op === Ops.IN || op === Ops.NOT_IN) {
|
|
1007
|
+
if (!isNotNullish(dialect)) {
|
|
1008
|
+
throw createError(`IN operators require a SQL dialect`, { operator: op });
|
|
1009
|
+
}
|
|
1010
|
+
return handleInOperator(expr, op, val, params, dialect);
|
|
1011
|
+
}
|
|
1012
|
+
return handleComparisonOperator(expr, op, val, params);
|
|
1013
|
+
}
|
|
1014
|
+
function handleNullValue(expr, op) {
|
|
1015
|
+
if (op === Ops.EQUALS) return `${expr} ${SQL_TEMPLATES.IS_NULL}`;
|
|
1016
|
+
if (op === Ops.NOT) return `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
|
|
1017
|
+
throw createError(`Operator '${op}' doesn't support null`, { operator: op });
|
|
1018
|
+
}
|
|
1019
|
+
function normalizeMode(v) {
|
|
1020
|
+
if (v === Modes.INSENSITIVE) return Modes.INSENSITIVE;
|
|
1021
|
+
if (v === Modes.DEFAULT) return Modes.DEFAULT;
|
|
1022
|
+
return void 0;
|
|
1023
|
+
}
|
|
1024
|
+
function handleNotOperator(expr, val, params, outerMode, fieldType, dialect) {
|
|
1025
|
+
const innerMode = normalizeMode(val.mode);
|
|
1026
|
+
const effectiveMode = innerMode != null ? innerMode : outerMode;
|
|
1027
|
+
return buildNotComposite(
|
|
1028
|
+
expr,
|
|
1029
|
+
val,
|
|
1030
|
+
params,
|
|
1031
|
+
dialect,
|
|
1032
|
+
(e, subOp, subVal, p, d) => buildScalarOperator(e, subOp, subVal, p, effectiveMode, fieldType, d),
|
|
1033
|
+
` ${SQL_TEMPLATES.AND} `
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
function buildDynamicLikePattern(op, placeholder, dialect) {
|
|
1037
|
+
if (dialect === "postgres") {
|
|
1038
|
+
switch (op) {
|
|
1039
|
+
case Ops.CONTAINS:
|
|
1040
|
+
return `('%' || ${placeholder} || '%')`;
|
|
1041
|
+
case Ops.STARTS_WITH:
|
|
1042
|
+
return `(${placeholder} || '%')`;
|
|
1043
|
+
case Ops.ENDS_WITH:
|
|
1044
|
+
return `('%' || ${placeholder})`;
|
|
1045
|
+
default:
|
|
1046
|
+
return placeholder;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
switch (op) {
|
|
1050
|
+
case Ops.CONTAINS:
|
|
1051
|
+
return `('%' || ${placeholder} || '%')`;
|
|
1052
|
+
case Ops.STARTS_WITH:
|
|
1053
|
+
return `(${placeholder} || '%')`;
|
|
1054
|
+
case Ops.ENDS_WITH:
|
|
1055
|
+
return `('%' || ${placeholder})`;
|
|
1056
|
+
default:
|
|
1057
|
+
return placeholder;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function handleLikeOperator(expr, op, val, params, mode, dialect) {
|
|
1061
|
+
if (val === void 0) return "";
|
|
1062
|
+
if (schemaParser.isDynamicParameter(val)) {
|
|
1063
|
+
const placeholder2 = params.addAuto(val);
|
|
1064
|
+
const patternExpr = buildDynamicLikePattern(op, placeholder2, dialect);
|
|
1065
|
+
if (mode === Modes.INSENSITIVE) {
|
|
1066
|
+
return caseInsensitiveLike(expr, patternExpr, dialect);
|
|
1067
|
+
}
|
|
1068
|
+
return `${expr} ${SQL_TEMPLATES.LIKE} ${patternExpr}`;
|
|
1069
|
+
}
|
|
1070
|
+
const placeholder = params.add(Wildcards[op](String(val)));
|
|
1071
|
+
if (mode === Modes.INSENSITIVE) {
|
|
1072
|
+
return caseInsensitiveLike(expr, placeholder, dialect);
|
|
1073
|
+
}
|
|
1074
|
+
return `${expr} ${SQL_TEMPLATES.LIKE} ${placeholder}`;
|
|
1075
|
+
}
|
|
1076
|
+
function handleInOperator(expr, op, val, params, dialect) {
|
|
1077
|
+
if (val === void 0) return "";
|
|
1078
|
+
if (schemaParser.isDynamicParameter(val)) {
|
|
1079
|
+
const placeholder2 = params.addAuto(val);
|
|
1080
|
+
return op === Ops.IN ? inArray(expr, placeholder2, dialect) : notInArray(expr, placeholder2, dialect);
|
|
1081
|
+
}
|
|
1082
|
+
if (!Array.isArray(val)) {
|
|
1083
|
+
throw createError(`IN operators require array value`, {
|
|
1084
|
+
operator: op,
|
|
1085
|
+
value: val
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (val.length === 0) {
|
|
1089
|
+
return op === Ops.IN ? "0=1" : "1=1";
|
|
1090
|
+
}
|
|
1091
|
+
const paramValue = prepareArrayParam(val, dialect);
|
|
1092
|
+
const placeholder = params.add(paramValue);
|
|
1093
|
+
return op === Ops.IN ? inArray(expr, placeholder, dialect) : notInArray(expr, placeholder, dialect);
|
|
1094
|
+
}
|
|
1095
|
+
function handleComparisonOperator(expr, op, val, params) {
|
|
1096
|
+
if (val === void 0) return "";
|
|
1097
|
+
const COMPARISON_OPS2 = {
|
|
1098
|
+
[Ops.EQUALS]: "=",
|
|
1099
|
+
[Ops.GT]: ">",
|
|
1100
|
+
[Ops.GTE]: ">=",
|
|
1101
|
+
[Ops.LT]: "<",
|
|
1102
|
+
[Ops.LTE]: "<="
|
|
1103
|
+
};
|
|
1104
|
+
const sqlOp = COMPARISON_OPS2[op];
|
|
1105
|
+
if (!sqlOp) {
|
|
1106
|
+
throw createError(`Unsupported scalar operator: ${op}`, { operator: op });
|
|
1107
|
+
}
|
|
1108
|
+
const placeholder = params.addAuto(val);
|
|
1109
|
+
return `${expr} ${sqlOp} ${placeholder}`;
|
|
1110
|
+
}
|
|
1111
|
+
function buildArrayParam(val, params, dialect) {
|
|
1112
|
+
if (schemaParser.isDynamicParameter(val)) {
|
|
1113
|
+
return params.addAuto(val);
|
|
1114
|
+
}
|
|
1115
|
+
if (!Array.isArray(val)) {
|
|
1116
|
+
throw createError(`Array operation requires array value`, { value: val });
|
|
1117
|
+
}
|
|
1118
|
+
const paramValue = prepareArrayParam(val, dialect);
|
|
1119
|
+
return params.add(paramValue);
|
|
1120
|
+
}
|
|
1121
|
+
function buildArrayOperator(expr, op, val, params, fieldType, dialect) {
|
|
1122
|
+
if (val === void 0) return "";
|
|
1123
|
+
if (val === null) {
|
|
1124
|
+
if (op === Ops.EQUALS) return `${expr} ${SQL_TEMPLATES.IS_NULL}`;
|
|
1125
|
+
if (op === Ops.NOT) return `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
|
|
1126
|
+
}
|
|
1127
|
+
const cast = getArrayType(fieldType, dialect);
|
|
1128
|
+
if (op === Ops.EQUALS) {
|
|
1129
|
+
return handleArrayEquals(expr, val, params, cast, dialect);
|
|
1130
|
+
}
|
|
1131
|
+
if (op === Ops.NOT) {
|
|
1132
|
+
return handleArrayNot(expr, val, params, cast, dialect);
|
|
1133
|
+
}
|
|
1134
|
+
switch (op) {
|
|
1135
|
+
case Ops.HAS:
|
|
1136
|
+
return handleArrayHas(expr, val, params, cast, dialect);
|
|
1137
|
+
case Ops.HAS_SOME:
|
|
1138
|
+
return handleArrayHasSome(expr, val, params, cast, dialect);
|
|
1139
|
+
case Ops.HAS_EVERY:
|
|
1140
|
+
return handleArrayHasEvery(expr, val, params, cast, dialect);
|
|
1141
|
+
case Ops.IS_EMPTY:
|
|
1142
|
+
return handleArrayIsEmpty(expr, val, dialect);
|
|
1143
|
+
default:
|
|
1144
|
+
throw createError(`Unknown array operator: ${op}`, { operator: op });
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function handleArrayEquals(expr, val, params, cast, dialect) {
|
|
1148
|
+
if (val === void 0) return "";
|
|
1149
|
+
if (isEmptyArray(val)) {
|
|
1150
|
+
return arrayIsEmpty(expr, dialect);
|
|
1151
|
+
}
|
|
1152
|
+
const placeholder = buildArrayParam(val, params, dialect);
|
|
1153
|
+
return arrayEquals(expr, placeholder, cast, dialect);
|
|
1154
|
+
}
|
|
1155
|
+
function handleArrayNot(expr, val, params, cast, dialect) {
|
|
1156
|
+
if (val === void 0) return "";
|
|
1157
|
+
let target = val;
|
|
1158
|
+
if (isPlainObject(val)) {
|
|
1159
|
+
const entries = Object.entries(val).filter(([, v]) => v !== void 0);
|
|
1160
|
+
if (entries.length === 1 && entries[0][0] === Ops.EQUALS) {
|
|
1161
|
+
target = entries[0][1];
|
|
1162
|
+
} else {
|
|
1163
|
+
throw createError(`Array NOT only supports { equals: ... } shape`, {
|
|
1164
|
+
operator: Ops.NOT,
|
|
1165
|
+
value: val
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (target === null) {
|
|
1170
|
+
return `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
|
|
1171
|
+
}
|
|
1172
|
+
if (isEmptyArray(target)) {
|
|
1173
|
+
return arrayIsNotEmpty(expr, dialect);
|
|
1174
|
+
}
|
|
1175
|
+
const placeholder = buildArrayParam(target, params, dialect);
|
|
1176
|
+
return `${SQL_TEMPLATES.NOT} (${arrayEquals(expr, placeholder, cast, dialect)})`;
|
|
1177
|
+
}
|
|
1178
|
+
function handleArrayHas(expr, val, params, cast, dialect) {
|
|
1179
|
+
if (val === void 0) return "";
|
|
1180
|
+
if (val === null) {
|
|
1181
|
+
throw createError(`has requires scalar value`, {
|
|
1182
|
+
operator: Ops.HAS,
|
|
1183
|
+
value: val
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
if (!schemaParser.isDynamicParameter(val) && Array.isArray(val)) {
|
|
1187
|
+
throw createError(`has requires scalar value (single element), not array`, {
|
|
1188
|
+
operator: Ops.HAS,
|
|
1189
|
+
value: val
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
if (isPlainObject(val)) {
|
|
1193
|
+
throw createError(`has requires scalar value`, {
|
|
1194
|
+
operator: Ops.HAS,
|
|
1195
|
+
value: val
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
const placeholder = params.addAuto(val);
|
|
1199
|
+
return arrayContains(expr, placeholder, cast, dialect);
|
|
1200
|
+
}
|
|
1201
|
+
function handleArrayHasSome(expr, val, params, cast, dialect) {
|
|
1202
|
+
if (val === void 0) return "";
|
|
1203
|
+
if (schemaParser.isDynamicParameter(val)) {
|
|
1204
|
+
const placeholder2 = params.addAuto(val);
|
|
1205
|
+
return arrayOverlaps(expr, placeholder2, cast, dialect);
|
|
1206
|
+
}
|
|
1207
|
+
if (!Array.isArray(val)) {
|
|
1208
|
+
throw createError(`hasSome requires array value`, {
|
|
1209
|
+
operator: Ops.HAS_SOME,
|
|
1210
|
+
value: val
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
if (val.length > LIMITS.MAX_ARRAY_SIZE) {
|
|
1214
|
+
throw createError(
|
|
1215
|
+
`Array too large (${val.length} elements, max ${LIMITS.MAX_ARRAY_SIZE})`,
|
|
1216
|
+
{ operator: Ops.HAS_SOME, value: `[${val.length} items]` }
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
if (val.length === 0) return "0=1";
|
|
1220
|
+
const paramValue = prepareArrayParam(val, dialect);
|
|
1221
|
+
const placeholder = params.add(paramValue);
|
|
1222
|
+
return arrayOverlaps(expr, placeholder, cast, dialect);
|
|
1223
|
+
}
|
|
1224
|
+
function handleArrayHasEvery(expr, val, params, cast, dialect) {
|
|
1225
|
+
if (val === void 0) return "";
|
|
1226
|
+
const placeholder = buildArrayParam(val, params, dialect);
|
|
1227
|
+
return arrayContainsAll(expr, placeholder, cast, dialect);
|
|
1228
|
+
}
|
|
1229
|
+
function handleArrayIsEmpty(expr, val, dialect) {
|
|
1230
|
+
if (typeof val !== "boolean") {
|
|
1231
|
+
throw createError(`isEmpty requires boolean value`, {
|
|
1232
|
+
operator: Ops.IS_EMPTY,
|
|
1233
|
+
value: val
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
return val === true ? arrayIsEmpty(expr, dialect) : arrayIsNotEmpty(expr, dialect);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/builder/where/operators-json.ts
|
|
1240
|
+
var SAFE_JSON_PATH_SEGMENT = /^[a-zA-Z_]\w*$/;
|
|
1241
|
+
function validateJsonPathSegments(segments) {
|
|
1242
|
+
for (const segment of segments) {
|
|
1243
|
+
if (typeof segment !== "string") {
|
|
1244
|
+
throw createError("JSON path segments must be strings", {
|
|
1245
|
+
operator: Ops.PATH,
|
|
1246
|
+
value: segment
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
if (!SAFE_JSON_PATH_SEGMENT.test(segment)) {
|
|
1250
|
+
throw createError(
|
|
1251
|
+
`Invalid JSON path segment: '${segment}'. Must be alphanumeric with underscores, starting with letter or underscore.`,
|
|
1252
|
+
{ operator: Ops.PATH, value: segment }
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function buildJsonOperator(expr, op, val, params, dialect) {
|
|
1258
|
+
if (val === void 0) return "";
|
|
1259
|
+
if (op === Ops.PATH && isPlainObject(val) && "path" in val) {
|
|
1260
|
+
return handleJsonPath(expr, val, params, dialect);
|
|
1261
|
+
}
|
|
1262
|
+
const jsonWildcards = {
|
|
1263
|
+
[Ops.STRING_CONTAINS]: (v) => `%${v}%`,
|
|
1264
|
+
[Ops.STRING_STARTS_WITH]: (v) => `${v}%`,
|
|
1265
|
+
[Ops.STRING_ENDS_WITH]: (v) => `%${v}`
|
|
1266
|
+
};
|
|
1267
|
+
if (op in jsonWildcards) {
|
|
1268
|
+
return handleJsonWildcard(expr, op, val, params, jsonWildcards, dialect);
|
|
1269
|
+
}
|
|
1270
|
+
throw createError(`Unsupported JSON operator: ${op}`, { operator: op });
|
|
1271
|
+
}
|
|
1272
|
+
function handleJsonPath(expr, val, params, dialect) {
|
|
1273
|
+
const v = val;
|
|
1274
|
+
if (!Array.isArray(v.path)) {
|
|
1275
|
+
throw createError("JSON path must be an array", { operator: Ops.PATH });
|
|
1276
|
+
}
|
|
1277
|
+
if (v.path.length === 0) {
|
|
1278
|
+
throw createError("JSON path cannot be empty", { operator: Ops.PATH });
|
|
1279
|
+
}
|
|
1280
|
+
validateJsonPathSegments(v.path);
|
|
1281
|
+
const pathExpr = dialect === "sqlite" ? params.add(`$.${v.path.join(".")}`) : params.add(v.path);
|
|
1282
|
+
const rawOps = [
|
|
1283
|
+
["=", v.equals],
|
|
1284
|
+
[">", v.gt],
|
|
1285
|
+
[">=", v.gte],
|
|
1286
|
+
["<", v.lt],
|
|
1287
|
+
["<=", v.lte]
|
|
1288
|
+
];
|
|
1289
|
+
const ops = rawOps.filter(
|
|
1290
|
+
([, value]) => value !== void 0
|
|
1291
|
+
);
|
|
1292
|
+
if (ops.length === 0) {
|
|
1293
|
+
throw createError("JSON path query missing comparison operator", {
|
|
1294
|
+
operator: Ops.PATH
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
const parts = [];
|
|
1298
|
+
for (const [sqlOp, value] of ops) {
|
|
1299
|
+
if (value === null) {
|
|
1300
|
+
const base2 = jsonExtractText(expr, pathExpr, dialect);
|
|
1301
|
+
parts.push(`${base2} ${SQL_TEMPLATES.IS_NULL}`);
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
const valPh = params.add(value);
|
|
1305
|
+
const base = typeof value === "number" ? jsonExtractNumeric(expr, pathExpr, dialect) : jsonExtractText(expr, pathExpr, dialect);
|
|
1306
|
+
parts.push(`${base} ${sqlOp} ${valPh}`);
|
|
1307
|
+
}
|
|
1308
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
1309
|
+
}
|
|
1310
|
+
function handleJsonWildcard(expr, op, val, params, wildcards, dialect) {
|
|
1311
|
+
if (!isNotNullish(val)) {
|
|
1312
|
+
throw createError(`JSON string operator requires non-null value`, {
|
|
1313
|
+
operator: op,
|
|
1314
|
+
value: val
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
if (isPlainObject(val) || Array.isArray(val)) {
|
|
1318
|
+
throw createError(`JSON string operator requires scalar value`, {
|
|
1319
|
+
operator: op,
|
|
1320
|
+
value: val
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
const strVal = String(val);
|
|
1324
|
+
if (strVal.length > LIMITS.MAX_STRING_LENGTH) {
|
|
1325
|
+
throw createError(
|
|
1326
|
+
`String too long (${strVal.length} chars, max ${LIMITS.MAX_STRING_LENGTH})`,
|
|
1327
|
+
{ operator: op }
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
const placeholder = params.add(wildcards[op](strVal));
|
|
1331
|
+
const jsonText = jsonToText(expr, dialect);
|
|
1332
|
+
return caseInsensitiveLike(jsonText, placeholder, dialect);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/builder/where/relations.ts
|
|
1336
|
+
var NO_JOINS = Object.freeze([]);
|
|
1337
|
+
function isListRelation(fieldType) {
|
|
1338
|
+
return typeof fieldType === "string" && fieldType.endsWith("[]");
|
|
1339
|
+
}
|
|
1340
|
+
function buildToOneNullCheck(field, parentAlias, relTable, relAlias, join2, wantNull) {
|
|
1341
|
+
const isLocal = field.isForeignKeyLocal === true;
|
|
1342
|
+
const fkFields = normalizeKeyList(field.foreignKey);
|
|
1343
|
+
if (isLocal) {
|
|
1344
|
+
if (fkFields.length === 0) {
|
|
1345
|
+
throw createError(`Relation '${field.name}' is missing foreignKey`, {
|
|
1346
|
+
field: field.name
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
const parts = fkFields.map((fk) => {
|
|
1350
|
+
const safe = fk.replace(/"/g, '""');
|
|
1351
|
+
const expr = `${parentAlias}."${safe}"`;
|
|
1352
|
+
return wantNull ? `${expr} ${SQL_TEMPLATES.IS_NULL}` : `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
|
|
1353
|
+
});
|
|
1354
|
+
if (parts.length === 1) return parts[0];
|
|
1355
|
+
return wantNull ? `(${parts.join(" OR ")})` : `(${parts.join(" AND ")})`;
|
|
1356
|
+
}
|
|
1357
|
+
const existsSql = `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${relTable} ${relAlias} ${SQL_TEMPLATES.WHERE} ${join2})`;
|
|
1358
|
+
return wantNull ? `${SQL_TEMPLATES.NOT} ${existsSql}` : existsSql;
|
|
1359
|
+
}
|
|
1360
|
+
function buildToOneExistsMatch(relTable, relAlias, join2, sub) {
|
|
1361
|
+
const joins = sub.joins.length > 0 ? ` ${sub.joins.join(" ")}` : "";
|
|
1362
|
+
return `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${relTable} ${relAlias}${joins} ${SQL_TEMPLATES.WHERE} ${join2} ${SQL_TEMPLATES.AND} ${sub.clause})`;
|
|
1363
|
+
}
|
|
1364
|
+
function buildToOneNotExistsMatch(relTable, relAlias, join2, sub) {
|
|
1365
|
+
const joins = sub.joins.length > 0 ? ` ${sub.joins.join(" ")}` : "";
|
|
1366
|
+
return `${SQL_TEMPLATES.NOT} EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${relTable} ${relAlias}${joins} ${SQL_TEMPLATES.WHERE} ${join2} ${SQL_TEMPLATES.AND} ${sub.clause})`;
|
|
1367
|
+
}
|
|
1368
|
+
function buildListRelationFilters(args) {
|
|
1369
|
+
const {
|
|
1370
|
+
fieldName,
|
|
1371
|
+
value,
|
|
1372
|
+
ctx,
|
|
1373
|
+
whereBuilder,
|
|
1374
|
+
relModel,
|
|
1375
|
+
relTable,
|
|
1376
|
+
relAlias,
|
|
1377
|
+
join: join2
|
|
1378
|
+
} = args;
|
|
1379
|
+
const noneValue = value[RelationFilters.NONE];
|
|
1380
|
+
if (noneValue !== void 0 && noneValue !== null) {
|
|
1381
|
+
const sub = whereBuilder.build(noneValue, __spreadProps(__spreadValues({}, ctx), {
|
|
1382
|
+
alias: relAlias,
|
|
1383
|
+
model: relModel,
|
|
1384
|
+
path: [...ctx.path, fieldName, RelationFilters.NONE],
|
|
1385
|
+
isSubquery: true,
|
|
1386
|
+
depth: ctx.depth + 1
|
|
1387
|
+
}));
|
|
1388
|
+
const isEmptyFilter = isPlainObject(noneValue) && Object.keys(noneValue).length === 0;
|
|
1389
|
+
const canOptimize = !ctx.isSubquery && isEmptyFilter && sub.clause === DEFAULT_WHERE_CLAUSE && sub.joins.length === 0;
|
|
1390
|
+
if (canOptimize) {
|
|
1391
|
+
const checkField = relModel.fields.find(
|
|
1392
|
+
(f) => !f.isRelation && f.isRequired && f.name !== "id"
|
|
1393
|
+
) || relModel.fields.find((f) => !f.isRelation && f.name === "id");
|
|
1394
|
+
if (checkField) {
|
|
1395
|
+
const leftJoinSql = `LEFT JOIN ${relTable} ${relAlias} ON ${join2}`;
|
|
1396
|
+
const whereClause = `${relAlias}.${quote(checkField.name)} IS NULL`;
|
|
1397
|
+
return Object.freeze({
|
|
1398
|
+
clause: whereClause,
|
|
1399
|
+
joins: [leftJoinSql]
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const filters = [
|
|
1405
|
+
{
|
|
1406
|
+
key: RelationFilters.SOME,
|
|
1407
|
+
wrap: (c, j) => `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${relTable} ${relAlias}${j} ${SQL_TEMPLATES.WHERE} ${join2} ${SQL_TEMPLATES.AND} ${c})`
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
key: RelationFilters.EVERY,
|
|
1411
|
+
wrap: (c, j) => `${SQL_TEMPLATES.NOT} EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${relTable} ${relAlias}${j} ${SQL_TEMPLATES.WHERE} ${join2} ${SQL_TEMPLATES.AND} ${SQL_TEMPLATES.NOT} (${c}))`
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
key: RelationFilters.NONE,
|
|
1415
|
+
wrap: (c, j) => {
|
|
1416
|
+
const condition = c === DEFAULT_WHERE_CLAUSE ? "" : ` ${SQL_TEMPLATES.AND} ${c}`;
|
|
1417
|
+
return `${SQL_TEMPLATES.NOT} EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${relTable} ${relAlias}${j} ${SQL_TEMPLATES.WHERE} ${join2}${condition})`;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
];
|
|
1421
|
+
const clauses = [];
|
|
1422
|
+
for (const { key, wrap } of filters) {
|
|
1423
|
+
const raw = value[key];
|
|
1424
|
+
if (raw === void 0 || raw === null) continue;
|
|
1425
|
+
const sub = whereBuilder.build(raw, __spreadProps(__spreadValues({}, ctx), {
|
|
1426
|
+
alias: relAlias,
|
|
1427
|
+
model: relModel,
|
|
1428
|
+
path: [...ctx.path, fieldName, key],
|
|
1429
|
+
isSubquery: true,
|
|
1430
|
+
depth: ctx.depth + 1
|
|
1431
|
+
}));
|
|
1432
|
+
const j = sub.joins.length > 0 ? ` ${sub.joins.join(" ")}` : "";
|
|
1433
|
+
clauses.push(wrap(sub.clause, j));
|
|
1434
|
+
}
|
|
1435
|
+
if (clauses.length === 0) {
|
|
1436
|
+
throw createError(
|
|
1437
|
+
`List relation '${fieldName}' requires one of { some, every, none }`,
|
|
1438
|
+
{ field: fieldName, path: ctx.path, modelName: ctx.model.name }
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
return Object.freeze({
|
|
1442
|
+
clause: clauses.join(SQL_SEPARATORS.CONDITION_AND),
|
|
1443
|
+
joins: NO_JOINS
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
function buildToOneRelationFilters(args) {
|
|
1447
|
+
const {
|
|
1448
|
+
fieldName,
|
|
1449
|
+
value,
|
|
1450
|
+
ctx,
|
|
1451
|
+
whereBuilder,
|
|
1452
|
+
field,
|
|
1453
|
+
relModel,
|
|
1454
|
+
relTable,
|
|
1455
|
+
relAlias,
|
|
1456
|
+
join: join2
|
|
1457
|
+
} = args;
|
|
1458
|
+
const hasSomeEveryNone = isNotNullish(value[RelationFilters.SOME]) || isNotNullish(value[RelationFilters.EVERY]) || isNotNullish(value[RelationFilters.NONE]);
|
|
1459
|
+
if (hasSomeEveryNone) {
|
|
1460
|
+
throw createError(
|
|
1461
|
+
`To-one relation '${fieldName}' does not support { some, every, none }; use { is, isNot }`,
|
|
1462
|
+
{ field: fieldName, path: ctx.path, modelName: ctx.model.name }
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
const hasIs = Object.prototype.hasOwnProperty.call(value, "is");
|
|
1466
|
+
const hasIsNot = Object.prototype.hasOwnProperty.call(value, "isNot");
|
|
1467
|
+
let filterKey;
|
|
1468
|
+
let filterVal;
|
|
1469
|
+
if (hasIs) {
|
|
1470
|
+
filterKey = "is";
|
|
1471
|
+
filterVal = value.is;
|
|
1472
|
+
} else if (hasIsNot) {
|
|
1473
|
+
filterKey = "isNot";
|
|
1474
|
+
filterVal = value.isNot;
|
|
1475
|
+
} else {
|
|
1476
|
+
filterKey = "is";
|
|
1477
|
+
filterVal = value;
|
|
1478
|
+
}
|
|
1479
|
+
if (filterVal === void 0) {
|
|
1480
|
+
return Object.freeze({
|
|
1481
|
+
clause: DEFAULT_WHERE_CLAUSE,
|
|
1482
|
+
joins: NO_JOINS
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
if (filterVal === null) {
|
|
1486
|
+
const wantNull = filterKey === "is";
|
|
1487
|
+
const clause2 = buildToOneNullCheck(
|
|
1488
|
+
field,
|
|
1489
|
+
ctx.alias,
|
|
1490
|
+
relTable,
|
|
1491
|
+
relAlias,
|
|
1492
|
+
join2,
|
|
1493
|
+
wantNull
|
|
1494
|
+
);
|
|
1495
|
+
return Object.freeze({
|
|
1496
|
+
clause: clause2,
|
|
1497
|
+
joins: NO_JOINS
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
if (!isPlainObject(filterVal)) {
|
|
1501
|
+
throw createError(
|
|
1502
|
+
`Relation '${fieldName}' filter must be an object or null`,
|
|
1503
|
+
{
|
|
1504
|
+
field: fieldName,
|
|
1505
|
+
path: ctx.path,
|
|
1506
|
+
modelName: ctx.model.name,
|
|
1507
|
+
value: filterVal
|
|
1508
|
+
}
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
const sub = whereBuilder.build(filterVal, __spreadProps(__spreadValues({}, ctx), {
|
|
1512
|
+
alias: relAlias,
|
|
1513
|
+
model: relModel,
|
|
1514
|
+
path: [...ctx.path, fieldName, filterKey],
|
|
1515
|
+
isSubquery: true,
|
|
1516
|
+
depth: ctx.depth + 1
|
|
1517
|
+
}));
|
|
1518
|
+
const clause = filterKey === "is" ? buildToOneExistsMatch(relTable, relAlias, join2, sub) : buildToOneNotExistsMatch(relTable, relAlias, join2, sub);
|
|
1519
|
+
return Object.freeze({
|
|
1520
|
+
clause,
|
|
1521
|
+
joins: NO_JOINS
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
function ensureRelationFilterObject(fieldName, value, ctx) {
|
|
1525
|
+
if (!isPlainObject(value)) {
|
|
1526
|
+
throw createError(`Relation filter '${fieldName}' must be an object`, {
|
|
1527
|
+
path: [...ctx.path, fieldName],
|
|
1528
|
+
field: fieldName,
|
|
1529
|
+
modelName: ctx.model.name,
|
|
1530
|
+
value
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function buildRelation(fieldName, value, ctx, whereBuilder) {
|
|
1535
|
+
const field = ctx.model.fields.find((f) => f.name === fieldName);
|
|
1536
|
+
if (!isValidRelationField(field)) {
|
|
1537
|
+
throw createError(`Invalid relation '${fieldName}'`, {
|
|
1538
|
+
field: fieldName,
|
|
1539
|
+
path: ctx.path,
|
|
1540
|
+
modelName: ctx.model.name
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
const relModel = ctx.schemaModels.find((m) => m.name === field.relatedModel);
|
|
1544
|
+
if (!isNotNullish(relModel)) {
|
|
1545
|
+
throw createError(
|
|
1546
|
+
`Related model '${field.relatedModel}' not found in schema. Available models: ${ctx.schemaModels.map((m) => m.name).join(", ")}`,
|
|
1547
|
+
{
|
|
1548
|
+
field: fieldName,
|
|
1549
|
+
path: ctx.path,
|
|
1550
|
+
modelName: ctx.model.name
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
const relTable = buildTableReference(
|
|
1555
|
+
SQL_TEMPLATES.PUBLIC_SCHEMA,
|
|
1556
|
+
relModel.tableName,
|
|
1557
|
+
ctx.dialect
|
|
1558
|
+
);
|
|
1559
|
+
const relAlias = ctx.aliasGen.next(fieldName);
|
|
1560
|
+
const join2 = joinCondition(field, ctx.alias, relAlias);
|
|
1561
|
+
const args = {
|
|
1562
|
+
fieldName,
|
|
1563
|
+
value,
|
|
1564
|
+
ctx,
|
|
1565
|
+
whereBuilder,
|
|
1566
|
+
field,
|
|
1567
|
+
relModel,
|
|
1568
|
+
relTable,
|
|
1569
|
+
relAlias,
|
|
1570
|
+
join: join2
|
|
1571
|
+
};
|
|
1572
|
+
if (isListRelation(field.type)) return buildListRelationFilters(args);
|
|
1573
|
+
return buildToOneRelationFilters(args);
|
|
1574
|
+
}
|
|
1575
|
+
function buildTopLevelRelation(fieldName, value, ctx, whereBuilder) {
|
|
1576
|
+
ensureRelationFilterObject(fieldName, value, ctx);
|
|
1577
|
+
return buildRelation(fieldName, value, ctx, whereBuilder);
|
|
1578
|
+
}
|
|
1579
|
+
function buildNestedRelation(fieldName, value, ctx, whereBuilder) {
|
|
1580
|
+
return buildTopLevelRelation(fieldName, value, ctx, whereBuilder);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/builder/shared/validators/field-validators.ts
|
|
1584
|
+
function assertFieldExists(name, model, path) {
|
|
1585
|
+
const field = model.fields.find((f) => f.name === name);
|
|
1586
|
+
if (!isNotNullish(field)) {
|
|
1587
|
+
throw createError(`Field '${name}' does not exist on '${model.name}'`, {
|
|
1588
|
+
field: name,
|
|
1589
|
+
path,
|
|
1590
|
+
modelName: model.name,
|
|
1591
|
+
availableFields: model.fields.map((f) => f.name)
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
return field;
|
|
1595
|
+
}
|
|
1596
|
+
function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
1597
|
+
if (!isNotNullish(fieldType)) return;
|
|
1598
|
+
const ARRAY_OPS = /* @__PURE__ */ new Set([
|
|
1599
|
+
Ops.HAS,
|
|
1600
|
+
Ops.HAS_SOME,
|
|
1601
|
+
Ops.HAS_EVERY,
|
|
1602
|
+
Ops.IS_EMPTY
|
|
1603
|
+
]);
|
|
1604
|
+
const JSON_OPS = /* @__PURE__ */ new Set([
|
|
1605
|
+
Ops.PATH,
|
|
1606
|
+
Ops.STRING_CONTAINS,
|
|
1607
|
+
Ops.STRING_STARTS_WITH,
|
|
1608
|
+
Ops.STRING_ENDS_WITH
|
|
1609
|
+
]);
|
|
1610
|
+
const isArrayOp = ARRAY_OPS.has(op);
|
|
1611
|
+
const isFieldArray = isArrayType(fieldType);
|
|
1612
|
+
const arrayOpMismatch = isArrayOp && !isFieldArray;
|
|
1613
|
+
if (arrayOpMismatch) {
|
|
1614
|
+
throw createError(`'${op}' requires array field, got '${fieldType}'`, {
|
|
1615
|
+
operator: op,
|
|
1616
|
+
field: fieldName,
|
|
1617
|
+
path,
|
|
1618
|
+
modelName
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
const isJsonOp = JSON_OPS.has(op);
|
|
1622
|
+
const isFieldJson = isJsonType(fieldType);
|
|
1623
|
+
const jsonOpMismatch = isJsonOp && !isFieldJson;
|
|
1624
|
+
if (jsonOpMismatch) {
|
|
1625
|
+
throw createError(`'${op}' requires JSON field, got '${fieldType}'`, {
|
|
1626
|
+
operator: op,
|
|
1627
|
+
field: fieldName,
|
|
1628
|
+
path,
|
|
1629
|
+
modelName
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/builder/where/builder.ts
|
|
1635
|
+
var WhereBuilder = class {
|
|
1636
|
+
build(where, ctx) {
|
|
1637
|
+
if (!isPlainObject(where)) {
|
|
1638
|
+
throw createError("where must be an object", {
|
|
1639
|
+
path: ctx.path,
|
|
1640
|
+
modelName: ctx.model.name
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
return buildWhereInternal(where, ctx, this);
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
var MAX_QUERY_DEPTH = 50;
|
|
1647
|
+
var EMPTY_JOINS = Object.freeze([]);
|
|
1648
|
+
var whereBuilderInstance = new WhereBuilder();
|
|
1649
|
+
function freezeResult(clause, joins = EMPTY_JOINS) {
|
|
1650
|
+
return Object.freeze({ clause, joins });
|
|
1651
|
+
}
|
|
1652
|
+
function dedupePreserveOrder(items) {
|
|
1653
|
+
if (items.length <= 1) return Object.freeze([...items]);
|
|
1654
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1655
|
+
const out = [];
|
|
1656
|
+
for (const s of items) {
|
|
1657
|
+
if (!seen.has(s)) {
|
|
1658
|
+
seen.add(s);
|
|
1659
|
+
out.push(s);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return Object.freeze(out);
|
|
1663
|
+
}
|
|
1664
|
+
function appendResult(result, clauses, allJoins) {
|
|
1665
|
+
if (isValidWhereClause(result.clause)) clauses.push(result.clause);
|
|
1666
|
+
if (isNonEmptyArray(result.joins)) allJoins.push(...result.joins);
|
|
1667
|
+
}
|
|
1668
|
+
function asLogicalOperator(key) {
|
|
1669
|
+
if (key === LogicalOps.AND) return "AND";
|
|
1670
|
+
if (key === LogicalOps.OR) return "OR";
|
|
1671
|
+
if (key === LogicalOps.NOT) return "NOT";
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
function nextContext(ctx) {
|
|
1675
|
+
return __spreadProps(__spreadValues({}, ctx), { depth: ctx.depth + 1 });
|
|
1676
|
+
}
|
|
1677
|
+
function buildRelationFilter(fieldName, value, ctx, builder) {
|
|
1678
|
+
const ctx2 = nextContext(ctx);
|
|
1679
|
+
if (ctx.isSubquery) {
|
|
1680
|
+
return buildNestedRelation(fieldName, value, ctx2, builder);
|
|
1681
|
+
}
|
|
1682
|
+
return buildTopLevelRelation(fieldName, value, ctx2, builder);
|
|
1683
|
+
}
|
|
1684
|
+
function buildWhereEntry(key, value, ctx, builder) {
|
|
1685
|
+
const op = asLogicalOperator(key);
|
|
1686
|
+
if (op) return buildLogical(op, value, ctx, builder);
|
|
1687
|
+
if (isRelationField(key, ctx.model)) {
|
|
1688
|
+
if (!isPlainObject(value)) {
|
|
1689
|
+
throw createError(`Relation filter '${key}' must be an object`, {
|
|
1690
|
+
path: [...ctx.path, key],
|
|
1691
|
+
field: key,
|
|
1692
|
+
modelName: ctx.model.name,
|
|
1693
|
+
value
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
return buildRelationFilter(key, value, ctx, builder);
|
|
1697
|
+
}
|
|
1698
|
+
return buildScalarField(key, value, ctx);
|
|
1699
|
+
}
|
|
1700
|
+
function buildWhereInternal(where, ctx, builder) {
|
|
1701
|
+
if (ctx.depth > MAX_QUERY_DEPTH) {
|
|
1702
|
+
throw createError(
|
|
1703
|
+
`Query nesting too deep (max ${MAX_QUERY_DEPTH} levels). This usually indicates a circular reference.`,
|
|
1704
|
+
{ path: ctx.path, modelName: ctx.model.name }
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
if (isEmptyWhere(where)) {
|
|
1708
|
+
return freezeResult(DEFAULT_WHERE_CLAUSE, EMPTY_JOINS);
|
|
1709
|
+
}
|
|
1710
|
+
const allJoins = [];
|
|
1711
|
+
const clauses = [];
|
|
1712
|
+
for (const [key, value] of Object.entries(where)) {
|
|
1713
|
+
if (value === void 0) continue;
|
|
1714
|
+
const result = buildWhereEntry(key, value, ctx, builder);
|
|
1715
|
+
appendResult(result, clauses, allJoins);
|
|
1716
|
+
}
|
|
1717
|
+
const finalClause = clauses.length > 0 ? clauses.join(SQL_SEPARATORS.CONDITION_AND) : DEFAULT_WHERE_CLAUSE;
|
|
1718
|
+
return freezeResult(finalClause, dedupePreserveOrder(allJoins));
|
|
1719
|
+
}
|
|
1720
|
+
function normalizeLogicalValue(operator, value, ctx) {
|
|
1721
|
+
if (Array.isArray(value)) {
|
|
1722
|
+
const out = [];
|
|
1723
|
+
for (let i = 0; i < value.length; i++) {
|
|
1724
|
+
const v = value[i];
|
|
1725
|
+
if (v === void 0) continue;
|
|
1726
|
+
if (!isPlainObject(v)) {
|
|
1727
|
+
throw createError(`${operator} entries must be objects`, {
|
|
1728
|
+
path: [...ctx.path, operator, String(i)],
|
|
1729
|
+
modelName: ctx.model.name,
|
|
1730
|
+
value: v
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
out.push(v);
|
|
1734
|
+
}
|
|
1735
|
+
return out;
|
|
1736
|
+
}
|
|
1737
|
+
if (isPlainObject(value)) {
|
|
1738
|
+
return [value];
|
|
1739
|
+
}
|
|
1740
|
+
throw createError(`${operator} must be an object or array of objects`, {
|
|
1741
|
+
path: [...ctx.path, operator],
|
|
1742
|
+
modelName: ctx.model.name,
|
|
1743
|
+
value
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
function collectLogicalParts(operator, conditions, ctx, builder) {
|
|
1747
|
+
const allJoins = [];
|
|
1748
|
+
const clauses = [];
|
|
1749
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
1750
|
+
const result = builder.build(conditions[i], __spreadProps(__spreadValues({}, ctx), {
|
|
1751
|
+
path: [...ctx.path, operator, String(i)],
|
|
1752
|
+
depth: ctx.depth + 1
|
|
1753
|
+
}));
|
|
1754
|
+
if (isNonEmptyArray(result.joins)) allJoins.push(...result.joins);
|
|
1755
|
+
if (result.clause && result.clause !== DEFAULT_WHERE_CLAUSE) {
|
|
1756
|
+
clauses.push(`(${result.clause})`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
joins: dedupePreserveOrder(allJoins),
|
|
1761
|
+
clauses: Object.freeze(clauses)
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
function buildLogicalClause(operator, clauses) {
|
|
1765
|
+
if (clauses.length === 0) return DEFAULT_WHERE_CLAUSE;
|
|
1766
|
+
if (operator === "NOT") {
|
|
1767
|
+
if (clauses.length === 1) return `${SQL_TEMPLATES.NOT} ${clauses[0]}`;
|
|
1768
|
+
return `${SQL_TEMPLATES.NOT} (${clauses.join(SQL_SEPARATORS.CONDITION_AND)})`;
|
|
1769
|
+
}
|
|
1770
|
+
return clauses.join(` ${operator} `);
|
|
1771
|
+
}
|
|
1772
|
+
function buildLogical(operator, value, ctx, builder) {
|
|
1773
|
+
const conditions = normalizeLogicalValue(operator, value, ctx);
|
|
1774
|
+
if (conditions.length === 0) {
|
|
1775
|
+
const clause2 = operator === "OR" ? "0=1" : DEFAULT_WHERE_CLAUSE;
|
|
1776
|
+
return freezeResult(clause2, EMPTY_JOINS);
|
|
1777
|
+
}
|
|
1778
|
+
const { joins, clauses } = collectLogicalParts(
|
|
1779
|
+
operator,
|
|
1780
|
+
conditions,
|
|
1781
|
+
ctx,
|
|
1782
|
+
builder
|
|
1783
|
+
);
|
|
1784
|
+
const clause = buildLogicalClause(operator, clauses);
|
|
1785
|
+
return freezeResult(clause, joins);
|
|
1786
|
+
}
|
|
1787
|
+
function buildScalarField(fieldName, value, ctx) {
|
|
1788
|
+
const field = assertFieldExists(fieldName, ctx.model, ctx.path);
|
|
1789
|
+
const expr = col(ctx.alias, fieldName);
|
|
1790
|
+
if (value === null) {
|
|
1791
|
+
return freezeResult(`${expr} ${SQL_TEMPLATES.IS_NULL}`, EMPTY_JOINS);
|
|
1792
|
+
}
|
|
1793
|
+
if (isPlainObject(value)) {
|
|
1794
|
+
const mode = value.mode;
|
|
1795
|
+
const ops = Object.entries(value).filter(
|
|
1796
|
+
([k, v]) => k !== "mode" && v !== void 0
|
|
1797
|
+
);
|
|
1798
|
+
if (ops.length === 0) {
|
|
1799
|
+
return freezeResult(DEFAULT_WHERE_CLAUSE, EMPTY_JOINS);
|
|
1800
|
+
}
|
|
1801
|
+
const parts = [];
|
|
1802
|
+
for (const [op, val] of ops) {
|
|
1803
|
+
assertValidOperator(fieldName, op, field.type, ctx.path, ctx.model.name);
|
|
1804
|
+
const clause3 = buildOperator(expr, op, val, ctx, mode, field.type);
|
|
1805
|
+
if (isValidWhereClause(clause3)) parts.push(clause3);
|
|
1806
|
+
}
|
|
1807
|
+
const clause2 = parts.length > 0 ? parts.join(SQL_SEPARATORS.CONDITION_AND) : DEFAULT_WHERE_CLAUSE;
|
|
1808
|
+
return freezeResult(clause2, EMPTY_JOINS);
|
|
1809
|
+
}
|
|
1810
|
+
const clause = buildOperator(
|
|
1811
|
+
expr,
|
|
1812
|
+
Ops.EQUALS,
|
|
1813
|
+
value,
|
|
1814
|
+
ctx,
|
|
1815
|
+
void 0,
|
|
1816
|
+
field.type
|
|
1817
|
+
);
|
|
1818
|
+
return freezeResult(clause || DEFAULT_WHERE_CLAUSE, EMPTY_JOINS);
|
|
1819
|
+
}
|
|
1820
|
+
function buildOperator(expr, op, val, ctx, mode, fieldType) {
|
|
1821
|
+
if (fieldType && isArrayType(fieldType)) {
|
|
1822
|
+
return buildArrayOperator(expr, op, val, ctx.params, fieldType, ctx.dialect);
|
|
1823
|
+
}
|
|
1824
|
+
if (fieldType && isJsonType(fieldType)) {
|
|
1825
|
+
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
1826
|
+
}
|
|
1827
|
+
return buildScalarOperator(
|
|
1828
|
+
expr,
|
|
1829
|
+
op,
|
|
1830
|
+
val,
|
|
1831
|
+
ctx.params,
|
|
1832
|
+
mode,
|
|
1833
|
+
fieldType,
|
|
1834
|
+
ctx.dialect
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/builder/shared/alias-generator.ts
|
|
1839
|
+
function toSafeSqlIdentifier(input) {
|
|
1840
|
+
const raw = String(input);
|
|
1841
|
+
const cleaned = raw.replace(/\W/g, "_");
|
|
1842
|
+
const startsOk = /^[a-zA-Z_]/.test(cleaned);
|
|
1843
|
+
const base = startsOk ? cleaned : `_${cleaned}`;
|
|
1844
|
+
const fallback = base.length > 0 ? base : "_t";
|
|
1845
|
+
const lowered = fallback.toLowerCase();
|
|
1846
|
+
return SQL_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
|
|
1847
|
+
}
|
|
1848
|
+
function createAliasGenerator(maxAliases = 1e4) {
|
|
1849
|
+
let counter = 0;
|
|
1850
|
+
const usedAliases = /* @__PURE__ */ new Set();
|
|
1851
|
+
return {
|
|
1852
|
+
next(baseName) {
|
|
1853
|
+
if (usedAliases.size >= maxAliases) {
|
|
1854
|
+
throw new Error(
|
|
1855
|
+
`Alias generator exceeded maximum of ${maxAliases} aliases. This indicates a query complexity issue or potential infinite loop.`
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
const base = toSafeSqlIdentifier(baseName);
|
|
1859
|
+
const suffix = `_${counter}`;
|
|
1860
|
+
const maxLen = 63;
|
|
1861
|
+
const baseMax = Math.max(1, maxLen - suffix.length);
|
|
1862
|
+
const trimmedBase = base.length > baseMax ? base.slice(0, baseMax) : base;
|
|
1863
|
+
const alias = `${trimmedBase}${suffix}`;
|
|
1864
|
+
counter += 1;
|
|
1865
|
+
if (usedAliases.has(alias)) {
|
|
1866
|
+
throw new Error(
|
|
1867
|
+
`CRITICAL: Duplicate alias '${alias}' at counter=${counter}. This indicates a bug in alias generation logic.`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
usedAliases.add(alias);
|
|
1871
|
+
return alias;
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
var MAX_PARAM_INDEX = Number.MAX_SAFE_INTEGER - 1e3;
|
|
1876
|
+
function assertSameLength(params, mappings) {
|
|
1877
|
+
if (params.length !== mappings.length) {
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
`CRITICAL: State corruption - params=${params.length}, mappings=${mappings.length}`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function assertValidNextIndex(index) {
|
|
1884
|
+
if (!Number.isInteger(index) || index < 1) {
|
|
1885
|
+
throw new Error(`CRITICAL: Index must be integer >= 1, got ${index}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function assertNextIndexMatches(mappingsLength, nextIndex) {
|
|
1889
|
+
const expected = mappingsLength + 1;
|
|
1890
|
+
if (nextIndex !== expected) {
|
|
1891
|
+
throw new Error(
|
|
1892
|
+
`CRITICAL: Next index mismatch - expected ${expected}, got ${nextIndex}`
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function assertSequentialIndex(actual, expected) {
|
|
1897
|
+
if (actual !== expected) {
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
`CRITICAL: Indices must be sequential from 1..N. Expected ${expected}, got ${actual}`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
function assertExactlyOneOfDynamicOrValue(m) {
|
|
1904
|
+
const hasDynamic = typeof m.dynamicName === "string";
|
|
1905
|
+
const hasStatic = m.value !== void 0;
|
|
1906
|
+
if (hasDynamic === hasStatic) {
|
|
1907
|
+
throw new Error(
|
|
1908
|
+
`CRITICAL: ParamMap ${m.index} must have exactly one of dynamicName or value`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function normalizeDynamicNameOrThrow(dynamicName, index) {
|
|
1913
|
+
const dn = dynamicName.trim();
|
|
1914
|
+
if (dn.length === 0) {
|
|
1915
|
+
throw new Error(`CRITICAL: dynamicName cannot be empty (index=${index})`);
|
|
1916
|
+
}
|
|
1917
|
+
return dn;
|
|
1918
|
+
}
|
|
1919
|
+
function assertUniqueDynamicName(dn, seen) {
|
|
1920
|
+
if (seen.has(dn)) {
|
|
1921
|
+
throw new Error(`CRITICAL: Duplicate dynamic param name in mappings: ${dn}`);
|
|
1922
|
+
}
|
|
1923
|
+
seen.add(dn);
|
|
1924
|
+
}
|
|
1925
|
+
function validateMappingEntry(m, expectedIndex, seenDynamic) {
|
|
1926
|
+
assertSequentialIndex(m.index, expectedIndex);
|
|
1927
|
+
assertExactlyOneOfDynamicOrValue(m);
|
|
1928
|
+
if (typeof m.dynamicName === "string") {
|
|
1929
|
+
const dn = normalizeDynamicNameOrThrow(m.dynamicName, m.index);
|
|
1930
|
+
assertUniqueDynamicName(dn, seenDynamic);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function validateMappings(mappings) {
|
|
1934
|
+
const seenDynamic = /* @__PURE__ */ new Set();
|
|
1935
|
+
for (let i = 0; i < mappings.length; i++) {
|
|
1936
|
+
validateMappingEntry(mappings[i], i + 1, seenDynamic);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function validateState(params, mappings, index) {
|
|
1940
|
+
assertSameLength(params, mappings);
|
|
1941
|
+
assertValidNextIndex(index);
|
|
1942
|
+
if (mappings.length === 0) return;
|
|
1943
|
+
validateMappings(mappings);
|
|
1944
|
+
assertNextIndexMatches(mappings.length, index);
|
|
1945
|
+
}
|
|
1946
|
+
function normalizeValue(value) {
|
|
1947
|
+
if (value instanceof Date) {
|
|
1948
|
+
return value.toISOString();
|
|
1949
|
+
}
|
|
1950
|
+
return value;
|
|
1951
|
+
}
|
|
1952
|
+
function createStoreInternal(startIndex, initialParams = [], initialMappings = []) {
|
|
1953
|
+
let index = startIndex;
|
|
1954
|
+
const params = [...initialParams];
|
|
1955
|
+
const mappings = [...initialMappings];
|
|
1956
|
+
const dynamicNameToIndex = /* @__PURE__ */ new Map();
|
|
1957
|
+
for (const m of initialMappings) {
|
|
1958
|
+
if (typeof m.dynamicName === "string") {
|
|
1959
|
+
dynamicNameToIndex.set(m.dynamicName.trim(), m.index);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function assertCanAdd() {
|
|
1963
|
+
if (index > MAX_PARAM_INDEX) {
|
|
1964
|
+
throw new Error(
|
|
1965
|
+
`CRITICAL: Cannot add param - would overflow MAX_SAFE_INTEGER. Current index: ${index}`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
function normalizeDynamicName(dynamicName) {
|
|
1970
|
+
const dn = dynamicName.trim();
|
|
1971
|
+
if (dn.length === 0) {
|
|
1972
|
+
throw new Error("CRITICAL: dynamicName cannot be empty");
|
|
1973
|
+
}
|
|
1974
|
+
return dn;
|
|
1975
|
+
}
|
|
1976
|
+
function format(position) {
|
|
1977
|
+
return `$${position}`;
|
|
1978
|
+
}
|
|
1979
|
+
function addDynamic(dynamicName) {
|
|
1980
|
+
const dn = normalizeDynamicName(dynamicName);
|
|
1981
|
+
const existing = dynamicNameToIndex.get(dn);
|
|
1982
|
+
if (existing !== void 0) {
|
|
1983
|
+
return format(existing);
|
|
1984
|
+
}
|
|
1985
|
+
const position = index;
|
|
1986
|
+
dynamicNameToIndex.set(dn, position);
|
|
1987
|
+
params.push(void 0);
|
|
1988
|
+
mappings.push({ index: position, dynamicName: dn });
|
|
1989
|
+
index++;
|
|
1990
|
+
return format(position);
|
|
1991
|
+
}
|
|
1992
|
+
function addStatic(value) {
|
|
1993
|
+
const position = index;
|
|
1994
|
+
const normalizedValue = normalizeValue(value);
|
|
1995
|
+
params.push(normalizedValue);
|
|
1996
|
+
mappings.push({ index: position, value: normalizedValue });
|
|
1997
|
+
index++;
|
|
1998
|
+
return format(position);
|
|
1999
|
+
}
|
|
2000
|
+
function add(value, dynamicName) {
|
|
2001
|
+
assertCanAdd();
|
|
2002
|
+
return dynamicName === void 0 ? addStatic(value) : addDynamic(dynamicName);
|
|
2003
|
+
}
|
|
2004
|
+
function addAuto(value) {
|
|
2005
|
+
if (schemaParser.isDynamicParameter(value)) {
|
|
2006
|
+
const dynamicName = schemaParser.extractDynamicName(value);
|
|
2007
|
+
return add(void 0, dynamicName);
|
|
2008
|
+
}
|
|
2009
|
+
return add(value);
|
|
2010
|
+
}
|
|
2011
|
+
function snapshot() {
|
|
2012
|
+
return Object.freeze({
|
|
2013
|
+
index,
|
|
2014
|
+
params: Object.freeze([...params]),
|
|
2015
|
+
mappings: Object.freeze([...mappings])
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
return {
|
|
2019
|
+
add,
|
|
2020
|
+
addAuto,
|
|
2021
|
+
snapshot,
|
|
2022
|
+
get index() {
|
|
2023
|
+
return index;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function createParamStore(startIndex = 1) {
|
|
2028
|
+
if (!Number.isInteger(startIndex) || startIndex < 1) {
|
|
2029
|
+
throw new Error(`Start index must be integer >= 1, got ${startIndex}`);
|
|
2030
|
+
}
|
|
2031
|
+
if (startIndex > MAX_PARAM_INDEX) {
|
|
2032
|
+
throw new Error(
|
|
2033
|
+
`Start index too high (${startIndex}), risk of overflow at MAX_SAFE_INTEGER`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
return createStoreInternal(startIndex);
|
|
2037
|
+
}
|
|
2038
|
+
function createParamStoreFrom(existingParams, existingMappings, nextIndex) {
|
|
2039
|
+
validateState([...existingParams], [...existingMappings], nextIndex);
|
|
2040
|
+
return createStoreInternal(
|
|
2041
|
+
nextIndex,
|
|
2042
|
+
[...existingParams],
|
|
2043
|
+
[...existingMappings]
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/builder/shared/state.ts
|
|
2048
|
+
function toPublicResult(clause, joins, params) {
|
|
2049
|
+
const snapshot = params.snapshot();
|
|
2050
|
+
return Object.freeze({
|
|
2051
|
+
clause: clause || DEFAULT_WHERE_CLAUSE,
|
|
2052
|
+
joins: Object.freeze([...joins]),
|
|
2053
|
+
params: snapshot.params,
|
|
2054
|
+
paramMappings: snapshot.mappings,
|
|
2055
|
+
nextParamIndex: snapshot.index
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/builder/where.ts
|
|
2060
|
+
function buildWhereClause(where, options) {
|
|
2061
|
+
var _a, _b, _c, _d, _e;
|
|
2062
|
+
const dialect = options.dialect || getGlobalDialect();
|
|
2063
|
+
const params = (_a = options.params) != null ? _a : createParamStore();
|
|
2064
|
+
const ctx = {
|
|
2065
|
+
alias: options.alias,
|
|
2066
|
+
model: options.model,
|
|
2067
|
+
schemaModels: (_b = options.schemaModels) != null ? _b : [],
|
|
2068
|
+
path: (_c = options.path) != null ? _c : [],
|
|
2069
|
+
isSubquery: (_d = options.isSubquery) != null ? _d : false,
|
|
2070
|
+
aliasGen: (_e = options.aliasGen) != null ? _e : createAliasGenerator(),
|
|
2071
|
+
dialect,
|
|
2072
|
+
params,
|
|
2073
|
+
depth: 0
|
|
2074
|
+
};
|
|
2075
|
+
const result = whereBuilderInstance.build(where, ctx);
|
|
2076
|
+
const publicResult = toPublicResult(result.clause, result.joins, params);
|
|
2077
|
+
if (!options.isSubquery) {
|
|
2078
|
+
const nums = [...publicResult.clause.matchAll(/\$(\d+)/g)].map(
|
|
2079
|
+
(m) => parseInt(m[1], 10)
|
|
2080
|
+
);
|
|
2081
|
+
if (nums.length > 0) {
|
|
2082
|
+
const min = Math.min(...nums);
|
|
2083
|
+
if (min === 1) {
|
|
2084
|
+
validateParamConsistency(publicResult.clause, publicResult.params);
|
|
2085
|
+
} else {
|
|
2086
|
+
validateParamConsistencyFragment(
|
|
2087
|
+
publicResult.clause,
|
|
2088
|
+
publicResult.params
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return publicResult;
|
|
2094
|
+
}
|
|
2095
|
+
function normalizeIntLike(name, v, opts = {}) {
|
|
2096
|
+
var _a, _b;
|
|
2097
|
+
if (!isNotNullish(v)) return void 0;
|
|
2098
|
+
if (schemaParser.isDynamicParameter(v)) return v;
|
|
2099
|
+
if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) {
|
|
2100
|
+
throw new Error(`${name} must be an integer`);
|
|
2101
|
+
}
|
|
2102
|
+
const min = (_a = opts.min) != null ? _a : 0;
|
|
2103
|
+
const allowZero = (_b = opts.allowZero) != null ? _b : true;
|
|
2104
|
+
if (!allowZero && v === 0) {
|
|
2105
|
+
throw new Error(`${name} must be > 0`);
|
|
2106
|
+
}
|
|
2107
|
+
if (v < min) {
|
|
2108
|
+
throw new Error(`${name} must be >= ${min}`);
|
|
2109
|
+
}
|
|
2110
|
+
if (typeof opts.max === "number" && v > opts.max) {
|
|
2111
|
+
throw new Error(`${name} must be <= ${opts.max}`);
|
|
2112
|
+
}
|
|
2113
|
+
return v;
|
|
2114
|
+
}
|
|
2115
|
+
function scopeName(scope, dynamicName) {
|
|
2116
|
+
const s = String(scope).trim();
|
|
2117
|
+
const dn = String(dynamicName).trim();
|
|
2118
|
+
if (s.length === 0) return dn;
|
|
2119
|
+
return `${s}:${dn}`;
|
|
2120
|
+
}
|
|
2121
|
+
function addAutoScoped(params, value, scope) {
|
|
2122
|
+
if (schemaParser.isDynamicParameter(value)) {
|
|
2123
|
+
const dn = schemaParser.extractDynamicName(value);
|
|
2124
|
+
return params.add(void 0, scopeName(scope, dn));
|
|
2125
|
+
}
|
|
2126
|
+
return params.add(value);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/builder/shared/order-by-utils.ts
|
|
2130
|
+
var flipNulls = (v) => {
|
|
2131
|
+
const s = String(v).toLowerCase();
|
|
2132
|
+
if (s === "first") return "last";
|
|
2133
|
+
if (s === "last") return "first";
|
|
2134
|
+
return v;
|
|
2135
|
+
};
|
|
2136
|
+
var flipSortString = (v) => {
|
|
2137
|
+
if (typeof v !== "string") return v;
|
|
2138
|
+
const s = v.toLowerCase();
|
|
2139
|
+
if (s === "asc") return "desc";
|
|
2140
|
+
if (s === "desc") return "asc";
|
|
2141
|
+
return v;
|
|
2142
|
+
};
|
|
2143
|
+
var getNextSort = (sortRaw) => {
|
|
2144
|
+
if (typeof sortRaw !== "string") return sortRaw;
|
|
2145
|
+
const s = sortRaw.toLowerCase();
|
|
2146
|
+
if (s === "asc") return "desc";
|
|
2147
|
+
if (s === "desc") return "asc";
|
|
2148
|
+
return sortRaw;
|
|
2149
|
+
};
|
|
2150
|
+
var flipObjectSort = (obj) => {
|
|
2151
|
+
const sortRaw = obj.sort;
|
|
2152
|
+
const out = __spreadProps(__spreadValues({}, obj), { sort: getNextSort(sortRaw) });
|
|
2153
|
+
const nullsRaw = obj.nulls;
|
|
2154
|
+
if (typeof nullsRaw === "string") {
|
|
2155
|
+
out.nulls = flipNulls(nullsRaw);
|
|
2156
|
+
}
|
|
2157
|
+
return out;
|
|
2158
|
+
};
|
|
2159
|
+
var flipValue = (v) => {
|
|
2160
|
+
if (typeof v === "string") return flipSortString(v);
|
|
2161
|
+
if (isPlainObject(v)) return flipObjectSort(v);
|
|
2162
|
+
return v;
|
|
2163
|
+
};
|
|
2164
|
+
var assertSingleFieldObject = (item) => {
|
|
2165
|
+
if (!isPlainObject(item)) {
|
|
2166
|
+
throw new Error("orderBy array entries must be objects");
|
|
2167
|
+
}
|
|
2168
|
+
const entries = Object.entries(item);
|
|
2169
|
+
if (entries.length !== 1) {
|
|
2170
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
2171
|
+
}
|
|
2172
|
+
return entries[0];
|
|
2173
|
+
};
|
|
2174
|
+
var flipOrderByArray = (orderBy) => {
|
|
2175
|
+
return orderBy.map((item) => {
|
|
2176
|
+
const [k, v] = assertSingleFieldObject(item);
|
|
2177
|
+
return { [k]: flipValue(v) };
|
|
2178
|
+
});
|
|
2179
|
+
};
|
|
2180
|
+
var flipOrderByObject = (orderBy) => {
|
|
2181
|
+
const out = {};
|
|
2182
|
+
for (const [k, v] of Object.entries(orderBy)) {
|
|
2183
|
+
out[k] = flipValue(v);
|
|
2184
|
+
}
|
|
2185
|
+
return out;
|
|
2186
|
+
};
|
|
2187
|
+
function reverseOrderByInput(orderBy) {
|
|
2188
|
+
if (!isNotNullish(orderBy)) return orderBy;
|
|
2189
|
+
if (Array.isArray(orderBy)) {
|
|
2190
|
+
return flipOrderByArray(orderBy);
|
|
2191
|
+
}
|
|
2192
|
+
if (isPlainObject(orderBy)) {
|
|
2193
|
+
return flipOrderByObject(orderBy);
|
|
2194
|
+
}
|
|
2195
|
+
throw new Error("orderBy must be an object or array of objects");
|
|
2196
|
+
}
|
|
2197
|
+
var normalizePairs = (pairs, parseValue) => {
|
|
2198
|
+
return pairs.map(([field, rawValue]) => {
|
|
2199
|
+
const parsed = parseValue(rawValue, field);
|
|
2200
|
+
return {
|
|
2201
|
+
[field]: parsed.nulls !== void 0 ? { sort: parsed.direction, nulls: parsed.nulls } : parsed.direction
|
|
2202
|
+
};
|
|
2203
|
+
});
|
|
2204
|
+
};
|
|
2205
|
+
function normalizeOrderByInput(orderBy, parseValue) {
|
|
2206
|
+
if (!isNotNullish(orderBy)) return [];
|
|
2207
|
+
if (Array.isArray(orderBy)) {
|
|
2208
|
+
const pairs = orderBy.map(assertSingleFieldObject);
|
|
2209
|
+
return normalizePairs(pairs, parseValue);
|
|
2210
|
+
}
|
|
2211
|
+
if (isPlainObject(orderBy)) {
|
|
2212
|
+
return normalizePairs(Object.entries(orderBy), parseValue);
|
|
2213
|
+
}
|
|
2214
|
+
throw new Error("orderBy must be an object or array of objects");
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// src/builder/pagination.ts
|
|
2218
|
+
var MAX_LIMIT_OFFSET = 2147483647;
|
|
2219
|
+
function parseDirectionRaw(raw, errorLabel) {
|
|
2220
|
+
const s = String(raw).toLowerCase();
|
|
2221
|
+
if (s === "asc" || s === "desc") return s;
|
|
2222
|
+
throw new Error(`Invalid ${errorLabel}: ${raw}`);
|
|
2223
|
+
}
|
|
2224
|
+
function parseNullsRaw(raw, errorLabel) {
|
|
2225
|
+
if (!isNotNullish(raw)) return void 0;
|
|
2226
|
+
const s = String(raw).toLowerCase();
|
|
2227
|
+
if (s === "first" || s === "last") return s;
|
|
2228
|
+
throw new Error(`Invalid ${errorLabel}: ${raw}`);
|
|
2229
|
+
}
|
|
2230
|
+
function requireOrderByObject(v, errorPrefix) {
|
|
2231
|
+
if (!isPlainObject(v) || !("sort" in v)) {
|
|
2232
|
+
throw new Error(`${errorPrefix} must be 'asc' | 'desc' or { sort, nulls? }`);
|
|
2233
|
+
}
|
|
2234
|
+
return v;
|
|
2235
|
+
}
|
|
2236
|
+
function assertAllowedOrderByKeys(obj, fieldName) {
|
|
2237
|
+
const allowed = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
2238
|
+
for (const k of Object.keys(obj)) {
|
|
2239
|
+
if (!allowed.has(k)) {
|
|
2240
|
+
throw new Error(
|
|
2241
|
+
fieldName ? `Unsupported orderBy key '${k}' for field '${fieldName}'` : `Unsupported orderBy key '${k}'`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
function parseOrderByValue(v, fieldName) {
|
|
2247
|
+
const errorPrefix = fieldName ? `orderBy for '${fieldName}'` : "orderBy value";
|
|
2248
|
+
if (typeof v === "string") {
|
|
2249
|
+
return { direction: parseDirectionRaw(v, `${errorPrefix} direction`) };
|
|
2250
|
+
}
|
|
2251
|
+
const obj = requireOrderByObject(v, errorPrefix);
|
|
2252
|
+
const direction = parseDirectionRaw(obj.sort, `${errorPrefix}.sort`);
|
|
2253
|
+
const nulls = parseNullsRaw(obj.nulls, `${errorPrefix}.nulls`);
|
|
2254
|
+
assertAllowedOrderByKeys(obj, fieldName);
|
|
2255
|
+
return { direction, nulls };
|
|
2256
|
+
}
|
|
2257
|
+
function normalizeFiniteInteger(name, v) {
|
|
2258
|
+
if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) {
|
|
2259
|
+
throw new Error(`${name} must be an integer`);
|
|
2260
|
+
}
|
|
2261
|
+
return v;
|
|
2262
|
+
}
|
|
2263
|
+
function normalizeNonNegativeInt(name, v) {
|
|
2264
|
+
if (schemaParser.isDynamicParameter(v)) return v;
|
|
2265
|
+
const n = normalizeFiniteInteger(name, v);
|
|
2266
|
+
if (n < 0) {
|
|
2267
|
+
throw new Error(`${name} must be >= 0`);
|
|
2268
|
+
}
|
|
2269
|
+
if (n > MAX_LIMIT_OFFSET) {
|
|
2270
|
+
throw new Error(`${name} must be <= ${MAX_LIMIT_OFFSET}`);
|
|
2271
|
+
}
|
|
2272
|
+
return n;
|
|
2273
|
+
}
|
|
2274
|
+
function hasNonNullishProp(v, key) {
|
|
2275
|
+
return isPlainObject(v) && key in v && isNotNullish(v[key]);
|
|
2276
|
+
}
|
|
2277
|
+
function normalizeIntegerOrDynamic(name, v) {
|
|
2278
|
+
if (schemaParser.isDynamicParameter(v)) return v;
|
|
2279
|
+
return normalizeFiniteInteger(name, v);
|
|
2280
|
+
}
|
|
2281
|
+
function readSkipTake(relArgs) {
|
|
2282
|
+
const hasSkip = hasNonNullishProp(relArgs, "skip");
|
|
2283
|
+
const hasTake = hasNonNullishProp(relArgs, "take");
|
|
2284
|
+
if (!hasSkip && !hasTake) {
|
|
2285
|
+
return {
|
|
2286
|
+
hasSkip: false,
|
|
2287
|
+
hasTake: false,
|
|
2288
|
+
skipVal: void 0,
|
|
2289
|
+
takeVal: void 0
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
const obj = relArgs;
|
|
2293
|
+
const skipVal = hasSkip ? normalizeNonNegativeInt("skip", obj.skip) : void 0;
|
|
2294
|
+
const takeVal = hasTake ? normalizeIntegerOrDynamic("take", obj.take) : void 0;
|
|
2295
|
+
return { hasSkip, hasTake, skipVal, takeVal };
|
|
2296
|
+
}
|
|
2297
|
+
function buildOrderByFragment(entries, alias, dialect) {
|
|
2298
|
+
if (entries.length === 0) return "";
|
|
2299
|
+
const out = [];
|
|
2300
|
+
for (const e of entries) {
|
|
2301
|
+
const dir = e.direction.toUpperCase();
|
|
2302
|
+
const c = col(alias, e.field);
|
|
2303
|
+
if (dialect === "postgres") {
|
|
2304
|
+
const nulls = isNotNullish(e.nulls) ? ` NULLS ${e.nulls.toUpperCase()}` : "";
|
|
2305
|
+
out.push(`${c} ${dir}${nulls}`);
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
if (isNotNullish(e.nulls)) {
|
|
2309
|
+
const isNullExpr = `(${c} IS NULL)`;
|
|
2310
|
+
const nullRankDir = e.nulls === "first" ? "DESC" : "ASC";
|
|
2311
|
+
out.push(`${isNullExpr} ${nullRankDir}`);
|
|
2312
|
+
out.push(`${c} ${dir}`);
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
out.push(`${c} ${dir}`);
|
|
2316
|
+
}
|
|
2317
|
+
return out.join(SQL_SEPARATORS.ORDER_BY);
|
|
2318
|
+
}
|
|
2319
|
+
function defaultNullsFor(dialect, direction) {
|
|
2320
|
+
if (dialect === "postgres") {
|
|
2321
|
+
return direction === "asc" ? "last" : "first";
|
|
2322
|
+
}
|
|
2323
|
+
return direction === "asc" ? "first" : "last";
|
|
2324
|
+
}
|
|
2325
|
+
function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
|
|
2326
|
+
const existing = /* @__PURE__ */ new Map();
|
|
2327
|
+
for (const e of orderEntries) existing.set(e.field, e);
|
|
2328
|
+
const out = [...orderEntries];
|
|
2329
|
+
for (const [field] of cursorEntries) {
|
|
2330
|
+
if (!existing.has(field)) {
|
|
2331
|
+
out.push({ field, direction: "asc" });
|
|
2332
|
+
existing.set(field, out[out.length - 1]);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return out;
|
|
2336
|
+
}
|
|
2337
|
+
function buildCursorFilterParts(cursor, cursorAlias, params) {
|
|
2338
|
+
const entries = Object.entries(cursor);
|
|
2339
|
+
if (entries.length === 0) {
|
|
2340
|
+
throw new Error("cursor must have at least one field");
|
|
2341
|
+
}
|
|
2342
|
+
const placeholdersByField = /* @__PURE__ */ new Map();
|
|
2343
|
+
const parts = [];
|
|
2344
|
+
for (const [field, value] of entries) {
|
|
2345
|
+
const c = `${cursorAlias}.${quote(field)}`;
|
|
2346
|
+
if (value === null) {
|
|
2347
|
+
parts.push(`${c} IS NULL`);
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
const ph = addAutoScoped(params, value, `cursor.filter.${field}`);
|
|
2351
|
+
placeholdersByField.set(field, ph);
|
|
2352
|
+
parts.push(`${c} = ${ph}`);
|
|
2353
|
+
}
|
|
2354
|
+
return {
|
|
2355
|
+
whereSql: parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`,
|
|
2356
|
+
placeholdersByField
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function cursorValueExpr(tableName, cursorAlias, cursorWhereSql, field) {
|
|
2360
|
+
return `(SELECT ${cursorAlias}.${quote(field)} ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
2361
|
+
}
|
|
2362
|
+
function buildCursorRowExistsExpr(tableName, cursorAlias, cursorWhereSql) {
|
|
2363
|
+
return `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
2364
|
+
}
|
|
2365
|
+
function buildCursorEqualityExpr(columnExpr, valueExpr) {
|
|
2366
|
+
return `((${valueExpr} IS NULL AND ${columnExpr} IS NULL) OR (${valueExpr} IS NOT NULL AND ${columnExpr} = ${valueExpr}))`;
|
|
2367
|
+
}
|
|
2368
|
+
function buildCursorInequalityExpr(columnExpr, direction, nulls, valueExpr) {
|
|
2369
|
+
const op = direction === "asc" ? ">" : "<";
|
|
2370
|
+
if (nulls === "first") {
|
|
2371
|
+
return `(CASE WHEN ${valueExpr} IS NULL THEN (${columnExpr} IS NOT NULL) ELSE (${columnExpr} ${op} ${valueExpr}) END)`;
|
|
2372
|
+
}
|
|
2373
|
+
return `(CASE WHEN ${valueExpr} IS NULL THEN 0=1 ELSE ((${columnExpr} ${op} ${valueExpr}) OR (${columnExpr} IS NULL)) END)`;
|
|
2374
|
+
}
|
|
2375
|
+
function buildOuterCursorMatch(cursor, outerAlias, placeholdersByField, params) {
|
|
2376
|
+
const parts = [];
|
|
2377
|
+
for (const [field, value] of Object.entries(cursor)) {
|
|
2378
|
+
const c = col(outerAlias, field);
|
|
2379
|
+
if (value === null) {
|
|
2380
|
+
parts.push(`${c} IS NULL`);
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
const existing = placeholdersByField.get(field);
|
|
2384
|
+
if (typeof existing === "string" && existing.length > 0) {
|
|
2385
|
+
parts.push(`${c} = ${existing}`);
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
const ph = addAutoScoped(params, value, `cursor.outerMatch.${field}`);
|
|
2389
|
+
parts.push(`${c} = ${ph}`);
|
|
2390
|
+
}
|
|
2391
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
2392
|
+
}
|
|
2393
|
+
function buildOrderEntries(orderBy) {
|
|
2394
|
+
const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
2395
|
+
const entries = [];
|
|
2396
|
+
for (const item of normalized) {
|
|
2397
|
+
for (const [field, value] of Object.entries(item)) {
|
|
2398
|
+
if (typeof value === "string") {
|
|
2399
|
+
entries.push({ field, direction: value });
|
|
2400
|
+
} else {
|
|
2401
|
+
entries.push({
|
|
2402
|
+
field,
|
|
2403
|
+
direction: value.sort,
|
|
2404
|
+
nulls: value.nulls
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return entries;
|
|
2410
|
+
}
|
|
2411
|
+
function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect) {
|
|
2412
|
+
var _a;
|
|
2413
|
+
const d = dialect != null ? dialect : getGlobalDialect();
|
|
2414
|
+
const cursorEntries = Object.entries(cursor);
|
|
2415
|
+
if (cursorEntries.length === 0) {
|
|
2416
|
+
throw new Error("cursor must have at least one field");
|
|
2417
|
+
}
|
|
2418
|
+
const cursorAlias = "__tp_cursor_src";
|
|
2419
|
+
const { whereSql: cursorWhereSql, placeholdersByField } = buildCursorFilterParts(cursor, cursorAlias, params);
|
|
2420
|
+
let orderEntries = buildOrderEntries(orderBy);
|
|
2421
|
+
if (orderEntries.length === 0) {
|
|
2422
|
+
orderEntries = cursorEntries.map(([field]) => ({
|
|
2423
|
+
field,
|
|
2424
|
+
direction: "asc"
|
|
2425
|
+
}));
|
|
2426
|
+
} else {
|
|
2427
|
+
orderEntries = ensureCursorFieldsInOrder(orderEntries, cursorEntries);
|
|
2428
|
+
}
|
|
2429
|
+
const existsExpr = buildCursorRowExistsExpr(
|
|
2430
|
+
tableName,
|
|
2431
|
+
cursorAlias,
|
|
2432
|
+
cursorWhereSql
|
|
2433
|
+
);
|
|
2434
|
+
const outerCursorMatch = buildOuterCursorMatch(
|
|
2435
|
+
cursor,
|
|
2436
|
+
alias,
|
|
2437
|
+
placeholdersByField,
|
|
2438
|
+
params
|
|
2439
|
+
);
|
|
2440
|
+
const orClauses = [];
|
|
2441
|
+
for (let level = 0; level < orderEntries.length; level++) {
|
|
2442
|
+
const andParts = [];
|
|
2443
|
+
for (let i = 0; i < level; i++) {
|
|
2444
|
+
const e2 = orderEntries[i];
|
|
2445
|
+
const c2 = col(alias, e2.field);
|
|
2446
|
+
const v2 = cursorValueExpr(tableName, cursorAlias, cursorWhereSql, e2.field);
|
|
2447
|
+
andParts.push(buildCursorEqualityExpr(c2, v2));
|
|
2448
|
+
}
|
|
2449
|
+
const e = orderEntries[level];
|
|
2450
|
+
const c = col(alias, e.field);
|
|
2451
|
+
const v = cursorValueExpr(tableName, cursorAlias, cursorWhereSql, e.field);
|
|
2452
|
+
const nulls = (_a = e.nulls) != null ? _a : defaultNullsFor(d, e.direction);
|
|
2453
|
+
andParts.push(buildCursorInequalityExpr(c, e.direction, nulls, v));
|
|
2454
|
+
orClauses.push(`(${andParts.join(SQL_SEPARATORS.CONDITION_AND)})`);
|
|
2455
|
+
}
|
|
2456
|
+
const exclusive = orClauses.join(SQL_SEPARATORS.CONDITION_OR);
|
|
2457
|
+
return `(${existsExpr} ${SQL_SEPARATORS.CONDITION_AND} ((${exclusive})${SQL_SEPARATORS.CONDITION_OR}(${outerCursorMatch})))`;
|
|
2458
|
+
}
|
|
2459
|
+
function buildOrderBy(orderBy, alias, dialect) {
|
|
2460
|
+
const entries = buildOrderEntries(orderBy);
|
|
2461
|
+
if (entries.length === 0) return "";
|
|
2462
|
+
const d = dialect != null ? dialect : getGlobalDialect();
|
|
2463
|
+
return buildOrderByFragment(entries, alias, d);
|
|
2464
|
+
}
|
|
2465
|
+
function buildOrderByClause(args, alias, dialect) {
|
|
2466
|
+
if (!isNotNullish(args.orderBy)) return "";
|
|
2467
|
+
const result = buildOrderBy(args.orderBy, alias, dialect);
|
|
2468
|
+
if (!isNonEmptyString(result)) {
|
|
2469
|
+
throw new Error(
|
|
2470
|
+
"buildOrderByClause: orderBy specified but produced empty result"
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
return result;
|
|
2474
|
+
}
|
|
2475
|
+
function normalizeTakeLike(v) {
|
|
2476
|
+
const n = normalizeIntLike("take", v, {
|
|
2477
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
2478
|
+
max: MAX_LIMIT_OFFSET,
|
|
2479
|
+
allowZero: true
|
|
2480
|
+
});
|
|
2481
|
+
if (typeof n === "number") {
|
|
2482
|
+
if (n === 0) return 0;
|
|
2483
|
+
}
|
|
2484
|
+
return n;
|
|
2485
|
+
}
|
|
2486
|
+
function normalizeSkipLike(v) {
|
|
2487
|
+
return normalizeIntLike("skip", v, {
|
|
2488
|
+
min: 0,
|
|
2489
|
+
max: MAX_LIMIT_OFFSET,
|
|
2490
|
+
allowZero: true
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
function getPaginationParams(method, args) {
|
|
2494
|
+
if (method === "findMany") {
|
|
2495
|
+
return {
|
|
2496
|
+
take: normalizeTakeLike(args.take),
|
|
2497
|
+
skip: normalizeSkipLike(args.skip),
|
|
2498
|
+
cursor: args.cursor
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
if (method === "findFirst") {
|
|
2502
|
+
const skip = normalizeSkipLike(args.skip);
|
|
2503
|
+
return { take: 1, skip: skip != null ? skip : 0 };
|
|
2504
|
+
}
|
|
2505
|
+
if (method === "findUnique") {
|
|
2506
|
+
return { take: 1, skip: 0 };
|
|
2507
|
+
}
|
|
2508
|
+
return {};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// src/builder/select/fields.ts
|
|
2512
|
+
function toSelectEntries(select) {
|
|
2513
|
+
const out = [];
|
|
2514
|
+
for (const [k, v] of Object.entries(select)) {
|
|
2515
|
+
if (v !== false && v !== void 0) out.push([k, v]);
|
|
2516
|
+
}
|
|
2517
|
+
return out;
|
|
2518
|
+
}
|
|
2519
|
+
function validateSelectKeys(entries, scalarSet, relationSet) {
|
|
2520
|
+
const unknown = [];
|
|
2521
|
+
for (const [k] of entries) {
|
|
2522
|
+
if (k === "_count") continue;
|
|
2523
|
+
if (!scalarSet.has(k) && !relationSet.has(k)) unknown.push(k);
|
|
2524
|
+
}
|
|
2525
|
+
if (unknown.length > 0) {
|
|
2526
|
+
throw new Error(`Select contains unknown fields: ${unknown.join(", ")}`);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
function analyzeSelectEntries(entries, scalarSet, relationSet) {
|
|
2530
|
+
const scalarSelected = [];
|
|
2531
|
+
let hasRelationSelection = false;
|
|
2532
|
+
let hasCount = false;
|
|
2533
|
+
for (const [k, v] of entries) {
|
|
2534
|
+
if (k === "_count") {
|
|
2535
|
+
hasCount = true;
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
if (relationSet.has(k)) hasRelationSelection = true;
|
|
2539
|
+
if (scalarSet.has(k) && v === true) scalarSelected.push(k);
|
|
2540
|
+
}
|
|
2541
|
+
return { scalarSelected, hasRelationSelection, hasCount };
|
|
2542
|
+
}
|
|
2543
|
+
function buildDefaultScalarFields(model, alias) {
|
|
2544
|
+
const excludedPrefixes = [
|
|
2545
|
+
SCHEMA_PREFIXES.INTERNAL,
|
|
2546
|
+
SCHEMA_PREFIXES.COMMENT
|
|
2547
|
+
];
|
|
2548
|
+
const out = [];
|
|
2549
|
+
for (const f of model.fields) {
|
|
2550
|
+
if (f.isRelation) continue;
|
|
2551
|
+
const excluded = excludedPrefixes.some((p) => f.name.startsWith(p));
|
|
2552
|
+
if (!excluded) out.push(col(alias, f.name));
|
|
2553
|
+
}
|
|
2554
|
+
if (!isNonEmptyArray(out)) {
|
|
2555
|
+
throw new Error(`Model ${model.name} has no selectable fields`);
|
|
2556
|
+
}
|
|
2557
|
+
return out;
|
|
2558
|
+
}
|
|
2559
|
+
function buildSelectFields(args, model, alias) {
|
|
2560
|
+
const scalarSet = getScalarFieldSet(model);
|
|
2561
|
+
const relationSet = getRelationFieldSet(model);
|
|
2562
|
+
if (!isNotNullish(args.select)) {
|
|
2563
|
+
return buildDefaultScalarFields(model, alias).join(
|
|
2564
|
+
SQL_SEPARATORS.FIELD_LIST
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
const entries = toSelectEntries(args.select);
|
|
2568
|
+
validateSelectKeys(entries, scalarSet, relationSet);
|
|
2569
|
+
const { scalarSelected, hasRelationSelection, hasCount } = analyzeSelectEntries(entries, scalarSet, relationSet);
|
|
2570
|
+
const fields = scalarSelected.map((field) => col(alias, field));
|
|
2571
|
+
if (!isNonEmptyArray(fields)) {
|
|
2572
|
+
if (hasRelationSelection) return "";
|
|
2573
|
+
if (!hasCount) {
|
|
2574
|
+
throw new Error("Select must have at least one scalar field set to true");
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return fields.join(SQL_SEPARATORS.FIELD_LIST);
|
|
2578
|
+
}
|
|
2579
|
+
function buildAllScalarParts(model, alias) {
|
|
2580
|
+
const scalarFields = model.fields.filter((f) => !f.isRelation);
|
|
2581
|
+
if (!isNonEmptyArray(scalarFields)) {
|
|
2582
|
+
throw new Error(`Model ${model.name} has no scalar fields`);
|
|
2583
|
+
}
|
|
2584
|
+
const parts = [];
|
|
2585
|
+
for (const field of scalarFields) {
|
|
2586
|
+
parts.push(`${sqlStringLiteral(field.name)}, ${col(alias, field.name)}`);
|
|
2587
|
+
}
|
|
2588
|
+
return parts;
|
|
2589
|
+
}
|
|
2590
|
+
function validateRelationSelectKeys(entries, scalarNames, relationNames) {
|
|
2591
|
+
const unknown = [];
|
|
2592
|
+
for (const [k] of entries) {
|
|
2593
|
+
if (!scalarNames.has(k) && !relationNames.has(k)) unknown.push(k);
|
|
2594
|
+
}
|
|
2595
|
+
if (unknown.length > 0) {
|
|
2596
|
+
throw new Error(`Select contains unknown fields: ${unknown.join(", ")}`);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
function buildSelectedScalarParts(entries, scalarNames, alias) {
|
|
2600
|
+
const parts = [];
|
|
2601
|
+
for (const [key, value] of entries) {
|
|
2602
|
+
if (!scalarNames.has(key)) continue;
|
|
2603
|
+
if (value === true) {
|
|
2604
|
+
parts.push(`${sqlStringLiteral(key)}, ${col(alias, key)}`);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
return parts;
|
|
2608
|
+
}
|
|
2609
|
+
function buildRelationSelect(relArgs, relModel, relAlias) {
|
|
2610
|
+
if (relArgs === true) {
|
|
2611
|
+
return buildAllScalarParts(relModel, relAlias).join(
|
|
2612
|
+
SQL_SEPARATORS.FIELD_LIST
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
if (isPlainObject(relArgs) && hasProperty(relArgs, "select")) {
|
|
2616
|
+
const sel = relArgs.select;
|
|
2617
|
+
if (!isPlainObject(sel)) {
|
|
2618
|
+
throw new Error(
|
|
2619
|
+
`Relation select must be an object for model ${relModel.name}`
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
const scalarNames = new Set(
|
|
2623
|
+
relModel.fields.filter((f) => !f.isRelation).map((f) => f.name)
|
|
2624
|
+
);
|
|
2625
|
+
const relationNames = new Set(
|
|
2626
|
+
relModel.fields.filter((f) => f.isRelation).map((f) => f.name)
|
|
2627
|
+
);
|
|
2628
|
+
const entries = toSelectEntries(sel);
|
|
2629
|
+
validateRelationSelectKeys(entries, scalarNames, relationNames);
|
|
2630
|
+
return buildSelectedScalarParts(entries, scalarNames, relAlias).join(
|
|
2631
|
+
SQL_SEPARATORS.FIELD_LIST
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
return buildAllScalarParts(relModel, relAlias).join(SQL_SEPARATORS.FIELD_LIST);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/builder/select/includes.ts
|
|
2638
|
+
function getRelationTableReference(relModel, dialect) {
|
|
2639
|
+
return buildTableReference(
|
|
2640
|
+
SQL_TEMPLATES.PUBLIC_SCHEMA,
|
|
2641
|
+
relModel.tableName,
|
|
2642
|
+
dialect
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
function resolveRelationOrThrow(model, schemas, relName) {
|
|
2646
|
+
const field = model.fields.find((f) => f.name === relName);
|
|
2647
|
+
if (!isNotNullish(field)) {
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
`Unknown relation '${relName}' on model ${model.name}. Available relation fields: ${model.fields.filter((f) => f.isRelation).map((f) => f.name).join(", ")}`
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
if (!isValidRelationField(field)) {
|
|
2653
|
+
throw new Error(
|
|
2654
|
+
`Invalid relation metadata for '${relName}' on model ${model.name}. This usually indicates a schema parsing error (missing foreignKey/references).`
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
const relModel = schemas.find((m) => m.name === field.relatedModel);
|
|
2658
|
+
if (!isNotNullish(relModel)) {
|
|
2659
|
+
throw new Error(
|
|
2660
|
+
`Relation '${relName}' on model ${model.name} references missing model '${field.relatedModel}'.`
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
return { field, relModel };
|
|
2664
|
+
}
|
|
2665
|
+
function relationEntriesFromArgs(args, model) {
|
|
2666
|
+
const relationSet = getRelationFieldSet(model);
|
|
2667
|
+
const out = [];
|
|
2668
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2669
|
+
const pushFrom = (src) => {
|
|
2670
|
+
if (!isPlainObject(src)) return;
|
|
2671
|
+
for (const [k, v] of Object.entries(src)) {
|
|
2672
|
+
if (v === false) continue;
|
|
2673
|
+
if (!relationSet.has(k)) continue;
|
|
2674
|
+
if (seen.has(k)) continue;
|
|
2675
|
+
seen.add(k);
|
|
2676
|
+
out.push([k, v]);
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
pushFrom(args.include);
|
|
2680
|
+
pushFrom(args.select);
|
|
2681
|
+
return out;
|
|
2682
|
+
}
|
|
2683
|
+
function assertScalarField(model, fieldName) {
|
|
2684
|
+
const f = model.fields.find((x) => x.name === fieldName);
|
|
2685
|
+
if (!f) {
|
|
2686
|
+
throw new Error(
|
|
2687
|
+
`orderBy references unknown field '${fieldName}' on model ${model.name}`
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
if (f.isRelation) {
|
|
2691
|
+
throw new Error(
|
|
2692
|
+
`orderBy does not support relation field '${fieldName}' on model ${model.name}`
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
function validateOrderByDirection(fieldName, v) {
|
|
2697
|
+
const s = String(v).toLowerCase();
|
|
2698
|
+
if (s !== "asc" && s !== "desc") {
|
|
2699
|
+
throw new Error(
|
|
2700
|
+
`Invalid orderBy direction for '${fieldName}': ${String(v)}`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function validateOrderByObject(fieldName, v) {
|
|
2705
|
+
if (!("sort" in v)) {
|
|
2706
|
+
throw new Error(
|
|
2707
|
+
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
validateOrderByDirection(fieldName, v.sort);
|
|
2711
|
+
if ("nulls" in v && isNotNullish(v.nulls)) {
|
|
2712
|
+
const n = String(v.nulls).toLowerCase();
|
|
2713
|
+
if (n !== "first" && n !== "last") {
|
|
2714
|
+
throw new Error(
|
|
2715
|
+
`Invalid orderBy.nulls for '${fieldName}': ${String(v.nulls)}`
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
const allowed = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
2720
|
+
for (const k of Object.keys(v)) {
|
|
2721
|
+
if (!allowed.has(k)) {
|
|
2722
|
+
throw new Error(`Unsupported orderBy key '${k}' for field '${fieldName}'`);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function normalizeOrderByFieldName(name) {
|
|
2727
|
+
const fieldName = String(name).trim();
|
|
2728
|
+
if (fieldName.length === 0) {
|
|
2729
|
+
throw new Error("orderBy field name cannot be empty");
|
|
2730
|
+
}
|
|
2731
|
+
return fieldName;
|
|
2732
|
+
}
|
|
2733
|
+
function requirePlainObjectForOrderByEntry(v) {
|
|
2734
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
2735
|
+
throw new Error("orderBy array entries must be objects");
|
|
2736
|
+
}
|
|
2737
|
+
return v;
|
|
2738
|
+
}
|
|
2739
|
+
function parseSingleFieldOrderByObject(obj) {
|
|
2740
|
+
const entries = Object.entries(obj);
|
|
2741
|
+
if (entries.length !== 1) {
|
|
2742
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
2743
|
+
}
|
|
2744
|
+
const fieldName = normalizeOrderByFieldName(entries[0][0]);
|
|
2745
|
+
return [fieldName, entries[0][1]];
|
|
2746
|
+
}
|
|
2747
|
+
function parseOrderByArray(orderBy) {
|
|
2748
|
+
return orderBy.map(
|
|
2749
|
+
(item) => parseSingleFieldOrderByObject(requirePlainObjectForOrderByEntry(item))
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
function parseOrderByObject(orderBy) {
|
|
2753
|
+
const out = [];
|
|
2754
|
+
for (const [k, v] of Object.entries(orderBy)) {
|
|
2755
|
+
out.push([normalizeOrderByFieldName(k), v]);
|
|
2756
|
+
}
|
|
2757
|
+
return out;
|
|
2758
|
+
}
|
|
2759
|
+
function getOrderByEntries(orderBy) {
|
|
2760
|
+
if (!isNotNullish(orderBy)) return [];
|
|
2761
|
+
if (Array.isArray(orderBy)) {
|
|
2762
|
+
return parseOrderByArray(orderBy);
|
|
2763
|
+
}
|
|
2764
|
+
if (typeof orderBy === "object" && orderBy !== null) {
|
|
2765
|
+
return parseOrderByObject(orderBy);
|
|
2766
|
+
}
|
|
2767
|
+
throw new Error("orderBy must be an object or array of objects");
|
|
2768
|
+
}
|
|
2769
|
+
function validateOrderByForModel(model, orderBy) {
|
|
2770
|
+
const entries = getOrderByEntries(orderBy);
|
|
2771
|
+
for (const [fieldName, v] of entries) {
|
|
2772
|
+
assertScalarField(model, fieldName);
|
|
2773
|
+
if (typeof v === "string") {
|
|
2774
|
+
validateOrderByDirection(fieldName, v);
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
if (isPlainObject(v)) {
|
|
2778
|
+
validateOrderByObject(fieldName, v);
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
throw new Error(
|
|
2782
|
+
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
function appendLimitOffset(sql, dialect, params, takeVal, skipVal, scope) {
|
|
2787
|
+
const hasTake = isNotNullish(takeVal);
|
|
2788
|
+
const hasSkip = isNotNullish(skipVal);
|
|
2789
|
+
if (dialect === "sqlite" && !hasTake && hasSkip) {
|
|
2790
|
+
const skipPh = addAutoScoped(params, skipVal, `${scope}.skip`);
|
|
2791
|
+
return `${sql} ${SQL_TEMPLATES.LIMIT} -1 ${SQL_TEMPLATES.OFFSET} ${skipPh}`;
|
|
2792
|
+
}
|
|
2793
|
+
if (hasTake) {
|
|
2794
|
+
const takePh = addAutoScoped(params, takeVal, `${scope}.take`);
|
|
2795
|
+
sql = `${sql} ${SQL_TEMPLATES.LIMIT} ${takePh}`;
|
|
2796
|
+
}
|
|
2797
|
+
if (hasSkip) {
|
|
2798
|
+
const skipPh = addAutoScoped(params, skipVal, `${scope}.skip`);
|
|
2799
|
+
sql = `${sql} ${SQL_TEMPLATES.OFFSET} ${skipPh}`;
|
|
2800
|
+
}
|
|
2801
|
+
return sql;
|
|
2802
|
+
}
|
|
2803
|
+
function readWhereInput(relArgs) {
|
|
2804
|
+
if (!isPlainObject(relArgs)) return {};
|
|
2805
|
+
if (!hasProperty(relArgs, "where")) return {};
|
|
2806
|
+
const w = relArgs.where;
|
|
2807
|
+
return isPlainObject(w) ? w : {};
|
|
2808
|
+
}
|
|
2809
|
+
function readOrderByInput(relArgs) {
|
|
2810
|
+
if (!isPlainObject(relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
2811
|
+
if (!("orderBy" in relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
2812
|
+
return { hasOrderBy: true, orderBy: relArgs.orderBy };
|
|
2813
|
+
}
|
|
2814
|
+
function extractRelationPaginationConfig(relArgs) {
|
|
2815
|
+
const { hasOrderBy, orderBy: rawOrderByInput } = readOrderByInput(relArgs);
|
|
2816
|
+
const {
|
|
2817
|
+
hasSkip,
|
|
2818
|
+
hasTake,
|
|
2819
|
+
skipVal,
|
|
2820
|
+
takeVal: rawTakeVal
|
|
2821
|
+
} = readSkipTake(relArgs);
|
|
2822
|
+
return {
|
|
2823
|
+
hasOrderBy,
|
|
2824
|
+
orderBy: rawOrderByInput,
|
|
2825
|
+
hasSkip,
|
|
2826
|
+
hasTake,
|
|
2827
|
+
skipVal,
|
|
2828
|
+
takeVal: rawTakeVal
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
function maybeReverseNegativeTake(takeVal, hasOrderBy, orderByInput) {
|
|
2832
|
+
if (typeof takeVal !== "number") return { takeVal, orderByInput };
|
|
2833
|
+
if (takeVal >= 0) return { takeVal, orderByInput };
|
|
2834
|
+
if (!hasOrderBy) {
|
|
2835
|
+
throw new Error("Negative take requires orderBy for deterministic results");
|
|
2836
|
+
}
|
|
2837
|
+
return {
|
|
2838
|
+
takeVal: Math.abs(takeVal),
|
|
2839
|
+
orderByInput: reverseOrderByInput(orderByInput)
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
function hasIdTiebreaker(orderByInput) {
|
|
2843
|
+
const entries = Array.isArray(orderByInput) ? orderByInput : [orderByInput];
|
|
2844
|
+
return entries.some(
|
|
2845
|
+
(entry) => isPlainObject(entry) ? Object.prototype.hasOwnProperty.call(entry, "id") : false
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
function modelHasScalarId(relModel) {
|
|
2849
|
+
const idField = relModel.fields.find((f) => f.name === "id");
|
|
2850
|
+
return Boolean(idField && !idField.isRelation);
|
|
2851
|
+
}
|
|
2852
|
+
function addIdTiebreaker(orderByInput) {
|
|
2853
|
+
if (Array.isArray(orderByInput)) return [...orderByInput, { id: "asc" }];
|
|
2854
|
+
return [orderByInput, { id: "asc" }];
|
|
2855
|
+
}
|
|
2856
|
+
function ensureDeterministicOrderBy(relModel, hasOrderBy, orderByInput, hasPagination) {
|
|
2857
|
+
if (!hasPagination) {
|
|
2858
|
+
if (hasOrderBy && isNotNullish(orderByInput)) {
|
|
2859
|
+
validateOrderByForModel(relModel, orderByInput);
|
|
2860
|
+
}
|
|
2861
|
+
return orderByInput;
|
|
2862
|
+
}
|
|
2863
|
+
if (!hasOrderBy) {
|
|
2864
|
+
return modelHasScalarId(relModel) ? { id: "asc" } : orderByInput;
|
|
2865
|
+
}
|
|
2866
|
+
if (isNotNullish(orderByInput)) {
|
|
2867
|
+
validateOrderByForModel(relModel, orderByInput);
|
|
2868
|
+
}
|
|
2869
|
+
if (!modelHasScalarId(relModel)) return orderByInput;
|
|
2870
|
+
if (hasIdTiebreaker(orderByInput)) return orderByInput;
|
|
2871
|
+
return addIdTiebreaker(orderByInput);
|
|
2872
|
+
}
|
|
2873
|
+
function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
2874
|
+
let relSelect = buildRelationSelect(relArgs, relModel, relAlias);
|
|
2875
|
+
const nestedIncludes = isPlainObject(relArgs) ? buildIncludeSqlInternal(
|
|
2876
|
+
relArgs,
|
|
2877
|
+
relModel,
|
|
2878
|
+
ctx.schemas,
|
|
2879
|
+
relAlias,
|
|
2880
|
+
ctx.aliasGen,
|
|
2881
|
+
ctx.params,
|
|
2882
|
+
ctx.dialect
|
|
2883
|
+
) : [];
|
|
2884
|
+
if (isNonEmptyArray(nestedIncludes)) {
|
|
2885
|
+
const emptyJson = ctx.dialect === "postgres" ? `'[]'::json` : `json('[]')`;
|
|
2886
|
+
const nestedSelects = nestedIncludes.map(
|
|
2887
|
+
(inc) => inc.isOneToOne ? `${sqlStringLiteral(inc.name)}, (${inc.sql})` : `${sqlStringLiteral(inc.name)}, COALESCE((${inc.sql}), ${emptyJson})`
|
|
2888
|
+
).join(SQL_SEPARATORS.FIELD_LIST);
|
|
2889
|
+
relSelect = isNotNullish(relSelect) && relSelect.trim().length > 0 ? `${relSelect}${SQL_SEPARATORS.FIELD_LIST}${nestedSelects}` : nestedSelects;
|
|
2890
|
+
}
|
|
2891
|
+
if (!isNotNullish(relSelect) || relSelect.trim().length === 0) {
|
|
2892
|
+
throw new Error(
|
|
2893
|
+
`Select must include at least one field or nested relation for model ${relModel.name}`
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
return relSelect;
|
|
2897
|
+
}
|
|
2898
|
+
function buildWhereParts(whereInput, relModel, relAlias, ctx) {
|
|
2899
|
+
const whereResult = buildWhereClause(whereInput, {
|
|
2900
|
+
alias: relAlias,
|
|
2901
|
+
schemaModels: ctx.schemas,
|
|
2902
|
+
model: relModel,
|
|
2903
|
+
params: ctx.params,
|
|
2904
|
+
isSubquery: true,
|
|
2905
|
+
aliasGen: ctx.aliasGen,
|
|
2906
|
+
dialect: ctx.dialect
|
|
2907
|
+
});
|
|
2908
|
+
const joins = whereResult.joins.join(" ");
|
|
2909
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? ` ${SQL_TEMPLATES.AND} ${whereResult.clause}` : "";
|
|
2910
|
+
return { joins, whereClause };
|
|
2911
|
+
}
|
|
2912
|
+
function limitOneSql(sql, params, skipVal, scope) {
|
|
2913
|
+
if (isNotNullish(skipVal)) {
|
|
2914
|
+
const skipPh = addAutoScoped(params, skipVal, `${scope}.skip`);
|
|
2915
|
+
return `${sql} ${SQL_TEMPLATES.LIMIT} 1 ${SQL_TEMPLATES.OFFSET} ${skipPh}`;
|
|
2916
|
+
}
|
|
2917
|
+
return `${sql} ${SQL_TEMPLATES.LIMIT} 1`;
|
|
2918
|
+
}
|
|
2919
|
+
function buildOrderBySql(finalOrderByInput, relAlias, dialect) {
|
|
2920
|
+
return isNotNullish(finalOrderByInput) ? buildOrderBy(finalOrderByInput, relAlias, dialect) : "";
|
|
2921
|
+
}
|
|
2922
|
+
function buildBaseSql(args) {
|
|
2923
|
+
return `${SQL_TEMPLATES.SELECT} ${args.selectExpr} ${SQL_TEMPLATES.FROM} ${args.relTable} ${args.relAlias} ${args.joins} ${SQL_TEMPLATES.WHERE} ${args.joinPredicate}${args.whereClause}`;
|
|
2924
|
+
}
|
|
2925
|
+
function buildOneToOneIncludeSql(args) {
|
|
2926
|
+
const objExpr = jsonBuildObject(args.relSelect, args.ctx.dialect);
|
|
2927
|
+
let sql = buildBaseSql({
|
|
2928
|
+
selectExpr: objExpr,
|
|
2929
|
+
relTable: args.relTable,
|
|
2930
|
+
relAlias: args.relAlias,
|
|
2931
|
+
joins: args.joins,
|
|
2932
|
+
joinPredicate: args.joinPredicate,
|
|
2933
|
+
whereClause: args.whereClause
|
|
2934
|
+
});
|
|
2935
|
+
if (args.orderBySql) {
|
|
2936
|
+
sql += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
|
|
2937
|
+
}
|
|
2938
|
+
if (isNotNullish(args.takeVal)) {
|
|
2939
|
+
return appendLimitOffset(
|
|
2940
|
+
sql,
|
|
2941
|
+
args.ctx.dialect,
|
|
2942
|
+
args.ctx.params,
|
|
2943
|
+
args.takeVal,
|
|
2944
|
+
args.skipVal,
|
|
2945
|
+
`include.${args.relName}`
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
return limitOneSql(
|
|
2949
|
+
sql,
|
|
2950
|
+
args.ctx.params,
|
|
2951
|
+
args.skipVal,
|
|
2952
|
+
`include.${args.relName}`
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
function buildListIncludeSpec(args) {
|
|
2956
|
+
const rowExpr = jsonBuildObject(args.relSelect, args.ctx.dialect);
|
|
2957
|
+
const noTake = !isNotNullish(args.takeVal);
|
|
2958
|
+
const noSkip = !isNotNullish(args.skipVal);
|
|
2959
|
+
if (args.ctx.dialect === "postgres" && noTake && noSkip) {
|
|
2960
|
+
const selectExpr2 = args.orderBySql ? `json_agg(${rowExpr} ORDER BY ${args.orderBySql})` : `json_agg(${rowExpr})`;
|
|
2961
|
+
const sql2 = buildBaseSql({
|
|
2962
|
+
selectExpr: selectExpr2,
|
|
2963
|
+
relTable: args.relTable,
|
|
2964
|
+
relAlias: args.relAlias,
|
|
2965
|
+
joins: args.joins,
|
|
2966
|
+
joinPredicate: args.joinPredicate,
|
|
2967
|
+
whereClause: args.whereClause
|
|
2968
|
+
});
|
|
2969
|
+
return Object.freeze({
|
|
2970
|
+
name: args.relName,
|
|
2971
|
+
sql: sql2,
|
|
2972
|
+
isOneToOne: false
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
const rowAlias = args.ctx.aliasGen.next(`${args.relName}_row`);
|
|
2976
|
+
let base = buildBaseSql({
|
|
2977
|
+
selectExpr: `${rowExpr} ${SQL_TEMPLATES.AS} row`,
|
|
2978
|
+
relTable: args.relTable,
|
|
2979
|
+
relAlias: args.relAlias,
|
|
2980
|
+
joins: args.joins,
|
|
2981
|
+
joinPredicate: args.joinPredicate,
|
|
2982
|
+
whereClause: args.whereClause
|
|
2983
|
+
});
|
|
2984
|
+
if (args.orderBySql) {
|
|
2985
|
+
base += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
|
|
2986
|
+
}
|
|
2987
|
+
base = appendLimitOffset(
|
|
2988
|
+
base,
|
|
2989
|
+
args.ctx.dialect,
|
|
2990
|
+
args.ctx.params,
|
|
2991
|
+
args.takeVal,
|
|
2992
|
+
args.skipVal,
|
|
2993
|
+
`include.${args.relName}`
|
|
2994
|
+
);
|
|
2995
|
+
const selectExpr = jsonAgg("row", args.ctx.dialect);
|
|
2996
|
+
const sql = `${SQL_TEMPLATES.SELECT} ${selectExpr} ${SQL_TEMPLATES.FROM} (${base}) ${rowAlias}`;
|
|
2997
|
+
return Object.freeze({
|
|
2998
|
+
name: args.relName,
|
|
2999
|
+
sql,
|
|
3000
|
+
isOneToOne: false
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
3004
|
+
const relTable = getRelationTableReference(relModel, ctx.dialect);
|
|
3005
|
+
const relAlias = ctx.aliasGen.next(relName);
|
|
3006
|
+
const isList = typeof field.type === "string" && field.type.endsWith("[]");
|
|
3007
|
+
const joinPredicate = joinCondition(field, ctx.parentAlias, relAlias);
|
|
3008
|
+
const whereInput = readWhereInput(relArgs);
|
|
3009
|
+
const relSelect = buildSelectWithNestedIncludes(
|
|
3010
|
+
relArgs,
|
|
3011
|
+
relModel,
|
|
3012
|
+
relAlias,
|
|
3013
|
+
ctx
|
|
3014
|
+
);
|
|
3015
|
+
const whereParts = buildWhereParts(whereInput, relModel, relAlias, ctx);
|
|
3016
|
+
const paginationConfig = extractRelationPaginationConfig(relArgs);
|
|
3017
|
+
if (!isList && typeof paginationConfig.takeVal === "number" && paginationConfig.takeVal < 0) {
|
|
3018
|
+
throw new Error("Negative take is only supported for list relations");
|
|
3019
|
+
}
|
|
3020
|
+
const adjusted = maybeReverseNegativeTake(
|
|
3021
|
+
paginationConfig.takeVal,
|
|
3022
|
+
paginationConfig.hasOrderBy,
|
|
3023
|
+
paginationConfig.orderBy
|
|
3024
|
+
);
|
|
3025
|
+
const hasPagination = paginationConfig.hasSkip || paginationConfig.hasTake;
|
|
3026
|
+
const finalOrderByInput = ensureDeterministicOrderBy(
|
|
3027
|
+
relModel,
|
|
3028
|
+
paginationConfig.hasOrderBy,
|
|
3029
|
+
adjusted.orderByInput,
|
|
3030
|
+
hasPagination
|
|
3031
|
+
);
|
|
3032
|
+
const orderBySql = buildOrderBySql(finalOrderByInput, relAlias, ctx.dialect);
|
|
3033
|
+
if (!isList) {
|
|
3034
|
+
const sql = buildOneToOneIncludeSql({
|
|
3035
|
+
relName,
|
|
3036
|
+
relTable,
|
|
3037
|
+
relAlias,
|
|
3038
|
+
joins: whereParts.joins,
|
|
3039
|
+
joinPredicate,
|
|
3040
|
+
whereClause: whereParts.whereClause,
|
|
3041
|
+
orderBySql,
|
|
3042
|
+
relSelect,
|
|
3043
|
+
takeVal: adjusted.takeVal,
|
|
3044
|
+
skipVal: paginationConfig.skipVal,
|
|
3045
|
+
ctx
|
|
3046
|
+
});
|
|
3047
|
+
return Object.freeze({
|
|
3048
|
+
name: relName,
|
|
3049
|
+
sql,
|
|
3050
|
+
isOneToOne: true
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
return buildListIncludeSpec({
|
|
3054
|
+
relName,
|
|
3055
|
+
relTable,
|
|
3056
|
+
relAlias,
|
|
3057
|
+
joins: whereParts.joins,
|
|
3058
|
+
joinPredicate,
|
|
3059
|
+
whereClause: whereParts.whereClause,
|
|
3060
|
+
orderBySql,
|
|
3061
|
+
relSelect,
|
|
3062
|
+
takeVal: adjusted.takeVal,
|
|
3063
|
+
skipVal: paginationConfig.skipVal,
|
|
3064
|
+
ctx
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect) {
|
|
3068
|
+
const includes = [];
|
|
3069
|
+
const entries = relationEntriesFromArgs(args, model);
|
|
3070
|
+
for (const [relName, relArgs] of entries) {
|
|
3071
|
+
if (relArgs === false) continue;
|
|
3072
|
+
const resolved = resolveRelationOrThrow(model, schemas, relName);
|
|
3073
|
+
const include = buildSingleInclude(
|
|
3074
|
+
relName,
|
|
3075
|
+
relArgs,
|
|
3076
|
+
resolved.field,
|
|
3077
|
+
resolved.relModel,
|
|
3078
|
+
{
|
|
3079
|
+
model,
|
|
3080
|
+
schemas,
|
|
3081
|
+
parentAlias,
|
|
3082
|
+
aliasGen,
|
|
3083
|
+
dialect,
|
|
3084
|
+
params
|
|
3085
|
+
}
|
|
3086
|
+
);
|
|
3087
|
+
includes.push(include);
|
|
3088
|
+
}
|
|
3089
|
+
return includes;
|
|
3090
|
+
}
|
|
3091
|
+
function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
3092
|
+
const aliasGen = createAliasGenerator();
|
|
3093
|
+
return buildIncludeSqlInternal(
|
|
3094
|
+
args,
|
|
3095
|
+
model,
|
|
3096
|
+
schemas,
|
|
3097
|
+
parentAlias,
|
|
3098
|
+
aliasGen,
|
|
3099
|
+
params,
|
|
3100
|
+
dialect
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
3104
|
+
const relationSet = getRelationFieldSet(model);
|
|
3105
|
+
if (!relationSet.has(relName)) {
|
|
3106
|
+
throw new Error(
|
|
3107
|
+
`_count.${relName} references unknown relation on model ${model.name}`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
const field = model.fields.find((f) => f.name === relName);
|
|
3111
|
+
if (!field) {
|
|
3112
|
+
throw new Error(
|
|
3113
|
+
`_count.${relName} references unknown relation on model ${model.name}`
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
const relModel = schemas.find((m) => m.name === field.relatedModel);
|
|
3117
|
+
if (!relModel) {
|
|
3118
|
+
throw new Error(
|
|
3119
|
+
`Related model '${field.relatedModel}' not found for _count.${relName}`
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
return { field, relModel };
|
|
3123
|
+
}
|
|
3124
|
+
function groupByColForCount(field, countAlias) {
|
|
3125
|
+
const fkFields = normalizeKeyList(field.foreignKey);
|
|
3126
|
+
const refFields = normalizeKeyList(field.references);
|
|
3127
|
+
return field.isForeignKeyLocal ? `${countAlias}.${quote(refFields[0] || "id")}` : `${countAlias}.${quote(fkFields[0])}`;
|
|
3128
|
+
}
|
|
3129
|
+
function leftJoinOnForCount(field, parentAlias, joinAlias) {
|
|
3130
|
+
const fkFields = normalizeKeyList(field.foreignKey);
|
|
3131
|
+
const refFields = normalizeKeyList(field.references);
|
|
3132
|
+
return field.isForeignKeyLocal ? `${joinAlias}.__fk = ${parentAlias}.${quote(fkFields[0])}` : `${joinAlias}.__fk = ${parentAlias}.${quote(refFields[0] || "id")}`;
|
|
3133
|
+
}
|
|
3134
|
+
function subqueryForCount(dialect, relTable, countAlias, groupByCol) {
|
|
3135
|
+
return dialect === "postgres" ? `(SELECT ${groupByCol} AS __fk, COUNT(*)::int AS __cnt FROM ${relTable} ${countAlias} GROUP BY ${groupByCol})` : `(SELECT ${groupByCol} AS __fk, COUNT(*) AS __cnt FROM ${relTable} ${countAlias} GROUP BY ${groupByCol})`;
|
|
3136
|
+
}
|
|
3137
|
+
function buildCountJoinAndPair(args) {
|
|
3138
|
+
const relTable = getRelationTableReference(args.relModel, args.dialect);
|
|
3139
|
+
const countAlias = `__tp_cnt_${args.relName}`;
|
|
3140
|
+
const groupByCol = groupByColForCount(args.field, countAlias);
|
|
3141
|
+
const subquery = subqueryForCount(
|
|
3142
|
+
args.dialect,
|
|
3143
|
+
relTable,
|
|
3144
|
+
countAlias,
|
|
3145
|
+
groupByCol
|
|
3146
|
+
);
|
|
3147
|
+
const joinAlias = `__tp_cnt_j_${args.relName}`;
|
|
3148
|
+
const leftJoinOn = leftJoinOnForCount(args.field, args.parentAlias, joinAlias);
|
|
3149
|
+
return {
|
|
3150
|
+
joinSql: `LEFT JOIN ${subquery} ${joinAlias} ON ${leftJoinOn}`,
|
|
3151
|
+
pairSql: `${sqlStringLiteral(args.relName)}, COALESCE(${joinAlias}.__cnt, 0)`
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params, dialect) {
|
|
3155
|
+
const joins = [];
|
|
3156
|
+
const pairs = [];
|
|
3157
|
+
for (const [relName, shouldCount] of Object.entries(countSelect)) {
|
|
3158
|
+
if (!shouldCount) continue;
|
|
3159
|
+
const resolved = resolveCountRelationOrThrow(relName, model, schemas);
|
|
3160
|
+
const built = buildCountJoinAndPair({
|
|
3161
|
+
relName,
|
|
3162
|
+
field: resolved.field,
|
|
3163
|
+
relModel: resolved.relModel,
|
|
3164
|
+
parentAlias,
|
|
3165
|
+
dialect
|
|
3166
|
+
});
|
|
3167
|
+
joins.push(built.joinSql);
|
|
3168
|
+
pairs.push(built.pairSql);
|
|
3169
|
+
}
|
|
3170
|
+
return {
|
|
3171
|
+
joins,
|
|
3172
|
+
jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST)
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// src/builder/select/assembly.ts
|
|
3177
|
+
var SIMPLE_SELECT_RE_CACHE = /* @__PURE__ */ new Map();
|
|
3178
|
+
function joinNonEmpty(parts, sep) {
|
|
3179
|
+
return parts.filter((s) => s.trim().length > 0).join(sep);
|
|
3180
|
+
}
|
|
3181
|
+
function buildWhereSql(conditions) {
|
|
3182
|
+
if (!isNonEmptyArray(conditions)) return "";
|
|
3183
|
+
return ` ${SQL_TEMPLATES.WHERE} ${conditions.join(SQL_SEPARATORS.CONDITION_AND)}`;
|
|
3184
|
+
}
|
|
3185
|
+
function buildJoinsSql(...joinGroups) {
|
|
3186
|
+
const all = [];
|
|
3187
|
+
for (const g of joinGroups) {
|
|
3188
|
+
if (isNonEmptyArray(g)) all.push(...g);
|
|
3189
|
+
}
|
|
3190
|
+
return all.length > 0 ? ` ${all.join(" ")}` : "";
|
|
3191
|
+
}
|
|
3192
|
+
function buildSelectList(baseSelect, extraCols) {
|
|
3193
|
+
const base = baseSelect.trim();
|
|
3194
|
+
const extra = extraCols.trim();
|
|
3195
|
+
if (base && extra) return `${base}${SQL_SEPARATORS.FIELD_LIST}${extra}`;
|
|
3196
|
+
return base || extra;
|
|
3197
|
+
}
|
|
3198
|
+
function finalizeSql(sql, params) {
|
|
3199
|
+
const snapshot = params.snapshot();
|
|
3200
|
+
validateSelectQuery(sql);
|
|
3201
|
+
validateParamConsistency(sql, snapshot.params);
|
|
3202
|
+
return Object.freeze({
|
|
3203
|
+
sql,
|
|
3204
|
+
params: snapshot.params,
|
|
3205
|
+
paramMappings: snapshot.mappings
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
function parseSimpleScalarSelect(select, alias) {
|
|
3209
|
+
var _a, _b;
|
|
3210
|
+
const raw = select.trim();
|
|
3211
|
+
if (raw.length === 0) return [];
|
|
3212
|
+
let re = SIMPLE_SELECT_RE_CACHE.get(alias);
|
|
3213
|
+
if (!re) {
|
|
3214
|
+
const safeAlias2 = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3215
|
+
re = new RegExp(`^${safeAlias2}\\.(?:"([^"]+)"|([a-z_][a-z0-9_]*))$`, "i");
|
|
3216
|
+
SIMPLE_SELECT_RE_CACHE.set(alias, re);
|
|
3217
|
+
}
|
|
3218
|
+
const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
|
|
3219
|
+
const names = [];
|
|
3220
|
+
for (const part of parts) {
|
|
3221
|
+
const p = part.trim();
|
|
3222
|
+
const m = p.match(re);
|
|
3223
|
+
if (!m) {
|
|
3224
|
+
throw new Error(
|
|
3225
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns. Got: ${p}`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
const name = ((_b = (_a = m[1]) != null ? _a : m[2]) != null ? _b : "").trim();
|
|
3229
|
+
if (name.length === 0) {
|
|
3230
|
+
throw new Error(`Failed to parse selected column name from: ${p}`);
|
|
3231
|
+
}
|
|
3232
|
+
names.push(name);
|
|
3233
|
+
}
|
|
3234
|
+
return names;
|
|
3235
|
+
}
|
|
3236
|
+
function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
|
|
3237
|
+
const needle = `${fromAlias}.`;
|
|
3238
|
+
const replacement = `${outerAlias}.`;
|
|
3239
|
+
return orderBy.split(needle).join(replacement);
|
|
3240
|
+
}
|
|
3241
|
+
function buildDistinctColumns(distinct, fromAlias) {
|
|
3242
|
+
return distinct.map((f) => col(fromAlias, f)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3243
|
+
}
|
|
3244
|
+
function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
3245
|
+
const outputCols = [...scalarNames, ...includeNames];
|
|
3246
|
+
if (hasCount) {
|
|
3247
|
+
outputCols.push("_count");
|
|
3248
|
+
}
|
|
3249
|
+
const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3250
|
+
if (!isNonEmptyString(formatted)) {
|
|
3251
|
+
throw new Error("distinct emulation requires at least one output column");
|
|
3252
|
+
}
|
|
3253
|
+
return formatted;
|
|
3254
|
+
}
|
|
3255
|
+
function buildWindowOrder(args) {
|
|
3256
|
+
const { baseOrder, idField, fromAlias } = args;
|
|
3257
|
+
const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
|
|
3258
|
+
const hasIdInOrder = orderFields.some(
|
|
3259
|
+
(f) => f.startsWith(`${fromAlias}.id `) || f.startsWith(`${fromAlias}."id" `)
|
|
3260
|
+
);
|
|
3261
|
+
if (hasIdInOrder) return baseOrder;
|
|
3262
|
+
const idTiebreaker = idField ? `, ${col(fromAlias, "id")} ASC` : "";
|
|
3263
|
+
return `${baseOrder}${idTiebreaker}`;
|
|
3264
|
+
}
|
|
3265
|
+
function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
3266
|
+
var _a, _b;
|
|
3267
|
+
const { includes, from, whereClause, whereJoins, orderBy, distinct, model } = spec;
|
|
3268
|
+
if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) {
|
|
3269
|
+
throw new Error("buildSqliteDistinctQuery requires distinct fields");
|
|
3270
|
+
}
|
|
3271
|
+
const scalarNames = parseSimpleScalarSelect(spec.select, from.alias);
|
|
3272
|
+
const includeNames = includes.map((i) => i.name);
|
|
3273
|
+
const hasCount = Boolean((_b = (_a = spec.args) == null ? void 0 : _a.select) == null ? void 0 : _b._count);
|
|
3274
|
+
const outerSelectCols = buildOutputColumns(
|
|
3275
|
+
scalarNames,
|
|
3276
|
+
includeNames,
|
|
3277
|
+
hasCount
|
|
3278
|
+
);
|
|
3279
|
+
const distinctCols = buildDistinctColumns([...distinct], from.alias);
|
|
3280
|
+
const fallbackOrder = [...distinct].map((f) => `${col(from.alias, f)} ASC`).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3281
|
+
const idField = model.fields.find((f) => f.name === "id" && !f.isRelation);
|
|
3282
|
+
const baseOrder = isNonEmptyString(orderBy) ? orderBy : fallbackOrder;
|
|
3283
|
+
const windowOrder = buildWindowOrder({
|
|
3284
|
+
baseOrder,
|
|
3285
|
+
idField,
|
|
3286
|
+
fromAlias: from.alias
|
|
3287
|
+
});
|
|
3288
|
+
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, `"__tp_distinct"`) : replaceOrderByAlias(fallbackOrder, from.alias, `"__tp_distinct"`);
|
|
3289
|
+
const joins = buildJoinsSql(whereJoins, countJoins);
|
|
3290
|
+
const conditions = [];
|
|
3291
|
+
if (whereClause && whereClause !== "1=1") {
|
|
3292
|
+
conditions.push(whereClause);
|
|
3293
|
+
}
|
|
3294
|
+
const whereSql = buildWhereSql(conditions);
|
|
3295
|
+
const innerSelectList = selectWithIncludes.trim();
|
|
3296
|
+
const innerComma = innerSelectList.length > 0 ? SQL_SEPARATORS.FIELD_LIST : "";
|
|
3297
|
+
const inner = `${SQL_TEMPLATES.SELECT} ${innerSelectList}${innerComma}ROW_NUMBER() OVER (PARTITION BY ${distinctCols} ORDER BY ${windowOrder}) ${SQL_TEMPLATES.AS} "__tp_rn" ${SQL_TEMPLATES.FROM} ${from.table} ${from.alias}${joins}${whereSql}`;
|
|
3298
|
+
const outer = `${SQL_TEMPLATES.SELECT} ${outerSelectCols} ${SQL_TEMPLATES.FROM} (${inner}) ${SQL_TEMPLATES.AS} "__tp_distinct" ${SQL_TEMPLATES.WHERE} "__tp_rn" = 1` + (isNonEmptyString(outerOrder) ? ` ${SQL_TEMPLATES.ORDER_BY} ${outerOrder}` : "");
|
|
3299
|
+
return outer;
|
|
3300
|
+
}
|
|
3301
|
+
function buildIncludeColumns(spec) {
|
|
3302
|
+
var _a, _b;
|
|
3303
|
+
const { select, includes, dialect, model, schemas, from, params } = spec;
|
|
3304
|
+
const baseSelect = (select != null ? select : "").trim();
|
|
3305
|
+
let countCols = "";
|
|
3306
|
+
let countJoins = [];
|
|
3307
|
+
const countSelect = (_b = (_a = spec.args) == null ? void 0 : _a.select) == null ? void 0 : _b._count;
|
|
3308
|
+
if (countSelect) {
|
|
3309
|
+
if (isPlainObject(countSelect) && "select" in countSelect) {
|
|
3310
|
+
const countBuild = buildRelationCountSql(
|
|
3311
|
+
countSelect.select,
|
|
3312
|
+
model,
|
|
3313
|
+
schemas,
|
|
3314
|
+
from.alias,
|
|
3315
|
+
params,
|
|
3316
|
+
dialect
|
|
3317
|
+
);
|
|
3318
|
+
if (countBuild.jsonPairs) {
|
|
3319
|
+
countCols = `${jsonBuildObject(countBuild.jsonPairs, dialect)} ${SQL_TEMPLATES.AS} ${quote("_count")}`;
|
|
3320
|
+
}
|
|
3321
|
+
countJoins = countBuild.joins;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
const hasIncludes = isNonEmptyArray(includes);
|
|
3325
|
+
const hasCountCols = isNonEmptyString(countCols);
|
|
3326
|
+
if (!hasIncludes && !hasCountCols) {
|
|
3327
|
+
return { includeCols: "", selectWithIncludes: baseSelect, countJoins: [] };
|
|
3328
|
+
}
|
|
3329
|
+
const emptyJson = dialect === "postgres" ? `'[]'::json` : `json('[]')`;
|
|
3330
|
+
const includeCols = hasIncludes ? includes.map((inc) => {
|
|
3331
|
+
const expr = inc.isOneToOne ? `(${inc.sql})` : `COALESCE((${inc.sql}), ${emptyJson})`;
|
|
3332
|
+
return `${expr} ${SQL_TEMPLATES.AS} ${quote(inc.name)}`;
|
|
3333
|
+
}).join(SQL_SEPARATORS.FIELD_LIST) : "";
|
|
3334
|
+
const allCols = joinNonEmpty(
|
|
3335
|
+
[includeCols, countCols],
|
|
3336
|
+
SQL_SEPARATORS.FIELD_LIST
|
|
3337
|
+
);
|
|
3338
|
+
const selectWithIncludes = buildSelectList(baseSelect, allCols);
|
|
3339
|
+
return { includeCols: allCols, selectWithIncludes, countJoins };
|
|
3340
|
+
}
|
|
3341
|
+
function appendPagination(sql, spec) {
|
|
3342
|
+
const { method, pagination, params } = spec;
|
|
3343
|
+
const isFindUniqueOrFirst = method === "findUnique" || method === "findFirst";
|
|
3344
|
+
if (isFindUniqueOrFirst) {
|
|
3345
|
+
const parts2 = [sql, SQL_TEMPLATES.LIMIT, "1"];
|
|
3346
|
+
const hasSkip = isNotNullish(pagination.skip) && (schemaParser.isDynamicParameter(pagination.skip) || typeof pagination.skip === "number" && pagination.skip > 0) && method === "findFirst";
|
|
3347
|
+
if (hasSkip) {
|
|
3348
|
+
const placeholder = addAutoScoped(
|
|
3349
|
+
params,
|
|
3350
|
+
pagination.skip,
|
|
3351
|
+
"root.pagination.skip"
|
|
3352
|
+
);
|
|
3353
|
+
parts2.push(SQL_TEMPLATES.OFFSET, placeholder);
|
|
3354
|
+
}
|
|
3355
|
+
return parts2.join(" ");
|
|
3356
|
+
}
|
|
3357
|
+
const parts = [sql];
|
|
3358
|
+
if (isNotNullish(pagination.take)) {
|
|
3359
|
+
const placeholder = addAutoScoped(
|
|
3360
|
+
params,
|
|
3361
|
+
pagination.take,
|
|
3362
|
+
"root.pagination.take"
|
|
3363
|
+
);
|
|
3364
|
+
parts.push(SQL_TEMPLATES.LIMIT, placeholder);
|
|
3365
|
+
}
|
|
3366
|
+
if (isNotNullish(pagination.skip)) {
|
|
3367
|
+
const placeholder = addAutoScoped(
|
|
3368
|
+
params,
|
|
3369
|
+
pagination.skip,
|
|
3370
|
+
"root.pagination.skip"
|
|
3371
|
+
);
|
|
3372
|
+
parts.push(SQL_TEMPLATES.OFFSET, placeholder);
|
|
3373
|
+
}
|
|
3374
|
+
return parts.join(" ");
|
|
3375
|
+
}
|
|
3376
|
+
function hasWindowDistinct(spec) {
|
|
3377
|
+
const d = spec.distinct;
|
|
3378
|
+
return isNotNullish(d) && isNonEmptyArray(d);
|
|
3379
|
+
}
|
|
3380
|
+
function assertDistinctAllowed(method, enabled) {
|
|
3381
|
+
if (enabled && method !== "findMany") {
|
|
3382
|
+
throw new Error(
|
|
3383
|
+
"distinct is only supported for findMany in this SQL builder"
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
function assertHasSelectFields(baseSelect, includeCols) {
|
|
3388
|
+
if (!isNonEmptyString(baseSelect) && !isNonEmptyString(includeCols)) {
|
|
3389
|
+
throw new Error("SELECT requires at least one selected field or include");
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
function withCountJoins(spec, countJoins, whereJoins) {
|
|
3393
|
+
return __spreadProps(__spreadValues({}, spec), {
|
|
3394
|
+
whereJoins: [...whereJoins || [], ...countJoins || []]
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
function buildPostgresDistinctOnClause(fromAlias, distinct) {
|
|
3398
|
+
if (!isNonEmptyArray(distinct)) return null;
|
|
3399
|
+
const distinctCols = buildDistinctColumns([...distinct], fromAlias);
|
|
3400
|
+
return `${SQL_TEMPLATES.DISTINCT_ON} (${distinctCols})`;
|
|
3401
|
+
}
|
|
3402
|
+
function pushJoinGroups(parts, ...groups) {
|
|
3403
|
+
for (const g of groups) {
|
|
3404
|
+
if (isNonEmptyArray(g)) parts.push(g.join(" "));
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
function buildConditions(whereClause, cursorClause) {
|
|
3408
|
+
const conditions = [];
|
|
3409
|
+
if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
|
|
3410
|
+
if (isNotNullish(cursorClause) && isNonEmptyString(cursorClause))
|
|
3411
|
+
conditions.push(cursorClause);
|
|
3412
|
+
return conditions;
|
|
3413
|
+
}
|
|
3414
|
+
function pushWhere(parts, conditions) {
|
|
3415
|
+
if (!isNonEmptyArray(conditions)) return;
|
|
3416
|
+
parts.push(SQL_TEMPLATES.WHERE, conditions.join(SQL_SEPARATORS.CONDITION_AND));
|
|
3417
|
+
}
|
|
3418
|
+
function constructFinalSql(spec) {
|
|
3419
|
+
const {
|
|
3420
|
+
select,
|
|
3421
|
+
from,
|
|
3422
|
+
whereClause,
|
|
3423
|
+
whereJoins,
|
|
3424
|
+
orderBy,
|
|
3425
|
+
distinct,
|
|
3426
|
+
method,
|
|
3427
|
+
cursorClause,
|
|
3428
|
+
params,
|
|
3429
|
+
dialect
|
|
3430
|
+
} = spec;
|
|
3431
|
+
const useWindowDistinct = hasWindowDistinct(spec);
|
|
3432
|
+
assertDistinctAllowed(method, useWindowDistinct);
|
|
3433
|
+
const { includeCols, selectWithIncludes, countJoins } = buildIncludeColumns(spec);
|
|
3434
|
+
if (useWindowDistinct) {
|
|
3435
|
+
const baseSelect2 = (select != null ? select : "").trim();
|
|
3436
|
+
assertHasSelectFields(baseSelect2, includeCols);
|
|
3437
|
+
const spec2 = withCountJoins(spec, countJoins, whereJoins);
|
|
3438
|
+
let sql2 = buildSqliteDistinctQuery(spec2, selectWithIncludes).trim();
|
|
3439
|
+
sql2 = appendPagination(sql2, spec);
|
|
3440
|
+
return finalizeSql(sql2, params);
|
|
3441
|
+
}
|
|
3442
|
+
const parts = [SQL_TEMPLATES.SELECT];
|
|
3443
|
+
const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct) : null;
|
|
3444
|
+
if (distinctOn) parts.push(distinctOn);
|
|
3445
|
+
const baseSelect = (select != null ? select : "").trim();
|
|
3446
|
+
const fullSelectList = buildSelectList(baseSelect, includeCols);
|
|
3447
|
+
if (!isNonEmptyString(fullSelectList)) {
|
|
3448
|
+
throw new Error("SELECT requires at least one selected field or include");
|
|
3449
|
+
}
|
|
3450
|
+
parts.push(fullSelectList);
|
|
3451
|
+
parts.push(SQL_TEMPLATES.FROM, from.table, from.alias);
|
|
3452
|
+
pushJoinGroups(parts, whereJoins, countJoins);
|
|
3453
|
+
const conditions = buildConditions(whereClause, cursorClause);
|
|
3454
|
+
pushWhere(parts, conditions);
|
|
3455
|
+
if (isNonEmptyString(orderBy)) parts.push(SQL_TEMPLATES.ORDER_BY, orderBy);
|
|
3456
|
+
let sql = parts.join(" ").trim();
|
|
3457
|
+
sql = appendPagination(sql, spec);
|
|
3458
|
+
return finalizeSql(sql, params);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// src/builder/select.ts
|
|
3462
|
+
function normalizeOrderByInput2(orderBy) {
|
|
3463
|
+
return normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
3464
|
+
}
|
|
3465
|
+
function normalizeDistinctFields(distinct) {
|
|
3466
|
+
if (!isNonEmptyArray(distinct)) return [];
|
|
3467
|
+
return distinct.filter((f) => typeof f === "string").map((f) => f.trim()).filter((f) => f.length > 0);
|
|
3468
|
+
}
|
|
3469
|
+
function mapFirstOrderByByField(existing) {
|
|
3470
|
+
const m = /* @__PURE__ */ new Map();
|
|
3471
|
+
for (const obj of existing) {
|
|
3472
|
+
const field = Object.keys(obj)[0];
|
|
3473
|
+
if (field && !m.has(field)) m.set(field, obj);
|
|
3474
|
+
}
|
|
3475
|
+
return m;
|
|
3476
|
+
}
|
|
3477
|
+
function buildPostgresDistinctOrderBy(distinctFields, existing) {
|
|
3478
|
+
var _a;
|
|
3479
|
+
const firstByField = mapFirstOrderByByField(existing);
|
|
3480
|
+
const next = [];
|
|
3481
|
+
for (const f of distinctFields) {
|
|
3482
|
+
next.push((_a = firstByField.get(f)) != null ? _a : { [f]: "asc" });
|
|
3483
|
+
}
|
|
3484
|
+
const distinctSet = new Set(distinctFields);
|
|
3485
|
+
for (const obj of existing) {
|
|
3486
|
+
const field = Object.keys(obj)[0];
|
|
3487
|
+
if (!distinctSet.has(field)) next.push(obj);
|
|
3488
|
+
}
|
|
3489
|
+
return next;
|
|
3490
|
+
}
|
|
3491
|
+
function applyPostgresDistinctOrderBy(args, _model) {
|
|
3492
|
+
const distinctFields = normalizeDistinctFields(args.distinct);
|
|
3493
|
+
if (distinctFields.length === 0) return args;
|
|
3494
|
+
if (!isNotNullish(args.orderBy)) return args;
|
|
3495
|
+
const existing = normalizeOrderByInput2(args.orderBy);
|
|
3496
|
+
if (existing.length === 0) return args;
|
|
3497
|
+
return __spreadProps(__spreadValues({}, args), {
|
|
3498
|
+
orderBy: buildPostgresDistinctOrderBy(distinctFields, existing)
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
function assertScalarFieldOnModel(model, fieldName, ctx) {
|
|
3502
|
+
const f = model.fields.find((x) => x.name === fieldName);
|
|
3503
|
+
if (!f) {
|
|
3504
|
+
throw new Error(
|
|
3505
|
+
`${ctx} references unknown field '${fieldName}' on model ${model.name}`
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
if (f.isRelation) {
|
|
3509
|
+
throw new Error(
|
|
3510
|
+
`${ctx} does not support relation field '${fieldName}' on model ${model.name}`
|
|
3511
|
+
);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function validateDistinct(model, distinct) {
|
|
3515
|
+
if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) return;
|
|
3516
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3517
|
+
for (const raw of distinct) {
|
|
3518
|
+
const f = String(raw).trim();
|
|
3519
|
+
if (f.length === 0) continue;
|
|
3520
|
+
if (seen.has(f)) {
|
|
3521
|
+
throw new Error(`distinct must not contain duplicates (field: '${f}')`);
|
|
3522
|
+
}
|
|
3523
|
+
seen.add(f);
|
|
3524
|
+
assertScalarFieldOnModel(model, f, "distinct");
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
function validateOrderByValue(fieldName, v) {
|
|
3528
|
+
parseOrderByValue(v, fieldName);
|
|
3529
|
+
}
|
|
3530
|
+
function validateOrderBy(model, orderBy) {
|
|
3531
|
+
if (!isNotNullish(orderBy)) return;
|
|
3532
|
+
const items = normalizeOrderByInput2(orderBy);
|
|
3533
|
+
if (items.length === 0) return;
|
|
3534
|
+
for (const it of items) {
|
|
3535
|
+
const entries = Object.entries(it);
|
|
3536
|
+
const fieldName = String(entries[0][0]).trim();
|
|
3537
|
+
if (fieldName.length === 0) {
|
|
3538
|
+
throw new Error("orderBy field name cannot be empty");
|
|
3539
|
+
}
|
|
3540
|
+
assertScalarFieldOnModel(model, fieldName, "orderBy");
|
|
3541
|
+
validateOrderByValue(fieldName, entries[0][1]);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
function validateCursor(model, cursor) {
|
|
3545
|
+
if (!isNotNullish(cursor)) return;
|
|
3546
|
+
if (!isPlainObject(cursor)) {
|
|
3547
|
+
throw new Error("cursor must be an object");
|
|
3548
|
+
}
|
|
3549
|
+
const entries = Object.entries(cursor);
|
|
3550
|
+
if (entries.length === 0) {
|
|
3551
|
+
throw new Error("cursor must have at least one field");
|
|
3552
|
+
}
|
|
3553
|
+
for (const [fieldName] of entries) {
|
|
3554
|
+
const f = String(fieldName).trim();
|
|
3555
|
+
if (f.length === 0) {
|
|
3556
|
+
throw new Error("cursor field name cannot be empty");
|
|
3557
|
+
}
|
|
3558
|
+
assertScalarFieldOnModel(model, f, "cursor");
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
function resolveDialect(dialect) {
|
|
3562
|
+
return dialect != null ? dialect : getGlobalDialect();
|
|
3563
|
+
}
|
|
3564
|
+
function normalizeArgsForNegativeTake(method, args) {
|
|
3565
|
+
if (method !== "findMany") return args;
|
|
3566
|
+
if (typeof args.take !== "number") return args;
|
|
3567
|
+
if (!Number.isInteger(args.take)) return args;
|
|
3568
|
+
if (args.take >= 0) return args;
|
|
3569
|
+
if (!isNotNullish(args.orderBy)) {
|
|
3570
|
+
throw new Error("Negative take requires orderBy for deterministic results");
|
|
3571
|
+
}
|
|
3572
|
+
return __spreadProps(__spreadValues({}, args), {
|
|
3573
|
+
take: Math.abs(args.take),
|
|
3574
|
+
orderBy: reverseOrderByInput(args.orderBy)
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
function normalizeArgsForDialect(dialect, args, model) {
|
|
3578
|
+
if (dialect !== "postgres") return args;
|
|
3579
|
+
return applyPostgresDistinctOrderBy(args);
|
|
3580
|
+
}
|
|
3581
|
+
function buildCursorClauseIfAny(input) {
|
|
3582
|
+
const { cursor, orderBy, tableName, alias, params, dialect } = input;
|
|
3583
|
+
if (!isNotNullish(cursor)) return void 0;
|
|
3584
|
+
return buildCursorCondition(
|
|
3585
|
+
cursor,
|
|
3586
|
+
orderBy,
|
|
3587
|
+
tableName,
|
|
3588
|
+
alias,
|
|
3589
|
+
params,
|
|
3590
|
+
dialect
|
|
3591
|
+
);
|
|
3592
|
+
}
|
|
3593
|
+
function buildSelectSpec(input) {
|
|
3594
|
+
const {
|
|
3595
|
+
method,
|
|
3596
|
+
normalizedArgs,
|
|
3597
|
+
model,
|
|
3598
|
+
schemas,
|
|
3599
|
+
tableName,
|
|
3600
|
+
alias,
|
|
3601
|
+
whereResult,
|
|
3602
|
+
dialect
|
|
3603
|
+
} = input;
|
|
3604
|
+
const selectFields = buildSelectFields(
|
|
3605
|
+
{ select: normalizedArgs.select },
|
|
3606
|
+
model,
|
|
3607
|
+
alias
|
|
3608
|
+
);
|
|
3609
|
+
const orderByClause = buildOrderByClause(normalizedArgs, alias, dialect);
|
|
3610
|
+
const { take, skip, cursor } = getPaginationParams(method, normalizedArgs);
|
|
3611
|
+
const params = createParamStoreFrom(
|
|
3612
|
+
whereResult.params,
|
|
3613
|
+
whereResult.paramMappings,
|
|
3614
|
+
whereResult.nextParamIndex
|
|
3615
|
+
);
|
|
3616
|
+
const includes = buildIncludeSql(
|
|
3617
|
+
normalizedArgs,
|
|
3618
|
+
model,
|
|
3619
|
+
schemas,
|
|
3620
|
+
alias,
|
|
3621
|
+
params,
|
|
3622
|
+
dialect
|
|
3623
|
+
);
|
|
3624
|
+
const cursorClause = buildCursorClauseIfAny({
|
|
3625
|
+
cursor,
|
|
3626
|
+
orderBy: normalizedArgs.orderBy,
|
|
3627
|
+
tableName,
|
|
3628
|
+
alias,
|
|
3629
|
+
params,
|
|
3630
|
+
dialect
|
|
3631
|
+
});
|
|
3632
|
+
return {
|
|
3633
|
+
select: selectFields,
|
|
3634
|
+
includes,
|
|
3635
|
+
from: { table: tableName, alias },
|
|
3636
|
+
whereClause: whereResult.clause,
|
|
3637
|
+
whereJoins: whereResult.joins,
|
|
3638
|
+
orderBy: orderByClause,
|
|
3639
|
+
pagination: { take, skip },
|
|
3640
|
+
distinct: normalizedArgs.distinct,
|
|
3641
|
+
method,
|
|
3642
|
+
cursorClause,
|
|
3643
|
+
params,
|
|
3644
|
+
dialect,
|
|
3645
|
+
model,
|
|
3646
|
+
schemas,
|
|
3647
|
+
args: normalizedArgs
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
function buildSelectSql(input) {
|
|
3651
|
+
const { method, args, model, schemas, from, whereResult, dialect } = input;
|
|
3652
|
+
assertSafeAlias(from.alias);
|
|
3653
|
+
assertSafeTableRef(from.tableName);
|
|
3654
|
+
const dialectToUse = resolveDialect(dialect);
|
|
3655
|
+
const argsForSql = normalizeArgsForNegativeTake(method, args);
|
|
3656
|
+
const normalizedArgs = normalizeArgsForDialect(
|
|
3657
|
+
dialectToUse,
|
|
3658
|
+
argsForSql);
|
|
3659
|
+
validateDistinct(model, normalizedArgs.distinct);
|
|
3660
|
+
validateOrderBy(model, normalizedArgs.orderBy);
|
|
3661
|
+
validateCursor(model, normalizedArgs.cursor);
|
|
3662
|
+
const spec = buildSelectSpec({
|
|
3663
|
+
method,
|
|
3664
|
+
normalizedArgs,
|
|
3665
|
+
model,
|
|
3666
|
+
schemas,
|
|
3667
|
+
tableName: from.tableName,
|
|
3668
|
+
alias: from.alias,
|
|
3669
|
+
whereResult,
|
|
3670
|
+
dialect: dialectToUse
|
|
3671
|
+
});
|
|
3672
|
+
return constructFinalSql(spec);
|
|
3673
|
+
}
|
|
3674
|
+
var MODEL_FIELD_CACHE = /* @__PURE__ */ new WeakMap();
|
|
3675
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Decimal", "BigInt"]);
|
|
3676
|
+
var AGGREGATES = [
|
|
3677
|
+
["_sum", "SUM"],
|
|
3678
|
+
["_avg", "AVG"],
|
|
3679
|
+
["_min", "MIN"],
|
|
3680
|
+
["_max", "MAX"]
|
|
3681
|
+
];
|
|
3682
|
+
var COMPARISON_OPS = {
|
|
3683
|
+
[Ops.EQUALS]: "=",
|
|
3684
|
+
[Ops.NOT]: "<>",
|
|
3685
|
+
[Ops.GT]: ">",
|
|
3686
|
+
[Ops.GTE]: ">=",
|
|
3687
|
+
[Ops.LT]: "<",
|
|
3688
|
+
[Ops.LTE]: "<="
|
|
3689
|
+
};
|
|
3690
|
+
function getModelFieldMap(model) {
|
|
3691
|
+
const cached = MODEL_FIELD_CACHE.get(model);
|
|
3692
|
+
if (cached) return cached;
|
|
3693
|
+
const m = /* @__PURE__ */ new Map();
|
|
3694
|
+
for (const f of model.fields) {
|
|
3695
|
+
m.set(f.name, { name: f.name, type: f.type, isRelation: !!f.isRelation });
|
|
3696
|
+
}
|
|
3697
|
+
MODEL_FIELD_CACHE.set(model, m);
|
|
3698
|
+
return m;
|
|
3699
|
+
}
|
|
3700
|
+
function isTruthySelection(v) {
|
|
3701
|
+
return v === true;
|
|
3702
|
+
}
|
|
3703
|
+
function aggExprForField(aggKey, field, alias) {
|
|
3704
|
+
if (aggKey === "_count") {
|
|
3705
|
+
return field === "_all" ? `COUNT(*)` : `COUNT(${col(alias, field)})`;
|
|
3706
|
+
}
|
|
3707
|
+
if (field === "_all") {
|
|
3708
|
+
throw new Error(`'${aggKey}' does not support '_all'`);
|
|
3709
|
+
}
|
|
3710
|
+
if (aggKey === "_sum") return `SUM(${col(alias, field)})`;
|
|
3711
|
+
if (aggKey === "_avg") return `AVG(${col(alias, field)})`;
|
|
3712
|
+
if (aggKey === "_min") return `MIN(${col(alias, field)})`;
|
|
3713
|
+
return `MAX(${col(alias, field)})`;
|
|
3714
|
+
}
|
|
3715
|
+
function buildComparisonOp(op) {
|
|
3716
|
+
const sqlOp = COMPARISON_OPS[op];
|
|
3717
|
+
if (!sqlOp) {
|
|
3718
|
+
throw new Error(`Unsupported HAVING operator: ${op}`);
|
|
3719
|
+
}
|
|
3720
|
+
return sqlOp;
|
|
3721
|
+
}
|
|
3722
|
+
function addHavingParam(params, op, value) {
|
|
3723
|
+
return addAutoScoped(params, value, `having.${op}`);
|
|
3724
|
+
}
|
|
3725
|
+
function normalizeLogicalValue2(operator, value) {
|
|
3726
|
+
if (Array.isArray(value)) {
|
|
3727
|
+
const out = [];
|
|
3728
|
+
for (const v of value) {
|
|
3729
|
+
if (!isPlainObject(v)) {
|
|
3730
|
+
throw new Error(`${operator} entries must be objects in HAVING`);
|
|
3731
|
+
}
|
|
3732
|
+
out.push(v);
|
|
3733
|
+
}
|
|
3734
|
+
return out;
|
|
3735
|
+
}
|
|
3736
|
+
if (isPlainObject(value)) {
|
|
3737
|
+
return [value];
|
|
3738
|
+
}
|
|
3739
|
+
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
3740
|
+
}
|
|
3741
|
+
function assertScalarField2(model, fieldName, ctx) {
|
|
3742
|
+
const m = getModelFieldMap(model);
|
|
3743
|
+
const field = m.get(fieldName);
|
|
3744
|
+
if (!field) {
|
|
3745
|
+
throw new Error(
|
|
3746
|
+
`${ctx} references unknown field '${fieldName}' on model ${model.name}. Available fields: ${model.fields.map((f) => f.name).join(", ")}`
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
if (field.isRelation) {
|
|
3750
|
+
throw new Error(`${ctx} does not support relation field '${fieldName}'`);
|
|
3751
|
+
}
|
|
3752
|
+
return { name: field.name, type: field.type };
|
|
3753
|
+
}
|
|
3754
|
+
function assertAggregateFieldType(aggKey, fieldType, fieldName, modelName) {
|
|
3755
|
+
const baseType = fieldType.replace(/\[\]|\?/g, "");
|
|
3756
|
+
if ((aggKey === "_sum" || aggKey === "_avg") && !NUMERIC_TYPES.has(baseType)) {
|
|
3757
|
+
throw new Error(
|
|
3758
|
+
`Cannot use ${aggKey} on non-numeric field '${fieldName}' (type: ${fieldType}) on model ${modelName}`
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
function buildNullComparison(expr, op) {
|
|
3763
|
+
if (op === Ops.EQUALS) return `${expr} ${SQL_TEMPLATES.IS_NULL}`;
|
|
3764
|
+
if (op === Ops.NOT) return `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
|
|
3765
|
+
throw new Error(`Operator '${op}' doesn't support null in HAVING`);
|
|
3766
|
+
}
|
|
3767
|
+
function buildInComparison(expr, op, val, params, dialect) {
|
|
3768
|
+
if (schemaParser.isDynamicParameter(val)) {
|
|
3769
|
+
const placeholder2 = addHavingParam(params, op, val);
|
|
3770
|
+
return op === Ops.IN ? inArray(expr, placeholder2, dialect) : notInArray(expr, placeholder2, dialect);
|
|
3771
|
+
}
|
|
3772
|
+
if (!Array.isArray(val)) {
|
|
3773
|
+
throw new Error(`HAVING '${op}' requires array value`);
|
|
3774
|
+
}
|
|
3775
|
+
if (val.length === 0) {
|
|
3776
|
+
return op === Ops.IN ? "0=1" : "1=1";
|
|
3777
|
+
}
|
|
3778
|
+
const paramValue = prepareArrayParam(val, dialect);
|
|
3779
|
+
const placeholder = params.add(paramValue);
|
|
3780
|
+
return op === Ops.IN ? inArray(expr, placeholder, dialect) : notInArray(expr, placeholder, dialect);
|
|
3781
|
+
}
|
|
3782
|
+
function buildBinaryComparison(expr, op, val, params) {
|
|
3783
|
+
const sqlOp = buildComparisonOp(op);
|
|
3784
|
+
const placeholder = addHavingParam(params, op, val);
|
|
3785
|
+
return `${expr} ${sqlOp} ${placeholder}`;
|
|
3786
|
+
}
|
|
3787
|
+
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
3788
|
+
if (val === null) return buildNullComparison(expr, op);
|
|
3789
|
+
if (op === Ops.NOT && isPlainObject(val)) {
|
|
3790
|
+
return buildNotComposite(
|
|
3791
|
+
expr,
|
|
3792
|
+
val,
|
|
3793
|
+
params,
|
|
3794
|
+
dialect,
|
|
3795
|
+
buildSimpleComparison,
|
|
3796
|
+
SQL_SEPARATORS.CONDITION_AND
|
|
3797
|
+
);
|
|
3798
|
+
}
|
|
3799
|
+
if (op === Ops.IN || op === Ops.NOT_IN) {
|
|
3800
|
+
return buildInComparison(expr, op, val, params, dialect);
|
|
3801
|
+
}
|
|
3802
|
+
return buildBinaryComparison(expr, op, val, params);
|
|
3803
|
+
}
|
|
3804
|
+
function isLogicalKey(key) {
|
|
3805
|
+
return key === LogicalOps.AND || key === LogicalOps.OR || key === LogicalOps.NOT;
|
|
3806
|
+
}
|
|
3807
|
+
function isAggregateKey(key) {
|
|
3808
|
+
return key === "_count" || key === "_sum" || key === "_avg" || key === "_min" || key === "_max";
|
|
3809
|
+
}
|
|
3810
|
+
function negateClauses(subClauses) {
|
|
3811
|
+
if (subClauses.length === 1) return `${SQL_TEMPLATES.NOT} ${subClauses[0]}`;
|
|
3812
|
+
return `${SQL_TEMPLATES.NOT} (${subClauses.join(SQL_SEPARATORS.CONDITION_AND)})`;
|
|
3813
|
+
}
|
|
3814
|
+
function combineLogical(key, subClauses) {
|
|
3815
|
+
if (key === LogicalOps.NOT) return negateClauses(subClauses);
|
|
3816
|
+
return subClauses.join(` ${key} `);
|
|
3817
|
+
}
|
|
3818
|
+
function buildLogicalClause2(key, value, alias, params, dialect, model) {
|
|
3819
|
+
const items = normalizeLogicalValue2(key, value);
|
|
3820
|
+
const subClauses = [];
|
|
3821
|
+
for (const it of items) {
|
|
3822
|
+
const c = buildHavingNode(it, alias, params, dialect, model);
|
|
3823
|
+
if (c && c !== "") subClauses.push(`(${c})`);
|
|
3824
|
+
}
|
|
3825
|
+
if (subClauses.length === 0) return "";
|
|
3826
|
+
return combineLogical(key, subClauses);
|
|
3827
|
+
}
|
|
3828
|
+
function buildHavingEntry(key, value, alias, params, dialect, model) {
|
|
3829
|
+
if (isLogicalKey(key)) {
|
|
3830
|
+
const logical = buildLogicalClause2(
|
|
3831
|
+
key,
|
|
3832
|
+
value,
|
|
3833
|
+
alias,
|
|
3834
|
+
params,
|
|
3835
|
+
dialect,
|
|
3836
|
+
model
|
|
3837
|
+
);
|
|
3838
|
+
return logical ? [logical] : [];
|
|
3839
|
+
}
|
|
3840
|
+
if (isAggregateKey(key)) {
|
|
3841
|
+
return buildHavingForAggregateFirstShape(
|
|
3842
|
+
key,
|
|
3843
|
+
value,
|
|
3844
|
+
alias,
|
|
3845
|
+
params,
|
|
3846
|
+
dialect,
|
|
3847
|
+
model
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3850
|
+
return buildHavingForFieldFirstShape(
|
|
3851
|
+
key,
|
|
3852
|
+
value,
|
|
3853
|
+
alias,
|
|
3854
|
+
params,
|
|
3855
|
+
dialect,
|
|
3856
|
+
model
|
|
3857
|
+
);
|
|
3858
|
+
}
|
|
3859
|
+
function buildHavingNode(node, alias, params, dialect, model) {
|
|
3860
|
+
const clauses = [];
|
|
3861
|
+
for (const [key, value] of Object.entries(node)) {
|
|
3862
|
+
const built = buildHavingEntry(key, value, alias, params, dialect, model);
|
|
3863
|
+
for (const c of built) {
|
|
3864
|
+
if (c && c.trim().length > 0) clauses.push(c);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
return clauses.join(SQL_SEPARATORS.CONDITION_AND);
|
|
3868
|
+
}
|
|
3869
|
+
function assertHavingAggTarget(aggKey, field, model) {
|
|
3870
|
+
if (field === "_all") {
|
|
3871
|
+
if (aggKey !== "_count") {
|
|
3872
|
+
throw new Error(`HAVING '${aggKey}' does not support '_all'`);
|
|
3873
|
+
}
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
const f = assertScalarField2(model, field, "HAVING");
|
|
3877
|
+
assertAggregateFieldType(aggKey, f.type, f.name, model.name);
|
|
3878
|
+
}
|
|
3879
|
+
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
3880
|
+
const out = [];
|
|
3881
|
+
for (const [op, val] of Object.entries(filter)) {
|
|
3882
|
+
if (op === "mode") continue;
|
|
3883
|
+
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
3884
|
+
if (built && built.trim().length > 0) out.push(built);
|
|
3885
|
+
}
|
|
3886
|
+
return out;
|
|
3887
|
+
}
|
|
3888
|
+
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
3889
|
+
if (!isPlainObject(target)) return [];
|
|
3890
|
+
const out = [];
|
|
3891
|
+
for (const [field, filter] of Object.entries(target)) {
|
|
3892
|
+
assertHavingAggTarget(aggKey, field, model);
|
|
3893
|
+
if (!isPlainObject(filter) || Object.keys(filter).length === 0) continue;
|
|
3894
|
+
const expr = aggExprForField(aggKey, field, alias);
|
|
3895
|
+
out.push(...buildHavingOpsForExpr(expr, filter, params, dialect));
|
|
3896
|
+
}
|
|
3897
|
+
return out;
|
|
3898
|
+
}
|
|
3899
|
+
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
3900
|
+
if (!isPlainObject(target)) return [];
|
|
3901
|
+
const field = assertScalarField2(model, fieldName, "HAVING");
|
|
3902
|
+
const out = [];
|
|
3903
|
+
const obj = target;
|
|
3904
|
+
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
3905
|
+
for (const aggKey of keys) {
|
|
3906
|
+
const aggFilter = obj[aggKey];
|
|
3907
|
+
if (!isPlainObject(aggFilter)) continue;
|
|
3908
|
+
assertAggregateFieldType(aggKey, field.type, field.name, model.name);
|
|
3909
|
+
const entries = Object.entries(aggFilter);
|
|
3910
|
+
if (entries.length === 0) continue;
|
|
3911
|
+
const expr = aggExprForField(aggKey, fieldName, alias);
|
|
3912
|
+
for (const [op, val] of entries) {
|
|
3913
|
+
if (op === "mode") continue;
|
|
3914
|
+
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
3915
|
+
if (built && built.trim().length > 0) out.push(built);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
return out;
|
|
3919
|
+
}
|
|
3920
|
+
function buildHavingClause(having, alias, params, model, dialect) {
|
|
3921
|
+
if (!isNotNullish(having)) return "";
|
|
3922
|
+
const d = dialect != null ? dialect : getGlobalDialect();
|
|
3923
|
+
if (!isPlainObject(having)) return "";
|
|
3924
|
+
return buildHavingNode(having, alias, params, d, model);
|
|
3925
|
+
}
|
|
3926
|
+
function normalizeCountArg(v) {
|
|
3927
|
+
if (!isNotNullish(v)) return void 0;
|
|
3928
|
+
if (v === true) return true;
|
|
3929
|
+
if (isPlainObject(v)) return v;
|
|
3930
|
+
return void 0;
|
|
3931
|
+
}
|
|
3932
|
+
function pushCountAllField(fields) {
|
|
3933
|
+
fields.push(
|
|
3934
|
+
`${SQL_TEMPLATES.COUNT_ALL} ${SQL_TEMPLATES.AS} ${quote("_count._all")}`
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
function assertCountableScalarField(fieldMap, model, fieldName) {
|
|
3938
|
+
const field = fieldMap.get(fieldName);
|
|
3939
|
+
if (!field) {
|
|
3940
|
+
throw new Error(
|
|
3941
|
+
`Field '${fieldName}' does not exist on model ${model.name}`
|
|
3942
|
+
);
|
|
3943
|
+
}
|
|
3944
|
+
if (field.isRelation) {
|
|
3945
|
+
throw new Error(
|
|
3946
|
+
`Cannot use _count on relation field '${fieldName}' on model ${model.name}`
|
|
3947
|
+
);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
function pushCountField(fields, alias, fieldName) {
|
|
3951
|
+
const outAlias = `_count.${fieldName}`;
|
|
3952
|
+
fields.push(
|
|
3953
|
+
`COUNT(${col(alias, fieldName)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
function addCountFields(fields, countArg, alias, model, fieldMap) {
|
|
3957
|
+
if (!isNotNullish(countArg)) return;
|
|
3958
|
+
if (countArg === true) {
|
|
3959
|
+
pushCountAllField(fields);
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
if (!isPlainObject(countArg)) return;
|
|
3963
|
+
if (countArg._all === true) {
|
|
3964
|
+
pushCountAllField(fields);
|
|
3965
|
+
}
|
|
3966
|
+
const selected = Object.entries(countArg).filter(
|
|
3967
|
+
([f, v]) => f !== "_all" && isTruthySelection(v)
|
|
3968
|
+
);
|
|
3969
|
+
for (const [f] of selected) {
|
|
3970
|
+
assertCountableScalarField(fieldMap, model, f);
|
|
3971
|
+
pushCountField(fields, alias, f);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
function getAggregateSelectionObject(args, agg) {
|
|
3975
|
+
const obj = args[agg];
|
|
3976
|
+
return isPlainObject(obj) ? obj : void 0;
|
|
3977
|
+
}
|
|
3978
|
+
function assertAggregatableScalarField(fieldMap, model, agg, fieldName) {
|
|
3979
|
+
const field = fieldMap.get(fieldName);
|
|
3980
|
+
if (!field) {
|
|
3981
|
+
throw new Error(
|
|
3982
|
+
`Field '${fieldName}' does not exist on model ${model.name}`
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3985
|
+
if (field.isRelation) {
|
|
3986
|
+
throw new Error(
|
|
3987
|
+
`Cannot use ${agg} on relation field '${fieldName}' on model ${model.name}`
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
return field;
|
|
3991
|
+
}
|
|
3992
|
+
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName) {
|
|
3993
|
+
const outAlias = `${agg}.${fieldName}`;
|
|
3994
|
+
fields.push(
|
|
3995
|
+
`${aggFn}(${col(alias, fieldName)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
function addAggregateFields(fields, args, alias, model, fieldMap) {
|
|
3999
|
+
for (const [agg, aggFn] of AGGREGATES) {
|
|
4000
|
+
const obj = getAggregateSelectionObject(args, agg);
|
|
4001
|
+
if (!obj) continue;
|
|
4002
|
+
for (const [fieldName, selection] of Object.entries(obj)) {
|
|
4003
|
+
if (fieldName === "_all")
|
|
4004
|
+
throw new Error(`'${agg}' does not support '_all'`);
|
|
4005
|
+
if (!isTruthySelection(selection)) continue;
|
|
4006
|
+
const field = assertAggregatableScalarField(
|
|
4007
|
+
fieldMap,
|
|
4008
|
+
model,
|
|
4009
|
+
agg,
|
|
4010
|
+
fieldName
|
|
4011
|
+
);
|
|
4012
|
+
assertAggregateFieldType(agg, field.type, fieldName, model.name);
|
|
4013
|
+
pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
function buildAggregateFields(args, alias, model) {
|
|
4018
|
+
const fields = [];
|
|
4019
|
+
const fieldMap = getModelFieldMap(model);
|
|
4020
|
+
const countArg = normalizeCountArg(args._count);
|
|
4021
|
+
addCountFields(fields, countArg, alias, model, fieldMap);
|
|
4022
|
+
addAggregateFields(fields, args, alias, model, fieldMap);
|
|
4023
|
+
return fields;
|
|
4024
|
+
}
|
|
4025
|
+
function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
4026
|
+
assertSafeAlias(alias);
|
|
4027
|
+
assertSafeTableRef(tableName);
|
|
4028
|
+
const aggFields = buildAggregateFields(args, alias, model);
|
|
4029
|
+
if (!isNonEmptyArray(aggFields)) {
|
|
4030
|
+
throw new Error("buildAggregateSql requires at least one aggregate field");
|
|
4031
|
+
}
|
|
4032
|
+
const selectClause = aggFields.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4033
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4034
|
+
const sql = [
|
|
4035
|
+
SQL_TEMPLATES.SELECT,
|
|
4036
|
+
selectClause,
|
|
4037
|
+
SQL_TEMPLATES.FROM,
|
|
4038
|
+
tableName,
|
|
4039
|
+
alias,
|
|
4040
|
+
whereClause
|
|
4041
|
+
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4042
|
+
validateSelectQuery(sql);
|
|
4043
|
+
validateParamConsistency(sql, whereResult.params);
|
|
4044
|
+
return Object.freeze({
|
|
4045
|
+
sql,
|
|
4046
|
+
params: Object.freeze([...whereResult.params]),
|
|
4047
|
+
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
function assertGroupByBy(args, model) {
|
|
4051
|
+
if (!isNotNullish(args.by) || !isNonEmptyArray(args.by)) {
|
|
4052
|
+
throw new Error("buildGroupBySql: by is required and cannot be empty");
|
|
4053
|
+
}
|
|
4054
|
+
const byFields = args.by.map((f) => String(f));
|
|
4055
|
+
const bySet = new Set(byFields);
|
|
4056
|
+
if (bySet.size !== byFields.length) {
|
|
4057
|
+
throw new Error("buildGroupBySql: by must not contain duplicates");
|
|
4058
|
+
}
|
|
4059
|
+
const modelFieldMap = getModelFieldMap(model);
|
|
4060
|
+
for (const f of byFields) {
|
|
4061
|
+
const field = modelFieldMap.get(f);
|
|
4062
|
+
if (!field) {
|
|
4063
|
+
throw new Error(
|
|
4064
|
+
`groupBy.by references unknown field '${f}' on model ${model.name}`
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
if (field.isRelation) {
|
|
4068
|
+
throw new Error(
|
|
4069
|
+
`groupBy.by does not support relation field '${f}' on model ${model.name}`
|
|
4070
|
+
);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
return byFields;
|
|
4074
|
+
}
|
|
4075
|
+
function buildGroupBySelectParts(args, alias, model, byFields) {
|
|
4076
|
+
const groupCols = byFields.map((f) => col(alias, f));
|
|
4077
|
+
const groupFields = groupCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4078
|
+
const aggFields = buildAggregateFields(args, alias, model);
|
|
4079
|
+
const selectFields = isNonEmptyArray(aggFields) ? groupCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : groupCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4080
|
+
return { groupCols, groupFields, selectFields };
|
|
4081
|
+
}
|
|
4082
|
+
function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
4083
|
+
if (!isNotNullish(args.having)) return "";
|
|
4084
|
+
if (!isPlainObject(args.having)) return "";
|
|
4085
|
+
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4086
|
+
if (!h || h.trim().length === 0) return "";
|
|
4087
|
+
return `${SQL_TEMPLATES.HAVING} ${h}`;
|
|
4088
|
+
}
|
|
4089
|
+
function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
4090
|
+
assertSafeAlias(alias);
|
|
4091
|
+
assertSafeTableRef(tableName);
|
|
4092
|
+
const byFields = assertGroupByBy(args, model);
|
|
4093
|
+
const d = getGlobalDialect();
|
|
4094
|
+
const params = createParamStore(whereResult.nextParamIndex);
|
|
4095
|
+
const { groupFields, selectFields } = buildGroupBySelectParts(
|
|
4096
|
+
args,
|
|
4097
|
+
alias,
|
|
4098
|
+
model,
|
|
4099
|
+
byFields
|
|
4100
|
+
);
|
|
4101
|
+
const havingClause = buildGroupByHaving(args, alias, params, model, d);
|
|
4102
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4103
|
+
const sql = [
|
|
4104
|
+
SQL_TEMPLATES.SELECT,
|
|
4105
|
+
selectFields,
|
|
4106
|
+
SQL_TEMPLATES.FROM,
|
|
4107
|
+
tableName,
|
|
4108
|
+
alias,
|
|
4109
|
+
whereClause,
|
|
4110
|
+
SQL_TEMPLATES.GROUP_BY,
|
|
4111
|
+
groupFields,
|
|
4112
|
+
havingClause
|
|
4113
|
+
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4114
|
+
const snapshot = params.snapshot();
|
|
4115
|
+
validateSelectQuery(sql);
|
|
4116
|
+
validateParamConsistency(sql, [...whereResult.params, ...snapshot.params]);
|
|
4117
|
+
return Object.freeze({
|
|
4118
|
+
sql,
|
|
4119
|
+
params: Object.freeze([...whereResult.params, ...snapshot.params]),
|
|
4120
|
+
paramMappings: Object.freeze([
|
|
4121
|
+
...whereResult.paramMappings,
|
|
4122
|
+
...snapshot.mappings
|
|
4123
|
+
])
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
function buildCountSql(whereResult, tableName, alias, skip, dialect) {
|
|
4127
|
+
assertSafeAlias(alias);
|
|
4128
|
+
assertSafeTableRef(tableName);
|
|
4129
|
+
const d = getGlobalDialect();
|
|
4130
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4131
|
+
const params = createParamStore(whereResult.nextParamIndex);
|
|
4132
|
+
const baseSubSelect = [
|
|
4133
|
+
SQL_TEMPLATES.SELECT,
|
|
4134
|
+
"1",
|
|
4135
|
+
SQL_TEMPLATES.FROM,
|
|
4136
|
+
tableName,
|
|
4137
|
+
alias,
|
|
4138
|
+
whereClause
|
|
4139
|
+
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4140
|
+
const normalizedSkip = normalizeSkipLike(skip);
|
|
4141
|
+
const subSelect = applyCountSkip(baseSubSelect, normalizedSkip, params, d);
|
|
4142
|
+
const sql = [
|
|
4143
|
+
SQL_TEMPLATES.SELECT,
|
|
4144
|
+
SQL_TEMPLATES.COUNT_ALL,
|
|
4145
|
+
SQL_TEMPLATES.AS,
|
|
4146
|
+
quote("_count._all"),
|
|
4147
|
+
SQL_TEMPLATES.FROM,
|
|
4148
|
+
`(${subSelect})`,
|
|
4149
|
+
SQL_TEMPLATES.AS,
|
|
4150
|
+
`"sub"`
|
|
4151
|
+
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4152
|
+
validateSelectQuery(sql);
|
|
4153
|
+
const snapshot = params.snapshot();
|
|
4154
|
+
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4155
|
+
validateParamConsistency(sql, mergedParams);
|
|
4156
|
+
return Object.freeze({
|
|
4157
|
+
sql,
|
|
4158
|
+
params: Object.freeze(mergedParams),
|
|
4159
|
+
paramMappings: Object.freeze([
|
|
4160
|
+
...whereResult.paramMappings,
|
|
4161
|
+
...snapshot.mappings
|
|
4162
|
+
])
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
function applyCountSkip(subSelect, normalizedSkip, params, dialect) {
|
|
4166
|
+
const shouldApply = schemaParser.isDynamicParameter(normalizedSkip) || typeof normalizedSkip === "number" && normalizedSkip > 0;
|
|
4167
|
+
if (!shouldApply) return subSelect;
|
|
4168
|
+
const placeholder = addAutoScoped(params, normalizedSkip, "count.skip");
|
|
4169
|
+
if (dialect === "sqlite") {
|
|
4170
|
+
return `${subSelect} ${SQL_TEMPLATES.LIMIT} -1 ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4171
|
+
}
|
|
4172
|
+
return `${subSelect} ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4173
|
+
}
|
|
4174
|
+
function safeAlias(input) {
|
|
4175
|
+
const raw = String(input).toLowerCase();
|
|
4176
|
+
const cleaned = raw.replace(/[^a-z0-9_]/g, "_");
|
|
4177
|
+
const startsOk = /^[a-z_]/.test(cleaned);
|
|
4178
|
+
let base = startsOk ? cleaned : `_${cleaned}`;
|
|
4179
|
+
base = base.length > 0 ? base : "_t";
|
|
4180
|
+
if (SQL_RESERVED_WORDS.has(base)) {
|
|
4181
|
+
base = `_${base}`;
|
|
4182
|
+
}
|
|
4183
|
+
return base;
|
|
4184
|
+
}
|
|
4185
|
+
function isPrismaMethod(v) {
|
|
4186
|
+
return v === "findMany" || v === "findFirst" || v === "findUnique" || v === "aggregate" || v === "groupBy" || v === "count";
|
|
4187
|
+
}
|
|
4188
|
+
function getMethodFromProcessed(processed) {
|
|
4189
|
+
const maybe = processed == null ? void 0 : processed.method;
|
|
4190
|
+
if (isPrismaMethod(maybe)) return maybe;
|
|
4191
|
+
return "findMany";
|
|
4192
|
+
}
|
|
4193
|
+
function buildSqlResult(args) {
|
|
4194
|
+
const {
|
|
4195
|
+
method,
|
|
4196
|
+
processed,
|
|
4197
|
+
whereResult,
|
|
4198
|
+
tableName,
|
|
4199
|
+
alias,
|
|
4200
|
+
modelDef,
|
|
4201
|
+
schemaModels,
|
|
4202
|
+
dialect
|
|
4203
|
+
} = args;
|
|
4204
|
+
if (method === "aggregate") {
|
|
4205
|
+
return buildAggregateSql(processed, whereResult, tableName, alias, modelDef);
|
|
4206
|
+
}
|
|
4207
|
+
if (method === "groupBy") {
|
|
4208
|
+
return buildGroupBySql(processed, whereResult, tableName, alias, modelDef);
|
|
4209
|
+
}
|
|
4210
|
+
if (method === "count") {
|
|
4211
|
+
return buildCountSql(
|
|
4212
|
+
whereResult,
|
|
4213
|
+
tableName,
|
|
4214
|
+
alias,
|
|
4215
|
+
processed.skip
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
return buildSelectSql({
|
|
4219
|
+
method,
|
|
4220
|
+
args: processed,
|
|
4221
|
+
model: modelDef,
|
|
4222
|
+
schemas: schemaModels,
|
|
4223
|
+
from: { tableName, alias },
|
|
4224
|
+
whereResult,
|
|
4225
|
+
dialect
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
function normalizeSqlAndMappingsForDialect(sql, paramMappings, dialect) {
|
|
4229
|
+
if (dialect !== "sqlite") return { sql, paramMappings };
|
|
4230
|
+
const byIndex = /* @__PURE__ */ new Map();
|
|
4231
|
+
for (const m of paramMappings) byIndex.set(m.index, m);
|
|
4232
|
+
const placeholderPositions = [];
|
|
4233
|
+
const normalizedSql = sql.replace(/\$(\d+)/g, (_, num) => {
|
|
4234
|
+
placeholderPositions.push(parseInt(num, 10));
|
|
4235
|
+
return "?";
|
|
4236
|
+
});
|
|
4237
|
+
const expandedMappings = placeholderPositions.map(
|
|
4238
|
+
(originalIndex, i) => {
|
|
4239
|
+
const originalMapping = byIndex.get(originalIndex);
|
|
4240
|
+
if (!originalMapping) {
|
|
4241
|
+
throw new Error(
|
|
4242
|
+
`CRITICAL: No mapping found for parameter $${originalIndex}`
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
return {
|
|
4246
|
+
index: i + 1,
|
|
4247
|
+
value: originalMapping.value,
|
|
4248
|
+
dynamicName: originalMapping.dynamicName
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
);
|
|
4252
|
+
return { sql: normalizedSql, paramMappings: expandedMappings };
|
|
4253
|
+
}
|
|
4254
|
+
function buildParamsFromMappings(mappings) {
|
|
4255
|
+
const sorted = [...mappings].sort((a, b) => a.index - b.index);
|
|
4256
|
+
return sorted.reduce(
|
|
4257
|
+
(acc, m) => {
|
|
4258
|
+
if (m.dynamicName !== void 0) {
|
|
4259
|
+
acc.dynamicKeys.push(m.dynamicName);
|
|
4260
|
+
return acc;
|
|
4261
|
+
}
|
|
4262
|
+
if (m.value !== void 0) {
|
|
4263
|
+
acc.staticParams.push(m.value);
|
|
4264
|
+
return acc;
|
|
4265
|
+
}
|
|
4266
|
+
throw new Error(
|
|
4267
|
+
`CRITICAL: ParamMap ${m.index} has neither dynamicName nor value`
|
|
4268
|
+
);
|
|
4269
|
+
},
|
|
4270
|
+
{ staticParams: [], dynamicKeys: [] }
|
|
4271
|
+
);
|
|
4272
|
+
}
|
|
4273
|
+
function resolveModelContext(directive) {
|
|
4274
|
+
const { model, datamodel } = directive.context;
|
|
4275
|
+
const schemaModels = schemaParser.convertDMMFToModels(datamodel);
|
|
4276
|
+
const modelDef = getModelByName(schemaModels, model.name);
|
|
4277
|
+
if (!modelDef) throw new Error(`Model ${model.name} not found in schema`);
|
|
4278
|
+
return { schemaModels, modelDef };
|
|
4279
|
+
}
|
|
4280
|
+
function buildMainTableAndAlias(args) {
|
|
4281
|
+
const { modelDef, dialect } = args;
|
|
4282
|
+
const baseName = modelDef.tableName || modelDef.name;
|
|
4283
|
+
return {
|
|
4284
|
+
tableName: buildTableReference(
|
|
4285
|
+
SQL_TEMPLATES.PUBLIC_SCHEMA,
|
|
4286
|
+
baseName,
|
|
4287
|
+
dialect
|
|
4288
|
+
),
|
|
4289
|
+
alias: safeAlias(`${baseName}_main`)
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
function buildMainWhere(args) {
|
|
4293
|
+
const { processed, alias, schemaModels, modelDef, dialect } = args;
|
|
4294
|
+
return buildWhereClause(processed.where || {}, {
|
|
4295
|
+
alias,
|
|
4296
|
+
schemaModels,
|
|
4297
|
+
model: modelDef,
|
|
4298
|
+
path: ["where"],
|
|
4299
|
+
isSubquery: false,
|
|
4300
|
+
dialect
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
function buildAndNormalizeSql(args) {
|
|
4304
|
+
const {
|
|
4305
|
+
processed,
|
|
4306
|
+
whereResult,
|
|
4307
|
+
tableName,
|
|
4308
|
+
alias,
|
|
4309
|
+
modelDef,
|
|
4310
|
+
schemaModels,
|
|
4311
|
+
dialect
|
|
4312
|
+
} = args;
|
|
4313
|
+
const method = getMethodFromProcessed(processed);
|
|
4314
|
+
const sqlResult = buildSqlResult({
|
|
4315
|
+
method,
|
|
4316
|
+
processed,
|
|
4317
|
+
whereResult,
|
|
4318
|
+
tableName,
|
|
4319
|
+
alias,
|
|
4320
|
+
modelDef,
|
|
4321
|
+
schemaModels,
|
|
4322
|
+
dialect
|
|
4323
|
+
});
|
|
4324
|
+
return normalizeSqlAndMappingsForDialect(
|
|
4325
|
+
sqlResult.sql,
|
|
4326
|
+
sqlResult.paramMappings,
|
|
4327
|
+
dialect
|
|
4328
|
+
);
|
|
4329
|
+
}
|
|
4330
|
+
function finalizeDirective(args) {
|
|
4331
|
+
const { directive, normalizedSql, normalizedMappings } = args;
|
|
4332
|
+
validateSqlPositions(normalizedSql, normalizedMappings, getGlobalDialect());
|
|
4333
|
+
const { staticParams, dynamicKeys } = buildParamsFromMappings(normalizedMappings);
|
|
4334
|
+
return {
|
|
4335
|
+
header: directive.header,
|
|
4336
|
+
sql: normalizedSql,
|
|
4337
|
+
staticParams,
|
|
4338
|
+
dynamicKeys,
|
|
4339
|
+
paramMappings: normalizedMappings,
|
|
4340
|
+
originalDirective: directive
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
function generateSQL(directive) {
|
|
4344
|
+
const { query } = directive;
|
|
4345
|
+
const { schemaModels, modelDef } = resolveModelContext(directive);
|
|
4346
|
+
const dialect = getGlobalDialect();
|
|
4347
|
+
const { tableName, alias } = buildMainTableAndAlias({ modelDef, dialect });
|
|
4348
|
+
const whereResult = buildMainWhere({
|
|
4349
|
+
processed: query.processed,
|
|
4350
|
+
alias,
|
|
4351
|
+
schemaModels,
|
|
4352
|
+
modelDef,
|
|
4353
|
+
dialect
|
|
4354
|
+
});
|
|
4355
|
+
const normalized = buildAndNormalizeSql({
|
|
4356
|
+
processed: query.processed,
|
|
4357
|
+
whereResult,
|
|
4358
|
+
tableName,
|
|
4359
|
+
alias,
|
|
4360
|
+
modelDef,
|
|
4361
|
+
schemaModels,
|
|
4362
|
+
dialect
|
|
4363
|
+
});
|
|
4364
|
+
return finalizeDirective({
|
|
4365
|
+
directive,
|
|
4366
|
+
normalizedSql: normalized.sql,
|
|
4367
|
+
normalizedMappings: normalized.paramMappings
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
function generateSQL2(directive) {
|
|
4371
|
+
return generateSQL(directive);
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// src/code-emitter.ts
|
|
4375
|
+
function generateClient(options) {
|
|
4376
|
+
return __async(this, null, function* () {
|
|
4377
|
+
const { datamodel, outputDir, config } = options;
|
|
4378
|
+
setGlobalDialect(config.dialect);
|
|
4379
|
+
const models = schemaParser.convertDMMFToModels(datamodel);
|
|
4380
|
+
const directiveResults = schemaParser.processAllDirectives(
|
|
4381
|
+
datamodel.models,
|
|
4382
|
+
datamodel,
|
|
4383
|
+
{
|
|
4384
|
+
skipInvalid: config.skipInvalid
|
|
4385
|
+
}
|
|
4386
|
+
);
|
|
4387
|
+
const queries = /* @__PURE__ */ new Map();
|
|
4388
|
+
for (const [modelName, result] of directiveResults) {
|
|
4389
|
+
if (result.directives.length === 0) continue;
|
|
4390
|
+
const modelQueries = /* @__PURE__ */ new Map();
|
|
4391
|
+
for (const directive of result.directives) {
|
|
4392
|
+
try {
|
|
4393
|
+
const sqlDirective = generateSQL2(directive);
|
|
4394
|
+
const queryKey = JSON.stringify(
|
|
4395
|
+
directive.query.original,
|
|
4396
|
+
Object.keys(directive.query.original).sort()
|
|
4397
|
+
);
|
|
4398
|
+
modelQueries.set(queryKey, {
|
|
4399
|
+
sql: sqlDirective.sql,
|
|
4400
|
+
params: sqlDirective.staticParams,
|
|
4401
|
+
dynamicKeys: sqlDirective.dynamicKeys,
|
|
4402
|
+
paramMappings: sqlDirective.paramMappings
|
|
4403
|
+
});
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
if (!config.skipInvalid) throw error;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
if (modelQueries.size > 0) {
|
|
4409
|
+
queries.set(modelName, modelQueries);
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
yield promises.mkdir(outputDir, { recursive: true });
|
|
4413
|
+
const code = generateCode(models, queries, config.dialect);
|
|
4414
|
+
yield promises.writeFile(path.join(outputDir, "index.ts"), code);
|
|
4415
|
+
const totalQueries = Array.from(queries.values()).reduce(
|
|
4416
|
+
(sum, m) => sum + m.size,
|
|
4417
|
+
0
|
|
4418
|
+
);
|
|
4419
|
+
console.log(`\u2713 Generated ${queries.size} models, ${totalQueries} queries`);
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
function generateCode(models, queries, dialect) {
|
|
4423
|
+
return `// Generated by @prisma-sql/generator - DO NOT EDIT
|
|
4424
|
+
import { buildSQL, transformQueryResults, type PrismaMethod } from 'prisma-sql'
|
|
4425
|
+
|
|
4426
|
+
const MODELS = ${JSON.stringify(models, null, 2)}
|
|
4427
|
+
|
|
4428
|
+
const QUERIES = ${formatQueries(queries)}
|
|
4429
|
+
|
|
4430
|
+
const DIALECT = ${JSON.stringify(dialect)}
|
|
4431
|
+
|
|
4432
|
+
function normalizeQuery(args: any): string {
|
|
4433
|
+
if (!args) return '{}'
|
|
4434
|
+
return JSON.stringify(args, Object.keys(args).sort())
|
|
4435
|
+
}
|
|
4436
|
+
|
|
4437
|
+
function extractDynamicParams(args: any, dynamicKeys: string[]): unknown[] {
|
|
4438
|
+
const params: unknown[] = []
|
|
4439
|
+
|
|
4440
|
+
for (const key of dynamicKeys) {
|
|
4441
|
+
const parts = key.split('.')
|
|
4442
|
+
let value: any = args
|
|
4443
|
+
|
|
4444
|
+
for (const part of parts) {
|
|
4445
|
+
if (value === null || value === undefined) break
|
|
4446
|
+
value = value[part]
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
if (value === undefined) {
|
|
4450
|
+
throw new Error(\`Missing required parameter: \${key}\`)
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
params.push(value)
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
return params
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
async function executeQuery(client: any, sql: string, params: unknown[]): Promise<unknown[]> {
|
|
4460
|
+
if (DIALECT === 'postgres') {
|
|
4461
|
+
return await client.unsafe(sql, params)
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
const stmt = client.prepare(sql)
|
|
4465
|
+
|
|
4466
|
+
if (sql.toUpperCase().includes('COUNT(*) AS')) {
|
|
4467
|
+
return [stmt.get(...params)]
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
return stmt.all(...params)
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
export function createExtension(config: {
|
|
4474
|
+
postgres?: any
|
|
4475
|
+
sqlite?: any
|
|
4476
|
+
debug?: boolean
|
|
4477
|
+
onQuery?: (info: {
|
|
4478
|
+
model: string
|
|
4479
|
+
method: string
|
|
4480
|
+
sql: string
|
|
4481
|
+
params: unknown[]
|
|
4482
|
+
duration: number
|
|
4483
|
+
prebaked: boolean
|
|
4484
|
+
}) => void
|
|
4485
|
+
}) {
|
|
4486
|
+
const { postgres, sqlite, debug, onQuery } = config
|
|
4487
|
+
|
|
4488
|
+
if (!postgres && !sqlite) {
|
|
4489
|
+
throw new Error('Extension requires postgres or sqlite client')
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
const client = postgres || sqlite
|
|
4493
|
+
const actualDialect = postgres ? 'postgres' : 'sqlite'
|
|
4494
|
+
|
|
4495
|
+
if (actualDialect !== DIALECT) {
|
|
4496
|
+
throw new Error(\`Generated code is for \${DIALECT}, but you provided \${actualDialect}\`)
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
return (prisma: any) => {
|
|
4500
|
+
const handleMethod = async function(this: any, method: PrismaMethod, args: any) {
|
|
4501
|
+
const modelName = this?.name || this?.$name
|
|
4502
|
+
const startTime = Date.now()
|
|
4503
|
+
|
|
4504
|
+
// Try prebaked query first
|
|
4505
|
+
const queryKey = normalizeQuery(args)
|
|
4506
|
+
const prebakedQuery = QUERIES[modelName]?.[queryKey]
|
|
4507
|
+
|
|
4508
|
+
let sql: string
|
|
4509
|
+
let params: unknown[]
|
|
4510
|
+
let prebaked = false
|
|
4511
|
+
|
|
4512
|
+
if (prebakedQuery) {
|
|
4513
|
+
// \u26A1 Use prebaked SQL
|
|
4514
|
+
sql = prebakedQuery.sql
|
|
4515
|
+
params = [...prebakedQuery.params, ...extractDynamicParams(args, prebakedQuery.dynamicKeys)]
|
|
4516
|
+
prebaked = true
|
|
4517
|
+
} else {
|
|
4518
|
+
// \u{1F528} Fall back to runtime generation
|
|
4519
|
+
const model = MODELS.find((m: any) => m.name === modelName)
|
|
4520
|
+
|
|
4521
|
+
if (!model) {
|
|
4522
|
+
return this.$parent[modelName][method](args)
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
const result = buildSQL(model, MODELS, method, args, DIALECT)
|
|
4526
|
+
sql = result.sql
|
|
4527
|
+
params = result.params
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
if (debug) {
|
|
4531
|
+
console.log(\`[\${DIALECT}] \${modelName}.\${method} \${prebaked ? '\u26A1 PREBAKED' : '\u{1F528} RUNTIME'}\`)
|
|
4532
|
+
console.log('SQL:', sql)
|
|
4533
|
+
console.log('Params:', params)
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
const results = await executeQuery(client, sql, params)
|
|
4537
|
+
const duration = Date.now() - startTime
|
|
4538
|
+
|
|
4539
|
+
onQuery?.({
|
|
4540
|
+
model: modelName,
|
|
4541
|
+
method,
|
|
4542
|
+
sql,
|
|
4543
|
+
params,
|
|
4544
|
+
duration,
|
|
4545
|
+
prebaked,
|
|
4546
|
+
})
|
|
4547
|
+
|
|
4548
|
+
return transformQueryResults(method, results)
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
return prisma.$extends({
|
|
4552
|
+
name: 'prisma-sql-generated',
|
|
4553
|
+
|
|
4554
|
+
client: {
|
|
4555
|
+
$original: prisma,
|
|
4556
|
+
},
|
|
4557
|
+
|
|
4558
|
+
model: {
|
|
4559
|
+
$allModels: {
|
|
4560
|
+
async findMany(args: any) {
|
|
4561
|
+
return handleMethod.call(this, 'findMany', args)
|
|
4562
|
+
},
|
|
4563
|
+
async findFirst(args: any) {
|
|
4564
|
+
return handleMethod.call(this, 'findFirst', args)
|
|
4565
|
+
},
|
|
4566
|
+
async findUnique(args: any) {
|
|
4567
|
+
return handleMethod.call(this, 'findUnique', args)
|
|
4568
|
+
},
|
|
4569
|
+
async count(args: any) {
|
|
4570
|
+
return handleMethod.call(this, 'count', args)
|
|
4571
|
+
},
|
|
4572
|
+
async aggregate(args: any) {
|
|
4573
|
+
return handleMethod.call(this, 'aggregate', args)
|
|
4574
|
+
},
|
|
4575
|
+
async groupBy(args: any) {
|
|
4576
|
+
return handleMethod.call(this, 'groupBy', args)
|
|
4577
|
+
},
|
|
4578
|
+
},
|
|
4579
|
+
},
|
|
4580
|
+
})
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
`;
|
|
4584
|
+
}
|
|
4585
|
+
function formatQueries(queries) {
|
|
4586
|
+
const entries = [];
|
|
4587
|
+
for (const [modelName, modelQueries] of queries) {
|
|
4588
|
+
const queryEntries = [];
|
|
4589
|
+
for (const [queryKey, query] of modelQueries) {
|
|
4590
|
+
queryEntries.push(` ${JSON.stringify(queryKey)}: {
|
|
4591
|
+
sql: ${JSON.stringify(query.sql)},
|
|
4592
|
+
params: ${JSON.stringify(query.params)},
|
|
4593
|
+
dynamicKeys: ${JSON.stringify(query.dynamicKeys)},
|
|
4594
|
+
paramMappings: ${JSON.stringify(query.paramMappings)},
|
|
4595
|
+
}`);
|
|
4596
|
+
}
|
|
4597
|
+
entries.push(` ${JSON.stringify(modelName)}: {
|
|
4598
|
+
${queryEntries.join(",\n")}
|
|
4599
|
+
}`);
|
|
4600
|
+
}
|
|
4601
|
+
return `{
|
|
4602
|
+
${entries.join(",\n")}
|
|
4603
|
+
}`;
|
|
4604
|
+
}
|
|
4605
|
+
var { version } = require_package();
|
|
4606
|
+
function getDialectFromProvider(provider) {
|
|
4607
|
+
const normalized = provider.toLowerCase();
|
|
4608
|
+
if (normalized === "sqlite") return "sqlite";
|
|
4609
|
+
if (normalized === "postgresql" || normalized === "postgres")
|
|
4610
|
+
return "postgres";
|
|
4611
|
+
throw new Error(
|
|
4612
|
+
`Unsupported database provider: ${provider}. Supported: postgresql, postgres, sqlite`
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
generatorHelper.generatorHandler({
|
|
4616
|
+
onManifest() {
|
|
4617
|
+
return {
|
|
4618
|
+
version,
|
|
4619
|
+
defaultOutput: "../node_modules/.prisma/client/sql",
|
|
4620
|
+
prettyName: "prisma-sql-generator",
|
|
4621
|
+
requiresGenerators: ["prisma-client-js"]
|
|
4622
|
+
};
|
|
4623
|
+
},
|
|
4624
|
+
onGenerate(options) {
|
|
4625
|
+
return __async(this, null, function* () {
|
|
4626
|
+
var _a;
|
|
4627
|
+
const { generator, dmmf, datasources } = options;
|
|
4628
|
+
if (!datasources || datasources.length === 0) {
|
|
4629
|
+
throw new Error("No datasource found in schema");
|
|
4630
|
+
}
|
|
4631
|
+
const autoDialect = getDialectFromProvider(datasources[0].provider);
|
|
4632
|
+
const configDialect = generator.config.dialect;
|
|
4633
|
+
const dialect = configDialect || autoDialect;
|
|
4634
|
+
if (configDialect && configDialect !== autoDialect) {
|
|
4635
|
+
internals.logger.warn(
|
|
4636
|
+
`Generator dialect (${configDialect}) differs from datasource provider (${datasources[0].provider}). Using generator config: ${configDialect}`
|
|
4637
|
+
);
|
|
4638
|
+
}
|
|
4639
|
+
const config = {
|
|
4640
|
+
dialect,
|
|
4641
|
+
skipInvalid: generator.config.skipInvalid === "true"
|
|
4642
|
+
};
|
|
4643
|
+
const outputDir = ((_a = generator.output) == null ? void 0 : _a.value) || "../node_modules/.prisma/client/sql";
|
|
4644
|
+
internals.logger.info(`Generating SQL client to ${outputDir}`);
|
|
4645
|
+
internals.logger.info(`Datasource: ${datasources[0].provider}`);
|
|
4646
|
+
internals.logger.info(`Dialect: ${config.dialect}`);
|
|
4647
|
+
internals.logger.info(`Skip invalid: ${config.skipInvalid}`);
|
|
4648
|
+
yield generateClient({
|
|
4649
|
+
datamodel: dmmf.datamodel,
|
|
4650
|
+
outputDir,
|
|
4651
|
+
config
|
|
4652
|
+
});
|
|
4653
|
+
internals.logger.info("\u2713 Generated SQL client successfully");
|
|
4654
|
+
});
|
|
4655
|
+
}
|
|
4656
|
+
});
|
|
4657
|
+
//# sourceMappingURL=generator.cjs.map
|
|
4658
|
+
//# sourceMappingURL=generator.cjs.map
|