prisma-sql 1.44.0 → 1.46.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 +916 -786
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +916 -786
- package/dist/generator.js.map +1 -1
- package/dist/index.cjs +851 -770
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +851 -770
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,20 +48,13 @@ var SQL_SEPARATORS = Object.freeze({
|
|
|
48
48
|
CONDITION_OR: " OR ",
|
|
49
49
|
ORDER_BY: ", "
|
|
50
50
|
});
|
|
51
|
-
var
|
|
51
|
+
var ALIAS_FORBIDDEN_KEYWORDS = /* @__PURE__ */ new Set([
|
|
52
52
|
"select",
|
|
53
53
|
"from",
|
|
54
54
|
"where",
|
|
55
|
-
"
|
|
56
|
-
"or",
|
|
57
|
-
"not",
|
|
58
|
-
"in",
|
|
59
|
-
"like",
|
|
60
|
-
"between",
|
|
55
|
+
"having",
|
|
61
56
|
"order",
|
|
62
|
-
"by",
|
|
63
57
|
"group",
|
|
64
|
-
"having",
|
|
65
58
|
"limit",
|
|
66
59
|
"offset",
|
|
67
60
|
"join",
|
|
@@ -69,14 +62,42 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
69
62
|
"left",
|
|
70
63
|
"right",
|
|
71
64
|
"outer",
|
|
72
|
-
"
|
|
65
|
+
"cross",
|
|
66
|
+
"full",
|
|
67
|
+
"and",
|
|
68
|
+
"or",
|
|
69
|
+
"not",
|
|
70
|
+
"by",
|
|
73
71
|
"as",
|
|
72
|
+
"on",
|
|
73
|
+
"union",
|
|
74
|
+
"intersect",
|
|
75
|
+
"except",
|
|
76
|
+
"case",
|
|
77
|
+
"when",
|
|
78
|
+
"then",
|
|
79
|
+
"else",
|
|
80
|
+
"end"
|
|
81
|
+
]);
|
|
82
|
+
var SQL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
83
|
+
...ALIAS_FORBIDDEN_KEYWORDS,
|
|
84
|
+
"user",
|
|
85
|
+
"users",
|
|
74
86
|
"table",
|
|
75
87
|
"column",
|
|
76
88
|
"index",
|
|
77
|
-
"user",
|
|
78
|
-
"users",
|
|
79
89
|
"values",
|
|
90
|
+
"in",
|
|
91
|
+
"like",
|
|
92
|
+
"between",
|
|
93
|
+
"is",
|
|
94
|
+
"exists",
|
|
95
|
+
"null",
|
|
96
|
+
"true",
|
|
97
|
+
"false",
|
|
98
|
+
"all",
|
|
99
|
+
"any",
|
|
100
|
+
"some",
|
|
80
101
|
"update",
|
|
81
102
|
"insert",
|
|
82
103
|
"delete",
|
|
@@ -87,25 +108,9 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
87
108
|
"grant",
|
|
88
109
|
"revoke",
|
|
89
110
|
"exec",
|
|
90
|
-
"execute"
|
|
91
|
-
"union",
|
|
92
|
-
"intersect",
|
|
93
|
-
"except",
|
|
94
|
-
"case",
|
|
95
|
-
"when",
|
|
96
|
-
"then",
|
|
97
|
-
"else",
|
|
98
|
-
"end",
|
|
99
|
-
"null",
|
|
100
|
-
"true",
|
|
101
|
-
"false",
|
|
102
|
-
"is",
|
|
103
|
-
"exists",
|
|
104
|
-
"all",
|
|
105
|
-
"any",
|
|
106
|
-
"some"
|
|
111
|
+
"execute"
|
|
107
112
|
]);
|
|
108
|
-
var
|
|
113
|
+
var SQL_RESERVED_WORDS = SQL_KEYWORDS;
|
|
109
114
|
var DEFAULT_WHERE_CLAUSE = "1=1";
|
|
110
115
|
var SPECIAL_FIELDS = Object.freeze({
|
|
111
116
|
ID: "id"
|
|
@@ -184,12 +189,48 @@ var LIMITS = Object.freeze({
|
|
|
184
189
|
});
|
|
185
190
|
|
|
186
191
|
// src/utils/normalize-value.ts
|
|
187
|
-
|
|
192
|
+
var MAX_DEPTH = 20;
|
|
193
|
+
function normalizeValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
194
|
+
if (depth > MAX_DEPTH) {
|
|
195
|
+
throw new Error(`Max normalization depth exceeded (${MAX_DEPTH} levels)`);
|
|
196
|
+
}
|
|
188
197
|
if (value instanceof Date) {
|
|
198
|
+
const t = value.getTime();
|
|
199
|
+
if (!Number.isFinite(t)) {
|
|
200
|
+
throw new Error("Invalid Date value in SQL params");
|
|
201
|
+
}
|
|
189
202
|
return value.toISOString();
|
|
190
203
|
}
|
|
204
|
+
if (typeof value === "bigint") {
|
|
205
|
+
return value.toString();
|
|
206
|
+
}
|
|
191
207
|
if (Array.isArray(value)) {
|
|
192
|
-
|
|
208
|
+
const arrRef = value;
|
|
209
|
+
if (seen.has(arrRef)) {
|
|
210
|
+
throw new Error("Circular reference in SQL params");
|
|
211
|
+
}
|
|
212
|
+
seen.add(arrRef);
|
|
213
|
+
const out = value.map((v) => normalizeValue(v, seen, depth + 1));
|
|
214
|
+
seen.delete(arrRef);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
if (value && typeof value === "object") {
|
|
218
|
+
if (value instanceof Uint8Array) return value;
|
|
219
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) return value;
|
|
220
|
+
const proto = Object.getPrototypeOf(value);
|
|
221
|
+
const isPlain = proto === Object.prototype || proto === null;
|
|
222
|
+
if (!isPlain) return value;
|
|
223
|
+
const obj = value;
|
|
224
|
+
if (seen.has(obj)) {
|
|
225
|
+
throw new Error("Circular reference in SQL params");
|
|
226
|
+
}
|
|
227
|
+
seen.add(obj);
|
|
228
|
+
const out = {};
|
|
229
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
230
|
+
out[k] = normalizeValue(v, seen, depth + 1);
|
|
231
|
+
}
|
|
232
|
+
seen.delete(obj);
|
|
233
|
+
return out;
|
|
193
234
|
}
|
|
194
235
|
return value;
|
|
195
236
|
}
|
|
@@ -370,7 +411,7 @@ function prepareArrayParam(value, dialect) {
|
|
|
370
411
|
throw new Error("prepareArrayParam requires array value");
|
|
371
412
|
}
|
|
372
413
|
if (dialect === "postgres") {
|
|
373
|
-
return value.map(normalizeValue);
|
|
414
|
+
return value.map((v) => normalizeValue(v));
|
|
374
415
|
}
|
|
375
416
|
return JSON.stringify(value);
|
|
376
417
|
}
|
|
@@ -442,36 +483,46 @@ function createError(message, ctx, code = "VALIDATION_ERROR") {
|
|
|
442
483
|
}
|
|
443
484
|
|
|
444
485
|
// src/builder/shared/model-field-cache.ts
|
|
445
|
-
var
|
|
446
|
-
|
|
486
|
+
var MODEL_CACHE = /* @__PURE__ */ new WeakMap();
|
|
487
|
+
function ensureFullCache(model) {
|
|
488
|
+
let cache = MODEL_CACHE.get(model);
|
|
489
|
+
if (!cache) {
|
|
490
|
+
const fieldInfo = /* @__PURE__ */ new Map();
|
|
491
|
+
const scalarFields = /* @__PURE__ */ new Set();
|
|
492
|
+
const relationFields = /* @__PURE__ */ new Set();
|
|
493
|
+
const columnMap = /* @__PURE__ */ new Map();
|
|
494
|
+
for (const f of model.fields) {
|
|
495
|
+
const info = {
|
|
496
|
+
name: f.name,
|
|
497
|
+
dbName: f.dbName || f.name,
|
|
498
|
+
type: f.type,
|
|
499
|
+
isRelation: !!f.isRelation,
|
|
500
|
+
isRequired: !!f.isRequired
|
|
501
|
+
};
|
|
502
|
+
fieldInfo.set(f.name, info);
|
|
503
|
+
if (info.isRelation) {
|
|
504
|
+
relationFields.add(f.name);
|
|
505
|
+
} else {
|
|
506
|
+
scalarFields.add(f.name);
|
|
507
|
+
columnMap.set(f.name, info.dbName);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
cache = { fieldInfo, scalarFields, relationFields, columnMap };
|
|
511
|
+
MODEL_CACHE.set(model, cache);
|
|
512
|
+
}
|
|
513
|
+
return cache;
|
|
514
|
+
}
|
|
515
|
+
function getFieldInfo(model, fieldName) {
|
|
516
|
+
return ensureFullCache(model).fieldInfo.get(fieldName);
|
|
517
|
+
}
|
|
447
518
|
function getScalarFieldSet(model) {
|
|
448
|
-
|
|
449
|
-
if (cached) return cached;
|
|
450
|
-
const s = /* @__PURE__ */ new Set();
|
|
451
|
-
for (const f of model.fields) if (!f.isRelation) s.add(f.name);
|
|
452
|
-
SCALAR_SET_CACHE.set(model, s);
|
|
453
|
-
return s;
|
|
519
|
+
return ensureFullCache(model).scalarFields;
|
|
454
520
|
}
|
|
455
521
|
function getRelationFieldSet(model) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const s = /* @__PURE__ */ new Set();
|
|
459
|
-
for (const f of model.fields) if (f.isRelation) s.add(f.name);
|
|
460
|
-
RELATION_SET_CACHE.set(model, s);
|
|
461
|
-
return s;
|
|
462
|
-
}
|
|
463
|
-
var COLUMN_MAP_CACHE = /* @__PURE__ */ new WeakMap();
|
|
522
|
+
return ensureFullCache(model).relationFields;
|
|
523
|
+
}
|
|
464
524
|
function getColumnMap(model) {
|
|
465
|
-
|
|
466
|
-
if (cached) return cached;
|
|
467
|
-
const map = /* @__PURE__ */ new Map();
|
|
468
|
-
for (const f of model.fields) {
|
|
469
|
-
if (!f.isRelation) {
|
|
470
|
-
map.set(f.name, f.dbName || f.name);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
COLUMN_MAP_CACHE.set(model, map);
|
|
474
|
-
return map;
|
|
525
|
+
return ensureFullCache(model).columnMap;
|
|
475
526
|
}
|
|
476
527
|
|
|
477
528
|
// src/builder/shared/validators/sql-validators.ts
|
|
@@ -482,20 +533,21 @@ function isEmptyWhere(where) {
|
|
|
482
533
|
if (!isNotNullish(where)) return true;
|
|
483
534
|
return Object.keys(where).length === 0;
|
|
484
535
|
}
|
|
536
|
+
function sqlPreview(sql) {
|
|
537
|
+
const s = String(sql);
|
|
538
|
+
if (s.length <= 160) return s;
|
|
539
|
+
return `${s.slice(0, 160)}...`;
|
|
540
|
+
}
|
|
485
541
|
function validateSelectQuery(sql) {
|
|
486
542
|
if (!hasValidContent(sql)) {
|
|
487
543
|
throw new Error("CRITICAL: Generated empty SQL query");
|
|
488
544
|
}
|
|
489
545
|
if (!hasRequiredKeywords(sql)) {
|
|
490
|
-
throw new Error(
|
|
491
|
-
`CRITICAL: Invalid SQL structure. SQL: ${sql.substring(0, 100)}...`
|
|
492
|
-
);
|
|
546
|
+
throw new Error(`CRITICAL: Invalid SQL structure. SQL: ${sqlPreview(sql)}`);
|
|
493
547
|
}
|
|
494
548
|
}
|
|
495
|
-
function
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
function parseDollarNumber(sql, start, n) {
|
|
549
|
+
function parseDollarNumber(sql, start) {
|
|
550
|
+
const n = sql.length;
|
|
499
551
|
let i = start;
|
|
500
552
|
let num = 0;
|
|
501
553
|
let hasDigit = false;
|
|
@@ -506,14 +558,14 @@ function parseDollarNumber(sql, start, n) {
|
|
|
506
558
|
num = num * 10 + (c - 48);
|
|
507
559
|
i++;
|
|
508
560
|
}
|
|
509
|
-
if (!hasDigit || num <= 0) return { next: i, num: 0
|
|
510
|
-
return { next: i, num
|
|
561
|
+
if (!hasDigit || num <= 0) return { next: i, num: 0 };
|
|
562
|
+
return { next: i, num };
|
|
511
563
|
}
|
|
512
564
|
function scanDollarPlaceholders(sql, markUpTo) {
|
|
513
565
|
const seen = new Uint8Array(markUpTo + 1);
|
|
514
|
-
let count = 0;
|
|
515
566
|
let min = Number.POSITIVE_INFINITY;
|
|
516
567
|
let max = 0;
|
|
568
|
+
let sawAny = false;
|
|
517
569
|
const n = sql.length;
|
|
518
570
|
let i = 0;
|
|
519
571
|
while (i < n) {
|
|
@@ -521,17 +573,21 @@ function scanDollarPlaceholders(sql, markUpTo) {
|
|
|
521
573
|
i++;
|
|
522
574
|
continue;
|
|
523
575
|
}
|
|
524
|
-
const
|
|
525
|
-
i = next;
|
|
526
|
-
|
|
527
|
-
|
|
576
|
+
const parsed = parseDollarNumber(sql, i + 1);
|
|
577
|
+
i = parsed.next;
|
|
578
|
+
const num = parsed.num;
|
|
579
|
+
if (num === 0) continue;
|
|
580
|
+
sawAny = true;
|
|
528
581
|
if (num < min) min = num;
|
|
529
582
|
if (num > max) max = num;
|
|
530
583
|
if (num <= markUpTo) seen[num] = 1;
|
|
531
584
|
}
|
|
532
|
-
|
|
585
|
+
if (!sawAny) {
|
|
586
|
+
return { min: 0, max: 0, seen, sawAny: false };
|
|
587
|
+
}
|
|
588
|
+
return { min, max, seen, sawAny: true };
|
|
533
589
|
}
|
|
534
|
-
function
|
|
590
|
+
function assertNoGapsDollar(scan, rangeMin, rangeMax, sql) {
|
|
535
591
|
for (let k = rangeMin; k <= rangeMax; k++) {
|
|
536
592
|
if (scan.seen[k] !== 1) {
|
|
537
593
|
throw new Error(
|
|
@@ -542,174 +598,75 @@ function assertNoGaps(scan, rangeMin, rangeMax, sql) {
|
|
|
542
598
|
}
|
|
543
599
|
function validateParamConsistency(sql, params) {
|
|
544
600
|
const paramLen = params.length;
|
|
545
|
-
if (paramLen === 0) {
|
|
546
|
-
if (sql.indexOf("$") === -1) return;
|
|
547
|
-
}
|
|
548
601
|
const scan = scanDollarPlaceholders(sql, paramLen);
|
|
549
|
-
if (
|
|
550
|
-
if (
|
|
602
|
+
if (paramLen === 0) {
|
|
603
|
+
if (scan.sawAny) {
|
|
551
604
|
throw new Error(
|
|
552
|
-
`CRITICAL:
|
|
605
|
+
`CRITICAL: SQL contains placeholders but params is empty. SQL: ${sqlPreview(sql)}`
|
|
553
606
|
);
|
|
554
607
|
}
|
|
555
608
|
return;
|
|
556
609
|
}
|
|
557
|
-
if (scan.
|
|
610
|
+
if (!scan.sawAny) {
|
|
558
611
|
throw new Error(
|
|
559
|
-
`CRITICAL:
|
|
612
|
+
`CRITICAL: SQL is missing placeholders ($1..$${paramLen}) but params has length ${paramLen}. SQL: ${sqlPreview(sql)}`
|
|
560
613
|
);
|
|
561
614
|
}
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
function needsQuoting(id) {
|
|
565
|
-
if (!isNonEmptyString(id)) return true;
|
|
566
|
-
const isKeyword = SQL_KEYWORDS.has(id.toLowerCase());
|
|
567
|
-
if (isKeyword) return true;
|
|
568
|
-
const isValidIdentifier = REGEX_CACHE.VALID_IDENTIFIER.test(id);
|
|
569
|
-
return !isValidIdentifier;
|
|
570
|
-
}
|
|
571
|
-
function validateParamConsistencyFragment(sql, params) {
|
|
572
|
-
const paramLen = params.length;
|
|
573
|
-
const scan = scanDollarPlaceholders(sql, paramLen);
|
|
574
|
-
if (scan.max === 0) return;
|
|
575
|
-
if (scan.max > paramLen) {
|
|
615
|
+
if (scan.min !== 1) {
|
|
576
616
|
throw new Error(
|
|
577
|
-
`CRITICAL:
|
|
617
|
+
`CRITICAL: Placeholder range must start at $1, got min=$${scan.min}. SQL: ${sqlPreview(sql)}`
|
|
578
618
|
);
|
|
579
619
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (!condition) throw new Error(message);
|
|
584
|
-
}
|
|
585
|
-
function dialectPlaceholderPrefix(dialect) {
|
|
586
|
-
return dialect === "sqlite" ? "?" : "$";
|
|
587
|
-
}
|
|
588
|
-
function parseSqlitePlaceholderIndices(sql) {
|
|
589
|
-
const re = /\?(?:(\d+))?/g;
|
|
590
|
-
const indices = [];
|
|
591
|
-
let anonCount = 0;
|
|
592
|
-
let sawNumbered = false;
|
|
593
|
-
let sawAnonymous = false;
|
|
594
|
-
for (const m of sql.matchAll(re)) {
|
|
595
|
-
const n = m[1];
|
|
596
|
-
if (n) {
|
|
597
|
-
sawNumbered = true;
|
|
598
|
-
indices.push(parseInt(n, 10));
|
|
599
|
-
} else {
|
|
600
|
-
sawAnonymous = true;
|
|
601
|
-
anonCount += 1;
|
|
602
|
-
indices.push(anonCount);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
return { indices, sawNumbered, sawAnonymous };
|
|
606
|
-
}
|
|
607
|
-
function parseDollarPlaceholderIndices(sql) {
|
|
608
|
-
const re = /\$(\d+)/g;
|
|
609
|
-
const indices = [];
|
|
610
|
-
for (const m of sql.matchAll(re)) indices.push(parseInt(m[1], 10));
|
|
611
|
-
return indices;
|
|
612
|
-
}
|
|
613
|
-
function getPlaceholderIndices(sql, dialect) {
|
|
614
|
-
if (dialect === "sqlite") return parseSqlitePlaceholderIndices(sql);
|
|
615
|
-
return {
|
|
616
|
-
indices: parseDollarPlaceholderIndices(sql),
|
|
617
|
-
sawNumbered: false,
|
|
618
|
-
sawAnonymous: false
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
function maxIndex(indices) {
|
|
622
|
-
return indices.length > 0 ? Math.max(...indices) : 0;
|
|
623
|
-
}
|
|
624
|
-
function ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous) {
|
|
625
|
-
assertOrThrow(
|
|
626
|
-
!(sawNumbered && sawAnonymous),
|
|
627
|
-
`CRITICAL: Mixed sqlite placeholders ('?' and '?NNN') are not supported.`
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
function ensurePlaceholderMaxMatchesMappingsLength(max, mappingsLength, dialect) {
|
|
631
|
-
assertOrThrow(
|
|
632
|
-
max === mappingsLength,
|
|
633
|
-
`CRITICAL: SQL placeholder max mismatch - max is ${dialectPlaceholderPrefix(dialect)}${max}, but mappings length is ${mappingsLength}.`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
function ensureSequentialPlaceholders(placeholders, max, dialect) {
|
|
637
|
-
const prefix = dialectPlaceholderPrefix(dialect);
|
|
638
|
-
for (let i = 1; i <= max; i++) {
|
|
639
|
-
assertOrThrow(
|
|
640
|
-
placeholders.has(i),
|
|
641
|
-
`CRITICAL: Missing SQL placeholder ${prefix}${i} - placeholders must be sequential 1..${max}.`
|
|
620
|
+
if (scan.max !== paramLen) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`CRITICAL: Placeholder max must match params length. max=$${scan.max}, params=${paramLen}. SQL: ${sqlPreview(sql)}`
|
|
642
623
|
);
|
|
643
624
|
}
|
|
625
|
+
assertNoGapsDollar(scan, 1, paramLen, sql);
|
|
644
626
|
}
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
assertOrThrow(
|
|
653
|
-
!mappingIndices.has(index),
|
|
654
|
-
`CRITICAL: Duplicate ParamMapping index ${index} - each placeholder index must map to exactly one ParamMap.`
|
|
655
|
-
);
|
|
656
|
-
mappingIndices.add(index);
|
|
657
|
-
}
|
|
658
|
-
function ensureMappingIndexExistsInSql(placeholders, index) {
|
|
659
|
-
assertOrThrow(
|
|
660
|
-
placeholders.has(index),
|
|
661
|
-
`CRITICAL: ParamMapping index ${index} not found in SQL placeholders.`
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
function validateMappingValueShape(mapping) {
|
|
665
|
-
assertOrThrow(
|
|
666
|
-
!(mapping.dynamicName !== void 0 && mapping.value !== void 0),
|
|
667
|
-
`CRITICAL: ParamMap ${mapping.index} has both dynamicName and value`
|
|
668
|
-
);
|
|
669
|
-
assertOrThrow(
|
|
670
|
-
!(mapping.dynamicName === void 0 && mapping.value === void 0),
|
|
671
|
-
`CRITICAL: ParamMap ${mapping.index} has neither dynamicName nor value`
|
|
672
|
-
);
|
|
627
|
+
function countQuestionMarkPlaceholders(sql) {
|
|
628
|
+
const s = String(sql);
|
|
629
|
+
let count = 0;
|
|
630
|
+
for (let i = 0; i < s.length; i++) {
|
|
631
|
+
if (s.charCodeAt(i) === 63) count++;
|
|
632
|
+
}
|
|
633
|
+
return count;
|
|
673
634
|
}
|
|
674
|
-
function
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
`CRITICAL:
|
|
635
|
+
function validateQuestionMarkConsistency(sql, params) {
|
|
636
|
+
const expected = params.length;
|
|
637
|
+
const found = countQuestionMarkPlaceholders(sql);
|
|
638
|
+
if (expected !== found) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`CRITICAL: Parameter mismatch - expected ${expected} '?' placeholders, found ${found}. SQL: ${sqlPreview(sql)}`
|
|
680
641
|
);
|
|
681
642
|
}
|
|
682
643
|
}
|
|
683
|
-
function
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
ensureUniqueMappingIndex(mappingIndices, mapping.index);
|
|
688
|
-
ensureMappingIndexExistsInSql(placeholders, mapping.index);
|
|
689
|
-
validateMappingValueShape(mapping);
|
|
644
|
+
function validateParamConsistencyByDialect(sql, params, dialect) {
|
|
645
|
+
if (dialect === "postgres") {
|
|
646
|
+
validateParamConsistency(sql, params);
|
|
647
|
+
return;
|
|
690
648
|
}
|
|
691
|
-
ensureMappingsCoverAllIndices(mappingIndices, max, dialect);
|
|
692
|
-
}
|
|
693
|
-
function validateSqlPositions(sql, mappings, dialect) {
|
|
694
|
-
const { indices, sawNumbered, sawAnonymous } = getPlaceholderIndices(
|
|
695
|
-
sql,
|
|
696
|
-
dialect
|
|
697
|
-
);
|
|
698
649
|
if (dialect === "sqlite") {
|
|
699
|
-
|
|
650
|
+
validateQuestionMarkConsistency(sql, params);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (dialect === "mysql" || dialect === "mariadb") {
|
|
654
|
+
validateQuestionMarkConsistency(sql, params);
|
|
655
|
+
return;
|
|
700
656
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
657
|
+
validateParamConsistency(sql, params);
|
|
658
|
+
}
|
|
659
|
+
function needsQuoting(identifier) {
|
|
660
|
+
const s = String(identifier);
|
|
661
|
+
if (!REGEX_CACHE.VALID_IDENTIFIER.test(s)) return true;
|
|
662
|
+
const lowered = s.toLowerCase();
|
|
663
|
+
if (SQL_KEYWORDS.has(lowered)) return true;
|
|
664
|
+
return false;
|
|
707
665
|
}
|
|
708
666
|
|
|
709
667
|
// src/builder/shared/sql-utils.ts
|
|
710
|
-
var NUL = String.fromCharCode(0);
|
|
711
668
|
function containsControlChars(s) {
|
|
712
|
-
return
|
|
669
|
+
return /[\u0000-\u001F\u007F]/.test(s);
|
|
713
670
|
}
|
|
714
671
|
function assertNoControlChars(label, s) {
|
|
715
672
|
if (containsControlChars(s)) {
|
|
@@ -928,16 +885,46 @@ function buildTableReference(schemaName, tableName, dialect) {
|
|
|
928
885
|
return `"${safeSchema}"."${safeTable}"`;
|
|
929
886
|
}
|
|
930
887
|
function assertSafeAlias(alias) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
888
|
+
if (typeof alias !== "string") {
|
|
889
|
+
throw new Error(`Invalid alias: expected string, got ${typeof alias}`);
|
|
890
|
+
}
|
|
891
|
+
const a = alias.trim();
|
|
892
|
+
if (a.length === 0) {
|
|
893
|
+
throw new Error("Invalid alias: required and cannot be empty");
|
|
894
|
+
}
|
|
895
|
+
if (a !== alias) {
|
|
896
|
+
throw new Error("Invalid alias: leading/trailing whitespace");
|
|
934
897
|
}
|
|
935
|
-
if (
|
|
936
|
-
throw new Error(
|
|
898
|
+
if (/[\u0000-\u001F\u007F]/.test(a)) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
"Invalid alias: contains unsafe characters (control characters)"
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (a.includes('"') || a.includes("'") || a.includes("`")) {
|
|
904
|
+
throw new Error("Invalid alias: contains unsafe characters (quotes)");
|
|
905
|
+
}
|
|
906
|
+
if (a.includes(";")) {
|
|
907
|
+
throw new Error("Invalid alias: contains unsafe characters (semicolon)");
|
|
908
|
+
}
|
|
909
|
+
if (a.includes("--") || a.includes("/*") || a.includes("*/")) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
"Invalid alias: contains unsafe characters (SQL comment tokens)"
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
if (/\s/.test(a)) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
"Invalid alias: must be a simple identifier without whitespace"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(a)) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Invalid alias: must be a simple identifier (alphanumeric with underscores): "${alias}"`
|
|
922
|
+
);
|
|
937
923
|
}
|
|
938
|
-
|
|
924
|
+
const lowered = a.toLowerCase();
|
|
925
|
+
if (ALIAS_FORBIDDEN_KEYWORDS.has(lowered)) {
|
|
939
926
|
throw new Error(
|
|
940
|
-
`alias
|
|
927
|
+
`Invalid alias: '${alias}' is a SQL keyword that would break query parsing. Forbidden aliases: ${[...ALIAS_FORBIDDEN_KEYWORDS].join(", ")}`
|
|
941
928
|
);
|
|
942
929
|
}
|
|
943
930
|
}
|
|
@@ -979,7 +966,9 @@ function isValidRelationField(field) {
|
|
|
979
966
|
if (fk.length === 0) return false;
|
|
980
967
|
const refsRaw = field.references;
|
|
981
968
|
const refs = normalizeKeyList(refsRaw);
|
|
982
|
-
if (refs.length === 0)
|
|
969
|
+
if (refs.length === 0) {
|
|
970
|
+
return fk.length === 1;
|
|
971
|
+
}
|
|
983
972
|
if (refs.length !== fk.length) return false;
|
|
984
973
|
return true;
|
|
985
974
|
}
|
|
@@ -994,6 +983,8 @@ function getReferenceFieldNames(field, foreignKeyCount) {
|
|
|
994
983
|
return refs;
|
|
995
984
|
}
|
|
996
985
|
function joinCondition(field, parentModel, childModel, parentAlias, childAlias) {
|
|
986
|
+
assertSafeAlias(parentAlias);
|
|
987
|
+
assertSafeAlias(childAlias);
|
|
997
988
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
998
989
|
if (fkFields.length === 0) {
|
|
999
990
|
throw createError(
|
|
@@ -1143,6 +1134,66 @@ function normalizeOrderByInput(orderBy, parseValue) {
|
|
|
1143
1134
|
throw new Error("orderBy must be an object or array of objects");
|
|
1144
1135
|
}
|
|
1145
1136
|
|
|
1137
|
+
// src/builder/shared/order-by-determinism.ts
|
|
1138
|
+
function modelHasScalarId(model) {
|
|
1139
|
+
if (!model) return false;
|
|
1140
|
+
return getScalarFieldSet(model).has("id");
|
|
1141
|
+
}
|
|
1142
|
+
function hasIdTiebreaker(orderBy, parse) {
|
|
1143
|
+
if (!isNotNullish(orderBy)) return false;
|
|
1144
|
+
const normalized = normalizeOrderByInput(orderBy, parse);
|
|
1145
|
+
return normalized.some(
|
|
1146
|
+
(obj) => Object.prototype.hasOwnProperty.call(obj, "id")
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
function addIdTiebreaker(orderBy) {
|
|
1150
|
+
if (Array.isArray(orderBy)) return [...orderBy, { id: "asc" }];
|
|
1151
|
+
return [orderBy, { id: "asc" }];
|
|
1152
|
+
}
|
|
1153
|
+
function ensureDeterministicOrderByInput(args) {
|
|
1154
|
+
const { orderBy, model, parseValue } = args;
|
|
1155
|
+
if (!modelHasScalarId(model)) return orderBy;
|
|
1156
|
+
if (!isNotNullish(orderBy)) {
|
|
1157
|
+
return { id: "asc" };
|
|
1158
|
+
}
|
|
1159
|
+
if (hasIdTiebreaker(orderBy, parseValue)) return orderBy;
|
|
1160
|
+
return addIdTiebreaker(orderBy);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/builder/shared/validators/field-assertions.ts
|
|
1164
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Decimal", "BigInt"]);
|
|
1165
|
+
function assertScalarField(model, fieldName, context) {
|
|
1166
|
+
const field = getFieldInfo(model, fieldName);
|
|
1167
|
+
if (!field) {
|
|
1168
|
+
throw createError(
|
|
1169
|
+
`${context} references unknown field '${fieldName}' on model ${model.name}`,
|
|
1170
|
+
{
|
|
1171
|
+
field: fieldName,
|
|
1172
|
+
modelName: model.name,
|
|
1173
|
+
availableFields: model.fields.map((f) => f.name)
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
if (field.isRelation) {
|
|
1178
|
+
throw createError(
|
|
1179
|
+
`${context} does not support relation field '${fieldName}'`,
|
|
1180
|
+
{ field: fieldName, modelName: model.name }
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
return field;
|
|
1184
|
+
}
|
|
1185
|
+
function assertNumericField(model, fieldName, context) {
|
|
1186
|
+
const field = assertScalarField(model, fieldName, context);
|
|
1187
|
+
const baseType = field.type.replace(/\[\]|\?/g, "");
|
|
1188
|
+
if (!NUMERIC_TYPES.has(baseType)) {
|
|
1189
|
+
throw createError(
|
|
1190
|
+
`${context} requires numeric field, got '${field.type}'`,
|
|
1191
|
+
{ field: fieldName, modelName: model.name }
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
return field;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1146
1197
|
// src/builder/pagination.ts
|
|
1147
1198
|
var MAX_LIMIT_OFFSET = 2147483647;
|
|
1148
1199
|
function parseDirectionRaw(raw, errorLabel) {
|
|
@@ -1183,30 +1234,31 @@ function parseOrderByValue(v, fieldName) {
|
|
|
1183
1234
|
assertAllowedOrderByKeys(obj, fieldName);
|
|
1184
1235
|
return { direction, nulls };
|
|
1185
1236
|
}
|
|
1186
|
-
function normalizeFiniteInteger(name, v) {
|
|
1187
|
-
if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) {
|
|
1188
|
-
throw new Error(`${name} must be an integer`);
|
|
1189
|
-
}
|
|
1190
|
-
return v;
|
|
1191
|
-
}
|
|
1192
1237
|
function normalizeNonNegativeInt(name, v) {
|
|
1193
1238
|
if (isDynamicParameter(v)) return v;
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
return
|
|
1239
|
+
const result = normalizeIntLike(name, v, {
|
|
1240
|
+
min: 0,
|
|
1241
|
+
max: MAX_LIMIT_OFFSET,
|
|
1242
|
+
allowZero: true
|
|
1243
|
+
});
|
|
1244
|
+
if (result === void 0)
|
|
1245
|
+
throw new Error(`${name} normalization returned undefined`);
|
|
1246
|
+
return result;
|
|
1247
|
+
}
|
|
1248
|
+
function normalizeIntAllowNegative(name, v) {
|
|
1249
|
+
if (isDynamicParameter(v)) return v;
|
|
1250
|
+
const result = normalizeIntLike(name, v, {
|
|
1251
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
1252
|
+
max: MAX_LIMIT_OFFSET,
|
|
1253
|
+
allowZero: true
|
|
1254
|
+
});
|
|
1255
|
+
if (result === void 0)
|
|
1256
|
+
throw new Error(`${name} normalization returned undefined`);
|
|
1257
|
+
return result;
|
|
1202
1258
|
}
|
|
1203
1259
|
function hasNonNullishProp(v, key) {
|
|
1204
1260
|
return isPlainObject(v) && key in v && isNotNullish(v[key]);
|
|
1205
1261
|
}
|
|
1206
|
-
function normalizeIntegerOrDynamic(name, v) {
|
|
1207
|
-
if (isDynamicParameter(v)) return v;
|
|
1208
|
-
return normalizeFiniteInteger(name, v);
|
|
1209
|
-
}
|
|
1210
1262
|
function readSkipTake(relArgs) {
|
|
1211
1263
|
const hasSkip = hasNonNullishProp(relArgs, "skip");
|
|
1212
1264
|
const hasTake = hasNonNullishProp(relArgs, "take");
|
|
@@ -1220,7 +1272,7 @@ function readSkipTake(relArgs) {
|
|
|
1220
1272
|
}
|
|
1221
1273
|
const obj = relArgs;
|
|
1222
1274
|
const skipVal = hasSkip ? normalizeNonNegativeInt("skip", obj.skip) : void 0;
|
|
1223
|
-
const takeVal = hasTake ?
|
|
1275
|
+
const takeVal = hasTake ? normalizeIntAllowNegative("take", obj.take) : void 0;
|
|
1224
1276
|
return { hasSkip, hasTake, skipVal, takeVal };
|
|
1225
1277
|
}
|
|
1226
1278
|
function buildOrderByFragment(entries, alias, dialect, model) {
|
|
@@ -1246,9 +1298,7 @@ function buildOrderByFragment(entries, alias, dialect, model) {
|
|
|
1246
1298
|
return out.join(SQL_SEPARATORS.ORDER_BY);
|
|
1247
1299
|
}
|
|
1248
1300
|
function defaultNullsFor(dialect, direction) {
|
|
1249
|
-
if (dialect === "postgres")
|
|
1250
|
-
return direction === "asc" ? "last" : "first";
|
|
1251
|
-
}
|
|
1301
|
+
if (dialect === "postgres") return direction === "asc" ? "last" : "first";
|
|
1252
1302
|
return direction === "asc" ? "first" : "last";
|
|
1253
1303
|
}
|
|
1254
1304
|
function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
|
|
@@ -1265,13 +1315,12 @@ function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
|
|
|
1265
1315
|
}
|
|
1266
1316
|
function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
1267
1317
|
const entries = Object.entries(cursor);
|
|
1268
|
-
if (entries.length === 0)
|
|
1318
|
+
if (entries.length === 0)
|
|
1269
1319
|
throw new Error("cursor must have at least one field");
|
|
1270
|
-
}
|
|
1271
1320
|
const placeholdersByField = /* @__PURE__ */ new Map();
|
|
1272
1321
|
const parts = [];
|
|
1273
1322
|
for (const [field, value] of entries) {
|
|
1274
|
-
const c = `${cursorAlias}.${
|
|
1323
|
+
const c = `${cursorAlias}.${quoteColumn(model, field)}`;
|
|
1275
1324
|
if (value === null) {
|
|
1276
1325
|
parts.push(`${c} IS NULL`);
|
|
1277
1326
|
continue;
|
|
@@ -1285,13 +1334,6 @@ function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
|
1285
1334
|
placeholdersByField
|
|
1286
1335
|
};
|
|
1287
1336
|
}
|
|
1288
|
-
function cursorValueExpr(tableName, cursorAlias, cursorWhereSql, field, model) {
|
|
1289
|
-
const colName = quote(field);
|
|
1290
|
-
return `(SELECT ${cursorAlias}.${colName} ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1291
|
-
}
|
|
1292
|
-
function buildCursorRowExistsExpr(tableName, cursorAlias, cursorWhereSql) {
|
|
1293
|
-
return `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1294
|
-
}
|
|
1295
1337
|
function buildCursorEqualityExpr(columnExpr, valueExpr) {
|
|
1296
1338
|
return `((${valueExpr} IS NULL AND ${columnExpr} IS NULL) OR (${valueExpr} IS NOT NULL AND ${columnExpr} = ${valueExpr}))`;
|
|
1297
1339
|
}
|
|
@@ -1328,26 +1370,70 @@ function buildOrderEntries(orderBy) {
|
|
|
1328
1370
|
if (typeof value === "string") {
|
|
1329
1371
|
entries.push({ field, direction: value });
|
|
1330
1372
|
} else {
|
|
1331
|
-
entries.push({
|
|
1332
|
-
field,
|
|
1333
|
-
direction: value.sort,
|
|
1334
|
-
nulls: value.nulls
|
|
1335
|
-
});
|
|
1373
|
+
entries.push({ field, direction: value.direction, nulls: value.nulls });
|
|
1336
1374
|
}
|
|
1337
1375
|
}
|
|
1338
1376
|
}
|
|
1339
1377
|
return entries;
|
|
1340
1378
|
}
|
|
1379
|
+
function buildCursorCteSelectList(cursorEntries, orderEntries, model) {
|
|
1380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1381
|
+
const ordered = [];
|
|
1382
|
+
for (const [f] of cursorEntries) {
|
|
1383
|
+
if (!seen.has(f)) {
|
|
1384
|
+
seen.add(f);
|
|
1385
|
+
ordered.push(f);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
for (const e of orderEntries) {
|
|
1389
|
+
if (!seen.has(e.field)) {
|
|
1390
|
+
seen.add(e.field);
|
|
1391
|
+
ordered.push(e.field);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (ordered.length === 0) throw new Error("cursor cte select list is empty");
|
|
1395
|
+
return ordered.map((f) => quoteColumn(model, f)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
1396
|
+
}
|
|
1397
|
+
function truncateIdent(name, maxLen) {
|
|
1398
|
+
const s = String(name);
|
|
1399
|
+
if (s.length <= maxLen) return s;
|
|
1400
|
+
return s.slice(0, maxLen);
|
|
1401
|
+
}
|
|
1402
|
+
function buildCursorNames(outerAlias) {
|
|
1403
|
+
const maxLen = 63;
|
|
1404
|
+
const base = outerAlias.toLowerCase();
|
|
1405
|
+
const cteName = truncateIdent(`__tp_cursor_${base}`, maxLen);
|
|
1406
|
+
const srcAlias = truncateIdent(`__tp_cursor_src_${base}`, maxLen);
|
|
1407
|
+
if (cteName === outerAlias || srcAlias === outerAlias) {
|
|
1408
|
+
return {
|
|
1409
|
+
cteName: truncateIdent(`__tp_cursor_${base}_x`, maxLen),
|
|
1410
|
+
srcAlias: truncateIdent(`__tp_cursor_src_${base}_x`, maxLen)
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
return { cteName, srcAlias };
|
|
1414
|
+
}
|
|
1415
|
+
function assertCursorAndOrderFieldsScalar(model, cursor, orderEntries) {
|
|
1416
|
+
if (!model) return;
|
|
1417
|
+
for (const k of Object.keys(cursor)) assertScalarField(model, k, "cursor");
|
|
1418
|
+
for (const e of orderEntries) assertScalarField(model, e.field, "orderBy");
|
|
1419
|
+
}
|
|
1341
1420
|
function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect, model) {
|
|
1342
1421
|
var _a;
|
|
1422
|
+
assertSafeTableRef(tableName);
|
|
1423
|
+
assertSafeAlias(alias);
|
|
1343
1424
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
1344
1425
|
const cursorEntries = Object.entries(cursor);
|
|
1345
|
-
if (cursorEntries.length === 0)
|
|
1426
|
+
if (cursorEntries.length === 0)
|
|
1346
1427
|
throw new Error("cursor must have at least one field");
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1428
|
+
const { cteName, srcAlias } = buildCursorNames(alias);
|
|
1429
|
+
assertSafeAlias(cteName);
|
|
1430
|
+
assertSafeAlias(srcAlias);
|
|
1431
|
+
const deterministicOrderBy = ensureDeterministicOrderByInput({
|
|
1432
|
+
orderBy,
|
|
1433
|
+
model,
|
|
1434
|
+
parseValue: parseOrderByValue
|
|
1435
|
+
});
|
|
1436
|
+
let orderEntries = buildOrderEntries(deterministicOrderBy);
|
|
1351
1437
|
if (orderEntries.length === 0) {
|
|
1352
1438
|
orderEntries = cursorEntries.map(([field]) => ({
|
|
1353
1439
|
field,
|
|
@@ -1356,11 +1442,23 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1356
1442
|
} else {
|
|
1357
1443
|
orderEntries = ensureCursorFieldsInOrder(orderEntries, cursorEntries);
|
|
1358
1444
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1445
|
+
assertCursorAndOrderFieldsScalar(model, cursor, orderEntries);
|
|
1446
|
+
const { whereSql: cursorWhereSql, placeholdersByField } = buildCursorFilterParts(cursor, srcAlias, params, model);
|
|
1447
|
+
const cursorOrderBy = orderEntries.map(
|
|
1448
|
+
(e) => `${srcAlias}.${quoteColumn(model, e.field)} ${e.direction.toUpperCase()}`
|
|
1449
|
+
).join(", ");
|
|
1450
|
+
const selectList = buildCursorCteSelectList(
|
|
1451
|
+
cursorEntries,
|
|
1452
|
+
orderEntries,
|
|
1453
|
+
model
|
|
1363
1454
|
);
|
|
1455
|
+
const cte = `${cteName} AS (
|
|
1456
|
+
SELECT ${selectList} FROM ${tableName} ${srcAlias}
|
|
1457
|
+
WHERE ${cursorWhereSql}
|
|
1458
|
+
ORDER BY ${cursorOrderBy}
|
|
1459
|
+
LIMIT 1
|
|
1460
|
+
)`;
|
|
1461
|
+
const existsExpr = `EXISTS (SELECT 1 FROM ${cteName})`;
|
|
1364
1462
|
const outerCursorMatch = buildOuterCursorMatch(
|
|
1365
1463
|
cursor,
|
|
1366
1464
|
alias,
|
|
@@ -1368,34 +1466,29 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1368
1466
|
params,
|
|
1369
1467
|
model
|
|
1370
1468
|
);
|
|
1469
|
+
const getValueExpr = (field) => `(SELECT ${quoteColumn(model, field)} FROM ${cteName})`;
|
|
1371
1470
|
const orClauses = [];
|
|
1372
1471
|
for (let level = 0; level < orderEntries.length; level++) {
|
|
1373
1472
|
const andParts = [];
|
|
1374
1473
|
for (let i = 0; i < level; i++) {
|
|
1375
1474
|
const e2 = orderEntries[i];
|
|
1376
1475
|
const c2 = col(alias, e2.field, model);
|
|
1377
|
-
const v2 =
|
|
1378
|
-
tableName,
|
|
1379
|
-
cursorAlias,
|
|
1380
|
-
cursorWhereSql,
|
|
1381
|
-
e2.field);
|
|
1476
|
+
const v2 = getValueExpr(e2.field);
|
|
1382
1477
|
andParts.push(buildCursorEqualityExpr(c2, v2));
|
|
1383
1478
|
}
|
|
1384
1479
|
const e = orderEntries[level];
|
|
1385
1480
|
const c = col(alias, e.field, model);
|
|
1386
|
-
const v =
|
|
1387
|
-
tableName,
|
|
1388
|
-
cursorAlias,
|
|
1389
|
-
cursorWhereSql,
|
|
1390
|
-
e.field);
|
|
1481
|
+
const v = getValueExpr(e.field);
|
|
1391
1482
|
const nulls = (_a = e.nulls) != null ? _a : defaultNullsFor(d, e.direction);
|
|
1392
1483
|
andParts.push(buildCursorInequalityExpr(c, e.direction, nulls, v));
|
|
1393
1484
|
orClauses.push(`(${andParts.join(SQL_SEPARATORS.CONDITION_AND)})`);
|
|
1394
1485
|
}
|
|
1395
1486
|
const exclusive = orClauses.join(SQL_SEPARATORS.CONDITION_OR);
|
|
1396
|
-
|
|
1487
|
+
const condition = `(${existsExpr} ${SQL_SEPARATORS.CONDITION_AND} ((${exclusive})${SQL_SEPARATORS.CONDITION_OR}(${outerCursorMatch})))`;
|
|
1488
|
+
return { cte, condition };
|
|
1397
1489
|
}
|
|
1398
1490
|
function buildOrderBy(orderBy, alias, dialect, model) {
|
|
1491
|
+
assertSafeAlias(alias);
|
|
1399
1492
|
const entries = buildOrderEntries(orderBy);
|
|
1400
1493
|
if (entries.length === 0) return "";
|
|
1401
1494
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
@@ -1417,9 +1510,7 @@ function normalizeTakeLike(v) {
|
|
|
1417
1510
|
max: MAX_LIMIT_OFFSET,
|
|
1418
1511
|
allowZero: true
|
|
1419
1512
|
});
|
|
1420
|
-
if (typeof n === "number")
|
|
1421
|
-
if (n === 0) return 0;
|
|
1422
|
-
}
|
|
1513
|
+
if (typeof n === "number" && n === 0) return 0;
|
|
1423
1514
|
return n;
|
|
1424
1515
|
}
|
|
1425
1516
|
function normalizeSkipLike(v) {
|
|
@@ -1495,6 +1586,11 @@ function buildScalarOperator(expr, op, val, params, mode, fieldType, dialect) {
|
|
|
1495
1586
|
}
|
|
1496
1587
|
return handleInOperator(expr, op, val, params, dialect);
|
|
1497
1588
|
}
|
|
1589
|
+
if (op === Ops.EQUALS && mode === Modes.INSENSITIVE && !isNotNullish(dialect)) {
|
|
1590
|
+
throw createError(`Insensitive equals requires a SQL dialect`, {
|
|
1591
|
+
operator: op
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1498
1594
|
return handleComparisonOperator(expr, op, val, params);
|
|
1499
1595
|
}
|
|
1500
1596
|
function handleNullValue(expr, op) {
|
|
@@ -1510,6 +1606,28 @@ function normalizeMode(v) {
|
|
|
1510
1606
|
function handleNotOperator(expr, val, params, outerMode, fieldType, dialect) {
|
|
1511
1607
|
const innerMode = normalizeMode(val.mode);
|
|
1512
1608
|
const effectiveMode = innerMode != null ? innerMode : outerMode;
|
|
1609
|
+
const entries = Object.entries(val).filter(
|
|
1610
|
+
([k, v]) => k !== "mode" && v !== void 0
|
|
1611
|
+
);
|
|
1612
|
+
if (entries.length === 0) return "";
|
|
1613
|
+
if (!isNotNullish(dialect)) {
|
|
1614
|
+
const clauses = [];
|
|
1615
|
+
for (const [subOp, subVal] of entries) {
|
|
1616
|
+
const sub = buildScalarOperator(
|
|
1617
|
+
expr,
|
|
1618
|
+
subOp,
|
|
1619
|
+
subVal,
|
|
1620
|
+
params,
|
|
1621
|
+
effectiveMode,
|
|
1622
|
+
fieldType,
|
|
1623
|
+
void 0
|
|
1624
|
+
);
|
|
1625
|
+
if (sub && sub.trim().length > 0) clauses.push(`(${sub})`);
|
|
1626
|
+
}
|
|
1627
|
+
if (clauses.length === 0) return "";
|
|
1628
|
+
if (clauses.length === 1) return `${SQL_TEMPLATES.NOT} ${clauses[0]}`;
|
|
1629
|
+
return `${SQL_TEMPLATES.NOT} (${clauses.join(` ${SQL_TEMPLATES.AND} `)})`;
|
|
1630
|
+
}
|
|
1513
1631
|
return buildNotComposite(
|
|
1514
1632
|
expr,
|
|
1515
1633
|
val,
|
|
@@ -1724,6 +1842,7 @@ function handleArrayIsEmpty(expr, val, dialect) {
|
|
|
1724
1842
|
|
|
1725
1843
|
// src/builder/where/operators-json.ts
|
|
1726
1844
|
var SAFE_JSON_PATH_SEGMENT = /^[a-zA-Z_]\w*$/;
|
|
1845
|
+
var MAX_PATH_SEGMENT_LENGTH = 255;
|
|
1727
1846
|
function validateJsonPathSegments(segments) {
|
|
1728
1847
|
for (const segment of segments) {
|
|
1729
1848
|
if (typeof segment !== "string") {
|
|
@@ -1732,6 +1851,12 @@ function validateJsonPathSegments(segments) {
|
|
|
1732
1851
|
value: segment
|
|
1733
1852
|
});
|
|
1734
1853
|
}
|
|
1854
|
+
if (segment.length > MAX_PATH_SEGMENT_LENGTH) {
|
|
1855
|
+
throw createError(
|
|
1856
|
+
`JSON path segment too long: max ${MAX_PATH_SEGMENT_LENGTH} characters`,
|
|
1857
|
+
{ operator: Ops.PATH, value: `[${segment.length} chars]` }
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1735
1860
|
if (!SAFE_JSON_PATH_SEGMENT.test(segment)) {
|
|
1736
1861
|
throw createError(
|
|
1737
1862
|
`Invalid JSON path segment: '${segment}'. Must be alphanumeric with underscores, starting with letter or underscore.`,
|
|
@@ -1820,6 +1945,9 @@ function handleJsonWildcard(expr, op, val, params, wildcards, dialect) {
|
|
|
1820
1945
|
|
|
1821
1946
|
// src/builder/where/relations.ts
|
|
1822
1947
|
var NO_JOINS = Object.freeze([]);
|
|
1948
|
+
function freezeJoins(items) {
|
|
1949
|
+
return Object.freeze([...items]);
|
|
1950
|
+
}
|
|
1823
1951
|
function isListRelation(fieldType) {
|
|
1824
1952
|
return typeof fieldType === "string" && fieldType.endsWith("[]");
|
|
1825
1953
|
}
|
|
@@ -1882,7 +2010,7 @@ function buildListRelationFilters(args) {
|
|
|
1882
2010
|
const whereClause = `${relAlias}.${quote(checkField.name)} IS NULL`;
|
|
1883
2011
|
return Object.freeze({
|
|
1884
2012
|
clause: whereClause,
|
|
1885
|
-
joins: [leftJoinSql]
|
|
2013
|
+
joins: freezeJoins([leftJoinSql])
|
|
1886
2014
|
});
|
|
1887
2015
|
}
|
|
1888
2016
|
}
|
|
@@ -2087,7 +2215,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2087
2215
|
Ops.HAS_EVERY,
|
|
2088
2216
|
Ops.IS_EMPTY
|
|
2089
2217
|
]);
|
|
2090
|
-
const
|
|
2218
|
+
const JSON_OPS2 = /* @__PURE__ */ new Set([
|
|
2091
2219
|
Ops.PATH,
|
|
2092
2220
|
Ops.STRING_CONTAINS,
|
|
2093
2221
|
Ops.STRING_STARTS_WITH,
|
|
@@ -2104,7 +2232,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2104
2232
|
modelName
|
|
2105
2233
|
});
|
|
2106
2234
|
}
|
|
2107
|
-
const isJsonOp =
|
|
2235
|
+
const isJsonOp = JSON_OPS2.has(op);
|
|
2108
2236
|
const isFieldJson = isJsonType(fieldType);
|
|
2109
2237
|
const jsonOpMismatch = isJsonOp && !isFieldJson;
|
|
2110
2238
|
if (jsonOpMismatch) {
|
|
@@ -2118,6 +2246,14 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2118
2246
|
}
|
|
2119
2247
|
|
|
2120
2248
|
// src/builder/where/builder.ts
|
|
2249
|
+
var MAX_QUERY_DEPTH = 50;
|
|
2250
|
+
var EMPTY_JOINS = Object.freeze([]);
|
|
2251
|
+
var JSON_OPS = /* @__PURE__ */ new Set([
|
|
2252
|
+
Ops.PATH,
|
|
2253
|
+
Ops.STRING_CONTAINS,
|
|
2254
|
+
Ops.STRING_STARTS_WITH,
|
|
2255
|
+
Ops.STRING_ENDS_WITH
|
|
2256
|
+
]);
|
|
2121
2257
|
var WhereBuilder = class {
|
|
2122
2258
|
build(where, ctx) {
|
|
2123
2259
|
if (!isPlainObject(where)) {
|
|
@@ -2129,8 +2265,6 @@ var WhereBuilder = class {
|
|
|
2129
2265
|
return buildWhereInternal(where, ctx, this);
|
|
2130
2266
|
}
|
|
2131
2267
|
};
|
|
2132
|
-
var MAX_QUERY_DEPTH = 50;
|
|
2133
|
-
var EMPTY_JOINS = Object.freeze([]);
|
|
2134
2268
|
var whereBuilderInstance = new WhereBuilder();
|
|
2135
2269
|
function freezeResult(clause, joins = EMPTY_JOINS) {
|
|
2136
2270
|
return Object.freeze({ clause, joins });
|
|
@@ -2307,16 +2441,8 @@ function buildOperator(expr, op, val, ctx, mode, fieldType) {
|
|
|
2307
2441
|
if (fieldType && isArrayType(fieldType)) {
|
|
2308
2442
|
return buildArrayOperator(expr, op, val, ctx.params, fieldType, ctx.dialect);
|
|
2309
2443
|
}
|
|
2310
|
-
if (fieldType && isJsonType(fieldType)) {
|
|
2311
|
-
|
|
2312
|
-
Ops.PATH,
|
|
2313
|
-
Ops.STRING_CONTAINS,
|
|
2314
|
-
Ops.STRING_STARTS_WITH,
|
|
2315
|
-
Ops.STRING_ENDS_WITH
|
|
2316
|
-
]);
|
|
2317
|
-
if (JSON_OPS.has(op)) {
|
|
2318
|
-
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2319
|
-
}
|
|
2444
|
+
if (fieldType && isJsonType(fieldType) && JSON_OPS.has(op)) {
|
|
2445
|
+
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2320
2446
|
}
|
|
2321
2447
|
return buildScalarOperator(
|
|
2322
2448
|
expr,
|
|
@@ -2337,7 +2463,7 @@ function toSafeSqlIdentifier(input) {
|
|
|
2337
2463
|
const base = startsOk ? cleaned : `_${cleaned}`;
|
|
2338
2464
|
const fallback = base.length > 0 ? base : "_t";
|
|
2339
2465
|
const lowered = fallback.toLowerCase();
|
|
2340
|
-
return
|
|
2466
|
+
return ALIAS_FORBIDDEN_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
|
|
2341
2467
|
}
|
|
2342
2468
|
function createAliasGenerator(maxAliases = 1e4) {
|
|
2343
2469
|
let counter = 0;
|
|
@@ -2547,6 +2673,7 @@ function toPublicResult(clause, joins, params) {
|
|
|
2547
2673
|
// src/builder/where.ts
|
|
2548
2674
|
function buildWhereClause(where, options) {
|
|
2549
2675
|
var _a, _b, _c, _d, _e;
|
|
2676
|
+
assertSafeAlias(options.alias);
|
|
2550
2677
|
const dialect = options.dialect || getGlobalDialect();
|
|
2551
2678
|
const params = (_a = options.params) != null ? _a : createParamStore();
|
|
2552
2679
|
const ctx = {
|
|
@@ -2562,22 +2689,6 @@ function buildWhereClause(where, options) {
|
|
|
2562
2689
|
};
|
|
2563
2690
|
const result = whereBuilderInstance.build(where, ctx);
|
|
2564
2691
|
const publicResult = toPublicResult(result.clause, result.joins, params);
|
|
2565
|
-
if (!options.isSubquery) {
|
|
2566
|
-
const nums = [...publicResult.clause.matchAll(/\$(\d+)/g)].map(
|
|
2567
|
-
(m) => parseInt(m[1], 10)
|
|
2568
|
-
);
|
|
2569
|
-
if (nums.length > 0) {
|
|
2570
|
-
const min = Math.min(...nums);
|
|
2571
|
-
if (min === 1) {
|
|
2572
|
-
validateParamConsistency(publicResult.clause, publicResult.params);
|
|
2573
|
-
} else {
|
|
2574
|
-
validateParamConsistencyFragment(
|
|
2575
|
-
publicResult.clause,
|
|
2576
|
-
publicResult.params
|
|
2577
|
-
);
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
2692
|
return publicResult;
|
|
2582
2693
|
}
|
|
2583
2694
|
|
|
@@ -2715,6 +2826,9 @@ function buildRelationSelect(relArgs, relModel, relAlias) {
|
|
|
2715
2826
|
}
|
|
2716
2827
|
|
|
2717
2828
|
// src/builder/select/includes.ts
|
|
2829
|
+
var MAX_INCLUDE_DEPTH = 10;
|
|
2830
|
+
var MAX_TOTAL_SUBQUERIES = 100;
|
|
2831
|
+
var MAX_TOTAL_INCLUDES = 50;
|
|
2718
2832
|
function getRelationTableReference(relModel, dialect) {
|
|
2719
2833
|
return buildTableReference(
|
|
2720
2834
|
SQL_TEMPLATES.PUBLIC_SCHEMA,
|
|
@@ -2760,107 +2874,23 @@ function relationEntriesFromArgs(args, model) {
|
|
|
2760
2874
|
pushFrom(args.select);
|
|
2761
2875
|
return out;
|
|
2762
2876
|
}
|
|
2763
|
-
function assertScalarField(model, fieldName) {
|
|
2764
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
2765
|
-
if (!f) {
|
|
2766
|
-
throw new Error(
|
|
2767
|
-
`orderBy references unknown field '${fieldName}' on model ${model.name}`
|
|
2768
|
-
);
|
|
2769
|
-
}
|
|
2770
|
-
if (f.isRelation) {
|
|
2771
|
-
throw new Error(
|
|
2772
|
-
`orderBy does not support relation field '${fieldName}' on model ${model.name}`
|
|
2773
|
-
);
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
function validateOrderByDirection(fieldName, v) {
|
|
2777
|
-
const s = String(v).toLowerCase();
|
|
2778
|
-
if (s !== "asc" && s !== "desc") {
|
|
2779
|
-
throw new Error(
|
|
2780
|
-
`Invalid orderBy direction for '${fieldName}': ${String(v)}`
|
|
2781
|
-
);
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
function validateOrderByObject(fieldName, v) {
|
|
2785
|
-
if (!("sort" in v)) {
|
|
2786
|
-
throw new Error(
|
|
2787
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2788
|
-
);
|
|
2789
|
-
}
|
|
2790
|
-
validateOrderByDirection(fieldName, v.sort);
|
|
2791
|
-
if ("nulls" in v && isNotNullish(v.nulls)) {
|
|
2792
|
-
const n = String(v.nulls).toLowerCase();
|
|
2793
|
-
if (n !== "first" && n !== "last") {
|
|
2794
|
-
throw new Error(
|
|
2795
|
-
`Invalid orderBy.nulls for '${fieldName}': ${String(v.nulls)}`
|
|
2796
|
-
);
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
const allowed = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
2800
|
-
for (const k of Object.keys(v)) {
|
|
2801
|
-
if (!allowed.has(k)) {
|
|
2802
|
-
throw new Error(`Unsupported orderBy key '${k}' for field '${fieldName}'`);
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
function normalizeOrderByFieldName(name) {
|
|
2807
|
-
const fieldName = String(name).trim();
|
|
2808
|
-
if (fieldName.length === 0) {
|
|
2809
|
-
throw new Error("orderBy field name cannot be empty");
|
|
2810
|
-
}
|
|
2811
|
-
return fieldName;
|
|
2812
|
-
}
|
|
2813
|
-
function requirePlainObjectForOrderByEntry(v) {
|
|
2814
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
2815
|
-
throw new Error("orderBy array entries must be objects");
|
|
2816
|
-
}
|
|
2817
|
-
return v;
|
|
2818
|
-
}
|
|
2819
|
-
function parseSingleFieldOrderByObject(obj) {
|
|
2820
|
-
const entries = Object.entries(obj);
|
|
2821
|
-
if (entries.length !== 1) {
|
|
2822
|
-
throw new Error("orderBy array entries must have exactly one field");
|
|
2823
|
-
}
|
|
2824
|
-
const fieldName = normalizeOrderByFieldName(entries[0][0]);
|
|
2825
|
-
return [fieldName, entries[0][1]];
|
|
2826
|
-
}
|
|
2827
|
-
function parseOrderByArray(orderBy) {
|
|
2828
|
-
return orderBy.map(
|
|
2829
|
-
(item) => parseSingleFieldOrderByObject(requirePlainObjectForOrderByEntry(item))
|
|
2830
|
-
);
|
|
2831
|
-
}
|
|
2832
|
-
function parseOrderByObject(orderBy) {
|
|
2833
|
-
const out = [];
|
|
2834
|
-
for (const [k, v] of Object.entries(orderBy)) {
|
|
2835
|
-
out.push([normalizeOrderByFieldName(k), v]);
|
|
2836
|
-
}
|
|
2837
|
-
return out;
|
|
2838
|
-
}
|
|
2839
|
-
function getOrderByEntries(orderBy) {
|
|
2840
|
-
if (!isNotNullish(orderBy)) return [];
|
|
2841
|
-
if (Array.isArray(orderBy)) {
|
|
2842
|
-
return parseOrderByArray(orderBy);
|
|
2843
|
-
}
|
|
2844
|
-
if (typeof orderBy === "object" && orderBy !== null) {
|
|
2845
|
-
return parseOrderByObject(orderBy);
|
|
2846
|
-
}
|
|
2847
|
-
throw new Error("orderBy must be an object or array of objects");
|
|
2848
|
-
}
|
|
2849
2877
|
function validateOrderByForModel(model, orderBy) {
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2878
|
+
if (!isNotNullish(orderBy)) return;
|
|
2879
|
+
const scalarSet = getScalarFieldSet(model);
|
|
2880
|
+
const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
2881
|
+
for (const item of normalized) {
|
|
2882
|
+
const entries = Object.entries(item);
|
|
2883
|
+
if (entries.length !== 1) {
|
|
2884
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
2856
2885
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2886
|
+
const fieldName = String(entries[0][0]).trim();
|
|
2887
|
+
if (fieldName.length === 0)
|
|
2888
|
+
throw new Error("orderBy field name cannot be empty");
|
|
2889
|
+
if (!scalarSet.has(fieldName)) {
|
|
2890
|
+
throw new Error(
|
|
2891
|
+
`orderBy references unknown or non-scalar field '${fieldName}' on model ${model.name}`
|
|
2892
|
+
);
|
|
2860
2893
|
}
|
|
2861
|
-
throw new Error(
|
|
2862
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2863
|
-
);
|
|
2864
2894
|
}
|
|
2865
2895
|
}
|
|
2866
2896
|
function appendLimitOffset(sql, dialect, params, takeVal, skipVal, scope) {
|
|
@@ -2889,7 +2919,10 @@ function readWhereInput(relArgs) {
|
|
|
2889
2919
|
function readOrderByInput(relArgs) {
|
|
2890
2920
|
if (!isPlainObject(relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
2891
2921
|
if (!("orderBy" in relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
2892
|
-
return {
|
|
2922
|
+
return {
|
|
2923
|
+
hasOrderBy: true,
|
|
2924
|
+
orderBy: relArgs.orderBy
|
|
2925
|
+
};
|
|
2893
2926
|
}
|
|
2894
2927
|
function extractRelationPaginationConfig(relArgs) {
|
|
2895
2928
|
const { hasOrderBy, orderBy: rawOrderByInput } = readOrderByInput(relArgs);
|
|
@@ -2911,44 +2944,25 @@ function extractRelationPaginationConfig(relArgs) {
|
|
|
2911
2944
|
function maybeReverseNegativeTake(takeVal, hasOrderBy, orderByInput) {
|
|
2912
2945
|
if (typeof takeVal !== "number") return { takeVal, orderByInput };
|
|
2913
2946
|
if (takeVal >= 0) return { takeVal, orderByInput };
|
|
2914
|
-
if (!hasOrderBy)
|
|
2947
|
+
if (!hasOrderBy)
|
|
2915
2948
|
throw new Error("Negative take requires orderBy for deterministic results");
|
|
2916
|
-
}
|
|
2917
2949
|
return {
|
|
2918
2950
|
takeVal: Math.abs(takeVal),
|
|
2919
2951
|
orderByInput: reverseOrderByInput(orderByInput)
|
|
2920
2952
|
};
|
|
2921
2953
|
}
|
|
2922
|
-
function
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
(entry) => isPlainObject(entry) ? Object.prototype.hasOwnProperty.call(entry, "id") : false
|
|
2926
|
-
);
|
|
2927
|
-
}
|
|
2928
|
-
function modelHasScalarId(relModel) {
|
|
2929
|
-
const idField = relModel.fields.find((f) => f.name === "id");
|
|
2930
|
-
return Boolean(idField && !idField.isRelation);
|
|
2931
|
-
}
|
|
2932
|
-
function addIdTiebreaker(orderByInput) {
|
|
2933
|
-
if (Array.isArray(orderByInput)) return [...orderByInput, { id: "asc" }];
|
|
2934
|
-
return [orderByInput, { id: "asc" }];
|
|
2935
|
-
}
|
|
2936
|
-
function ensureDeterministicOrderBy(relModel, hasOrderBy, orderByInput, hasPagination) {
|
|
2937
|
-
if (!hasPagination) {
|
|
2938
|
-
if (hasOrderBy && isNotNullish(orderByInput)) {
|
|
2939
|
-
validateOrderByForModel(relModel, orderByInput);
|
|
2940
|
-
}
|
|
2941
|
-
return orderByInput;
|
|
2954
|
+
function finalizeOrderByForInclude(args) {
|
|
2955
|
+
if (args.hasOrderBy && isNotNullish(args.orderByInput)) {
|
|
2956
|
+
validateOrderByForModel(args.relModel, args.orderByInput);
|
|
2942
2957
|
}
|
|
2943
|
-
if (!
|
|
2944
|
-
return
|
|
2958
|
+
if (!args.hasPagination) {
|
|
2959
|
+
return args.orderByInput;
|
|
2945
2960
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
return addIdTiebreaker(orderByInput);
|
|
2961
|
+
return ensureDeterministicOrderByInput({
|
|
2962
|
+
orderBy: args.hasOrderBy ? args.orderByInput : void 0,
|
|
2963
|
+
model: args.relModel,
|
|
2964
|
+
parseValue: parseOrderByValue
|
|
2965
|
+
});
|
|
2952
2966
|
}
|
|
2953
2967
|
function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
2954
2968
|
let relSelect = buildRelationSelect(relArgs, relModel, relAlias);
|
|
@@ -2959,7 +2973,10 @@ function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
|
2959
2973
|
relAlias,
|
|
2960
2974
|
ctx.aliasGen,
|
|
2961
2975
|
ctx.params,
|
|
2962
|
-
ctx.dialect
|
|
2976
|
+
ctx.dialect,
|
|
2977
|
+
ctx.visitPath || [],
|
|
2978
|
+
(ctx.depth || 0) + 1,
|
|
2979
|
+
ctx.stats
|
|
2963
2980
|
) : [];
|
|
2964
2981
|
if (isNonEmptyArray(nestedIncludes)) {
|
|
2965
2982
|
const emptyJson = ctx.dialect === "postgres" ? `'[]'::json` : `json('[]')`;
|
|
@@ -3046,11 +3063,7 @@ function buildListIncludeSpec(args) {
|
|
|
3046
3063
|
joinPredicate: args.joinPredicate,
|
|
3047
3064
|
whereClause: args.whereClause
|
|
3048
3065
|
});
|
|
3049
|
-
return Object.freeze({
|
|
3050
|
-
name: args.relName,
|
|
3051
|
-
sql: sql2,
|
|
3052
|
-
isOneToOne: false
|
|
3053
|
-
});
|
|
3066
|
+
return Object.freeze({ name: args.relName, sql: sql2, isOneToOne: false });
|
|
3054
3067
|
}
|
|
3055
3068
|
const rowAlias = args.ctx.aliasGen.next(`${args.relName}_row`);
|
|
3056
3069
|
let base = buildBaseSql({
|
|
@@ -3061,9 +3074,7 @@ function buildListIncludeSpec(args) {
|
|
|
3061
3074
|
joinPredicate: args.joinPredicate,
|
|
3062
3075
|
whereClause: args.whereClause
|
|
3063
3076
|
});
|
|
3064
|
-
if (args.orderBySql) {
|
|
3065
|
-
base += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
|
|
3066
|
-
}
|
|
3077
|
+
if (args.orderBySql) base += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
|
|
3067
3078
|
base = appendLimitOffset(
|
|
3068
3079
|
base,
|
|
3069
3080
|
args.ctx.dialect,
|
|
@@ -3074,11 +3085,7 @@ function buildListIncludeSpec(args) {
|
|
|
3074
3085
|
);
|
|
3075
3086
|
const selectExpr = jsonAgg("row", args.ctx.dialect);
|
|
3076
3087
|
const sql = `${SQL_TEMPLATES.SELECT} ${selectExpr} ${SQL_TEMPLATES.FROM} (${base}) ${rowAlias}`;
|
|
3077
|
-
return Object.freeze({
|
|
3078
|
-
name: args.relName,
|
|
3079
|
-
sql,
|
|
3080
|
-
isOneToOne: false
|
|
3081
|
-
});
|
|
3088
|
+
return Object.freeze({ name: args.relName, sql, isOneToOne: false });
|
|
3082
3089
|
}
|
|
3083
3090
|
function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
3084
3091
|
const relTable = getRelationTableReference(relModel, ctx.dialect);
|
|
@@ -3109,12 +3116,12 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3109
3116
|
paginationConfig.orderBy
|
|
3110
3117
|
);
|
|
3111
3118
|
const hasPagination = paginationConfig.hasSkip || paginationConfig.hasTake;
|
|
3112
|
-
const finalOrderByInput =
|
|
3119
|
+
const finalOrderByInput = finalizeOrderByForInclude({
|
|
3113
3120
|
relModel,
|
|
3114
|
-
paginationConfig.hasOrderBy,
|
|
3115
|
-
adjusted.orderByInput,
|
|
3121
|
+
hasOrderBy: paginationConfig.hasOrderBy,
|
|
3122
|
+
orderByInput: adjusted.orderByInput,
|
|
3116
3123
|
hasPagination
|
|
3117
|
-
);
|
|
3124
|
+
});
|
|
3118
3125
|
const orderBySql = buildOrderBySql(
|
|
3119
3126
|
finalOrderByInput,
|
|
3120
3127
|
relAlias,
|
|
@@ -3135,11 +3142,7 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3135
3142
|
skipVal: paginationConfig.skipVal,
|
|
3136
3143
|
ctx
|
|
3137
3144
|
});
|
|
3138
|
-
return Object.freeze({
|
|
3139
|
-
name: relName,
|
|
3140
|
-
sql,
|
|
3141
|
-
isOneToOne: true
|
|
3142
|
-
});
|
|
3145
|
+
return Object.freeze({ name: relName, sql, isOneToOne: true });
|
|
3143
3146
|
}
|
|
3144
3147
|
return buildListIncludeSpec({
|
|
3145
3148
|
relName,
|
|
@@ -3155,32 +3158,69 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3155
3158
|
ctx
|
|
3156
3159
|
});
|
|
3157
3160
|
}
|
|
3158
|
-
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect) {
|
|
3161
|
+
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect, visitPath = [], depth = 0, stats) {
|
|
3162
|
+
if (!stats) stats = { totalIncludes: 0, totalSubqueries: 0, maxDepth: 0 };
|
|
3163
|
+
if (depth > MAX_INCLUDE_DEPTH) {
|
|
3164
|
+
throw new Error(
|
|
3165
|
+
`Maximum include depth of ${MAX_INCLUDE_DEPTH} exceeded. Path: ${visitPath.join(" -> ")}. Deep includes cause exponential SQL complexity and performance issues.`
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
stats.maxDepth = Math.max(stats.maxDepth, depth);
|
|
3159
3169
|
const includes = [];
|
|
3160
3170
|
const entries = relationEntriesFromArgs(args, model);
|
|
3161
3171
|
for (const [relName, relArgs] of entries) {
|
|
3162
3172
|
if (relArgs === false) continue;
|
|
3173
|
+
stats.totalIncludes++;
|
|
3174
|
+
if (stats.totalIncludes > MAX_TOTAL_INCLUDES) {
|
|
3175
|
+
throw new Error(
|
|
3176
|
+
`Maximum total includes (${MAX_TOTAL_INCLUDES}) exceeded. Current: ${stats.totalIncludes} includes. Query has ${stats.maxDepth} levels deep. Simplify your query structure or use multiple queries.`
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
stats.totalSubqueries++;
|
|
3180
|
+
if (stats.totalSubqueries > MAX_TOTAL_SUBQUERIES) {
|
|
3181
|
+
throw new Error(
|
|
3182
|
+
`Query complexity limit exceeded: ${stats.totalSubqueries} subqueries generated. Maximum allowed: ${MAX_TOTAL_SUBQUERIES}. This indicates exponential include nesting. Stats: depth=${stats.maxDepth}, includes=${stats.totalIncludes}. Path: ${visitPath.join(" -> ")}. Simplify your include structure or split into multiple queries.`
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3163
3185
|
const resolved = resolveRelationOrThrow(model, schemas, relName);
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3186
|
+
const relationPath = `${model.name}.${relName}`;
|
|
3187
|
+
const currentPath = [...visitPath, relationPath];
|
|
3188
|
+
if (visitPath.includes(relationPath)) {
|
|
3189
|
+
throw new Error(
|
|
3190
|
+
`Circular include detected: ${currentPath.join(" -> ")}. Relation '${relationPath}' creates an infinite loop.`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
const modelOccurrences = currentPath.filter(
|
|
3194
|
+
(p) => p.startsWith(`${resolved.relModel.name}.`)
|
|
3195
|
+
).length;
|
|
3196
|
+
if (modelOccurrences > 2) {
|
|
3197
|
+
throw new Error(
|
|
3198
|
+
`Include too deeply nested: model '${resolved.relModel.name}' appears ${modelOccurrences} times in path: ${currentPath.join(" -> ")}`
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
includes.push(
|
|
3202
|
+
buildSingleInclude(relName, relArgs, resolved.field, resolved.relModel, {
|
|
3170
3203
|
model,
|
|
3171
3204
|
schemas,
|
|
3172
3205
|
parentAlias,
|
|
3173
3206
|
aliasGen,
|
|
3174
3207
|
dialect,
|
|
3175
|
-
params
|
|
3176
|
-
|
|
3208
|
+
params,
|
|
3209
|
+
visitPath: currentPath,
|
|
3210
|
+
depth: depth + 1,
|
|
3211
|
+
stats
|
|
3212
|
+
})
|
|
3177
3213
|
);
|
|
3178
|
-
includes.push(include);
|
|
3179
3214
|
}
|
|
3180
3215
|
return includes;
|
|
3181
3216
|
}
|
|
3182
3217
|
function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
3183
3218
|
const aliasGen = createAliasGenerator();
|
|
3219
|
+
const stats = {
|
|
3220
|
+
totalIncludes: 0,
|
|
3221
|
+
totalSubqueries: 0,
|
|
3222
|
+
maxDepth: 0
|
|
3223
|
+
};
|
|
3184
3224
|
return buildIncludeSqlInternal(
|
|
3185
3225
|
args,
|
|
3186
3226
|
model,
|
|
@@ -3188,7 +3228,10 @@ function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
|
3188
3228
|
parentAlias,
|
|
3189
3229
|
aliasGen,
|
|
3190
3230
|
params,
|
|
3191
|
-
dialect
|
|
3231
|
+
dialect,
|
|
3232
|
+
[],
|
|
3233
|
+
0,
|
|
3234
|
+
stats
|
|
3192
3235
|
);
|
|
3193
3236
|
}
|
|
3194
3237
|
function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
@@ -3199,10 +3242,14 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3199
3242
|
);
|
|
3200
3243
|
}
|
|
3201
3244
|
const field = model.fields.find((f) => f.name === relName);
|
|
3202
|
-
if (!field)
|
|
3245
|
+
if (!field)
|
|
3203
3246
|
throw new Error(
|
|
3204
3247
|
`_count.${relName} references unknown relation on model ${model.name}`
|
|
3205
3248
|
);
|
|
3249
|
+
if (!isValidRelationField(field)) {
|
|
3250
|
+
throw new Error(
|
|
3251
|
+
`_count.${relName} has invalid relation metadata on model ${model.name}`
|
|
3252
|
+
);
|
|
3206
3253
|
}
|
|
3207
3254
|
const relModel = schemas.find((m) => m.name === field.relatedModel);
|
|
3208
3255
|
if (!relModel) {
|
|
@@ -3212,31 +3259,78 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3212
3259
|
}
|
|
3213
3260
|
return { field, relModel };
|
|
3214
3261
|
}
|
|
3215
|
-
function
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3262
|
+
function defaultReferencesForCount(fkCount) {
|
|
3263
|
+
if (fkCount === 1) return ["id"];
|
|
3264
|
+
throw new Error(
|
|
3265
|
+
"Relation count for composite keys requires explicit references matching foreignKey length"
|
|
3266
|
+
);
|
|
3219
3267
|
}
|
|
3220
|
-
function
|
|
3268
|
+
function resolveCountKeyPairs(field) {
|
|
3221
3269
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
3222
|
-
|
|
3223
|
-
|
|
3270
|
+
if (fkFields.length === 0)
|
|
3271
|
+
throw new Error("Relation count requires foreignKey");
|
|
3272
|
+
const refsRaw = field.references;
|
|
3273
|
+
const refs = normalizeKeyList(refsRaw);
|
|
3274
|
+
const refFields = refs.length > 0 ? refs : defaultReferencesForCount(fkFields.length);
|
|
3275
|
+
if (refFields.length !== fkFields.length) {
|
|
3276
|
+
throw new Error(
|
|
3277
|
+
"Relation count requires references count to match foreignKey count"
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
const relKeyFields = field.isForeignKeyLocal ? refFields : fkFields;
|
|
3281
|
+
const parentKeyFields = field.isForeignKeyLocal ? fkFields : refFields;
|
|
3282
|
+
return { relKeyFields, parentKeyFields };
|
|
3283
|
+
}
|
|
3284
|
+
function aliasQualifiedColumn(alias, model, field) {
|
|
3285
|
+
return `${alias}.${quoteColumn(model, field)}`;
|
|
3286
|
+
}
|
|
3287
|
+
function subqueryForCount(args) {
|
|
3288
|
+
const selectKeys = args.relKeyFields.map(
|
|
3289
|
+
(f, i) => `${aliasQualifiedColumn(args.countAlias, args.relModel, f)} AS "__fk${i}"`
|
|
3290
|
+
).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3291
|
+
const groupByKeys = args.relKeyFields.map((f) => aliasQualifiedColumn(args.countAlias, args.relModel, f)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3292
|
+
const cntExpr = args.dialect === "postgres" ? "COUNT(*)::int AS __cnt" : "COUNT(*) AS __cnt";
|
|
3293
|
+
return `(SELECT ${selectKeys}${SQL_SEPARATORS.FIELD_LIST}${cntExpr} FROM ${args.relTable} ${args.countAlias} GROUP BY ${groupByKeys})`;
|
|
3224
3294
|
}
|
|
3225
|
-
function
|
|
3226
|
-
|
|
3295
|
+
function leftJoinOnForCount(args) {
|
|
3296
|
+
const parts = args.parentKeyFields.map(
|
|
3297
|
+
(f, i) => `${args.joinAlias}."__fk${i}" = ${aliasQualifiedColumn(args.parentAlias, args.parentModel, f)}`
|
|
3298
|
+
);
|
|
3299
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
3300
|
+
}
|
|
3301
|
+
function nextAliasAvoiding(aliasGen, base, forbidden) {
|
|
3302
|
+
let a = aliasGen.next(base);
|
|
3303
|
+
while (forbidden.has(a)) a = aliasGen.next(base);
|
|
3304
|
+
return a;
|
|
3227
3305
|
}
|
|
3228
3306
|
function buildCountJoinAndPair(args) {
|
|
3229
3307
|
const relTable = getRelationTableReference(args.relModel, args.dialect);
|
|
3230
|
-
const
|
|
3231
|
-
const
|
|
3232
|
-
const
|
|
3233
|
-
args.
|
|
3308
|
+
const { relKeyFields, parentKeyFields } = resolveCountKeyPairs(args.field);
|
|
3309
|
+
const forbidden = /* @__PURE__ */ new Set([args.parentAlias]);
|
|
3310
|
+
const countAlias = nextAliasAvoiding(
|
|
3311
|
+
args.aliasGen,
|
|
3312
|
+
`__tp_cnt_${args.relName}`,
|
|
3313
|
+
forbidden
|
|
3314
|
+
);
|
|
3315
|
+
forbidden.add(countAlias);
|
|
3316
|
+
const subquery = subqueryForCount({
|
|
3317
|
+
dialect: args.dialect,
|
|
3234
3318
|
relTable,
|
|
3235
3319
|
countAlias,
|
|
3236
|
-
|
|
3320
|
+
relModel: args.relModel,
|
|
3321
|
+
relKeyFields
|
|
3322
|
+
});
|
|
3323
|
+
const joinAlias = nextAliasAvoiding(
|
|
3324
|
+
args.aliasGen,
|
|
3325
|
+
`__tp_cnt_j_${args.relName}`,
|
|
3326
|
+
forbidden
|
|
3237
3327
|
);
|
|
3238
|
-
const
|
|
3239
|
-
|
|
3328
|
+
const leftJoinOn = leftJoinOnForCount({
|
|
3329
|
+
joinAlias,
|
|
3330
|
+
parentAlias: args.parentAlias,
|
|
3331
|
+
parentModel: args.parentModel,
|
|
3332
|
+
parentKeyFields
|
|
3333
|
+
});
|
|
3240
3334
|
return {
|
|
3241
3335
|
joinSql: `LEFT JOIN ${subquery} ${joinAlias} ON ${leftJoinOn}`,
|
|
3242
3336
|
pairSql: `${sqlStringLiteral(args.relName)}, COALESCE(${joinAlias}.__cnt, 0)`
|
|
@@ -3245,6 +3339,7 @@ function buildCountJoinAndPair(args) {
|
|
|
3245
3339
|
function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params, dialect) {
|
|
3246
3340
|
const joins = [];
|
|
3247
3341
|
const pairs = [];
|
|
3342
|
+
const aliasGen = createAliasGenerator();
|
|
3248
3343
|
for (const [relName, shouldCount] of Object.entries(countSelect)) {
|
|
3249
3344
|
if (!shouldCount) continue;
|
|
3250
3345
|
const resolved = resolveCountRelationOrThrow(relName, model, schemas);
|
|
@@ -3252,29 +3347,33 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3252
3347
|
relName,
|
|
3253
3348
|
field: resolved.field,
|
|
3254
3349
|
relModel: resolved.relModel,
|
|
3350
|
+
parentModel: model,
|
|
3255
3351
|
parentAlias,
|
|
3256
|
-
dialect
|
|
3352
|
+
dialect,
|
|
3353
|
+
aliasGen
|
|
3257
3354
|
});
|
|
3258
3355
|
joins.push(built.joinSql);
|
|
3259
3356
|
pairs.push(built.pairSql);
|
|
3260
3357
|
}
|
|
3261
|
-
return {
|
|
3262
|
-
joins,
|
|
3263
|
-
jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST)
|
|
3264
|
-
};
|
|
3358
|
+
return { joins, jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST) };
|
|
3265
3359
|
}
|
|
3266
3360
|
|
|
3267
3361
|
// src/builder/select/assembly.ts
|
|
3268
|
-
var
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
}
|
|
3362
|
+
var ALIAS_CAPTURE = "([A-Za-z_][A-Za-z0-9_]*)";
|
|
3363
|
+
var COLUMN_PART = '(?:"([^"]+)"|([a-z_][a-z0-9_]*))';
|
|
3364
|
+
var AS_PART = `(?:\\s+AS\\s+${COLUMN_PART})?`;
|
|
3365
|
+
var SIMPLE_COLUMN_PATTERN = `^${ALIAS_CAPTURE}\\.${COLUMN_PART}${AS_PART}$`;
|
|
3366
|
+
var SIMPLE_COLUMN_RE = new RegExp(SIMPLE_COLUMN_PATTERN, "i");
|
|
3272
3367
|
function joinNonEmpty(parts, sep) {
|
|
3273
3368
|
return parts.filter((s) => s.trim().length > 0).join(sep);
|
|
3274
3369
|
}
|
|
3275
3370
|
function buildWhereSql(conditions) {
|
|
3276
3371
|
if (!isNonEmptyArray(conditions)) return "";
|
|
3277
|
-
|
|
3372
|
+
const parts = [
|
|
3373
|
+
SQL_TEMPLATES.WHERE,
|
|
3374
|
+
conditions.join(SQL_SEPARATORS.CONDITION_AND)
|
|
3375
|
+
];
|
|
3376
|
+
return ` ${parts.join(" ")}`;
|
|
3278
3377
|
}
|
|
3279
3378
|
function buildJoinsSql(...joinGroups) {
|
|
3280
3379
|
const all = [];
|
|
@@ -3289,37 +3388,43 @@ function buildSelectList(baseSelect, extraCols) {
|
|
|
3289
3388
|
if (base && extra) return `${base}${SQL_SEPARATORS.FIELD_LIST}${extra}`;
|
|
3290
3389
|
return base || extra;
|
|
3291
3390
|
}
|
|
3292
|
-
function finalizeSql(sql, params) {
|
|
3391
|
+
function finalizeSql(sql, params, dialect) {
|
|
3293
3392
|
const snapshot = params.snapshot();
|
|
3294
3393
|
validateSelectQuery(sql);
|
|
3295
|
-
|
|
3394
|
+
validateParamConsistencyByDialect(
|
|
3395
|
+
sql,
|
|
3396
|
+
snapshot.params,
|
|
3397
|
+
dialect === "sqlite" ? "postgres" : dialect
|
|
3398
|
+
);
|
|
3296
3399
|
return Object.freeze({
|
|
3297
3400
|
sql,
|
|
3298
|
-
params:
|
|
3401
|
+
params: snapshot.params,
|
|
3299
3402
|
paramMappings: Object.freeze(snapshot.mappings)
|
|
3300
3403
|
});
|
|
3301
3404
|
}
|
|
3302
|
-
function parseSimpleScalarSelect(select,
|
|
3303
|
-
var _a, _b;
|
|
3405
|
+
function parseSimpleScalarSelect(select, fromAlias) {
|
|
3406
|
+
var _a, _b, _c, _d;
|
|
3304
3407
|
const raw = select.trim();
|
|
3305
3408
|
if (raw.length === 0) return [];
|
|
3306
|
-
let re = SIMPLE_SELECT_RE_CACHE.get(alias);
|
|
3307
|
-
if (!re) {
|
|
3308
|
-
const safeAlias2 = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3309
|
-
re = new RegExp(`^${safeAlias2}\\.(?:"([^"]+)"|([a-z_][a-z0-9_]*))$`, "i");
|
|
3310
|
-
SIMPLE_SELECT_RE_CACHE.set(alias, re);
|
|
3311
|
-
}
|
|
3312
3409
|
const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
|
|
3313
3410
|
const names = [];
|
|
3314
3411
|
for (const part of parts) {
|
|
3315
3412
|
const p = part.trim();
|
|
3316
|
-
const m = p.match(
|
|
3413
|
+
const m = p.match(SIMPLE_COLUMN_RE);
|
|
3317
3414
|
if (!m) {
|
|
3318
3415
|
throw new Error(
|
|
3319
|
-
`sqlite distinct emulation requires scalar select fields to be simple columns. Got: ${p}`
|
|
3416
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (optionally with AS). Got: ${p}`
|
|
3320
3417
|
);
|
|
3321
3418
|
}
|
|
3322
|
-
const
|
|
3419
|
+
const actualAlias = m[1];
|
|
3420
|
+
if (actualAlias.toLowerCase() !== fromAlias.toLowerCase()) {
|
|
3421
|
+
throw new Error(
|
|
3422
|
+
`Expected alias '${fromAlias}', got '${actualAlias}' in: ${p}`
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
const columnName = ((_b = (_a = m[2]) != null ? _a : m[3]) != null ? _b : "").trim();
|
|
3426
|
+
const outAlias = ((_d = (_c = m[4]) != null ? _c : m[5]) != null ? _d : "").trim();
|
|
3427
|
+
const name = outAlias.length > 0 ? outAlias : columnName;
|
|
3323
3428
|
if (name.length === 0) {
|
|
3324
3429
|
throw new Error(`Failed to parse selected column name from: ${p}`);
|
|
3325
3430
|
}
|
|
@@ -3328,18 +3433,18 @@ function parseSimpleScalarSelect(select, alias) {
|
|
|
3328
3433
|
return names;
|
|
3329
3434
|
}
|
|
3330
3435
|
function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
|
|
3331
|
-
const
|
|
3332
|
-
|
|
3333
|
-
|
|
3436
|
+
const src = String(fromAlias);
|
|
3437
|
+
if (src.length === 0) return orderBy;
|
|
3438
|
+
const escaped = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3439
|
+
const re = new RegExp(`\\b${escaped}\\.`, "gi");
|
|
3440
|
+
return orderBy.replace(re, `${outerAlias}.`);
|
|
3334
3441
|
}
|
|
3335
3442
|
function buildDistinctColumns(distinct, fromAlias, model) {
|
|
3336
3443
|
return distinct.map((f) => col(fromAlias, f, model)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3337
3444
|
}
|
|
3338
3445
|
function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
3339
3446
|
const outputCols = [...scalarNames, ...includeNames];
|
|
3340
|
-
if (hasCount)
|
|
3341
|
-
outputCols.push("_count");
|
|
3342
|
-
}
|
|
3447
|
+
if (hasCount) outputCols.push("_count");
|
|
3343
3448
|
const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3344
3449
|
if (!isNonEmptyString(formatted)) {
|
|
3345
3450
|
throw new Error("distinct emulation requires at least one output column");
|
|
@@ -3348,9 +3453,10 @@ function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
|
3348
3453
|
}
|
|
3349
3454
|
function buildWindowOrder(args) {
|
|
3350
3455
|
const { baseOrder, idField, fromAlias, model } = args;
|
|
3456
|
+
const fromLower = String(fromAlias).toLowerCase();
|
|
3351
3457
|
const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
|
|
3352
3458
|
const hasIdInOrder = orderFields.some(
|
|
3353
|
-
(f) => f.startsWith(`${
|
|
3459
|
+
(f) => f.startsWith(`${fromLower}.id `) || f.startsWith(`${fromLower}."id" `)
|
|
3354
3460
|
);
|
|
3355
3461
|
if (hasIdInOrder) return baseOrder;
|
|
3356
3462
|
const idTiebreaker = idField ? `, ${col(fromAlias, "id", model)} ASC` : "";
|
|
@@ -3385,15 +3491,37 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3385
3491
|
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, `"__tp_distinct"`) : replaceOrderByAlias(fallbackOrder, from.alias, `"__tp_distinct"`);
|
|
3386
3492
|
const joins = buildJoinsSql(whereJoins, countJoins);
|
|
3387
3493
|
const conditions = [];
|
|
3388
|
-
if (whereClause && whereClause !== "1=1")
|
|
3389
|
-
conditions.push(whereClause);
|
|
3390
|
-
}
|
|
3494
|
+
if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
|
|
3391
3495
|
const whereSql = buildWhereSql(conditions);
|
|
3392
3496
|
const innerSelectList = selectWithIncludes.trim();
|
|
3393
3497
|
const innerComma = innerSelectList.length > 0 ? SQL_SEPARATORS.FIELD_LIST : "";
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
|
|
3498
|
+
const innerParts = [
|
|
3499
|
+
SQL_TEMPLATES.SELECT,
|
|
3500
|
+
innerSelectList + innerComma,
|
|
3501
|
+
`ROW_NUMBER() OVER (PARTITION BY ${distinctCols} ORDER BY ${windowOrder})`,
|
|
3502
|
+
SQL_TEMPLATES.AS,
|
|
3503
|
+
'"__tp_rn"',
|
|
3504
|
+
SQL_TEMPLATES.FROM,
|
|
3505
|
+
from.table,
|
|
3506
|
+
from.alias
|
|
3507
|
+
];
|
|
3508
|
+
if (joins) innerParts.push(joins);
|
|
3509
|
+
if (whereSql) innerParts.push(whereSql);
|
|
3510
|
+
const inner = innerParts.filter(Boolean).join(" ");
|
|
3511
|
+
const outerParts = [
|
|
3512
|
+
SQL_TEMPLATES.SELECT,
|
|
3513
|
+
outerSelectCols,
|
|
3514
|
+
SQL_TEMPLATES.FROM,
|
|
3515
|
+
`(${inner})`,
|
|
3516
|
+
SQL_TEMPLATES.AS,
|
|
3517
|
+
'"__tp_distinct"',
|
|
3518
|
+
SQL_TEMPLATES.WHERE,
|
|
3519
|
+
'"__tp_rn" = 1'
|
|
3520
|
+
];
|
|
3521
|
+
if (isNonEmptyString(outerOrder)) {
|
|
3522
|
+
outerParts.push(SQL_TEMPLATES.ORDER_BY, outerOrder);
|
|
3523
|
+
}
|
|
3524
|
+
return outerParts.filter(Boolean).join(" ");
|
|
3397
3525
|
}
|
|
3398
3526
|
function buildIncludeColumns(spec) {
|
|
3399
3527
|
var _a, _b;
|
|
@@ -3521,6 +3649,7 @@ function constructFinalSql(spec) {
|
|
|
3521
3649
|
orderBy,
|
|
3522
3650
|
distinct,
|
|
3523
3651
|
method,
|
|
3652
|
+
cursorCte,
|
|
3524
3653
|
cursorClause,
|
|
3525
3654
|
params,
|
|
3526
3655
|
dialect,
|
|
@@ -3535,9 +3664,13 @@ function constructFinalSql(spec) {
|
|
|
3535
3664
|
const spec2 = withCountJoins(spec, countJoins, whereJoins);
|
|
3536
3665
|
let sql2 = buildSqliteDistinctQuery(spec2, selectWithIncludes).trim();
|
|
3537
3666
|
sql2 = appendPagination(sql2, spec);
|
|
3538
|
-
return finalizeSql(sql2, params);
|
|
3667
|
+
return finalizeSql(sql2, params, dialect);
|
|
3539
3668
|
}
|
|
3540
|
-
const parts = [
|
|
3669
|
+
const parts = [];
|
|
3670
|
+
if (cursorCte) {
|
|
3671
|
+
parts.push(`WITH ${cursorCte}`);
|
|
3672
|
+
}
|
|
3673
|
+
parts.push(SQL_TEMPLATES.SELECT);
|
|
3541
3674
|
const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct, model) : null;
|
|
3542
3675
|
if (distinctOn) parts.push(distinctOn);
|
|
3543
3676
|
const baseSelect = (select != null ? select : "").trim();
|
|
@@ -3553,7 +3686,7 @@ function constructFinalSql(spec) {
|
|
|
3553
3686
|
if (isNonEmptyString(orderBy)) parts.push(SQL_TEMPLATES.ORDER_BY, orderBy);
|
|
3554
3687
|
let sql = parts.join(" ").trim();
|
|
3555
3688
|
sql = appendPagination(sql, spec);
|
|
3556
|
-
return finalizeSql(sql, params);
|
|
3689
|
+
return finalizeSql(sql, params, dialect);
|
|
3557
3690
|
}
|
|
3558
3691
|
|
|
3559
3692
|
// src/builder/select.ts
|
|
@@ -3586,7 +3719,7 @@ function buildPostgresDistinctOrderBy(distinctFields, existing) {
|
|
|
3586
3719
|
}
|
|
3587
3720
|
return next;
|
|
3588
3721
|
}
|
|
3589
|
-
function applyPostgresDistinctOrderBy(args
|
|
3722
|
+
function applyPostgresDistinctOrderBy(args) {
|
|
3590
3723
|
const distinctFields = normalizeDistinctFields(args.distinct);
|
|
3591
3724
|
if (distinctFields.length === 0) return args;
|
|
3592
3725
|
if (!isNotNullish(args.orderBy)) return args;
|
|
@@ -3596,19 +3729,6 @@ function applyPostgresDistinctOrderBy(args, _model) {
|
|
|
3596
3729
|
orderBy: buildPostgresDistinctOrderBy(distinctFields, existing)
|
|
3597
3730
|
});
|
|
3598
3731
|
}
|
|
3599
|
-
function assertScalarFieldOnModel(model, fieldName, ctx) {
|
|
3600
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
3601
|
-
if (!f) {
|
|
3602
|
-
throw new Error(
|
|
3603
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}`
|
|
3604
|
-
);
|
|
3605
|
-
}
|
|
3606
|
-
if (f.isRelation) {
|
|
3607
|
-
throw new Error(
|
|
3608
|
-
`${ctx} does not support relation field '${fieldName}' on model ${model.name}`
|
|
3609
|
-
);
|
|
3610
|
-
}
|
|
3611
|
-
}
|
|
3612
3732
|
function validateDistinct(model, distinct) {
|
|
3613
3733
|
if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) return;
|
|
3614
3734
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3619,24 +3739,24 @@ function validateDistinct(model, distinct) {
|
|
|
3619
3739
|
throw new Error(`distinct must not contain duplicates (field: '${f}')`);
|
|
3620
3740
|
}
|
|
3621
3741
|
seen.add(f);
|
|
3622
|
-
|
|
3742
|
+
assertScalarField(model, f, "distinct");
|
|
3623
3743
|
}
|
|
3624
3744
|
}
|
|
3625
|
-
function validateOrderByValue(fieldName, v) {
|
|
3626
|
-
parseOrderByValue(v, fieldName);
|
|
3627
|
-
}
|
|
3628
3745
|
function validateOrderBy(model, orderBy) {
|
|
3629
3746
|
if (!isNotNullish(orderBy)) return;
|
|
3630
3747
|
const items = normalizeOrderByInput2(orderBy);
|
|
3631
3748
|
if (items.length === 0) return;
|
|
3632
3749
|
for (const it of items) {
|
|
3633
3750
|
const entries = Object.entries(it);
|
|
3751
|
+
if (entries.length !== 1) {
|
|
3752
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
3753
|
+
}
|
|
3634
3754
|
const fieldName = String(entries[0][0]).trim();
|
|
3635
3755
|
if (fieldName.length === 0) {
|
|
3636
3756
|
throw new Error("orderBy field name cannot be empty");
|
|
3637
3757
|
}
|
|
3638
|
-
|
|
3639
|
-
|
|
3758
|
+
assertScalarField(model, fieldName, "orderBy");
|
|
3759
|
+
parseOrderByValue(entries[0][1], fieldName);
|
|
3640
3760
|
}
|
|
3641
3761
|
}
|
|
3642
3762
|
function validateCursor(model, cursor) {
|
|
@@ -3653,7 +3773,7 @@ function validateCursor(model, cursor) {
|
|
|
3653
3773
|
if (f.length === 0) {
|
|
3654
3774
|
throw new Error("cursor field name cannot be empty");
|
|
3655
3775
|
}
|
|
3656
|
-
|
|
3776
|
+
assertScalarField(model, f, "cursor");
|
|
3657
3777
|
}
|
|
3658
3778
|
}
|
|
3659
3779
|
function resolveDialect(dialect) {
|
|
@@ -3672,20 +3792,21 @@ function normalizeArgsForNegativeTake(method, args) {
|
|
|
3672
3792
|
orderBy: reverseOrderByInput(args.orderBy)
|
|
3673
3793
|
});
|
|
3674
3794
|
}
|
|
3675
|
-
function normalizeArgsForDialect(dialect, args
|
|
3795
|
+
function normalizeArgsForDialect(dialect, args) {
|
|
3676
3796
|
if (dialect !== "postgres") return args;
|
|
3677
3797
|
return applyPostgresDistinctOrderBy(args);
|
|
3678
3798
|
}
|
|
3679
3799
|
function buildCursorClauseIfAny(input) {
|
|
3680
|
-
const { cursor, orderBy, tableName, alias, params, dialect } = input;
|
|
3681
|
-
if (!isNotNullish(cursor)) return
|
|
3800
|
+
const { cursor, orderBy, tableName, alias, params, dialect, model } = input;
|
|
3801
|
+
if (!isNotNullish(cursor)) return {};
|
|
3682
3802
|
return buildCursorCondition(
|
|
3683
3803
|
cursor,
|
|
3684
3804
|
orderBy,
|
|
3685
3805
|
tableName,
|
|
3686
3806
|
alias,
|
|
3687
3807
|
params,
|
|
3688
|
-
dialect
|
|
3808
|
+
dialect,
|
|
3809
|
+
model
|
|
3689
3810
|
);
|
|
3690
3811
|
}
|
|
3691
3812
|
function buildSelectSpec(input) {
|
|
@@ -3724,14 +3845,20 @@ function buildSelectSpec(input) {
|
|
|
3724
3845
|
params,
|
|
3725
3846
|
dialect
|
|
3726
3847
|
);
|
|
3727
|
-
const
|
|
3848
|
+
const cursorResult = buildCursorClauseIfAny({
|
|
3728
3849
|
cursor,
|
|
3729
3850
|
orderBy: normalizedArgs.orderBy,
|
|
3730
3851
|
tableName,
|
|
3731
3852
|
alias,
|
|
3732
3853
|
params,
|
|
3733
|
-
dialect
|
|
3854
|
+
dialect,
|
|
3855
|
+
model
|
|
3734
3856
|
});
|
|
3857
|
+
if (dialect === "sqlite" && isNonEmptyArray(normalizedArgs.distinct) && cursorResult.condition) {
|
|
3858
|
+
throw new Error(
|
|
3859
|
+
"Cursor pagination with distinct is not supported in SQLite due to window function limitations. Use findMany with skip/take instead, or remove distinct."
|
|
3860
|
+
);
|
|
3861
|
+
}
|
|
3735
3862
|
return {
|
|
3736
3863
|
select: selectFields,
|
|
3737
3864
|
includes,
|
|
@@ -3742,7 +3869,8 @@ function buildSelectSpec(input) {
|
|
|
3742
3869
|
pagination: { take, skip },
|
|
3743
3870
|
distinct: normalizedArgs.distinct,
|
|
3744
3871
|
method,
|
|
3745
|
-
|
|
3872
|
+
cursorCte: cursorResult.cte,
|
|
3873
|
+
cursorClause: cursorResult.condition,
|
|
3746
3874
|
params,
|
|
3747
3875
|
dialect,
|
|
3748
3876
|
model,
|
|
@@ -3756,9 +3884,7 @@ function buildSelectSql(input) {
|
|
|
3756
3884
|
assertSafeTableRef(from.tableName);
|
|
3757
3885
|
const dialectToUse = resolveDialect(dialect);
|
|
3758
3886
|
const argsForSql = normalizeArgsForNegativeTake(method, args);
|
|
3759
|
-
const normalizedArgs = normalizeArgsForDialect(
|
|
3760
|
-
dialectToUse,
|
|
3761
|
-
argsForSql);
|
|
3887
|
+
const normalizedArgs = normalizeArgsForDialect(dialectToUse, argsForSql);
|
|
3762
3888
|
validateDistinct(model, normalizedArgs.distinct);
|
|
3763
3889
|
validateOrderBy(model, normalizedArgs.orderBy);
|
|
3764
3890
|
validateCursor(model, normalizedArgs.cursor);
|
|
@@ -3774,8 +3900,21 @@ function buildSelectSql(input) {
|
|
|
3774
3900
|
});
|
|
3775
3901
|
return constructFinalSql(spec);
|
|
3776
3902
|
}
|
|
3777
|
-
|
|
3778
|
-
|
|
3903
|
+
|
|
3904
|
+
// src/builder/shared/comparison-builder.ts
|
|
3905
|
+
function buildComparisons(expr, filter, params, dialect, builder, excludeKeys = /* @__PURE__ */ new Set(["mode"])) {
|
|
3906
|
+
const out = [];
|
|
3907
|
+
for (const [op, val] of Object.entries(filter)) {
|
|
3908
|
+
if (excludeKeys.has(op) || val === void 0) continue;
|
|
3909
|
+
const built = builder(expr, op, val, params, dialect);
|
|
3910
|
+
if (built && built.trim().length > 0) {
|
|
3911
|
+
out.push(built);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
return out;
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
// src/builder/aggregates.ts
|
|
3779
3918
|
var AGGREGATES = [
|
|
3780
3919
|
["_sum", "SUM"],
|
|
3781
3920
|
["_avg", "AVG"],
|
|
@@ -3790,22 +3929,32 @@ var COMPARISON_OPS = {
|
|
|
3790
3929
|
[Ops.LT]: "<",
|
|
3791
3930
|
[Ops.LTE]: "<="
|
|
3792
3931
|
};
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
MODEL_FIELD_CACHE.set(model, m);
|
|
3804
|
-
return m;
|
|
3805
|
-
}
|
|
3932
|
+
var HAVING_ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
3933
|
+
Ops.EQUALS,
|
|
3934
|
+
Ops.NOT,
|
|
3935
|
+
Ops.GT,
|
|
3936
|
+
Ops.GTE,
|
|
3937
|
+
Ops.LT,
|
|
3938
|
+
Ops.LTE,
|
|
3939
|
+
Ops.IN,
|
|
3940
|
+
Ops.NOT_IN
|
|
3941
|
+
]);
|
|
3806
3942
|
function isTruthySelection(v) {
|
|
3807
3943
|
return v === true;
|
|
3808
3944
|
}
|
|
3945
|
+
function isLogicalKey(key) {
|
|
3946
|
+
return key === LogicalOps.AND || key === LogicalOps.OR || key === LogicalOps.NOT;
|
|
3947
|
+
}
|
|
3948
|
+
function isAggregateKey(key) {
|
|
3949
|
+
return key === "_count" || key === "_sum" || key === "_avg" || key === "_min" || key === "_max";
|
|
3950
|
+
}
|
|
3951
|
+
function assertHavingOp(op) {
|
|
3952
|
+
if (!HAVING_ALLOWED_OPS.has(op)) {
|
|
3953
|
+
throw new Error(
|
|
3954
|
+
`Unsupported HAVING operator '${op}'. Allowed: ${[...HAVING_ALLOWED_OPS].join(", ")}`
|
|
3955
|
+
);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3809
3958
|
function aggExprForField(aggKey, field, alias, model) {
|
|
3810
3959
|
if (aggKey === "_count") {
|
|
3811
3960
|
return field === "_all" ? `COUNT(*)` : `COUNT(${col(alias, field, model)})`;
|
|
@@ -3839,32 +3988,9 @@ function normalizeLogicalValue2(operator, value) {
|
|
|
3839
3988
|
}
|
|
3840
3989
|
return out;
|
|
3841
3990
|
}
|
|
3842
|
-
if (isPlainObject(value))
|
|
3843
|
-
return [value];
|
|
3844
|
-
}
|
|
3991
|
+
if (isPlainObject(value)) return [value];
|
|
3845
3992
|
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
3846
3993
|
}
|
|
3847
|
-
function assertScalarField2(model, fieldName, ctx) {
|
|
3848
|
-
const m = getModelFieldMap(model);
|
|
3849
|
-
const field = m.get(fieldName);
|
|
3850
|
-
if (!field) {
|
|
3851
|
-
throw new Error(
|
|
3852
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}. Available fields: ${model.fields.map((f) => f.name).join(", ")}`
|
|
3853
|
-
);
|
|
3854
|
-
}
|
|
3855
|
-
if (field.isRelation) {
|
|
3856
|
-
throw new Error(`${ctx} does not support relation field '${fieldName}'`);
|
|
3857
|
-
}
|
|
3858
|
-
return { name: field.name, type: field.type };
|
|
3859
|
-
}
|
|
3860
|
-
function assertAggregateFieldType(aggKey, fieldType, fieldName, modelName) {
|
|
3861
|
-
const baseType = fieldType.replace(/\[\]|\?/g, "");
|
|
3862
|
-
if ((aggKey === "_sum" || aggKey === "_avg") && !NUMERIC_TYPES.has(baseType)) {
|
|
3863
|
-
throw new Error(
|
|
3864
|
-
`Cannot use ${aggKey} on non-numeric field '${fieldName}' (type: ${fieldType}) on model ${modelName}`
|
|
3865
|
-
);
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
3994
|
function buildNullComparison(expr, op) {
|
|
3869
3995
|
if (op === Ops.EQUALS) return `${expr} ${SQL_TEMPLATES.IS_NULL}`;
|
|
3870
3996
|
if (op === Ops.NOT) return `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
|
|
@@ -3891,6 +4017,7 @@ function buildBinaryComparison(expr, op, val, params) {
|
|
|
3891
4017
|
return `${expr} ${sqlOp} ${placeholder}`;
|
|
3892
4018
|
}
|
|
3893
4019
|
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
4020
|
+
assertHavingOp(op);
|
|
3894
4021
|
if (val === null) return buildNullComparison(expr, op);
|
|
3895
4022
|
if (op === Ops.NOT && isPlainObject(val)) {
|
|
3896
4023
|
return buildNotComposite(
|
|
@@ -3907,12 +4034,6 @@ function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
|
3907
4034
|
}
|
|
3908
4035
|
return buildBinaryComparison(expr, op, val, params);
|
|
3909
4036
|
}
|
|
3910
|
-
function isLogicalKey(key) {
|
|
3911
|
-
return key === LogicalOps.AND || key === LogicalOps.OR || key === LogicalOps.NOT;
|
|
3912
|
-
}
|
|
3913
|
-
function isAggregateKey(key) {
|
|
3914
|
-
return key === "_count" || key === "_sum" || key === "_avg" || key === "_min" || key === "_max";
|
|
3915
|
-
}
|
|
3916
4037
|
function negateClauses(subClauses) {
|
|
3917
4038
|
if (subClauses.length === 1) return `${SQL_TEMPLATES.NOT} ${subClauses[0]}`;
|
|
3918
4039
|
return `${SQL_TEMPLATES.NOT} (${subClauses.join(SQL_SEPARATORS.CONDITION_AND)})`;
|
|
@@ -3921,16 +4042,75 @@ function combineLogical(key, subClauses) {
|
|
|
3921
4042
|
if (key === LogicalOps.NOT) return negateClauses(subClauses);
|
|
3922
4043
|
return subClauses.join(` ${key} `);
|
|
3923
4044
|
}
|
|
4045
|
+
function buildHavingNode(node, alias, params, dialect, model) {
|
|
4046
|
+
const clauses = [];
|
|
4047
|
+
const entries = Object.entries(node);
|
|
4048
|
+
for (const [key, value] of entries) {
|
|
4049
|
+
const built = buildHavingEntry(key, value, alias, params, dialect, model);
|
|
4050
|
+
for (const c of built) {
|
|
4051
|
+
if (c && c.trim().length > 0) clauses.push(c);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
return clauses.join(SQL_SEPARATORS.CONDITION_AND);
|
|
4055
|
+
}
|
|
3924
4056
|
function buildLogicalClause2(key, value, alias, params, dialect, model) {
|
|
3925
4057
|
const items = normalizeLogicalValue2(key, value);
|
|
3926
4058
|
const subClauses = [];
|
|
3927
4059
|
for (const it of items) {
|
|
3928
4060
|
const c = buildHavingNode(it, alias, params, dialect, model);
|
|
3929
|
-
if (c && c
|
|
4061
|
+
if (c && c.trim().length > 0) subClauses.push(`(${c})`);
|
|
3930
4062
|
}
|
|
3931
4063
|
if (subClauses.length === 0) return "";
|
|
3932
4064
|
return combineLogical(key, subClauses);
|
|
3933
4065
|
}
|
|
4066
|
+
function assertHavingAggTarget(aggKey, field, model) {
|
|
4067
|
+
if (field === "_all") {
|
|
4068
|
+
if (aggKey !== "_count")
|
|
4069
|
+
throw new Error(`HAVING '${aggKey}' does not support '_all'`);
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4072
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4073
|
+
assertNumericField(model, field, "HAVING");
|
|
4074
|
+
} else {
|
|
4075
|
+
assertScalarField(model, field, "HAVING");
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
4079
|
+
return buildComparisons(expr, filter, params, dialect, buildSimpleComparison);
|
|
4080
|
+
}
|
|
4081
|
+
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
4082
|
+
if (!isPlainObject(target)) {
|
|
4083
|
+
throw new Error(`HAVING '${aggKey}' must be an object`);
|
|
4084
|
+
}
|
|
4085
|
+
const out = [];
|
|
4086
|
+
for (const [field, filter] of Object.entries(target)) {
|
|
4087
|
+
assertHavingAggTarget(aggKey, field, model);
|
|
4088
|
+
if (!isPlainObject(filter) || Object.keys(filter).length === 0) continue;
|
|
4089
|
+
const expr = aggExprForField(aggKey, field, alias, model);
|
|
4090
|
+
out.push(...buildHavingOpsForExpr(expr, filter, params, dialect));
|
|
4091
|
+
}
|
|
4092
|
+
return out;
|
|
4093
|
+
}
|
|
4094
|
+
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
4095
|
+
if (!isPlainObject(target)) {
|
|
4096
|
+
throw new Error(`HAVING '${fieldName}' must be an object`);
|
|
4097
|
+
}
|
|
4098
|
+
assertScalarField(model, fieldName, "HAVING");
|
|
4099
|
+
const out = [];
|
|
4100
|
+
const obj = target;
|
|
4101
|
+
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
4102
|
+
for (const aggKey of keys) {
|
|
4103
|
+
const aggFilter = obj[aggKey];
|
|
4104
|
+
if (!isPlainObject(aggFilter)) continue;
|
|
4105
|
+
if (Object.keys(aggFilter).length === 0) continue;
|
|
4106
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4107
|
+
assertNumericField(model, fieldName, "HAVING");
|
|
4108
|
+
}
|
|
4109
|
+
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4110
|
+
out.push(...buildHavingOpsForExpr(expr, aggFilter, params, dialect));
|
|
4111
|
+
}
|
|
4112
|
+
return out;
|
|
4113
|
+
}
|
|
3934
4114
|
function buildHavingEntry(key, value, alias, params, dialect, model) {
|
|
3935
4115
|
if (isLogicalKey(key)) {
|
|
3936
4116
|
const logical = buildLogicalClause2(
|
|
@@ -3962,71 +4142,10 @@ function buildHavingEntry(key, value, alias, params, dialect, model) {
|
|
|
3962
4142
|
model
|
|
3963
4143
|
);
|
|
3964
4144
|
}
|
|
3965
|
-
function buildHavingNode(node, alias, params, dialect, model) {
|
|
3966
|
-
const clauses = [];
|
|
3967
|
-
for (const [key, value] of Object.entries(node)) {
|
|
3968
|
-
const built = buildHavingEntry(key, value, alias, params, dialect, model);
|
|
3969
|
-
for (const c of built) {
|
|
3970
|
-
if (c && c.trim().length > 0) clauses.push(c);
|
|
3971
|
-
}
|
|
3972
|
-
}
|
|
3973
|
-
return clauses.join(SQL_SEPARATORS.CONDITION_AND);
|
|
3974
|
-
}
|
|
3975
|
-
function assertHavingAggTarget(aggKey, field, model) {
|
|
3976
|
-
if (field === "_all") {
|
|
3977
|
-
if (aggKey !== "_count") {
|
|
3978
|
-
throw new Error(`HAVING '${aggKey}' does not support '_all'`);
|
|
3979
|
-
}
|
|
3980
|
-
return;
|
|
3981
|
-
}
|
|
3982
|
-
const f = assertScalarField2(model, field, "HAVING");
|
|
3983
|
-
assertAggregateFieldType(aggKey, f.type, f.name, model.name);
|
|
3984
|
-
}
|
|
3985
|
-
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
3986
|
-
const out = [];
|
|
3987
|
-
for (const [op, val] of Object.entries(filter)) {
|
|
3988
|
-
if (op === "mode") continue;
|
|
3989
|
-
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
3990
|
-
if (built && built.trim().length > 0) out.push(built);
|
|
3991
|
-
}
|
|
3992
|
-
return out;
|
|
3993
|
-
}
|
|
3994
|
-
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
3995
|
-
if (!isPlainObject(target)) return [];
|
|
3996
|
-
const out = [];
|
|
3997
|
-
for (const [field, filter] of Object.entries(target)) {
|
|
3998
|
-
assertHavingAggTarget(aggKey, field, model);
|
|
3999
|
-
if (!isPlainObject(filter) || Object.keys(filter).length === 0) continue;
|
|
4000
|
-
const expr = aggExprForField(aggKey, field, alias, model);
|
|
4001
|
-
out.push(...buildHavingOpsForExpr(expr, filter, params, dialect));
|
|
4002
|
-
}
|
|
4003
|
-
return out;
|
|
4004
|
-
}
|
|
4005
|
-
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
4006
|
-
if (!isPlainObject(target)) return [];
|
|
4007
|
-
const field = assertScalarField2(model, fieldName, "HAVING");
|
|
4008
|
-
const out = [];
|
|
4009
|
-
const obj = target;
|
|
4010
|
-
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
4011
|
-
for (const aggKey of keys) {
|
|
4012
|
-
const aggFilter = obj[aggKey];
|
|
4013
|
-
if (!isPlainObject(aggFilter)) continue;
|
|
4014
|
-
assertAggregateFieldType(aggKey, field.type, field.name, model.name);
|
|
4015
|
-
const entries = Object.entries(aggFilter);
|
|
4016
|
-
if (entries.length === 0) continue;
|
|
4017
|
-
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4018
|
-
for (const [op, val] of entries) {
|
|
4019
|
-
if (op === "mode") continue;
|
|
4020
|
-
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
4021
|
-
if (built && built.trim().length > 0) out.push(built);
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
return out;
|
|
4025
|
-
}
|
|
4026
4145
|
function buildHavingClause(having, alias, params, model, dialect) {
|
|
4027
4146
|
if (!isNotNullish(having)) return "";
|
|
4028
4147
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
4029
|
-
if (!isPlainObject(having))
|
|
4148
|
+
if (!isPlainObject(having)) throw new Error("having must be an object");
|
|
4030
4149
|
return buildHavingNode(having, alias, params, d, model);
|
|
4031
4150
|
}
|
|
4032
4151
|
function normalizeCountArg(v) {
|
|
@@ -4040,26 +4159,13 @@ function pushCountAllField(fields) {
|
|
|
4040
4159
|
`${SQL_TEMPLATES.COUNT_ALL} ${SQL_TEMPLATES.AS} ${quote("_count._all")}`
|
|
4041
4160
|
);
|
|
4042
4161
|
}
|
|
4043
|
-
function assertCountableScalarField(fieldMap, model, fieldName) {
|
|
4044
|
-
const field = fieldMap.get(fieldName);
|
|
4045
|
-
if (!field) {
|
|
4046
|
-
throw new Error(
|
|
4047
|
-
`Field '${fieldName}' does not exist on model ${model.name}`
|
|
4048
|
-
);
|
|
4049
|
-
}
|
|
4050
|
-
if (field.isRelation) {
|
|
4051
|
-
throw new Error(
|
|
4052
|
-
`Cannot use _count on relation field '${fieldName}' on model ${model.name}`
|
|
4053
|
-
);
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4056
4162
|
function pushCountField(fields, alias, fieldName, model) {
|
|
4057
4163
|
const outAlias = `_count.${fieldName}`;
|
|
4058
4164
|
fields.push(
|
|
4059
4165
|
`COUNT(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4060
4166
|
);
|
|
4061
4167
|
}
|
|
4062
|
-
function addCountFields(fields, countArg, alias, model
|
|
4168
|
+
function addCountFields(fields, countArg, alias, model) {
|
|
4063
4169
|
if (!isNotNullish(countArg)) return;
|
|
4064
4170
|
if (countArg === true) {
|
|
4065
4171
|
pushCountAllField(fields);
|
|
@@ -4073,7 +4179,7 @@ function addCountFields(fields, countArg, alias, model, fieldMap) {
|
|
|
4073
4179
|
([f, v]) => f !== "_all" && isTruthySelection(v)
|
|
4074
4180
|
);
|
|
4075
4181
|
for (const [f] of selected) {
|
|
4076
|
-
|
|
4182
|
+
assertScalarField(model, f, "_count");
|
|
4077
4183
|
pushCountField(fields, alias, f, model);
|
|
4078
4184
|
}
|
|
4079
4185
|
}
|
|
@@ -4081,19 +4187,12 @@ function getAggregateSelectionObject(args, agg) {
|
|
|
4081
4187
|
const obj = args[agg];
|
|
4082
4188
|
return isPlainObject(obj) ? obj : void 0;
|
|
4083
4189
|
}
|
|
4084
|
-
function assertAggregatableScalarField(
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
);
|
|
4090
|
-
}
|
|
4091
|
-
if (field.isRelation) {
|
|
4092
|
-
throw new Error(
|
|
4093
|
-
`Cannot use ${agg} on relation field '${fieldName}' on model ${model.name}`
|
|
4094
|
-
);
|
|
4190
|
+
function assertAggregatableScalarField(model, agg, fieldName) {
|
|
4191
|
+
if (agg === "_sum" || agg === "_avg") {
|
|
4192
|
+
assertNumericField(model, fieldName, agg);
|
|
4193
|
+
} else {
|
|
4194
|
+
assertScalarField(model, fieldName, agg);
|
|
4095
4195
|
}
|
|
4096
|
-
return field;
|
|
4097
4196
|
}
|
|
4098
4197
|
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
4099
4198
|
const outAlias = `${agg}.${fieldName}`;
|
|
@@ -4101,7 +4200,7 @@ function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
|
4101
4200
|
`${aggFn}(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4102
4201
|
);
|
|
4103
4202
|
}
|
|
4104
|
-
function addAggregateFields(fields, args, alias, model
|
|
4203
|
+
function addAggregateFields(fields, args, alias, model) {
|
|
4105
4204
|
for (const [agg, aggFn] of AGGREGATES) {
|
|
4106
4205
|
const obj = getAggregateSelectionObject(args, agg);
|
|
4107
4206
|
if (!obj) continue;
|
|
@@ -4109,23 +4208,16 @@ function addAggregateFields(fields, args, alias, model, fieldMap) {
|
|
|
4109
4208
|
if (fieldName === "_all")
|
|
4110
4209
|
throw new Error(`'${agg}' does not support '_all'`);
|
|
4111
4210
|
if (!isTruthySelection(selection)) continue;
|
|
4112
|
-
|
|
4113
|
-
fieldMap,
|
|
4114
|
-
model,
|
|
4115
|
-
agg,
|
|
4116
|
-
fieldName
|
|
4117
|
-
);
|
|
4118
|
-
assertAggregateFieldType(agg, field.type, fieldName, model.name);
|
|
4211
|
+
assertAggregatableScalarField(model, agg, fieldName);
|
|
4119
4212
|
pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model);
|
|
4120
4213
|
}
|
|
4121
4214
|
}
|
|
4122
4215
|
}
|
|
4123
4216
|
function buildAggregateFields(args, alias, model) {
|
|
4124
4217
|
const fields = [];
|
|
4125
|
-
const fieldMap = getModelFieldMap(model);
|
|
4126
4218
|
const countArg = normalizeCountArg(args._count);
|
|
4127
|
-
addCountFields(fields, countArg, alias, model
|
|
4128
|
-
addAggregateFields(fields, args, alias, model
|
|
4219
|
+
addCountFields(fields, countArg, alias, model);
|
|
4220
|
+
addAggregateFields(fields, args, alias, model);
|
|
4129
4221
|
return fields;
|
|
4130
4222
|
}
|
|
4131
4223
|
function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
@@ -4149,7 +4241,7 @@ function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
|
4149
4241
|
validateParamConsistency(sql, whereResult.params);
|
|
4150
4242
|
return Object.freeze({
|
|
4151
4243
|
sql,
|
|
4152
|
-
params: Object.freeze(
|
|
4244
|
+
params: Object.freeze([...whereResult.params]),
|
|
4153
4245
|
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4154
4246
|
});
|
|
4155
4247
|
}
|
|
@@ -4162,32 +4254,22 @@ function assertGroupByBy(args, model) {
|
|
|
4162
4254
|
if (bySet.size !== byFields.length) {
|
|
4163
4255
|
throw new Error("buildGroupBySql: by must not contain duplicates");
|
|
4164
4256
|
}
|
|
4165
|
-
const modelFieldMap = getModelFieldMap(model);
|
|
4166
4257
|
for (const f of byFields) {
|
|
4167
|
-
|
|
4168
|
-
if (!field) {
|
|
4169
|
-
throw new Error(
|
|
4170
|
-
`groupBy.by references unknown field '${f}' on model ${model.name}`
|
|
4171
|
-
);
|
|
4172
|
-
}
|
|
4173
|
-
if (field.isRelation) {
|
|
4174
|
-
throw new Error(
|
|
4175
|
-
`groupBy.by does not support relation field '${f}' on model ${model.name}`
|
|
4176
|
-
);
|
|
4177
|
-
}
|
|
4258
|
+
assertScalarField(model, f, "groupBy.by");
|
|
4178
4259
|
}
|
|
4179
4260
|
return byFields;
|
|
4180
4261
|
}
|
|
4181
4262
|
function buildGroupBySelectParts(args, alias, model, byFields) {
|
|
4182
4263
|
const groupCols = byFields.map((f) => col(alias, f, model));
|
|
4264
|
+
const selectCols = byFields.map((f) => colWithAlias(alias, f, model));
|
|
4183
4265
|
const groupFields = groupCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4184
4266
|
const aggFields = buildAggregateFields(args, alias, model);
|
|
4185
|
-
const selectFields = isNonEmptyArray(aggFields) ?
|
|
4267
|
+
const selectFields = isNonEmptyArray(aggFields) ? selectCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : selectCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4186
4268
|
return { groupCols, groupFields, selectFields };
|
|
4187
4269
|
}
|
|
4188
4270
|
function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
4189
4271
|
if (!isNotNullish(args.having)) return "";
|
|
4190
|
-
if (!isPlainObject(args.having))
|
|
4272
|
+
if (!isPlainObject(args.having)) throw new Error("having must be an object");
|
|
4191
4273
|
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4192
4274
|
if (!h || h.trim().length === 0) return "";
|
|
4193
4275
|
return `${SQL_TEMPLATES.HAVING} ${h}`;
|
|
@@ -4220,64 +4302,60 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
|
4220
4302
|
const snapshot = params.snapshot();
|
|
4221
4303
|
validateSelectQuery(sql);
|
|
4222
4304
|
validateParamConsistency(sql, [...whereResult.params, ...snapshot.params]);
|
|
4223
|
-
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4224
4305
|
return Object.freeze({
|
|
4225
4306
|
sql,
|
|
4226
|
-
params: Object.freeze(
|
|
4307
|
+
params: Object.freeze([...whereResult.params, ...snapshot.params]),
|
|
4227
4308
|
paramMappings: Object.freeze([
|
|
4228
4309
|
...whereResult.paramMappings,
|
|
4229
4310
|
...snapshot.mappings
|
|
4230
4311
|
])
|
|
4231
4312
|
});
|
|
4232
4313
|
}
|
|
4233
|
-
function buildCountSql(whereResult, tableName, alias, skip,
|
|
4314
|
+
function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
4234
4315
|
assertSafeAlias(alias);
|
|
4235
4316
|
assertSafeTableRef(tableName);
|
|
4236
|
-
|
|
4317
|
+
if (skip !== void 0 && skip !== null) {
|
|
4318
|
+
if (isDynamicParameter(skip)) {
|
|
4319
|
+
throw new Error(
|
|
4320
|
+
"count() with skip is not supported because it produces nondeterministic results. Dynamic skip cannot be validated at build time. Use findMany().length or add explicit orderBy + cursor/skip logic in a deterministic query."
|
|
4321
|
+
);
|
|
4322
|
+
}
|
|
4323
|
+
if (typeof skip === "string") {
|
|
4324
|
+
const s = skip.trim();
|
|
4325
|
+
if (s.length > 0) {
|
|
4326
|
+
const n = Number(s);
|
|
4327
|
+
if (Number.isFinite(n) && Number.isInteger(n) && n > 0) {
|
|
4328
|
+
throw new Error(
|
|
4329
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
if (typeof skip === "number" && Number.isFinite(skip) && Number.isInteger(skip) && skip > 0) {
|
|
4335
|
+
throw new Error(
|
|
4336
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4337
|
+
);
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4237
4340
|
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4238
|
-
const params = createParamStore(whereResult.nextParamIndex);
|
|
4239
|
-
const baseSubSelect = [
|
|
4240
|
-
SQL_TEMPLATES.SELECT,
|
|
4241
|
-
"1",
|
|
4242
|
-
SQL_TEMPLATES.FROM,
|
|
4243
|
-
tableName,
|
|
4244
|
-
alias,
|
|
4245
|
-
whereClause
|
|
4246
|
-
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4247
|
-
const normalizedSkip = normalizeSkipLike(skip);
|
|
4248
|
-
const subSelect = applyCountSkip(baseSubSelect, normalizedSkip, params, d);
|
|
4249
4341
|
const sql = [
|
|
4250
4342
|
SQL_TEMPLATES.SELECT,
|
|
4251
4343
|
SQL_TEMPLATES.COUNT_ALL,
|
|
4252
4344
|
SQL_TEMPLATES.AS,
|
|
4253
4345
|
quote("_count._all"),
|
|
4254
4346
|
SQL_TEMPLATES.FROM,
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4347
|
+
tableName,
|
|
4348
|
+
alias,
|
|
4349
|
+
whereClause
|
|
4258
4350
|
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4259
4351
|
validateSelectQuery(sql);
|
|
4260
|
-
|
|
4261
|
-
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4262
|
-
validateParamConsistency(sql, mergedParams);
|
|
4352
|
+
validateParamConsistency(sql, whereResult.params);
|
|
4263
4353
|
return Object.freeze({
|
|
4264
4354
|
sql,
|
|
4265
|
-
params: Object.freeze(
|
|
4266
|
-
paramMappings: Object.freeze([
|
|
4267
|
-
...whereResult.paramMappings,
|
|
4268
|
-
...snapshot.mappings
|
|
4269
|
-
])
|
|
4355
|
+
params: Object.freeze([...whereResult.params]),
|
|
4356
|
+
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4270
4357
|
});
|
|
4271
4358
|
}
|
|
4272
|
-
function applyCountSkip(subSelect, normalizedSkip, params, dialect) {
|
|
4273
|
-
const shouldApply = isDynamicParameter(normalizedSkip) || typeof normalizedSkip === "number" && normalizedSkip > 0;
|
|
4274
|
-
if (!shouldApply) return subSelect;
|
|
4275
|
-
const placeholder = addAutoScoped(params, normalizedSkip, "count.skip");
|
|
4276
|
-
if (dialect === "sqlite") {
|
|
4277
|
-
return `${subSelect} ${SQL_TEMPLATES.LIMIT} -1 ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4278
|
-
}
|
|
4279
|
-
return `${subSelect} ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4280
|
-
}
|
|
4281
4359
|
function safeAlias(input) {
|
|
4282
4360
|
const raw = String(input).toLowerCase();
|
|
4283
4361
|
const cleaned = raw.replace(/[^a-z0-9_]/g, "_");
|
|
@@ -4427,8 +4505,12 @@ function buildAndNormalizeSql(args) {
|
|
|
4427
4505
|
);
|
|
4428
4506
|
}
|
|
4429
4507
|
function finalizeDirective(args) {
|
|
4430
|
-
const { directive, normalizedSql, normalizedMappings } = args;
|
|
4431
|
-
|
|
4508
|
+
const { directive, normalizedSql, normalizedMappings, dialect } = args;
|
|
4509
|
+
const params = normalizedMappings.map((m) => {
|
|
4510
|
+
var _a;
|
|
4511
|
+
return (_a = m.value) != null ? _a : void 0;
|
|
4512
|
+
});
|
|
4513
|
+
validateParamConsistencyByDialect(normalizedSql, params, dialect);
|
|
4432
4514
|
const { staticParams, dynamicKeys } = buildParamsFromMappings(normalizedMappings);
|
|
4433
4515
|
return {
|
|
4434
4516
|
method: directive.method,
|
|
@@ -4465,7 +4547,8 @@ function generateSQL(directive) {
|
|
|
4465
4547
|
return finalizeDirective({
|
|
4466
4548
|
directive,
|
|
4467
4549
|
normalizedSql: normalized.sql,
|
|
4468
|
-
normalizedMappings: normalized.paramMappings
|
|
4550
|
+
normalizedMappings: normalized.paramMappings,
|
|
4551
|
+
dialect
|
|
4469
4552
|
});
|
|
4470
4553
|
}
|
|
4471
4554
|
|
|
@@ -4842,9 +4925,7 @@ function buildSQLFull(model, models, method, args, dialect) {
|
|
|
4842
4925
|
whereResult,
|
|
4843
4926
|
tableName,
|
|
4844
4927
|
alias,
|
|
4845
|
-
args.skip
|
|
4846
|
-
dialect
|
|
4847
|
-
);
|
|
4928
|
+
args.skip);
|
|
4848
4929
|
break;
|
|
4849
4930
|
default:
|
|
4850
4931
|
result = buildSelectSql({
|