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