prisma-sql 1.44.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator.cjs +808 -562
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +808 -562
- package/dist/generator.js.map +1 -1
- package/dist/index.cjs +765 -560
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +765 -560
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/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)}`;
|
|
3335
3468
|
}
|
|
3336
|
-
function subqueryForCount(
|
|
3337
|
-
|
|
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 ")})`;
|
|
3482
|
+
}
|
|
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,16 +3546,21 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3376
3546
|
}
|
|
3377
3547
|
|
|
3378
3548
|
// src/builder/select/assembly.ts
|
|
3379
|
-
var
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
}
|
|
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");
|
|
3383
3554
|
function joinNonEmpty(parts, sep) {
|
|
3384
3555
|
return parts.filter((s) => s.trim().length > 0).join(sep);
|
|
3385
3556
|
}
|
|
3386
3557
|
function buildWhereSql(conditions) {
|
|
3387
3558
|
if (!isNonEmptyArray(conditions)) return "";
|
|
3388
|
-
|
|
3559
|
+
const parts = [
|
|
3560
|
+
SQL_TEMPLATES.WHERE,
|
|
3561
|
+
conditions.join(SQL_SEPARATORS.CONDITION_AND)
|
|
3562
|
+
];
|
|
3563
|
+
return ` ${parts.join(" ")}`;
|
|
3389
3564
|
}
|
|
3390
3565
|
function buildJoinsSql(...joinGroups) {
|
|
3391
3566
|
const all = [];
|
|
@@ -3400,37 +3575,39 @@ function buildSelectList(baseSelect, extraCols) {
|
|
|
3400
3575
|
if (base && extra) return `${base}${SQL_SEPARATORS.FIELD_LIST}${extra}`;
|
|
3401
3576
|
return base || extra;
|
|
3402
3577
|
}
|
|
3403
|
-
function finalizeSql(sql, params) {
|
|
3578
|
+
function finalizeSql(sql, params, dialect) {
|
|
3404
3579
|
const snapshot = params.snapshot();
|
|
3405
3580
|
validateSelectQuery(sql);
|
|
3406
|
-
|
|
3581
|
+
validateParamConsistencyByDialect(sql, snapshot.params, dialect);
|
|
3407
3582
|
return Object.freeze({
|
|
3408
3583
|
sql,
|
|
3409
|
-
params:
|
|
3584
|
+
params: snapshot.params,
|
|
3410
3585
|
paramMappings: Object.freeze(snapshot.mappings)
|
|
3411
3586
|
});
|
|
3412
3587
|
}
|
|
3413
|
-
function parseSimpleScalarSelect(select,
|
|
3414
|
-
var _a, _b;
|
|
3588
|
+
function parseSimpleScalarSelect(select, fromAlias) {
|
|
3589
|
+
var _a, _b, _c, _d;
|
|
3415
3590
|
const raw = select.trim();
|
|
3416
3591
|
if (raw.length === 0) return [];
|
|
3417
|
-
let re = SIMPLE_SELECT_RE_CACHE.get(alias);
|
|
3418
|
-
if (!re) {
|
|
3419
|
-
const safeAlias2 = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3420
|
-
re = new RegExp(`^${safeAlias2}\\.(?:"([^"]+)"|([a-z_][a-z0-9_]*))$`, "i");
|
|
3421
|
-
SIMPLE_SELECT_RE_CACHE.set(alias, re);
|
|
3422
|
-
}
|
|
3423
3592
|
const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
|
|
3424
3593
|
const names = [];
|
|
3425
3594
|
for (const part of parts) {
|
|
3426
3595
|
const p = part.trim();
|
|
3427
|
-
const m = p.match(
|
|
3596
|
+
const m = p.match(SIMPLE_COLUMN_RE);
|
|
3428
3597
|
if (!m) {
|
|
3429
3598
|
throw new Error(
|
|
3430
|
-
`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}`
|
|
3431
3606
|
);
|
|
3432
3607
|
}
|
|
3433
|
-
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;
|
|
3434
3611
|
if (name.length === 0) {
|
|
3435
3612
|
throw new Error(`Failed to parse selected column name from: ${p}`);
|
|
3436
3613
|
}
|
|
@@ -3439,18 +3616,18 @@ function parseSimpleScalarSelect(select, alias) {
|
|
|
3439
3616
|
return names;
|
|
3440
3617
|
}
|
|
3441
3618
|
function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
|
|
3442
|
-
const
|
|
3443
|
-
|
|
3444
|
-
|
|
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}.`);
|
|
3445
3624
|
}
|
|
3446
3625
|
function buildDistinctColumns(distinct, fromAlias, model) {
|
|
3447
3626
|
return distinct.map((f) => col(fromAlias, f, model)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3448
3627
|
}
|
|
3449
3628
|
function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
3450
3629
|
const outputCols = [...scalarNames, ...includeNames];
|
|
3451
|
-
if (hasCount)
|
|
3452
|
-
outputCols.push("_count");
|
|
3453
|
-
}
|
|
3630
|
+
if (hasCount) outputCols.push("_count");
|
|
3454
3631
|
const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3455
3632
|
if (!isNonEmptyString(formatted)) {
|
|
3456
3633
|
throw new Error("distinct emulation requires at least one output column");
|
|
@@ -3459,9 +3636,10 @@ function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
|
3459
3636
|
}
|
|
3460
3637
|
function buildWindowOrder(args) {
|
|
3461
3638
|
const { baseOrder, idField, fromAlias, model } = args;
|
|
3639
|
+
const fromLower = String(fromAlias).toLowerCase();
|
|
3462
3640
|
const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
|
|
3463
3641
|
const hasIdInOrder = orderFields.some(
|
|
3464
|
-
(f) => f.startsWith(`${
|
|
3642
|
+
(f) => f.startsWith(`${fromLower}.id `) || f.startsWith(`${fromLower}."id" `)
|
|
3465
3643
|
);
|
|
3466
3644
|
if (hasIdInOrder) return baseOrder;
|
|
3467
3645
|
const idTiebreaker = idField ? `, ${col(fromAlias, "id", model)} ASC` : "";
|
|
@@ -3496,15 +3674,37 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3496
3674
|
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, `"__tp_distinct"`) : replaceOrderByAlias(fallbackOrder, from.alias, `"__tp_distinct"`);
|
|
3497
3675
|
const joins = buildJoinsSql(whereJoins, countJoins);
|
|
3498
3676
|
const conditions = [];
|
|
3499
|
-
if (whereClause && whereClause !== "1=1")
|
|
3500
|
-
conditions.push(whereClause);
|
|
3501
|
-
}
|
|
3677
|
+
if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
|
|
3502
3678
|
const whereSql = buildWhereSql(conditions);
|
|
3503
3679
|
const innerSelectList = selectWithIncludes.trim();
|
|
3504
3680
|
const innerComma = innerSelectList.length > 0 ? SQL_SEPARATORS.FIELD_LIST : "";
|
|
3505
|
-
const
|
|
3506
|
-
|
|
3507
|
-
|
|
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(" ");
|
|
3508
3708
|
}
|
|
3509
3709
|
function buildIncludeColumns(spec) {
|
|
3510
3710
|
var _a, _b;
|
|
@@ -3632,6 +3832,7 @@ function constructFinalSql(spec) {
|
|
|
3632
3832
|
orderBy,
|
|
3633
3833
|
distinct,
|
|
3634
3834
|
method,
|
|
3835
|
+
cursorCte,
|
|
3635
3836
|
cursorClause,
|
|
3636
3837
|
params,
|
|
3637
3838
|
dialect,
|
|
@@ -3646,9 +3847,13 @@ function constructFinalSql(spec) {
|
|
|
3646
3847
|
const spec2 = withCountJoins(spec, countJoins, whereJoins);
|
|
3647
3848
|
let sql2 = buildSqliteDistinctQuery(spec2, selectWithIncludes).trim();
|
|
3648
3849
|
sql2 = appendPagination(sql2, spec);
|
|
3649
|
-
return finalizeSql(sql2, params);
|
|
3850
|
+
return finalizeSql(sql2, params, dialect);
|
|
3650
3851
|
}
|
|
3651
|
-
const parts = [
|
|
3852
|
+
const parts = [];
|
|
3853
|
+
if (cursorCte) {
|
|
3854
|
+
parts.push(`WITH ${cursorCte}`);
|
|
3855
|
+
}
|
|
3856
|
+
parts.push(SQL_TEMPLATES.SELECT);
|
|
3652
3857
|
const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct, model) : null;
|
|
3653
3858
|
if (distinctOn) parts.push(distinctOn);
|
|
3654
3859
|
const baseSelect = (select != null ? select : "").trim();
|
|
@@ -3664,7 +3869,41 @@ function constructFinalSql(spec) {
|
|
|
3664
3869
|
if (isNonEmptyString(orderBy)) parts.push(SQL_TEMPLATES.ORDER_BY, orderBy);
|
|
3665
3870
|
let sql = parts.join(" ").trim();
|
|
3666
3871
|
sql = appendPagination(sql, spec);
|
|
3667
|
-
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;
|
|
3668
3907
|
}
|
|
3669
3908
|
|
|
3670
3909
|
// src/builder/select.ts
|
|
@@ -3697,7 +3936,7 @@ function buildPostgresDistinctOrderBy(distinctFields, existing) {
|
|
|
3697
3936
|
}
|
|
3698
3937
|
return next;
|
|
3699
3938
|
}
|
|
3700
|
-
function applyPostgresDistinctOrderBy(args
|
|
3939
|
+
function applyPostgresDistinctOrderBy(args) {
|
|
3701
3940
|
const distinctFields = normalizeDistinctFields(args.distinct);
|
|
3702
3941
|
if (distinctFields.length === 0) return args;
|
|
3703
3942
|
if (!isNotNullish(args.orderBy)) return args;
|
|
@@ -3707,19 +3946,6 @@ function applyPostgresDistinctOrderBy(args, _model) {
|
|
|
3707
3946
|
orderBy: buildPostgresDistinctOrderBy(distinctFields, existing)
|
|
3708
3947
|
});
|
|
3709
3948
|
}
|
|
3710
|
-
function assertScalarFieldOnModel(model, fieldName, ctx) {
|
|
3711
|
-
const f = model.fields.find((x) => x.name === fieldName);
|
|
3712
|
-
if (!f) {
|
|
3713
|
-
throw new Error(
|
|
3714
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}`
|
|
3715
|
-
);
|
|
3716
|
-
}
|
|
3717
|
-
if (f.isRelation) {
|
|
3718
|
-
throw new Error(
|
|
3719
|
-
`${ctx} does not support relation field '${fieldName}' on model ${model.name}`
|
|
3720
|
-
);
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
3949
|
function validateDistinct(model, distinct) {
|
|
3724
3950
|
if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) return;
|
|
3725
3951
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3730,24 +3956,24 @@ function validateDistinct(model, distinct) {
|
|
|
3730
3956
|
throw new Error(`distinct must not contain duplicates (field: '${f}')`);
|
|
3731
3957
|
}
|
|
3732
3958
|
seen.add(f);
|
|
3733
|
-
|
|
3959
|
+
assertScalarField(model, f, "distinct");
|
|
3734
3960
|
}
|
|
3735
3961
|
}
|
|
3736
|
-
function validateOrderByValue(fieldName, v) {
|
|
3737
|
-
parseOrderByValue(v, fieldName);
|
|
3738
|
-
}
|
|
3739
3962
|
function validateOrderBy(model, orderBy) {
|
|
3740
3963
|
if (!isNotNullish(orderBy)) return;
|
|
3741
3964
|
const items = normalizeOrderByInput2(orderBy);
|
|
3742
3965
|
if (items.length === 0) return;
|
|
3743
3966
|
for (const it of items) {
|
|
3744
3967
|
const entries = Object.entries(it);
|
|
3968
|
+
if (entries.length !== 1) {
|
|
3969
|
+
throw new Error("orderBy array entries must have exactly one field");
|
|
3970
|
+
}
|
|
3745
3971
|
const fieldName = String(entries[0][0]).trim();
|
|
3746
3972
|
if (fieldName.length === 0) {
|
|
3747
3973
|
throw new Error("orderBy field name cannot be empty");
|
|
3748
3974
|
}
|
|
3749
|
-
|
|
3750
|
-
|
|
3975
|
+
assertScalarField(model, fieldName, "orderBy");
|
|
3976
|
+
parseOrderByValue(entries[0][1], fieldName);
|
|
3751
3977
|
}
|
|
3752
3978
|
}
|
|
3753
3979
|
function validateCursor(model, cursor) {
|
|
@@ -3764,7 +3990,7 @@ function validateCursor(model, cursor) {
|
|
|
3764
3990
|
if (f.length === 0) {
|
|
3765
3991
|
throw new Error("cursor field name cannot be empty");
|
|
3766
3992
|
}
|
|
3767
|
-
|
|
3993
|
+
assertScalarField(model, f, "cursor");
|
|
3768
3994
|
}
|
|
3769
3995
|
}
|
|
3770
3996
|
function resolveDialect(dialect) {
|
|
@@ -3783,20 +4009,21 @@ function normalizeArgsForNegativeTake(method, args) {
|
|
|
3783
4009
|
orderBy: reverseOrderByInput(args.orderBy)
|
|
3784
4010
|
});
|
|
3785
4011
|
}
|
|
3786
|
-
function normalizeArgsForDialect(dialect, args
|
|
4012
|
+
function normalizeArgsForDialect(dialect, args) {
|
|
3787
4013
|
if (dialect !== "postgres") return args;
|
|
3788
4014
|
return applyPostgresDistinctOrderBy(args);
|
|
3789
4015
|
}
|
|
3790
4016
|
function buildCursorClauseIfAny(input) {
|
|
3791
|
-
const { cursor, orderBy, tableName, alias, params, dialect } = input;
|
|
3792
|
-
if (!isNotNullish(cursor)) return
|
|
4017
|
+
const { cursor, orderBy, tableName, alias, params, dialect, model } = input;
|
|
4018
|
+
if (!isNotNullish(cursor)) return {};
|
|
3793
4019
|
return buildCursorCondition(
|
|
3794
4020
|
cursor,
|
|
3795
4021
|
orderBy,
|
|
3796
4022
|
tableName,
|
|
3797
4023
|
alias,
|
|
3798
4024
|
params,
|
|
3799
|
-
dialect
|
|
4025
|
+
dialect,
|
|
4026
|
+
model
|
|
3800
4027
|
);
|
|
3801
4028
|
}
|
|
3802
4029
|
function buildSelectSpec(input) {
|
|
@@ -3835,14 +4062,20 @@ function buildSelectSpec(input) {
|
|
|
3835
4062
|
params,
|
|
3836
4063
|
dialect
|
|
3837
4064
|
);
|
|
3838
|
-
const
|
|
4065
|
+
const cursorResult = buildCursorClauseIfAny({
|
|
3839
4066
|
cursor,
|
|
3840
4067
|
orderBy: normalizedArgs.orderBy,
|
|
3841
4068
|
tableName,
|
|
3842
4069
|
alias,
|
|
3843
4070
|
params,
|
|
3844
|
-
dialect
|
|
4071
|
+
dialect,
|
|
4072
|
+
model
|
|
3845
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
|
+
}
|
|
3846
4079
|
return {
|
|
3847
4080
|
select: selectFields,
|
|
3848
4081
|
includes,
|
|
@@ -3853,7 +4086,8 @@ function buildSelectSpec(input) {
|
|
|
3853
4086
|
pagination: { take, skip },
|
|
3854
4087
|
distinct: normalizedArgs.distinct,
|
|
3855
4088
|
method,
|
|
3856
|
-
|
|
4089
|
+
cursorCte: cursorResult.cte,
|
|
4090
|
+
cursorClause: cursorResult.condition,
|
|
3857
4091
|
params,
|
|
3858
4092
|
dialect,
|
|
3859
4093
|
model,
|
|
@@ -3867,9 +4101,7 @@ function buildSelectSql(input) {
|
|
|
3867
4101
|
assertSafeTableRef(from.tableName);
|
|
3868
4102
|
const dialectToUse = resolveDialect(dialect);
|
|
3869
4103
|
const argsForSql = normalizeArgsForNegativeTake(method, args);
|
|
3870
|
-
const normalizedArgs = normalizeArgsForDialect(
|
|
3871
|
-
dialectToUse,
|
|
3872
|
-
argsForSql);
|
|
4104
|
+
const normalizedArgs = normalizeArgsForDialect(dialectToUse, argsForSql);
|
|
3873
4105
|
validateDistinct(model, normalizedArgs.distinct);
|
|
3874
4106
|
validateOrderBy(model, normalizedArgs.orderBy);
|
|
3875
4107
|
validateCursor(model, normalizedArgs.cursor);
|
|
@@ -3885,8 +4117,21 @@ function buildSelectSql(input) {
|
|
|
3885
4117
|
});
|
|
3886
4118
|
return constructFinalSql(spec);
|
|
3887
4119
|
}
|
|
3888
|
-
|
|
3889
|
-
|
|
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
|
|
3890
4135
|
var AGGREGATES = [
|
|
3891
4136
|
["_sum", "SUM"],
|
|
3892
4137
|
["_avg", "AVG"],
|
|
@@ -3901,19 +4146,16 @@ var COMPARISON_OPS = {
|
|
|
3901
4146
|
[Ops.LT]: "<",
|
|
3902
4147
|
[Ops.LTE]: "<="
|
|
3903
4148
|
};
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
MODEL_FIELD_CACHE.set(model, m);
|
|
3915
|
-
return m;
|
|
3916
|
-
}
|
|
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
|
+
]);
|
|
3917
4159
|
function isTruthySelection(v) {
|
|
3918
4160
|
return v === true;
|
|
3919
4161
|
}
|
|
@@ -3955,24 +4197,10 @@ function normalizeLogicalValue2(operator, value) {
|
|
|
3955
4197
|
}
|
|
3956
4198
|
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
3957
4199
|
}
|
|
3958
|
-
function
|
|
3959
|
-
|
|
3960
|
-
const field = m.get(fieldName);
|
|
3961
|
-
if (!field) {
|
|
3962
|
-
throw new Error(
|
|
3963
|
-
`${ctx} references unknown field '${fieldName}' on model ${model.name}. Available fields: ${model.fields.map((f) => f.name).join(", ")}`
|
|
3964
|
-
);
|
|
3965
|
-
}
|
|
3966
|
-
if (field.isRelation) {
|
|
3967
|
-
throw new Error(`${ctx} does not support relation field '${fieldName}'`);
|
|
3968
|
-
}
|
|
3969
|
-
return { name: field.name, type: field.type };
|
|
3970
|
-
}
|
|
3971
|
-
function assertAggregateFieldType(aggKey, fieldType, fieldName, modelName) {
|
|
3972
|
-
const baseType = fieldType.replace(/\[\]|\?/g, "");
|
|
3973
|
-
if ((aggKey === "_sum" || aggKey === "_avg") && !NUMERIC_TYPES.has(baseType)) {
|
|
4200
|
+
function assertHavingOp(op) {
|
|
4201
|
+
if (!HAVING_ALLOWED_OPS.has(op)) {
|
|
3974
4202
|
throw new Error(
|
|
3975
|
-
`
|
|
4203
|
+
`Unsupported HAVING operator '${op}'. Allowed: ${[...HAVING_ALLOWED_OPS].join(", ")}`
|
|
3976
4204
|
);
|
|
3977
4205
|
}
|
|
3978
4206
|
}
|
|
@@ -4002,6 +4230,7 @@ function buildBinaryComparison(expr, op, val, params) {
|
|
|
4002
4230
|
return `${expr} ${sqlOp} ${placeholder}`;
|
|
4003
4231
|
}
|
|
4004
4232
|
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
4233
|
+
assertHavingOp(op);
|
|
4005
4234
|
if (val === null) return buildNullComparison(expr, op);
|
|
4006
4235
|
if (op === Ops.NOT && isPlainObject(val)) {
|
|
4007
4236
|
return buildNotComposite(
|
|
@@ -4090,20 +4319,19 @@ function assertHavingAggTarget(aggKey, field, model) {
|
|
|
4090
4319
|
}
|
|
4091
4320
|
return;
|
|
4092
4321
|
}
|
|
4093
|
-
|
|
4094
|
-
|
|
4322
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4323
|
+
assertNumericField(model, field, "HAVING");
|
|
4324
|
+
} else {
|
|
4325
|
+
assertScalarField(model, field, "HAVING");
|
|
4326
|
+
}
|
|
4095
4327
|
}
|
|
4096
4328
|
function buildHavingOpsForExpr(expr, filter, params, dialect) {
|
|
4097
|
-
|
|
4098
|
-
for (const [op, val] of Object.entries(filter)) {
|
|
4099
|
-
if (op === "mode") continue;
|
|
4100
|
-
const built = buildSimpleComparison(expr, op, val, params, dialect);
|
|
4101
|
-
if (built && built.trim().length > 0) out.push(built);
|
|
4102
|
-
}
|
|
4103
|
-
return out;
|
|
4329
|
+
return buildComparisons(expr, filter, params, dialect, buildSimpleComparison);
|
|
4104
4330
|
}
|
|
4105
4331
|
function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
|
|
4106
|
-
if (!isPlainObject(target))
|
|
4332
|
+
if (!isPlainObject(target)) {
|
|
4333
|
+
throw new Error(`HAVING '${aggKey}' must be an object`);
|
|
4334
|
+
}
|
|
4107
4335
|
const out = [];
|
|
4108
4336
|
for (const [field, filter] of Object.entries(target)) {
|
|
4109
4337
|
assertHavingAggTarget(aggKey, field, model);
|
|
@@ -4114,30 +4342,39 @@ function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialec
|
|
|
4114
4342
|
return out;
|
|
4115
4343
|
}
|
|
4116
4344
|
function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
|
|
4117
|
-
if (!isPlainObject(target))
|
|
4118
|
-
|
|
4345
|
+
if (!isPlainObject(target)) {
|
|
4346
|
+
throw new Error(`HAVING '${fieldName}' must be an object`);
|
|
4347
|
+
}
|
|
4348
|
+
assertScalarField(model, fieldName, "HAVING");
|
|
4119
4349
|
const out = [];
|
|
4120
4350
|
const obj = target;
|
|
4121
4351
|
const keys = ["_count", "_sum", "_avg", "_min", "_max"];
|
|
4122
4352
|
for (const aggKey of keys) {
|
|
4123
4353
|
const aggFilter = obj[aggKey];
|
|
4124
4354
|
if (!isPlainObject(aggFilter)) continue;
|
|
4125
|
-
|
|
4355
|
+
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4356
|
+
assertNumericField(model, fieldName, "HAVING");
|
|
4357
|
+
}
|
|
4126
4358
|
const entries = Object.entries(aggFilter);
|
|
4127
4359
|
if (entries.length === 0) continue;
|
|
4128
4360
|
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4361
|
+
const clauses = buildComparisons(
|
|
4362
|
+
expr,
|
|
4363
|
+
aggFilter,
|
|
4364
|
+
params,
|
|
4365
|
+
dialect,
|
|
4366
|
+
buildSimpleComparison
|
|
4367
|
+
);
|
|
4368
|
+
out.push(...clauses);
|
|
4134
4369
|
}
|
|
4135
4370
|
return out;
|
|
4136
4371
|
}
|
|
4137
4372
|
function buildHavingClause(having, alias, params, model, dialect) {
|
|
4138
4373
|
if (!isNotNullish(having)) return "";
|
|
4139
4374
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
4140
|
-
if (!isPlainObject(having))
|
|
4375
|
+
if (!isPlainObject(having)) {
|
|
4376
|
+
throw new Error("having must be an object");
|
|
4377
|
+
}
|
|
4141
4378
|
return buildHavingNode(having, alias, params, d, model);
|
|
4142
4379
|
}
|
|
4143
4380
|
function normalizeCountArg(v) {
|
|
@@ -4151,18 +4388,8 @@ function pushCountAllField(fields) {
|
|
|
4151
4388
|
`${SQL_TEMPLATES.COUNT_ALL} ${SQL_TEMPLATES.AS} ${quote("_count._all")}`
|
|
4152
4389
|
);
|
|
4153
4390
|
}
|
|
4154
|
-
function assertCountableScalarField(
|
|
4155
|
-
|
|
4156
|
-
if (!field) {
|
|
4157
|
-
throw new Error(
|
|
4158
|
-
`Field '${fieldName}' does not exist on model ${model.name}`
|
|
4159
|
-
);
|
|
4160
|
-
}
|
|
4161
|
-
if (field.isRelation) {
|
|
4162
|
-
throw new Error(
|
|
4163
|
-
`Cannot use _count on relation field '${fieldName}' on model ${model.name}`
|
|
4164
|
-
);
|
|
4165
|
-
}
|
|
4391
|
+
function assertCountableScalarField(model, fieldName) {
|
|
4392
|
+
assertScalarField(model, fieldName, "_count");
|
|
4166
4393
|
}
|
|
4167
4394
|
function pushCountField(fields, alias, fieldName, model) {
|
|
4168
4395
|
const outAlias = `_count.${fieldName}`;
|
|
@@ -4170,7 +4397,7 @@ function pushCountField(fields, alias, fieldName, model) {
|
|
|
4170
4397
|
`COUNT(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4171
4398
|
);
|
|
4172
4399
|
}
|
|
4173
|
-
function addCountFields(fields, countArg, alias, model
|
|
4400
|
+
function addCountFields(fields, countArg, alias, model) {
|
|
4174
4401
|
if (!isNotNullish(countArg)) return;
|
|
4175
4402
|
if (countArg === true) {
|
|
4176
4403
|
pushCountAllField(fields);
|
|
@@ -4184,7 +4411,7 @@ function addCountFields(fields, countArg, alias, model, fieldMap) {
|
|
|
4184
4411
|
([f, v]) => f !== "_all" && isTruthySelection(v)
|
|
4185
4412
|
);
|
|
4186
4413
|
for (const [f] of selected) {
|
|
4187
|
-
assertCountableScalarField(
|
|
4414
|
+
assertCountableScalarField(model, f);
|
|
4188
4415
|
pushCountField(fields, alias, f, model);
|
|
4189
4416
|
}
|
|
4190
4417
|
}
|
|
@@ -4192,19 +4419,12 @@ function getAggregateSelectionObject(args, agg) {
|
|
|
4192
4419
|
const obj = args[agg];
|
|
4193
4420
|
return isPlainObject(obj) ? obj : void 0;
|
|
4194
4421
|
}
|
|
4195
|
-
function assertAggregatableScalarField(
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
);
|
|
4201
|
-
}
|
|
4202
|
-
if (field.isRelation) {
|
|
4203
|
-
throw new Error(
|
|
4204
|
-
`Cannot use ${agg} on relation field '${fieldName}' on model ${model.name}`
|
|
4205
|
-
);
|
|
4422
|
+
function assertAggregatableScalarField(model, agg, fieldName) {
|
|
4423
|
+
if (agg === "_sum" || agg === "_avg") {
|
|
4424
|
+
assertNumericField(model, fieldName, agg);
|
|
4425
|
+
} else {
|
|
4426
|
+
assertScalarField(model, fieldName, agg);
|
|
4206
4427
|
}
|
|
4207
|
-
return field;
|
|
4208
4428
|
}
|
|
4209
4429
|
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
4210
4430
|
const outAlias = `${agg}.${fieldName}`;
|
|
@@ -4212,7 +4432,7 @@ function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
|
4212
4432
|
`${aggFn}(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
|
|
4213
4433
|
);
|
|
4214
4434
|
}
|
|
4215
|
-
function addAggregateFields(fields, args, alias, model
|
|
4435
|
+
function addAggregateFields(fields, args, alias, model) {
|
|
4216
4436
|
for (const [agg, aggFn] of AGGREGATES) {
|
|
4217
4437
|
const obj = getAggregateSelectionObject(args, agg);
|
|
4218
4438
|
if (!obj) continue;
|
|
@@ -4220,23 +4440,16 @@ function addAggregateFields(fields, args, alias, model, fieldMap) {
|
|
|
4220
4440
|
if (fieldName === "_all")
|
|
4221
4441
|
throw new Error(`'${agg}' does not support '_all'`);
|
|
4222
4442
|
if (!isTruthySelection(selection)) continue;
|
|
4223
|
-
|
|
4224
|
-
fieldMap,
|
|
4225
|
-
model,
|
|
4226
|
-
agg,
|
|
4227
|
-
fieldName
|
|
4228
|
-
);
|
|
4229
|
-
assertAggregateFieldType(agg, field.type, fieldName, model.name);
|
|
4443
|
+
assertAggregatableScalarField(model, agg, fieldName);
|
|
4230
4444
|
pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model);
|
|
4231
4445
|
}
|
|
4232
4446
|
}
|
|
4233
4447
|
}
|
|
4234
4448
|
function buildAggregateFields(args, alias, model) {
|
|
4235
4449
|
const fields = [];
|
|
4236
|
-
const fieldMap = getModelFieldMap(model);
|
|
4237
4450
|
const countArg = normalizeCountArg(args._count);
|
|
4238
|
-
addCountFields(fields, countArg, alias, model
|
|
4239
|
-
addAggregateFields(fields, args, alias, model
|
|
4451
|
+
addCountFields(fields, countArg, alias, model);
|
|
4452
|
+
addAggregateFields(fields, args, alias, model);
|
|
4240
4453
|
return fields;
|
|
4241
4454
|
}
|
|
4242
4455
|
function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
@@ -4260,7 +4473,7 @@ function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
|
4260
4473
|
validateParamConsistency(sql, whereResult.params);
|
|
4261
4474
|
return Object.freeze({
|
|
4262
4475
|
sql,
|
|
4263
|
-
params: Object.freeze(
|
|
4476
|
+
params: Object.freeze([...whereResult.params]),
|
|
4264
4477
|
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4265
4478
|
});
|
|
4266
4479
|
}
|
|
@@ -4273,32 +4486,24 @@ function assertGroupByBy(args, model) {
|
|
|
4273
4486
|
if (bySet.size !== byFields.length) {
|
|
4274
4487
|
throw new Error("buildGroupBySql: by must not contain duplicates");
|
|
4275
4488
|
}
|
|
4276
|
-
const modelFieldMap = getModelFieldMap(model);
|
|
4277
4489
|
for (const f of byFields) {
|
|
4278
|
-
|
|
4279
|
-
if (!field) {
|
|
4280
|
-
throw new Error(
|
|
4281
|
-
`groupBy.by references unknown field '${f}' on model ${model.name}`
|
|
4282
|
-
);
|
|
4283
|
-
}
|
|
4284
|
-
if (field.isRelation) {
|
|
4285
|
-
throw new Error(
|
|
4286
|
-
`groupBy.by does not support relation field '${f}' on model ${model.name}`
|
|
4287
|
-
);
|
|
4288
|
-
}
|
|
4490
|
+
assertScalarField(model, f, "groupBy.by");
|
|
4289
4491
|
}
|
|
4290
4492
|
return byFields;
|
|
4291
4493
|
}
|
|
4292
4494
|
function buildGroupBySelectParts(args, alias, model, byFields) {
|
|
4293
4495
|
const groupCols = byFields.map((f) => col(alias, f, model));
|
|
4496
|
+
const selectCols = byFields.map((f) => colWithAlias(alias, f, model));
|
|
4294
4497
|
const groupFields = groupCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4295
4498
|
const aggFields = buildAggregateFields(args, alias, model);
|
|
4296
|
-
const selectFields = isNonEmptyArray(aggFields) ?
|
|
4499
|
+
const selectFields = isNonEmptyArray(aggFields) ? selectCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : selectCols.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4297
4500
|
return { groupCols, groupFields, selectFields };
|
|
4298
4501
|
}
|
|
4299
4502
|
function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
4300
4503
|
if (!isNotNullish(args.having)) return "";
|
|
4301
|
-
if (!isPlainObject(args.having))
|
|
4504
|
+
if (!isPlainObject(args.having)) {
|
|
4505
|
+
throw new Error("having must be an object");
|
|
4506
|
+
}
|
|
4302
4507
|
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4303
4508
|
if (!h || h.trim().length === 0) return "";
|
|
4304
4509
|
return `${SQL_TEMPLATES.HAVING} ${h}`;
|
|
@@ -4334,61 +4539,58 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
|
4334
4539
|
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4335
4540
|
return Object.freeze({
|
|
4336
4541
|
sql,
|
|
4337
|
-
params: Object.freeze(
|
|
4542
|
+
params: Object.freeze(mergedParams),
|
|
4338
4543
|
paramMappings: Object.freeze([
|
|
4339
4544
|
...whereResult.paramMappings,
|
|
4340
4545
|
...snapshot.mappings
|
|
4341
4546
|
])
|
|
4342
4547
|
});
|
|
4343
4548
|
}
|
|
4344
|
-
function buildCountSql(whereResult, tableName, alias, skip,
|
|
4549
|
+
function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
4345
4550
|
assertSafeAlias(alias);
|
|
4346
4551
|
assertSafeTableRef(tableName);
|
|
4347
|
-
|
|
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
|
+
}
|
|
4348
4575
|
const whereClause = isValidWhereClause(whereResult.clause) ? `${SQL_TEMPLATES.WHERE} ${whereResult.clause}` : "";
|
|
4349
|
-
const params = createParamStore(whereResult.nextParamIndex);
|
|
4350
|
-
const baseSubSelect = [
|
|
4351
|
-
SQL_TEMPLATES.SELECT,
|
|
4352
|
-
"1",
|
|
4353
|
-
SQL_TEMPLATES.FROM,
|
|
4354
|
-
tableName,
|
|
4355
|
-
alias,
|
|
4356
|
-
whereClause
|
|
4357
|
-
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4358
|
-
const normalizedSkip = normalizeSkipLike(skip);
|
|
4359
|
-
const subSelect = applyCountSkip(baseSubSelect, normalizedSkip, params, d);
|
|
4360
4576
|
const sql = [
|
|
4361
4577
|
SQL_TEMPLATES.SELECT,
|
|
4362
4578
|
SQL_TEMPLATES.COUNT_ALL,
|
|
4363
4579
|
SQL_TEMPLATES.AS,
|
|
4364
4580
|
quote("_count._all"),
|
|
4365
4581
|
SQL_TEMPLATES.FROM,
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4582
|
+
tableName,
|
|
4583
|
+
alias,
|
|
4584
|
+
whereClause
|
|
4369
4585
|
].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
|
|
4370
4586
|
validateSelectQuery(sql);
|
|
4371
|
-
|
|
4372
|
-
const mergedParams = [...whereResult.params, ...snapshot.params];
|
|
4373
|
-
validateParamConsistency(sql, mergedParams);
|
|
4587
|
+
validateParamConsistency(sql, whereResult.params);
|
|
4374
4588
|
return Object.freeze({
|
|
4375
4589
|
sql,
|
|
4376
|
-
params: Object.freeze(
|
|
4377
|
-
paramMappings: Object.freeze([
|
|
4378
|
-
...whereResult.paramMappings,
|
|
4379
|
-
...snapshot.mappings
|
|
4380
|
-
])
|
|
4590
|
+
params: Object.freeze([...whereResult.params]),
|
|
4591
|
+
paramMappings: Object.freeze([...whereResult.paramMappings])
|
|
4381
4592
|
});
|
|
4382
4593
|
}
|
|
4383
|
-
function applyCountSkip(subSelect, normalizedSkip, params, dialect) {
|
|
4384
|
-
const shouldApply = schemaParser.isDynamicParameter(normalizedSkip) || typeof normalizedSkip === "number" && normalizedSkip > 0;
|
|
4385
|
-
if (!shouldApply) return subSelect;
|
|
4386
|
-
const placeholder = addAutoScoped(params, normalizedSkip, "count.skip");
|
|
4387
|
-
if (dialect === "sqlite") {
|
|
4388
|
-
return `${subSelect} ${SQL_TEMPLATES.LIMIT} -1 ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4389
|
-
}
|
|
4390
|
-
return `${subSelect} ${SQL_TEMPLATES.OFFSET} ${placeholder}`;
|
|
4391
|
-
}
|
|
4392
4594
|
function safeAlias(input) {
|
|
4393
4595
|
const raw = String(input).toLowerCase();
|
|
4394
4596
|
const cleaned = raw.replace(/[^a-z0-9_]/g, "_");
|
|
@@ -4538,8 +4740,12 @@ function buildAndNormalizeSql(args) {
|
|
|
4538
4740
|
);
|
|
4539
4741
|
}
|
|
4540
4742
|
function finalizeDirective(args) {
|
|
4541
|
-
const { directive, normalizedSql, normalizedMappings } = args;
|
|
4542
|
-
|
|
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);
|
|
4543
4749
|
const { staticParams, dynamicKeys } = buildParamsFromMappings(normalizedMappings);
|
|
4544
4750
|
return {
|
|
4545
4751
|
method: directive.method,
|
|
@@ -4576,7 +4782,8 @@ function generateSQL(directive) {
|
|
|
4576
4782
|
return finalizeDirective({
|
|
4577
4783
|
directive,
|
|
4578
4784
|
normalizedSql: normalized.sql,
|
|
4579
|
-
normalizedMappings: normalized.paramMappings
|
|
4785
|
+
normalizedMappings: normalized.paramMappings,
|
|
4786
|
+
dialect
|
|
4580
4787
|
});
|
|
4581
4788
|
}
|
|
4582
4789
|
function generateSQL2(directive) {
|
|
@@ -4684,18 +4891,57 @@ function generateCode(models, queries, dialect, datamodel) {
|
|
|
4684
4891
|
return `// Generated by @prisma-sql/generator - DO NOT EDIT
|
|
4685
4892
|
import { buildSQL, transformQueryResults, type PrismaMethod, type Model } from 'prisma-sql'
|
|
4686
4893
|
|
|
4687
|
-
|
|
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
|
+
}
|
|
4688
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
|
+
}
|
|
4689
4908
|
return value.toISOString()
|
|
4690
4909
|
}
|
|
4691
|
-
|
|
4910
|
+
if (typeof value === 'bigint') {
|
|
4911
|
+
return value.toString()
|
|
4912
|
+
}
|
|
4692
4913
|
if (Array.isArray(value)) {
|
|
4693
|
-
|
|
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
|
|
4694
4940
|
}
|
|
4695
|
-
|
|
4696
4941
|
return value
|
|
4697
4942
|
}
|
|
4698
4943
|
|
|
4944
|
+
|
|
4699
4945
|
export const MODELS: Model[] = ${JSON.stringify(cleanModels, null, 2)}
|
|
4700
4946
|
|
|
4701
4947
|
const ENUM_MAPPINGS: Record<string, Record<string, string>> = ${JSON.stringify(mappings, null, 2)}
|