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