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