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