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/generator.cjs
CHANGED
|
@@ -56,7 +56,7 @@ var require_package = __commonJS({
|
|
|
56
56
|
"package.json"(exports$1, module) {
|
|
57
57
|
module.exports = {
|
|
58
58
|
name: "prisma-sql",
|
|
59
|
-
version: "1.
|
|
59
|
+
version: "1.45.0",
|
|
60
60
|
description: "Convert Prisma queries to optimized SQL with type safety. 2-7x faster than Prisma Client.",
|
|
61
61
|
main: "dist/index.cjs",
|
|
62
62
|
module: "dist/index.js",
|
|
@@ -159,20 +159,13 @@ var SQL_SEPARATORS = Object.freeze({
|
|
|
159
159
|
CONDITION_OR: " OR ",
|
|
160
160
|
ORDER_BY: ", "
|
|
161
161
|
});
|
|
162
|
-
var
|
|
162
|
+
var ALIAS_FORBIDDEN_KEYWORDS = /* @__PURE__ */ new Set([
|
|
163
163
|
"select",
|
|
164
164
|
"from",
|
|
165
165
|
"where",
|
|
166
|
-
"
|
|
167
|
-
"or",
|
|
168
|
-
"not",
|
|
169
|
-
"in",
|
|
170
|
-
"like",
|
|
171
|
-
"between",
|
|
166
|
+
"having",
|
|
172
167
|
"order",
|
|
173
|
-
"by",
|
|
174
168
|
"group",
|
|
175
|
-
"having",
|
|
176
169
|
"limit",
|
|
177
170
|
"offset",
|
|
178
171
|
"join",
|
|
@@ -180,14 +173,42 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
180
173
|
"left",
|
|
181
174
|
"right",
|
|
182
175
|
"outer",
|
|
183
|
-
"
|
|
176
|
+
"cross",
|
|
177
|
+
"full",
|
|
178
|
+
"and",
|
|
179
|
+
"or",
|
|
180
|
+
"not",
|
|
181
|
+
"by",
|
|
184
182
|
"as",
|
|
183
|
+
"on",
|
|
184
|
+
"union",
|
|
185
|
+
"intersect",
|
|
186
|
+
"except",
|
|
187
|
+
"case",
|
|
188
|
+
"when",
|
|
189
|
+
"then",
|
|
190
|
+
"else",
|
|
191
|
+
"end"
|
|
192
|
+
]);
|
|
193
|
+
var SQL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
194
|
+
...ALIAS_FORBIDDEN_KEYWORDS,
|
|
195
|
+
"user",
|
|
196
|
+
"users",
|
|
185
197
|
"table",
|
|
186
198
|
"column",
|
|
187
199
|
"index",
|
|
188
|
-
"user",
|
|
189
|
-
"users",
|
|
190
200
|
"values",
|
|
201
|
+
"in",
|
|
202
|
+
"like",
|
|
203
|
+
"between",
|
|
204
|
+
"is",
|
|
205
|
+
"exists",
|
|
206
|
+
"null",
|
|
207
|
+
"true",
|
|
208
|
+
"false",
|
|
209
|
+
"all",
|
|
210
|
+
"any",
|
|
211
|
+
"some",
|
|
191
212
|
"update",
|
|
192
213
|
"insert",
|
|
193
214
|
"delete",
|
|
@@ -198,25 +219,9 @@ var SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
|
198
219
|
"grant",
|
|
199
220
|
"revoke",
|
|
200
221
|
"exec",
|
|
201
|
-
"execute"
|
|
202
|
-
"union",
|
|
203
|
-
"intersect",
|
|
204
|
-
"except",
|
|
205
|
-
"case",
|
|
206
|
-
"when",
|
|
207
|
-
"then",
|
|
208
|
-
"else",
|
|
209
|
-
"end",
|
|
210
|
-
"null",
|
|
211
|
-
"true",
|
|
212
|
-
"false",
|
|
213
|
-
"is",
|
|
214
|
-
"exists",
|
|
215
|
-
"all",
|
|
216
|
-
"any",
|
|
217
|
-
"some"
|
|
222
|
+
"execute"
|
|
218
223
|
]);
|
|
219
|
-
var
|
|
224
|
+
var SQL_RESERVED_WORDS = SQL_KEYWORDS;
|
|
220
225
|
var DEFAULT_WHERE_CLAUSE = "1=1";
|
|
221
226
|
var SPECIAL_FIELDS = Object.freeze({
|
|
222
227
|
ID: "id"
|
|
@@ -295,12 +300,48 @@ var LIMITS = Object.freeze({
|
|
|
295
300
|
});
|
|
296
301
|
|
|
297
302
|
// src/utils/normalize-value.ts
|
|
298
|
-
|
|
303
|
+
var MAX_DEPTH = 20;
|
|
304
|
+
function normalizeValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
305
|
+
if (depth > MAX_DEPTH) {
|
|
306
|
+
throw new Error(`Max normalization depth exceeded (${MAX_DEPTH} levels)`);
|
|
307
|
+
}
|
|
299
308
|
if (value instanceof Date) {
|
|
309
|
+
const t = value.getTime();
|
|
310
|
+
if (!Number.isFinite(t)) {
|
|
311
|
+
throw new Error("Invalid Date value in SQL params");
|
|
312
|
+
}
|
|
300
313
|
return value.toISOString();
|
|
301
314
|
}
|
|
315
|
+
if (typeof value === "bigint") {
|
|
316
|
+
return value.toString();
|
|
317
|
+
}
|
|
302
318
|
if (Array.isArray(value)) {
|
|
303
|
-
|
|
319
|
+
const arrRef = value;
|
|
320
|
+
if (seen.has(arrRef)) {
|
|
321
|
+
throw new Error("Circular reference in SQL params");
|
|
322
|
+
}
|
|
323
|
+
seen.add(arrRef);
|
|
324
|
+
const out = value.map((v) => normalizeValue(v, seen, depth + 1));
|
|
325
|
+
seen.delete(arrRef);
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
if (value && typeof value === "object") {
|
|
329
|
+
if (value instanceof Uint8Array) return value;
|
|
330
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) return value;
|
|
331
|
+
const proto = Object.getPrototypeOf(value);
|
|
332
|
+
const isPlain = proto === Object.prototype || proto === null;
|
|
333
|
+
if (!isPlain) return value;
|
|
334
|
+
const obj = value;
|
|
335
|
+
if (seen.has(obj)) {
|
|
336
|
+
throw new Error("Circular reference in SQL params");
|
|
337
|
+
}
|
|
338
|
+
seen.add(obj);
|
|
339
|
+
const out = {};
|
|
340
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
341
|
+
out[k] = normalizeValue(v, seen, depth + 1);
|
|
342
|
+
}
|
|
343
|
+
seen.delete(obj);
|
|
344
|
+
return out;
|
|
304
345
|
}
|
|
305
346
|
return value;
|
|
306
347
|
}
|
|
@@ -481,7 +522,7 @@ function prepareArrayParam(value, dialect) {
|
|
|
481
522
|
throw new Error("prepareArrayParam requires array value");
|
|
482
523
|
}
|
|
483
524
|
if (dialect === "postgres") {
|
|
484
|
-
return value.map(normalizeValue);
|
|
525
|
+
return value.map((v) => normalizeValue(v));
|
|
485
526
|
}
|
|
486
527
|
return JSON.stringify(value);
|
|
487
528
|
}
|
|
@@ -553,36 +594,46 @@ function createError(message, ctx, code = "VALIDATION_ERROR") {
|
|
|
553
594
|
}
|
|
554
595
|
|
|
555
596
|
// src/builder/shared/model-field-cache.ts
|
|
556
|
-
var
|
|
557
|
-
|
|
597
|
+
var MODEL_CACHE = /* @__PURE__ */ new WeakMap();
|
|
598
|
+
function ensureFullCache(model) {
|
|
599
|
+
let cache = MODEL_CACHE.get(model);
|
|
600
|
+
if (!cache) {
|
|
601
|
+
const fieldInfo = /* @__PURE__ */ new Map();
|
|
602
|
+
const scalarFields = /* @__PURE__ */ new Set();
|
|
603
|
+
const relationFields = /* @__PURE__ */ new Set();
|
|
604
|
+
const columnMap = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const f of model.fields) {
|
|
606
|
+
const info = {
|
|
607
|
+
name: f.name,
|
|
608
|
+
dbName: f.dbName || f.name,
|
|
609
|
+
type: f.type,
|
|
610
|
+
isRelation: !!f.isRelation,
|
|
611
|
+
isRequired: !!f.isRequired
|
|
612
|
+
};
|
|
613
|
+
fieldInfo.set(f.name, info);
|
|
614
|
+
if (info.isRelation) {
|
|
615
|
+
relationFields.add(f.name);
|
|
616
|
+
} else {
|
|
617
|
+
scalarFields.add(f.name);
|
|
618
|
+
columnMap.set(f.name, info.dbName);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
cache = { fieldInfo, scalarFields, relationFields, columnMap };
|
|
622
|
+
MODEL_CACHE.set(model, cache);
|
|
623
|
+
}
|
|
624
|
+
return cache;
|
|
625
|
+
}
|
|
626
|
+
function getFieldInfo(model, fieldName) {
|
|
627
|
+
return ensureFullCache(model).fieldInfo.get(fieldName);
|
|
628
|
+
}
|
|
558
629
|
function getScalarFieldSet(model) {
|
|
559
|
-
|
|
560
|
-
if (cached) return cached;
|
|
561
|
-
const s = /* @__PURE__ */ new Set();
|
|
562
|
-
for (const f of model.fields) if (!f.isRelation) s.add(f.name);
|
|
563
|
-
SCALAR_SET_CACHE.set(model, s);
|
|
564
|
-
return s;
|
|
630
|
+
return ensureFullCache(model).scalarFields;
|
|
565
631
|
}
|
|
566
632
|
function getRelationFieldSet(model) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const s = /* @__PURE__ */ new Set();
|
|
570
|
-
for (const f of model.fields) if (f.isRelation) s.add(f.name);
|
|
571
|
-
RELATION_SET_CACHE.set(model, s);
|
|
572
|
-
return s;
|
|
573
|
-
}
|
|
574
|
-
var COLUMN_MAP_CACHE = /* @__PURE__ */ new WeakMap();
|
|
633
|
+
return ensureFullCache(model).relationFields;
|
|
634
|
+
}
|
|
575
635
|
function getColumnMap(model) {
|
|
576
|
-
|
|
577
|
-
if (cached) return cached;
|
|
578
|
-
const map = /* @__PURE__ */ new Map();
|
|
579
|
-
for (const f of model.fields) {
|
|
580
|
-
if (!f.isRelation) {
|
|
581
|
-
map.set(f.name, f.dbName || f.name);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
COLUMN_MAP_CACHE.set(model, map);
|
|
585
|
-
return map;
|
|
636
|
+
return ensureFullCache(model).columnMap;
|
|
586
637
|
}
|
|
587
638
|
|
|
588
639
|
// src/builder/shared/validators/sql-validators.ts
|
|
@@ -642,7 +693,7 @@ function scanDollarPlaceholders(sql, markUpTo) {
|
|
|
642
693
|
}
|
|
643
694
|
return { count, min, max, seen };
|
|
644
695
|
}
|
|
645
|
-
function
|
|
696
|
+
function assertNoGapsDollar(scan, rangeMin, rangeMax, sql) {
|
|
646
697
|
for (let k = rangeMin; k <= rangeMax; k++) {
|
|
647
698
|
if (scan.seen[k] !== 1) {
|
|
648
699
|
throw new Error(
|
|
@@ -670,7 +721,7 @@ function validateParamConsistency(sql, params) {
|
|
|
670
721
|
`CRITICAL: Parameter mismatch - SQL max placeholder is $${scan.max} but ${paramLen} params provided. This will cause SQL execution to fail. SQL: ${sqlPreview(sql)}`
|
|
671
722
|
);
|
|
672
723
|
}
|
|
673
|
-
|
|
724
|
+
assertNoGapsDollar(scan, 1, scan.max, sql);
|
|
674
725
|
}
|
|
675
726
|
function needsQuoting(id) {
|
|
676
727
|
if (!isNonEmptyString(id)) return true;
|
|
@@ -688,14 +739,11 @@ function validateParamConsistencyFragment(sql, params) {
|
|
|
688
739
|
`CRITICAL: Parameter mismatch - SQL references $${scan.max} but only ${paramLen} params provided. SQL: ${sqlPreview(sql)}`
|
|
689
740
|
);
|
|
690
741
|
}
|
|
691
|
-
|
|
742
|
+
assertNoGapsDollar(scan, scan.min, scan.max, sql);
|
|
692
743
|
}
|
|
693
744
|
function assertOrThrow(condition, message) {
|
|
694
745
|
if (!condition) throw new Error(message);
|
|
695
746
|
}
|
|
696
|
-
function dialectPlaceholderPrefix(dialect) {
|
|
697
|
-
return dialect === "sqlite" ? "?" : "$";
|
|
698
|
-
}
|
|
699
747
|
function parseSqlitePlaceholderIndices(sql) {
|
|
700
748
|
const re = /\?(?:(\d+))?/g;
|
|
701
749
|
const indices = [];
|
|
@@ -715,112 +763,70 @@ function parseSqlitePlaceholderIndices(sql) {
|
|
|
715
763
|
}
|
|
716
764
|
return { indices, sawNumbered, sawAnonymous };
|
|
717
765
|
}
|
|
718
|
-
function parseDollarPlaceholderIndices(sql) {
|
|
719
|
-
const re = /\$(\d+)/g;
|
|
720
|
-
const indices = [];
|
|
721
|
-
for (const m of sql.matchAll(re)) indices.push(parseInt(m[1], 10));
|
|
722
|
-
return indices;
|
|
723
|
-
}
|
|
724
|
-
function getPlaceholderIndices(sql, dialect) {
|
|
725
|
-
if (dialect === "sqlite") return parseSqlitePlaceholderIndices(sql);
|
|
726
|
-
return {
|
|
727
|
-
indices: parseDollarPlaceholderIndices(sql),
|
|
728
|
-
sawNumbered: false,
|
|
729
|
-
sawAnonymous: false
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
766
|
function maxIndex(indices) {
|
|
733
767
|
return indices.length > 0 ? Math.max(...indices) : 0;
|
|
734
768
|
}
|
|
735
|
-
function
|
|
736
|
-
assertOrThrow(
|
|
737
|
-
!(sawNumbered && sawAnonymous),
|
|
738
|
-
`CRITICAL: Mixed sqlite placeholders ('?' and '?NNN') are not supported.`
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
function ensurePlaceholderMaxMatchesMappingsLength(max, mappingsLength, dialect) {
|
|
742
|
-
assertOrThrow(
|
|
743
|
-
max === mappingsLength,
|
|
744
|
-
`CRITICAL: SQL placeholder max mismatch - max is ${dialectPlaceholderPrefix(dialect)}${max}, but mappings length is ${mappingsLength}.`
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
function ensureSequentialPlaceholders(placeholders, max, dialect) {
|
|
748
|
-
const prefix = dialectPlaceholderPrefix(dialect);
|
|
769
|
+
function ensureSequentialIndices(seen, max, prefix) {
|
|
749
770
|
for (let i = 1; i <= max; i++) {
|
|
750
771
|
assertOrThrow(
|
|
751
|
-
|
|
772
|
+
seen.has(i),
|
|
752
773
|
`CRITICAL: Missing SQL placeholder ${prefix}${i} - placeholders must be sequential 1..${max}.`
|
|
753
774
|
);
|
|
754
775
|
}
|
|
755
776
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
777
|
+
function validateSqlitePlaceholders(sql, params) {
|
|
778
|
+
const paramLen = params.length;
|
|
779
|
+
const { indices, sawNumbered, sawAnonymous } = parseSqlitePlaceholderIndices(sql);
|
|
780
|
+
if (indices.length === 0) {
|
|
781
|
+
if (paramLen !== 0) {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`CRITICAL: Parameter mismatch - SQL has no sqlite placeholders but ${paramLen} params provided. SQL: ${sqlPreview(sql)}`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
763
788
|
assertOrThrow(
|
|
764
|
-
!
|
|
765
|
-
`CRITICAL:
|
|
789
|
+
!(sawNumbered && sawAnonymous),
|
|
790
|
+
`CRITICAL: Mixed sqlite placeholders ('?' and '?NNN') are not supported.`
|
|
766
791
|
);
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
function ensureMappingIndexExistsInSql(placeholders, index) {
|
|
792
|
+
const max = maxIndex(indices);
|
|
770
793
|
assertOrThrow(
|
|
771
|
-
|
|
772
|
-
`CRITICAL:
|
|
794
|
+
max === paramLen,
|
|
795
|
+
`CRITICAL: SQL placeholder max mismatch - max is ?${max}, but params length is ${paramLen}. SQL: ${sqlPreview(sql)}`
|
|
773
796
|
);
|
|
797
|
+
const set = new Set(indices);
|
|
798
|
+
ensureSequentialIndices(set, max, "?");
|
|
774
799
|
}
|
|
775
|
-
function
|
|
776
|
-
|
|
777
|
-
!(mapping.dynamicName !== void 0 && mapping.value !== void 0),
|
|
778
|
-
`CRITICAL: ParamMap ${mapping.index} has both dynamicName and value`
|
|
779
|
-
);
|
|
780
|
-
assertOrThrow(
|
|
781
|
-
!(mapping.dynamicName === void 0 && mapping.value === void 0),
|
|
782
|
-
`CRITICAL: ParamMap ${mapping.index} has neither dynamicName nor value`
|
|
783
|
-
);
|
|
800
|
+
function validateDollarPlaceholders(sql, params) {
|
|
801
|
+
validateParamConsistency(sql, params);
|
|
784
802
|
}
|
|
785
|
-
function
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
mappingIndices.has(i),
|
|
790
|
-
`CRITICAL: Missing ParamMap for placeholder ${prefix}${i} - mappings must cover 1..${max} with no gaps.`
|
|
791
|
-
);
|
|
792
|
-
}
|
|
803
|
+
function detectPlaceholderStyle(sql) {
|
|
804
|
+
const hasDollar = /\$\d+/.test(sql);
|
|
805
|
+
const hasSqliteQ = /\?(?:\d+)?/.test(sql);
|
|
806
|
+
return { hasDollar, hasSqliteQ };
|
|
793
807
|
}
|
|
794
|
-
function
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
808
|
+
function validateParamConsistencyByDialect(sql, params, dialect) {
|
|
809
|
+
const { hasDollar, hasSqliteQ } = detectPlaceholderStyle(sql);
|
|
810
|
+
if (dialect !== "sqlite") {
|
|
811
|
+
if (hasSqliteQ && !hasDollar) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`CRITICAL: Non-sqlite dialect query contains sqlite '?' placeholders. SQL: ${sqlPreview(sql)}`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
return validateDollarPlaceholders(sql, params);
|
|
801
817
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
sql,
|
|
807
|
-
dialect
|
|
808
|
-
);
|
|
809
|
-
if (dialect === "sqlite") {
|
|
810
|
-
ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous);
|
|
818
|
+
if (hasDollar && hasSqliteQ) {
|
|
819
|
+
throw new Error(
|
|
820
|
+
`CRITICAL: Mixed placeholder styles ($N and ?/ ?NNN) are not supported. SQL: ${sqlPreview(sql)}`
|
|
821
|
+
);
|
|
811
822
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const max = maxIndex(indices);
|
|
815
|
-
ensurePlaceholderMaxMatchesMappingsLength(max, mappings.length, dialect);
|
|
816
|
-
ensureSequentialPlaceholders(placeholders, max, dialect);
|
|
817
|
-
validateMappingsAgainstPlaceholders(mappings, placeholders, max, dialect);
|
|
823
|
+
if (hasSqliteQ) return validateSqlitePlaceholders(sql, params);
|
|
824
|
+
return validateDollarPlaceholders(sql, params);
|
|
818
825
|
}
|
|
819
826
|
|
|
820
827
|
// src/builder/shared/sql-utils.ts
|
|
821
|
-
var NUL = String.fromCharCode(0);
|
|
822
828
|
function containsControlChars(s) {
|
|
823
|
-
return
|
|
829
|
+
return /[\u0000-\u001F\u007F]/.test(s);
|
|
824
830
|
}
|
|
825
831
|
function assertNoControlChars(label, s) {
|
|
826
832
|
if (containsControlChars(s)) {
|
|
@@ -1039,16 +1045,46 @@ function buildTableReference(schemaName, tableName, dialect) {
|
|
|
1039
1045
|
return `"${safeSchema}"."${safeTable}"`;
|
|
1040
1046
|
}
|
|
1041
1047
|
function assertSafeAlias(alias) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1048
|
+
if (typeof alias !== "string") {
|
|
1049
|
+
throw new Error(`Invalid alias: expected string, got ${typeof alias}`);
|
|
1050
|
+
}
|
|
1051
|
+
const a = alias.trim();
|
|
1052
|
+
if (a.length === 0) {
|
|
1053
|
+
throw new Error("Invalid alias: required and cannot be empty");
|
|
1045
1054
|
}
|
|
1046
|
-
if (
|
|
1047
|
-
throw new Error(
|
|
1055
|
+
if (a !== alias) {
|
|
1056
|
+
throw new Error("Invalid alias: leading/trailing whitespace");
|
|
1048
1057
|
}
|
|
1049
|
-
if (
|
|
1058
|
+
if (/[\u0000-\u001F\u007F]/.test(a)) {
|
|
1050
1059
|
throw new Error(
|
|
1051
|
-
|
|
1060
|
+
"Invalid alias: contains unsafe characters (control characters)"
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
if (a.includes('"') || a.includes("'") || a.includes("`")) {
|
|
1064
|
+
throw new Error("Invalid alias: contains unsafe characters (quotes)");
|
|
1065
|
+
}
|
|
1066
|
+
if (a.includes(";")) {
|
|
1067
|
+
throw new Error("Invalid alias: contains unsafe characters (semicolon)");
|
|
1068
|
+
}
|
|
1069
|
+
if (a.includes("--") || a.includes("/*") || a.includes("*/")) {
|
|
1070
|
+
throw new Error(
|
|
1071
|
+
"Invalid alias: contains unsafe characters (SQL comment tokens)"
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
if (/\s/.test(a)) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
"Invalid alias: must be a simple identifier without whitespace"
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(a)) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`Invalid alias: must be a simple identifier (alphanumeric with underscores): "${alias}"`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
const lowered = a.toLowerCase();
|
|
1085
|
+
if (ALIAS_FORBIDDEN_KEYWORDS.has(lowered)) {
|
|
1086
|
+
throw new Error(
|
|
1087
|
+
`Invalid alias: '${alias}' is a SQL keyword that would break query parsing. Forbidden aliases: ${[...ALIAS_FORBIDDEN_KEYWORDS].join(", ")}`
|
|
1052
1088
|
);
|
|
1053
1089
|
}
|
|
1054
1090
|
}
|
|
@@ -1090,7 +1126,9 @@ function isValidRelationField(field) {
|
|
|
1090
1126
|
if (fk.length === 0) return false;
|
|
1091
1127
|
const refsRaw = field.references;
|
|
1092
1128
|
const refs = normalizeKeyList(refsRaw);
|
|
1093
|
-
if (refs.length === 0)
|
|
1129
|
+
if (refs.length === 0) {
|
|
1130
|
+
return fk.length === 1;
|
|
1131
|
+
}
|
|
1094
1132
|
if (refs.length !== fk.length) return false;
|
|
1095
1133
|
return true;
|
|
1096
1134
|
}
|
|
@@ -1105,6 +1143,8 @@ function getReferenceFieldNames(field, foreignKeyCount) {
|
|
|
1105
1143
|
return refs;
|
|
1106
1144
|
}
|
|
1107
1145
|
function joinCondition(field, parentModel, childModel, parentAlias, childAlias) {
|
|
1146
|
+
assertSafeAlias(parentAlias);
|
|
1147
|
+
assertSafeAlias(childAlias);
|
|
1108
1148
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
1109
1149
|
if (fkFields.length === 0) {
|
|
1110
1150
|
throw createError(
|
|
@@ -1254,6 +1294,32 @@ function normalizeOrderByInput(orderBy, parseValue) {
|
|
|
1254
1294
|
throw new Error("orderBy must be an object or array of objects");
|
|
1255
1295
|
}
|
|
1256
1296
|
|
|
1297
|
+
// src/builder/shared/order-by-determinism.ts
|
|
1298
|
+
function modelHasScalarId(model) {
|
|
1299
|
+
if (!model) return false;
|
|
1300
|
+
return getScalarFieldSet(model).has("id");
|
|
1301
|
+
}
|
|
1302
|
+
function hasIdTiebreaker(orderBy, parse) {
|
|
1303
|
+
if (!isNotNullish(orderBy)) return false;
|
|
1304
|
+
const normalized = normalizeOrderByInput(orderBy, parse);
|
|
1305
|
+
return normalized.some(
|
|
1306
|
+
(obj) => Object.prototype.hasOwnProperty.call(obj, "id")
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
function addIdTiebreaker(orderBy) {
|
|
1310
|
+
if (Array.isArray(orderBy)) return [...orderBy, { id: "asc" }];
|
|
1311
|
+
return [orderBy, { id: "asc" }];
|
|
1312
|
+
}
|
|
1313
|
+
function ensureDeterministicOrderByInput(args) {
|
|
1314
|
+
const { orderBy, model, parseValue } = args;
|
|
1315
|
+
if (!modelHasScalarId(model)) return orderBy;
|
|
1316
|
+
if (!isNotNullish(orderBy)) {
|
|
1317
|
+
return { id: "asc" };
|
|
1318
|
+
}
|
|
1319
|
+
if (hasIdTiebreaker(orderBy, parseValue)) return orderBy;
|
|
1320
|
+
return addIdTiebreaker(orderBy);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1257
1323
|
// src/builder/pagination.ts
|
|
1258
1324
|
var MAX_LIMIT_OFFSET = 2147483647;
|
|
1259
1325
|
function parseDirectionRaw(raw, errorLabel) {
|
|
@@ -1316,7 +1382,15 @@ function hasNonNullishProp(v, key) {
|
|
|
1316
1382
|
}
|
|
1317
1383
|
function normalizeIntegerOrDynamic(name, v) {
|
|
1318
1384
|
if (schemaParser.isDynamicParameter(v)) return v;
|
|
1319
|
-
|
|
1385
|
+
const result = normalizeIntLike(name, v, {
|
|
1386
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
1387
|
+
max: MAX_LIMIT_OFFSET,
|
|
1388
|
+
allowZero: true
|
|
1389
|
+
});
|
|
1390
|
+
if (result === void 0) {
|
|
1391
|
+
throw new Error(`${name} normalization returned undefined`);
|
|
1392
|
+
}
|
|
1393
|
+
return result;
|
|
1320
1394
|
}
|
|
1321
1395
|
function readSkipTake(relArgs) {
|
|
1322
1396
|
const hasSkip = hasNonNullishProp(relArgs, "skip");
|
|
@@ -1382,7 +1456,7 @@ function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
|
1382
1456
|
const placeholdersByField = /* @__PURE__ */ new Map();
|
|
1383
1457
|
const parts = [];
|
|
1384
1458
|
for (const [field, value] of entries) {
|
|
1385
|
-
const c = `${cursorAlias}.${
|
|
1459
|
+
const c = `${cursorAlias}.${quoteColumn(model, field)}`;
|
|
1386
1460
|
if (value === null) {
|
|
1387
1461
|
parts.push(`${c} IS NULL`);
|
|
1388
1462
|
continue;
|
|
@@ -1396,13 +1470,6 @@ function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
|
1396
1470
|
placeholdersByField
|
|
1397
1471
|
};
|
|
1398
1472
|
}
|
|
1399
|
-
function cursorValueExpr(tableName, cursorAlias, cursorWhereSql, field, model) {
|
|
1400
|
-
const colName = quote(field);
|
|
1401
|
-
return `(SELECT ${cursorAlias}.${colName} ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1402
|
-
}
|
|
1403
|
-
function buildCursorRowExistsExpr(tableName, cursorAlias, cursorWhereSql) {
|
|
1404
|
-
return `EXISTS (${SQL_TEMPLATES.SELECT} 1 ${SQL_TEMPLATES.FROM} ${tableName} ${cursorAlias} ${SQL_TEMPLATES.WHERE} ${cursorWhereSql} ${SQL_TEMPLATES.LIMIT} 1)`;
|
|
1405
|
-
}
|
|
1406
1473
|
function buildCursorEqualityExpr(columnExpr, valueExpr) {
|
|
1407
1474
|
return `((${valueExpr} IS NULL AND ${columnExpr} IS NULL) OR (${valueExpr} IS NOT NULL AND ${columnExpr} = ${valueExpr}))`;
|
|
1408
1475
|
}
|
|
@@ -1441,7 +1508,7 @@ function buildOrderEntries(orderBy) {
|
|
|
1441
1508
|
} else {
|
|
1442
1509
|
entries.push({
|
|
1443
1510
|
field,
|
|
1444
|
-
direction: value.
|
|
1511
|
+
direction: value.direction,
|
|
1445
1512
|
nulls: value.nulls
|
|
1446
1513
|
});
|
|
1447
1514
|
}
|
|
@@ -1449,16 +1516,53 @@ function buildOrderEntries(orderBy) {
|
|
|
1449
1516
|
}
|
|
1450
1517
|
return entries;
|
|
1451
1518
|
}
|
|
1519
|
+
function buildCursorCteSelectList(cursorEntries, orderEntries, model) {
|
|
1520
|
+
const set = /* @__PURE__ */ new Set();
|
|
1521
|
+
for (const [f] of cursorEntries) set.add(f);
|
|
1522
|
+
for (const e of orderEntries) set.add(e.field);
|
|
1523
|
+
const cols = [...set].map((f) => quoteColumn(model, f));
|
|
1524
|
+
if (cols.length === 0) {
|
|
1525
|
+
throw new Error("cursor cte select list is empty");
|
|
1526
|
+
}
|
|
1527
|
+
return cols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
1528
|
+
}
|
|
1529
|
+
function truncateIdent(name, maxLen) {
|
|
1530
|
+
const s = String(name);
|
|
1531
|
+
if (s.length <= maxLen) return s;
|
|
1532
|
+
return s.slice(0, maxLen);
|
|
1533
|
+
}
|
|
1534
|
+
function buildCursorNames(outerAlias) {
|
|
1535
|
+
const maxLen = 63;
|
|
1536
|
+
const base = outerAlias.toLowerCase();
|
|
1537
|
+
const cteName = truncateIdent(`__tp_cursor_${base}`, maxLen);
|
|
1538
|
+
const srcAlias = truncateIdent(`__tp_cursor_src_${base}`, maxLen);
|
|
1539
|
+
if (cteName === outerAlias || srcAlias === outerAlias) {
|
|
1540
|
+
return {
|
|
1541
|
+
cteName: truncateIdent(`__tp_cursor_${base}_x`, maxLen),
|
|
1542
|
+
srcAlias: truncateIdent(`__tp_cursor_src_${base}_x`, maxLen)
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
return { cteName, srcAlias };
|
|
1546
|
+
}
|
|
1452
1547
|
function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect, model) {
|
|
1453
1548
|
var _a;
|
|
1549
|
+
assertSafeTableRef(tableName);
|
|
1550
|
+
assertSafeAlias(alias);
|
|
1454
1551
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
1455
1552
|
const cursorEntries = Object.entries(cursor);
|
|
1456
1553
|
if (cursorEntries.length === 0) {
|
|
1457
1554
|
throw new Error("cursor must have at least one field");
|
|
1458
1555
|
}
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1556
|
+
const { cteName, srcAlias } = buildCursorNames(alias);
|
|
1557
|
+
assertSafeAlias(cteName);
|
|
1558
|
+
assertSafeAlias(srcAlias);
|
|
1559
|
+
const { whereSql: cursorWhereSql, placeholdersByField } = buildCursorFilterParts(cursor, srcAlias, params, model);
|
|
1560
|
+
const deterministicOrderBy = ensureDeterministicOrderByInput({
|
|
1561
|
+
orderBy,
|
|
1562
|
+
model,
|
|
1563
|
+
parseValue: parseOrderByValue
|
|
1564
|
+
});
|
|
1565
|
+
let orderEntries = buildOrderEntries(deterministicOrderBy);
|
|
1462
1566
|
if (orderEntries.length === 0) {
|
|
1463
1567
|
orderEntries = cursorEntries.map(([field]) => ({
|
|
1464
1568
|
field,
|
|
@@ -1467,11 +1571,21 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1467
1571
|
} else {
|
|
1468
1572
|
orderEntries = ensureCursorFieldsInOrder(orderEntries, cursorEntries);
|
|
1469
1573
|
}
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1574
|
+
const cursorOrderBy = orderEntries.map(
|
|
1575
|
+
(e) => `${srcAlias}.${quoteColumn(model, e.field)} ${e.direction.toUpperCase()}`
|
|
1576
|
+
).join(", ");
|
|
1577
|
+
const selectList = buildCursorCteSelectList(
|
|
1578
|
+
cursorEntries,
|
|
1579
|
+
orderEntries,
|
|
1580
|
+
model
|
|
1474
1581
|
);
|
|
1582
|
+
const cte = `${cteName} AS (
|
|
1583
|
+
SELECT ${selectList} FROM ${tableName} ${srcAlias}
|
|
1584
|
+
WHERE ${cursorWhereSql}
|
|
1585
|
+
ORDER BY ${cursorOrderBy}
|
|
1586
|
+
LIMIT 1
|
|
1587
|
+
)`;
|
|
1588
|
+
const existsExpr = `EXISTS (SELECT 1 FROM ${cteName})`;
|
|
1475
1589
|
const outerCursorMatch = buildOuterCursorMatch(
|
|
1476
1590
|
cursor,
|
|
1477
1591
|
alias,
|
|
@@ -1479,34 +1593,31 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1479
1593
|
params,
|
|
1480
1594
|
model
|
|
1481
1595
|
);
|
|
1596
|
+
const getValueExpr = (field) => {
|
|
1597
|
+
return `(SELECT ${quoteColumn(model, field)} FROM ${cteName})`;
|
|
1598
|
+
};
|
|
1482
1599
|
const orClauses = [];
|
|
1483
1600
|
for (let level = 0; level < orderEntries.length; level++) {
|
|
1484
1601
|
const andParts = [];
|
|
1485
1602
|
for (let i = 0; i < level; i++) {
|
|
1486
1603
|
const e2 = orderEntries[i];
|
|
1487
1604
|
const c2 = col(alias, e2.field, model);
|
|
1488
|
-
const v2 =
|
|
1489
|
-
tableName,
|
|
1490
|
-
cursorAlias,
|
|
1491
|
-
cursorWhereSql,
|
|
1492
|
-
e2.field);
|
|
1605
|
+
const v2 = getValueExpr(e2.field);
|
|
1493
1606
|
andParts.push(buildCursorEqualityExpr(c2, v2));
|
|
1494
1607
|
}
|
|
1495
1608
|
const e = orderEntries[level];
|
|
1496
1609
|
const c = col(alias, e.field, model);
|
|
1497
|
-
const v =
|
|
1498
|
-
tableName,
|
|
1499
|
-
cursorAlias,
|
|
1500
|
-
cursorWhereSql,
|
|
1501
|
-
e.field);
|
|
1610
|
+
const v = getValueExpr(e.field);
|
|
1502
1611
|
const nulls = (_a = e.nulls) != null ? _a : defaultNullsFor(d, e.direction);
|
|
1503
1612
|
andParts.push(buildCursorInequalityExpr(c, e.direction, nulls, v));
|
|
1504
1613
|
orClauses.push(`(${andParts.join(SQL_SEPARATORS.CONDITION_AND)})`);
|
|
1505
1614
|
}
|
|
1506
1615
|
const exclusive = orClauses.join(SQL_SEPARATORS.CONDITION_OR);
|
|
1507
|
-
|
|
1616
|
+
const condition = `(${existsExpr} ${SQL_SEPARATORS.CONDITION_AND} ((${exclusive})${SQL_SEPARATORS.CONDITION_OR}(${outerCursorMatch})))`;
|
|
1617
|
+
return { cte, condition };
|
|
1508
1618
|
}
|
|
1509
1619
|
function buildOrderBy(orderBy, alias, dialect, model) {
|
|
1620
|
+
assertSafeAlias(alias);
|
|
1510
1621
|
const entries = buildOrderEntries(orderBy);
|
|
1511
1622
|
if (entries.length === 0) return "";
|
|
1512
1623
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
@@ -1606,6 +1717,11 @@ function buildScalarOperator(expr, op, val, params, mode, fieldType, dialect) {
|
|
|
1606
1717
|
}
|
|
1607
1718
|
return handleInOperator(expr, op, val, params, dialect);
|
|
1608
1719
|
}
|
|
1720
|
+
if (op === Ops.EQUALS && mode === Modes.INSENSITIVE && !isNotNullish(dialect)) {
|
|
1721
|
+
throw createError(`Insensitive equals requires a SQL dialect`, {
|
|
1722
|
+
operator: op
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1609
1725
|
return handleComparisonOperator(expr, op, val, params);
|
|
1610
1726
|
}
|
|
1611
1727
|
function handleNullValue(expr, op) {
|
|
@@ -1621,6 +1737,28 @@ function normalizeMode(v) {
|
|
|
1621
1737
|
function handleNotOperator(expr, val, params, outerMode, fieldType, dialect) {
|
|
1622
1738
|
const innerMode = normalizeMode(val.mode);
|
|
1623
1739
|
const effectiveMode = innerMode != null ? innerMode : outerMode;
|
|
1740
|
+
const entries = Object.entries(val).filter(
|
|
1741
|
+
([k, v]) => k !== "mode" && v !== void 0
|
|
1742
|
+
);
|
|
1743
|
+
if (entries.length === 0) return "";
|
|
1744
|
+
if (!isNotNullish(dialect)) {
|
|
1745
|
+
const clauses = [];
|
|
1746
|
+
for (const [subOp, subVal] of entries) {
|
|
1747
|
+
const sub = buildScalarOperator(
|
|
1748
|
+
expr,
|
|
1749
|
+
subOp,
|
|
1750
|
+
subVal,
|
|
1751
|
+
params,
|
|
1752
|
+
effectiveMode,
|
|
1753
|
+
fieldType,
|
|
1754
|
+
void 0
|
|
1755
|
+
);
|
|
1756
|
+
if (sub && sub.trim().length > 0) clauses.push(`(${sub})`);
|
|
1757
|
+
}
|
|
1758
|
+
if (clauses.length === 0) return "";
|
|
1759
|
+
if (clauses.length === 1) return `${SQL_TEMPLATES.NOT} ${clauses[0]}`;
|
|
1760
|
+
return `${SQL_TEMPLATES.NOT} (${clauses.join(` ${SQL_TEMPLATES.AND} `)})`;
|
|
1761
|
+
}
|
|
1624
1762
|
return buildNotComposite(
|
|
1625
1763
|
expr,
|
|
1626
1764
|
val,
|
|
@@ -1835,6 +1973,7 @@ function handleArrayIsEmpty(expr, val, dialect) {
|
|
|
1835
1973
|
|
|
1836
1974
|
// src/builder/where/operators-json.ts
|
|
1837
1975
|
var SAFE_JSON_PATH_SEGMENT = /^[a-zA-Z_]\w*$/;
|
|
1976
|
+
var MAX_PATH_SEGMENT_LENGTH = 255;
|
|
1838
1977
|
function validateJsonPathSegments(segments) {
|
|
1839
1978
|
for (const segment of segments) {
|
|
1840
1979
|
if (typeof segment !== "string") {
|
|
@@ -1843,6 +1982,12 @@ function validateJsonPathSegments(segments) {
|
|
|
1843
1982
|
value: segment
|
|
1844
1983
|
});
|
|
1845
1984
|
}
|
|
1985
|
+
if (segment.length > MAX_PATH_SEGMENT_LENGTH) {
|
|
1986
|
+
throw createError(
|
|
1987
|
+
`JSON path segment too long: max ${MAX_PATH_SEGMENT_LENGTH} characters`,
|
|
1988
|
+
{ operator: Ops.PATH, value: `[${segment.length} chars]` }
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1846
1991
|
if (!SAFE_JSON_PATH_SEGMENT.test(segment)) {
|
|
1847
1992
|
throw createError(
|
|
1848
1993
|
`Invalid JSON path segment: '${segment}'. Must be alphanumeric with underscores, starting with letter or underscore.`,
|
|
@@ -1931,6 +2076,9 @@ function handleJsonWildcard(expr, op, val, params, wildcards, dialect) {
|
|
|
1931
2076
|
|
|
1932
2077
|
// src/builder/where/relations.ts
|
|
1933
2078
|
var NO_JOINS = Object.freeze([]);
|
|
2079
|
+
function freezeJoins(items) {
|
|
2080
|
+
return Object.freeze([...items]);
|
|
2081
|
+
}
|
|
1934
2082
|
function isListRelation(fieldType) {
|
|
1935
2083
|
return typeof fieldType === "string" && fieldType.endsWith("[]");
|
|
1936
2084
|
}
|
|
@@ -1993,7 +2141,7 @@ function buildListRelationFilters(args) {
|
|
|
1993
2141
|
const whereClause = `${relAlias}.${quote(checkField.name)} IS NULL`;
|
|
1994
2142
|
return Object.freeze({
|
|
1995
2143
|
clause: whereClause,
|
|
1996
|
-
joins: [leftJoinSql]
|
|
2144
|
+
joins: freezeJoins([leftJoinSql])
|
|
1997
2145
|
});
|
|
1998
2146
|
}
|
|
1999
2147
|
}
|
|
@@ -2198,7 +2346,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2198
2346
|
Ops.HAS_EVERY,
|
|
2199
2347
|
Ops.IS_EMPTY
|
|
2200
2348
|
]);
|
|
2201
|
-
const
|
|
2349
|
+
const JSON_OPS2 = /* @__PURE__ */ new Set([
|
|
2202
2350
|
Ops.PATH,
|
|
2203
2351
|
Ops.STRING_CONTAINS,
|
|
2204
2352
|
Ops.STRING_STARTS_WITH,
|
|
@@ -2215,7 +2363,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2215
2363
|
modelName
|
|
2216
2364
|
});
|
|
2217
2365
|
}
|
|
2218
|
-
const isJsonOp =
|
|
2366
|
+
const isJsonOp = JSON_OPS2.has(op);
|
|
2219
2367
|
const isFieldJson = isJsonType(fieldType);
|
|
2220
2368
|
const jsonOpMismatch = isJsonOp && !isFieldJson;
|
|
2221
2369
|
if (jsonOpMismatch) {
|
|
@@ -2229,6 +2377,14 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
|
|
|
2229
2377
|
}
|
|
2230
2378
|
|
|
2231
2379
|
// src/builder/where/builder.ts
|
|
2380
|
+
var MAX_QUERY_DEPTH = 50;
|
|
2381
|
+
var EMPTY_JOINS = Object.freeze([]);
|
|
2382
|
+
var JSON_OPS = /* @__PURE__ */ new Set([
|
|
2383
|
+
Ops.PATH,
|
|
2384
|
+
Ops.STRING_CONTAINS,
|
|
2385
|
+
Ops.STRING_STARTS_WITH,
|
|
2386
|
+
Ops.STRING_ENDS_WITH
|
|
2387
|
+
]);
|
|
2232
2388
|
var WhereBuilder = class {
|
|
2233
2389
|
build(where, ctx) {
|
|
2234
2390
|
if (!isPlainObject(where)) {
|
|
@@ -2240,8 +2396,6 @@ var WhereBuilder = class {
|
|
|
2240
2396
|
return buildWhereInternal(where, ctx, this);
|
|
2241
2397
|
}
|
|
2242
2398
|
};
|
|
2243
|
-
var MAX_QUERY_DEPTH = 50;
|
|
2244
|
-
var EMPTY_JOINS = Object.freeze([]);
|
|
2245
2399
|
var whereBuilderInstance = new WhereBuilder();
|
|
2246
2400
|
function freezeResult(clause, joins = EMPTY_JOINS) {
|
|
2247
2401
|
return Object.freeze({ clause, joins });
|
|
@@ -2418,16 +2572,8 @@ function buildOperator(expr, op, val, ctx, mode, fieldType) {
|
|
|
2418
2572
|
if (fieldType && isArrayType(fieldType)) {
|
|
2419
2573
|
return buildArrayOperator(expr, op, val, ctx.params, fieldType, ctx.dialect);
|
|
2420
2574
|
}
|
|
2421
|
-
if (fieldType && isJsonType(fieldType)) {
|
|
2422
|
-
|
|
2423
|
-
Ops.PATH,
|
|
2424
|
-
Ops.STRING_CONTAINS,
|
|
2425
|
-
Ops.STRING_STARTS_WITH,
|
|
2426
|
-
Ops.STRING_ENDS_WITH
|
|
2427
|
-
]);
|
|
2428
|
-
if (JSON_OPS.has(op)) {
|
|
2429
|
-
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2430
|
-
}
|
|
2575
|
+
if (fieldType && isJsonType(fieldType) && JSON_OPS.has(op)) {
|
|
2576
|
+
return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
|
|
2431
2577
|
}
|
|
2432
2578
|
return buildScalarOperator(
|
|
2433
2579
|
expr,
|
|
@@ -2448,7 +2594,7 @@ function toSafeSqlIdentifier(input) {
|
|
|
2448
2594
|
const base = startsOk ? cleaned : `_${cleaned}`;
|
|
2449
2595
|
const fallback = base.length > 0 ? base : "_t";
|
|
2450
2596
|
const lowered = fallback.toLowerCase();
|
|
2451
|
-
return
|
|
2597
|
+
return ALIAS_FORBIDDEN_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
|
|
2452
2598
|
}
|
|
2453
2599
|
function createAliasGenerator(maxAliases = 1e4) {
|
|
2454
2600
|
let counter = 0;
|
|
@@ -2658,6 +2804,7 @@ function toPublicResult(clause, joins, params) {
|
|
|
2658
2804
|
// src/builder/where.ts
|
|
2659
2805
|
function buildWhereClause(where, options) {
|
|
2660
2806
|
var _a, _b, _c, _d, _e;
|
|
2807
|
+
assertSafeAlias(options.alias);
|
|
2661
2808
|
const dialect = options.dialect || getGlobalDialect();
|
|
2662
2809
|
const params = (_a = options.params) != null ? _a : createParamStore();
|
|
2663
2810
|
const ctx = {
|
|
@@ -2826,6 +2973,9 @@ function buildRelationSelect(relArgs, relModel, relAlias) {
|
|
|
2826
2973
|
}
|
|
2827
2974
|
|
|
2828
2975
|
// src/builder/select/includes.ts
|
|
2976
|
+
var MAX_INCLUDE_DEPTH = 10;
|
|
2977
|
+
var MAX_TOTAL_SUBQUERIES = 100;
|
|
2978
|
+
var MAX_TOTAL_INCLUDES = 50;
|
|
2829
2979
|
function getRelationTableReference(relModel, dialect) {
|
|
2830
2980
|
return buildTableReference(
|
|
2831
2981
|
SQL_TEMPLATES.PUBLIC_SCHEMA,
|
|
@@ -2871,107 +3021,24 @@ function relationEntriesFromArgs(args, model) {
|
|
|
2871
3021
|
pushFrom(args.select);
|
|
2872
3022
|
return out;
|
|
2873
3023
|
}
|
|
2874
|
-
function assertScalarField(model, fieldName) {
|
|
2875
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
2876
|
-
if (!f) {
|
|
2877
|
-
throw new Error(
|
|
2878
|
-
`orderBy references unknown field '${fieldName}' on model ${model.name}`
|
|
2879
|
-
);
|
|
2880
|
-
}
|
|
2881
|
-
if (f.isRelation) {
|
|
2882
|
-
throw new Error(
|
|
2883
|
-
`orderBy does not support relation field '${fieldName}' on model ${model.name}`
|
|
2884
|
-
);
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
function validateOrderByDirection(fieldName, v) {
|
|
2888
|
-
const s = String(v).toLowerCase();
|
|
2889
|
-
if (s !== "asc" && s !== "desc") {
|
|
2890
|
-
throw new Error(
|
|
2891
|
-
`Invalid orderBy direction for '${fieldName}': ${String(v)}`
|
|
2892
|
-
);
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
function validateOrderByObject(fieldName, v) {
|
|
2896
|
-
if (!("sort" in v)) {
|
|
2897
|
-
throw new Error(
|
|
2898
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2899
|
-
);
|
|
2900
|
-
}
|
|
2901
|
-
validateOrderByDirection(fieldName, v.sort);
|
|
2902
|
-
if ("nulls" in v && isNotNullish(v.nulls)) {
|
|
2903
|
-
const n = String(v.nulls).toLowerCase();
|
|
2904
|
-
if (n !== "first" && n !== "last") {
|
|
2905
|
-
throw new Error(
|
|
2906
|
-
`Invalid orderBy.nulls for '${fieldName}': ${String(v.nulls)}`
|
|
2907
|
-
);
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
const allowed = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
2911
|
-
for (const k of Object.keys(v)) {
|
|
2912
|
-
if (!allowed.has(k)) {
|
|
2913
|
-
throw new Error(`Unsupported orderBy key '${k}' for field '${fieldName}'`);
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
function normalizeOrderByFieldName(name) {
|
|
2918
|
-
const fieldName = String(name).trim();
|
|
2919
|
-
if (fieldName.length === 0) {
|
|
2920
|
-
throw new Error("orderBy field name cannot be empty");
|
|
2921
|
-
}
|
|
2922
|
-
return fieldName;
|
|
2923
|
-
}
|
|
2924
|
-
function requirePlainObjectForOrderByEntry(v) {
|
|
2925
|
-
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
2926
|
-
throw new Error("orderBy array entries must be objects");
|
|
2927
|
-
}
|
|
2928
|
-
return v;
|
|
2929
|
-
}
|
|
2930
|
-
function parseSingleFieldOrderByObject(obj) {
|
|
2931
|
-
const entries = Object.entries(obj);
|
|
2932
|
-
if (entries.length !== 1) {
|
|
2933
|
-
throw new Error("orderBy array entries must have exactly one field");
|
|
2934
|
-
}
|
|
2935
|
-
const fieldName = normalizeOrderByFieldName(entries[0][0]);
|
|
2936
|
-
return [fieldName, entries[0][1]];
|
|
2937
|
-
}
|
|
2938
|
-
function parseOrderByArray(orderBy) {
|
|
2939
|
-
return orderBy.map(
|
|
2940
|
-
(item) => parseSingleFieldOrderByObject(requirePlainObjectForOrderByEntry(item))
|
|
2941
|
-
);
|
|
2942
|
-
}
|
|
2943
|
-
function parseOrderByObject(orderBy) {
|
|
2944
|
-
const out = [];
|
|
2945
|
-
for (const [k, v] of Object.entries(orderBy)) {
|
|
2946
|
-
out.push([normalizeOrderByFieldName(k), v]);
|
|
2947
|
-
}
|
|
2948
|
-
return out;
|
|
2949
|
-
}
|
|
2950
|
-
function getOrderByEntries(orderBy) {
|
|
2951
|
-
if (!isNotNullish(orderBy)) return [];
|
|
2952
|
-
if (Array.isArray(orderBy)) {
|
|
2953
|
-
return parseOrderByArray(orderBy);
|
|
2954
|
-
}
|
|
2955
|
-
if (typeof orderBy === "object" && orderBy !== null) {
|
|
2956
|
-
return parseOrderByObject(orderBy);
|
|
2957
|
-
}
|
|
2958
|
-
throw new Error("orderBy must be an object or array of objects");
|
|
2959
|
-
}
|
|
2960
3024
|
function validateOrderByForModel(model, orderBy) {
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3025
|
+
if (!isNotNullish(orderBy)) return;
|
|
3026
|
+
const scalarSet = getScalarFieldSet(model);
|
|
3027
|
+
const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
3028
|
+
for (const item of normalized) {
|
|
3029
|
+
const entries = Object.entries(item);
|
|
3030
|
+
if (entries.length !== 1) {
|
|
3031
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
2967
3032
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3033
|
+
const fieldName = String(entries[0][0]).trim();
|
|
3034
|
+
if (fieldName.length === 0) {
|
|
3035
|
+
throw new Error("orderBy field name cannot be empty");
|
|
3036
|
+
}
|
|
3037
|
+
if (!scalarSet.has(fieldName)) {
|
|
3038
|
+
throw new Error(
|
|
3039
|
+
`orderBy references unknown or non-scalar field '${fieldName}' on model ${model.name}`
|
|
3040
|
+
);
|
|
2971
3041
|
}
|
|
2972
|
-
throw new Error(
|
|
2973
|
-
`orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
|
|
2974
|
-
);
|
|
2975
3042
|
}
|
|
2976
3043
|
}
|
|
2977
3044
|
function appendLimitOffset(sql, dialect, params, takeVal, skipVal, scope) {
|
|
@@ -3000,7 +3067,10 @@ function readWhereInput(relArgs) {
|
|
|
3000
3067
|
function readOrderByInput(relArgs) {
|
|
3001
3068
|
if (!isPlainObject(relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
3002
3069
|
if (!("orderBy" in relArgs)) return { hasOrderBy: false, orderBy: void 0 };
|
|
3003
|
-
return {
|
|
3070
|
+
return {
|
|
3071
|
+
hasOrderBy: true,
|
|
3072
|
+
orderBy: relArgs.orderBy
|
|
3073
|
+
};
|
|
3004
3074
|
}
|
|
3005
3075
|
function extractRelationPaginationConfig(relArgs) {
|
|
3006
3076
|
const { hasOrderBy, orderBy: rawOrderByInput } = readOrderByInput(relArgs);
|
|
@@ -3030,36 +3100,28 @@ function maybeReverseNegativeTake(takeVal, hasOrderBy, orderByInput) {
|
|
|
3030
3100
|
orderByInput: reverseOrderByInput(orderByInput)
|
|
3031
3101
|
};
|
|
3032
3102
|
}
|
|
3033
|
-
function
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
);
|
|
3038
|
-
}
|
|
3039
|
-
function modelHasScalarId(relModel) {
|
|
3040
|
-
const idField = relModel.fields.find((f) => f.name === "id");
|
|
3041
|
-
return Boolean(idField && !idField.isRelation);
|
|
3042
|
-
}
|
|
3043
|
-
function addIdTiebreaker(orderByInput) {
|
|
3044
|
-
if (Array.isArray(orderByInput)) return [...orderByInput, { id: "asc" }];
|
|
3045
|
-
return [orderByInput, { id: "asc" }];
|
|
3046
|
-
}
|
|
3047
|
-
function ensureDeterministicOrderBy(relModel, hasOrderBy, orderByInput, hasPagination) {
|
|
3048
|
-
if (!hasPagination) {
|
|
3049
|
-
if (hasOrderBy && isNotNullish(orderByInput)) {
|
|
3050
|
-
validateOrderByForModel(relModel, orderByInput);
|
|
3103
|
+
function ensureDeterministicOrderByForInclude(args) {
|
|
3104
|
+
if (!args.hasPagination) {
|
|
3105
|
+
if (args.hasOrderBy && isNotNullish(args.orderByInput)) {
|
|
3106
|
+
validateOrderByForModel(args.relModel, args.orderByInput);
|
|
3051
3107
|
}
|
|
3052
|
-
return orderByInput;
|
|
3108
|
+
return args.orderByInput;
|
|
3053
3109
|
}
|
|
3054
|
-
if (!hasOrderBy) {
|
|
3055
|
-
return
|
|
3110
|
+
if (!args.hasOrderBy) {
|
|
3111
|
+
return ensureDeterministicOrderByInput({
|
|
3112
|
+
orderBy: void 0,
|
|
3113
|
+
model: args.relModel,
|
|
3114
|
+
parseValue: parseOrderByValue
|
|
3115
|
+
});
|
|
3056
3116
|
}
|
|
3057
|
-
if (isNotNullish(orderByInput)) {
|
|
3058
|
-
validateOrderByForModel(relModel, orderByInput);
|
|
3117
|
+
if (isNotNullish(args.orderByInput)) {
|
|
3118
|
+
validateOrderByForModel(args.relModel, args.orderByInput);
|
|
3059
3119
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3120
|
+
return ensureDeterministicOrderByInput({
|
|
3121
|
+
orderBy: args.orderByInput,
|
|
3122
|
+
model: args.relModel,
|
|
3123
|
+
parseValue: parseOrderByValue
|
|
3124
|
+
});
|
|
3063
3125
|
}
|
|
3064
3126
|
function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
3065
3127
|
let relSelect = buildRelationSelect(relArgs, relModel, relAlias);
|
|
@@ -3070,7 +3132,10 @@ function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
|
3070
3132
|
relAlias,
|
|
3071
3133
|
ctx.aliasGen,
|
|
3072
3134
|
ctx.params,
|
|
3073
|
-
ctx.dialect
|
|
3135
|
+
ctx.dialect,
|
|
3136
|
+
ctx.visitPath || [],
|
|
3137
|
+
(ctx.depth || 0) + 1,
|
|
3138
|
+
ctx.stats
|
|
3074
3139
|
) : [];
|
|
3075
3140
|
if (isNonEmptyArray(nestedIncludes)) {
|
|
3076
3141
|
const emptyJson = ctx.dialect === "postgres" ? `'[]'::json` : `json('[]')`;
|
|
@@ -3220,12 +3285,12 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3220
3285
|
paginationConfig.orderBy
|
|
3221
3286
|
);
|
|
3222
3287
|
const hasPagination = paginationConfig.hasSkip || paginationConfig.hasTake;
|
|
3223
|
-
const finalOrderByInput =
|
|
3288
|
+
const finalOrderByInput = ensureDeterministicOrderByForInclude({
|
|
3224
3289
|
relModel,
|
|
3225
|
-
paginationConfig.hasOrderBy,
|
|
3226
|
-
adjusted.orderByInput,
|
|
3290
|
+
hasOrderBy: paginationConfig.hasOrderBy,
|
|
3291
|
+
orderByInput: adjusted.orderByInput,
|
|
3227
3292
|
hasPagination
|
|
3228
|
-
);
|
|
3293
|
+
});
|
|
3229
3294
|
const orderBySql = buildOrderBySql(
|
|
3230
3295
|
finalOrderByInput,
|
|
3231
3296
|
relAlias,
|
|
@@ -3266,12 +3331,48 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3266
3331
|
ctx
|
|
3267
3332
|
});
|
|
3268
3333
|
}
|
|
3269
|
-
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect) {
|
|
3334
|
+
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect, visitPath = [], depth = 0, stats) {
|
|
3335
|
+
if (!stats) {
|
|
3336
|
+
stats = { totalIncludes: 0, totalSubqueries: 0, maxDepth: 0 };
|
|
3337
|
+
}
|
|
3338
|
+
if (depth > MAX_INCLUDE_DEPTH) {
|
|
3339
|
+
throw new Error(
|
|
3340
|
+
`Maximum include depth of ${MAX_INCLUDE_DEPTH} exceeded. Path: ${visitPath.join(" -> ")}. Deep includes cause exponential SQL complexity and performance issues.`
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
stats.maxDepth = Math.max(stats.maxDepth, depth);
|
|
3270
3344
|
const includes = [];
|
|
3271
3345
|
const entries = relationEntriesFromArgs(args, model);
|
|
3272
3346
|
for (const [relName, relArgs] of entries) {
|
|
3273
3347
|
if (relArgs === false) continue;
|
|
3348
|
+
stats.totalIncludes++;
|
|
3349
|
+
if (stats.totalIncludes > MAX_TOTAL_INCLUDES) {
|
|
3350
|
+
throw new Error(
|
|
3351
|
+
`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.`
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
stats.totalSubqueries++;
|
|
3355
|
+
if (stats.totalSubqueries > MAX_TOTAL_SUBQUERIES) {
|
|
3356
|
+
throw new Error(
|
|
3357
|
+
`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.`
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3274
3360
|
const resolved = resolveRelationOrThrow(model, schemas, relName);
|
|
3361
|
+
const relationPath = `${model.name}.${relName}`;
|
|
3362
|
+
const currentPath = [...visitPath, relationPath];
|
|
3363
|
+
if (visitPath.includes(relationPath)) {
|
|
3364
|
+
throw new Error(
|
|
3365
|
+
`Circular include detected: ${currentPath.join(" -> ")}. Relation '${relationPath}' creates an infinite loop.`
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
const modelOccurrences = currentPath.filter(
|
|
3369
|
+
(p) => p.startsWith(`${resolved.relModel.name}.`)
|
|
3370
|
+
).length;
|
|
3371
|
+
if (modelOccurrences > 2) {
|
|
3372
|
+
throw new Error(
|
|
3373
|
+
`Include too deeply nested: model '${resolved.relModel.name}' appears ${modelOccurrences} times in path: ${currentPath.join(" -> ")}`
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3275
3376
|
const include = buildSingleInclude(
|
|
3276
3377
|
relName,
|
|
3277
3378
|
relArgs,
|
|
@@ -3283,7 +3384,10 @@ function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, pa
|
|
|
3283
3384
|
parentAlias,
|
|
3284
3385
|
aliasGen,
|
|
3285
3386
|
dialect,
|
|
3286
|
-
params
|
|
3387
|
+
params,
|
|
3388
|
+
visitPath: currentPath,
|
|
3389
|
+
depth: depth + 1,
|
|
3390
|
+
stats
|
|
3287
3391
|
}
|
|
3288
3392
|
);
|
|
3289
3393
|
includes.push(include);
|
|
@@ -3292,6 +3396,11 @@ function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, pa
|
|
|
3292
3396
|
}
|
|
3293
3397
|
function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
3294
3398
|
const aliasGen = createAliasGenerator();
|
|
3399
|
+
const stats = {
|
|
3400
|
+
totalIncludes: 0,
|
|
3401
|
+
totalSubqueries: 0,
|
|
3402
|
+
maxDepth: 0
|
|
3403
|
+
};
|
|
3295
3404
|
return buildIncludeSqlInternal(
|
|
3296
3405
|
args,
|
|
3297
3406
|
model,
|
|
@@ -3299,7 +3408,10 @@ function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
|
3299
3408
|
parentAlias,
|
|
3300
3409
|
aliasGen,
|
|
3301
3410
|
params,
|
|
3302
|
-
dialect
|
|
3411
|
+
dialect,
|
|
3412
|
+
[],
|
|
3413
|
+
0,
|
|
3414
|
+
stats
|
|
3303
3415
|
);
|
|
3304
3416
|
}
|
|
3305
3417
|
function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
@@ -3315,6 +3427,11 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3315
3427
|
`_count.${relName} references unknown relation on model ${model.name}`
|
|
3316
3428
|
);
|
|
3317
3429
|
}
|
|
3430
|
+
if (!isValidRelationField(field)) {
|
|
3431
|
+
throw new Error(
|
|
3432
|
+
`_count.${relName} has invalid relation metadata on model ${model.name}`
|
|
3433
|
+
);
|
|
3434
|
+
}
|
|
3318
3435
|
const relModel = schemas.find((m) => m.name === field.relatedModel);
|
|
3319
3436
|
if (!relModel) {
|
|
3320
3437
|
throw new Error(
|
|
@@ -3323,31 +3440,81 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3323
3440
|
}
|
|
3324
3441
|
return { field, relModel };
|
|
3325
3442
|
}
|
|
3326
|
-
function
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3443
|
+
function defaultReferencesForCount(fkCount) {
|
|
3444
|
+
if (fkCount === 1) return ["id"];
|
|
3445
|
+
throw new Error(
|
|
3446
|
+
"Relation count for composite keys requires explicit references matching foreignKey length"
|
|
3447
|
+
);
|
|
3330
3448
|
}
|
|
3331
|
-
function
|
|
3449
|
+
function resolveCountKeyPairs(field) {
|
|
3332
3450
|
const fkFields = normalizeKeyList(field.foreignKey);
|
|
3333
|
-
|
|
3334
|
-
|
|
3451
|
+
if (fkFields.length === 0) {
|
|
3452
|
+
throw new Error("Relation count requires foreignKey");
|
|
3453
|
+
}
|
|
3454
|
+
const refsRaw = field.references;
|
|
3455
|
+
const refs = normalizeKeyList(refsRaw);
|
|
3456
|
+
const refFields = refs.length > 0 ? refs : defaultReferencesForCount(fkFields.length);
|
|
3457
|
+
if (refFields.length !== fkFields.length) {
|
|
3458
|
+
throw new Error(
|
|
3459
|
+
"Relation count requires references count to match foreignKey count"
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
const relKeyFields = field.isForeignKeyLocal ? refFields : fkFields;
|
|
3463
|
+
const parentKeyFields = field.isForeignKeyLocal ? fkFields : refFields;
|
|
3464
|
+
return { relKeyFields, parentKeyFields };
|
|
3465
|
+
}
|
|
3466
|
+
function aliasQualifiedColumn(alias, model, field) {
|
|
3467
|
+
return `${alias}.${quoteColumn(model, field)}`;
|
|
3468
|
+
}
|
|
3469
|
+
function subqueryForCount(args) {
|
|
3470
|
+
const selectKeys = args.relKeyFields.map(
|
|
3471
|
+
(f, i) => `${aliasQualifiedColumn(args.countAlias, args.relModel, f)} AS "__fk${i}"`
|
|
3472
|
+
).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3473
|
+
const groupByKeys = args.relKeyFields.map((f) => aliasQualifiedColumn(args.countAlias, args.relModel, f)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3474
|
+
const cntExpr = args.dialect === "postgres" ? "COUNT(*)::int AS __cnt" : "COUNT(*) AS __cnt";
|
|
3475
|
+
return `(SELECT ${selectKeys}${SQL_SEPARATORS.FIELD_LIST}${cntExpr} FROM ${args.relTable} ${args.countAlias} GROUP BY ${groupByKeys})`;
|
|
3476
|
+
}
|
|
3477
|
+
function leftJoinOnForCount(args) {
|
|
3478
|
+
const parts = args.parentKeyFields.map(
|
|
3479
|
+
(f, i) => `${args.joinAlias}."__fk${i}" = ${aliasQualifiedColumn(args.parentAlias, args.parentModel, f)}`
|
|
3480
|
+
);
|
|
3481
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
3335
3482
|
}
|
|
3336
|
-
function
|
|
3337
|
-
|
|
3483
|
+
function nextAliasAvoiding(aliasGen, base, forbidden) {
|
|
3484
|
+
let a = aliasGen.next(base);
|
|
3485
|
+
while (forbidden.has(a)) {
|
|
3486
|
+
a = aliasGen.next(base);
|
|
3487
|
+
}
|
|
3488
|
+
return a;
|
|
3338
3489
|
}
|
|
3339
3490
|
function buildCountJoinAndPair(args) {
|
|
3340
3491
|
const relTable = getRelationTableReference(args.relModel, args.dialect);
|
|
3341
|
-
const
|
|
3342
|
-
const
|
|
3343
|
-
const
|
|
3344
|
-
args.
|
|
3492
|
+
const { relKeyFields, parentKeyFields } = resolveCountKeyPairs(args.field);
|
|
3493
|
+
const forbidden = /* @__PURE__ */ new Set([args.parentAlias]);
|
|
3494
|
+
const countAlias = nextAliasAvoiding(
|
|
3495
|
+
args.aliasGen,
|
|
3496
|
+
`__tp_cnt_${args.relName}`,
|
|
3497
|
+
forbidden
|
|
3498
|
+
);
|
|
3499
|
+
forbidden.add(countAlias);
|
|
3500
|
+
const subquery = subqueryForCount({
|
|
3501
|
+
dialect: args.dialect,
|
|
3345
3502
|
relTable,
|
|
3346
3503
|
countAlias,
|
|
3347
|
-
|
|
3504
|
+
relModel: args.relModel,
|
|
3505
|
+
relKeyFields
|
|
3506
|
+
});
|
|
3507
|
+
const joinAlias = nextAliasAvoiding(
|
|
3508
|
+
args.aliasGen,
|
|
3509
|
+
`__tp_cnt_j_${args.relName}`,
|
|
3510
|
+
forbidden
|
|
3348
3511
|
);
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3512
|
+
const leftJoinOn = leftJoinOnForCount({
|
|
3513
|
+
joinAlias,
|
|
3514
|
+
parentAlias: args.parentAlias,
|
|
3515
|
+
parentModel: args.parentModel,
|
|
3516
|
+
parentKeyFields
|
|
3517
|
+
});
|
|
3351
3518
|
return {
|
|
3352
3519
|
joinSql: `LEFT JOIN ${subquery} ${joinAlias} ON ${leftJoinOn}`,
|
|
3353
3520
|
pairSql: `${sqlStringLiteral(args.relName)}, COALESCE(${joinAlias}.__cnt, 0)`
|
|
@@ -3356,6 +3523,7 @@ function buildCountJoinAndPair(args) {
|
|
|
3356
3523
|
function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params, dialect) {
|
|
3357
3524
|
const joins = [];
|
|
3358
3525
|
const pairs = [];
|
|
3526
|
+
const aliasGen = createAliasGenerator();
|
|
3359
3527
|
for (const [relName, shouldCount] of Object.entries(countSelect)) {
|
|
3360
3528
|
if (!shouldCount) continue;
|
|
3361
3529
|
const resolved = resolveCountRelationOrThrow(relName, model, schemas);
|
|
@@ -3363,8 +3531,10 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3363
3531
|
relName,
|
|
3364
3532
|
field: resolved.field,
|
|
3365
3533
|
relModel: resolved.relModel,
|
|
3534
|
+
parentModel: model,
|
|
3366
3535
|
parentAlias,
|
|
3367
|
-
dialect
|
|
3536
|
+
dialect,
|
|
3537
|
+
aliasGen
|
|
3368
3538
|
});
|
|
3369
3539
|
joins.push(built.joinSql);
|
|
3370
3540
|
pairs.push(built.pairSql);
|
|
@@ -3376,13 +3546,21 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3376
3546
|
}
|
|
3377
3547
|
|
|
3378
3548
|
// src/builder/select/assembly.ts
|
|
3379
|
-
var
|
|
3549
|
+
var ALIAS_CAPTURE = "([A-Za-z_][A-Za-z0-9_]*)";
|
|
3550
|
+
var COLUMN_PART = '(?:"([^"]+)"|([a-z_][a-z0-9_]*))';
|
|
3551
|
+
var AS_PART = `(?:\\s+AS\\s+${COLUMN_PART})?`;
|
|
3552
|
+
var SIMPLE_COLUMN_PATTERN = `^${ALIAS_CAPTURE}\\.${COLUMN_PART}${AS_PART}$`;
|
|
3553
|
+
var SIMPLE_COLUMN_RE = new RegExp(SIMPLE_COLUMN_PATTERN, "i");
|
|
3380
3554
|
function joinNonEmpty(parts, sep) {
|
|
3381
3555
|
return parts.filter((s) => s.trim().length > 0).join(sep);
|
|
3382
3556
|
}
|
|
3383
3557
|
function buildWhereSql(conditions) {
|
|
3384
3558
|
if (!isNonEmptyArray(conditions)) return "";
|
|
3385
|
-
|
|
3559
|
+
const parts = [
|
|
3560
|
+
SQL_TEMPLATES.WHERE,
|
|
3561
|
+
conditions.join(SQL_SEPARATORS.CONDITION_AND)
|
|
3562
|
+
];
|
|
3563
|
+
return ` ${parts.join(" ")}`;
|
|
3386
3564
|
}
|
|
3387
3565
|
function buildJoinsSql(...joinGroups) {
|
|
3388
3566
|
const all = [];
|
|
@@ -3397,37 +3575,39 @@ function buildSelectList(baseSelect, extraCols) {
|
|
|
3397
3575
|
if (base && extra) return `${base}${SQL_SEPARATORS.FIELD_LIST}${extra}`;
|
|
3398
3576
|
return base || extra;
|
|
3399
3577
|
}
|
|
3400
|
-
function finalizeSql(sql, params) {
|
|
3578
|
+
function finalizeSql(sql, params, dialect) {
|
|
3401
3579
|
const snapshot = params.snapshot();
|
|
3402
3580
|
validateSelectQuery(sql);
|
|
3403
|
-
|
|
3581
|
+
validateParamConsistencyByDialect(sql, snapshot.params, dialect);
|
|
3404
3582
|
return Object.freeze({
|
|
3405
3583
|
sql,
|
|
3406
3584
|
params: snapshot.params,
|
|
3407
|
-
paramMappings: snapshot.mappings
|
|
3585
|
+
paramMappings: Object.freeze(snapshot.mappings)
|
|
3408
3586
|
});
|
|
3409
3587
|
}
|
|
3410
|
-
function parseSimpleScalarSelect(select,
|
|
3411
|
-
var _a, _b;
|
|
3588
|
+
function parseSimpleScalarSelect(select, fromAlias) {
|
|
3589
|
+
var _a, _b, _c, _d;
|
|
3412
3590
|
const raw = select.trim();
|
|
3413
3591
|
if (raw.length === 0) return [];
|
|
3414
|
-
let re = SIMPLE_SELECT_RE_CACHE.get(alias);
|
|
3415
|
-
if (!re) {
|
|
3416
|
-
const safeAlias2 = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3417
|
-
re = new RegExp(`^${safeAlias2}\\.(?:"([^"]+)"|([a-z_][a-z0-9_]*))$`, "i");
|
|
3418
|
-
SIMPLE_SELECT_RE_CACHE.set(alias, re);
|
|
3419
|
-
}
|
|
3420
3592
|
const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
|
|
3421
3593
|
const names = [];
|
|
3422
3594
|
for (const part of parts) {
|
|
3423
3595
|
const p = part.trim();
|
|
3424
|
-
const m = p.match(
|
|
3596
|
+
const m = p.match(SIMPLE_COLUMN_RE);
|
|
3425
3597
|
if (!m) {
|
|
3426
3598
|
throw new Error(
|
|
3427
|
-
`sqlite distinct emulation requires scalar select fields to be simple columns. Got: ${p}`
|
|
3599
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (optionally with AS). Got: ${p}`
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
const actualAlias = m[1];
|
|
3603
|
+
if (actualAlias.toLowerCase() !== fromAlias.toLowerCase()) {
|
|
3604
|
+
throw new Error(
|
|
3605
|
+
`Expected alias '${fromAlias}', got '${actualAlias}' in: ${p}`
|
|
3428
3606
|
);
|
|
3429
3607
|
}
|
|
3430
|
-
const
|
|
3608
|
+
const columnName = ((_b = (_a = m[2]) != null ? _a : m[3]) != null ? _b : "").trim();
|
|
3609
|
+
const outAlias = ((_d = (_c = m[4]) != null ? _c : m[5]) != null ? _d : "").trim();
|
|
3610
|
+
const name = outAlias.length > 0 ? outAlias : columnName;
|
|
3431
3611
|
if (name.length === 0) {
|
|
3432
3612
|
throw new Error(`Failed to parse selected column name from: ${p}`);
|
|
3433
3613
|
}
|
|
@@ -3436,18 +3616,18 @@ function parseSimpleScalarSelect(select, alias) {
|
|
|
3436
3616
|
return names;
|
|
3437
3617
|
}
|
|
3438
3618
|
function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
|
|
3439
|
-
const
|
|
3440
|
-
|
|
3441
|
-
|
|
3619
|
+
const src = String(fromAlias);
|
|
3620
|
+
if (src.length === 0) return orderBy;
|
|
3621
|
+
const escaped = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3622
|
+
const re = new RegExp(`\\b${escaped}\\.`, "gi");
|
|
3623
|
+
return orderBy.replace(re, `${outerAlias}.`);
|
|
3442
3624
|
}
|
|
3443
3625
|
function buildDistinctColumns(distinct, fromAlias, model) {
|
|
3444
3626
|
return distinct.map((f) => col(fromAlias, f, model)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3445
3627
|
}
|
|
3446
3628
|
function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
3447
3629
|
const outputCols = [...scalarNames, ...includeNames];
|
|
3448
|
-
if (hasCount)
|
|
3449
|
-
outputCols.push("_count");
|
|
3450
|
-
}
|
|
3630
|
+
if (hasCount) outputCols.push("_count");
|
|
3451
3631
|
const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3452
3632
|
if (!isNonEmptyString(formatted)) {
|
|
3453
3633
|
throw new Error("distinct emulation requires at least one output column");
|
|
@@ -3456,9 +3636,10 @@ function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
|
3456
3636
|
}
|
|
3457
3637
|
function buildWindowOrder(args) {
|
|
3458
3638
|
const { baseOrder, idField, fromAlias, model } = args;
|
|
3639
|
+
const fromLower = String(fromAlias).toLowerCase();
|
|
3459
3640
|
const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
|
|
3460
3641
|
const hasIdInOrder = orderFields.some(
|
|
3461
|
-
(f) => f.startsWith(`${
|
|
3642
|
+
(f) => f.startsWith(`${fromLower}.id `) || f.startsWith(`${fromLower}."id" `)
|
|
3462
3643
|
);
|
|
3463
3644
|
if (hasIdInOrder) return baseOrder;
|
|
3464
3645
|
const idTiebreaker = idField ? `, ${col(fromAlias, "id", model)} ASC` : "";
|
|
@@ -3493,15 +3674,37 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3493
3674
|
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, `"__tp_distinct"`) : replaceOrderByAlias(fallbackOrder, from.alias, `"__tp_distinct"`);
|
|
3494
3675
|
const joins = buildJoinsSql(whereJoins, countJoins);
|
|
3495
3676
|
const conditions = [];
|
|
3496
|
-
if (whereClause && whereClause !== "1=1")
|
|
3497
|
-
conditions.push(whereClause);
|
|
3498
|
-
}
|
|
3677
|
+
if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
|
|
3499
3678
|
const whereSql = buildWhereSql(conditions);
|
|
3500
3679
|
const innerSelectList = selectWithIncludes.trim();
|
|
3501
3680
|
const innerComma = innerSelectList.length > 0 ? SQL_SEPARATORS.FIELD_LIST : "";
|
|
3502
|
-
const
|
|
3503
|
-
|
|
3504
|
-
|
|
3681
|
+
const innerParts = [
|
|
3682
|
+
SQL_TEMPLATES.SELECT,
|
|
3683
|
+
innerSelectList + innerComma,
|
|
3684
|
+
`ROW_NUMBER() OVER (PARTITION BY ${distinctCols} ORDER BY ${windowOrder})`,
|
|
3685
|
+
SQL_TEMPLATES.AS,
|
|
3686
|
+
'"__tp_rn"',
|
|
3687
|
+
SQL_TEMPLATES.FROM,
|
|
3688
|
+
from.table,
|
|
3689
|
+
from.alias
|
|
3690
|
+
];
|
|
3691
|
+
if (joins) innerParts.push(joins);
|
|
3692
|
+
if (whereSql) innerParts.push(whereSql);
|
|
3693
|
+
const inner = innerParts.filter(Boolean).join(" ");
|
|
3694
|
+
const outerParts = [
|
|
3695
|
+
SQL_TEMPLATES.SELECT,
|
|
3696
|
+
outerSelectCols,
|
|
3697
|
+
SQL_TEMPLATES.FROM,
|
|
3698
|
+
`(${inner})`,
|
|
3699
|
+
SQL_TEMPLATES.AS,
|
|
3700
|
+
'"__tp_distinct"',
|
|
3701
|
+
SQL_TEMPLATES.WHERE,
|
|
3702
|
+
'"__tp_rn" = 1'
|
|
3703
|
+
];
|
|
3704
|
+
if (isNonEmptyString(outerOrder)) {
|
|
3705
|
+
outerParts.push(SQL_TEMPLATES.ORDER_BY, outerOrder);
|
|
3706
|
+
}
|
|
3707
|
+
return outerParts.filter(Boolean).join(" ");
|
|
3505
3708
|
}
|
|
3506
3709
|
function buildIncludeColumns(spec) {
|
|
3507
3710
|
var _a, _b;
|
|
@@ -3629,6 +3832,7 @@ function constructFinalSql(spec) {
|
|
|
3629
3832
|
orderBy,
|
|
3630
3833
|
distinct,
|
|
3631
3834
|
method,
|
|
3835
|
+
cursorCte,
|
|
3632
3836
|
cursorClause,
|
|
3633
3837
|
params,
|
|
3634
3838
|
dialect,
|
|
@@ -3643,9 +3847,13 @@ function constructFinalSql(spec) {
|
|
|
3643
3847
|
const spec2 = withCountJoins(spec, countJoins, whereJoins);
|
|
3644
3848
|
let sql2 = buildSqliteDistinctQuery(spec2, selectWithIncludes).trim();
|
|
3645
3849
|
sql2 = appendPagination(sql2, spec);
|
|
3646
|
-
return finalizeSql(sql2, params);
|
|
3850
|
+
return finalizeSql(sql2, params, dialect);
|
|
3647
3851
|
}
|
|
3648
|
-
const parts = [
|
|
3852
|
+
const parts = [];
|
|
3853
|
+
if (cursorCte) {
|
|
3854
|
+
parts.push(`WITH ${cursorCte}`);
|
|
3855
|
+
}
|
|
3856
|
+
parts.push(SQL_TEMPLATES.SELECT);
|
|
3649
3857
|
const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct, model) : null;
|
|
3650
3858
|
if (distinctOn) parts.push(distinctOn);
|
|
3651
3859
|
const baseSelect = (select != null ? select : "").trim();
|
|
@@ -3661,7 +3869,41 @@ function constructFinalSql(spec) {
|
|
|
3661
3869
|
if (isNonEmptyString(orderBy)) parts.push(SQL_TEMPLATES.ORDER_BY, orderBy);
|
|
3662
3870
|
let sql = parts.join(" ").trim();
|
|
3663
3871
|
sql = appendPagination(sql, spec);
|
|
3664
|
-
return finalizeSql(sql, params);
|
|
3872
|
+
return finalizeSql(sql, params, dialect);
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
// src/builder/shared/validators/field-assertions.ts
|
|
3876
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Decimal", "BigInt"]);
|
|
3877
|
+
function assertScalarField(model, fieldName, context) {
|
|
3878
|
+
const field = getFieldInfo(model, fieldName);
|
|
3879
|
+
if (!field) {
|
|
3880
|
+
throw createError(
|
|
3881
|
+
`${context} references unknown field '${fieldName}' on model ${model.name}`,
|
|
3882
|
+
{
|
|
3883
|
+
field: fieldName,
|
|
3884
|
+
modelName: model.name,
|
|
3885
|
+
availableFields: model.fields.map((f) => f.name)
|
|
3886
|
+
}
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3889
|
+
if (field.isRelation) {
|
|
3890
|
+
throw createError(
|
|
3891
|
+
`${context} does not support relation field '${fieldName}'`,
|
|
3892
|
+
{ field: fieldName, modelName: model.name }
|
|
3893
|
+
);
|
|
3894
|
+
}
|
|
3895
|
+
return field;
|
|
3896
|
+
}
|
|
3897
|
+
function assertNumericField(model, fieldName, context) {
|
|
3898
|
+
const field = assertScalarField(model, fieldName, context);
|
|
3899
|
+
const baseType = field.type.replace(/\[\]|\?/g, "");
|
|
3900
|
+
if (!NUMERIC_TYPES.has(baseType)) {
|
|
3901
|
+
throw createError(
|
|
3902
|
+
`${context} requires numeric field, got '${field.type}'`,
|
|
3903
|
+
{ field: fieldName, modelName: model.name }
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
return field;
|
|
3665
3907
|
}
|
|
3666
3908
|
|
|
3667
3909
|
// src/builder/select.ts
|
|
@@ -3694,7 +3936,7 @@ function buildPostgresDistinctOrderBy(distinctFields, existing) {
|
|
|
3694
3936
|
}
|
|
3695
3937
|
return next;
|
|
3696
3938
|
}
|
|
3697
|
-
function applyPostgresDistinctOrderBy(args
|
|
3939
|
+
function applyPostgresDistinctOrderBy(args) {
|
|
3698
3940
|
const distinctFields = normalizeDistinctFields(args.distinct);
|
|
3699
3941
|
if (distinctFields.length === 0) return args;
|
|
3700
3942
|
if (!isNotNullish(args.orderBy)) return args;
|
|
@@ -3704,19 +3946,6 @@ function applyPostgresDistinctOrderBy(args, _model) {
|
|
|
3704
3946
|
orderBy: buildPostgresDistinctOrderBy(distinctFields, existing)
|
|
3705
3947
|
});
|
|
3706
3948
|
}
|
|
3707
|
-
function assertScalarFieldOnModel(model, fieldName, ctx) {
|
|
3708
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
3709
|
-
if (!f) {
|
|
3710
|
-
throw new Error(
|
|
3711
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}`
|
|
3712
|
-
);
|
|
3713
|
-
}
|
|
3714
|
-
if (f.isRelation) {
|
|
3715
|
-
throw new Error(
|
|
3716
|
-
`${ctx} does not support relation field '${fieldName}' on model ${model.name}`
|
|
3717
|
-
);
|
|
3718
|
-
}
|
|
3719
|
-
}
|
|
3720
3949
|
function validateDistinct(model, distinct) {
|
|
3721
3950
|
if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) return;
|
|
3722
3951
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3727,24 +3956,24 @@ function validateDistinct(model, distinct) {
|
|
|
3727
3956
|
throw new Error(`distinct must not contain duplicates (field: '${f}')`);
|
|
3728
3957
|
}
|
|
3729
3958
|
seen.add(f);
|
|
3730
|
-
|
|
3959
|
+
assertScalarField(model, f, "distinct");
|
|
3731
3960
|
}
|
|
3732
3961
|
}
|
|
3733
|
-
function validateOrderByValue(fieldName, v) {
|
|
3734
|
-
parseOrderByValue(v, fieldName);
|
|
3735
|
-
}
|
|
3736
3962
|
function validateOrderBy(model, orderBy) {
|
|
3737
3963
|
if (!isNotNullish(orderBy)) return;
|
|
3738
3964
|
const items = normalizeOrderByInput2(orderBy);
|
|
3739
3965
|
if (items.length === 0) return;
|
|
3740
3966
|
for (const it of items) {
|
|
3741
3967
|
const entries = Object.entries(it);
|
|
3968
|
+
if (entries.length !== 1) {
|
|
3969
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
3970
|
+
}
|
|
3742
3971
|
const fieldName = String(entries[0][0]).trim();
|
|
3743
3972
|
if (fieldName.length === 0) {
|
|
3744
3973
|
throw new Error("orderBy field name cannot be empty");
|
|
3745
3974
|
}
|
|
3746
|
-
|
|
3747
|
-
|
|
3975
|
+
assertScalarField(model, fieldName, "orderBy");
|
|
3976
|
+
parseOrderByValue(entries[0][1], fieldName);
|
|
3748
3977
|
}
|
|
3749
3978
|
}
|
|
3750
3979
|
function validateCursor(model, cursor) {
|
|
@@ -3761,7 +3990,7 @@ function validateCursor(model, cursor) {
|
|
|
3761
3990
|
if (f.length === 0) {
|
|
3762
3991
|
throw new Error("cursor field name cannot be empty");
|
|
3763
3992
|
}
|
|
3764
|
-
|
|
3993
|
+
assertScalarField(model, f, "cursor");
|
|
3765
3994
|
}
|
|
3766
3995
|
}
|
|
3767
3996
|
function resolveDialect(dialect) {
|
|
@@ -3780,20 +4009,21 @@ function normalizeArgsForNegativeTake(method, args) {
|
|
|
3780
4009
|
orderBy: reverseOrderByInput(args.orderBy)
|
|
3781
4010
|
});
|
|
3782
4011
|
}
|
|
3783
|
-
function normalizeArgsForDialect(dialect, args
|
|
4012
|
+
function normalizeArgsForDialect(dialect, args) {
|
|
3784
4013
|
if (dialect !== "postgres") return args;
|
|
3785
4014
|
return applyPostgresDistinctOrderBy(args);
|
|
3786
4015
|
}
|
|
3787
4016
|
function buildCursorClauseIfAny(input) {
|
|
3788
|
-
const { cursor, orderBy, tableName, alias, params, dialect } = input;
|
|
3789
|
-
if (!isNotNullish(cursor)) return
|
|
4017
|
+
const { cursor, orderBy, tableName, alias, params, dialect, model } = input;
|
|
4018
|
+
if (!isNotNullish(cursor)) return {};
|
|
3790
4019
|
return buildCursorCondition(
|
|
3791
4020
|
cursor,
|
|
3792
4021
|
orderBy,
|
|
3793
4022
|
tableName,
|
|
3794
4023
|
alias,
|
|
3795
4024
|
params,
|
|
3796
|
-
dialect
|
|
4025
|
+
dialect,
|
|
4026
|
+
model
|
|
3797
4027
|
);
|
|
3798
4028
|
}
|
|
3799
4029
|
function buildSelectSpec(input) {
|
|
@@ -3832,14 +4062,20 @@ function buildSelectSpec(input) {
|
|
|
3832
4062
|
params,
|
|
3833
4063
|
dialect
|
|
3834
4064
|
);
|
|
3835
|
-
const
|
|
4065
|
+
const cursorResult = buildCursorClauseIfAny({
|
|
3836
4066
|
cursor,
|
|
3837
4067
|
orderBy: normalizedArgs.orderBy,
|
|
3838
4068
|
tableName,
|
|
3839
4069
|
alias,
|
|
3840
4070
|
params,
|
|
3841
|
-
dialect
|
|
4071
|
+
dialect,
|
|
4072
|
+
model
|
|
3842
4073
|
});
|
|
4074
|
+
if (dialect === "sqlite" && isNonEmptyArray(normalizedArgs.distinct) && cursorResult.condition) {
|
|
4075
|
+
throw new Error(
|
|
4076
|
+
"Cursor pagination with distinct is not supported in SQLite due to window function limitations. Use findMany with skip/take instead, or remove distinct."
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
3843
4079
|
return {
|
|
3844
4080
|
select: selectFields,
|
|
3845
4081
|
includes,
|
|
@@ -3850,7 +4086,8 @@ function buildSelectSpec(input) {
|
|
|
3850
4086
|
pagination: { take, skip },
|
|
3851
4087
|
distinct: normalizedArgs.distinct,
|
|
3852
4088
|
method,
|
|
3853
|
-
|
|
4089
|
+
cursorCte: cursorResult.cte,
|
|
4090
|
+
cursorClause: cursorResult.condition,
|
|
3854
4091
|
params,
|
|
3855
4092
|
dialect,
|
|
3856
4093
|
model,
|
|
@@ -3864,9 +4101,7 @@ function buildSelectSql(input) {
|
|
|
3864
4101
|
assertSafeTableRef(from.tableName);
|
|
3865
4102
|
const dialectToUse = resolveDialect(dialect);
|
|
3866
4103
|
const argsForSql = normalizeArgsForNegativeTake(method, args);
|
|
3867
|
-
const normalizedArgs = normalizeArgsForDialect(
|
|
3868
|
-
dialectToUse,
|
|
3869
|
-
argsForSql);
|
|
4104
|
+
const normalizedArgs = normalizeArgsForDialect(dialectToUse, argsForSql);
|
|
3870
4105
|
validateDistinct(model, normalizedArgs.distinct);
|
|
3871
4106
|
validateOrderBy(model, normalizedArgs.orderBy);
|
|
3872
4107
|
validateCursor(model, normalizedArgs.cursor);
|
|
@@ -3882,8 +4117,21 @@ function buildSelectSql(input) {
|
|
|
3882
4117
|
});
|
|
3883
4118
|
return constructFinalSql(spec);
|
|
3884
4119
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
4120
|
+
|
|
4121
|
+
// src/builder/shared/comparison-builder.ts
|
|
4122
|
+
function buildComparisons(expr, filter, params, dialect, builder, excludeKeys = /* @__PURE__ */ new Set(["mode"])) {
|
|
4123
|
+
const out = [];
|
|
4124
|
+
for (const [op, val] of Object.entries(filter)) {
|
|
4125
|
+
if (excludeKeys.has(op) || val === void 0) continue;
|
|
4126
|
+
const built = builder(expr, op, val, params, dialect);
|
|
4127
|
+
if (built && built.trim().length > 0) {
|
|
4128
|
+
out.push(built);
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
return out;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
// src/builder/aggregates.ts
|
|
3887
4135
|
var AGGREGATES = [
|
|
3888
4136
|
["_sum", "SUM"],
|
|
3889
4137
|
["_avg", "AVG"],
|
|
@@ -3898,16 +4146,16 @@ var COMPARISON_OPS = {
|
|
|
3898
4146
|
[Ops.LT]: "<",
|
|
3899
4147
|
[Ops.LTE]: "<="
|
|
3900
4148
|
};
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4149
|
+
var HAVING_ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
4150
|
+
Ops.EQUALS,
|
|
4151
|
+
Ops.NOT,
|
|
4152
|
+
Ops.GT,
|
|
4153
|
+
Ops.GTE,
|
|
4154
|
+
Ops.LT,
|
|
4155
|
+
Ops.LTE,
|
|
4156
|
+
Ops.IN,
|
|
4157
|
+
Ops.NOT_IN
|
|
4158
|
+
]);
|
|
3911
4159
|
function isTruthySelection(v) {
|
|
3912
4160
|
return v === true;
|
|
3913
4161
|
}
|
|
@@ -3949,24 +4197,10 @@ function normalizeLogicalValue2(operator, value) {
|
|
|
3949
4197
|
}
|
|
3950
4198
|
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
3951
4199
|
}
|
|
3952
|
-
function
|
|
3953
|
-
|
|
3954
|
-
const field = m.get(fieldName);
|
|
3955
|
-
if (!field) {
|
|
3956
|
-
throw new Error(
|
|
3957
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}. Available fields: ${model.fields.map((f) => f.name).join(", ")}`
|
|
3958
|
-
);
|
|
3959
|
-
}
|
|
3960
|
-
if (field.isRelation) {
|
|
3961
|
-
throw new Error(`${ctx} does not support relation field '${fieldName}'`);
|
|
3962
|
-
}
|
|
3963
|
-
return { name: field.name, type: field.type };
|
|
3964
|
-
}
|
|
3965
|
-
function assertAggregateFieldType(aggKey, fieldType, fieldName, modelName) {
|
|
3966
|
-
const baseType = fieldType.replace(/\[\]|\?/g, "");
|
|
3967
|
-
if ((aggKey === "_sum" || aggKey === "_avg") && !NUMERIC_TYPES.has(baseType)) {
|
|
4200
|
+
function assertHavingOp(op) {
|
|
4201
|
+
if (!HAVING_ALLOWED_OPS.has(op)) {
|
|
3968
4202
|
throw new Error(
|
|
3969
|
-
`
|
|
4203
|
+
`Unsupported HAVING operator '${op}'. Allowed: ${[...HAVING_ALLOWED_OPS].join(", ")}`
|
|
3970
4204
|
);
|
|
3971
4205
|
}
|
|
3972
4206
|
}
|
|
@@ -3996,6 +4230,7 @@ function buildBinaryComparison(expr, op, val, params) {
|
|
|
3996
4230
|
return `${expr} ${sqlOp} ${placeholder}`;
|
|
3997
4231
|
}
|
|
3998
4232
|
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
4233
|
+
assertHavingOp(op);
|
|
3999
4234
|
if (val === null) return buildNullComparison(expr, op);
|
|
4000
4235
|
if (op === Ops.NOT && isPlainObject(val)) {
|
|
4001
4236
|
return buildNotComposite(
|
|
@@ -4084,20 +4319,19 @@ function assertHavingAggTarget(aggKey, field, model) {
|
|
|
4084
4319
|
}
|
|
4085
4320
|
return;
|
|
4086
4321
|
}
|
|
4087
|
-
|
|
4088
|
-
|
|
4322
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4323
|
+
assertNumericField(model, field, "HAVING");
|
|
4324
|
+
} else {
|
|
4325
|
+
assertScalarField(model, field, "HAVING");
|
|
4326
|
+
}
|
|
4089
4327
|
}
|
|
4090
4328
|
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
4091
|
-
|
|
4092
|
-
for (const [op, val] of Object.entries(filter)) {
|
|
4093
|
-
if (op === "mode") continue;
|
|
4094
|
-
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
4095
|
-
if (built && built.trim().length > 0) out.push(built);
|
|
4096
|
-
}
|
|
4097
|
-
return out;
|
|
4329
|
+
return buildComparisons(expr, filter, params, dialect, buildSimpleComparison);
|
|
4098
4330
|
}
|
|
4099
4331
|
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
4100
|
-
if (!isPlainObject(target))
|
|
4332
|
+
if (!isPlainObject(target)) {
|
|
4333
|
+
throw new Error(`HAVING '${aggKey}' must be an object`);
|
|
4334
|
+
}
|
|
4101
4335
|
const out = [];
|
|
4102
4336
|
for (const [field, filter] of Object.entries(target)) {
|
|
4103
4337
|
assertHavingAggTarget(aggKey, field, model);
|
|
@@ -4108,30 +4342,39 @@ function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialec
|
|
|
4108
4342
|
return out;
|
|
4109
4343
|
}
|
|
4110
4344
|
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
4111
|
-
if (!isPlainObject(target))
|
|
4112
|
-
|
|
4345
|
+
if (!isPlainObject(target)) {
|
|
4346
|
+
throw new Error(`HAVING '${fieldName}' must be an object`);
|
|
4347
|
+
}
|
|
4348
|
+
assertScalarField(model, fieldName, "HAVING");
|
|
4113
4349
|
const out = [];
|
|
4114
4350
|
const obj = target;
|
|
4115
4351
|
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
4116
4352
|
for (const aggKey of keys) {
|
|
4117
4353
|
const aggFilter = obj[aggKey];
|
|
4118
4354
|
if (!isPlainObject(aggFilter)) continue;
|
|
4119
|
-
|
|
4355
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4356
|
+
assertNumericField(model, fieldName, "HAVING");
|
|
4357
|
+
}
|
|
4120
4358
|
const entries = Object.entries(aggFilter);
|
|
4121
4359
|
if (entries.length === 0) continue;
|
|
4122
4360
|
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4361
|
+
const clauses = buildComparisons(
|
|
4362
|
+
expr,
|
|
4363
|
+
aggFilter,
|
|
4364
|
+
params,
|
|
4365
|
+
dialect,
|
|
4366
|
+
buildSimpleComparison
|
|
4367
|
+
);
|
|
4368
|
+
out.push(...clauses);
|
|
4128
4369
|
}
|
|
4129
4370
|
return out;
|
|
4130
4371
|
}
|
|
4131
4372
|
function buildHavingClause(having, alias, params, model, dialect) {
|
|
4132
4373
|
if (!isNotNullish(having)) return "";
|
|
4133
4374
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
4134
|
-
if (!isPlainObject(having))
|
|
4375
|
+
if (!isPlainObject(having)) {
|
|
4376
|
+
throw new Error("having must be an object");
|
|
4377
|
+
}
|
|
4135
4378
|
return buildHavingNode(having, alias, params, d, model);
|
|
4136
4379
|
}
|
|
4137
4380
|
function normalizeCountArg(v) {
|
|
@@ -4145,18 +4388,8 @@ function pushCountAllField(fields) {
|
|
|
4145
4388
|
`${SQL_TEMPLATES.COUNT_ALL} ${SQL_TEMPLATES.AS} ${quote("_count._all")}`
|
|
4146
4389
|
);
|
|
4147
4390
|
}
|
|
4148
|
-
function assertCountableScalarField(
|
|
4149
|
-
|
|
4150
|
-
if (!field) {
|
|
4151
|
-
throw new Error(
|
|
4152
|
-
`Field '${fieldName}' does not exist on model ${model.name}`
|
|
4153
|
-
);
|
|
4154
|
-
}
|
|
4155
|
-
if (field.isRelation) {
|
|
4156
|
-
throw new Error(
|
|
4157
|
-
`Cannot use _count on relation field '${fieldName}' on model ${model.name}`
|
|
4158
|
-
);
|
|
4159
|
-
}
|
|
4391
|
+
function assertCountableScalarField(model, fieldName) {
|
|
4392
|
+
assertScalarField(model, fieldName, "_count");
|
|
4160
4393
|
}
|
|
4161
4394
|
function pushCountField(fields, alias, fieldName, model) {
|
|
4162
4395
|
const outAlias = `_count.${fieldName}`;
|
|
@@ -4164,7 +4397,7 @@ function pushCountField(fields, alias, fieldName, model) {
|
|
|
4164
4397
|
`COUNT(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4165
4398
|
);
|
|
4166
4399
|
}
|
|
4167
|
-
function addCountFields(fields, countArg, alias, model
|
|
4400
|
+
function addCountFields(fields, countArg, alias, model) {
|
|
4168
4401
|
if (!isNotNullish(countArg)) return;
|
|
4169
4402
|
if (countArg === true) {
|
|
4170
4403
|
pushCountAllField(fields);
|
|
@@ -4178,7 +4411,7 @@ function addCountFields(fields, countArg, alias, model, fieldMap) {
|
|
|
4178
4411
|
([f, v]) => f !== "_all" && isTruthySelection(v)
|
|
4179
4412
|
);
|
|
4180
4413
|
for (const [f] of selected) {
|
|
4181
|
-
assertCountableScalarField(
|
|
4414
|
+
assertCountableScalarField(model, f);
|
|
4182
4415
|
pushCountField(fields, alias, f, model);
|
|
4183
4416
|
}
|
|
4184
4417
|
}
|
|
@@ -4186,19 +4419,12 @@ function getAggregateSelectionObject(args, agg) {
|
|
|
4186
4419
|
const obj = args[agg];
|
|
4187
4420
|
return isPlainObject(obj) ? obj : void 0;
|
|
4188
4421
|
}
|
|
4189
|
-
function assertAggregatableScalarField(
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
);
|
|
4195
|
-
}
|
|
4196
|
-
if (field.isRelation) {
|
|
4197
|
-
throw new Error(
|
|
4198
|
-
`Cannot use ${agg} on relation field '${fieldName}' on model ${model.name}`
|
|
4199
|
-
);
|
|
4422
|
+
function assertAggregatableScalarField(model, agg, fieldName) {
|
|
4423
|
+
if (agg === "_sum" || agg === "_avg") {
|
|
4424
|
+
assertNumericField(model, fieldName, agg);
|
|
4425
|
+
} else {
|
|
4426
|
+
assertScalarField(model, fieldName, agg);
|
|
4200
4427
|
}
|
|
4201
|
-
return field;
|
|
4202
4428
|
}
|
|
4203
4429
|
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
4204
4430
|
const outAlias = `${agg}.${fieldName}`;
|
|
@@ -4206,7 +4432,7 @@ function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
|
4206
4432
|
`${aggFn}(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4207
4433
|
);
|
|
4208
4434
|
}
|
|
4209
|
-
function addAggregateFields(fields, args, alias, model
|
|
4435
|
+
function addAggregateFields(fields, args, alias, model) {
|
|
4210
4436
|
for (const [agg, aggFn] of AGGREGATES) {
|
|
4211
4437
|
const obj = getAggregateSelectionObject(args, agg);
|
|
4212
4438
|
if (!obj) continue;
|
|
@@ -4214,23 +4440,16 @@ function addAggregateFields(fields, args, alias, model, fieldMap) {
|
|
|
4214
4440
|
if (fieldName === "_all")
|
|
4215
4441
|
throw new Error(`'${agg}' does not support '_all'`);
|
|
4216
4442
|
if (!isTruthySelection(selection)) continue;
|
|
4217
|
-
|
|
4218
|
-
fieldMap,
|
|
4219
|
-
model,
|
|
4220
|
-
agg,
|
|
4221
|
-
fieldName
|
|
4222
|
-
);
|
|
4223
|
-
assertAggregateFieldType(agg, field.type, fieldName, model.name);
|
|
4443
|
+
assertAggregatableScalarField(model, agg, fieldName);
|
|
4224
4444
|
pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model);
|
|
4225
4445
|
}
|
|
4226
4446
|
}
|
|
4227
4447
|
}
|
|
4228
4448
|
function buildAggregateFields(args, alias, model) {
|
|
4229
4449
|
const fields = [];
|
|
4230
|
-
const fieldMap = getModelFieldMap(model);
|
|
4231
4450
|
const countArg = normalizeCountArg(args._count);
|
|
4232
|
-
addCountFields(fields, countArg, alias, model
|
|
4233
|
-
addAggregateFields(fields, args, alias, model
|
|
4451
|
+
addCountFields(fields, countArg, alias, model);
|
|
4452
|
+
addAggregateFields(fields, args, alias, model);
|
|
4234
4453
|
return fields;
|
|
4235
4454
|
}
|
|
4236
4455
|
function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
@@ -4267,32 +4486,24 @@ function assertGroupByBy(args, model) {
|
|
|
4267
4486
|
if (bySet.size !== byFields.length) {
|
|
4268
4487
|
throw new Error("buildGroupBySql: by must not contain duplicates");
|
|
4269
4488
|
}
|
|
4270
|
-
const modelFieldMap = getModelFieldMap(model);
|
|
4271
4489
|
for (const f of byFields) {
|
|
4272
|
-
|
|
4273
|
-
if (!field) {
|
|
4274
|
-
throw new Error(
|
|
4275
|
-
`groupBy.by references unknown field '${f}' on model ${model.name}`
|
|
4276
|
-
);
|
|
4277
|
-
}
|
|
4278
|
-
if (field.isRelation) {
|
|
4279
|
-
throw new Error(
|
|
4280
|
-
`groupBy.by does not support relation field '${f}' on model ${model.name}`
|
|
4281
|
-
);
|
|
4282
|
-
}
|
|
4490
|
+
assertScalarField(model, f, "groupBy.by");
|
|
4283
4491
|
}
|
|
4284
4492
|
return byFields;
|
|
4285
4493
|
}
|
|
4286
4494
|
function buildGroupBySelectParts(args, alias, model, byFields) {
|
|
4287
4495
|
const groupCols = byFields.map((f) => col(alias, f, model));
|
|
4496
|
+
const selectCols = byFields.map((f) => colWithAlias(alias, f, model));
|
|
4288
4497
|
const groupFields = groupCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4289
4498
|
const aggFields = buildAggregateFields(args, alias, model);
|
|
4290
|
-
const selectFields = isNonEmptyArray(aggFields) ?
|
|
4499
|
+
const selectFields = isNonEmptyArray(aggFields) ? selectCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : selectCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4291
4500
|
return { groupCols, groupFields, selectFields };
|
|
4292
4501
|
}
|
|
4293
4502
|
function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
4294
4503
|
if (!isNotNullish(args.having)) return "";
|
|
4295
|
-
if (!isPlainObject(args.having))
|
|
4504
|
+
if (!isPlainObject(args.having)) {
|
|
4505
|
+
throw new Error("having must be an object");
|
|
4506
|
+
}
|
|
4296
4507
|
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4297
4508
|
if (!h || h.trim().length === 0) return "";
|
|
4298
4509
|
return `${SQL_TEMPLATES.HAVING} ${h}`;
|
|
@@ -4325,63 +4536,61 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
|
4325
4536
|
const snapshot = params.snapshot();
|
|
4326
4537
|
validateSelectQuery(sql);
|
|
4327
4538
|
validateParamConsistency(sql, [...whereResult.params, ...snapshot.params]);
|
|
4539
|
+
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4328
4540
|
return Object.freeze({
|
|
4329
4541
|
sql,
|
|
4330
|
-
params: Object.freeze(
|
|
4542
|
+
params: Object.freeze(mergedParams),
|
|
4331
4543
|
paramMappings: Object.freeze([
|
|
4332
4544
|
...whereResult.paramMappings,
|
|
4333
4545
|
...snapshot.mappings
|
|
4334
4546
|
])
|
|
4335
4547
|
});
|
|
4336
4548
|
}
|
|
4337
|
-
function buildCountSql(whereResult, tableName, alias, skip,
|
|
4549
|
+
function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
4338
4550
|
assertSafeAlias(alias);
|
|
4339
4551
|
assertSafeTableRef(tableName);
|
|
4340
|
-
|
|
4552
|
+
if (skip !== void 0 && skip !== null) {
|
|
4553
|
+
if (schemaParser.isDynamicParameter(skip)) {
|
|
4554
|
+
throw new Error(
|
|
4555
|
+
"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."
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
if (typeof skip === "string") {
|
|
4559
|
+
const s = skip.trim();
|
|
4560
|
+
if (s.length > 0) {
|
|
4561
|
+
const n = Number(s);
|
|
4562
|
+
if (Number.isFinite(n) && Number.isInteger(n) && n > 0) {
|
|
4563
|
+
throw new Error(
|
|
4564
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4565
|
+
);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
if (typeof skip === "number" && Number.isFinite(skip) && Number.isInteger(skip) && skip > 0) {
|
|
4570
|
+
throw new Error(
|
|
4571
|
+
"count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
|
|
4572
|
+
);
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4341
4575
|
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4342
|
-
const params = createParamStore(whereResult.nextParamIndex);
|
|
4343
|
-
const baseSubSelect = [
|
|
4344
|
-
SQL_TEMPLATES.SELECT,
|
|
4345
|
-
"1",
|
|
4346
|
-
SQL_TEMPLATES.FROM,
|
|
4347
|
-
tableName,
|
|
4348
|
-
alias,
|
|
4349
|
-
whereClause
|
|
4350
|
-
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4351
|
-
const normalizedSkip = normalizeSkipLike(skip);
|
|
4352
|
-
const subSelect = applyCountSkip(baseSubSelect, normalizedSkip, params, d);
|
|
4353
4576
|
const sql = [
|
|
4354
4577
|
SQL_TEMPLATES.SELECT,
|
|
4355
4578
|
SQL_TEMPLATES.COUNT_ALL,
|
|
4356
4579
|
SQL_TEMPLATES.AS,
|
|
4357
4580
|
quote("_count._all"),
|
|
4358
4581
|
SQL_TEMPLATES.FROM,
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4582
|
+
tableName,
|
|
4583
|
+
alias,
|
|
4584
|
+
whereClause
|
|
4362
4585
|
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4363
4586
|
validateSelectQuery(sql);
|
|
4364
|
-
|
|
4365
|
-
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4366
|
-
validateParamConsistency(sql, mergedParams);
|
|
4587
|
+
validateParamConsistency(sql, whereResult.params);
|
|
4367
4588
|
return Object.freeze({
|
|
4368
4589
|
sql,
|
|
4369
|
-
params: Object.freeze(
|
|
4370
|
-
paramMappings: Object.freeze([
|
|
4371
|
-
...whereResult.paramMappings,
|
|
4372
|
-
...snapshot.mappings
|
|
4373
|
-
])
|
|
4590
|
+
params: Object.freeze([...whereResult.params]),
|
|
4591
|
+
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4374
4592
|
});
|
|
4375
4593
|
}
|
|
4376
|
-
function applyCountSkip(subSelect, normalizedSkip, params, dialect) {
|
|
4377
|
-
const shouldApply = schemaParser.isDynamicParameter(normalizedSkip) || typeof normalizedSkip === "number" && normalizedSkip > 0;
|
|
4378
|
-
if (!shouldApply) return subSelect;
|
|
4379
|
-
const placeholder = addAutoScoped(params, normalizedSkip, "count.skip");
|
|
4380
|
-
if (dialect === "sqlite") {
|
|
4381
|
-
return `${subSelect} ${SQL_TEMPLATES.LIMIT} -1 ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4382
|
-
}
|
|
4383
|
-
return `${subSelect} ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4384
|
-
}
|
|
4385
4594
|
function safeAlias(input) {
|
|
4386
4595
|
const raw = String(input).toLowerCase();
|
|
4387
4596
|
const cleaned = raw.replace(/[^a-z0-9_]/g, "_");
|
|
@@ -4531,8 +4740,12 @@ function buildAndNormalizeSql(args) {
|
|
|
4531
4740
|
);
|
|
4532
4741
|
}
|
|
4533
4742
|
function finalizeDirective(args) {
|
|
4534
|
-
const { directive, normalizedSql, normalizedMappings } = args;
|
|
4535
|
-
|
|
4743
|
+
const { directive, normalizedSql, normalizedMappings, dialect } = args;
|
|
4744
|
+
const params = normalizedMappings.map((m) => {
|
|
4745
|
+
var _a;
|
|
4746
|
+
return (_a = m.value) != null ? _a : void 0;
|
|
4747
|
+
});
|
|
4748
|
+
validateParamConsistencyByDialect(normalizedSql, params, dialect);
|
|
4536
4749
|
const { staticParams, dynamicKeys } = buildParamsFromMappings(normalizedMappings);
|
|
4537
4750
|
return {
|
|
4538
4751
|
method: directive.method,
|
|
@@ -4569,7 +4782,8 @@ function generateSQL(directive) {
|
|
|
4569
4782
|
return finalizeDirective({
|
|
4570
4783
|
directive,
|
|
4571
4784
|
normalizedSql: normalized.sql,
|
|
4572
|
-
normalizedMappings: normalized.paramMappings
|
|
4785
|
+
normalizedMappings: normalized.paramMappings,
|
|
4786
|
+
dialect
|
|
4573
4787
|
});
|
|
4574
4788
|
}
|
|
4575
4789
|
function generateSQL2(directive) {
|
|
@@ -4677,18 +4891,57 @@ function generateCode(models, queries, dialect, datamodel) {
|
|
|
4677
4891
|
return `// Generated by @prisma-sql/generator - DO NOT EDIT
|
|
4678
4892
|
import { buildSQL, transformQueryResults, type PrismaMethod, type Model } from 'prisma-sql'
|
|
4679
4893
|
|
|
4680
|
-
|
|
4894
|
+
/**
|
|
4895
|
+
* Normalize values for SQL params.
|
|
4896
|
+
* Synced from src/utils/normalize-value.ts
|
|
4897
|
+
*/
|
|
4898
|
+
function normalizeValue(value: unknown, seen = new WeakSet<object>(), depth = 0): unknown {
|
|
4899
|
+
const MAX_DEPTH = 20
|
|
4900
|
+
if (depth > MAX_DEPTH) {
|
|
4901
|
+
throw new Error(\`Max normalization depth exceeded (\${MAX_DEPTH} levels)\`)
|
|
4902
|
+
}
|
|
4681
4903
|
if (value instanceof Date) {
|
|
4904
|
+
const t = value.getTime()
|
|
4905
|
+
if (!Number.isFinite(t)) {
|
|
4906
|
+
throw new Error('Invalid Date value in SQL params')
|
|
4907
|
+
}
|
|
4682
4908
|
return value.toISOString()
|
|
4683
4909
|
}
|
|
4684
|
-
|
|
4910
|
+
if (typeof value === 'bigint') {
|
|
4911
|
+
return value.toString()
|
|
4912
|
+
}
|
|
4685
4913
|
if (Array.isArray(value)) {
|
|
4686
|
-
|
|
4914
|
+
const arrRef = value as unknown as object
|
|
4915
|
+
if (seen.has(arrRef)) {
|
|
4916
|
+
throw new Error('Circular reference in SQL params')
|
|
4917
|
+
}
|
|
4918
|
+
seen.add(arrRef)
|
|
4919
|
+
const out = value.map((v) => normalizeValue(v, seen, depth + 1))
|
|
4920
|
+
seen.delete(arrRef)
|
|
4921
|
+
return out
|
|
4922
|
+
}
|
|
4923
|
+
if (value && typeof value === 'object') {
|
|
4924
|
+
if (value instanceof Uint8Array) return value
|
|
4925
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return value
|
|
4926
|
+
const proto = Object.getPrototypeOf(value)
|
|
4927
|
+
const isPlain = proto === Object.prototype || proto === null
|
|
4928
|
+
if (!isPlain) return value
|
|
4929
|
+
const obj = value as Record<string, unknown>
|
|
4930
|
+
if (seen.has(obj)) {
|
|
4931
|
+
throw new Error('Circular reference in SQL params')
|
|
4932
|
+
}
|
|
4933
|
+
seen.add(obj)
|
|
4934
|
+
const out: Record<string, unknown> = {}
|
|
4935
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
4936
|
+
out[k] = normalizeValue(v, seen, depth + 1)
|
|
4937
|
+
}
|
|
4938
|
+
seen.delete(obj)
|
|
4939
|
+
return out
|
|
4687
4940
|
}
|
|
4688
|
-
|
|
4689
4941
|
return value
|
|
4690
4942
|
}
|
|
4691
4943
|
|
|
4944
|
+
|
|
4692
4945
|
export const MODELS: Model[] = ${JSON.stringify(cleanModels, null, 2)}
|
|
4693
4946
|
|
|
4694
4947
|
const ENUM_MAPPINGS: Record<string, Record<string, string>> = ${JSON.stringify(mappings, null, 2)}
|