prisma-sql 1.44.0 → 1.45.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 +808 -562
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +808 -562
- package/dist/generator.js.map +1 -1
- package/dist/index.cjs +765 -560
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +765 -560
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -49,20 +49,13 @@ var SQL_SEPARATORS = Object.freeze({
|
|
|
49
49
|
CONDITION_OR: " OR ",
|
|
50
50
|
ORDER_BY: ", "
|
|
51
51
|
});
|
|
52
|
-
var
|
|
52
|
+
var ALIAS_FORBIDDEN_KEYWORDS = /* @__PURE__ */ new Set([
|
|
53
53
|
"select",
|
|
54
54
|
"from",
|
|
55
55
|
"where",
|
|
56
|
-
"
|
|
57
|
-
"or",
|
|
58
|
-
"not",
|
|
59
|
-
"in",
|
|
60
|
-
"like",
|
|
61
|
-
"between",
|
|
56
|
+
"having",
|
|
62
57
|
"order",
|
|
63
|
-
"by",
|
|
64
58
|
"group",
|
|
65
|
-
"having",
|
|
66
59
|
"limit",
|
|
67
60
|
"offset",
|
|
68
61
|
"join",
|
|
@@ -70,14 +63,42 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
70
63
|
"left",
|
|
71
64
|
"right",
|
|
72
65
|
"outer",
|
|
73
|
-
"
|
|
66
|
+
"cross",
|
|
67
|
+
"full",
|
|
68
|
+
"and",
|
|
69
|
+
"or",
|
|
70
|
+
"not",
|
|
71
|
+
"by",
|
|
74
72
|
"as",
|
|
73
|
+
"on",
|
|
74
|
+
"union",
|
|
75
|
+
"intersect",
|
|
76
|
+
"except",
|
|
77
|
+
"case",
|
|
78
|
+
"when",
|
|
79
|
+
"then",
|
|
80
|
+
"else",
|
|
81
|
+
"end"
|
|
82
|
+
]);
|
|
83
|
+
var SQL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
84
|
+
...ALIAS_FORBIDDEN_KEYWORDS,
|
|
85
|
+
"user",
|
|
86
|
+
"users",
|
|
75
87
|
"table",
|
|
76
88
|
"column",
|
|
77
89
|
"index",
|
|
78
|
-
"user",
|
|
79
|
-
"users",
|
|
80
90
|
"values",
|
|
91
|
+
"in",
|
|
92
|
+
"like",
|
|
93
|
+
"between",
|
|
94
|
+
"is",
|
|
95
|
+
"exists",
|
|
96
|
+
"null",
|
|
97
|
+
"true",
|
|
98
|
+
"false",
|
|
99
|
+
"all",
|
|
100
|
+
"any",
|
|
101
|
+
"some",
|
|
81
102
|
"update",
|
|
82
103
|
"insert",
|
|
83
104
|
"delete",
|
|
@@ -88,25 +109,9 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
88
109
|
"grant",
|
|
89
110
|
"revoke",
|
|
90
111
|
"exec",
|
|
91
|
-
"execute"
|
|
92
|
-
"union",
|
|
93
|
-
"intersect",
|
|
94
|
-
"except",
|
|
95
|
-
"case",
|
|
96
|
-
"when",
|
|
97
|
-
"then",
|
|
98
|
-
"else",
|
|
99
|
-
"end",
|
|
100
|
-
"null",
|
|
101
|
-
"true",
|
|
102
|
-
"false",
|
|
103
|
-
"is",
|
|
104
|
-
"exists",
|
|
105
|
-
"all",
|
|
106
|
-
"any",
|
|
107
|
-
"some"
|
|
112
|
+
"execute"
|
|
108
113
|
]);
|
|
109
|
-
var
|
|
114
|
+
var SQL_RESERVED_WORDS = SQL_KEYWORDS;
|
|
110
115
|
var DEFAULT_WHERE_CLAUSE = "1=1";
|
|
111
116
|
var SPECIAL_FIELDS = Object.freeze({
|
|
112
117
|
ID: "id"
|
|
@@ -185,12 +190,48 @@ var LIMITS = Object.freeze({
|
|
|
185
190
|
});
|
|
186
191
|
|
|
187
192
|
// src/utils/normalize-value.ts
|
|
188
|
-
|
|
193
|
+
var MAX_DEPTH = 20;
|
|
194
|
+
function normalizeValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
195
|
+
if (depth > MAX_DEPTH) {
|
|
196
|
+
throw new Error(`Max normalization depth exceeded (${MAX_DEPTH} levels)`);
|
|
197
|
+
}
|
|
189
198
|
if (value instanceof Date) {
|
|
199
|
+
const t = value.getTime();
|
|
200
|
+
if (!Number.isFinite(t)) {
|
|
201
|
+
throw new Error("Invalid Date value in SQL params");
|
|
202
|
+
}
|
|
190
203
|
return value.toISOString();
|
|
191
204
|
}
|
|
205
|
+
if (typeof value === "bigint") {
|
|
206
|
+
return value.toString();
|
|
207
|
+
}
|
|
192
208
|
if (Array.isArray(value)) {
|
|
193
|
-
|
|
209
|
+
const arrRef = value;
|
|
210
|
+
if (seen.has(arrRef)) {
|
|
211
|
+
throw new Error("Circular reference in SQL params");
|
|
212
|
+
}
|
|
213
|
+
seen.add(arrRef);
|
|
214
|
+
const out = value.map((v) => normalizeValue(v, seen, depth + 1));
|
|
215
|
+
seen.delete(arrRef);
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
if (value && typeof value === "object") {
|
|
219
|
+
if (value instanceof Uint8Array) return value;
|
|
220
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) return value;
|
|
221
|
+
const proto = Object.getPrototypeOf(value);
|
|
222
|
+
const isPlain = proto === Object.prototype || proto === null;
|
|
223
|
+
if (!isPlain) return value;
|
|
224
|
+
const obj = value;
|
|
225
|
+
if (seen.has(obj)) {
|
|
226
|
+
throw new Error("Circular reference in SQL params");
|
|
227
|
+
}
|
|
228
|
+
seen.add(obj);
|
|
229
|
+
const out = {};
|
|
230
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
231
|
+
out[k] = normalizeValue(v, seen, depth + 1);
|
|
232
|
+
}
|
|
233
|
+
seen.delete(obj);
|
|
234
|
+
return out;
|
|
194
235
|
}
|
|
195
236
|
return value;
|
|
196
237
|
}
|
|
@@ -371,7 +412,7 @@ function prepareArrayParam(value, dialect) {
|
|
|
371
412
|
throw new Error("prepareArrayParam requires array value");
|
|
372
413
|
}
|
|
373
414
|
if (dialect === "postgres") {
|
|
374
|
-
return value.map(normalizeValue);
|
|
415
|
+
return value.map((v) => normalizeValue(v));
|
|
375
416
|
}
|
|
376
417
|
return JSON.stringify(value);
|
|
377
418
|
}
|
|
@@ -443,36 +484,46 @@ function createError(message, ctx, code = "VALIDATION_ERROR") {
|
|
|
443
484
|
}
|
|
444
485
|
|
|
445
486
|
// src/builder/shared/model-field-cache.ts
|
|
446
|
-
var
|
|
447
|
-
|
|
487
|
+
var MODEL_CACHE = /* @__PURE__ */ new WeakMap();
|
|
488
|
+
function ensureFullCache(model) {
|
|
489
|
+
let cache = MODEL_CACHE.get(model);
|
|
490
|
+
if (!cache) {
|
|
491
|
+
const fieldInfo = /* @__PURE__ */ new Map();
|
|
492
|
+
const scalarFields = /* @__PURE__ */ new Set();
|
|
493
|
+
const relationFields = /* @__PURE__ */ new Set();
|
|
494
|
+
const columnMap = /* @__PURE__ */ new Map();
|
|
495
|
+
for (const f of model.fields) {
|
|
496
|
+
const info = {
|
|
497
|
+
name: f.name,
|
|
498
|
+
dbName: f.dbName || f.name,
|
|
499
|
+
type: f.type,
|
|
500
|
+
isRelation: !!f.isRelation,
|
|
501
|
+
isRequired: !!f.isRequired
|
|
502
|
+
};
|
|
503
|
+
fieldInfo.set(f.name, info);
|
|
504
|
+
if (info.isRelation) {
|
|
505
|
+
relationFields.add(f.name);
|
|
506
|
+
} else {
|
|
507
|
+
scalarFields.add(f.name);
|
|
508
|
+
columnMap.set(f.name, info.dbName);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
cache = { fieldInfo, scalarFields, relationFields, columnMap };
|
|
512
|
+
MODEL_CACHE.set(model, cache);
|
|
513
|
+
}
|
|
514
|
+
return cache;
|
|
515
|
+
}
|
|
516
|
+
function getFieldInfo(model, fieldName) {
|
|
517
|
+
return ensureFullCache(model).fieldInfo.get(fieldName);
|
|
518
|
+
}
|
|
448
519
|
function getScalarFieldSet(model) {
|
|
449
|
-
|
|
450
|
-
if (cached) return cached;
|
|
451
|
-
const s = /* @__PURE__ */ new Set();
|
|
452
|
-
for (const f of model.fields) if (!f.isRelation) s.add(f.name);
|
|
453
|
-
SCALAR_SET_CACHE.set(model, s);
|
|
454
|
-
return s;
|
|
520
|
+
return ensureFullCache(model).scalarFields;
|
|
455
521
|
}
|
|
456
522
|
function getRelationFieldSet(model) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const s = /* @__PURE__ */ new Set();
|
|
460
|
-
for (const f of model.fields) if (f.isRelation) s.add(f.name);
|
|
461
|
-
RELATION_SET_CACHE.set(model, s);
|
|
462
|
-
return s;
|
|
463
|
-
}
|
|
464
|
-
var COLUMN_MAP_CACHE = /* @__PURE__ */ new WeakMap();
|
|
523
|
+
return ensureFullCache(model).relationFields;
|
|
524
|
+
}
|
|
465
525
|
function getColumnMap(model) {
|
|
466
|
-
|
|
467
|
-
if (cached) return cached;
|
|
468
|
-
const map = /* @__PURE__ */ new Map();
|
|
469
|
-
for (const f of model.fields) {
|
|
470
|
-
if (!f.isRelation) {
|
|
471
|
-
map.set(f.name, f.dbName || f.name);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
COLUMN_MAP_CACHE.set(model, map);
|
|
475
|
-
return map;
|
|
526
|
+
return ensureFullCache(model).columnMap;
|
|
476
527
|
}
|
|
477
528
|
|
|
478
529
|
// src/builder/shared/validators/sql-validators.ts
|
|
@@ -532,7 +583,7 @@ function scanDollarPlaceholders(sql, markUpTo) {
|
|
|
532
583
|
}
|
|
533
584
|
return { count, min, max, seen };
|
|
534
585
|
}
|
|
535
|
-
function
|
|
586
|
+
function assertNoGapsDollar(scan, rangeMin, rangeMax, sql) {
|
|
536
587
|
for (let k = rangeMin; k <= rangeMax; k++) {
|
|
537
588
|
if (scan.seen[k] !== 1) {
|
|
538
589
|
throw new Error(
|
|
@@ -560,7 +611,7 @@ function validateParamConsistency(sql, params) {
|
|
|
560
611
|
`CRITICAL: Parameter mismatch - SQL max placeholder is $${scan.max} but ${paramLen} params provided. This will cause SQL execution to fail. SQL: ${sqlPreview(sql)}`
|
|
561
612
|
);
|
|
562
613
|
}
|
|
563
|
-
|
|
614
|
+
assertNoGapsDollar(scan, 1, scan.max, sql);
|
|
564
615
|
}
|
|
565
616
|
function needsQuoting(id) {
|
|
566
617
|
if (!isNonEmptyString(id)) return true;
|
|
@@ -578,14 +629,11 @@ function validateParamConsistencyFragment(sql, params) {
|
|
|
578
629
|
`CRITICAL: Parameter mismatch - SQL references $${scan.max} but only ${paramLen} params provided. SQL: ${sqlPreview(sql)}`
|
|
579
630
|
);
|
|
580
631
|
}
|
|
581
|
-
|
|
632
|
+
assertNoGapsDollar(scan, scan.min, scan.max, sql);
|
|
582
633
|
}
|
|
583
634
|
function assertOrThrow(condition, message) {
|
|
584
635
|
if (!condition) throw new Error(message);
|
|
585
636
|
}
|
|
586
|
-
function dialectPlaceholderPrefix(dialect) {
|
|
587
|
-
return dialect === "sqlite" ? "?" : "$";
|
|
588
|
-
}
|
|
589
637
|
function parseSqlitePlaceholderIndices(sql) {
|
|
590
638
|
const re = /\?(?:(\d+))?/g;
|
|
591
639
|
const indices = [];
|
|
@@ -605,112 +653,70 @@ function parseSqlitePlaceholderIndices(sql) {
|
|
|
605
653
|
}
|
|
606
654
|
return { indices, sawNumbered, sawAnonymous };
|
|
607
655
|
}
|
|
608
|
-
function parseDollarPlaceholderIndices(sql) {
|
|
609
|
-
const re = /\$(\d+)/g;
|
|
610
|
-
const indices = [];
|
|
611
|
-
for (const m of sql.matchAll(re)) indices.push(parseInt(m[1], 10));
|
|
612
|
-
return indices;
|
|
613
|
-
}
|
|
614
|
-
function getPlaceholderIndices(sql, dialect) {
|
|
615
|
-
if (dialect === "sqlite") return parseSqlitePlaceholderIndices(sql);
|
|
616
|
-
return {
|
|
617
|
-
indices: parseDollarPlaceholderIndices(sql),
|
|
618
|
-
sawNumbered: false,
|
|
619
|
-
sawAnonymous: false
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
656
|
function maxIndex(indices) {
|
|
623
657
|
return indices.length > 0 ? Math.max(...indices) : 0;
|
|
624
658
|
}
|
|
625
|
-
function
|
|
626
|
-
assertOrThrow(
|
|
627
|
-
!(sawNumbered && sawAnonymous),
|
|
628
|
-
`CRITICAL: Mixed sqlite placeholders ('?' and '?NNN') are not supported.`
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
function ensurePlaceholderMaxMatchesMappingsLength(max, mappingsLength, dialect) {
|
|
632
|
-
assertOrThrow(
|
|
633
|
-
max === mappingsLength,
|
|
634
|
-
`CRITICAL: SQL placeholder max mismatch - max is ${dialectPlaceholderPrefix(dialect)}${max}, but mappings length is ${mappingsLength}.`
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
function ensureSequentialPlaceholders(placeholders, max, dialect) {
|
|
638
|
-
const prefix = dialectPlaceholderPrefix(dialect);
|
|
659
|
+
function ensureSequentialIndices(seen, max, prefix) {
|
|
639
660
|
for (let i = 1; i <= max; i++) {
|
|
640
661
|
assertOrThrow(
|
|
641
|
-
|
|
662
|
+
seen.has(i),
|
|
642
663
|
`CRITICAL: Missing SQL placeholder ${prefix}${i} - placeholders must be sequential 1..${max}.`
|
|
643
664
|
);
|
|
644
665
|
}
|
|
645
666
|
}
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
667
|
+
function validateSqlitePlaceholders(sql, params) {
|
|
668
|
+
const paramLen = params.length;
|
|
669
|
+
const { indices, sawNumbered, sawAnonymous } = parseSqlitePlaceholderIndices(sql);
|
|
670
|
+
if (indices.length === 0) {
|
|
671
|
+
if (paramLen !== 0) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`CRITICAL: Parameter mismatch - SQL has no sqlite placeholders but ${paramLen} params provided. SQL: ${sqlPreview(sql)}`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
653
678
|
assertOrThrow(
|
|
654
|
-
!
|
|
655
|
-
`CRITICAL:
|
|
679
|
+
!(sawNumbered && sawAnonymous),
|
|
680
|
+
`CRITICAL: Mixed sqlite placeholders ('?' and '?NNN') are not supported.`
|
|
656
681
|
);
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
function ensureMappingIndexExistsInSql(placeholders, index) {
|
|
682
|
+
const max = maxIndex(indices);
|
|
660
683
|
assertOrThrow(
|
|
661
|
-
|
|
662
|
-
`CRITICAL:
|
|
684
|
+
max === paramLen,
|
|
685
|
+
`CRITICAL: SQL placeholder max mismatch - max is ?${max}, but params length is ${paramLen}. SQL: ${sqlPreview(sql)}`
|
|
663
686
|
);
|
|
687
|
+
const set = new Set(indices);
|
|
688
|
+
ensureSequentialIndices(set, max, "?");
|
|
664
689
|
}
|
|
665
|
-
function
|
|
666
|
-
|
|
667
|
-
!(mapping.dynamicName !== void 0 && mapping.value !== void 0),
|
|
668
|
-
`CRITICAL: ParamMap ${mapping.index} has both dynamicName and value`
|
|
669
|
-
);
|
|
670
|
-
assertOrThrow(
|
|
671
|
-
!(mapping.dynamicName === void 0 && mapping.value === void 0),
|
|
672
|
-
`CRITICAL: ParamMap ${mapping.index} has neither dynamicName nor value`
|
|
673
|
-
);
|
|
690
|
+
function validateDollarPlaceholders(sql, params) {
|
|
691
|
+
validateParamConsistency(sql, params);
|
|
674
692
|
}
|
|
675
|
-
function
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
mappingIndices.has(i),
|
|
680
|
-
`CRITICAL: Missing ParamMap for placeholder ${prefix}${i} - mappings must cover 1..${max} with no gaps.`
|
|
681
|
-
);
|
|
682
|
-
}
|
|
693
|
+
function detectPlaceholderStyle(sql) {
|
|
694
|
+
const hasDollar = /\$\d+/.test(sql);
|
|
695
|
+
const hasSqliteQ = /\?(?:\d+)?/.test(sql);
|
|
696
|
+
return { hasDollar, hasSqliteQ };
|
|
683
697
|
}
|
|
684
|
-
function
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
698
|
+
function validateParamConsistencyByDialect(sql, params, dialect) {
|
|
699
|
+
const { hasDollar, hasSqliteQ } = detectPlaceholderStyle(sql);
|
|
700
|
+
if (dialect !== "sqlite") {
|
|
701
|
+
if (hasSqliteQ && !hasDollar) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
`CRITICAL: Non-sqlite dialect query contains sqlite '?' placeholders. SQL: ${sqlPreview(sql)}`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
return validateDollarPlaceholders(sql, params);
|
|
691
707
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
sql,
|
|
697
|
-
dialect
|
|
698
|
-
);
|
|
699
|
-
if (dialect === "sqlite") {
|
|
700
|
-
ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous);
|
|
708
|
+
if (hasDollar && hasSqliteQ) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`CRITICAL: Mixed placeholder styles ($N and ?/ ?NNN) are not supported. SQL: ${sqlPreview(sql)}`
|
|
711
|
+
);
|
|
701
712
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const max = maxIndex(indices);
|
|
705
|
-
ensurePlaceholderMaxMatchesMappingsLength(max, mappings.length, dialect);
|
|
706
|
-
ensureSequentialPlaceholders(placeholders, max, dialect);
|
|
707
|
-
validateMappingsAgainstPlaceholders(mappings, placeholders, max, dialect);
|
|
713
|
+
if (hasSqliteQ) return validateSqlitePlaceholders(sql, params);
|
|
714
|
+
return validateDollarPlaceholders(sql, params);
|
|
708
715
|
}
|
|
709
716
|
|
|
710
717
|
// src/builder/shared/sql-utils.ts
|
|
711
|
-
var NUL = String.fromCharCode(0);
|
|
712
718
|
function containsControlChars(s) {
|
|
713
|
-
return
|
|
719
|
+
return /[\u0000-\u001F\u007F]/.test(s);
|
|
714
720
|
}
|
|
715
721
|
function assertNoControlChars(label, s) {
|
|
716
722
|
if (containsControlChars(s)) {
|
|
@@ -929,16 +935,46 @@ function buildTableReference(schemaName, tableName, dialect) {
|
|
|
929
935
|
return `"${safeSchema}"."${safeTable}"`;
|
|
930
936
|
}
|
|
931
937
|
function assertSafeAlias(alias) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
throw new Error("alias is required and cannot be empty");
|
|
938
|
+
if (typeof alias !== "string") {
|
|
939
|
+
throw new Error(`Invalid alias: expected string, got ${typeof alias}`);
|
|
935
940
|
}
|
|
936
|
-
|
|
937
|
-
|
|
941
|
+
const a = alias.trim();
|
|
942
|
+
if (a.length === 0) {
|
|
943
|
+
throw new Error("Invalid alias: required and cannot be empty");
|
|
938
944
|
}
|
|
939
|
-
if (
|
|
945
|
+
if (a !== alias) {
|
|
946
|
+
throw new Error("Invalid alias: leading/trailing whitespace");
|
|
947
|
+
}
|
|
948
|
+
if (/[\u0000-\u001F\u007F]/.test(a)) {
|
|
949
|
+
throw new Error(
|
|
950
|
+
"Invalid alias: contains unsafe characters (control characters)"
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (a.includes('"') || a.includes("'") || a.includes("`")) {
|
|
954
|
+
throw new Error("Invalid alias: contains unsafe characters (quotes)");
|
|
955
|
+
}
|
|
956
|
+
if (a.includes(";")) {
|
|
957
|
+
throw new Error("Invalid alias: contains unsafe characters (semicolon)");
|
|
958
|
+
}
|
|
959
|
+
if (a.includes("--") || a.includes("/*") || a.includes("*/")) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
"Invalid alias: contains unsafe characters (SQL comment tokens)"
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
if (/\s/.test(a)) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
"Invalid alias: must be a simple identifier without whitespace"
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(a)) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`Invalid alias: must be a simple identifier (alphanumeric with underscores): "${alias}"`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
const lowered = a.toLowerCase();
|
|
975
|
+
if (ALIAS_FORBIDDEN_KEYWORDS.has(lowered)) {
|
|
940
976
|
throw new Error(
|
|
941
|
-
`alias
|
|
977
|
+
`Invalid alias: '${alias}' is a SQL keyword that would break query parsing. Forbidden aliases: ${[...ALIAS_FORBIDDEN_KEYWORDS].join(", ")}`
|
|
942
978
|
);
|
|
943
979
|
}
|
|
944
980
|
}
|
|
@@ -980,7 +1016,9 @@ function isValidRelationField(field) {
|
|
|
980
1016
|
if (fk.length === 0) return false;
|
|
981
1017
|
const refsRaw = field.references;
|
|
982
1018
|
const refs = normalizeKeyList(refsRaw);
|
|
983
|
-
if (refs.length === 0)
|
|
1019
|
+
if (refs.length === 0) {
|
|
1020
|
+
return fk.length === 1;
|
|
1021
|
+
}
|
|
984
1022
|
if (refs.length !== fk.length) return false;
|
|
985
1023
|
return true;
|
|
986
1024
|
}
|
|
@@ -995,6 +1033,8 @@ function getReferenceFieldNames(field, foreignKeyCount) {
|
|
|
995
1033
|
return refs;
|
|
996
1034
|
}
|
|
997
1035
|
function joinCondition(field, parentModel, childModel, parentAlias, childAlias) {
|
|
1036
|
+
assertSafeAlias(parentAlias);
|
|
1037
|
+
assertSafeAlias(childAlias);
|
|
998
1038
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
999
1039
|
if (fkFields.length === 0) {
|
|
1000
1040
|
throw createError(
|
|
@@ -1144,6 +1184,32 @@ function normalizeOrderByInput(orderBy, parseValue) {
|
|
|
1144
1184
|
throw new Error("orderBy must be an object or array of objects");
|
|
1145
1185
|
}
|
|
1146
1186
|
|
|
1187
|
+
// src/builder/shared/order-by-determinism.ts
|
|
1188
|
+
function modelHasScalarId(model) {
|
|
1189
|
+
if (!model) return false;
|
|
1190
|
+
return getScalarFieldSet(model).has("id");
|
|
1191
|
+
}
|
|
1192
|
+
function hasIdTiebreaker(orderBy, parse) {
|
|
1193
|
+
if (!isNotNullish(orderBy)) return false;
|
|
1194
|
+
const normalized = normalizeOrderByInput(orderBy, parse);
|
|
1195
|
+
return normalized.some(
|
|
1196
|
+
(obj) => Object.prototype.hasOwnProperty.call(obj, "id")
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
function addIdTiebreaker(orderBy) {
|
|
1200
|
+
if (Array.isArray(orderBy)) return [...orderBy, { id: "asc" }];
|
|
1201
|
+
return [orderBy, { id: "asc" }];
|
|
1202
|
+
}
|
|
1203
|
+
function ensureDeterministicOrderByInput(args) {
|
|
1204
|
+
const { orderBy, model, parseValue } = args;
|
|
1205
|
+
if (!modelHasScalarId(model)) return orderBy;
|
|
1206
|
+
if (!isNotNullish(orderBy)) {
|
|
1207
|
+
return { id: "asc" };
|
|
1208
|
+
}
|
|
1209
|
+
if (hasIdTiebreaker(orderBy, parseValue)) return orderBy;
|
|
1210
|
+
return addIdTiebreaker(orderBy);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1147
1213
|
// src/builder/pagination.ts
|
|
1148
1214
|
var MAX_LIMIT_OFFSET = 2147483647;
|
|
1149
1215
|
function parseDirectionRaw(raw, errorLabel) {
|
|
@@ -1206,7 +1272,15 @@ function hasNonNullishProp(v, key) {
|
|
|
1206
1272
|
}
|
|
1207
1273
|
function normalizeIntegerOrDynamic(name, v) {
|
|
1208
1274
|
if (schemaParser.isDynamicParameter(v)) return v;
|
|
1209
|
-
|
|
1275
|
+
const result = normalizeIntLike(name, v, {
|
|
1276
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
1277
|
+
max: MAX_LIMIT_OFFSET,
|
|
1278
|
+
allowZero: true
|
|
1279
|
+
});
|
|
1280
|
+
if (result === void 0) {
|
|
1281
|
+
throw new Error(`${name} normalization returned undefined`);
|
|
1282
|
+
}
|
|
1283
|
+
return result;
|
|
1210
1284
|
}
|
|
1211
1285
|
function readSkipTake(relArgs) {
|
|
1212
1286
|
const hasSkip = hasNonNullishProp(relArgs, "skip");
|
|
@@ -1272,7 +1346,7 @@ function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
|
1272
1346
|
const placeholdersByField = /* @__PURE__ */ new Map();
|
|
1273
1347
|
const parts = [];
|
|
1274
1348
|
for (const [field, value] of entries) {
|
|
1275
|
-
const c = `${cursorAlias}.${
|
|
1349
|
+
const c = `${cursorAlias}.${quoteColumn(model, field)}`;
|
|
1276
1350
|
if (value === null) {
|
|
1277
1351
|
parts.push(`${c} IS NULL`);
|
|
1278
1352
|
continue;
|
|
@@ -1286,13 +1360,6 @@ function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
|
1286
1360
|
placeholdersByField
|
|
1287
1361
|
};
|
|
1288
1362
|
}
|
|
1289
|
-
function cursorValueExpr(tableName, cursorAlias, cursorWhereSql, field, model) {
|
|
1290
|
-
const colName = quote(field);
|
|
1291
|
-
return `(SELECT ${cursorAlias}.${colName} ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1292
|
-
}
|
|
1293
|
-
function buildCursorRowExistsExpr(tableName, cursorAlias, cursorWhereSql) {
|
|
1294
|
-
return `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1295
|
-
}
|
|
1296
1363
|
function buildCursorEqualityExpr(columnExpr, valueExpr) {
|
|
1297
1364
|
return `((${valueExpr} IS NULL AND ${columnExpr} IS NULL) OR (${valueExpr} IS NOT NULL AND ${columnExpr} = ${valueExpr}))`;
|
|
1298
1365
|
}
|
|
@@ -1331,7 +1398,7 @@ function buildOrderEntries(orderBy) {
|
|
|
1331
1398
|
} else {
|
|
1332
1399
|
entries.push({
|
|
1333
1400
|
field,
|
|
1334
|
-
direction: value.
|
|
1401
|
+
direction: value.direction,
|
|
1335
1402
|
nulls: value.nulls
|
|
1336
1403
|
});
|
|
1337
1404
|
}
|
|
@@ -1339,16 +1406,53 @@ function buildOrderEntries(orderBy) {
|
|
|
1339
1406
|
}
|
|
1340
1407
|
return entries;
|
|
1341
1408
|
}
|
|
1409
|
+
function buildCursorCteSelectList(cursorEntries, orderEntries, model) {
|
|
1410
|
+
const set = /* @__PURE__ */ new Set();
|
|
1411
|
+
for (const [f] of cursorEntries) set.add(f);
|
|
1412
|
+
for (const e of orderEntries) set.add(e.field);
|
|
1413
|
+
const cols = [...set].map((f) => quoteColumn(model, f));
|
|
1414
|
+
if (cols.length === 0) {
|
|
1415
|
+
throw new Error("cursor cte select list is empty");
|
|
1416
|
+
}
|
|
1417
|
+
return cols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
1418
|
+
}
|
|
1419
|
+
function truncateIdent(name, maxLen) {
|
|
1420
|
+
const s = String(name);
|
|
1421
|
+
if (s.length <= maxLen) return s;
|
|
1422
|
+
return s.slice(0, maxLen);
|
|
1423
|
+
}
|
|
1424
|
+
function buildCursorNames(outerAlias) {
|
|
1425
|
+
const maxLen = 63;
|
|
1426
|
+
const base = outerAlias.toLowerCase();
|
|
1427
|
+
const cteName = truncateIdent(`__tp_cursor_${base}`, maxLen);
|
|
1428
|
+
const srcAlias = truncateIdent(`__tp_cursor_src_${base}`, maxLen);
|
|
1429
|
+
if (cteName === outerAlias || srcAlias === outerAlias) {
|
|
1430
|
+
return {
|
|
1431
|
+
cteName: truncateIdent(`__tp_cursor_${base}_x`, maxLen),
|
|
1432
|
+
srcAlias: truncateIdent(`__tp_cursor_src_${base}_x`, maxLen)
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
return { cteName, srcAlias };
|
|
1436
|
+
}
|
|
1342
1437
|
function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect, model) {
|
|
1343
1438
|
var _a;
|
|
1439
|
+
assertSafeTableRef(tableName);
|
|
1440
|
+
assertSafeAlias(alias);
|
|
1344
1441
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
1345
1442
|
const cursorEntries = Object.entries(cursor);
|
|
1346
1443
|
if (cursorEntries.length === 0) {
|
|
1347
1444
|
throw new Error("cursor must have at least one field");
|
|
1348
1445
|
}
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1446
|
+
const { cteName, srcAlias } = buildCursorNames(alias);
|
|
1447
|
+
assertSafeAlias(cteName);
|
|
1448
|
+
assertSafeAlias(srcAlias);
|
|
1449
|
+
const { whereSql: cursorWhereSql, placeholdersByField } = buildCursorFilterParts(cursor, srcAlias, params, model);
|
|
1450
|
+
const deterministicOrderBy = ensureDeterministicOrderByInput({
|
|
1451
|
+
orderBy,
|
|
1452
|
+
model,
|
|
1453
|
+
parseValue: parseOrderByValue
|
|
1454
|
+
});
|
|
1455
|
+
let orderEntries = buildOrderEntries(deterministicOrderBy);
|
|
1352
1456
|
if (orderEntries.length === 0) {
|
|
1353
1457
|
orderEntries = cursorEntries.map(([field]) => ({
|
|
1354
1458
|
field,
|
|
@@ -1357,11 +1461,21 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1357
1461
|
} else {
|
|
1358
1462
|
orderEntries = ensureCursorFieldsInOrder(orderEntries, cursorEntries);
|
|
1359
1463
|
}
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1464
|
+
const cursorOrderBy = orderEntries.map(
|
|
1465
|
+
(e) => `${srcAlias}.${quoteColumn(model, e.field)} ${e.direction.toUpperCase()}`
|
|
1466
|
+
).join(", ");
|
|
1467
|
+
const selectList = buildCursorCteSelectList(
|
|
1468
|
+
cursorEntries,
|
|
1469
|
+
orderEntries,
|
|
1470
|
+
model
|
|
1364
1471
|
);
|
|
1472
|
+
const cte = `${cteName} AS (
|
|
1473
|
+
SELECT ${selectList} FROM ${tableName} ${srcAlias}
|
|
1474
|
+
WHERE ${cursorWhereSql}
|
|
1475
|
+
ORDER BY ${cursorOrderBy}
|
|
1476
|
+
LIMIT 1
|
|
1477
|
+
)`;
|
|
1478
|
+
const existsExpr = `EXISTS (SELECT 1 FROM ${cteName})`;
|
|
1365
1479
|
const outerCursorMatch = buildOuterCursorMatch(
|
|
1366
1480
|
cursor,
|
|
1367
1481
|
alias,
|
|
@@ -1369,34 +1483,31 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1369
1483
|
params,
|
|
1370
1484
|
model
|
|
1371
1485
|
);
|
|
1486
|
+
const getValueExpr = (field) => {
|
|
1487
|
+
return `(SELECT ${quoteColumn(model, field)} FROM ${cteName})`;
|
|
1488
|
+
};
|
|
1372
1489
|
const orClauses = [];
|
|
1373
1490
|
for (let level = 0; level < orderEntries.length; level++) {
|
|
1374
1491
|
const andParts = [];
|
|
1375
1492
|
for (let i = 0; i < level; i++) {
|
|
1376
1493
|
const e2 = orderEntries[i];
|
|
1377
1494
|
const c2 = col(alias, e2.field, model);
|
|
1378
|
-
const v2 =
|
|
1379
|
-
tableName,
|
|
1380
|
-
cursorAlias,
|
|
1381
|
-
cursorWhereSql,
|
|
1382
|
-
e2.field);
|
|
1495
|
+
const v2 = getValueExpr(e2.field);
|
|
1383
1496
|
andParts.push(buildCursorEqualityExpr(c2, v2));
|
|
1384
1497
|
}
|
|
1385
1498
|
const e = orderEntries[level];
|
|
1386
1499
|
const c = col(alias, e.field, model);
|
|
1387
|
-
const v =
|
|
1388
|
-
tableName,
|
|
1389
|
-
cursorAlias,
|
|
1390
|
-
cursorWhereSql,
|
|
1391
|
-
e.field);
|
|
1500
|
+
const v = getValueExpr(e.field);
|
|
1392
1501
|
const nulls = (_a = e.nulls) != null ? _a : defaultNullsFor(d, e.direction);
|
|
1393
1502
|
andParts.push(buildCursorInequalityExpr(c, e.direction, nulls, v));
|
|
1394
1503
|
orClauses.push(`(${andParts.join(SQL_SEPARATORS.CONDITION_AND)})`);
|
|
1395
1504
|
}
|
|
1396
1505
|
const exclusive = orClauses.join(SQL_SEPARATORS.CONDITION_OR);
|
|
1397
|
-
|
|
1506
|
+
const condition = `(${existsExpr} ${SQL_SEPARATORS.CONDITION_AND} ((${exclusive})${SQL_SEPARATORS.CONDITION_OR}(${outerCursorMatch})))`;
|
|
1507
|
+
return { cte, condition };
|
|
1398
1508
|
}
|
|
1399
1509
|
function buildOrderBy(orderBy, alias, dialect, model) {
|
|
1510
|
+
assertSafeAlias(alias);
|
|
1400
1511
|
const entries = buildOrderEntries(orderBy);
|
|
1401
1512
|
if (entries.length === 0) return "";
|
|
1402
1513
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
@@ -1496,6 +1607,11 @@ function buildScalarOperator(expr, op, val, params, mode, fieldType, dialect) {
|
|
|
1496
1607
|
}
|
|
1497
1608
|
return handleInOperator(expr, op, val, params, dialect);
|
|
1498
1609
|
}
|
|
1610
|
+
if (op === Ops.EQUALS && mode === Modes.INSENSITIVE && !isNotNullish(dialect)) {
|
|
1611
|
+
throw createError(`Insensitive equals requires a SQL dialect`, {
|
|
1612
|
+
operator: op
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1499
1615
|
return handleComparisonOperator(expr, op, val, params);
|
|
1500
1616
|
}
|
|
1501
1617
|
function handleNullValue(expr, op) {
|
|
@@ -1511,6 +1627,28 @@ function normalizeMode(v) {
|
|
|
1511
1627
|
function handleNotOperator(expr, val, params, outerMode, fieldType, dialect) {
|
|
1512
1628
|
const innerMode = normalizeMode(val.mode);
|
|
1513
1629
|
const effectiveMode = innerMode != null ? innerMode : outerMode;
|
|
1630
|
+
const entries = Object.entries(val).filter(
|
|
1631
|
+
([k, v]) => k !== "mode" && v !== void 0
|
|
1632
|
+
);
|
|
1633
|
+
if (entries.length === 0) return "";
|
|
1634
|
+
if (!isNotNullish(dialect)) {
|
|
1635
|
+
const clauses = [];
|
|
1636
|
+
for (const [subOp, subVal] of entries) {
|
|
1637
|
+
const sub = buildScalarOperator(
|
|
1638
|
+
expr,
|
|
1639
|
+
subOp,
|
|
1640
|
+
subVal,
|
|
1641
|
+
params,
|
|
1642
|
+
effectiveMode,
|
|
1643
|
+
fieldType,
|
|
1644
|
+
void 0
|
|
1645
|
+
);
|
|
1646
|
+
if (sub && sub.trim().length > 0) clauses.push(`(${sub})`);
|
|
1647
|
+
}
|
|
1648
|
+
if (clauses.length === 0) return "";
|
|
1649
|
+
if (clauses.length === 1) return `${SQL_TEMPLATES.NOT} ${clauses[0]}`;
|
|
1650
|
+
return `${SQL_TEMPLATES.NOT} (${clauses.join(` ${SQL_TEMPLATES.AND} `)})`;
|
|
1651
|
+
}
|
|
1514
1652
|
return buildNotComposite(
|
|
1515
1653
|
expr,
|
|
1516
1654
|
val,
|
|
@@ -1725,6 +1863,7 @@ function handleArrayIsEmpty(expr, val, dialect) {
|
|
|
1725
1863
|
|
|
1726
1864
|
// src/builder/where/operators-json.ts
|
|
1727
1865
|
var SAFE_JSON_PATH_SEGMENT = /^[a-zA-Z_]\w*$/;
|
|
1866
|
+
var MAX_PATH_SEGMENT_LENGTH = 255;
|
|
1728
1867
|
function validateJsonPathSegments(segments) {
|
|
1729
1868
|
for (const segment of segments) {
|
|
1730
1869
|
if (typeof segment !== "string") {
|
|
@@ -1733,6 +1872,12 @@ function validateJsonPathSegments(segments) {
|
|
|
1733
1872
|
value: segment
|
|
1734
1873
|
});
|
|
1735
1874
|
}
|
|
1875
|
+
if (segment.length > MAX_PATH_SEGMENT_LENGTH) {
|
|
1876
|
+
throw createError(
|
|
1877
|
+
`JSON path segment too long: max ${MAX_PATH_SEGMENT_LENGTH} characters`,
|
|
1878
|
+
{ operator: Ops.PATH, value: `[${segment.length} chars]` }
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1736
1881
|
if (!SAFE_JSON_PATH_SEGMENT.test(segment)) {
|
|
1737
1882
|
throw createError(
|
|
1738
1883
|
`Invalid JSON path segment: '${segment}'. Must be alphanumeric with underscores, starting with letter or underscore.`,
|
|
@@ -1821,6 +1966,9 @@ function handleJsonWildcard(expr, op, val, params, wildcards, dialect) {
|
|
|
1821
1966
|
|
|
1822
1967
|
// src/builder/where/relations.ts
|
|
1823
1968
|
var NO_JOINS = Object.freeze([]);
|
|
1969
|
+
function freezeJoins(items) {
|
|
1970
|
+
return Object.freeze([...items]);
|
|
1971
|
+
}
|
|
1824
1972
|
function isListRelation(fieldType) {
|
|
1825
1973
|
return typeof fieldType === "string" && fieldType.endsWith("[]");
|
|
1826
1974
|
}
|
|
@@ -1883,7 +2031,7 @@ function buildListRelationFilters(args) {
|
|
|
1883
2031
|
const whereClause = `${relAlias}.${quote(checkField.name)} IS NULL`;
|
|
1884
2032
|
return Object.freeze({
|
|
1885
2033
|
clause: whereClause,
|
|
1886
|
-
joins: [leftJoinSql]
|
|
2034
|
+
joins: freezeJoins([leftJoinSql])
|
|
1887
2035
|
});
|
|
1888
2036
|
}
|
|
1889
2037
|
}
|
|
@@ -2088,7 +2236,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2088
2236
|
Ops.HAS_EVERY,
|
|
2089
2237
|
Ops.IS_EMPTY
|
|
2090
2238
|
]);
|
|
2091
|
-
const
|
|
2239
|
+
const JSON_OPS2 = /* @__PURE__ */ new Set([
|
|
2092
2240
|
Ops.PATH,
|
|
2093
2241
|
Ops.STRING_CONTAINS,
|
|
2094
2242
|
Ops.STRING_STARTS_WITH,
|
|
@@ -2105,7 +2253,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2105
2253
|
modelName
|
|
2106
2254
|
});
|
|
2107
2255
|
}
|
|
2108
|
-
const isJsonOp =
|
|
2256
|
+
const isJsonOp = JSON_OPS2.has(op);
|
|
2109
2257
|
const isFieldJson = isJsonType(fieldType);
|
|
2110
2258
|
const jsonOpMismatch = isJsonOp && !isFieldJson;
|
|
2111
2259
|
if (jsonOpMismatch) {
|
|
@@ -2119,6 +2267,14 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2119
2267
|
}
|
|
2120
2268
|
|
|
2121
2269
|
// src/builder/where/builder.ts
|
|
2270
|
+
var MAX_QUERY_DEPTH = 50;
|
|
2271
|
+
var EMPTY_JOINS = Object.freeze([]);
|
|
2272
|
+
var JSON_OPS = /* @__PURE__ */ new Set([
|
|
2273
|
+
Ops.PATH,
|
|
2274
|
+
Ops.STRING_CONTAINS,
|
|
2275
|
+
Ops.STRING_STARTS_WITH,
|
|
2276
|
+
Ops.STRING_ENDS_WITH
|
|
2277
|
+
]);
|
|
2122
2278
|
var WhereBuilder = class {
|
|
2123
2279
|
build(where, ctx) {
|
|
2124
2280
|
if (!isPlainObject(where)) {
|
|
@@ -2130,8 +2286,6 @@ var WhereBuilder = class {
|
|
|
2130
2286
|
return buildWhereInternal(where, ctx, this);
|
|
2131
2287
|
}
|
|
2132
2288
|
};
|
|
2133
|
-
var MAX_QUERY_DEPTH = 50;
|
|
2134
|
-
var EMPTY_JOINS = Object.freeze([]);
|
|
2135
2289
|
var whereBuilderInstance = new WhereBuilder();
|
|
2136
2290
|
function freezeResult(clause, joins = EMPTY_JOINS) {
|
|
2137
2291
|
return Object.freeze({ clause, joins });
|
|
@@ -2308,16 +2462,8 @@ function buildOperator(expr, op, val, ctx, mode, fieldType) {
|
|
|
2308
2462
|
if (fieldType && isArrayType(fieldType)) {
|
|
2309
2463
|
return buildArrayOperator(expr, op, val, ctx.params, fieldType, ctx.dialect);
|
|
2310
2464
|
}
|
|
2311
|
-
if (fieldType && isJsonType(fieldType)) {
|
|
2312
|
-
|
|
2313
|
-
Ops.PATH,
|
|
2314
|
-
Ops.STRING_CONTAINS,
|
|
2315
|
-
Ops.STRING_STARTS_WITH,
|
|
2316
|
-
Ops.STRING_ENDS_WITH
|
|
2317
|
-
]);
|
|
2318
|
-
if (JSON_OPS.has(op)) {
|
|
2319
|
-
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2320
|
-
}
|
|
2465
|
+
if (fieldType && isJsonType(fieldType) && JSON_OPS.has(op)) {
|
|
2466
|
+
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2321
2467
|
}
|
|
2322
2468
|
return buildScalarOperator(
|
|
2323
2469
|
expr,
|
|
@@ -2338,7 +2484,7 @@ function toSafeSqlIdentifier(input) {
|
|
|
2338
2484
|
const base = startsOk ? cleaned : `_${cleaned}`;
|
|
2339
2485
|
const fallback = base.length > 0 ? base : "_t";
|
|
2340
2486
|
const lowered = fallback.toLowerCase();
|
|
2341
|
-
return
|
|
2487
|
+
return ALIAS_FORBIDDEN_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
|
|
2342
2488
|
}
|
|
2343
2489
|
function createAliasGenerator(maxAliases = 1e4) {
|
|
2344
2490
|
let counter = 0;
|
|
@@ -2548,6 +2694,7 @@ function toPublicResult(clause, joins, params) {
|
|
|
2548
2694
|
// src/builder/where.ts
|
|
2549
2695
|
function buildWhereClause(where, options) {
|
|
2550
2696
|
var _a, _b, _c, _d, _e;
|
|
2697
|
+
assertSafeAlias(options.alias);
|
|
2551
2698
|
const dialect = options.dialect || getGlobalDialect();
|
|
2552
2699
|
const params = (_a = options.params) != null ? _a : createParamStore();
|
|
2553
2700
|
const ctx = {
|
|
@@ -2716,6 +2863,9 @@ function buildRelationSelect(relArgs, relModel, relAlias) {
|
|
|
2716
2863
|
}
|
|
2717
2864
|
|
|
2718
2865
|
// src/builder/select/includes.ts
|
|
2866
|
+
var MAX_INCLUDE_DEPTH = 10;
|
|
2867
|
+
var MAX_TOTAL_SUBQUERIES = 100;
|
|
2868
|
+
var MAX_TOTAL_INCLUDES = 50;
|
|
2719
2869
|
function getRelationTableReference(relModel, dialect) {
|
|
2720
2870
|
return buildTableReference(
|
|
2721
2871
|
SQL_TEMPLATES.PUBLIC_SCHEMA,
|
|
@@ -2761,107 +2911,24 @@ function relationEntriesFromArgs(args, model) {
|
|
|
2761
2911
|
pushFrom(args.select);
|
|
2762
2912
|
return out;
|
|
2763
2913
|
}
|
|
2764
|
-
function assertScalarField(model, fieldName) {
|
|
2765
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
2766
|
-
if (!f) {
|
|
2767
|
-
throw new Error(
|
|
2768
|
-
`orderBy references unknown field '${fieldName}' on model ${model.name}`
|
|
2769
|
-
);
|
|
2770
|
-
}
|
|
2771
|
-
if (f.isRelation) {
|
|
2772
|
-
throw new Error(
|
|
2773
|
-
`orderBy does not support relation field '${fieldName}' on model ${model.name}`
|
|
2774
|
-
);
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
function validateOrderByDirection(fieldName, v) {
|
|
2778
|
-
const s = String(v).toLowerCase();
|
|
2779
|
-
if (s !== "asc" && s !== "desc") {
|
|
2780
|
-
throw new Error(
|
|
2781
|
-
`Invalid orderBy direction for '${fieldName}': ${String(v)}`
|
|
2782
|
-
);
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
function validateOrderByObject(fieldName, v) {
|
|
2786
|
-
if (!("sort" in v)) {
|
|
2787
|
-
throw new Error(
|
|
2788
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2789
|
-
);
|
|
2790
|
-
}
|
|
2791
|
-
validateOrderByDirection(fieldName, v.sort);
|
|
2792
|
-
if ("nulls" in v && isNotNullish(v.nulls)) {
|
|
2793
|
-
const n = String(v.nulls).toLowerCase();
|
|
2794
|
-
if (n !== "first" && n !== "last") {
|
|
2795
|
-
throw new Error(
|
|
2796
|
-
`Invalid orderBy.nulls for '${fieldName}': ${String(v.nulls)}`
|
|
2797
|
-
);
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
const allowed = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
2801
|
-
for (const k of Object.keys(v)) {
|
|
2802
|
-
if (!allowed.has(k)) {
|
|
2803
|
-
throw new Error(`Unsupported orderBy key '${k}' for field '${fieldName}'`);
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
function normalizeOrderByFieldName(name) {
|
|
2808
|
-
const fieldName = String(name).trim();
|
|
2809
|
-
if (fieldName.length === 0) {
|
|
2810
|
-
throw new Error("orderBy field name cannot be empty");
|
|
2811
|
-
}
|
|
2812
|
-
return fieldName;
|
|
2813
|
-
}
|
|
2814
|
-
function requirePlainObjectForOrderByEntry(v) {
|
|
2815
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
2816
|
-
throw new Error("orderBy array entries must be objects");
|
|
2817
|
-
}
|
|
2818
|
-
return v;
|
|
2819
|
-
}
|
|
2820
|
-
function parseSingleFieldOrderByObject(obj) {
|
|
2821
|
-
const entries = Object.entries(obj);
|
|
2822
|
-
if (entries.length !== 1) {
|
|
2823
|
-
throw new Error("orderBy array entries must have exactly one field");
|
|
2824
|
-
}
|
|
2825
|
-
const fieldName = normalizeOrderByFieldName(entries[0][0]);
|
|
2826
|
-
return [fieldName, entries[0][1]];
|
|
2827
|
-
}
|
|
2828
|
-
function parseOrderByArray(orderBy) {
|
|
2829
|
-
return orderBy.map(
|
|
2830
|
-
(item) => parseSingleFieldOrderByObject(requirePlainObjectForOrderByEntry(item))
|
|
2831
|
-
);
|
|
2832
|
-
}
|
|
2833
|
-
function parseOrderByObject(orderBy) {
|
|
2834
|
-
const out = [];
|
|
2835
|
-
for (const [k, v] of Object.entries(orderBy)) {
|
|
2836
|
-
out.push([normalizeOrderByFieldName(k), v]);
|
|
2837
|
-
}
|
|
2838
|
-
return out;
|
|
2839
|
-
}
|
|
2840
|
-
function getOrderByEntries(orderBy) {
|
|
2841
|
-
if (!isNotNullish(orderBy)) return [];
|
|
2842
|
-
if (Array.isArray(orderBy)) {
|
|
2843
|
-
return parseOrderByArray(orderBy);
|
|
2844
|
-
}
|
|
2845
|
-
if (typeof orderBy === "object" && orderBy !== null) {
|
|
2846
|
-
return parseOrderByObject(orderBy);
|
|
2847
|
-
}
|
|
2848
|
-
throw new Error("orderBy must be an object or array of objects");
|
|
2849
|
-
}
|
|
2850
2914
|
function validateOrderByForModel(model, orderBy) {
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2915
|
+
if (!isNotNullish(orderBy)) return;
|
|
2916
|
+
const scalarSet = getScalarFieldSet(model);
|
|
2917
|
+
const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
2918
|
+
for (const item of normalized) {
|
|
2919
|
+
const entries = Object.entries(item);
|
|
2920
|
+
if (entries.length !== 1) {
|
|
2921
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
2857
2922
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2923
|
+
const fieldName = String(entries[0][0]).trim();
|
|
2924
|
+
if (fieldName.length === 0) {
|
|
2925
|
+
throw new Error("orderBy field name cannot be empty");
|
|
2926
|
+
}
|
|
2927
|
+
if (!scalarSet.has(fieldName)) {
|
|
2928
|
+
throw new Error(
|
|
2929
|
+
`orderBy references unknown or non-scalar field '${fieldName}' on model ${model.name}`
|
|
2930
|
+
);
|
|
2861
2931
|
}
|
|
2862
|
-
throw new Error(
|
|
2863
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2864
|
-
);
|
|
2865
2932
|
}
|
|
2866
2933
|
}
|
|
2867
2934
|
function appendLimitOffset(sql, dialect, params, takeVal, skipVal, scope) {
|
|
@@ -2890,7 +2957,10 @@ function readWhereInput(relArgs) {
|
|
|
2890
2957
|
function readOrderByInput(relArgs) {
|
|
2891
2958
|
if (!isPlainObject(relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
2892
2959
|
if (!("orderBy" in relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
2893
|
-
return {
|
|
2960
|
+
return {
|
|
2961
|
+
hasOrderBy: true,
|
|
2962
|
+
orderBy: relArgs.orderBy
|
|
2963
|
+
};
|
|
2894
2964
|
}
|
|
2895
2965
|
function extractRelationPaginationConfig(relArgs) {
|
|
2896
2966
|
const { hasOrderBy, orderBy: rawOrderByInput } = readOrderByInput(relArgs);
|
|
@@ -2920,36 +2990,28 @@ function maybeReverseNegativeTake(takeVal, hasOrderBy, orderByInput) {
|
|
|
2920
2990
|
orderByInput: reverseOrderByInput(orderByInput)
|
|
2921
2991
|
};
|
|
2922
2992
|
}
|
|
2923
|
-
function
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
);
|
|
2928
|
-
}
|
|
2929
|
-
function modelHasScalarId(relModel) {
|
|
2930
|
-
const idField = relModel.fields.find((f) => f.name === "id");
|
|
2931
|
-
return Boolean(idField && !idField.isRelation);
|
|
2932
|
-
}
|
|
2933
|
-
function addIdTiebreaker(orderByInput) {
|
|
2934
|
-
if (Array.isArray(orderByInput)) return [...orderByInput, { id: "asc" }];
|
|
2935
|
-
return [orderByInput, { id: "asc" }];
|
|
2936
|
-
}
|
|
2937
|
-
function ensureDeterministicOrderBy(relModel, hasOrderBy, orderByInput, hasPagination) {
|
|
2938
|
-
if (!hasPagination) {
|
|
2939
|
-
if (hasOrderBy && isNotNullish(orderByInput)) {
|
|
2940
|
-
validateOrderByForModel(relModel, orderByInput);
|
|
2993
|
+
function ensureDeterministicOrderByForInclude(args) {
|
|
2994
|
+
if (!args.hasPagination) {
|
|
2995
|
+
if (args.hasOrderBy && isNotNullish(args.orderByInput)) {
|
|
2996
|
+
validateOrderByForModel(args.relModel, args.orderByInput);
|
|
2941
2997
|
}
|
|
2942
|
-
return orderByInput;
|
|
2998
|
+
return args.orderByInput;
|
|
2943
2999
|
}
|
|
2944
|
-
if (!hasOrderBy) {
|
|
2945
|
-
return
|
|
3000
|
+
if (!args.hasOrderBy) {
|
|
3001
|
+
return ensureDeterministicOrderByInput({
|
|
3002
|
+
orderBy: void 0,
|
|
3003
|
+
model: args.relModel,
|
|
3004
|
+
parseValue: parseOrderByValue
|
|
3005
|
+
});
|
|
2946
3006
|
}
|
|
2947
|
-
if (isNotNullish(orderByInput)) {
|
|
2948
|
-
validateOrderByForModel(relModel, orderByInput);
|
|
3007
|
+
if (isNotNullish(args.orderByInput)) {
|
|
3008
|
+
validateOrderByForModel(args.relModel, args.orderByInput);
|
|
2949
3009
|
}
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
3010
|
+
return ensureDeterministicOrderByInput({
|
|
3011
|
+
orderBy: args.orderByInput,
|
|
3012
|
+
model: args.relModel,
|
|
3013
|
+
parseValue: parseOrderByValue
|
|
3014
|
+
});
|
|
2953
3015
|
}
|
|
2954
3016
|
function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
2955
3017
|
let relSelect = buildRelationSelect(relArgs, relModel, relAlias);
|
|
@@ -2960,7 +3022,10 @@ function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
|
2960
3022
|
relAlias,
|
|
2961
3023
|
ctx.aliasGen,
|
|
2962
3024
|
ctx.params,
|
|
2963
|
-
ctx.dialect
|
|
3025
|
+
ctx.dialect,
|
|
3026
|
+
ctx.visitPath || [],
|
|
3027
|
+
(ctx.depth || 0) + 1,
|
|
3028
|
+
ctx.stats
|
|
2964
3029
|
) : [];
|
|
2965
3030
|
if (isNonEmptyArray(nestedIncludes)) {
|
|
2966
3031
|
const emptyJson = ctx.dialect === "postgres" ? `'[]'::json` : `json('[]')`;
|
|
@@ -3110,12 +3175,12 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3110
3175
|
paginationConfig.orderBy
|
|
3111
3176
|
);
|
|
3112
3177
|
const hasPagination = paginationConfig.hasSkip || paginationConfig.hasTake;
|
|
3113
|
-
const finalOrderByInput =
|
|
3178
|
+
const finalOrderByInput = ensureDeterministicOrderByForInclude({
|
|
3114
3179
|
relModel,
|
|
3115
|
-
paginationConfig.hasOrderBy,
|
|
3116
|
-
adjusted.orderByInput,
|
|
3180
|
+
hasOrderBy: paginationConfig.hasOrderBy,
|
|
3181
|
+
orderByInput: adjusted.orderByInput,
|
|
3117
3182
|
hasPagination
|
|
3118
|
-
);
|
|
3183
|
+
});
|
|
3119
3184
|
const orderBySql = buildOrderBySql(
|
|
3120
3185
|
finalOrderByInput,
|
|
3121
3186
|
relAlias,
|
|
@@ -3156,12 +3221,48 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3156
3221
|
ctx
|
|
3157
3222
|
});
|
|
3158
3223
|
}
|
|
3159
|
-
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect) {
|
|
3224
|
+
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect, visitPath = [], depth = 0, stats) {
|
|
3225
|
+
if (!stats) {
|
|
3226
|
+
stats = { totalIncludes: 0, totalSubqueries: 0, maxDepth: 0 };
|
|
3227
|
+
}
|
|
3228
|
+
if (depth > MAX_INCLUDE_DEPTH) {
|
|
3229
|
+
throw new Error(
|
|
3230
|
+
`Maximum include depth of ${MAX_INCLUDE_DEPTH} exceeded. Path: ${visitPath.join(" -> ")}. Deep includes cause exponential SQL complexity and performance issues.`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
stats.maxDepth = Math.max(stats.maxDepth, depth);
|
|
3160
3234
|
const includes = [];
|
|
3161
3235
|
const entries = relationEntriesFromArgs(args, model);
|
|
3162
3236
|
for (const [relName, relArgs] of entries) {
|
|
3163
3237
|
if (relArgs === false) continue;
|
|
3238
|
+
stats.totalIncludes++;
|
|
3239
|
+
if (stats.totalIncludes > MAX_TOTAL_INCLUDES) {
|
|
3240
|
+
throw new Error(
|
|
3241
|
+
`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.`
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
stats.totalSubqueries++;
|
|
3245
|
+
if (stats.totalSubqueries > MAX_TOTAL_SUBQUERIES) {
|
|
3246
|
+
throw new Error(
|
|
3247
|
+
`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.`
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3164
3250
|
const resolved = resolveRelationOrThrow(model, schemas, relName);
|
|
3251
|
+
const relationPath = `${model.name}.${relName}`;
|
|
3252
|
+
const currentPath = [...visitPath, relationPath];
|
|
3253
|
+
if (visitPath.includes(relationPath)) {
|
|
3254
|
+
throw new Error(
|
|
3255
|
+
`Circular include detected: ${currentPath.join(" -> ")}. Relation '${relationPath}' creates an infinite loop.`
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
const modelOccurrences = currentPath.filter(
|
|
3259
|
+
(p) => p.startsWith(`${resolved.relModel.name}.`)
|
|
3260
|
+
).length;
|
|
3261
|
+
if (modelOccurrences > 2) {
|
|
3262
|
+
throw new Error(
|
|
3263
|
+
`Include too deeply nested: model '${resolved.relModel.name}' appears ${modelOccurrences} times in path: ${currentPath.join(" -> ")}`
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3165
3266
|
const include = buildSingleInclude(
|
|
3166
3267
|
relName,
|
|
3167
3268
|
relArgs,
|
|
@@ -3173,7 +3274,10 @@ function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, pa
|
|
|
3173
3274
|
parentAlias,
|
|
3174
3275
|
aliasGen,
|
|
3175
3276
|
dialect,
|
|
3176
|
-
params
|
|
3277
|
+
params,
|
|
3278
|
+
visitPath: currentPath,
|
|
3279
|
+
depth: depth + 1,
|
|
3280
|
+
stats
|
|
3177
3281
|
}
|
|
3178
3282
|
);
|
|
3179
3283
|
includes.push(include);
|
|
@@ -3182,6 +3286,11 @@ function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, pa
|
|
|
3182
3286
|
}
|
|
3183
3287
|
function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
3184
3288
|
const aliasGen = createAliasGenerator();
|
|
3289
|
+
const stats = {
|
|
3290
|
+
totalIncludes: 0,
|
|
3291
|
+
totalSubqueries: 0,
|
|
3292
|
+
maxDepth: 0
|
|
3293
|
+
};
|
|
3185
3294
|
return buildIncludeSqlInternal(
|
|
3186
3295
|
args,
|
|
3187
3296
|
model,
|
|
@@ -3189,7 +3298,10 @@ function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
|
3189
3298
|
parentAlias,
|
|
3190
3299
|
aliasGen,
|
|
3191
3300
|
params,
|
|
3192
|
-
dialect
|
|
3301
|
+
dialect,
|
|
3302
|
+
[],
|
|
3303
|
+
0,
|
|
3304
|
+
stats
|
|
3193
3305
|
);
|
|
3194
3306
|
}
|
|
3195
3307
|
function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
@@ -3205,6 +3317,11 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3205
3317
|
`_count.${relName} references unknown relation on model ${model.name}`
|
|
3206
3318
|
);
|
|
3207
3319
|
}
|
|
3320
|
+
if (!isValidRelationField(field)) {
|
|
3321
|
+
throw new Error(
|
|
3322
|
+
`_count.${relName} has invalid relation metadata on model ${model.name}`
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3208
3325
|
const relModel = schemas.find((m) => m.name === field.relatedModel);
|
|
3209
3326
|
if (!relModel) {
|
|
3210
3327
|
throw new Error(
|
|
@@ -3213,31 +3330,81 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3213
3330
|
}
|
|
3214
3331
|
return { field, relModel };
|
|
3215
3332
|
}
|
|
3216
|
-
function
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3333
|
+
function defaultReferencesForCount(fkCount) {
|
|
3334
|
+
if (fkCount === 1) return ["id"];
|
|
3335
|
+
throw new Error(
|
|
3336
|
+
"Relation count for composite keys requires explicit references matching foreignKey length"
|
|
3337
|
+
);
|
|
3220
3338
|
}
|
|
3221
|
-
function
|
|
3339
|
+
function resolveCountKeyPairs(field) {
|
|
3222
3340
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
3223
|
-
|
|
3224
|
-
|
|
3341
|
+
if (fkFields.length === 0) {
|
|
3342
|
+
throw new Error("Relation count requires foreignKey");
|
|
3343
|
+
}
|
|
3344
|
+
const refsRaw = field.references;
|
|
3345
|
+
const refs = normalizeKeyList(refsRaw);
|
|
3346
|
+
const refFields = refs.length > 0 ? refs : defaultReferencesForCount(fkFields.length);
|
|
3347
|
+
if (refFields.length !== fkFields.length) {
|
|
3348
|
+
throw new Error(
|
|
3349
|
+
"Relation count requires references count to match foreignKey count"
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
const relKeyFields = field.isForeignKeyLocal ? refFields : fkFields;
|
|
3353
|
+
const parentKeyFields = field.isForeignKeyLocal ? fkFields : refFields;
|
|
3354
|
+
return { relKeyFields, parentKeyFields };
|
|
3355
|
+
}
|
|
3356
|
+
function aliasQualifiedColumn(alias, model, field) {
|
|
3357
|
+
return `${alias}.${quoteColumn(model, field)}`;
|
|
3358
|
+
}
|
|
3359
|
+
function subqueryForCount(args) {
|
|
3360
|
+
const selectKeys = args.relKeyFields.map(
|
|
3361
|
+
(f, i) => `${aliasQualifiedColumn(args.countAlias, args.relModel, f)} AS "__fk${i}"`
|
|
3362
|
+
).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3363
|
+
const groupByKeys = args.relKeyFields.map((f) => aliasQualifiedColumn(args.countAlias, args.relModel, f)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3364
|
+
const cntExpr = args.dialect === "postgres" ? "COUNT(*)::int AS __cnt" : "COUNT(*) AS __cnt";
|
|
3365
|
+
return `(SELECT ${selectKeys}${SQL_SEPARATORS.FIELD_LIST}${cntExpr} FROM ${args.relTable} ${args.countAlias} GROUP BY ${groupByKeys})`;
|
|
3366
|
+
}
|
|
3367
|
+
function leftJoinOnForCount(args) {
|
|
3368
|
+
const parts = args.parentKeyFields.map(
|
|
3369
|
+
(f, i) => `${args.joinAlias}."__fk${i}" = ${aliasQualifiedColumn(args.parentAlias, args.parentModel, f)}`
|
|
3370
|
+
);
|
|
3371
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
3225
3372
|
}
|
|
3226
|
-
function
|
|
3227
|
-
|
|
3373
|
+
function nextAliasAvoiding(aliasGen, base, forbidden) {
|
|
3374
|
+
let a = aliasGen.next(base);
|
|
3375
|
+
while (forbidden.has(a)) {
|
|
3376
|
+
a = aliasGen.next(base);
|
|
3377
|
+
}
|
|
3378
|
+
return a;
|
|
3228
3379
|
}
|
|
3229
3380
|
function buildCountJoinAndPair(args) {
|
|
3230
3381
|
const relTable = getRelationTableReference(args.relModel, args.dialect);
|
|
3231
|
-
const
|
|
3232
|
-
const
|
|
3233
|
-
const
|
|
3234
|
-
args.
|
|
3382
|
+
const { relKeyFields, parentKeyFields } = resolveCountKeyPairs(args.field);
|
|
3383
|
+
const forbidden = /* @__PURE__ */ new Set([args.parentAlias]);
|
|
3384
|
+
const countAlias = nextAliasAvoiding(
|
|
3385
|
+
args.aliasGen,
|
|
3386
|
+
`__tp_cnt_${args.relName}`,
|
|
3387
|
+
forbidden
|
|
3388
|
+
);
|
|
3389
|
+
forbidden.add(countAlias);
|
|
3390
|
+
const subquery = subqueryForCount({
|
|
3391
|
+
dialect: args.dialect,
|
|
3235
3392
|
relTable,
|
|
3236
3393
|
countAlias,
|
|
3237
|
-
|
|
3394
|
+
relModel: args.relModel,
|
|
3395
|
+
relKeyFields
|
|
3396
|
+
});
|
|
3397
|
+
const joinAlias = nextAliasAvoiding(
|
|
3398
|
+
args.aliasGen,
|
|
3399
|
+
`__tp_cnt_j_${args.relName}`,
|
|
3400
|
+
forbidden
|
|
3238
3401
|
);
|
|
3239
|
-
const
|
|
3240
|
-
|
|
3402
|
+
const leftJoinOn = leftJoinOnForCount({
|
|
3403
|
+
joinAlias,
|
|
3404
|
+
parentAlias: args.parentAlias,
|
|
3405
|
+
parentModel: args.parentModel,
|
|
3406
|
+
parentKeyFields
|
|
3407
|
+
});
|
|
3241
3408
|
return {
|
|
3242
3409
|
joinSql: `LEFT JOIN ${subquery} ${joinAlias} ON ${leftJoinOn}`,
|
|
3243
3410
|
pairSql: `${sqlStringLiteral(args.relName)}, COALESCE(${joinAlias}.__cnt, 0)`
|
|
@@ -3246,6 +3413,7 @@ function buildCountJoinAndPair(args) {
|
|
|
3246
3413
|
function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params, dialect) {
|
|
3247
3414
|
const joins = [];
|
|
3248
3415
|
const pairs = [];
|
|
3416
|
+
const aliasGen = createAliasGenerator();
|
|
3249
3417
|
for (const [relName, shouldCount] of Object.entries(countSelect)) {
|
|
3250
3418
|
if (!shouldCount) continue;
|
|
3251
3419
|
const resolved = resolveCountRelationOrThrow(relName, model, schemas);
|
|
@@ -3253,8 +3421,10 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3253
3421
|
relName,
|
|
3254
3422
|
field: resolved.field,
|
|
3255
3423
|
relModel: resolved.relModel,
|
|
3424
|
+
parentModel: model,
|
|
3256
3425
|
parentAlias,
|
|
3257
|
-
dialect
|
|
3426
|
+
dialect,
|
|
3427
|
+
aliasGen
|
|
3258
3428
|
});
|
|
3259
3429
|
joins.push(built.joinSql);
|
|
3260
3430
|
pairs.push(built.pairSql);
|
|
@@ -3266,16 +3436,21 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3266
3436
|
}
|
|
3267
3437
|
|
|
3268
3438
|
// src/builder/select/assembly.ts
|
|
3269
|
-
var
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
}
|
|
3439
|
+
var ALIAS_CAPTURE = "([A-Za-z_][A-Za-z0-9_]*)";
|
|
3440
|
+
var COLUMN_PART = '(?:"([^"]+)"|([a-z_][a-z0-9_]*))';
|
|
3441
|
+
var AS_PART = `(?:\\s+AS\\s+${COLUMN_PART})?`;
|
|
3442
|
+
var SIMPLE_COLUMN_PATTERN = `^${ALIAS_CAPTURE}\\.${COLUMN_PART}${AS_PART}$`;
|
|
3443
|
+
var SIMPLE_COLUMN_RE = new RegExp(SIMPLE_COLUMN_PATTERN, "i");
|
|
3273
3444
|
function joinNonEmpty(parts, sep) {
|
|
3274
3445
|
return parts.filter((s) => s.trim().length > 0).join(sep);
|
|
3275
3446
|
}
|
|
3276
3447
|
function buildWhereSql(conditions) {
|
|
3277
3448
|
if (!isNonEmptyArray(conditions)) return "";
|
|
3278
|
-
|
|
3449
|
+
const parts = [
|
|
3450
|
+
SQL_TEMPLATES.WHERE,
|
|
3451
|
+
conditions.join(SQL_SEPARATORS.CONDITION_AND)
|
|
3452
|
+
];
|
|
3453
|
+
return ` ${parts.join(" ")}`;
|
|
3279
3454
|
}
|
|
3280
3455
|
function buildJoinsSql(...joinGroups) {
|
|
3281
3456
|
const all = [];
|
|
@@ -3290,37 +3465,39 @@ function buildSelectList(baseSelect, extraCols) {
|
|
|
3290
3465
|
if (base && extra) return `${base}${SQL_SEPARATORS.FIELD_LIST}${extra}`;
|
|
3291
3466
|
return base || extra;
|
|
3292
3467
|
}
|
|
3293
|
-
function finalizeSql(sql, params) {
|
|
3468
|
+
function finalizeSql(sql, params, dialect) {
|
|
3294
3469
|
const snapshot = params.snapshot();
|
|
3295
3470
|
validateSelectQuery(sql);
|
|
3296
|
-
|
|
3471
|
+
validateParamConsistencyByDialect(sql, snapshot.params, dialect);
|
|
3297
3472
|
return Object.freeze({
|
|
3298
3473
|
sql,
|
|
3299
|
-
params:
|
|
3474
|
+
params: snapshot.params,
|
|
3300
3475
|
paramMappings: Object.freeze(snapshot.mappings)
|
|
3301
3476
|
});
|
|
3302
3477
|
}
|
|
3303
|
-
function parseSimpleScalarSelect(select,
|
|
3304
|
-
var _a, _b;
|
|
3478
|
+
function parseSimpleScalarSelect(select, fromAlias) {
|
|
3479
|
+
var _a, _b, _c, _d;
|
|
3305
3480
|
const raw = select.trim();
|
|
3306
3481
|
if (raw.length === 0) return [];
|
|
3307
|
-
let re = SIMPLE_SELECT_RE_CACHE.get(alias);
|
|
3308
|
-
if (!re) {
|
|
3309
|
-
const safeAlias2 = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3310
|
-
re = new RegExp(`^${safeAlias2}\\.(?:"([^"]+)"|([a-z_][a-z0-9_]*))$`, "i");
|
|
3311
|
-
SIMPLE_SELECT_RE_CACHE.set(alias, re);
|
|
3312
|
-
}
|
|
3313
3482
|
const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
|
|
3314
3483
|
const names = [];
|
|
3315
3484
|
for (const part of parts) {
|
|
3316
3485
|
const p = part.trim();
|
|
3317
|
-
const m = p.match(
|
|
3486
|
+
const m = p.match(SIMPLE_COLUMN_RE);
|
|
3318
3487
|
if (!m) {
|
|
3319
3488
|
throw new Error(
|
|
3320
|
-
`sqlite distinct emulation requires scalar select fields to be simple columns. Got: ${p}`
|
|
3489
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (optionally with AS). Got: ${p}`
|
|
3321
3490
|
);
|
|
3322
3491
|
}
|
|
3323
|
-
const
|
|
3492
|
+
const actualAlias = m[1];
|
|
3493
|
+
if (actualAlias.toLowerCase() !== fromAlias.toLowerCase()) {
|
|
3494
|
+
throw new Error(
|
|
3495
|
+
`Expected alias '${fromAlias}', got '${actualAlias}' in: ${p}`
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3498
|
+
const columnName = ((_b = (_a = m[2]) != null ? _a : m[3]) != null ? _b : "").trim();
|
|
3499
|
+
const outAlias = ((_d = (_c = m[4]) != null ? _c : m[5]) != null ? _d : "").trim();
|
|
3500
|
+
const name = outAlias.length > 0 ? outAlias : columnName;
|
|
3324
3501
|
if (name.length === 0) {
|
|
3325
3502
|
throw new Error(`Failed to parse selected column name from: ${p}`);
|
|
3326
3503
|
}
|
|
@@ -3329,18 +3506,18 @@ function parseSimpleScalarSelect(select, alias) {
|
|
|
3329
3506
|
return names;
|
|
3330
3507
|
}
|
|
3331
3508
|
function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
|
|
3332
|
-
const
|
|
3333
|
-
|
|
3334
|
-
|
|
3509
|
+
const src = String(fromAlias);
|
|
3510
|
+
if (src.length === 0) return orderBy;
|
|
3511
|
+
const escaped = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3512
|
+
const re = new RegExp(`\\b${escaped}\\.`, "gi");
|
|
3513
|
+
return orderBy.replace(re, `${outerAlias}.`);
|
|
3335
3514
|
}
|
|
3336
3515
|
function buildDistinctColumns(distinct, fromAlias, model) {
|
|
3337
3516
|
return distinct.map((f) => col(fromAlias, f, model)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3338
3517
|
}
|
|
3339
3518
|
function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
3340
3519
|
const outputCols = [...scalarNames, ...includeNames];
|
|
3341
|
-
if (hasCount)
|
|
3342
|
-
outputCols.push("_count");
|
|
3343
|
-
}
|
|
3520
|
+
if (hasCount) outputCols.push("_count");
|
|
3344
3521
|
const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3345
3522
|
if (!isNonEmptyString(formatted)) {
|
|
3346
3523
|
throw new Error("distinct emulation requires at least one output column");
|
|
@@ -3349,9 +3526,10 @@ function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
|
3349
3526
|
}
|
|
3350
3527
|
function buildWindowOrder(args) {
|
|
3351
3528
|
const { baseOrder, idField, fromAlias, model } = args;
|
|
3529
|
+
const fromLower = String(fromAlias).toLowerCase();
|
|
3352
3530
|
const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
|
|
3353
3531
|
const hasIdInOrder = orderFields.some(
|
|
3354
|
-
(f) => f.startsWith(`${
|
|
3532
|
+
(f) => f.startsWith(`${fromLower}.id `) || f.startsWith(`${fromLower}."id" `)
|
|
3355
3533
|
);
|
|
3356
3534
|
if (hasIdInOrder) return baseOrder;
|
|
3357
3535
|
const idTiebreaker = idField ? `, ${col(fromAlias, "id", model)} ASC` : "";
|
|
@@ -3386,15 +3564,37 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3386
3564
|
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, `"__tp_distinct"`) : replaceOrderByAlias(fallbackOrder, from.alias, `"__tp_distinct"`);
|
|
3387
3565
|
const joins = buildJoinsSql(whereJoins, countJoins);
|
|
3388
3566
|
const conditions = [];
|
|
3389
|
-
if (whereClause && whereClause !== "1=1")
|
|
3390
|
-
conditions.push(whereClause);
|
|
3391
|
-
}
|
|
3567
|
+
if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
|
|
3392
3568
|
const whereSql = buildWhereSql(conditions);
|
|
3393
3569
|
const innerSelectList = selectWithIncludes.trim();
|
|
3394
3570
|
const innerComma = innerSelectList.length > 0 ? SQL_SEPARATORS.FIELD_LIST : "";
|
|
3395
|
-
const
|
|
3396
|
-
|
|
3397
|
-
|
|
3571
|
+
const innerParts = [
|
|
3572
|
+
SQL_TEMPLATES.SELECT,
|
|
3573
|
+
innerSelectList + innerComma,
|
|
3574
|
+
`ROW_NUMBER() OVER (PARTITION BY ${distinctCols} ORDER BY ${windowOrder})`,
|
|
3575
|
+
SQL_TEMPLATES.AS,
|
|
3576
|
+
'"__tp_rn"',
|
|
3577
|
+
SQL_TEMPLATES.FROM,
|
|
3578
|
+
from.table,
|
|
3579
|
+
from.alias
|
|
3580
|
+
];
|
|
3581
|
+
if (joins) innerParts.push(joins);
|
|
3582
|
+
if (whereSql) innerParts.push(whereSql);
|
|
3583
|
+
const inner = innerParts.filter(Boolean).join(" ");
|
|
3584
|
+
const outerParts = [
|
|
3585
|
+
SQL_TEMPLATES.SELECT,
|
|
3586
|
+
outerSelectCols,
|
|
3587
|
+
SQL_TEMPLATES.FROM,
|
|
3588
|
+
`(${inner})`,
|
|
3589
|
+
SQL_TEMPLATES.AS,
|
|
3590
|
+
'"__tp_distinct"',
|
|
3591
|
+
SQL_TEMPLATES.WHERE,
|
|
3592
|
+
'"__tp_rn" = 1'
|
|
3593
|
+
];
|
|
3594
|
+
if (isNonEmptyString(outerOrder)) {
|
|
3595
|
+
outerParts.push(SQL_TEMPLATES.ORDER_BY, outerOrder);
|
|
3596
|
+
}
|
|
3597
|
+
return outerParts.filter(Boolean).join(" ");
|
|
3398
3598
|
}
|
|
3399
3599
|
function buildIncludeColumns(spec) {
|
|
3400
3600
|
var _a, _b;
|
|
@@ -3522,6 +3722,7 @@ function constructFinalSql(spec) {
|
|
|
3522
3722
|
orderBy,
|
|
3523
3723
|
distinct,
|
|
3524
3724
|
method,
|
|
3725
|
+
cursorCte,
|
|
3525
3726
|
cursorClause,
|
|
3526
3727
|
params,
|
|
3527
3728
|
dialect,
|
|
@@ -3536,9 +3737,13 @@ function constructFinalSql(spec) {
|
|
|
3536
3737
|
const spec2 = withCountJoins(spec, countJoins, whereJoins);
|
|
3537
3738
|
let sql2 = buildSqliteDistinctQuery(spec2, selectWithIncludes).trim();
|
|
3538
3739
|
sql2 = appendPagination(sql2, spec);
|
|
3539
|
-
return finalizeSql(sql2, params);
|
|
3740
|
+
return finalizeSql(sql2, params, dialect);
|
|
3540
3741
|
}
|
|
3541
|
-
const parts = [
|
|
3742
|
+
const parts = [];
|
|
3743
|
+
if (cursorCte) {
|
|
3744
|
+
parts.push(`WITH ${cursorCte}`);
|
|
3745
|
+
}
|
|
3746
|
+
parts.push(SQL_TEMPLATES.SELECT);
|
|
3542
3747
|
const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct, model) : null;
|
|
3543
3748
|
if (distinctOn) parts.push(distinctOn);
|
|
3544
3749
|
const baseSelect = (select != null ? select : "").trim();
|
|
@@ -3554,7 +3759,41 @@ function constructFinalSql(spec) {
|
|
|
3554
3759
|
if (isNonEmptyString(orderBy)) parts.push(SQL_TEMPLATES.ORDER_BY, orderBy);
|
|
3555
3760
|
let sql = parts.join(" ").trim();
|
|
3556
3761
|
sql = appendPagination(sql, spec);
|
|
3557
|
-
return finalizeSql(sql, params);
|
|
3762
|
+
return finalizeSql(sql, params, dialect);
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// src/builder/shared/validators/field-assertions.ts
|
|
3766
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Decimal", "BigInt"]);
|
|
3767
|
+
function assertScalarField(model, fieldName, context) {
|
|
3768
|
+
const field = getFieldInfo(model, fieldName);
|
|
3769
|
+
if (!field) {
|
|
3770
|
+
throw createError(
|
|
3771
|
+
`${context} references unknown field '${fieldName}' on model ${model.name}`,
|
|
3772
|
+
{
|
|
3773
|
+
field: fieldName,
|
|
3774
|
+
modelName: model.name,
|
|
3775
|
+
availableFields: model.fields.map((f) => f.name)
|
|
3776
|
+
}
|
|
3777
|
+
);
|
|
3778
|
+
}
|
|
3779
|
+
if (field.isRelation) {
|
|
3780
|
+
throw createError(
|
|
3781
|
+
`${context} does not support relation field '${fieldName}'`,
|
|
3782
|
+
{ field: fieldName, modelName: model.name }
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
3785
|
+
return field;
|
|
3786
|
+
}
|
|
3787
|
+
function assertNumericField(model, fieldName, context) {
|
|
3788
|
+
const field = assertScalarField(model, fieldName, context);
|
|
3789
|
+
const baseType = field.type.replace(/\[\]|\?/g, "");
|
|
3790
|
+
if (!NUMERIC_TYPES.has(baseType)) {
|
|
3791
|
+
throw createError(
|
|
3792
|
+
`${context} requires numeric field, got '${field.type}'`,
|
|
3793
|
+
{ field: fieldName, modelName: model.name }
|
|
3794
|
+
);
|
|
3795
|
+
}
|
|
3796
|
+
return field;
|
|
3558
3797
|
}
|
|
3559
3798
|
|
|
3560
3799
|
// src/builder/select.ts
|
|
@@ -3587,7 +3826,7 @@ function buildPostgresDistinctOrderBy(distinctFields, existing) {
|
|
|
3587
3826
|
}
|
|
3588
3827
|
return next;
|
|
3589
3828
|
}
|
|
3590
|
-
function applyPostgresDistinctOrderBy(args
|
|
3829
|
+
function applyPostgresDistinctOrderBy(args) {
|
|
3591
3830
|
const distinctFields = normalizeDistinctFields(args.distinct);
|
|
3592
3831
|
if (distinctFields.length === 0) return args;
|
|
3593
3832
|
if (!isNotNullish(args.orderBy)) return args;
|
|
@@ -3597,19 +3836,6 @@ function applyPostgresDistinctOrderBy(args, _model) {
|
|
|
3597
3836
|
orderBy: buildPostgresDistinctOrderBy(distinctFields, existing)
|
|
3598
3837
|
});
|
|
3599
3838
|
}
|
|
3600
|
-
function assertScalarFieldOnModel(model, fieldName, ctx) {
|
|
3601
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
3602
|
-
if (!f) {
|
|
3603
|
-
throw new Error(
|
|
3604
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}`
|
|
3605
|
-
);
|
|
3606
|
-
}
|
|
3607
|
-
if (f.isRelation) {
|
|
3608
|
-
throw new Error(
|
|
3609
|
-
`${ctx} does not support relation field '${fieldName}' on model ${model.name}`
|
|
3610
|
-
);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
3839
|
function validateDistinct(model, distinct) {
|
|
3614
3840
|
if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) return;
|
|
3615
3841
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3620,24 +3846,24 @@ function validateDistinct(model, distinct) {
|
|
|
3620
3846
|
throw new Error(`distinct must not contain duplicates (field: '${f}')`);
|
|
3621
3847
|
}
|
|
3622
3848
|
seen.add(f);
|
|
3623
|
-
|
|
3849
|
+
assertScalarField(model, f, "distinct");
|
|
3624
3850
|
}
|
|
3625
3851
|
}
|
|
3626
|
-
function validateOrderByValue(fieldName, v) {
|
|
3627
|
-
parseOrderByValue(v, fieldName);
|
|
3628
|
-
}
|
|
3629
3852
|
function validateOrderBy(model, orderBy) {
|
|
3630
3853
|
if (!isNotNullish(orderBy)) return;
|
|
3631
3854
|
const items = normalizeOrderByInput2(orderBy);
|
|
3632
3855
|
if (items.length === 0) return;
|
|
3633
3856
|
for (const it of items) {
|
|
3634
3857
|
const entries = Object.entries(it);
|
|
3858
|
+
if (entries.length !== 1) {
|
|
3859
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
3860
|
+
}
|
|
3635
3861
|
const fieldName = String(entries[0][0]).trim();
|
|
3636
3862
|
if (fieldName.length === 0) {
|
|
3637
3863
|
throw new Error("orderBy field name cannot be empty");
|
|
3638
3864
|
}
|
|
3639
|
-
|
|
3640
|
-
|
|
3865
|
+
assertScalarField(model, fieldName, "orderBy");
|
|
3866
|
+
parseOrderByValue(entries[0][1], fieldName);
|
|
3641
3867
|
}
|
|
3642
3868
|
}
|
|
3643
3869
|
function validateCursor(model, cursor) {
|
|
@@ -3654,7 +3880,7 @@ function validateCursor(model, cursor) {
|
|
|
3654
3880
|
if (f.length === 0) {
|
|
3655
3881
|
throw new Error("cursor field name cannot be empty");
|
|
3656
3882
|
}
|
|
3657
|
-
|
|
3883
|
+
assertScalarField(model, f, "cursor");
|
|
3658
3884
|
}
|
|
3659
3885
|
}
|
|
3660
3886
|
function resolveDialect(dialect) {
|
|
@@ -3673,20 +3899,21 @@ function normalizeArgsForNegativeTake(method, args) {
|
|
|
3673
3899
|
orderBy: reverseOrderByInput(args.orderBy)
|
|
3674
3900
|
});
|
|
3675
3901
|
}
|
|
3676
|
-
function normalizeArgsForDialect(dialect, args
|
|
3902
|
+
function normalizeArgsForDialect(dialect, args) {
|
|
3677
3903
|
if (dialect !== "postgres") return args;
|
|
3678
3904
|
return applyPostgresDistinctOrderBy(args);
|
|
3679
3905
|
}
|
|
3680
3906
|
function buildCursorClauseIfAny(input) {
|
|
3681
|
-
const { cursor, orderBy, tableName, alias, params, dialect } = input;
|
|
3682
|
-
if (!isNotNullish(cursor)) return
|
|
3907
|
+
const { cursor, orderBy, tableName, alias, params, dialect, model } = input;
|
|
3908
|
+
if (!isNotNullish(cursor)) return {};
|
|
3683
3909
|
return buildCursorCondition(
|
|
3684
3910
|
cursor,
|
|
3685
3911
|
orderBy,
|
|
3686
3912
|
tableName,
|
|
3687
3913
|
alias,
|
|
3688
3914
|
params,
|
|
3689
|
-
dialect
|
|
3915
|
+
dialect,
|
|
3916
|
+
model
|
|
3690
3917
|
);
|
|
3691
3918
|
}
|
|
3692
3919
|
function buildSelectSpec(input) {
|
|
@@ -3725,14 +3952,20 @@ function buildSelectSpec(input) {
|
|
|
3725
3952
|
params,
|
|
3726
3953
|
dialect
|
|
3727
3954
|
);
|
|
3728
|
-
const
|
|
3955
|
+
const cursorResult = buildCursorClauseIfAny({
|
|
3729
3956
|
cursor,
|
|
3730
3957
|
orderBy: normalizedArgs.orderBy,
|
|
3731
3958
|
tableName,
|
|
3732
3959
|
alias,
|
|
3733
3960
|
params,
|
|
3734
|
-
dialect
|
|
3961
|
+
dialect,
|
|
3962
|
+
model
|
|
3735
3963
|
});
|
|
3964
|
+
if (dialect === "sqlite" && isNonEmptyArray(normalizedArgs.distinct) && cursorResult.condition) {
|
|
3965
|
+
throw new Error(
|
|
3966
|
+
"Cursor pagination with distinct is not supported in SQLite due to window function limitations. Use findMany with skip/take instead, or remove distinct."
|
|
3967
|
+
);
|
|
3968
|
+
}
|
|
3736
3969
|
return {
|
|
3737
3970
|
select: selectFields,
|
|
3738
3971
|
includes,
|
|
@@ -3743,7 +3976,8 @@ function buildSelectSpec(input) {
|
|
|
3743
3976
|
pagination: { take, skip },
|
|
3744
3977
|
distinct: normalizedArgs.distinct,
|
|
3745
3978
|
method,
|
|
3746
|
-
|
|
3979
|
+
cursorCte: cursorResult.cte,
|
|
3980
|
+
cursorClause: cursorResult.condition,
|
|
3747
3981
|
params,
|
|
3748
3982
|
dialect,
|
|
3749
3983
|
model,
|
|
@@ -3757,9 +3991,7 @@ function buildSelectSql(input) {
|
|
|
3757
3991
|
assertSafeTableRef(from.tableName);
|
|
3758
3992
|
const dialectToUse = resolveDialect(dialect);
|
|
3759
3993
|
const argsForSql = normalizeArgsForNegativeTake(method, args);
|
|
3760
|
-
const normalizedArgs = normalizeArgsForDialect(
|
|
3761
|
-
dialectToUse,
|
|
3762
|
-
argsForSql);
|
|
3994
|
+
const normalizedArgs = normalizeArgsForDialect(dialectToUse, argsForSql);
|
|
3763
3995
|
validateDistinct(model, normalizedArgs.distinct);
|
|
3764
3996
|
validateOrderBy(model, normalizedArgs.orderBy);
|
|
3765
3997
|
validateCursor(model, normalizedArgs.cursor);
|
|
@@ -3775,8 +4007,21 @@ function buildSelectSql(input) {
|
|
|
3775
4007
|
});
|
|
3776
4008
|
return constructFinalSql(spec);
|
|
3777
4009
|
}
|
|
3778
|
-
|
|
3779
|
-
|
|
4010
|
+
|
|
4011
|
+
// src/builder/shared/comparison-builder.ts
|
|
4012
|
+
function buildComparisons(expr, filter, params, dialect, builder, excludeKeys = /* @__PURE__ */ new Set(["mode"])) {
|
|
4013
|
+
const out = [];
|
|
4014
|
+
for (const [op, val] of Object.entries(filter)) {
|
|
4015
|
+
if (excludeKeys.has(op) || val === void 0) continue;
|
|
4016
|
+
const built = builder(expr, op, val, params, dialect);
|
|
4017
|
+
if (built && built.trim().length > 0) {
|
|
4018
|
+
out.push(built);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
return out;
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
// src/builder/aggregates.ts
|
|
3780
4025
|
var AGGREGATES = [
|
|
3781
4026
|
["_sum", "SUM"],
|
|
3782
4027
|
["_avg", "AVG"],
|
|
@@ -3791,19 +4036,16 @@ var COMPARISON_OPS = {
|
|
|
3791
4036
|
[Ops.LT]: "<",
|
|
3792
4037
|
[Ops.LTE]: "<="
|
|
3793
4038
|
};
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
MODEL_FIELD_CACHE.set(model, m);
|
|
3805
|
-
return m;
|
|
3806
|
-
}
|
|
4039
|
+
var HAVING_ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
4040
|
+
Ops.EQUALS,
|
|
4041
|
+
Ops.NOT,
|
|
4042
|
+
Ops.GT,
|
|
4043
|
+
Ops.GTE,
|
|
4044
|
+
Ops.LT,
|
|
4045
|
+
Ops.LTE,
|
|
4046
|
+
Ops.IN,
|
|
4047
|
+
Ops.NOT_IN
|
|
4048
|
+
]);
|
|
3807
4049
|
function isTruthySelection(v) {
|
|
3808
4050
|
return v === true;
|
|
3809
4051
|
}
|
|
@@ -3845,24 +4087,10 @@ function normalizeLogicalValue2(operator, value) {
|
|
|
3845
4087
|
}
|
|
3846
4088
|
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
3847
4089
|
}
|
|
3848
|
-
function
|
|
3849
|
-
|
|
3850
|
-
const field = m.get(fieldName);
|
|
3851
|
-
if (!field) {
|
|
4090
|
+
function assertHavingOp(op) {
|
|
4091
|
+
if (!HAVING_ALLOWED_OPS.has(op)) {
|
|
3852
4092
|
throw new Error(
|
|
3853
|
-
|
|
3854
|
-
);
|
|
3855
|
-
}
|
|
3856
|
-
if (field.isRelation) {
|
|
3857
|
-
throw new Error(`${ctx} does not support relation field '${fieldName}'`);
|
|
3858
|
-
}
|
|
3859
|
-
return { name: field.name, type: field.type };
|
|
3860
|
-
}
|
|
3861
|
-
function assertAggregateFieldType(aggKey, fieldType, fieldName, modelName) {
|
|
3862
|
-
const baseType = fieldType.replace(/\[\]|\?/g, "");
|
|
3863
|
-
if ((aggKey === "_sum" || aggKey === "_avg") && !NUMERIC_TYPES.has(baseType)) {
|
|
3864
|
-
throw new Error(
|
|
3865
|
-
`Cannot use ${aggKey} on non-numeric field '${fieldName}' (type: ${fieldType}) on model ${modelName}`
|
|
4093
|
+
`Unsupported HAVING operator '${op}'. Allowed: ${[...HAVING_ALLOWED_OPS].join(", ")}`
|
|
3866
4094
|
);
|
|
3867
4095
|
}
|
|
3868
4096
|
}
|
|
@@ -3892,6 +4120,7 @@ function buildBinaryComparison(expr, op, val, params) {
|
|
|
3892
4120
|
return `${expr} ${sqlOp} ${placeholder}`;
|
|
3893
4121
|
}
|
|
3894
4122
|
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
4123
|
+
assertHavingOp(op);
|
|
3895
4124
|
if (val === null) return buildNullComparison(expr, op);
|
|
3896
4125
|
if (op === Ops.NOT && isPlainObject(val)) {
|
|
3897
4126
|
return buildNotComposite(
|
|
@@ -3980,20 +4209,19 @@ function assertHavingAggTarget(aggKey, field, model) {
|
|
|
3980
4209
|
}
|
|
3981
4210
|
return;
|
|
3982
4211
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
4212
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4213
|
+
assertNumericField(model, field, "HAVING");
|
|
4214
|
+
} else {
|
|
4215
|
+
assertScalarField(model, field, "HAVING");
|
|
4216
|
+
}
|
|
3985
4217
|
}
|
|
3986
4218
|
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
3987
|
-
|
|
3988
|
-
for (const [op, val] of Object.entries(filter)) {
|
|
3989
|
-
if (op === "mode") continue;
|
|
3990
|
-
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
3991
|
-
if (built && built.trim().length > 0) out.push(built);
|
|
3992
|
-
}
|
|
3993
|
-
return out;
|
|
4219
|
+
return buildComparisons(expr, filter, params, dialect, buildSimpleComparison);
|
|
3994
4220
|
}
|
|
3995
4221
|
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
3996
|
-
if (!isPlainObject(target))
|
|
4222
|
+
if (!isPlainObject(target)) {
|
|
4223
|
+
throw new Error(`HAVING '${aggKey}' must be an object`);
|
|
4224
|
+
}
|
|
3997
4225
|
const out = [];
|
|
3998
4226
|
for (const [field, filter] of Object.entries(target)) {
|
|
3999
4227
|
assertHavingAggTarget(aggKey, field, model);
|
|
@@ -4004,30 +4232,39 @@ function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialec
|
|
|
4004
4232
|
return out;
|
|
4005
4233
|
}
|
|
4006
4234
|
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
4007
|
-
if (!isPlainObject(target))
|
|
4008
|
-
|
|
4235
|
+
if (!isPlainObject(target)) {
|
|
4236
|
+
throw new Error(`HAVING '${fieldName}' must be an object`);
|
|
4237
|
+
}
|
|
4238
|
+
assertScalarField(model, fieldName, "HAVING");
|
|
4009
4239
|
const out = [];
|
|
4010
4240
|
const obj = target;
|
|
4011
4241
|
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
4012
4242
|
for (const aggKey of keys) {
|
|
4013
4243
|
const aggFilter = obj[aggKey];
|
|
4014
4244
|
if (!isPlainObject(aggFilter)) continue;
|
|
4015
|
-
|
|
4245
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4246
|
+
assertNumericField(model, fieldName, "HAVING");
|
|
4247
|
+
}
|
|
4016
4248
|
const entries = Object.entries(aggFilter);
|
|
4017
4249
|
if (entries.length === 0) continue;
|
|
4018
4250
|
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4251
|
+
const clauses = buildComparisons(
|
|
4252
|
+
expr,
|
|
4253
|
+
aggFilter,
|
|
4254
|
+
params,
|
|
4255
|
+
dialect,
|
|
4256
|
+
buildSimpleComparison
|
|
4257
|
+
);
|
|
4258
|
+
out.push(...clauses);
|
|
4024
4259
|
}
|
|
4025
4260
|
return out;
|
|
4026
4261
|
}
|
|
4027
4262
|
function buildHavingClause(having, alias, params, model, dialect) {
|
|
4028
4263
|
if (!isNotNullish(having)) return "";
|
|
4029
4264
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
4030
|
-
if (!isPlainObject(having))
|
|
4265
|
+
if (!isPlainObject(having)) {
|
|
4266
|
+
throw new Error("having must be an object");
|
|
4267
|
+
}
|
|
4031
4268
|
return buildHavingNode(having, alias, params, d, model);
|
|
4032
4269
|
}
|
|
4033
4270
|
function normalizeCountArg(v) {
|
|
@@ -4041,18 +4278,8 @@ function pushCountAllField(fields) {
|
|
|
4041
4278
|
`${SQL_TEMPLATES.COUNT_ALL} ${SQL_TEMPLATES.AS} ${quote("_count._all")}`
|
|
4042
4279
|
);
|
|
4043
4280
|
}
|
|
4044
|
-
function assertCountableScalarField(
|
|
4045
|
-
|
|
4046
|
-
if (!field) {
|
|
4047
|
-
throw new Error(
|
|
4048
|
-
`Field '${fieldName}' does not exist on model ${model.name}`
|
|
4049
|
-
);
|
|
4050
|
-
}
|
|
4051
|
-
if (field.isRelation) {
|
|
4052
|
-
throw new Error(
|
|
4053
|
-
`Cannot use _count on relation field '${fieldName}' on model ${model.name}`
|
|
4054
|
-
);
|
|
4055
|
-
}
|
|
4281
|
+
function assertCountableScalarField(model, fieldName) {
|
|
4282
|
+
assertScalarField(model, fieldName, "_count");
|
|
4056
4283
|
}
|
|
4057
4284
|
function pushCountField(fields, alias, fieldName, model) {
|
|
4058
4285
|
const outAlias = `_count.${fieldName}`;
|
|
@@ -4060,7 +4287,7 @@ function pushCountField(fields, alias, fieldName, model) {
|
|
|
4060
4287
|
`COUNT(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4061
4288
|
);
|
|
4062
4289
|
}
|
|
4063
|
-
function addCountFields(fields, countArg, alias, model
|
|
4290
|
+
function addCountFields(fields, countArg, alias, model) {
|
|
4064
4291
|
if (!isNotNullish(countArg)) return;
|
|
4065
4292
|
if (countArg === true) {
|
|
4066
4293
|
pushCountAllField(fields);
|
|
@@ -4074,7 +4301,7 @@ function addCountFields(fields, countArg, alias, model, fieldMap) {
|
|
|
4074
4301
|
([f, v]) => f !== "_all" && isTruthySelection(v)
|
|
4075
4302
|
);
|
|
4076
4303
|
for (const [f] of selected) {
|
|
4077
|
-
assertCountableScalarField(
|
|
4304
|
+
assertCountableScalarField(model, f);
|
|
4078
4305
|
pushCountField(fields, alias, f, model);
|
|
4079
4306
|
}
|
|
4080
4307
|
}
|
|
@@ -4082,19 +4309,12 @@ function getAggregateSelectionObject(args, agg) {
|
|
|
4082
4309
|
const obj = args[agg];
|
|
4083
4310
|
return isPlainObject(obj) ? obj : void 0;
|
|
4084
4311
|
}
|
|
4085
|
-
function assertAggregatableScalarField(
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
);
|
|
4091
|
-
}
|
|
4092
|
-
if (field.isRelation) {
|
|
4093
|
-
throw new Error(
|
|
4094
|
-
`Cannot use ${agg} on relation field '${fieldName}' on model ${model.name}`
|
|
4095
|
-
);
|
|
4312
|
+
function assertAggregatableScalarField(model, agg, fieldName) {
|
|
4313
|
+
if (agg === "_sum" || agg === "_avg") {
|
|
4314
|
+
assertNumericField(model, fieldName, agg);
|
|
4315
|
+
} else {
|
|
4316
|
+
assertScalarField(model, fieldName, agg);
|
|
4096
4317
|
}
|
|
4097
|
-
return field;
|
|
4098
4318
|
}
|
|
4099
4319
|
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
4100
4320
|
const outAlias = `${agg}.${fieldName}`;
|
|
@@ -4102,7 +4322,7 @@ function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
|
4102
4322
|
`${aggFn}(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4103
4323
|
);
|
|
4104
4324
|
}
|
|
4105
|
-
function addAggregateFields(fields, args, alias, model
|
|
4325
|
+
function addAggregateFields(fields, args, alias, model) {
|
|
4106
4326
|
for (const [agg, aggFn] of AGGREGATES) {
|
|
4107
4327
|
const obj = getAggregateSelectionObject(args, agg);
|
|
4108
4328
|
if (!obj) continue;
|
|
@@ -4110,23 +4330,16 @@ function addAggregateFields(fields, args, alias, model, fieldMap) {
|
|
|
4110
4330
|
if (fieldName === "_all")
|
|
4111
4331
|
throw new Error(`'${agg}' does not support '_all'`);
|
|
4112
4332
|
if (!isTruthySelection(selection)) continue;
|
|
4113
|
-
|
|
4114
|
-
fieldMap,
|
|
4115
|
-
model,
|
|
4116
|
-
agg,
|
|
4117
|
-
fieldName
|
|
4118
|
-
);
|
|
4119
|
-
assertAggregateFieldType(agg, field.type, fieldName, model.name);
|
|
4333
|
+
assertAggregatableScalarField(model, agg, fieldName);
|
|
4120
4334
|
pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model);
|
|
4121
4335
|
}
|
|
4122
4336
|
}
|
|
4123
4337
|
}
|
|
4124
4338
|
function buildAggregateFields(args, alias, model) {
|
|
4125
4339
|
const fields = [];
|
|
4126
|
-
const fieldMap = getModelFieldMap(model);
|
|
4127
4340
|
const countArg = normalizeCountArg(args._count);
|
|
4128
|
-
addCountFields(fields, countArg, alias, model
|
|
4129
|
-
addAggregateFields(fields, args, alias, model
|
|
4341
|
+
addCountFields(fields, countArg, alias, model);
|
|
4342
|
+
addAggregateFields(fields, args, alias, model);
|
|
4130
4343
|
return fields;
|
|
4131
4344
|
}
|
|
4132
4345
|
function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
@@ -4150,7 +4363,7 @@ function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
|
4150
4363
|
validateParamConsistency(sql, whereResult.params);
|
|
4151
4364
|
return Object.freeze({
|
|
4152
4365
|
sql,
|
|
4153
|
-
params: Object.freeze(
|
|
4366
|
+
params: Object.freeze([...whereResult.params]),
|
|
4154
4367
|
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4155
4368
|
});
|
|
4156
4369
|
}
|
|
@@ -4163,32 +4376,24 @@ function assertGroupByBy(args, model) {
|
|
|
4163
4376
|
if (bySet.size !== byFields.length) {
|
|
4164
4377
|
throw new Error("buildGroupBySql: by must not contain duplicates");
|
|
4165
4378
|
}
|
|
4166
|
-
const modelFieldMap = getModelFieldMap(model);
|
|
4167
4379
|
for (const f of byFields) {
|
|
4168
|
-
|
|
4169
|
-
if (!field) {
|
|
4170
|
-
throw new Error(
|
|
4171
|
-
`groupBy.by references unknown field '${f}' on model ${model.name}`
|
|
4172
|
-
);
|
|
4173
|
-
}
|
|
4174
|
-
if (field.isRelation) {
|
|
4175
|
-
throw new Error(
|
|
4176
|
-
`groupBy.by does not support relation field '${f}' on model ${model.name}`
|
|
4177
|
-
);
|
|
4178
|
-
}
|
|
4380
|
+
assertScalarField(model, f, "groupBy.by");
|
|
4179
4381
|
}
|
|
4180
4382
|
return byFields;
|
|
4181
4383
|
}
|
|
4182
4384
|
function buildGroupBySelectParts(args, alias, model, byFields) {
|
|
4183
4385
|
const groupCols = byFields.map((f) => col(alias, f, model));
|
|
4386
|
+
const selectCols = byFields.map((f) => colWithAlias(alias, f, model));
|
|
4184
4387
|
const groupFields = groupCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4185
4388
|
const aggFields = buildAggregateFields(args, alias, model);
|
|
4186
|
-
const selectFields = isNonEmptyArray(aggFields) ?
|
|
4389
|
+
const selectFields = isNonEmptyArray(aggFields) ? selectCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : selectCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4187
4390
|
return { groupCols, groupFields, selectFields };
|
|
4188
4391
|
}
|
|
4189
4392
|
function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
4190
4393
|
if (!isNotNullish(args.having)) return "";
|
|
4191
|
-
if (!isPlainObject(args.having))
|
|
4394
|
+
if (!isPlainObject(args.having)) {
|
|
4395
|
+
throw new Error("having must be an object");
|
|
4396
|
+
}
|
|
4192
4397
|
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4193
4398
|
if (!h || h.trim().length === 0) return "";
|
|
4194
4399
|
return `${SQL_TEMPLATES.HAVING} ${h}`;
|
|
@@ -4224,61 +4429,58 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
|
4224
4429
|
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4225
4430
|
return Object.freeze({
|
|
4226
4431
|
sql,
|
|
4227
|
-
params: Object.freeze(
|
|
4432
|
+
params: Object.freeze(mergedParams),
|
|
4228
4433
|
paramMappings: Object.freeze([
|
|
4229
4434
|
...whereResult.paramMappings,
|
|
4230
4435
|
...snapshot.mappings
|
|
4231
4436
|
])
|
|
4232
4437
|
});
|
|
4233
4438
|
}
|
|
4234
|
-
function buildCountSql(whereResult, tableName, alias, skip,
|
|
4439
|
+
function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
4235
4440
|
assertSafeAlias(alias);
|
|
4236
4441
|
assertSafeTableRef(tableName);
|
|
4237
|
-
|
|
4442
|
+
if (skip !== void 0 && skip !== null) {
|
|
4443
|
+
if (schemaParser.isDynamicParameter(skip)) {
|
|
4444
|
+
throw new Error(
|
|
4445
|
+
"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."
|
|
4446
|
+
);
|
|
4447
|
+
}
|
|
4448
|
+
if (typeof skip === "string") {
|
|
4449
|
+
const s = skip.trim();
|
|
4450
|
+
if (s.length > 0) {
|
|
4451
|
+
const n = Number(s);
|
|
4452
|
+
if (Number.isFinite(n) && Number.isInteger(n) && n > 0) {
|
|
4453
|
+
throw new Error(
|
|
4454
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
if (typeof skip === "number" && Number.isFinite(skip) && Number.isInteger(skip) && skip > 0) {
|
|
4460
|
+
throw new Error(
|
|
4461
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4462
|
+
);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4238
4465
|
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4239
|
-
const params = createParamStore(whereResult.nextParamIndex);
|
|
4240
|
-
const baseSubSelect = [
|
|
4241
|
-
SQL_TEMPLATES.SELECT,
|
|
4242
|
-
"1",
|
|
4243
|
-
SQL_TEMPLATES.FROM,
|
|
4244
|
-
tableName,
|
|
4245
|
-
alias,
|
|
4246
|
-
whereClause
|
|
4247
|
-
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4248
|
-
const normalizedSkip = normalizeSkipLike(skip);
|
|
4249
|
-
const subSelect = applyCountSkip(baseSubSelect, normalizedSkip, params, d);
|
|
4250
4466
|
const sql = [
|
|
4251
4467
|
SQL_TEMPLATES.SELECT,
|
|
4252
4468
|
SQL_TEMPLATES.COUNT_ALL,
|
|
4253
4469
|
SQL_TEMPLATES.AS,
|
|
4254
4470
|
quote("_count._all"),
|
|
4255
4471
|
SQL_TEMPLATES.FROM,
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4472
|
+
tableName,
|
|
4473
|
+
alias,
|
|
4474
|
+
whereClause
|
|
4259
4475
|
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4260
4476
|
validateSelectQuery(sql);
|
|
4261
|
-
|
|
4262
|
-
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4263
|
-
validateParamConsistency(sql, mergedParams);
|
|
4477
|
+
validateParamConsistency(sql, whereResult.params);
|
|
4264
4478
|
return Object.freeze({
|
|
4265
4479
|
sql,
|
|
4266
|
-
params: Object.freeze(
|
|
4267
|
-
paramMappings: Object.freeze([
|
|
4268
|
-
...whereResult.paramMappings,
|
|
4269
|
-
...snapshot.mappings
|
|
4270
|
-
])
|
|
4480
|
+
params: Object.freeze([...whereResult.params]),
|
|
4481
|
+
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4271
4482
|
});
|
|
4272
4483
|
}
|
|
4273
|
-
function applyCountSkip(subSelect, normalizedSkip, params, dialect) {
|
|
4274
|
-
const shouldApply = schemaParser.isDynamicParameter(normalizedSkip) || typeof normalizedSkip === "number" && normalizedSkip > 0;
|
|
4275
|
-
if (!shouldApply) return subSelect;
|
|
4276
|
-
const placeholder = addAutoScoped(params, normalizedSkip, "count.skip");
|
|
4277
|
-
if (dialect === "sqlite") {
|
|
4278
|
-
return `${subSelect} ${SQL_TEMPLATES.LIMIT} -1 ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4279
|
-
}
|
|
4280
|
-
return `${subSelect} ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4281
|
-
}
|
|
4282
4484
|
function safeAlias(input) {
|
|
4283
4485
|
const raw = String(input).toLowerCase();
|
|
4284
4486
|
const cleaned = raw.replace(/[^a-z0-9_]/g, "_");
|
|
@@ -4428,8 +4630,12 @@ function buildAndNormalizeSql(args) {
|
|
|
4428
4630
|
);
|
|
4429
4631
|
}
|
|
4430
4632
|
function finalizeDirective(args) {
|
|
4431
|
-
const { directive, normalizedSql, normalizedMappings } = args;
|
|
4432
|
-
|
|
4633
|
+
const { directive, normalizedSql, normalizedMappings, dialect } = args;
|
|
4634
|
+
const params = normalizedMappings.map((m) => {
|
|
4635
|
+
var _a;
|
|
4636
|
+
return (_a = m.value) != null ? _a : void 0;
|
|
4637
|
+
});
|
|
4638
|
+
validateParamConsistencyByDialect(normalizedSql, params, dialect);
|
|
4433
4639
|
const { staticParams, dynamicKeys } = buildParamsFromMappings(normalizedMappings);
|
|
4434
4640
|
return {
|
|
4435
4641
|
method: directive.method,
|
|
@@ -4466,7 +4672,8 @@ function generateSQL(directive) {
|
|
|
4466
4672
|
return finalizeDirective({
|
|
4467
4673
|
directive,
|
|
4468
4674
|
normalizedSql: normalized.sql,
|
|
4469
|
-
normalizedMappings: normalized.paramMappings
|
|
4675
|
+
normalizedMappings: normalized.paramMappings,
|
|
4676
|
+
dialect
|
|
4470
4677
|
});
|
|
4471
4678
|
}
|
|
4472
4679
|
|
|
@@ -4843,9 +5050,7 @@ function buildSQLFull(model, models, method, args, dialect) {
|
|
|
4843
5050
|
whereResult,
|
|
4844
5051
|
tableName,
|
|
4845
5052
|
alias,
|
|
4846
|
-
args.skip
|
|
4847
|
-
dialect
|
|
4848
|
-
);
|
|
5053
|
+
args.skip);
|
|
4849
5054
|
break;
|
|
4850
5055
|
default:
|
|
4851
5056
|
result = buildSelectSql({
|