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/generator.cjs
CHANGED
|
@@ -56,7 +56,7 @@ var require_package = __commonJS({
|
|
|
56
56
|
"package.json"(exports$1, module) {
|
|
57
57
|
module.exports = {
|
|
58
58
|
name: "prisma-sql",
|
|
59
|
-
version: "1.
|
|
59
|
+
version: "1.46.0",
|
|
60
60
|
description: "Convert Prisma queries to optimized SQL with type safety. 2-7x faster than Prisma Client.",
|
|
61
61
|
main: "dist/index.cjs",
|
|
62
62
|
module: "dist/index.js",
|
|
@@ -159,20 +159,13 @@ var SQL_SEPARATORS = Object.freeze({
|
|
|
159
159
|
CONDITION_OR: " OR ",
|
|
160
160
|
ORDER_BY: ", "
|
|
161
161
|
});
|
|
162
|
-
var
|
|
162
|
+
var ALIAS_FORBIDDEN_KEYWORDS = /* @__PURE__ */ new Set([
|
|
163
163
|
"select",
|
|
164
164
|
"from",
|
|
165
165
|
"where",
|
|
166
|
-
"
|
|
167
|
-
"or",
|
|
168
|
-
"not",
|
|
169
|
-
"in",
|
|
170
|
-
"like",
|
|
171
|
-
"between",
|
|
166
|
+
"having",
|
|
172
167
|
"order",
|
|
173
|
-
"by",
|
|
174
168
|
"group",
|
|
175
|
-
"having",
|
|
176
169
|
"limit",
|
|
177
170
|
"offset",
|
|
178
171
|
"join",
|
|
@@ -180,14 +173,42 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
180
173
|
"left",
|
|
181
174
|
"right",
|
|
182
175
|
"outer",
|
|
183
|
-
"
|
|
176
|
+
"cross",
|
|
177
|
+
"full",
|
|
178
|
+
"and",
|
|
179
|
+
"or",
|
|
180
|
+
"not",
|
|
181
|
+
"by",
|
|
184
182
|
"as",
|
|
183
|
+
"on",
|
|
184
|
+
"union",
|
|
185
|
+
"intersect",
|
|
186
|
+
"except",
|
|
187
|
+
"case",
|
|
188
|
+
"when",
|
|
189
|
+
"then",
|
|
190
|
+
"else",
|
|
191
|
+
"end"
|
|
192
|
+
]);
|
|
193
|
+
var SQL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
194
|
+
...ALIAS_FORBIDDEN_KEYWORDS,
|
|
195
|
+
"user",
|
|
196
|
+
"users",
|
|
185
197
|
"table",
|
|
186
198
|
"column",
|
|
187
199
|
"index",
|
|
188
|
-
"user",
|
|
189
|
-
"users",
|
|
190
200
|
"values",
|
|
201
|
+
"in",
|
|
202
|
+
"like",
|
|
203
|
+
"between",
|
|
204
|
+
"is",
|
|
205
|
+
"exists",
|
|
206
|
+
"null",
|
|
207
|
+
"true",
|
|
208
|
+
"false",
|
|
209
|
+
"all",
|
|
210
|
+
"any",
|
|
211
|
+
"some",
|
|
191
212
|
"update",
|
|
192
213
|
"insert",
|
|
193
214
|
"delete",
|
|
@@ -198,25 +219,9 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
198
219
|
"grant",
|
|
199
220
|
"revoke",
|
|
200
221
|
"exec",
|
|
201
|
-
"execute"
|
|
202
|
-
"union",
|
|
203
|
-
"intersect",
|
|
204
|
-
"except",
|
|
205
|
-
"case",
|
|
206
|
-
"when",
|
|
207
|
-
"then",
|
|
208
|
-
"else",
|
|
209
|
-
"end",
|
|
210
|
-
"null",
|
|
211
|
-
"true",
|
|
212
|
-
"false",
|
|
213
|
-
"is",
|
|
214
|
-
"exists",
|
|
215
|
-
"all",
|
|
216
|
-
"any",
|
|
217
|
-
"some"
|
|
222
|
+
"execute"
|
|
218
223
|
]);
|
|
219
|
-
var
|
|
224
|
+
var SQL_RESERVED_WORDS = SQL_KEYWORDS;
|
|
220
225
|
var DEFAULT_WHERE_CLAUSE = "1=1";
|
|
221
226
|
var SPECIAL_FIELDS = Object.freeze({
|
|
222
227
|
ID: "id"
|
|
@@ -295,12 +300,48 @@ var LIMITS = Object.freeze({
|
|
|
295
300
|
});
|
|
296
301
|
|
|
297
302
|
// src/utils/normalize-value.ts
|
|
298
|
-
|
|
303
|
+
var MAX_DEPTH = 20;
|
|
304
|
+
function normalizeValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
305
|
+
if (depth > MAX_DEPTH) {
|
|
306
|
+
throw new Error(`Max normalization depth exceeded (${MAX_DEPTH} levels)`);
|
|
307
|
+
}
|
|
299
308
|
if (value instanceof Date) {
|
|
309
|
+
const t = value.getTime();
|
|
310
|
+
if (!Number.isFinite(t)) {
|
|
311
|
+
throw new Error("Invalid Date value in SQL params");
|
|
312
|
+
}
|
|
300
313
|
return value.toISOString();
|
|
301
314
|
}
|
|
315
|
+
if (typeof value === "bigint") {
|
|
316
|
+
return value.toString();
|
|
317
|
+
}
|
|
302
318
|
if (Array.isArray(value)) {
|
|
303
|
-
|
|
319
|
+
const arrRef = value;
|
|
320
|
+
if (seen.has(arrRef)) {
|
|
321
|
+
throw new Error("Circular reference in SQL params");
|
|
322
|
+
}
|
|
323
|
+
seen.add(arrRef);
|
|
324
|
+
const out = value.map((v) => normalizeValue(v, seen, depth + 1));
|
|
325
|
+
seen.delete(arrRef);
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
if (value && typeof value === "object") {
|
|
329
|
+
if (value instanceof Uint8Array) return value;
|
|
330
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) return value;
|
|
331
|
+
const proto = Object.getPrototypeOf(value);
|
|
332
|
+
const isPlain = proto === Object.prototype || proto === null;
|
|
333
|
+
if (!isPlain) return value;
|
|
334
|
+
const obj = value;
|
|
335
|
+
if (seen.has(obj)) {
|
|
336
|
+
throw new Error("Circular reference in SQL params");
|
|
337
|
+
}
|
|
338
|
+
seen.add(obj);
|
|
339
|
+
const out = {};
|
|
340
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
341
|
+
out[k] = normalizeValue(v, seen, depth + 1);
|
|
342
|
+
}
|
|
343
|
+
seen.delete(obj);
|
|
344
|
+
return out;
|
|
304
345
|
}
|
|
305
346
|
return value;
|
|
306
347
|
}
|
|
@@ -481,7 +522,7 @@ function prepareArrayParam(value, dialect) {
|
|
|
481
522
|
throw new Error("prepareArrayParam requires array value");
|
|
482
523
|
}
|
|
483
524
|
if (dialect === "postgres") {
|
|
484
|
-
return value.map(normalizeValue);
|
|
525
|
+
return value.map((v) => normalizeValue(v));
|
|
485
526
|
}
|
|
486
527
|
return JSON.stringify(value);
|
|
487
528
|
}
|
|
@@ -553,36 +594,46 @@ function createError(message, ctx, code = "VALIDATION_ERROR") {
|
|
|
553
594
|
}
|
|
554
595
|
|
|
555
596
|
// src/builder/shared/model-field-cache.ts
|
|
556
|
-
var
|
|
557
|
-
|
|
597
|
+
var MODEL_CACHE = /* @__PURE__ */ new WeakMap();
|
|
598
|
+
function ensureFullCache(model) {
|
|
599
|
+
let cache = MODEL_CACHE.get(model);
|
|
600
|
+
if (!cache) {
|
|
601
|
+
const fieldInfo = /* @__PURE__ */ new Map();
|
|
602
|
+
const scalarFields = /* @__PURE__ */ new Set();
|
|
603
|
+
const relationFields = /* @__PURE__ */ new Set();
|
|
604
|
+
const columnMap = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const f of model.fields) {
|
|
606
|
+
const info = {
|
|
607
|
+
name: f.name,
|
|
608
|
+
dbName: f.dbName || f.name,
|
|
609
|
+
type: f.type,
|
|
610
|
+
isRelation: !!f.isRelation,
|
|
611
|
+
isRequired: !!f.isRequired
|
|
612
|
+
};
|
|
613
|
+
fieldInfo.set(f.name, info);
|
|
614
|
+
if (info.isRelation) {
|
|
615
|
+
relationFields.add(f.name);
|
|
616
|
+
} else {
|
|
617
|
+
scalarFields.add(f.name);
|
|
618
|
+
columnMap.set(f.name, info.dbName);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
cache = { fieldInfo, scalarFields, relationFields, columnMap };
|
|
622
|
+
MODEL_CACHE.set(model, cache);
|
|
623
|
+
}
|
|
624
|
+
return cache;
|
|
625
|
+
}
|
|
626
|
+
function getFieldInfo(model, fieldName) {
|
|
627
|
+
return ensureFullCache(model).fieldInfo.get(fieldName);
|
|
628
|
+
}
|
|
558
629
|
function getScalarFieldSet(model) {
|
|
559
|
-
|
|
560
|
-
if (cached) return cached;
|
|
561
|
-
const s = /* @__PURE__ */ new Set();
|
|
562
|
-
for (const f of model.fields) if (!f.isRelation) s.add(f.name);
|
|
563
|
-
SCALAR_SET_CACHE.set(model, s);
|
|
564
|
-
return s;
|
|
630
|
+
return ensureFullCache(model).scalarFields;
|
|
565
631
|
}
|
|
566
632
|
function getRelationFieldSet(model) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const s = /* @__PURE__ */ new Set();
|
|
570
|
-
for (const f of model.fields) if (f.isRelation) s.add(f.name);
|
|
571
|
-
RELATION_SET_CACHE.set(model, s);
|
|
572
|
-
return s;
|
|
573
|
-
}
|
|
574
|
-
var COLUMN_MAP_CACHE = /* @__PURE__ */ new WeakMap();
|
|
633
|
+
return ensureFullCache(model).relationFields;
|
|
634
|
+
}
|
|
575
635
|
function getColumnMap(model) {
|
|
576
|
-
|
|
577
|
-
if (cached) return cached;
|
|
578
|
-
const map = /* @__PURE__ */ new Map();
|
|
579
|
-
for (const f of model.fields) {
|
|
580
|
-
if (!f.isRelation) {
|
|
581
|
-
map.set(f.name, f.dbName || f.name);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
COLUMN_MAP_CACHE.set(model, map);
|
|
585
|
-
return map;
|
|
636
|
+
return ensureFullCache(model).columnMap;
|
|
586
637
|
}
|
|
587
638
|
|
|
588
639
|
// src/builder/shared/validators/sql-validators.ts
|
|
@@ -593,20 +644,21 @@ function isEmptyWhere(where) {
|
|
|
593
644
|
if (!isNotNullish(where)) return true;
|
|
594
645
|
return Object.keys(where).length === 0;
|
|
595
646
|
}
|
|
647
|
+
function sqlPreview(sql) {
|
|
648
|
+
const s = String(sql);
|
|
649
|
+
if (s.length <= 160) return s;
|
|
650
|
+
return `${s.slice(0, 160)}...`;
|
|
651
|
+
}
|
|
596
652
|
function validateSelectQuery(sql) {
|
|
597
653
|
if (!hasValidContent(sql)) {
|
|
598
654
|
throw new Error("CRITICAL: Generated empty SQL query");
|
|
599
655
|
}
|
|
600
656
|
if (!hasRequiredKeywords(sql)) {
|
|
601
|
-
throw new Error(
|
|
602
|
-
`CRITICAL: Invalid SQL structure. SQL: ${sql.substring(0, 100)}...`
|
|
603
|
-
);
|
|
657
|
+
throw new Error(`CRITICAL: Invalid SQL structure. SQL: ${sqlPreview(sql)}`);
|
|
604
658
|
}
|
|
605
659
|
}
|
|
606
|
-
function
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
function parseDollarNumber(sql, start, n) {
|
|
660
|
+
function parseDollarNumber(sql, start) {
|
|
661
|
+
const n = sql.length;
|
|
610
662
|
let i = start;
|
|
611
663
|
let num = 0;
|
|
612
664
|
let hasDigit = false;
|
|
@@ -617,14 +669,14 @@ function parseDollarNumber(sql, start, n) {
|
|
|
617
669
|
num = num * 10 + (c - 48);
|
|
618
670
|
i++;
|
|
619
671
|
}
|
|
620
|
-
if (!hasDigit || num <= 0) return { next: i, num: 0
|
|
621
|
-
return { next: i, num
|
|
672
|
+
if (!hasDigit || num <= 0) return { next: i, num: 0 };
|
|
673
|
+
return { next: i, num };
|
|
622
674
|
}
|
|
623
675
|
function scanDollarPlaceholders(sql, markUpTo) {
|
|
624
676
|
const seen = new Uint8Array(markUpTo + 1);
|
|
625
|
-
let count = 0;
|
|
626
677
|
let min = Number.POSITIVE_INFINITY;
|
|
627
678
|
let max = 0;
|
|
679
|
+
let sawAny = false;
|
|
628
680
|
const n = sql.length;
|
|
629
681
|
let i = 0;
|
|
630
682
|
while (i < n) {
|
|
@@ -632,17 +684,21 @@ function scanDollarPlaceholders(sql, markUpTo) {
|
|
|
632
684
|
i++;
|
|
633
685
|
continue;
|
|
634
686
|
}
|
|
635
|
-
const
|
|
636
|
-
i = next;
|
|
637
|
-
|
|
638
|
-
|
|
687
|
+
const parsed = parseDollarNumber(sql, i + 1);
|
|
688
|
+
i = parsed.next;
|
|
689
|
+
const num = parsed.num;
|
|
690
|
+
if (num === 0) continue;
|
|
691
|
+
sawAny = true;
|
|
639
692
|
if (num < min) min = num;
|
|
640
693
|
if (num > max) max = num;
|
|
641
694
|
if (num <= markUpTo) seen[num] = 1;
|
|
642
695
|
}
|
|
643
|
-
|
|
696
|
+
if (!sawAny) {
|
|
697
|
+
return { min: 0, max: 0, seen, sawAny: false };
|
|
698
|
+
}
|
|
699
|
+
return { min, max, seen, sawAny: true };
|
|
644
700
|
}
|
|
645
|
-
function
|
|
701
|
+
function assertNoGapsDollar(scan, rangeMin, rangeMax, sql) {
|
|
646
702
|
for (let k = rangeMin; k <= rangeMax; k++) {
|
|
647
703
|
if (scan.seen[k] !== 1) {
|
|
648
704
|
throw new Error(
|
|
@@ -653,174 +709,75 @@ function assertNoGaps(scan, rangeMin, rangeMax, sql) {
|
|
|
653
709
|
}
|
|
654
710
|
function validateParamConsistency(sql, params) {
|
|
655
711
|
const paramLen = params.length;
|
|
656
|
-
if (paramLen === 0) {
|
|
657
|
-
if (sql.indexOf("$") === -1) return;
|
|
658
|
-
}
|
|
659
712
|
const scan = scanDollarPlaceholders(sql, paramLen);
|
|
660
|
-
if (
|
|
661
|
-
if (
|
|
713
|
+
if (paramLen === 0) {
|
|
714
|
+
if (scan.sawAny) {
|
|
662
715
|
throw new Error(
|
|
663
|
-
`CRITICAL:
|
|
716
|
+
`CRITICAL: SQL contains placeholders but params is empty. SQL: ${sqlPreview(sql)}`
|
|
664
717
|
);
|
|
665
718
|
}
|
|
666
719
|
return;
|
|
667
720
|
}
|
|
668
|
-
if (scan.
|
|
721
|
+
if (!scan.sawAny) {
|
|
669
722
|
throw new Error(
|
|
670
|
-
`CRITICAL:
|
|
723
|
+
`CRITICAL: SQL is missing placeholders ($1..$${paramLen}) but params has length ${paramLen}. SQL: ${sqlPreview(sql)}`
|
|
671
724
|
);
|
|
672
725
|
}
|
|
673
|
-
|
|
674
|
-
}
|
|
675
|
-
function needsQuoting(id) {
|
|
676
|
-
if (!isNonEmptyString(id)) return true;
|
|
677
|
-
const isKeyword = SQL_KEYWORDS.has(id.toLowerCase());
|
|
678
|
-
if (isKeyword) return true;
|
|
679
|
-
const isValidIdentifier = REGEX_CACHE.VALID_IDENTIFIER.test(id);
|
|
680
|
-
return !isValidIdentifier;
|
|
681
|
-
}
|
|
682
|
-
function validateParamConsistencyFragment(sql, params) {
|
|
683
|
-
const paramLen = params.length;
|
|
684
|
-
const scan = scanDollarPlaceholders(sql, paramLen);
|
|
685
|
-
if (scan.max === 0) return;
|
|
686
|
-
if (scan.max > paramLen) {
|
|
726
|
+
if (scan.min !== 1) {
|
|
687
727
|
throw new Error(
|
|
688
|
-
`CRITICAL:
|
|
728
|
+
`CRITICAL: Placeholder range must start at $1, got min=$${scan.min}. SQL: ${sqlPreview(sql)}`
|
|
689
729
|
);
|
|
690
730
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
if (!condition) throw new Error(message);
|
|
695
|
-
}
|
|
696
|
-
function dialectPlaceholderPrefix(dialect) {
|
|
697
|
-
return dialect === "sqlite" ? "?" : "$";
|
|
698
|
-
}
|
|
699
|
-
function parseSqlitePlaceholderIndices(sql) {
|
|
700
|
-
const re = /\?(?:(\d+))?/g;
|
|
701
|
-
const indices = [];
|
|
702
|
-
let anonCount = 0;
|
|
703
|
-
let sawNumbered = false;
|
|
704
|
-
let sawAnonymous = false;
|
|
705
|
-
for (const m of sql.matchAll(re)) {
|
|
706
|
-
const n = m[1];
|
|
707
|
-
if (n) {
|
|
708
|
-
sawNumbered = true;
|
|
709
|
-
indices.push(parseInt(n, 10));
|
|
710
|
-
} else {
|
|
711
|
-
sawAnonymous = true;
|
|
712
|
-
anonCount += 1;
|
|
713
|
-
indices.push(anonCount);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
return { indices, sawNumbered, sawAnonymous };
|
|
717
|
-
}
|
|
718
|
-
function parseDollarPlaceholderIndices(sql) {
|
|
719
|
-
const re = /\$(\d+)/g;
|
|
720
|
-
const indices = [];
|
|
721
|
-
for (const m of sql.matchAll(re)) indices.push(parseInt(m[1], 10));
|
|
722
|
-
return indices;
|
|
723
|
-
}
|
|
724
|
-
function getPlaceholderIndices(sql, dialect) {
|
|
725
|
-
if (dialect === "sqlite") return parseSqlitePlaceholderIndices(sql);
|
|
726
|
-
return {
|
|
727
|
-
indices: parseDollarPlaceholderIndices(sql),
|
|
728
|
-
sawNumbered: false,
|
|
729
|
-
sawAnonymous: false
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
function maxIndex(indices) {
|
|
733
|
-
return indices.length > 0 ? Math.max(...indices) : 0;
|
|
734
|
-
}
|
|
735
|
-
function ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous) {
|
|
736
|
-
assertOrThrow(
|
|
737
|
-
!(sawNumbered && sawAnonymous),
|
|
738
|
-
`CRITICAL: Mixed sqlite placeholders ('?' and '?NNN') are not supported.`
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
function ensurePlaceholderMaxMatchesMappingsLength(max, mappingsLength, dialect) {
|
|
742
|
-
assertOrThrow(
|
|
743
|
-
max === mappingsLength,
|
|
744
|
-
`CRITICAL: SQL placeholder max mismatch - max is ${dialectPlaceholderPrefix(dialect)}${max}, but mappings length is ${mappingsLength}.`
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
function ensureSequentialPlaceholders(placeholders, max, dialect) {
|
|
748
|
-
const prefix = dialectPlaceholderPrefix(dialect);
|
|
749
|
-
for (let i = 1; i <= max; i++) {
|
|
750
|
-
assertOrThrow(
|
|
751
|
-
placeholders.has(i),
|
|
752
|
-
`CRITICAL: Missing SQL placeholder ${prefix}${i} - placeholders must be sequential 1..${max}.`
|
|
731
|
+
if (scan.max !== paramLen) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`CRITICAL: Placeholder max must match params length. max=$${scan.max}, params=${paramLen}. SQL: ${sqlPreview(sql)}`
|
|
753
734
|
);
|
|
754
735
|
}
|
|
736
|
+
assertNoGapsDollar(scan, 1, paramLen, sql);
|
|
755
737
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
assertOrThrow(
|
|
764
|
-
!mappingIndices.has(index),
|
|
765
|
-
`CRITICAL: Duplicate ParamMapping index ${index} - each placeholder index must map to exactly one ParamMap.`
|
|
766
|
-
);
|
|
767
|
-
mappingIndices.add(index);
|
|
768
|
-
}
|
|
769
|
-
function ensureMappingIndexExistsInSql(placeholders, index) {
|
|
770
|
-
assertOrThrow(
|
|
771
|
-
placeholders.has(index),
|
|
772
|
-
`CRITICAL: ParamMapping index ${index} not found in SQL placeholders.`
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
function validateMappingValueShape(mapping) {
|
|
776
|
-
assertOrThrow(
|
|
777
|
-
!(mapping.dynamicName !== void 0 && mapping.value !== void 0),
|
|
778
|
-
`CRITICAL: ParamMap ${mapping.index} has both dynamicName and value`
|
|
779
|
-
);
|
|
780
|
-
assertOrThrow(
|
|
781
|
-
!(mapping.dynamicName === void 0 && mapping.value === void 0),
|
|
782
|
-
`CRITICAL: ParamMap ${mapping.index} has neither dynamicName nor value`
|
|
783
|
-
);
|
|
738
|
+
function countQuestionMarkPlaceholders(sql) {
|
|
739
|
+
const s = String(sql);
|
|
740
|
+
let count = 0;
|
|
741
|
+
for (let i = 0; i < s.length; i++) {
|
|
742
|
+
if (s.charCodeAt(i) === 63) count++;
|
|
743
|
+
}
|
|
744
|
+
return count;
|
|
784
745
|
}
|
|
785
|
-
function
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
`CRITICAL:
|
|
746
|
+
function validateQuestionMarkConsistency(sql, params) {
|
|
747
|
+
const expected = params.length;
|
|
748
|
+
const found = countQuestionMarkPlaceholders(sql);
|
|
749
|
+
if (expected !== found) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`CRITICAL: Parameter mismatch - expected ${expected} '?' placeholders, found ${found}. SQL: ${sqlPreview(sql)}`
|
|
791
752
|
);
|
|
792
753
|
}
|
|
793
754
|
}
|
|
794
|
-
function
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
ensureUniqueMappingIndex(mappingIndices, mapping.index);
|
|
799
|
-
ensureMappingIndexExistsInSql(placeholders, mapping.index);
|
|
800
|
-
validateMappingValueShape(mapping);
|
|
755
|
+
function validateParamConsistencyByDialect(sql, params, dialect) {
|
|
756
|
+
if (dialect === "postgres") {
|
|
757
|
+
validateParamConsistency(sql, params);
|
|
758
|
+
return;
|
|
801
759
|
}
|
|
802
|
-
ensureMappingsCoverAllIndices(mappingIndices, max, dialect);
|
|
803
|
-
}
|
|
804
|
-
function validateSqlPositions(sql, mappings, dialect) {
|
|
805
|
-
const { indices, sawNumbered, sawAnonymous } = getPlaceholderIndices(
|
|
806
|
-
sql,
|
|
807
|
-
dialect
|
|
808
|
-
);
|
|
809
760
|
if (dialect === "sqlite") {
|
|
810
|
-
|
|
761
|
+
validateQuestionMarkConsistency(sql, params);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (dialect === "mysql" || dialect === "mariadb") {
|
|
765
|
+
validateQuestionMarkConsistency(sql, params);
|
|
766
|
+
return;
|
|
811
767
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
768
|
+
validateParamConsistency(sql, params);
|
|
769
|
+
}
|
|
770
|
+
function needsQuoting(identifier) {
|
|
771
|
+
const s = String(identifier);
|
|
772
|
+
if (!REGEX_CACHE.VALID_IDENTIFIER.test(s)) return true;
|
|
773
|
+
const lowered = s.toLowerCase();
|
|
774
|
+
if (SQL_KEYWORDS.has(lowered)) return true;
|
|
775
|
+
return false;
|
|
818
776
|
}
|
|
819
777
|
|
|
820
778
|
// src/builder/shared/sql-utils.ts
|
|
821
|
-
var NUL = String.fromCharCode(0);
|
|
822
779
|
function containsControlChars(s) {
|
|
823
|
-
return
|
|
780
|
+
return /[\u0000-\u001F\u007F]/.test(s);
|
|
824
781
|
}
|
|
825
782
|
function assertNoControlChars(label, s) {
|
|
826
783
|
if (containsControlChars(s)) {
|
|
@@ -1039,16 +996,46 @@ function buildTableReference(schemaName, tableName, dialect) {
|
|
|
1039
996
|
return `"${safeSchema}"."${safeTable}"`;
|
|
1040
997
|
}
|
|
1041
998
|
function assertSafeAlias(alias) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
999
|
+
if (typeof alias !== "string") {
|
|
1000
|
+
throw new Error(`Invalid alias: expected string, got ${typeof alias}`);
|
|
1001
|
+
}
|
|
1002
|
+
const a = alias.trim();
|
|
1003
|
+
if (a.length === 0) {
|
|
1004
|
+
throw new Error("Invalid alias: required and cannot be empty");
|
|
1005
|
+
}
|
|
1006
|
+
if (a !== alias) {
|
|
1007
|
+
throw new Error("Invalid alias: leading/trailing whitespace");
|
|
1045
1008
|
}
|
|
1046
|
-
if (
|
|
1047
|
-
throw new Error(
|
|
1009
|
+
if (/[\u0000-\u001F\u007F]/.test(a)) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
"Invalid alias: contains unsafe characters (control characters)"
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
if (a.includes('"') || a.includes("'") || a.includes("`")) {
|
|
1015
|
+
throw new Error("Invalid alias: contains unsafe characters (quotes)");
|
|
1016
|
+
}
|
|
1017
|
+
if (a.includes(";")) {
|
|
1018
|
+
throw new Error("Invalid alias: contains unsafe characters (semicolon)");
|
|
1019
|
+
}
|
|
1020
|
+
if (a.includes("--") || a.includes("/*") || a.includes("*/")) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
"Invalid alias: contains unsafe characters (SQL comment tokens)"
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
if (/\s/.test(a)) {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
"Invalid alias: must be a simple identifier without whitespace"
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(a)) {
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`Invalid alias: must be a simple identifier (alphanumeric with underscores): "${alias}"`
|
|
1033
|
+
);
|
|
1048
1034
|
}
|
|
1049
|
-
|
|
1035
|
+
const lowered = a.toLowerCase();
|
|
1036
|
+
if (ALIAS_FORBIDDEN_KEYWORDS.has(lowered)) {
|
|
1050
1037
|
throw new Error(
|
|
1051
|
-
`alias
|
|
1038
|
+
`Invalid alias: '${alias}' is a SQL keyword that would break query parsing. Forbidden aliases: ${[...ALIAS_FORBIDDEN_KEYWORDS].join(", ")}`
|
|
1052
1039
|
);
|
|
1053
1040
|
}
|
|
1054
1041
|
}
|
|
@@ -1090,7 +1077,9 @@ function isValidRelationField(field) {
|
|
|
1090
1077
|
if (fk.length === 0) return false;
|
|
1091
1078
|
const refsRaw = field.references;
|
|
1092
1079
|
const refs = normalizeKeyList(refsRaw);
|
|
1093
|
-
if (refs.length === 0)
|
|
1080
|
+
if (refs.length === 0) {
|
|
1081
|
+
return fk.length === 1;
|
|
1082
|
+
}
|
|
1094
1083
|
if (refs.length !== fk.length) return false;
|
|
1095
1084
|
return true;
|
|
1096
1085
|
}
|
|
@@ -1105,6 +1094,8 @@ function getReferenceFieldNames(field, foreignKeyCount) {
|
|
|
1105
1094
|
return refs;
|
|
1106
1095
|
}
|
|
1107
1096
|
function joinCondition(field, parentModel, childModel, parentAlias, childAlias) {
|
|
1097
|
+
assertSafeAlias(parentAlias);
|
|
1098
|
+
assertSafeAlias(childAlias);
|
|
1108
1099
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
1109
1100
|
if (fkFields.length === 0) {
|
|
1110
1101
|
throw createError(
|
|
@@ -1254,6 +1245,66 @@ function normalizeOrderByInput(orderBy, parseValue) {
|
|
|
1254
1245
|
throw new Error("orderBy must be an object or array of objects");
|
|
1255
1246
|
}
|
|
1256
1247
|
|
|
1248
|
+
// src/builder/shared/order-by-determinism.ts
|
|
1249
|
+
function modelHasScalarId(model) {
|
|
1250
|
+
if (!model) return false;
|
|
1251
|
+
return getScalarFieldSet(model).has("id");
|
|
1252
|
+
}
|
|
1253
|
+
function hasIdTiebreaker(orderBy, parse) {
|
|
1254
|
+
if (!isNotNullish(orderBy)) return false;
|
|
1255
|
+
const normalized = normalizeOrderByInput(orderBy, parse);
|
|
1256
|
+
return normalized.some(
|
|
1257
|
+
(obj) => Object.prototype.hasOwnProperty.call(obj, "id")
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
function addIdTiebreaker(orderBy) {
|
|
1261
|
+
if (Array.isArray(orderBy)) return [...orderBy, { id: "asc" }];
|
|
1262
|
+
return [orderBy, { id: "asc" }];
|
|
1263
|
+
}
|
|
1264
|
+
function ensureDeterministicOrderByInput(args) {
|
|
1265
|
+
const { orderBy, model, parseValue } = args;
|
|
1266
|
+
if (!modelHasScalarId(model)) return orderBy;
|
|
1267
|
+
if (!isNotNullish(orderBy)) {
|
|
1268
|
+
return { id: "asc" };
|
|
1269
|
+
}
|
|
1270
|
+
if (hasIdTiebreaker(orderBy, parseValue)) return orderBy;
|
|
1271
|
+
return addIdTiebreaker(orderBy);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/builder/shared/validators/field-assertions.ts
|
|
1275
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Decimal", "BigInt"]);
|
|
1276
|
+
function assertScalarField(model, fieldName, context) {
|
|
1277
|
+
const field = getFieldInfo(model, fieldName);
|
|
1278
|
+
if (!field) {
|
|
1279
|
+
throw createError(
|
|
1280
|
+
`${context} references unknown field '${fieldName}' on model ${model.name}`,
|
|
1281
|
+
{
|
|
1282
|
+
field: fieldName,
|
|
1283
|
+
modelName: model.name,
|
|
1284
|
+
availableFields: model.fields.map((f) => f.name)
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
if (field.isRelation) {
|
|
1289
|
+
throw createError(
|
|
1290
|
+
`${context} does not support relation field '${fieldName}'`,
|
|
1291
|
+
{ field: fieldName, modelName: model.name }
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
return field;
|
|
1295
|
+
}
|
|
1296
|
+
function assertNumericField(model, fieldName, context) {
|
|
1297
|
+
const field = assertScalarField(model, fieldName, context);
|
|
1298
|
+
const baseType = field.type.replace(/\[\]|\?/g, "");
|
|
1299
|
+
if (!NUMERIC_TYPES.has(baseType)) {
|
|
1300
|
+
throw createError(
|
|
1301
|
+
`${context} requires numeric field, got '${field.type}'`,
|
|
1302
|
+
{ field: fieldName, modelName: model.name }
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
return field;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1257
1308
|
// src/builder/pagination.ts
|
|
1258
1309
|
var MAX_LIMIT_OFFSET = 2147483647;
|
|
1259
1310
|
function parseDirectionRaw(raw, errorLabel) {
|
|
@@ -1294,30 +1345,31 @@ function parseOrderByValue(v, fieldName) {
|
|
|
1294
1345
|
assertAllowedOrderByKeys(obj, fieldName);
|
|
1295
1346
|
return { direction, nulls };
|
|
1296
1347
|
}
|
|
1297
|
-
function normalizeFiniteInteger(name, v) {
|
|
1298
|
-
if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) {
|
|
1299
|
-
throw new Error(`${name} must be an integer`);
|
|
1300
|
-
}
|
|
1301
|
-
return v;
|
|
1302
|
-
}
|
|
1303
1348
|
function normalizeNonNegativeInt(name, v) {
|
|
1304
1349
|
if (schemaParser.isDynamicParameter(v)) return v;
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
return
|
|
1350
|
+
const result = normalizeIntLike(name, v, {
|
|
1351
|
+
min: 0,
|
|
1352
|
+
max: MAX_LIMIT_OFFSET,
|
|
1353
|
+
allowZero: true
|
|
1354
|
+
});
|
|
1355
|
+
if (result === void 0)
|
|
1356
|
+
throw new Error(`${name} normalization returned undefined`);
|
|
1357
|
+
return result;
|
|
1358
|
+
}
|
|
1359
|
+
function normalizeIntAllowNegative(name, v) {
|
|
1360
|
+
if (schemaParser.isDynamicParameter(v)) return v;
|
|
1361
|
+
const result = normalizeIntLike(name, v, {
|
|
1362
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
1363
|
+
max: MAX_LIMIT_OFFSET,
|
|
1364
|
+
allowZero: true
|
|
1365
|
+
});
|
|
1366
|
+
if (result === void 0)
|
|
1367
|
+
throw new Error(`${name} normalization returned undefined`);
|
|
1368
|
+
return result;
|
|
1313
1369
|
}
|
|
1314
1370
|
function hasNonNullishProp(v, key) {
|
|
1315
1371
|
return isPlainObject(v) && key in v && isNotNullish(v[key]);
|
|
1316
1372
|
}
|
|
1317
|
-
function normalizeIntegerOrDynamic(name, v) {
|
|
1318
|
-
if (schemaParser.isDynamicParameter(v)) return v;
|
|
1319
|
-
return normalizeFiniteInteger(name, v);
|
|
1320
|
-
}
|
|
1321
1373
|
function readSkipTake(relArgs) {
|
|
1322
1374
|
const hasSkip = hasNonNullishProp(relArgs, "skip");
|
|
1323
1375
|
const hasTake = hasNonNullishProp(relArgs, "take");
|
|
@@ -1331,7 +1383,7 @@ function readSkipTake(relArgs) {
|
|
|
1331
1383
|
}
|
|
1332
1384
|
const obj = relArgs;
|
|
1333
1385
|
const skipVal = hasSkip ? normalizeNonNegativeInt("skip", obj.skip) : void 0;
|
|
1334
|
-
const takeVal = hasTake ?
|
|
1386
|
+
const takeVal = hasTake ? normalizeIntAllowNegative("take", obj.take) : void 0;
|
|
1335
1387
|
return { hasSkip, hasTake, skipVal, takeVal };
|
|
1336
1388
|
}
|
|
1337
1389
|
function buildOrderByFragment(entries, alias, dialect, model) {
|
|
@@ -1357,9 +1409,7 @@ function buildOrderByFragment(entries, alias, dialect, model) {
|
|
|
1357
1409
|
return out.join(SQL_SEPARATORS.ORDER_BY);
|
|
1358
1410
|
}
|
|
1359
1411
|
function defaultNullsFor(dialect, direction) {
|
|
1360
|
-
if (dialect === "postgres")
|
|
1361
|
-
return direction === "asc" ? "last" : "first";
|
|
1362
|
-
}
|
|
1412
|
+
if (dialect === "postgres") return direction === "asc" ? "last" : "first";
|
|
1363
1413
|
return direction === "asc" ? "first" : "last";
|
|
1364
1414
|
}
|
|
1365
1415
|
function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
|
|
@@ -1376,13 +1426,12 @@ function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
|
|
|
1376
1426
|
}
|
|
1377
1427
|
function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
1378
1428
|
const entries = Object.entries(cursor);
|
|
1379
|
-
if (entries.length === 0)
|
|
1429
|
+
if (entries.length === 0)
|
|
1380
1430
|
throw new Error("cursor must have at least one field");
|
|
1381
|
-
}
|
|
1382
1431
|
const placeholdersByField = /* @__PURE__ */ new Map();
|
|
1383
1432
|
const parts = [];
|
|
1384
1433
|
for (const [field, value] of entries) {
|
|
1385
|
-
const c = `${cursorAlias}.${
|
|
1434
|
+
const c = `${cursorAlias}.${quoteColumn(model, field)}`;
|
|
1386
1435
|
if (value === null) {
|
|
1387
1436
|
parts.push(`${c} IS NULL`);
|
|
1388
1437
|
continue;
|
|
@@ -1396,13 +1445,6 @@ function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
|
1396
1445
|
placeholdersByField
|
|
1397
1446
|
};
|
|
1398
1447
|
}
|
|
1399
|
-
function cursorValueExpr(tableName, cursorAlias, cursorWhereSql, field, model) {
|
|
1400
|
-
const colName = quote(field);
|
|
1401
|
-
return `(SELECT ${cursorAlias}.${colName} ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1402
|
-
}
|
|
1403
|
-
function buildCursorRowExistsExpr(tableName, cursorAlias, cursorWhereSql) {
|
|
1404
|
-
return `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1405
|
-
}
|
|
1406
1448
|
function buildCursorEqualityExpr(columnExpr, valueExpr) {
|
|
1407
1449
|
return `((${valueExpr} IS NULL AND ${columnExpr} IS NULL) OR (${valueExpr} IS NOT NULL AND ${columnExpr} = ${valueExpr}))`;
|
|
1408
1450
|
}
|
|
@@ -1439,26 +1481,70 @@ function buildOrderEntries(orderBy) {
|
|
|
1439
1481
|
if (typeof value === "string") {
|
|
1440
1482
|
entries.push({ field, direction: value });
|
|
1441
1483
|
} else {
|
|
1442
|
-
entries.push({
|
|
1443
|
-
field,
|
|
1444
|
-
direction: value.sort,
|
|
1445
|
-
nulls: value.nulls
|
|
1446
|
-
});
|
|
1484
|
+
entries.push({ field, direction: value.direction, nulls: value.nulls });
|
|
1447
1485
|
}
|
|
1448
1486
|
}
|
|
1449
1487
|
}
|
|
1450
1488
|
return entries;
|
|
1451
1489
|
}
|
|
1490
|
+
function buildCursorCteSelectList(cursorEntries, orderEntries, model) {
|
|
1491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1492
|
+
const ordered = [];
|
|
1493
|
+
for (const [f] of cursorEntries) {
|
|
1494
|
+
if (!seen.has(f)) {
|
|
1495
|
+
seen.add(f);
|
|
1496
|
+
ordered.push(f);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
for (const e of orderEntries) {
|
|
1500
|
+
if (!seen.has(e.field)) {
|
|
1501
|
+
seen.add(e.field);
|
|
1502
|
+
ordered.push(e.field);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (ordered.length === 0) throw new Error("cursor cte select list is empty");
|
|
1506
|
+
return ordered.map((f) => quoteColumn(model, f)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
1507
|
+
}
|
|
1508
|
+
function truncateIdent(name, maxLen) {
|
|
1509
|
+
const s = String(name);
|
|
1510
|
+
if (s.length <= maxLen) return s;
|
|
1511
|
+
return s.slice(0, maxLen);
|
|
1512
|
+
}
|
|
1513
|
+
function buildCursorNames(outerAlias) {
|
|
1514
|
+
const maxLen = 63;
|
|
1515
|
+
const base = outerAlias.toLowerCase();
|
|
1516
|
+
const cteName = truncateIdent(`__tp_cursor_${base}`, maxLen);
|
|
1517
|
+
const srcAlias = truncateIdent(`__tp_cursor_src_${base}`, maxLen);
|
|
1518
|
+
if (cteName === outerAlias || srcAlias === outerAlias) {
|
|
1519
|
+
return {
|
|
1520
|
+
cteName: truncateIdent(`__tp_cursor_${base}_x`, maxLen),
|
|
1521
|
+
srcAlias: truncateIdent(`__tp_cursor_src_${base}_x`, maxLen)
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
return { cteName, srcAlias };
|
|
1525
|
+
}
|
|
1526
|
+
function assertCursorAndOrderFieldsScalar(model, cursor, orderEntries) {
|
|
1527
|
+
if (!model) return;
|
|
1528
|
+
for (const k of Object.keys(cursor)) assertScalarField(model, k, "cursor");
|
|
1529
|
+
for (const e of orderEntries) assertScalarField(model, e.field, "orderBy");
|
|
1530
|
+
}
|
|
1452
1531
|
function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect, model) {
|
|
1453
1532
|
var _a;
|
|
1533
|
+
assertSafeTableRef(tableName);
|
|
1534
|
+
assertSafeAlias(alias);
|
|
1454
1535
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
1455
1536
|
const cursorEntries = Object.entries(cursor);
|
|
1456
|
-
if (cursorEntries.length === 0)
|
|
1537
|
+
if (cursorEntries.length === 0)
|
|
1457
1538
|
throw new Error("cursor must have at least one field");
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1539
|
+
const { cteName, srcAlias } = buildCursorNames(alias);
|
|
1540
|
+
assertSafeAlias(cteName);
|
|
1541
|
+
assertSafeAlias(srcAlias);
|
|
1542
|
+
const deterministicOrderBy = ensureDeterministicOrderByInput({
|
|
1543
|
+
orderBy,
|
|
1544
|
+
model,
|
|
1545
|
+
parseValue: parseOrderByValue
|
|
1546
|
+
});
|
|
1547
|
+
let orderEntries = buildOrderEntries(deterministicOrderBy);
|
|
1462
1548
|
if (orderEntries.length === 0) {
|
|
1463
1549
|
orderEntries = cursorEntries.map(([field]) => ({
|
|
1464
1550
|
field,
|
|
@@ -1467,11 +1553,23 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1467
1553
|
} else {
|
|
1468
1554
|
orderEntries = ensureCursorFieldsInOrder(orderEntries, cursorEntries);
|
|
1469
1555
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1556
|
+
assertCursorAndOrderFieldsScalar(model, cursor, orderEntries);
|
|
1557
|
+
const { whereSql: cursorWhereSql, placeholdersByField } = buildCursorFilterParts(cursor, srcAlias, params, model);
|
|
1558
|
+
const cursorOrderBy = orderEntries.map(
|
|
1559
|
+
(e) => `${srcAlias}.${quoteColumn(model, e.field)} ${e.direction.toUpperCase()}`
|
|
1560
|
+
).join(", ");
|
|
1561
|
+
const selectList = buildCursorCteSelectList(
|
|
1562
|
+
cursorEntries,
|
|
1563
|
+
orderEntries,
|
|
1564
|
+
model
|
|
1474
1565
|
);
|
|
1566
|
+
const cte = `${cteName} AS (
|
|
1567
|
+
SELECT ${selectList} FROM ${tableName} ${srcAlias}
|
|
1568
|
+
WHERE ${cursorWhereSql}
|
|
1569
|
+
ORDER BY ${cursorOrderBy}
|
|
1570
|
+
LIMIT 1
|
|
1571
|
+
)`;
|
|
1572
|
+
const existsExpr = `EXISTS (SELECT 1 FROM ${cteName})`;
|
|
1475
1573
|
const outerCursorMatch = buildOuterCursorMatch(
|
|
1476
1574
|
cursor,
|
|
1477
1575
|
alias,
|
|
@@ -1479,34 +1577,29 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1479
1577
|
params,
|
|
1480
1578
|
model
|
|
1481
1579
|
);
|
|
1580
|
+
const getValueExpr = (field) => `(SELECT ${quoteColumn(model, field)} FROM ${cteName})`;
|
|
1482
1581
|
const orClauses = [];
|
|
1483
1582
|
for (let level = 0; level < orderEntries.length; level++) {
|
|
1484
1583
|
const andParts = [];
|
|
1485
1584
|
for (let i = 0; i < level; i++) {
|
|
1486
1585
|
const e2 = orderEntries[i];
|
|
1487
1586
|
const c2 = col(alias, e2.field, model);
|
|
1488
|
-
const v2 =
|
|
1489
|
-
tableName,
|
|
1490
|
-
cursorAlias,
|
|
1491
|
-
cursorWhereSql,
|
|
1492
|
-
e2.field);
|
|
1587
|
+
const v2 = getValueExpr(e2.field);
|
|
1493
1588
|
andParts.push(buildCursorEqualityExpr(c2, v2));
|
|
1494
1589
|
}
|
|
1495
1590
|
const e = orderEntries[level];
|
|
1496
1591
|
const c = col(alias, e.field, model);
|
|
1497
|
-
const v =
|
|
1498
|
-
tableName,
|
|
1499
|
-
cursorAlias,
|
|
1500
|
-
cursorWhereSql,
|
|
1501
|
-
e.field);
|
|
1592
|
+
const v = getValueExpr(e.field);
|
|
1502
1593
|
const nulls = (_a = e.nulls) != null ? _a : defaultNullsFor(d, e.direction);
|
|
1503
1594
|
andParts.push(buildCursorInequalityExpr(c, e.direction, nulls, v));
|
|
1504
1595
|
orClauses.push(`(${andParts.join(SQL_SEPARATORS.CONDITION_AND)})`);
|
|
1505
1596
|
}
|
|
1506
1597
|
const exclusive = orClauses.join(SQL_SEPARATORS.CONDITION_OR);
|
|
1507
|
-
|
|
1598
|
+
const condition = `(${existsExpr} ${SQL_SEPARATORS.CONDITION_AND} ((${exclusive})${SQL_SEPARATORS.CONDITION_OR}(${outerCursorMatch})))`;
|
|
1599
|
+
return { cte, condition };
|
|
1508
1600
|
}
|
|
1509
1601
|
function buildOrderBy(orderBy, alias, dialect, model) {
|
|
1602
|
+
assertSafeAlias(alias);
|
|
1510
1603
|
const entries = buildOrderEntries(orderBy);
|
|
1511
1604
|
if (entries.length === 0) return "";
|
|
1512
1605
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
@@ -1528,9 +1621,7 @@ function normalizeTakeLike(v) {
|
|
|
1528
1621
|
max: MAX_LIMIT_OFFSET,
|
|
1529
1622
|
allowZero: true
|
|
1530
1623
|
});
|
|
1531
|
-
if (typeof n === "number")
|
|
1532
|
-
if (n === 0) return 0;
|
|
1533
|
-
}
|
|
1624
|
+
if (typeof n === "number" && n === 0) return 0;
|
|
1534
1625
|
return n;
|
|
1535
1626
|
}
|
|
1536
1627
|
function normalizeSkipLike(v) {
|
|
@@ -1606,6 +1697,11 @@ function buildScalarOperator(expr, op, val, params, mode, fieldType, dialect) {
|
|
|
1606
1697
|
}
|
|
1607
1698
|
return handleInOperator(expr, op, val, params, dialect);
|
|
1608
1699
|
}
|
|
1700
|
+
if (op === Ops.EQUALS && mode === Modes.INSENSITIVE && !isNotNullish(dialect)) {
|
|
1701
|
+
throw createError(`Insensitive equals requires a SQL dialect`, {
|
|
1702
|
+
operator: op
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1609
1705
|
return handleComparisonOperator(expr, op, val, params);
|
|
1610
1706
|
}
|
|
1611
1707
|
function handleNullValue(expr, op) {
|
|
@@ -1621,6 +1717,28 @@ function normalizeMode(v) {
|
|
|
1621
1717
|
function handleNotOperator(expr, val, params, outerMode, fieldType, dialect) {
|
|
1622
1718
|
const innerMode = normalizeMode(val.mode);
|
|
1623
1719
|
const effectiveMode = innerMode != null ? innerMode : outerMode;
|
|
1720
|
+
const entries = Object.entries(val).filter(
|
|
1721
|
+
([k, v]) => k !== "mode" && v !== void 0
|
|
1722
|
+
);
|
|
1723
|
+
if (entries.length === 0) return "";
|
|
1724
|
+
if (!isNotNullish(dialect)) {
|
|
1725
|
+
const clauses = [];
|
|
1726
|
+
for (const [subOp, subVal] of entries) {
|
|
1727
|
+
const sub = buildScalarOperator(
|
|
1728
|
+
expr,
|
|
1729
|
+
subOp,
|
|
1730
|
+
subVal,
|
|
1731
|
+
params,
|
|
1732
|
+
effectiveMode,
|
|
1733
|
+
fieldType,
|
|
1734
|
+
void 0
|
|
1735
|
+
);
|
|
1736
|
+
if (sub && sub.trim().length > 0) clauses.push(`(${sub})`);
|
|
1737
|
+
}
|
|
1738
|
+
if (clauses.length === 0) return "";
|
|
1739
|
+
if (clauses.length === 1) return `${SQL_TEMPLATES.NOT} ${clauses[0]}`;
|
|
1740
|
+
return `${SQL_TEMPLATES.NOT} (${clauses.join(` ${SQL_TEMPLATES.AND} `)})`;
|
|
1741
|
+
}
|
|
1624
1742
|
return buildNotComposite(
|
|
1625
1743
|
expr,
|
|
1626
1744
|
val,
|
|
@@ -1835,6 +1953,7 @@ function handleArrayIsEmpty(expr, val, dialect) {
|
|
|
1835
1953
|
|
|
1836
1954
|
// src/builder/where/operators-json.ts
|
|
1837
1955
|
var SAFE_JSON_PATH_SEGMENT = /^[a-zA-Z_]\w*$/;
|
|
1956
|
+
var MAX_PATH_SEGMENT_LENGTH = 255;
|
|
1838
1957
|
function validateJsonPathSegments(segments) {
|
|
1839
1958
|
for (const segment of segments) {
|
|
1840
1959
|
if (typeof segment !== "string") {
|
|
@@ -1843,6 +1962,12 @@ function validateJsonPathSegments(segments) {
|
|
|
1843
1962
|
value: segment
|
|
1844
1963
|
});
|
|
1845
1964
|
}
|
|
1965
|
+
if (segment.length > MAX_PATH_SEGMENT_LENGTH) {
|
|
1966
|
+
throw createError(
|
|
1967
|
+
`JSON path segment too long: max ${MAX_PATH_SEGMENT_LENGTH} characters`,
|
|
1968
|
+
{ operator: Ops.PATH, value: `[${segment.length} chars]` }
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1846
1971
|
if (!SAFE_JSON_PATH_SEGMENT.test(segment)) {
|
|
1847
1972
|
throw createError(
|
|
1848
1973
|
`Invalid JSON path segment: '${segment}'. Must be alphanumeric with underscores, starting with letter or underscore.`,
|
|
@@ -1931,6 +2056,9 @@ function handleJsonWildcard(expr, op, val, params, wildcards, dialect) {
|
|
|
1931
2056
|
|
|
1932
2057
|
// src/builder/where/relations.ts
|
|
1933
2058
|
var NO_JOINS = Object.freeze([]);
|
|
2059
|
+
function freezeJoins(items) {
|
|
2060
|
+
return Object.freeze([...items]);
|
|
2061
|
+
}
|
|
1934
2062
|
function isListRelation(fieldType) {
|
|
1935
2063
|
return typeof fieldType === "string" && fieldType.endsWith("[]");
|
|
1936
2064
|
}
|
|
@@ -1993,7 +2121,7 @@ function buildListRelationFilters(args) {
|
|
|
1993
2121
|
const whereClause = `${relAlias}.${quote(checkField.name)} IS NULL`;
|
|
1994
2122
|
return Object.freeze({
|
|
1995
2123
|
clause: whereClause,
|
|
1996
|
-
joins: [leftJoinSql]
|
|
2124
|
+
joins: freezeJoins([leftJoinSql])
|
|
1997
2125
|
});
|
|
1998
2126
|
}
|
|
1999
2127
|
}
|
|
@@ -2198,7 +2326,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2198
2326
|
Ops.HAS_EVERY,
|
|
2199
2327
|
Ops.IS_EMPTY
|
|
2200
2328
|
]);
|
|
2201
|
-
const
|
|
2329
|
+
const JSON_OPS2 = /* @__PURE__ */ new Set([
|
|
2202
2330
|
Ops.PATH,
|
|
2203
2331
|
Ops.STRING_CONTAINS,
|
|
2204
2332
|
Ops.STRING_STARTS_WITH,
|
|
@@ -2215,7 +2343,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2215
2343
|
modelName
|
|
2216
2344
|
});
|
|
2217
2345
|
}
|
|
2218
|
-
const isJsonOp =
|
|
2346
|
+
const isJsonOp = JSON_OPS2.has(op);
|
|
2219
2347
|
const isFieldJson = isJsonType(fieldType);
|
|
2220
2348
|
const jsonOpMismatch = isJsonOp && !isFieldJson;
|
|
2221
2349
|
if (jsonOpMismatch) {
|
|
@@ -2229,6 +2357,14 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2229
2357
|
}
|
|
2230
2358
|
|
|
2231
2359
|
// src/builder/where/builder.ts
|
|
2360
|
+
var MAX_QUERY_DEPTH = 50;
|
|
2361
|
+
var EMPTY_JOINS = Object.freeze([]);
|
|
2362
|
+
var JSON_OPS = /* @__PURE__ */ new Set([
|
|
2363
|
+
Ops.PATH,
|
|
2364
|
+
Ops.STRING_CONTAINS,
|
|
2365
|
+
Ops.STRING_STARTS_WITH,
|
|
2366
|
+
Ops.STRING_ENDS_WITH
|
|
2367
|
+
]);
|
|
2232
2368
|
var WhereBuilder = class {
|
|
2233
2369
|
build(where, ctx) {
|
|
2234
2370
|
if (!isPlainObject(where)) {
|
|
@@ -2240,8 +2376,6 @@ var WhereBuilder = class {
|
|
|
2240
2376
|
return buildWhereInternal(where, ctx, this);
|
|
2241
2377
|
}
|
|
2242
2378
|
};
|
|
2243
|
-
var MAX_QUERY_DEPTH = 50;
|
|
2244
|
-
var EMPTY_JOINS = Object.freeze([]);
|
|
2245
2379
|
var whereBuilderInstance = new WhereBuilder();
|
|
2246
2380
|
function freezeResult(clause, joins = EMPTY_JOINS) {
|
|
2247
2381
|
return Object.freeze({ clause, joins });
|
|
@@ -2418,16 +2552,8 @@ function buildOperator(expr, op, val, ctx, mode, fieldType) {
|
|
|
2418
2552
|
if (fieldType && isArrayType(fieldType)) {
|
|
2419
2553
|
return buildArrayOperator(expr, op, val, ctx.params, fieldType, ctx.dialect);
|
|
2420
2554
|
}
|
|
2421
|
-
if (fieldType && isJsonType(fieldType)) {
|
|
2422
|
-
|
|
2423
|
-
Ops.PATH,
|
|
2424
|
-
Ops.STRING_CONTAINS,
|
|
2425
|
-
Ops.STRING_STARTS_WITH,
|
|
2426
|
-
Ops.STRING_ENDS_WITH
|
|
2427
|
-
]);
|
|
2428
|
-
if (JSON_OPS.has(op)) {
|
|
2429
|
-
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2430
|
-
}
|
|
2555
|
+
if (fieldType && isJsonType(fieldType) && JSON_OPS.has(op)) {
|
|
2556
|
+
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2431
2557
|
}
|
|
2432
2558
|
return buildScalarOperator(
|
|
2433
2559
|
expr,
|
|
@@ -2448,7 +2574,7 @@ function toSafeSqlIdentifier(input) {
|
|
|
2448
2574
|
const base = startsOk ? cleaned : `_${cleaned}`;
|
|
2449
2575
|
const fallback = base.length > 0 ? base : "_t";
|
|
2450
2576
|
const lowered = fallback.toLowerCase();
|
|
2451
|
-
return
|
|
2577
|
+
return ALIAS_FORBIDDEN_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
|
|
2452
2578
|
}
|
|
2453
2579
|
function createAliasGenerator(maxAliases = 1e4) {
|
|
2454
2580
|
let counter = 0;
|
|
@@ -2658,6 +2784,7 @@ function toPublicResult(clause, joins, params) {
|
|
|
2658
2784
|
// src/builder/where.ts
|
|
2659
2785
|
function buildWhereClause(where, options) {
|
|
2660
2786
|
var _a, _b, _c, _d, _e;
|
|
2787
|
+
assertSafeAlias(options.alias);
|
|
2661
2788
|
const dialect = options.dialect || getGlobalDialect();
|
|
2662
2789
|
const params = (_a = options.params) != null ? _a : createParamStore();
|
|
2663
2790
|
const ctx = {
|
|
@@ -2673,22 +2800,6 @@ function buildWhereClause(where, options) {
|
|
|
2673
2800
|
};
|
|
2674
2801
|
const result = whereBuilderInstance.build(where, ctx);
|
|
2675
2802
|
const publicResult = toPublicResult(result.clause, result.joins, params);
|
|
2676
|
-
if (!options.isSubquery) {
|
|
2677
|
-
const nums = [...publicResult.clause.matchAll(/\$(\d+)/g)].map(
|
|
2678
|
-
(m) => parseInt(m[1], 10)
|
|
2679
|
-
);
|
|
2680
|
-
if (nums.length > 0) {
|
|
2681
|
-
const min = Math.min(...nums);
|
|
2682
|
-
if (min === 1) {
|
|
2683
|
-
validateParamConsistency(publicResult.clause, publicResult.params);
|
|
2684
|
-
} else {
|
|
2685
|
-
validateParamConsistencyFragment(
|
|
2686
|
-
publicResult.clause,
|
|
2687
|
-
publicResult.params
|
|
2688
|
-
);
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
2803
|
return publicResult;
|
|
2693
2804
|
}
|
|
2694
2805
|
|
|
@@ -2826,6 +2937,9 @@ function buildRelationSelect(relArgs, relModel, relAlias) {
|
|
|
2826
2937
|
}
|
|
2827
2938
|
|
|
2828
2939
|
// src/builder/select/includes.ts
|
|
2940
|
+
var MAX_INCLUDE_DEPTH = 10;
|
|
2941
|
+
var MAX_TOTAL_SUBQUERIES = 100;
|
|
2942
|
+
var MAX_TOTAL_INCLUDES = 50;
|
|
2829
2943
|
function getRelationTableReference(relModel, dialect) {
|
|
2830
2944
|
return buildTableReference(
|
|
2831
2945
|
SQL_TEMPLATES.PUBLIC_SCHEMA,
|
|
@@ -2871,107 +2985,23 @@ function relationEntriesFromArgs(args, model) {
|
|
|
2871
2985
|
pushFrom(args.select);
|
|
2872
2986
|
return out;
|
|
2873
2987
|
}
|
|
2874
|
-
function assertScalarField(model, fieldName) {
|
|
2875
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
2876
|
-
if (!f) {
|
|
2877
|
-
throw new Error(
|
|
2878
|
-
`orderBy references unknown field '${fieldName}' on model ${model.name}`
|
|
2879
|
-
);
|
|
2880
|
-
}
|
|
2881
|
-
if (f.isRelation) {
|
|
2882
|
-
throw new Error(
|
|
2883
|
-
`orderBy does not support relation field '${fieldName}' on model ${model.name}`
|
|
2884
|
-
);
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
function validateOrderByDirection(fieldName, v) {
|
|
2888
|
-
const s = String(v).toLowerCase();
|
|
2889
|
-
if (s !== "asc" && s !== "desc") {
|
|
2890
|
-
throw new Error(
|
|
2891
|
-
`Invalid orderBy direction for '${fieldName}': ${String(v)}`
|
|
2892
|
-
);
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
function validateOrderByObject(fieldName, v) {
|
|
2896
|
-
if (!("sort" in v)) {
|
|
2897
|
-
throw new Error(
|
|
2898
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2899
|
-
);
|
|
2900
|
-
}
|
|
2901
|
-
validateOrderByDirection(fieldName, v.sort);
|
|
2902
|
-
if ("nulls" in v && isNotNullish(v.nulls)) {
|
|
2903
|
-
const n = String(v.nulls).toLowerCase();
|
|
2904
|
-
if (n !== "first" && n !== "last") {
|
|
2905
|
-
throw new Error(
|
|
2906
|
-
`Invalid orderBy.nulls for '${fieldName}': ${String(v.nulls)}`
|
|
2907
|
-
);
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
const allowed = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
2911
|
-
for (const k of Object.keys(v)) {
|
|
2912
|
-
if (!allowed.has(k)) {
|
|
2913
|
-
throw new Error(`Unsupported orderBy key '${k}' for field '${fieldName}'`);
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
function normalizeOrderByFieldName(name) {
|
|
2918
|
-
const fieldName = String(name).trim();
|
|
2919
|
-
if (fieldName.length === 0) {
|
|
2920
|
-
throw new Error("orderBy field name cannot be empty");
|
|
2921
|
-
}
|
|
2922
|
-
return fieldName;
|
|
2923
|
-
}
|
|
2924
|
-
function requirePlainObjectForOrderByEntry(v) {
|
|
2925
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
2926
|
-
throw new Error("orderBy array entries must be objects");
|
|
2927
|
-
}
|
|
2928
|
-
return v;
|
|
2929
|
-
}
|
|
2930
|
-
function parseSingleFieldOrderByObject(obj) {
|
|
2931
|
-
const entries = Object.entries(obj);
|
|
2932
|
-
if (entries.length !== 1) {
|
|
2933
|
-
throw new Error("orderBy array entries must have exactly one field");
|
|
2934
|
-
}
|
|
2935
|
-
const fieldName = normalizeOrderByFieldName(entries[0][0]);
|
|
2936
|
-
return [fieldName, entries[0][1]];
|
|
2937
|
-
}
|
|
2938
|
-
function parseOrderByArray(orderBy) {
|
|
2939
|
-
return orderBy.map(
|
|
2940
|
-
(item) => parseSingleFieldOrderByObject(requirePlainObjectForOrderByEntry(item))
|
|
2941
|
-
);
|
|
2942
|
-
}
|
|
2943
|
-
function parseOrderByObject(orderBy) {
|
|
2944
|
-
const out = [];
|
|
2945
|
-
for (const [k, v] of Object.entries(orderBy)) {
|
|
2946
|
-
out.push([normalizeOrderByFieldName(k), v]);
|
|
2947
|
-
}
|
|
2948
|
-
return out;
|
|
2949
|
-
}
|
|
2950
|
-
function getOrderByEntries(orderBy) {
|
|
2951
|
-
if (!isNotNullish(orderBy)) return [];
|
|
2952
|
-
if (Array.isArray(orderBy)) {
|
|
2953
|
-
return parseOrderByArray(orderBy);
|
|
2954
|
-
}
|
|
2955
|
-
if (typeof orderBy === "object" && orderBy !== null) {
|
|
2956
|
-
return parseOrderByObject(orderBy);
|
|
2957
|
-
}
|
|
2958
|
-
throw new Error("orderBy must be an object or array of objects");
|
|
2959
|
-
}
|
|
2960
2988
|
function validateOrderByForModel(model, orderBy) {
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2989
|
+
if (!isNotNullish(orderBy)) return;
|
|
2990
|
+
const scalarSet = getScalarFieldSet(model);
|
|
2991
|
+
const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
2992
|
+
for (const item of normalized) {
|
|
2993
|
+
const entries = Object.entries(item);
|
|
2994
|
+
if (entries.length !== 1) {
|
|
2995
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
2967
2996
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2997
|
+
const fieldName = String(entries[0][0]).trim();
|
|
2998
|
+
if (fieldName.length === 0)
|
|
2999
|
+
throw new Error("orderBy field name cannot be empty");
|
|
3000
|
+
if (!scalarSet.has(fieldName)) {
|
|
3001
|
+
throw new Error(
|
|
3002
|
+
`orderBy references unknown or non-scalar field '${fieldName}' on model ${model.name}`
|
|
3003
|
+
);
|
|
2971
3004
|
}
|
|
2972
|
-
throw new Error(
|
|
2973
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2974
|
-
);
|
|
2975
3005
|
}
|
|
2976
3006
|
}
|
|
2977
3007
|
function appendLimitOffset(sql, dialect, params, takeVal, skipVal, scope) {
|
|
@@ -3000,7 +3030,10 @@ function readWhereInput(relArgs) {
|
|
|
3000
3030
|
function readOrderByInput(relArgs) {
|
|
3001
3031
|
if (!isPlainObject(relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
3002
3032
|
if (!("orderBy" in relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
3003
|
-
return {
|
|
3033
|
+
return {
|
|
3034
|
+
hasOrderBy: true,
|
|
3035
|
+
orderBy: relArgs.orderBy
|
|
3036
|
+
};
|
|
3004
3037
|
}
|
|
3005
3038
|
function extractRelationPaginationConfig(relArgs) {
|
|
3006
3039
|
const { hasOrderBy, orderBy: rawOrderByInput } = readOrderByInput(relArgs);
|
|
@@ -3022,44 +3055,25 @@ function extractRelationPaginationConfig(relArgs) {
|
|
|
3022
3055
|
function maybeReverseNegativeTake(takeVal, hasOrderBy, orderByInput) {
|
|
3023
3056
|
if (typeof takeVal !== "number") return { takeVal, orderByInput };
|
|
3024
3057
|
if (takeVal >= 0) return { takeVal, orderByInput };
|
|
3025
|
-
if (!hasOrderBy)
|
|
3058
|
+
if (!hasOrderBy)
|
|
3026
3059
|
throw new Error("Negative take requires orderBy for deterministic results");
|
|
3027
|
-
}
|
|
3028
3060
|
return {
|
|
3029
3061
|
takeVal: Math.abs(takeVal),
|
|
3030
3062
|
orderByInput: reverseOrderByInput(orderByInput)
|
|
3031
3063
|
};
|
|
3032
3064
|
}
|
|
3033
|
-
function
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
(entry) => isPlainObject(entry) ? Object.prototype.hasOwnProperty.call(entry, "id") : false
|
|
3037
|
-
);
|
|
3038
|
-
}
|
|
3039
|
-
function modelHasScalarId(relModel) {
|
|
3040
|
-
const idField = relModel.fields.find((f) => f.name === "id");
|
|
3041
|
-
return Boolean(idField && !idField.isRelation);
|
|
3042
|
-
}
|
|
3043
|
-
function addIdTiebreaker(orderByInput) {
|
|
3044
|
-
if (Array.isArray(orderByInput)) return [...orderByInput, { id: "asc" }];
|
|
3045
|
-
return [orderByInput, { id: "asc" }];
|
|
3046
|
-
}
|
|
3047
|
-
function ensureDeterministicOrderBy(relModel, hasOrderBy, orderByInput, hasPagination) {
|
|
3048
|
-
if (!hasPagination) {
|
|
3049
|
-
if (hasOrderBy && isNotNullish(orderByInput)) {
|
|
3050
|
-
validateOrderByForModel(relModel, orderByInput);
|
|
3051
|
-
}
|
|
3052
|
-
return orderByInput;
|
|
3053
|
-
}
|
|
3054
|
-
if (!hasOrderBy) {
|
|
3055
|
-
return modelHasScalarId(relModel) ? { id: "asc" } : orderByInput;
|
|
3065
|
+
function finalizeOrderByForInclude(args) {
|
|
3066
|
+
if (args.hasOrderBy && isNotNullish(args.orderByInput)) {
|
|
3067
|
+
validateOrderByForModel(args.relModel, args.orderByInput);
|
|
3056
3068
|
}
|
|
3057
|
-
if (
|
|
3058
|
-
|
|
3069
|
+
if (!args.hasPagination) {
|
|
3070
|
+
return args.orderByInput;
|
|
3059
3071
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3072
|
+
return ensureDeterministicOrderByInput({
|
|
3073
|
+
orderBy: args.hasOrderBy ? args.orderByInput : void 0,
|
|
3074
|
+
model: args.relModel,
|
|
3075
|
+
parseValue: parseOrderByValue
|
|
3076
|
+
});
|
|
3063
3077
|
}
|
|
3064
3078
|
function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
3065
3079
|
let relSelect = buildRelationSelect(relArgs, relModel, relAlias);
|
|
@@ -3070,7 +3084,10 @@ function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
|
3070
3084
|
relAlias,
|
|
3071
3085
|
ctx.aliasGen,
|
|
3072
3086
|
ctx.params,
|
|
3073
|
-
ctx.dialect
|
|
3087
|
+
ctx.dialect,
|
|
3088
|
+
ctx.visitPath || [],
|
|
3089
|
+
(ctx.depth || 0) + 1,
|
|
3090
|
+
ctx.stats
|
|
3074
3091
|
) : [];
|
|
3075
3092
|
if (isNonEmptyArray(nestedIncludes)) {
|
|
3076
3093
|
const emptyJson = ctx.dialect === "postgres" ? `'[]'::json` : `json('[]')`;
|
|
@@ -3157,11 +3174,7 @@ function buildListIncludeSpec(args) {
|
|
|
3157
3174
|
joinPredicate: args.joinPredicate,
|
|
3158
3175
|
whereClause: args.whereClause
|
|
3159
3176
|
});
|
|
3160
|
-
return Object.freeze({
|
|
3161
|
-
name: args.relName,
|
|
3162
|
-
sql: sql2,
|
|
3163
|
-
isOneToOne: false
|
|
3164
|
-
});
|
|
3177
|
+
return Object.freeze({ name: args.relName, sql: sql2, isOneToOne: false });
|
|
3165
3178
|
}
|
|
3166
3179
|
const rowAlias = args.ctx.aliasGen.next(`${args.relName}_row`);
|
|
3167
3180
|
let base = buildBaseSql({
|
|
@@ -3172,9 +3185,7 @@ function buildListIncludeSpec(args) {
|
|
|
3172
3185
|
joinPredicate: args.joinPredicate,
|
|
3173
3186
|
whereClause: args.whereClause
|
|
3174
3187
|
});
|
|
3175
|
-
if (args.orderBySql) {
|
|
3176
|
-
base += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
|
|
3177
|
-
}
|
|
3188
|
+
if (args.orderBySql) base += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
|
|
3178
3189
|
base = appendLimitOffset(
|
|
3179
3190
|
base,
|
|
3180
3191
|
args.ctx.dialect,
|
|
@@ -3185,11 +3196,7 @@ function buildListIncludeSpec(args) {
|
|
|
3185
3196
|
);
|
|
3186
3197
|
const selectExpr = jsonAgg("row", args.ctx.dialect);
|
|
3187
3198
|
const sql = `${SQL_TEMPLATES.SELECT} ${selectExpr} ${SQL_TEMPLATES.FROM} (${base}) ${rowAlias}`;
|
|
3188
|
-
return Object.freeze({
|
|
3189
|
-
name: args.relName,
|
|
3190
|
-
sql,
|
|
3191
|
-
isOneToOne: false
|
|
3192
|
-
});
|
|
3199
|
+
return Object.freeze({ name: args.relName, sql, isOneToOne: false });
|
|
3193
3200
|
}
|
|
3194
3201
|
function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
3195
3202
|
const relTable = getRelationTableReference(relModel, ctx.dialect);
|
|
@@ -3220,12 +3227,12 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3220
3227
|
paginationConfig.orderBy
|
|
3221
3228
|
);
|
|
3222
3229
|
const hasPagination = paginationConfig.hasSkip || paginationConfig.hasTake;
|
|
3223
|
-
const finalOrderByInput =
|
|
3230
|
+
const finalOrderByInput = finalizeOrderByForInclude({
|
|
3224
3231
|
relModel,
|
|
3225
|
-
paginationConfig.hasOrderBy,
|
|
3226
|
-
adjusted.orderByInput,
|
|
3232
|
+
hasOrderBy: paginationConfig.hasOrderBy,
|
|
3233
|
+
orderByInput: adjusted.orderByInput,
|
|
3227
3234
|
hasPagination
|
|
3228
|
-
);
|
|
3235
|
+
});
|
|
3229
3236
|
const orderBySql = buildOrderBySql(
|
|
3230
3237
|
finalOrderByInput,
|
|
3231
3238
|
relAlias,
|
|
@@ -3246,11 +3253,7 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3246
3253
|
skipVal: paginationConfig.skipVal,
|
|
3247
3254
|
ctx
|
|
3248
3255
|
});
|
|
3249
|
-
return Object.freeze({
|
|
3250
|
-
name: relName,
|
|
3251
|
-
sql,
|
|
3252
|
-
isOneToOne: true
|
|
3253
|
-
});
|
|
3256
|
+
return Object.freeze({ name: relName, sql, isOneToOne: true });
|
|
3254
3257
|
}
|
|
3255
3258
|
return buildListIncludeSpec({
|
|
3256
3259
|
relName,
|
|
@@ -3266,32 +3269,69 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3266
3269
|
ctx
|
|
3267
3270
|
});
|
|
3268
3271
|
}
|
|
3269
|
-
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect) {
|
|
3272
|
+
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect, visitPath = [], depth = 0, stats) {
|
|
3273
|
+
if (!stats) stats = { totalIncludes: 0, totalSubqueries: 0, maxDepth: 0 };
|
|
3274
|
+
if (depth > MAX_INCLUDE_DEPTH) {
|
|
3275
|
+
throw new Error(
|
|
3276
|
+
`Maximum include depth of ${MAX_INCLUDE_DEPTH} exceeded. Path: ${visitPath.join(" -> ")}. Deep includes cause exponential SQL complexity and performance issues.`
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
stats.maxDepth = Math.max(stats.maxDepth, depth);
|
|
3270
3280
|
const includes = [];
|
|
3271
3281
|
const entries = relationEntriesFromArgs(args, model);
|
|
3272
3282
|
for (const [relName, relArgs] of entries) {
|
|
3273
3283
|
if (relArgs === false) continue;
|
|
3284
|
+
stats.totalIncludes++;
|
|
3285
|
+
if (stats.totalIncludes > MAX_TOTAL_INCLUDES) {
|
|
3286
|
+
throw new Error(
|
|
3287
|
+
`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.`
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
stats.totalSubqueries++;
|
|
3291
|
+
if (stats.totalSubqueries > MAX_TOTAL_SUBQUERIES) {
|
|
3292
|
+
throw new Error(
|
|
3293
|
+
`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.`
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3274
3296
|
const resolved = resolveRelationOrThrow(model, schemas, relName);
|
|
3275
|
-
const
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3297
|
+
const relationPath = `${model.name}.${relName}`;
|
|
3298
|
+
const currentPath = [...visitPath, relationPath];
|
|
3299
|
+
if (visitPath.includes(relationPath)) {
|
|
3300
|
+
throw new Error(
|
|
3301
|
+
`Circular include detected: ${currentPath.join(" -> ")}. Relation '${relationPath}' creates an infinite loop.`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
const modelOccurrences = currentPath.filter(
|
|
3305
|
+
(p) => p.startsWith(`${resolved.relModel.name}.`)
|
|
3306
|
+
).length;
|
|
3307
|
+
if (modelOccurrences > 2) {
|
|
3308
|
+
throw new Error(
|
|
3309
|
+
`Include too deeply nested: model '${resolved.relModel.name}' appears ${modelOccurrences} times in path: ${currentPath.join(" -> ")}`
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
includes.push(
|
|
3313
|
+
buildSingleInclude(relName, relArgs, resolved.field, resolved.relModel, {
|
|
3281
3314
|
model,
|
|
3282
3315
|
schemas,
|
|
3283
3316
|
parentAlias,
|
|
3284
3317
|
aliasGen,
|
|
3285
3318
|
dialect,
|
|
3286
|
-
params
|
|
3287
|
-
|
|
3319
|
+
params,
|
|
3320
|
+
visitPath: currentPath,
|
|
3321
|
+
depth: depth + 1,
|
|
3322
|
+
stats
|
|
3323
|
+
})
|
|
3288
3324
|
);
|
|
3289
|
-
includes.push(include);
|
|
3290
3325
|
}
|
|
3291
3326
|
return includes;
|
|
3292
3327
|
}
|
|
3293
3328
|
function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
3294
3329
|
const aliasGen = createAliasGenerator();
|
|
3330
|
+
const stats = {
|
|
3331
|
+
totalIncludes: 0,
|
|
3332
|
+
totalSubqueries: 0,
|
|
3333
|
+
maxDepth: 0
|
|
3334
|
+
};
|
|
3295
3335
|
return buildIncludeSqlInternal(
|
|
3296
3336
|
args,
|
|
3297
3337
|
model,
|
|
@@ -3299,7 +3339,10 @@ function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
|
3299
3339
|
parentAlias,
|
|
3300
3340
|
aliasGen,
|
|
3301
3341
|
params,
|
|
3302
|
-
dialect
|
|
3342
|
+
dialect,
|
|
3343
|
+
[],
|
|
3344
|
+
0,
|
|
3345
|
+
stats
|
|
3303
3346
|
);
|
|
3304
3347
|
}
|
|
3305
3348
|
function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
@@ -3310,10 +3353,14 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3310
3353
|
);
|
|
3311
3354
|
}
|
|
3312
3355
|
const field = model.fields.find((f) => f.name === relName);
|
|
3313
|
-
if (!field)
|
|
3356
|
+
if (!field)
|
|
3314
3357
|
throw new Error(
|
|
3315
3358
|
`_count.${relName} references unknown relation on model ${model.name}`
|
|
3316
3359
|
);
|
|
3360
|
+
if (!isValidRelationField(field)) {
|
|
3361
|
+
throw new Error(
|
|
3362
|
+
`_count.${relName} has invalid relation metadata on model ${model.name}`
|
|
3363
|
+
);
|
|
3317
3364
|
}
|
|
3318
3365
|
const relModel = schemas.find((m) => m.name === field.relatedModel);
|
|
3319
3366
|
if (!relModel) {
|
|
@@ -3323,31 +3370,78 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3323
3370
|
}
|
|
3324
3371
|
return { field, relModel };
|
|
3325
3372
|
}
|
|
3326
|
-
function
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3373
|
+
function defaultReferencesForCount(fkCount) {
|
|
3374
|
+
if (fkCount === 1) return ["id"];
|
|
3375
|
+
throw new Error(
|
|
3376
|
+
"Relation count for composite keys requires explicit references matching foreignKey length"
|
|
3377
|
+
);
|
|
3330
3378
|
}
|
|
3331
|
-
function
|
|
3379
|
+
function resolveCountKeyPairs(field) {
|
|
3332
3380
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
3333
|
-
|
|
3334
|
-
|
|
3381
|
+
if (fkFields.length === 0)
|
|
3382
|
+
throw new Error("Relation count requires foreignKey");
|
|
3383
|
+
const refsRaw = field.references;
|
|
3384
|
+
const refs = normalizeKeyList(refsRaw);
|
|
3385
|
+
const refFields = refs.length > 0 ? refs : defaultReferencesForCount(fkFields.length);
|
|
3386
|
+
if (refFields.length !== fkFields.length) {
|
|
3387
|
+
throw new Error(
|
|
3388
|
+
"Relation count requires references count to match foreignKey count"
|
|
3389
|
+
);
|
|
3390
|
+
}
|
|
3391
|
+
const relKeyFields = field.isForeignKeyLocal ? refFields : fkFields;
|
|
3392
|
+
const parentKeyFields = field.isForeignKeyLocal ? fkFields : refFields;
|
|
3393
|
+
return { relKeyFields, parentKeyFields };
|
|
3394
|
+
}
|
|
3395
|
+
function aliasQualifiedColumn(alias, model, field) {
|
|
3396
|
+
return `${alias}.${quoteColumn(model, field)}`;
|
|
3397
|
+
}
|
|
3398
|
+
function subqueryForCount(args) {
|
|
3399
|
+
const selectKeys = args.relKeyFields.map(
|
|
3400
|
+
(f, i) => `${aliasQualifiedColumn(args.countAlias, args.relModel, f)} AS "__fk${i}"`
|
|
3401
|
+
).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3402
|
+
const groupByKeys = args.relKeyFields.map((f) => aliasQualifiedColumn(args.countAlias, args.relModel, f)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3403
|
+
const cntExpr = args.dialect === "postgres" ? "COUNT(*)::int AS __cnt" : "COUNT(*) AS __cnt";
|
|
3404
|
+
return `(SELECT ${selectKeys}${SQL_SEPARATORS.FIELD_LIST}${cntExpr} FROM ${args.relTable} ${args.countAlias} GROUP BY ${groupByKeys})`;
|
|
3405
|
+
}
|
|
3406
|
+
function leftJoinOnForCount(args) {
|
|
3407
|
+
const parts = args.parentKeyFields.map(
|
|
3408
|
+
(f, i) => `${args.joinAlias}."__fk${i}" = ${aliasQualifiedColumn(args.parentAlias, args.parentModel, f)}`
|
|
3409
|
+
);
|
|
3410
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
3335
3411
|
}
|
|
3336
|
-
function
|
|
3337
|
-
|
|
3412
|
+
function nextAliasAvoiding(aliasGen, base, forbidden) {
|
|
3413
|
+
let a = aliasGen.next(base);
|
|
3414
|
+
while (forbidden.has(a)) a = aliasGen.next(base);
|
|
3415
|
+
return a;
|
|
3338
3416
|
}
|
|
3339
3417
|
function buildCountJoinAndPair(args) {
|
|
3340
3418
|
const relTable = getRelationTableReference(args.relModel, args.dialect);
|
|
3341
|
-
const
|
|
3342
|
-
const
|
|
3343
|
-
const
|
|
3344
|
-
args.
|
|
3419
|
+
const { relKeyFields, parentKeyFields } = resolveCountKeyPairs(args.field);
|
|
3420
|
+
const forbidden = /* @__PURE__ */ new Set([args.parentAlias]);
|
|
3421
|
+
const countAlias = nextAliasAvoiding(
|
|
3422
|
+
args.aliasGen,
|
|
3423
|
+
`__tp_cnt_${args.relName}`,
|
|
3424
|
+
forbidden
|
|
3425
|
+
);
|
|
3426
|
+
forbidden.add(countAlias);
|
|
3427
|
+
const subquery = subqueryForCount({
|
|
3428
|
+
dialect: args.dialect,
|
|
3345
3429
|
relTable,
|
|
3346
3430
|
countAlias,
|
|
3347
|
-
|
|
3431
|
+
relModel: args.relModel,
|
|
3432
|
+
relKeyFields
|
|
3433
|
+
});
|
|
3434
|
+
const joinAlias = nextAliasAvoiding(
|
|
3435
|
+
args.aliasGen,
|
|
3436
|
+
`__tp_cnt_j_${args.relName}`,
|
|
3437
|
+
forbidden
|
|
3348
3438
|
);
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3439
|
+
const leftJoinOn = leftJoinOnForCount({
|
|
3440
|
+
joinAlias,
|
|
3441
|
+
parentAlias: args.parentAlias,
|
|
3442
|
+
parentModel: args.parentModel,
|
|
3443
|
+
parentKeyFields
|
|
3444
|
+
});
|
|
3351
3445
|
return {
|
|
3352
3446
|
joinSql: `LEFT JOIN ${subquery} ${joinAlias} ON ${leftJoinOn}`,
|
|
3353
3447
|
pairSql: `${sqlStringLiteral(args.relName)}, COALESCE(${joinAlias}.__cnt, 0)`
|
|
@@ -3356,6 +3450,7 @@ function buildCountJoinAndPair(args) {
|
|
|
3356
3450
|
function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params, dialect) {
|
|
3357
3451
|
const joins = [];
|
|
3358
3452
|
const pairs = [];
|
|
3453
|
+
const aliasGen = createAliasGenerator();
|
|
3359
3454
|
for (const [relName, shouldCount] of Object.entries(countSelect)) {
|
|
3360
3455
|
if (!shouldCount) continue;
|
|
3361
3456
|
const resolved = resolveCountRelationOrThrow(relName, model, schemas);
|
|
@@ -3363,29 +3458,33 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3363
3458
|
relName,
|
|
3364
3459
|
field: resolved.field,
|
|
3365
3460
|
relModel: resolved.relModel,
|
|
3461
|
+
parentModel: model,
|
|
3366
3462
|
parentAlias,
|
|
3367
|
-
dialect
|
|
3463
|
+
dialect,
|
|
3464
|
+
aliasGen
|
|
3368
3465
|
});
|
|
3369
3466
|
joins.push(built.joinSql);
|
|
3370
3467
|
pairs.push(built.pairSql);
|
|
3371
3468
|
}
|
|
3372
|
-
return {
|
|
3373
|
-
joins,
|
|
3374
|
-
jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST)
|
|
3375
|
-
};
|
|
3469
|
+
return { joins, jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST) };
|
|
3376
3470
|
}
|
|
3377
3471
|
|
|
3378
3472
|
// src/builder/select/assembly.ts
|
|
3379
|
-
var
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
}
|
|
3473
|
+
var ALIAS_CAPTURE = "([A-Za-z_][A-Za-z0-9_]*)";
|
|
3474
|
+
var COLUMN_PART = '(?:"([^"]+)"|([a-z_][a-z0-9_]*))';
|
|
3475
|
+
var AS_PART = `(?:\\s+AS\\s+${COLUMN_PART})?`;
|
|
3476
|
+
var SIMPLE_COLUMN_PATTERN = `^${ALIAS_CAPTURE}\\.${COLUMN_PART}${AS_PART}$`;
|
|
3477
|
+
var SIMPLE_COLUMN_RE = new RegExp(SIMPLE_COLUMN_PATTERN, "i");
|
|
3383
3478
|
function joinNonEmpty(parts, sep) {
|
|
3384
3479
|
return parts.filter((s) => s.trim().length > 0).join(sep);
|
|
3385
3480
|
}
|
|
3386
3481
|
function buildWhereSql(conditions) {
|
|
3387
3482
|
if (!isNonEmptyArray(conditions)) return "";
|
|
3388
|
-
|
|
3483
|
+
const parts = [
|
|
3484
|
+
SQL_TEMPLATES.WHERE,
|
|
3485
|
+
conditions.join(SQL_SEPARATORS.CONDITION_AND)
|
|
3486
|
+
];
|
|
3487
|
+
return ` ${parts.join(" ")}`;
|
|
3389
3488
|
}
|
|
3390
3489
|
function buildJoinsSql(...joinGroups) {
|
|
3391
3490
|
const all = [];
|
|
@@ -3400,37 +3499,43 @@ function buildSelectList(baseSelect, extraCols) {
|
|
|
3400
3499
|
if (base && extra) return `${base}${SQL_SEPARATORS.FIELD_LIST}${extra}`;
|
|
3401
3500
|
return base || extra;
|
|
3402
3501
|
}
|
|
3403
|
-
function finalizeSql(sql, params) {
|
|
3502
|
+
function finalizeSql(sql, params, dialect) {
|
|
3404
3503
|
const snapshot = params.snapshot();
|
|
3405
3504
|
validateSelectQuery(sql);
|
|
3406
|
-
|
|
3505
|
+
validateParamConsistencyByDialect(
|
|
3506
|
+
sql,
|
|
3507
|
+
snapshot.params,
|
|
3508
|
+
dialect === "sqlite" ? "postgres" : dialect
|
|
3509
|
+
);
|
|
3407
3510
|
return Object.freeze({
|
|
3408
3511
|
sql,
|
|
3409
|
-
params:
|
|
3512
|
+
params: snapshot.params,
|
|
3410
3513
|
paramMappings: Object.freeze(snapshot.mappings)
|
|
3411
3514
|
});
|
|
3412
3515
|
}
|
|
3413
|
-
function parseSimpleScalarSelect(select,
|
|
3414
|
-
var _a, _b;
|
|
3516
|
+
function parseSimpleScalarSelect(select, fromAlias) {
|
|
3517
|
+
var _a, _b, _c, _d;
|
|
3415
3518
|
const raw = select.trim();
|
|
3416
3519
|
if (raw.length === 0) return [];
|
|
3417
|
-
let re = SIMPLE_SELECT_RE_CACHE.get(alias);
|
|
3418
|
-
if (!re) {
|
|
3419
|
-
const safeAlias2 = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3420
|
-
re = new RegExp(`^${safeAlias2}\\.(?:"([^"]+)"|([a-z_][a-z0-9_]*))$`, "i");
|
|
3421
|
-
SIMPLE_SELECT_RE_CACHE.set(alias, re);
|
|
3422
|
-
}
|
|
3423
3520
|
const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
|
|
3424
3521
|
const names = [];
|
|
3425
3522
|
for (const part of parts) {
|
|
3426
3523
|
const p = part.trim();
|
|
3427
|
-
const m = p.match(
|
|
3524
|
+
const m = p.match(SIMPLE_COLUMN_RE);
|
|
3428
3525
|
if (!m) {
|
|
3429
3526
|
throw new Error(
|
|
3430
|
-
`sqlite distinct emulation requires scalar select fields to be simple columns. Got: ${p}`
|
|
3527
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (optionally with AS). Got: ${p}`
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
const actualAlias = m[1];
|
|
3531
|
+
if (actualAlias.toLowerCase() !== fromAlias.toLowerCase()) {
|
|
3532
|
+
throw new Error(
|
|
3533
|
+
`Expected alias '${fromAlias}', got '${actualAlias}' in: ${p}`
|
|
3431
3534
|
);
|
|
3432
3535
|
}
|
|
3433
|
-
const
|
|
3536
|
+
const columnName = ((_b = (_a = m[2]) != null ? _a : m[3]) != null ? _b : "").trim();
|
|
3537
|
+
const outAlias = ((_d = (_c = m[4]) != null ? _c : m[5]) != null ? _d : "").trim();
|
|
3538
|
+
const name = outAlias.length > 0 ? outAlias : columnName;
|
|
3434
3539
|
if (name.length === 0) {
|
|
3435
3540
|
throw new Error(`Failed to parse selected column name from: ${p}`);
|
|
3436
3541
|
}
|
|
@@ -3439,18 +3544,18 @@ function parseSimpleScalarSelect(select, alias) {
|
|
|
3439
3544
|
return names;
|
|
3440
3545
|
}
|
|
3441
3546
|
function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
|
|
3442
|
-
const
|
|
3443
|
-
|
|
3444
|
-
|
|
3547
|
+
const src = String(fromAlias);
|
|
3548
|
+
if (src.length === 0) return orderBy;
|
|
3549
|
+
const escaped = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3550
|
+
const re = new RegExp(`\\b${escaped}\\.`, "gi");
|
|
3551
|
+
return orderBy.replace(re, `${outerAlias}.`);
|
|
3445
3552
|
}
|
|
3446
3553
|
function buildDistinctColumns(distinct, fromAlias, model) {
|
|
3447
3554
|
return distinct.map((f) => col(fromAlias, f, model)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3448
3555
|
}
|
|
3449
3556
|
function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
3450
3557
|
const outputCols = [...scalarNames, ...includeNames];
|
|
3451
|
-
if (hasCount)
|
|
3452
|
-
outputCols.push("_count");
|
|
3453
|
-
}
|
|
3558
|
+
if (hasCount) outputCols.push("_count");
|
|
3454
3559
|
const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3455
3560
|
if (!isNonEmptyString(formatted)) {
|
|
3456
3561
|
throw new Error("distinct emulation requires at least one output column");
|
|
@@ -3459,9 +3564,10 @@ function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
|
3459
3564
|
}
|
|
3460
3565
|
function buildWindowOrder(args) {
|
|
3461
3566
|
const { baseOrder, idField, fromAlias, model } = args;
|
|
3567
|
+
const fromLower = String(fromAlias).toLowerCase();
|
|
3462
3568
|
const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
|
|
3463
3569
|
const hasIdInOrder = orderFields.some(
|
|
3464
|
-
(f) => f.startsWith(`${
|
|
3570
|
+
(f) => f.startsWith(`${fromLower}.id `) || f.startsWith(`${fromLower}."id" `)
|
|
3465
3571
|
);
|
|
3466
3572
|
if (hasIdInOrder) return baseOrder;
|
|
3467
3573
|
const idTiebreaker = idField ? `, ${col(fromAlias, "id", model)} ASC` : "";
|
|
@@ -3496,15 +3602,37 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3496
3602
|
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, `"__tp_distinct"`) : replaceOrderByAlias(fallbackOrder, from.alias, `"__tp_distinct"`);
|
|
3497
3603
|
const joins = buildJoinsSql(whereJoins, countJoins);
|
|
3498
3604
|
const conditions = [];
|
|
3499
|
-
if (whereClause && whereClause !== "1=1")
|
|
3500
|
-
conditions.push(whereClause);
|
|
3501
|
-
}
|
|
3605
|
+
if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
|
|
3502
3606
|
const whereSql = buildWhereSql(conditions);
|
|
3503
3607
|
const innerSelectList = selectWithIncludes.trim();
|
|
3504
3608
|
const innerComma = innerSelectList.length > 0 ? SQL_SEPARATORS.FIELD_LIST : "";
|
|
3505
|
-
const
|
|
3506
|
-
|
|
3507
|
-
|
|
3609
|
+
const innerParts = [
|
|
3610
|
+
SQL_TEMPLATES.SELECT,
|
|
3611
|
+
innerSelectList + innerComma,
|
|
3612
|
+
`ROW_NUMBER() OVER (PARTITION BY ${distinctCols} ORDER BY ${windowOrder})`,
|
|
3613
|
+
SQL_TEMPLATES.AS,
|
|
3614
|
+
'"__tp_rn"',
|
|
3615
|
+
SQL_TEMPLATES.FROM,
|
|
3616
|
+
from.table,
|
|
3617
|
+
from.alias
|
|
3618
|
+
];
|
|
3619
|
+
if (joins) innerParts.push(joins);
|
|
3620
|
+
if (whereSql) innerParts.push(whereSql);
|
|
3621
|
+
const inner = innerParts.filter(Boolean).join(" ");
|
|
3622
|
+
const outerParts = [
|
|
3623
|
+
SQL_TEMPLATES.SELECT,
|
|
3624
|
+
outerSelectCols,
|
|
3625
|
+
SQL_TEMPLATES.FROM,
|
|
3626
|
+
`(${inner})`,
|
|
3627
|
+
SQL_TEMPLATES.AS,
|
|
3628
|
+
'"__tp_distinct"',
|
|
3629
|
+
SQL_TEMPLATES.WHERE,
|
|
3630
|
+
'"__tp_rn" = 1'
|
|
3631
|
+
];
|
|
3632
|
+
if (isNonEmptyString(outerOrder)) {
|
|
3633
|
+
outerParts.push(SQL_TEMPLATES.ORDER_BY, outerOrder);
|
|
3634
|
+
}
|
|
3635
|
+
return outerParts.filter(Boolean).join(" ");
|
|
3508
3636
|
}
|
|
3509
3637
|
function buildIncludeColumns(spec) {
|
|
3510
3638
|
var _a, _b;
|
|
@@ -3632,6 +3760,7 @@ function constructFinalSql(spec) {
|
|
|
3632
3760
|
orderBy,
|
|
3633
3761
|
distinct,
|
|
3634
3762
|
method,
|
|
3763
|
+
cursorCte,
|
|
3635
3764
|
cursorClause,
|
|
3636
3765
|
params,
|
|
3637
3766
|
dialect,
|
|
@@ -3646,9 +3775,13 @@ function constructFinalSql(spec) {
|
|
|
3646
3775
|
const spec2 = withCountJoins(spec, countJoins, whereJoins);
|
|
3647
3776
|
let sql2 = buildSqliteDistinctQuery(spec2, selectWithIncludes).trim();
|
|
3648
3777
|
sql2 = appendPagination(sql2, spec);
|
|
3649
|
-
return finalizeSql(sql2, params);
|
|
3778
|
+
return finalizeSql(sql2, params, dialect);
|
|
3779
|
+
}
|
|
3780
|
+
const parts = [];
|
|
3781
|
+
if (cursorCte) {
|
|
3782
|
+
parts.push(`WITH ${cursorCte}`);
|
|
3650
3783
|
}
|
|
3651
|
-
|
|
3784
|
+
parts.push(SQL_TEMPLATES.SELECT);
|
|
3652
3785
|
const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct, model) : null;
|
|
3653
3786
|
if (distinctOn) parts.push(distinctOn);
|
|
3654
3787
|
const baseSelect = (select != null ? select : "").trim();
|
|
@@ -3664,7 +3797,7 @@ function constructFinalSql(spec) {
|
|
|
3664
3797
|
if (isNonEmptyString(orderBy)) parts.push(SQL_TEMPLATES.ORDER_BY, orderBy);
|
|
3665
3798
|
let sql = parts.join(" ").trim();
|
|
3666
3799
|
sql = appendPagination(sql, spec);
|
|
3667
|
-
return finalizeSql(sql, params);
|
|
3800
|
+
return finalizeSql(sql, params, dialect);
|
|
3668
3801
|
}
|
|
3669
3802
|
|
|
3670
3803
|
// src/builder/select.ts
|
|
@@ -3697,7 +3830,7 @@ function buildPostgresDistinctOrderBy(distinctFields, existing) {
|
|
|
3697
3830
|
}
|
|
3698
3831
|
return next;
|
|
3699
3832
|
}
|
|
3700
|
-
function applyPostgresDistinctOrderBy(args
|
|
3833
|
+
function applyPostgresDistinctOrderBy(args) {
|
|
3701
3834
|
const distinctFields = normalizeDistinctFields(args.distinct);
|
|
3702
3835
|
if (distinctFields.length === 0) return args;
|
|
3703
3836
|
if (!isNotNullish(args.orderBy)) return args;
|
|
@@ -3707,19 +3840,6 @@ function applyPostgresDistinctOrderBy(args, _model) {
|
|
|
3707
3840
|
orderBy: buildPostgresDistinctOrderBy(distinctFields, existing)
|
|
3708
3841
|
});
|
|
3709
3842
|
}
|
|
3710
|
-
function assertScalarFieldOnModel(model, fieldName, ctx) {
|
|
3711
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
3712
|
-
if (!f) {
|
|
3713
|
-
throw new Error(
|
|
3714
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}`
|
|
3715
|
-
);
|
|
3716
|
-
}
|
|
3717
|
-
if (f.isRelation) {
|
|
3718
|
-
throw new Error(
|
|
3719
|
-
`${ctx} does not support relation field '${fieldName}' on model ${model.name}`
|
|
3720
|
-
);
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
3843
|
function validateDistinct(model, distinct) {
|
|
3724
3844
|
if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) return;
|
|
3725
3845
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3730,24 +3850,24 @@ function validateDistinct(model, distinct) {
|
|
|
3730
3850
|
throw new Error(`distinct must not contain duplicates (field: '${f}')`);
|
|
3731
3851
|
}
|
|
3732
3852
|
seen.add(f);
|
|
3733
|
-
|
|
3853
|
+
assertScalarField(model, f, "distinct");
|
|
3734
3854
|
}
|
|
3735
3855
|
}
|
|
3736
|
-
function validateOrderByValue(fieldName, v) {
|
|
3737
|
-
parseOrderByValue(v, fieldName);
|
|
3738
|
-
}
|
|
3739
3856
|
function validateOrderBy(model, orderBy) {
|
|
3740
3857
|
if (!isNotNullish(orderBy)) return;
|
|
3741
3858
|
const items = normalizeOrderByInput2(orderBy);
|
|
3742
3859
|
if (items.length === 0) return;
|
|
3743
3860
|
for (const it of items) {
|
|
3744
3861
|
const entries = Object.entries(it);
|
|
3862
|
+
if (entries.length !== 1) {
|
|
3863
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
3864
|
+
}
|
|
3745
3865
|
const fieldName = String(entries[0][0]).trim();
|
|
3746
3866
|
if (fieldName.length === 0) {
|
|
3747
3867
|
throw new Error("orderBy field name cannot be empty");
|
|
3748
3868
|
}
|
|
3749
|
-
|
|
3750
|
-
|
|
3869
|
+
assertScalarField(model, fieldName, "orderBy");
|
|
3870
|
+
parseOrderByValue(entries[0][1], fieldName);
|
|
3751
3871
|
}
|
|
3752
3872
|
}
|
|
3753
3873
|
function validateCursor(model, cursor) {
|
|
@@ -3764,7 +3884,7 @@ function validateCursor(model, cursor) {
|
|
|
3764
3884
|
if (f.length === 0) {
|
|
3765
3885
|
throw new Error("cursor field name cannot be empty");
|
|
3766
3886
|
}
|
|
3767
|
-
|
|
3887
|
+
assertScalarField(model, f, "cursor");
|
|
3768
3888
|
}
|
|
3769
3889
|
}
|
|
3770
3890
|
function resolveDialect(dialect) {
|
|
@@ -3783,20 +3903,21 @@ function normalizeArgsForNegativeTake(method, args) {
|
|
|
3783
3903
|
orderBy: reverseOrderByInput(args.orderBy)
|
|
3784
3904
|
});
|
|
3785
3905
|
}
|
|
3786
|
-
function normalizeArgsForDialect(dialect, args
|
|
3906
|
+
function normalizeArgsForDialect(dialect, args) {
|
|
3787
3907
|
if (dialect !== "postgres") return args;
|
|
3788
3908
|
return applyPostgresDistinctOrderBy(args);
|
|
3789
3909
|
}
|
|
3790
3910
|
function buildCursorClauseIfAny(input) {
|
|
3791
|
-
const { cursor, orderBy, tableName, alias, params, dialect } = input;
|
|
3792
|
-
if (!isNotNullish(cursor)) return
|
|
3911
|
+
const { cursor, orderBy, tableName, alias, params, dialect, model } = input;
|
|
3912
|
+
if (!isNotNullish(cursor)) return {};
|
|
3793
3913
|
return buildCursorCondition(
|
|
3794
3914
|
cursor,
|
|
3795
3915
|
orderBy,
|
|
3796
3916
|
tableName,
|
|
3797
3917
|
alias,
|
|
3798
3918
|
params,
|
|
3799
|
-
dialect
|
|
3919
|
+
dialect,
|
|
3920
|
+
model
|
|
3800
3921
|
);
|
|
3801
3922
|
}
|
|
3802
3923
|
function buildSelectSpec(input) {
|
|
@@ -3835,14 +3956,20 @@ function buildSelectSpec(input) {
|
|
|
3835
3956
|
params,
|
|
3836
3957
|
dialect
|
|
3837
3958
|
);
|
|
3838
|
-
const
|
|
3959
|
+
const cursorResult = buildCursorClauseIfAny({
|
|
3839
3960
|
cursor,
|
|
3840
3961
|
orderBy: normalizedArgs.orderBy,
|
|
3841
3962
|
tableName,
|
|
3842
3963
|
alias,
|
|
3843
3964
|
params,
|
|
3844
|
-
dialect
|
|
3965
|
+
dialect,
|
|
3966
|
+
model
|
|
3845
3967
|
});
|
|
3968
|
+
if (dialect === "sqlite" && isNonEmptyArray(normalizedArgs.distinct) && cursorResult.condition) {
|
|
3969
|
+
throw new Error(
|
|
3970
|
+
"Cursor pagination with distinct is not supported in SQLite due to window function limitations. Use findMany with skip/take instead, or remove distinct."
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3846
3973
|
return {
|
|
3847
3974
|
select: selectFields,
|
|
3848
3975
|
includes,
|
|
@@ -3853,7 +3980,8 @@ function buildSelectSpec(input) {
|
|
|
3853
3980
|
pagination: { take, skip },
|
|
3854
3981
|
distinct: normalizedArgs.distinct,
|
|
3855
3982
|
method,
|
|
3856
|
-
|
|
3983
|
+
cursorCte: cursorResult.cte,
|
|
3984
|
+
cursorClause: cursorResult.condition,
|
|
3857
3985
|
params,
|
|
3858
3986
|
dialect,
|
|
3859
3987
|
model,
|
|
@@ -3867,9 +3995,7 @@ function buildSelectSql(input) {
|
|
|
3867
3995
|
assertSafeTableRef(from.tableName);
|
|
3868
3996
|
const dialectToUse = resolveDialect(dialect);
|
|
3869
3997
|
const argsForSql = normalizeArgsForNegativeTake(method, args);
|
|
3870
|
-
const normalizedArgs = normalizeArgsForDialect(
|
|
3871
|
-
dialectToUse,
|
|
3872
|
-
argsForSql);
|
|
3998
|
+
const normalizedArgs = normalizeArgsForDialect(dialectToUse, argsForSql);
|
|
3873
3999
|
validateDistinct(model, normalizedArgs.distinct);
|
|
3874
4000
|
validateOrderBy(model, normalizedArgs.orderBy);
|
|
3875
4001
|
validateCursor(model, normalizedArgs.cursor);
|
|
@@ -3885,8 +4011,21 @@ function buildSelectSql(input) {
|
|
|
3885
4011
|
});
|
|
3886
4012
|
return constructFinalSql(spec);
|
|
3887
4013
|
}
|
|
3888
|
-
|
|
3889
|
-
|
|
4014
|
+
|
|
4015
|
+
// src/builder/shared/comparison-builder.ts
|
|
4016
|
+
function buildComparisons(expr, filter, params, dialect, builder, excludeKeys = /* @__PURE__ */ new Set(["mode"])) {
|
|
4017
|
+
const out = [];
|
|
4018
|
+
for (const [op, val] of Object.entries(filter)) {
|
|
4019
|
+
if (excludeKeys.has(op) || val === void 0) continue;
|
|
4020
|
+
const built = builder(expr, op, val, params, dialect);
|
|
4021
|
+
if (built && built.trim().length > 0) {
|
|
4022
|
+
out.push(built);
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
return out;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
// src/builder/aggregates.ts
|
|
3890
4029
|
var AGGREGATES = [
|
|
3891
4030
|
["_sum", "SUM"],
|
|
3892
4031
|
["_avg", "AVG"],
|
|
@@ -3901,22 +4040,32 @@ var COMPARISON_OPS = {
|
|
|
3901
4040
|
[Ops.LT]: "<",
|
|
3902
4041
|
[Ops.LTE]: "<="
|
|
3903
4042
|
};
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
MODEL_FIELD_CACHE.set(model, m);
|
|
3915
|
-
return m;
|
|
3916
|
-
}
|
|
4043
|
+
var HAVING_ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
4044
|
+
Ops.EQUALS,
|
|
4045
|
+
Ops.NOT,
|
|
4046
|
+
Ops.GT,
|
|
4047
|
+
Ops.GTE,
|
|
4048
|
+
Ops.LT,
|
|
4049
|
+
Ops.LTE,
|
|
4050
|
+
Ops.IN,
|
|
4051
|
+
Ops.NOT_IN
|
|
4052
|
+
]);
|
|
3917
4053
|
function isTruthySelection(v) {
|
|
3918
4054
|
return v === true;
|
|
3919
4055
|
}
|
|
4056
|
+
function isLogicalKey(key) {
|
|
4057
|
+
return key === LogicalOps.AND || key === LogicalOps.OR || key === LogicalOps.NOT;
|
|
4058
|
+
}
|
|
4059
|
+
function isAggregateKey(key) {
|
|
4060
|
+
return key === "_count" || key === "_sum" || key === "_avg" || key === "_min" || key === "_max";
|
|
4061
|
+
}
|
|
4062
|
+
function assertHavingOp(op) {
|
|
4063
|
+
if (!HAVING_ALLOWED_OPS.has(op)) {
|
|
4064
|
+
throw new Error(
|
|
4065
|
+
`Unsupported HAVING operator '${op}'. Allowed: ${[...HAVING_ALLOWED_OPS].join(", ")}`
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
3920
4069
|
function aggExprForField(aggKey, field, alias, model) {
|
|
3921
4070
|
if (aggKey === "_count") {
|
|
3922
4071
|
return field === "_all" ? `COUNT(*)` : `COUNT(${col(alias, field, model)})`;
|
|
@@ -3950,32 +4099,9 @@ function normalizeLogicalValue2(operator, value) {
|
|
|
3950
4099
|
}
|
|
3951
4100
|
return out;
|
|
3952
4101
|
}
|
|
3953
|
-
if (isPlainObject(value))
|
|
3954
|
-
return [value];
|
|
3955
|
-
}
|
|
4102
|
+
if (isPlainObject(value)) return [value];
|
|
3956
4103
|
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
3957
4104
|
}
|
|
3958
|
-
function assertScalarField2(model, fieldName, ctx) {
|
|
3959
|
-
const m = getModelFieldMap(model);
|
|
3960
|
-
const field = m.get(fieldName);
|
|
3961
|
-
if (!field) {
|
|
3962
|
-
throw new Error(
|
|
3963
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}. Available fields: ${model.fields.map((f) => f.name).join(", ")}`
|
|
3964
|
-
);
|
|
3965
|
-
}
|
|
3966
|
-
if (field.isRelation) {
|
|
3967
|
-
throw new Error(`${ctx} does not support relation field '${fieldName}'`);
|
|
3968
|
-
}
|
|
3969
|
-
return { name: field.name, type: field.type };
|
|
3970
|
-
}
|
|
3971
|
-
function assertAggregateFieldType(aggKey, fieldType, fieldName, modelName) {
|
|
3972
|
-
const baseType = fieldType.replace(/\[\]|\?/g, "");
|
|
3973
|
-
if ((aggKey === "_sum" || aggKey === "_avg") && !NUMERIC_TYPES.has(baseType)) {
|
|
3974
|
-
throw new Error(
|
|
3975
|
-
`Cannot use ${aggKey} on non-numeric field '${fieldName}' (type: ${fieldType}) on model ${modelName}`
|
|
3976
|
-
);
|
|
3977
|
-
}
|
|
3978
|
-
}
|
|
3979
4105
|
function buildNullComparison(expr, op) {
|
|
3980
4106
|
if (op === Ops.EQUALS) return `${expr} ${SQL_TEMPLATES.IS_NULL}`;
|
|
3981
4107
|
if (op === Ops.NOT) return `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
|
|
@@ -4002,6 +4128,7 @@ function buildBinaryComparison(expr, op, val, params) {
|
|
|
4002
4128
|
return `${expr} ${sqlOp} ${placeholder}`;
|
|
4003
4129
|
}
|
|
4004
4130
|
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
4131
|
+
assertHavingOp(op);
|
|
4005
4132
|
if (val === null) return buildNullComparison(expr, op);
|
|
4006
4133
|
if (op === Ops.NOT && isPlainObject(val)) {
|
|
4007
4134
|
return buildNotComposite(
|
|
@@ -4018,12 +4145,6 @@ function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
|
4018
4145
|
}
|
|
4019
4146
|
return buildBinaryComparison(expr, op, val, params);
|
|
4020
4147
|
}
|
|
4021
|
-
function isLogicalKey(key) {
|
|
4022
|
-
return key === LogicalOps.AND || key === LogicalOps.OR || key === LogicalOps.NOT;
|
|
4023
|
-
}
|
|
4024
|
-
function isAggregateKey(key) {
|
|
4025
|
-
return key === "_count" || key === "_sum" || key === "_avg" || key === "_min" || key === "_max";
|
|
4026
|
-
}
|
|
4027
4148
|
function negateClauses(subClauses) {
|
|
4028
4149
|
if (subClauses.length === 1) return `${SQL_TEMPLATES.NOT} ${subClauses[0]}`;
|
|
4029
4150
|
return `${SQL_TEMPLATES.NOT} (${subClauses.join(SQL_SEPARATORS.CONDITION_AND)})`;
|
|
@@ -4032,16 +4153,75 @@ function combineLogical(key, subClauses) {
|
|
|
4032
4153
|
if (key === LogicalOps.NOT) return negateClauses(subClauses);
|
|
4033
4154
|
return subClauses.join(` ${key} `);
|
|
4034
4155
|
}
|
|
4156
|
+
function buildHavingNode(node, alias, params, dialect, model) {
|
|
4157
|
+
const clauses = [];
|
|
4158
|
+
const entries = Object.entries(node);
|
|
4159
|
+
for (const [key, value] of entries) {
|
|
4160
|
+
const built = buildHavingEntry(key, value, alias, params, dialect, model);
|
|
4161
|
+
for (const c of built) {
|
|
4162
|
+
if (c && c.trim().length > 0) clauses.push(c);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
return clauses.join(SQL_SEPARATORS.CONDITION_AND);
|
|
4166
|
+
}
|
|
4035
4167
|
function buildLogicalClause2(key, value, alias, params, dialect, model) {
|
|
4036
4168
|
const items = normalizeLogicalValue2(key, value);
|
|
4037
4169
|
const subClauses = [];
|
|
4038
4170
|
for (const it of items) {
|
|
4039
4171
|
const c = buildHavingNode(it, alias, params, dialect, model);
|
|
4040
|
-
if (c && c
|
|
4172
|
+
if (c && c.trim().length > 0) subClauses.push(`(${c})`);
|
|
4041
4173
|
}
|
|
4042
4174
|
if (subClauses.length === 0) return "";
|
|
4043
4175
|
return combineLogical(key, subClauses);
|
|
4044
4176
|
}
|
|
4177
|
+
function assertHavingAggTarget(aggKey, field, model) {
|
|
4178
|
+
if (field === "_all") {
|
|
4179
|
+
if (aggKey !== "_count")
|
|
4180
|
+
throw new Error(`HAVING '${aggKey}' does not support '_all'`);
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4184
|
+
assertNumericField(model, field, "HAVING");
|
|
4185
|
+
} else {
|
|
4186
|
+
assertScalarField(model, field, "HAVING");
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
4190
|
+
return buildComparisons(expr, filter, params, dialect, buildSimpleComparison);
|
|
4191
|
+
}
|
|
4192
|
+
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
4193
|
+
if (!isPlainObject(target)) {
|
|
4194
|
+
throw new Error(`HAVING '${aggKey}' must be an object`);
|
|
4195
|
+
}
|
|
4196
|
+
const out = [];
|
|
4197
|
+
for (const [field, filter] of Object.entries(target)) {
|
|
4198
|
+
assertHavingAggTarget(aggKey, field, model);
|
|
4199
|
+
if (!isPlainObject(filter) || Object.keys(filter).length === 0) continue;
|
|
4200
|
+
const expr = aggExprForField(aggKey, field, alias, model);
|
|
4201
|
+
out.push(...buildHavingOpsForExpr(expr, filter, params, dialect));
|
|
4202
|
+
}
|
|
4203
|
+
return out;
|
|
4204
|
+
}
|
|
4205
|
+
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
4206
|
+
if (!isPlainObject(target)) {
|
|
4207
|
+
throw new Error(`HAVING '${fieldName}' must be an object`);
|
|
4208
|
+
}
|
|
4209
|
+
assertScalarField(model, fieldName, "HAVING");
|
|
4210
|
+
const out = [];
|
|
4211
|
+
const obj = target;
|
|
4212
|
+
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
4213
|
+
for (const aggKey of keys) {
|
|
4214
|
+
const aggFilter = obj[aggKey];
|
|
4215
|
+
if (!isPlainObject(aggFilter)) continue;
|
|
4216
|
+
if (Object.keys(aggFilter).length === 0) continue;
|
|
4217
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4218
|
+
assertNumericField(model, fieldName, "HAVING");
|
|
4219
|
+
}
|
|
4220
|
+
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4221
|
+
out.push(...buildHavingOpsForExpr(expr, aggFilter, params, dialect));
|
|
4222
|
+
}
|
|
4223
|
+
return out;
|
|
4224
|
+
}
|
|
4045
4225
|
function buildHavingEntry(key, value, alias, params, dialect, model) {
|
|
4046
4226
|
if (isLogicalKey(key)) {
|
|
4047
4227
|
const logical = buildLogicalClause2(
|
|
@@ -4073,71 +4253,10 @@ function buildHavingEntry(key, value, alias, params, dialect, model) {
|
|
|
4073
4253
|
model
|
|
4074
4254
|
);
|
|
4075
4255
|
}
|
|
4076
|
-
function buildHavingNode(node, alias, params, dialect, model) {
|
|
4077
|
-
const clauses = [];
|
|
4078
|
-
for (const [key, value] of Object.entries(node)) {
|
|
4079
|
-
const built = buildHavingEntry(key, value, alias, params, dialect, model);
|
|
4080
|
-
for (const c of built) {
|
|
4081
|
-
if (c && c.trim().length > 0) clauses.push(c);
|
|
4082
|
-
}
|
|
4083
|
-
}
|
|
4084
|
-
return clauses.join(SQL_SEPARATORS.CONDITION_AND);
|
|
4085
|
-
}
|
|
4086
|
-
function assertHavingAggTarget(aggKey, field, model) {
|
|
4087
|
-
if (field === "_all") {
|
|
4088
|
-
if (aggKey !== "_count") {
|
|
4089
|
-
throw new Error(`HAVING '${aggKey}' does not support '_all'`);
|
|
4090
|
-
}
|
|
4091
|
-
return;
|
|
4092
|
-
}
|
|
4093
|
-
const f = assertScalarField2(model, field, "HAVING");
|
|
4094
|
-
assertAggregateFieldType(aggKey, f.type, f.name, model.name);
|
|
4095
|
-
}
|
|
4096
|
-
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
4097
|
-
const out = [];
|
|
4098
|
-
for (const [op, val] of Object.entries(filter)) {
|
|
4099
|
-
if (op === "mode") continue;
|
|
4100
|
-
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
4101
|
-
if (built && built.trim().length > 0) out.push(built);
|
|
4102
|
-
}
|
|
4103
|
-
return out;
|
|
4104
|
-
}
|
|
4105
|
-
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
4106
|
-
if (!isPlainObject(target)) return [];
|
|
4107
|
-
const out = [];
|
|
4108
|
-
for (const [field, filter] of Object.entries(target)) {
|
|
4109
|
-
assertHavingAggTarget(aggKey, field, model);
|
|
4110
|
-
if (!isPlainObject(filter) || Object.keys(filter).length === 0) continue;
|
|
4111
|
-
const expr = aggExprForField(aggKey, field, alias, model);
|
|
4112
|
-
out.push(...buildHavingOpsForExpr(expr, filter, params, dialect));
|
|
4113
|
-
}
|
|
4114
|
-
return out;
|
|
4115
|
-
}
|
|
4116
|
-
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
4117
|
-
if (!isPlainObject(target)) return [];
|
|
4118
|
-
const field = assertScalarField2(model, fieldName, "HAVING");
|
|
4119
|
-
const out = [];
|
|
4120
|
-
const obj = target;
|
|
4121
|
-
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
4122
|
-
for (const aggKey of keys) {
|
|
4123
|
-
const aggFilter = obj[aggKey];
|
|
4124
|
-
if (!isPlainObject(aggFilter)) continue;
|
|
4125
|
-
assertAggregateFieldType(aggKey, field.type, field.name, model.name);
|
|
4126
|
-
const entries = Object.entries(aggFilter);
|
|
4127
|
-
if (entries.length === 0) continue;
|
|
4128
|
-
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4129
|
-
for (const [op, val] of entries) {
|
|
4130
|
-
if (op === "mode") continue;
|
|
4131
|
-
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
4132
|
-
if (built && built.trim().length > 0) out.push(built);
|
|
4133
|
-
}
|
|
4134
|
-
}
|
|
4135
|
-
return out;
|
|
4136
|
-
}
|
|
4137
4256
|
function buildHavingClause(having, alias, params, model, dialect) {
|
|
4138
4257
|
if (!isNotNullish(having)) return "";
|
|
4139
4258
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
4140
|
-
if (!isPlainObject(having))
|
|
4259
|
+
if (!isPlainObject(having)) throw new Error("having must be an object");
|
|
4141
4260
|
return buildHavingNode(having, alias, params, d, model);
|
|
4142
4261
|
}
|
|
4143
4262
|
function normalizeCountArg(v) {
|
|
@@ -4151,26 +4270,13 @@ function pushCountAllField(fields) {
|
|
|
4151
4270
|
`${SQL_TEMPLATES.COUNT_ALL} ${SQL_TEMPLATES.AS} ${quote("_count._all")}`
|
|
4152
4271
|
);
|
|
4153
4272
|
}
|
|
4154
|
-
function assertCountableScalarField(fieldMap, model, fieldName) {
|
|
4155
|
-
const field = fieldMap.get(fieldName);
|
|
4156
|
-
if (!field) {
|
|
4157
|
-
throw new Error(
|
|
4158
|
-
`Field '${fieldName}' does not exist on model ${model.name}`
|
|
4159
|
-
);
|
|
4160
|
-
}
|
|
4161
|
-
if (field.isRelation) {
|
|
4162
|
-
throw new Error(
|
|
4163
|
-
`Cannot use _count on relation field '${fieldName}' on model ${model.name}`
|
|
4164
|
-
);
|
|
4165
|
-
}
|
|
4166
|
-
}
|
|
4167
4273
|
function pushCountField(fields, alias, fieldName, model) {
|
|
4168
4274
|
const outAlias = `_count.${fieldName}`;
|
|
4169
4275
|
fields.push(
|
|
4170
4276
|
`COUNT(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4171
4277
|
);
|
|
4172
4278
|
}
|
|
4173
|
-
function addCountFields(fields, countArg, alias, model
|
|
4279
|
+
function addCountFields(fields, countArg, alias, model) {
|
|
4174
4280
|
if (!isNotNullish(countArg)) return;
|
|
4175
4281
|
if (countArg === true) {
|
|
4176
4282
|
pushCountAllField(fields);
|
|
@@ -4184,7 +4290,7 @@ function addCountFields(fields, countArg, alias, model, fieldMap) {
|
|
|
4184
4290
|
([f, v]) => f !== "_all" && isTruthySelection(v)
|
|
4185
4291
|
);
|
|
4186
4292
|
for (const [f] of selected) {
|
|
4187
|
-
|
|
4293
|
+
assertScalarField(model, f, "_count");
|
|
4188
4294
|
pushCountField(fields, alias, f, model);
|
|
4189
4295
|
}
|
|
4190
4296
|
}
|
|
@@ -4192,19 +4298,12 @@ function getAggregateSelectionObject(args, agg) {
|
|
|
4192
4298
|
const obj = args[agg];
|
|
4193
4299
|
return isPlainObject(obj) ? obj : void 0;
|
|
4194
4300
|
}
|
|
4195
|
-
function assertAggregatableScalarField(
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
);
|
|
4201
|
-
}
|
|
4202
|
-
if (field.isRelation) {
|
|
4203
|
-
throw new Error(
|
|
4204
|
-
`Cannot use ${agg} on relation field '${fieldName}' on model ${model.name}`
|
|
4205
|
-
);
|
|
4301
|
+
function assertAggregatableScalarField(model, agg, fieldName) {
|
|
4302
|
+
if (agg === "_sum" || agg === "_avg") {
|
|
4303
|
+
assertNumericField(model, fieldName, agg);
|
|
4304
|
+
} else {
|
|
4305
|
+
assertScalarField(model, fieldName, agg);
|
|
4206
4306
|
}
|
|
4207
|
-
return field;
|
|
4208
4307
|
}
|
|
4209
4308
|
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
4210
4309
|
const outAlias = `${agg}.${fieldName}`;
|
|
@@ -4212,7 +4311,7 @@ function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
|
4212
4311
|
`${aggFn}(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4213
4312
|
);
|
|
4214
4313
|
}
|
|
4215
|
-
function addAggregateFields(fields, args, alias, model
|
|
4314
|
+
function addAggregateFields(fields, args, alias, model) {
|
|
4216
4315
|
for (const [agg, aggFn] of AGGREGATES) {
|
|
4217
4316
|
const obj = getAggregateSelectionObject(args, agg);
|
|
4218
4317
|
if (!obj) continue;
|
|
@@ -4220,23 +4319,16 @@ function addAggregateFields(fields, args, alias, model, fieldMap) {
|
|
|
4220
4319
|
if (fieldName === "_all")
|
|
4221
4320
|
throw new Error(`'${agg}' does not support '_all'`);
|
|
4222
4321
|
if (!isTruthySelection(selection)) continue;
|
|
4223
|
-
|
|
4224
|
-
fieldMap,
|
|
4225
|
-
model,
|
|
4226
|
-
agg,
|
|
4227
|
-
fieldName
|
|
4228
|
-
);
|
|
4229
|
-
assertAggregateFieldType(agg, field.type, fieldName, model.name);
|
|
4322
|
+
assertAggregatableScalarField(model, agg, fieldName);
|
|
4230
4323
|
pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model);
|
|
4231
4324
|
}
|
|
4232
4325
|
}
|
|
4233
4326
|
}
|
|
4234
4327
|
function buildAggregateFields(args, alias, model) {
|
|
4235
4328
|
const fields = [];
|
|
4236
|
-
const fieldMap = getModelFieldMap(model);
|
|
4237
4329
|
const countArg = normalizeCountArg(args._count);
|
|
4238
|
-
addCountFields(fields, countArg, alias, model
|
|
4239
|
-
addAggregateFields(fields, args, alias, model
|
|
4330
|
+
addCountFields(fields, countArg, alias, model);
|
|
4331
|
+
addAggregateFields(fields, args, alias, model);
|
|
4240
4332
|
return fields;
|
|
4241
4333
|
}
|
|
4242
4334
|
function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
@@ -4260,7 +4352,7 @@ function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
|
4260
4352
|
validateParamConsistency(sql, whereResult.params);
|
|
4261
4353
|
return Object.freeze({
|
|
4262
4354
|
sql,
|
|
4263
|
-
params: Object.freeze(
|
|
4355
|
+
params: Object.freeze([...whereResult.params]),
|
|
4264
4356
|
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4265
4357
|
});
|
|
4266
4358
|
}
|
|
@@ -4273,32 +4365,22 @@ function assertGroupByBy(args, model) {
|
|
|
4273
4365
|
if (bySet.size !== byFields.length) {
|
|
4274
4366
|
throw new Error("buildGroupBySql: by must not contain duplicates");
|
|
4275
4367
|
}
|
|
4276
|
-
const modelFieldMap = getModelFieldMap(model);
|
|
4277
4368
|
for (const f of byFields) {
|
|
4278
|
-
|
|
4279
|
-
if (!field) {
|
|
4280
|
-
throw new Error(
|
|
4281
|
-
`groupBy.by references unknown field '${f}' on model ${model.name}`
|
|
4282
|
-
);
|
|
4283
|
-
}
|
|
4284
|
-
if (field.isRelation) {
|
|
4285
|
-
throw new Error(
|
|
4286
|
-
`groupBy.by does not support relation field '${f}' on model ${model.name}`
|
|
4287
|
-
);
|
|
4288
|
-
}
|
|
4369
|
+
assertScalarField(model, f, "groupBy.by");
|
|
4289
4370
|
}
|
|
4290
4371
|
return byFields;
|
|
4291
4372
|
}
|
|
4292
4373
|
function buildGroupBySelectParts(args, alias, model, byFields) {
|
|
4293
4374
|
const groupCols = byFields.map((f) => col(alias, f, model));
|
|
4375
|
+
const selectCols = byFields.map((f) => colWithAlias(alias, f, model));
|
|
4294
4376
|
const groupFields = groupCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4295
4377
|
const aggFields = buildAggregateFields(args, alias, model);
|
|
4296
|
-
const selectFields = isNonEmptyArray(aggFields) ?
|
|
4378
|
+
const selectFields = isNonEmptyArray(aggFields) ? selectCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : selectCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4297
4379
|
return { groupCols, groupFields, selectFields };
|
|
4298
4380
|
}
|
|
4299
4381
|
function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
4300
4382
|
if (!isNotNullish(args.having)) return "";
|
|
4301
|
-
if (!isPlainObject(args.having))
|
|
4383
|
+
if (!isPlainObject(args.having)) throw new Error("having must be an object");
|
|
4302
4384
|
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4303
4385
|
if (!h || h.trim().length === 0) return "";
|
|
4304
4386
|
return `${SQL_TEMPLATES.HAVING} ${h}`;
|
|
@@ -4331,64 +4413,60 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
|
4331
4413
|
const snapshot = params.snapshot();
|
|
4332
4414
|
validateSelectQuery(sql);
|
|
4333
4415
|
validateParamConsistency(sql, [...whereResult.params, ...snapshot.params]);
|
|
4334
|
-
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4335
4416
|
return Object.freeze({
|
|
4336
4417
|
sql,
|
|
4337
|
-
params: Object.freeze(
|
|
4418
|
+
params: Object.freeze([...whereResult.params, ...snapshot.params]),
|
|
4338
4419
|
paramMappings: Object.freeze([
|
|
4339
4420
|
...whereResult.paramMappings,
|
|
4340
4421
|
...snapshot.mappings
|
|
4341
4422
|
])
|
|
4342
4423
|
});
|
|
4343
4424
|
}
|
|
4344
|
-
function buildCountSql(whereResult, tableName, alias, skip,
|
|
4425
|
+
function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
4345
4426
|
assertSafeAlias(alias);
|
|
4346
4427
|
assertSafeTableRef(tableName);
|
|
4347
|
-
|
|
4428
|
+
if (skip !== void 0 && skip !== null) {
|
|
4429
|
+
if (schemaParser.isDynamicParameter(skip)) {
|
|
4430
|
+
throw new Error(
|
|
4431
|
+
"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."
|
|
4432
|
+
);
|
|
4433
|
+
}
|
|
4434
|
+
if (typeof skip === "string") {
|
|
4435
|
+
const s = skip.trim();
|
|
4436
|
+
if (s.length > 0) {
|
|
4437
|
+
const n = Number(s);
|
|
4438
|
+
if (Number.isFinite(n) && Number.isInteger(n) && n > 0) {
|
|
4439
|
+
throw new Error(
|
|
4440
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4441
|
+
);
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
if (typeof skip === "number" && Number.isFinite(skip) && Number.isInteger(skip) && skip > 0) {
|
|
4446
|
+
throw new Error(
|
|
4447
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4448
|
+
);
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4348
4451
|
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4349
|
-
const params = createParamStore(whereResult.nextParamIndex);
|
|
4350
|
-
const baseSubSelect = [
|
|
4351
|
-
SQL_TEMPLATES.SELECT,
|
|
4352
|
-
"1",
|
|
4353
|
-
SQL_TEMPLATES.FROM,
|
|
4354
|
-
tableName,
|
|
4355
|
-
alias,
|
|
4356
|
-
whereClause
|
|
4357
|
-
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4358
|
-
const normalizedSkip = normalizeSkipLike(skip);
|
|
4359
|
-
const subSelect = applyCountSkip(baseSubSelect, normalizedSkip, params, d);
|
|
4360
4452
|
const sql = [
|
|
4361
4453
|
SQL_TEMPLATES.SELECT,
|
|
4362
4454
|
SQL_TEMPLATES.COUNT_ALL,
|
|
4363
4455
|
SQL_TEMPLATES.AS,
|
|
4364
4456
|
quote("_count._all"),
|
|
4365
4457
|
SQL_TEMPLATES.FROM,
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4458
|
+
tableName,
|
|
4459
|
+
alias,
|
|
4460
|
+
whereClause
|
|
4369
4461
|
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4370
4462
|
validateSelectQuery(sql);
|
|
4371
|
-
|
|
4372
|
-
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4373
|
-
validateParamConsistency(sql, mergedParams);
|
|
4463
|
+
validateParamConsistency(sql, whereResult.params);
|
|
4374
4464
|
return Object.freeze({
|
|
4375
4465
|
sql,
|
|
4376
|
-
params: Object.freeze(
|
|
4377
|
-
paramMappings: Object.freeze([
|
|
4378
|
-
...whereResult.paramMappings,
|
|
4379
|
-
...snapshot.mappings
|
|
4380
|
-
])
|
|
4466
|
+
params: Object.freeze([...whereResult.params]),
|
|
4467
|
+
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4381
4468
|
});
|
|
4382
4469
|
}
|
|
4383
|
-
function applyCountSkip(subSelect, normalizedSkip, params, dialect) {
|
|
4384
|
-
const shouldApply = schemaParser.isDynamicParameter(normalizedSkip) || typeof normalizedSkip === "number" && normalizedSkip > 0;
|
|
4385
|
-
if (!shouldApply) return subSelect;
|
|
4386
|
-
const placeholder = addAutoScoped(params, normalizedSkip, "count.skip");
|
|
4387
|
-
if (dialect === "sqlite") {
|
|
4388
|
-
return `${subSelect} ${SQL_TEMPLATES.LIMIT} -1 ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4389
|
-
}
|
|
4390
|
-
return `${subSelect} ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4391
|
-
}
|
|
4392
4470
|
function safeAlias(input) {
|
|
4393
4471
|
const raw = String(input).toLowerCase();
|
|
4394
4472
|
const cleaned = raw.replace(/[^a-z0-9_]/g, "_");
|
|
@@ -4538,8 +4616,12 @@ function buildAndNormalizeSql(args) {
|
|
|
4538
4616
|
);
|
|
4539
4617
|
}
|
|
4540
4618
|
function finalizeDirective(args) {
|
|
4541
|
-
const { directive, normalizedSql, normalizedMappings } = args;
|
|
4542
|
-
|
|
4619
|
+
const { directive, normalizedSql, normalizedMappings, dialect } = args;
|
|
4620
|
+
const params = normalizedMappings.map((m) => {
|
|
4621
|
+
var _a;
|
|
4622
|
+
return (_a = m.value) != null ? _a : void 0;
|
|
4623
|
+
});
|
|
4624
|
+
validateParamConsistencyByDialect(normalizedSql, params, dialect);
|
|
4543
4625
|
const { staticParams, dynamicKeys } = buildParamsFromMappings(normalizedMappings);
|
|
4544
4626
|
return {
|
|
4545
4627
|
method: directive.method,
|
|
@@ -4576,7 +4658,8 @@ function generateSQL(directive) {
|
|
|
4576
4658
|
return finalizeDirective({
|
|
4577
4659
|
directive,
|
|
4578
4660
|
normalizedSql: normalized.sql,
|
|
4579
|
-
normalizedMappings: normalized.paramMappings
|
|
4661
|
+
normalizedMappings: normalized.paramMappings,
|
|
4662
|
+
dialect
|
|
4580
4663
|
});
|
|
4581
4664
|
}
|
|
4582
4665
|
function generateSQL2(directive) {
|
|
@@ -4684,18 +4767,57 @@ function generateCode(models, queries, dialect, datamodel) {
|
|
|
4684
4767
|
return `// Generated by @prisma-sql/generator - DO NOT EDIT
|
|
4685
4768
|
import { buildSQL, transformQueryResults, type PrismaMethod, type Model } from 'prisma-sql'
|
|
4686
4769
|
|
|
4687
|
-
|
|
4770
|
+
/**
|
|
4771
|
+
* Normalize values for SQL params.
|
|
4772
|
+
* Synced from src/utils/normalize-value.ts
|
|
4773
|
+
*/
|
|
4774
|
+
function normalizeValue(value: unknown, seen = new WeakSet<object>(), depth = 0): unknown {
|
|
4775
|
+
const MAX_DEPTH = 20
|
|
4776
|
+
if (depth > MAX_DEPTH) {
|
|
4777
|
+
throw new Error(\`Max normalization depth exceeded (\${MAX_DEPTH} levels)\`)
|
|
4778
|
+
}
|
|
4688
4779
|
if (value instanceof Date) {
|
|
4780
|
+
const t = value.getTime()
|
|
4781
|
+
if (!Number.isFinite(t)) {
|
|
4782
|
+
throw new Error('Invalid Date value in SQL params')
|
|
4783
|
+
}
|
|
4689
4784
|
return value.toISOString()
|
|
4690
4785
|
}
|
|
4691
|
-
|
|
4786
|
+
if (typeof value === 'bigint') {
|
|
4787
|
+
return value.toString()
|
|
4788
|
+
}
|
|
4692
4789
|
if (Array.isArray(value)) {
|
|
4693
|
-
|
|
4790
|
+
const arrRef = value as unknown as object
|
|
4791
|
+
if (seen.has(arrRef)) {
|
|
4792
|
+
throw new Error('Circular reference in SQL params')
|
|
4793
|
+
}
|
|
4794
|
+
seen.add(arrRef)
|
|
4795
|
+
const out = value.map((v) => normalizeValue(v, seen, depth + 1))
|
|
4796
|
+
seen.delete(arrRef)
|
|
4797
|
+
return out
|
|
4798
|
+
}
|
|
4799
|
+
if (value && typeof value === 'object') {
|
|
4800
|
+
if (value instanceof Uint8Array) return value
|
|
4801
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return value
|
|
4802
|
+
const proto = Object.getPrototypeOf(value)
|
|
4803
|
+
const isPlain = proto === Object.prototype || proto === null
|
|
4804
|
+
if (!isPlain) return value
|
|
4805
|
+
const obj = value as Record<string, unknown>
|
|
4806
|
+
if (seen.has(obj)) {
|
|
4807
|
+
throw new Error('Circular reference in SQL params')
|
|
4808
|
+
}
|
|
4809
|
+
seen.add(obj)
|
|
4810
|
+
const out: Record<string, unknown> = {}
|
|
4811
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
4812
|
+
out[k] = normalizeValue(v, seen, depth + 1)
|
|
4813
|
+
}
|
|
4814
|
+
seen.delete(obj)
|
|
4815
|
+
return out
|
|
4694
4816
|
}
|
|
4695
|
-
|
|
4696
4817
|
return value
|
|
4697
4818
|
}
|
|
4698
4819
|
|
|
4820
|
+
|
|
4699
4821
|
export const MODELS: Model[] = ${JSON.stringify(cleanModels, null, 2)}
|
|
4700
4822
|
|
|
4701
4823
|
const ENUM_MAPPINGS: Record<string, Record<string, string>> = ${JSON.stringify(mappings, null, 2)}
|
|
@@ -4835,34 +4957,39 @@ function normalizeQuery(args: any): string {
|
|
|
4835
4957
|
|
|
4836
4958
|
function extractDynamicParams(args: any, dynamicKeys: string[]): unknown[] {
|
|
4837
4959
|
const params: unknown[] = []
|
|
4838
|
-
|
|
4960
|
+
|
|
4839
4961
|
for (const key of dynamicKeys) {
|
|
4840
4962
|
const parts = key.split(':')
|
|
4841
4963
|
const lookupKey = parts.length === 2 ? parts[1] : key
|
|
4842
|
-
|
|
4843
|
-
|
|
4964
|
+
|
|
4965
|
+
const value =
|
|
4966
|
+
lookupKey.includes('.') ? getByPath(args, lookupKey) : args?.[lookupKey]
|
|
4967
|
+
|
|
4844
4968
|
if (value === undefined) {
|
|
4845
4969
|
throw new Error(\`Missing required parameter: \${key}\`)
|
|
4846
4970
|
}
|
|
4847
|
-
|
|
4971
|
+
|
|
4848
4972
|
params.push(normalizeValue(value))
|
|
4849
4973
|
}
|
|
4850
|
-
|
|
4974
|
+
|
|
4851
4975
|
return params
|
|
4852
4976
|
}
|
|
4853
4977
|
|
|
4978
|
+
|
|
4854
4979
|
async function executeQuery(client: any, sql: string, params: unknown[]): Promise<unknown[]> {
|
|
4980
|
+
const normalizedParams = normalizeParams(params)
|
|
4981
|
+
|
|
4855
4982
|
if (DIALECT === 'postgres') {
|
|
4856
|
-
return await client.unsafe(sql,
|
|
4983
|
+
return await client.unsafe(sql, normalizedParams)
|
|
4857
4984
|
}
|
|
4858
|
-
|
|
4985
|
+
|
|
4859
4986
|
const stmt = client.prepare(sql)
|
|
4860
|
-
|
|
4987
|
+
|
|
4861
4988
|
if (sql.toUpperCase().includes('COUNT(*) AS')) {
|
|
4862
|
-
return [stmt.get(...
|
|
4989
|
+
return [stmt.get(...normalizedParams)]
|
|
4863
4990
|
}
|
|
4864
|
-
|
|
4865
|
-
return stmt.all(...
|
|
4991
|
+
|
|
4992
|
+
return stmt.all(...normalizedParams)
|
|
4866
4993
|
}
|
|
4867
4994
|
|
|
4868
4995
|
export function speedExtension(config: {
|
|
@@ -4907,18 +5034,21 @@ export function speedExtension(config: {
|
|
|
4907
5034
|
|
|
4908
5035
|
if (prebakedQuery) {
|
|
4909
5036
|
sql = prebakedQuery.sql
|
|
4910
|
-
params = [
|
|
5037
|
+
params = normalizeParams([
|
|
5038
|
+
...prebakedQuery.params,
|
|
5039
|
+
...extractDynamicParams(transformedArgs, prebakedQuery.dynamicKeys),
|
|
5040
|
+
])
|
|
4911
5041
|
prebaked = true
|
|
4912
5042
|
} else {
|
|
4913
5043
|
const model = MODELS.find((m) => m.name === modelName)
|
|
4914
|
-
|
|
5044
|
+
|
|
4915
5045
|
if (!model) {
|
|
4916
5046
|
return this.$parent[modelName][method](args)
|
|
4917
5047
|
}
|
|
4918
5048
|
|
|
4919
5049
|
const result = buildSQL(model, MODELS, method, transformedArgs, DIALECT)
|
|
4920
5050
|
sql = result.sql
|
|
4921
|
-
params = result.params
|
|
5051
|
+
params = normalizeParams(result.params as unknown[])
|
|
4922
5052
|
}
|
|
4923
5053
|
|
|
4924
5054
|
if (debug) {
|