prisma-sql 1.44.0 → 1.46.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.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.44.0",
57
+ version: "1.46.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 SQL_RESERVED_WORDS = /* @__PURE__ */ new Set([
160
+ var ALIAS_FORBIDDEN_KEYWORDS = /* @__PURE__ */ new Set([
161
161
  "select",
162
162
  "from",
163
163
  "where",
164
- "and",
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
- "on",
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 SQL_KEYWORDS = SQL_RESERVED_WORDS;
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
- function normalizeValue(value) {
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
- return value.map(normalizeValue);
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 SCALAR_SET_CACHE = /* @__PURE__ */ new WeakMap();
555
- var RELATION_SET_CACHE = /* @__PURE__ */ new WeakMap();
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
- const cached = SCALAR_SET_CACHE.get(model);
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
- const cached = RELATION_SET_CACHE.get(model);
566
- if (cached) return cached;
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
- const cached = COLUMN_MAP_CACHE.get(model);
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
@@ -591,20 +642,21 @@ function isEmptyWhere(where) {
591
642
  if (!isNotNullish(where)) return true;
592
643
  return Object.keys(where).length === 0;
593
644
  }
645
+ function sqlPreview(sql) {
646
+ const s = String(sql);
647
+ if (s.length <= 160) return s;
648
+ return `${s.slice(0, 160)}...`;
649
+ }
594
650
  function validateSelectQuery(sql) {
595
651
  if (!hasValidContent(sql)) {
596
652
  throw new Error("CRITICAL: Generated empty SQL query");
597
653
  }
598
654
  if (!hasRequiredKeywords(sql)) {
599
- throw new Error(
600
- `CRITICAL: Invalid SQL structure. SQL: ${sql.substring(0, 100)}...`
601
- );
655
+ throw new Error(`CRITICAL: Invalid SQL structure. SQL: ${sqlPreview(sql)}`);
602
656
  }
603
657
  }
604
- function sqlPreview(sql) {
605
- return `${sql.substring(0, 100)}...`;
606
- }
607
- function parseDollarNumber(sql, start, n) {
658
+ function parseDollarNumber(sql, start) {
659
+ const n = sql.length;
608
660
  let i = start;
609
661
  let num = 0;
610
662
  let hasDigit = false;
@@ -615,14 +667,14 @@ function parseDollarNumber(sql, start, n) {
615
667
  num = num * 10 + (c - 48);
616
668
  i++;
617
669
  }
618
- if (!hasDigit || num <= 0) return { next: i, num: 0, ok: false };
619
- return { next: i, num, ok: true };
670
+ if (!hasDigit || num <= 0) return { next: i, num: 0 };
671
+ return { next: i, num };
620
672
  }
621
673
  function scanDollarPlaceholders(sql, markUpTo) {
622
674
  const seen = new Uint8Array(markUpTo + 1);
623
- let count = 0;
624
675
  let min = Number.POSITIVE_INFINITY;
625
676
  let max = 0;
677
+ let sawAny = false;
626
678
  const n = sql.length;
627
679
  let i = 0;
628
680
  while (i < n) {
@@ -630,17 +682,21 @@ function scanDollarPlaceholders(sql, markUpTo) {
630
682
  i++;
631
683
  continue;
632
684
  }
633
- const { next, num, ok } = parseDollarNumber(sql, i + 1, n);
634
- i = next;
635
- if (!ok) continue;
636
- count++;
685
+ const parsed = parseDollarNumber(sql, i + 1);
686
+ i = parsed.next;
687
+ const num = parsed.num;
688
+ if (num === 0) continue;
689
+ sawAny = true;
637
690
  if (num < min) min = num;
638
691
  if (num > max) max = num;
639
692
  if (num <= markUpTo) seen[num] = 1;
640
693
  }
641
- return { count, min, max, seen };
694
+ if (!sawAny) {
695
+ return { min: 0, max: 0, seen, sawAny: false };
696
+ }
697
+ return { min, max, seen, sawAny: true };
642
698
  }
643
- function assertNoGaps(scan, rangeMin, rangeMax, sql) {
699
+ function assertNoGapsDollar(scan, rangeMin, rangeMax, sql) {
644
700
  for (let k = rangeMin; k <= rangeMax; k++) {
645
701
  if (scan.seen[k] !== 1) {
646
702
  throw new Error(
@@ -651,174 +707,75 @@ function assertNoGaps(scan, rangeMin, rangeMax, sql) {
651
707
  }
652
708
  function validateParamConsistency(sql, params) {
653
709
  const paramLen = params.length;
654
- if (paramLen === 0) {
655
- if (sql.indexOf("$") === -1) return;
656
- }
657
710
  const scan = scanDollarPlaceholders(sql, paramLen);
658
- if (scan.count === 0) {
659
- if (paramLen !== 0) {
711
+ if (paramLen === 0) {
712
+ if (scan.sawAny) {
660
713
  throw new Error(
661
- `CRITICAL: Parameter mismatch - SQL has no placeholders but ${paramLen} params provided.`
714
+ `CRITICAL: SQL contains placeholders but params is empty. SQL: ${sqlPreview(sql)}`
662
715
  );
663
716
  }
664
717
  return;
665
718
  }
666
- if (scan.max !== paramLen) {
719
+ if (!scan.sawAny) {
667
720
  throw new Error(
668
- `CRITICAL: Parameter mismatch - SQL max placeholder is $${scan.max} but ${paramLen} params provided. This will cause SQL execution to fail. SQL: ${sqlPreview(sql)}`
721
+ `CRITICAL: SQL is missing placeholders ($1..$${paramLen}) but params has length ${paramLen}. SQL: ${sqlPreview(sql)}`
669
722
  );
670
723
  }
671
- assertNoGaps(scan, 1, scan.max, sql);
672
- }
673
- function needsQuoting(id) {
674
- if (!isNonEmptyString(id)) return true;
675
- const isKeyword = SQL_KEYWORDS.has(id.toLowerCase());
676
- if (isKeyword) return true;
677
- const isValidIdentifier = REGEX_CACHE.VALID_IDENTIFIER.test(id);
678
- return !isValidIdentifier;
679
- }
680
- function validateParamConsistencyFragment(sql, params) {
681
- const paramLen = params.length;
682
- const scan = scanDollarPlaceholders(sql, paramLen);
683
- if (scan.max === 0) return;
684
- if (scan.max > paramLen) {
724
+ if (scan.min !== 1) {
685
725
  throw new Error(
686
- `CRITICAL: Parameter mismatch - SQL references $${scan.max} but only ${paramLen} params provided. SQL: ${sqlPreview(sql)}`
726
+ `CRITICAL: Placeholder range must start at $1, got min=$${scan.min}. SQL: ${sqlPreview(sql)}`
687
727
  );
688
728
  }
689
- assertNoGaps(scan, scan.min, scan.max, sql);
690
- }
691
- function assertOrThrow(condition, message) {
692
- if (!condition) throw new Error(message);
693
- }
694
- function dialectPlaceholderPrefix(dialect) {
695
- return dialect === "sqlite" ? "?" : "$";
696
- }
697
- function parseSqlitePlaceholderIndices(sql) {
698
- const re = /\?(?:(\d+))?/g;
699
- const indices = [];
700
- let anonCount = 0;
701
- let sawNumbered = false;
702
- let sawAnonymous = false;
703
- for (const m of sql.matchAll(re)) {
704
- const n = m[1];
705
- if (n) {
706
- sawNumbered = true;
707
- indices.push(parseInt(n, 10));
708
- } else {
709
- sawAnonymous = true;
710
- anonCount += 1;
711
- indices.push(anonCount);
712
- }
713
- }
714
- return { indices, sawNumbered, sawAnonymous };
715
- }
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
- function maxIndex(indices) {
731
- return indices.length > 0 ? Math.max(...indices) : 0;
732
- }
733
- function ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous) {
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);
747
- for (let i = 1; i <= max; i++) {
748
- assertOrThrow(
749
- placeholders.has(i),
750
- `CRITICAL: Missing SQL placeholder ${prefix}${i} - placeholders must be sequential 1..${max}.`
729
+ if (scan.max !== paramLen) {
730
+ throw new Error(
731
+ `CRITICAL: Placeholder max must match params length. max=$${scan.max}, params=${paramLen}. SQL: ${sqlPreview(sql)}`
751
732
  );
752
733
  }
734
+ assertNoGapsDollar(scan, 1, paramLen, sql);
753
735
  }
754
- function validateMappingIndex(mapping, max) {
755
- assertOrThrow(
756
- Number.isInteger(mapping.index) && mapping.index >= 1 && mapping.index <= max,
757
- `CRITICAL: ParamMapping index ${mapping.index} out of range 1..${max}.`
758
- );
759
- }
760
- function ensureUniqueMappingIndex(mappingIndices, index, dialect) {
761
- assertOrThrow(
762
- !mappingIndices.has(index),
763
- `CRITICAL: Duplicate ParamMapping index ${index} - each placeholder index must map to exactly one ParamMap.`
764
- );
765
- mappingIndices.add(index);
766
- }
767
- function ensureMappingIndexExistsInSql(placeholders, index) {
768
- assertOrThrow(
769
- placeholders.has(index),
770
- `CRITICAL: ParamMapping index ${index} not found in SQL placeholders.`
771
- );
772
- }
773
- function validateMappingValueShape(mapping) {
774
- assertOrThrow(
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
- );
736
+ function countQuestionMarkPlaceholders(sql) {
737
+ const s = String(sql);
738
+ let count = 0;
739
+ for (let i = 0; i < s.length; i++) {
740
+ if (s.charCodeAt(i) === 63) count++;
741
+ }
742
+ return count;
782
743
  }
783
- function ensureMappingsCoverAllIndices(mappingIndices, max, dialect) {
784
- const prefix = dialectPlaceholderPrefix(dialect);
785
- for (let i = 1; i <= max; i++) {
786
- assertOrThrow(
787
- mappingIndices.has(i),
788
- `CRITICAL: Missing ParamMap for placeholder ${prefix}${i} - mappings must cover 1..${max} with no gaps.`
744
+ function validateQuestionMarkConsistency(sql, params) {
745
+ const expected = params.length;
746
+ const found = countQuestionMarkPlaceholders(sql);
747
+ if (expected !== found) {
748
+ throw new Error(
749
+ `CRITICAL: Parameter mismatch - expected ${expected} '?' placeholders, found ${found}. SQL: ${sqlPreview(sql)}`
789
750
  );
790
751
  }
791
752
  }
792
- function validateMappingsAgainstPlaceholders(mappings, placeholders, max, dialect) {
793
- const mappingIndices = /* @__PURE__ */ new Set();
794
- for (const mapping of mappings) {
795
- validateMappingIndex(mapping, max);
796
- ensureUniqueMappingIndex(mappingIndices, mapping.index);
797
- ensureMappingIndexExistsInSql(placeholders, mapping.index);
798
- validateMappingValueShape(mapping);
753
+ function validateParamConsistencyByDialect(sql, params, dialect) {
754
+ if (dialect === "postgres") {
755
+ validateParamConsistency(sql, params);
756
+ return;
799
757
  }
800
- ensureMappingsCoverAllIndices(mappingIndices, max, dialect);
801
- }
802
- function validateSqlPositions(sql, mappings, dialect) {
803
- const { indices, sawNumbered, sawAnonymous } = getPlaceholderIndices(
804
- sql,
805
- dialect
806
- );
807
758
  if (dialect === "sqlite") {
808
- ensureNoMixedSqlitePlaceholders(sawNumbered, sawAnonymous);
759
+ validateQuestionMarkConsistency(sql, params);
760
+ return;
761
+ }
762
+ if (dialect === "mysql" || dialect === "mariadb") {
763
+ validateQuestionMarkConsistency(sql, params);
764
+ return;
809
765
  }
810
- const placeholders = new Set(indices);
811
- if (placeholders.size === 0 && mappings.length === 0) return;
812
- const max = maxIndex(indices);
813
- ensurePlaceholderMaxMatchesMappingsLength(max, mappings.length, dialect);
814
- ensureSequentialPlaceholders(placeholders, max, dialect);
815
- validateMappingsAgainstPlaceholders(mappings, placeholders, max, dialect);
766
+ validateParamConsistency(sql, params);
767
+ }
768
+ function needsQuoting(identifier) {
769
+ const s = String(identifier);
770
+ if (!REGEX_CACHE.VALID_IDENTIFIER.test(s)) return true;
771
+ const lowered = s.toLowerCase();
772
+ if (SQL_KEYWORDS.has(lowered)) return true;
773
+ return false;
816
774
  }
817
775
 
818
776
  // src/builder/shared/sql-utils.ts
819
- var NUL = String.fromCharCode(0);
820
777
  function containsControlChars(s) {
821
- return s.includes(NUL) || s.includes("\n") || s.includes("\r");
778
+ return /[\u0000-\u001F\u007F]/.test(s);
822
779
  }
823
780
  function assertNoControlChars(label, s) {
824
781
  if (containsControlChars(s)) {
@@ -1037,16 +994,46 @@ function buildTableReference(schemaName, tableName, dialect) {
1037
994
  return `"${safeSchema}"."${safeTable}"`;
1038
995
  }
1039
996
  function assertSafeAlias(alias) {
1040
- const a = String(alias);
1041
- if (a.trim().length === 0) {
1042
- throw new Error("alias is required and cannot be empty");
997
+ if (typeof alias !== "string") {
998
+ throw new Error(`Invalid alias: expected string, got ${typeof alias}`);
999
+ }
1000
+ const a = alias.trim();
1001
+ if (a.length === 0) {
1002
+ throw new Error("Invalid alias: required and cannot be empty");
1003
+ }
1004
+ if (a !== alias) {
1005
+ throw new Error("Invalid alias: leading/trailing whitespace");
1043
1006
  }
1044
- if (containsControlChars(a) || a.includes(";")) {
1045
- throw new Error(`alias contains unsafe characters: ${JSON.stringify(a)}`);
1007
+ if (/[\u0000-\u001F\u007F]/.test(a)) {
1008
+ throw new Error(
1009
+ "Invalid alias: contains unsafe characters (control characters)"
1010
+ );
1011
+ }
1012
+ if (a.includes('"') || a.includes("'") || a.includes("`")) {
1013
+ throw new Error("Invalid alias: contains unsafe characters (quotes)");
1014
+ }
1015
+ if (a.includes(";")) {
1016
+ throw new Error("Invalid alias: contains unsafe characters (semicolon)");
1017
+ }
1018
+ if (a.includes("--") || a.includes("/*") || a.includes("*/")) {
1019
+ throw new Error(
1020
+ "Invalid alias: contains unsafe characters (SQL comment tokens)"
1021
+ );
1022
+ }
1023
+ if (/\s/.test(a)) {
1024
+ throw new Error(
1025
+ "Invalid alias: must be a simple identifier without whitespace"
1026
+ );
1027
+ }
1028
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(a)) {
1029
+ throw new Error(
1030
+ `Invalid alias: must be a simple identifier (alphanumeric with underscores): "${alias}"`
1031
+ );
1046
1032
  }
1047
- if (!/^[A-Za-z_]\w*$/.test(a)) {
1033
+ const lowered = a.toLowerCase();
1034
+ if (ALIAS_FORBIDDEN_KEYWORDS.has(lowered)) {
1048
1035
  throw new Error(
1049
- `alias must be a simple identifier, got: ${JSON.stringify(a)}`
1036
+ `Invalid alias: '${alias}' is a SQL keyword that would break query parsing. Forbidden aliases: ${[...ALIAS_FORBIDDEN_KEYWORDS].join(", ")}`
1050
1037
  );
1051
1038
  }
1052
1039
  }
@@ -1088,7 +1075,9 @@ function isValidRelationField(field) {
1088
1075
  if (fk.length === 0) return false;
1089
1076
  const refsRaw = field.references;
1090
1077
  const refs = normalizeKeyList(refsRaw);
1091
- if (refs.length === 0) return false;
1078
+ if (refs.length === 0) {
1079
+ return fk.length === 1;
1080
+ }
1092
1081
  if (refs.length !== fk.length) return false;
1093
1082
  return true;
1094
1083
  }
@@ -1103,6 +1092,8 @@ function getReferenceFieldNames(field, foreignKeyCount) {
1103
1092
  return refs;
1104
1093
  }
1105
1094
  function joinCondition(field, parentModel, childModel, parentAlias, childAlias) {
1095
+ assertSafeAlias(parentAlias);
1096
+ assertSafeAlias(childAlias);
1106
1097
  const fkFields = normalizeKeyList(field.foreignKey);
1107
1098
  if (fkFields.length === 0) {
1108
1099
  throw createError(
@@ -1252,6 +1243,66 @@ function normalizeOrderByInput(orderBy, parseValue) {
1252
1243
  throw new Error("orderBy must be an object or array of objects");
1253
1244
  }
1254
1245
 
1246
+ // src/builder/shared/order-by-determinism.ts
1247
+ function modelHasScalarId(model) {
1248
+ if (!model) return false;
1249
+ return getScalarFieldSet(model).has("id");
1250
+ }
1251
+ function hasIdTiebreaker(orderBy, parse) {
1252
+ if (!isNotNullish(orderBy)) return false;
1253
+ const normalized = normalizeOrderByInput(orderBy, parse);
1254
+ return normalized.some(
1255
+ (obj) => Object.prototype.hasOwnProperty.call(obj, "id")
1256
+ );
1257
+ }
1258
+ function addIdTiebreaker(orderBy) {
1259
+ if (Array.isArray(orderBy)) return [...orderBy, { id: "asc" }];
1260
+ return [orderBy, { id: "asc" }];
1261
+ }
1262
+ function ensureDeterministicOrderByInput(args) {
1263
+ const { orderBy, model, parseValue } = args;
1264
+ if (!modelHasScalarId(model)) return orderBy;
1265
+ if (!isNotNullish(orderBy)) {
1266
+ return { id: "asc" };
1267
+ }
1268
+ if (hasIdTiebreaker(orderBy, parseValue)) return orderBy;
1269
+ return addIdTiebreaker(orderBy);
1270
+ }
1271
+
1272
+ // src/builder/shared/validators/field-assertions.ts
1273
+ var NUMERIC_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Decimal", "BigInt"]);
1274
+ function assertScalarField(model, fieldName, context) {
1275
+ const field = getFieldInfo(model, fieldName);
1276
+ if (!field) {
1277
+ throw createError(
1278
+ `${context} references unknown field '${fieldName}' on model ${model.name}`,
1279
+ {
1280
+ field: fieldName,
1281
+ modelName: model.name,
1282
+ availableFields: model.fields.map((f) => f.name)
1283
+ }
1284
+ );
1285
+ }
1286
+ if (field.isRelation) {
1287
+ throw createError(
1288
+ `${context} does not support relation field '${fieldName}'`,
1289
+ { field: fieldName, modelName: model.name }
1290
+ );
1291
+ }
1292
+ return field;
1293
+ }
1294
+ function assertNumericField(model, fieldName, context) {
1295
+ const field = assertScalarField(model, fieldName, context);
1296
+ const baseType = field.type.replace(/\[\]|\?/g, "");
1297
+ if (!NUMERIC_TYPES.has(baseType)) {
1298
+ throw createError(
1299
+ `${context} requires numeric field, got '${field.type}'`,
1300
+ { field: fieldName, modelName: model.name }
1301
+ );
1302
+ }
1303
+ return field;
1304
+ }
1305
+
1255
1306
  // src/builder/pagination.ts
1256
1307
  var MAX_LIMIT_OFFSET = 2147483647;
1257
1308
  function parseDirectionRaw(raw, errorLabel) {
@@ -1292,30 +1343,31 @@ function parseOrderByValue(v, fieldName) {
1292
1343
  assertAllowedOrderByKeys(obj, fieldName);
1293
1344
  return { direction, nulls };
1294
1345
  }
1295
- function normalizeFiniteInteger(name, v) {
1296
- if (typeof v !== "number" || !Number.isFinite(v) || !Number.isInteger(v)) {
1297
- throw new Error(`${name} must be an integer`);
1298
- }
1299
- return v;
1300
- }
1301
1346
  function normalizeNonNegativeInt(name, v) {
1302
1347
  if (isDynamicParameter(v)) return v;
1303
- const n = normalizeFiniteInteger(name, v);
1304
- if (n < 0) {
1305
- throw new Error(`${name} must be >= 0`);
1306
- }
1307
- if (n > MAX_LIMIT_OFFSET) {
1308
- throw new Error(`${name} must be <= ${MAX_LIMIT_OFFSET}`);
1309
- }
1310
- return n;
1348
+ const result = normalizeIntLike(name, v, {
1349
+ min: 0,
1350
+ max: MAX_LIMIT_OFFSET,
1351
+ allowZero: true
1352
+ });
1353
+ if (result === void 0)
1354
+ throw new Error(`${name} normalization returned undefined`);
1355
+ return result;
1356
+ }
1357
+ function normalizeIntAllowNegative(name, v) {
1358
+ if (isDynamicParameter(v)) return v;
1359
+ const result = normalizeIntLike(name, v, {
1360
+ min: Number.MIN_SAFE_INTEGER,
1361
+ max: MAX_LIMIT_OFFSET,
1362
+ allowZero: true
1363
+ });
1364
+ if (result === void 0)
1365
+ throw new Error(`${name} normalization returned undefined`);
1366
+ return result;
1311
1367
  }
1312
1368
  function hasNonNullishProp(v, key) {
1313
1369
  return isPlainObject(v) && key in v && isNotNullish(v[key]);
1314
1370
  }
1315
- function normalizeIntegerOrDynamic(name, v) {
1316
- if (isDynamicParameter(v)) return v;
1317
- return normalizeFiniteInteger(name, v);
1318
- }
1319
1371
  function readSkipTake(relArgs) {
1320
1372
  const hasSkip = hasNonNullishProp(relArgs, "skip");
1321
1373
  const hasTake = hasNonNullishProp(relArgs, "take");
@@ -1329,7 +1381,7 @@ function readSkipTake(relArgs) {
1329
1381
  }
1330
1382
  const obj = relArgs;
1331
1383
  const skipVal = hasSkip ? normalizeNonNegativeInt("skip", obj.skip) : void 0;
1332
- const takeVal = hasTake ? normalizeIntegerOrDynamic("take", obj.take) : void 0;
1384
+ const takeVal = hasTake ? normalizeIntAllowNegative("take", obj.take) : void 0;
1333
1385
  return { hasSkip, hasTake, skipVal, takeVal };
1334
1386
  }
1335
1387
  function buildOrderByFragment(entries, alias, dialect, model) {
@@ -1355,9 +1407,7 @@ function buildOrderByFragment(entries, alias, dialect, model) {
1355
1407
  return out.join(SQL_SEPARATORS.ORDER_BY);
1356
1408
  }
1357
1409
  function defaultNullsFor(dialect, direction) {
1358
- if (dialect === "postgres") {
1359
- return direction === "asc" ? "last" : "first";
1360
- }
1410
+ if (dialect === "postgres") return direction === "asc" ? "last" : "first";
1361
1411
  return direction === "asc" ? "first" : "last";
1362
1412
  }
1363
1413
  function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
@@ -1374,13 +1424,12 @@ function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
1374
1424
  }
1375
1425
  function buildCursorFilterParts(cursor, cursorAlias, params, model) {
1376
1426
  const entries = Object.entries(cursor);
1377
- if (entries.length === 0) {
1427
+ if (entries.length === 0)
1378
1428
  throw new Error("cursor must have at least one field");
1379
- }
1380
1429
  const placeholdersByField = /* @__PURE__ */ new Map();
1381
1430
  const parts = [];
1382
1431
  for (const [field, value] of entries) {
1383
- const c = `${cursorAlias}.${quote(field)}`;
1432
+ const c = `${cursorAlias}.${quoteColumn(model, field)}`;
1384
1433
  if (value === null) {
1385
1434
  parts.push(`${c} IS NULL`);
1386
1435
  continue;
@@ -1394,13 +1443,6 @@ function buildCursorFilterParts(cursor, cursorAlias, params, model) {
1394
1443
  placeholdersByField
1395
1444
  };
1396
1445
  }
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
1446
  function buildCursorEqualityExpr(columnExpr, valueExpr) {
1405
1447
  return `((${valueExpr} IS NULL AND ${columnExpr} IS NULL) OR (${valueExpr} IS NOT NULL AND ${columnExpr} = ${valueExpr}))`;
1406
1448
  }
@@ -1437,26 +1479,70 @@ function buildOrderEntries(orderBy) {
1437
1479
  if (typeof value === "string") {
1438
1480
  entries.push({ field, direction: value });
1439
1481
  } else {
1440
- entries.push({
1441
- field,
1442
- direction: value.sort,
1443
- nulls: value.nulls
1444
- });
1482
+ entries.push({ field, direction: value.direction, nulls: value.nulls });
1445
1483
  }
1446
1484
  }
1447
1485
  }
1448
1486
  return entries;
1449
1487
  }
1488
+ function buildCursorCteSelectList(cursorEntries, orderEntries, model) {
1489
+ const seen = /* @__PURE__ */ new Set();
1490
+ const ordered = [];
1491
+ for (const [f] of cursorEntries) {
1492
+ if (!seen.has(f)) {
1493
+ seen.add(f);
1494
+ ordered.push(f);
1495
+ }
1496
+ }
1497
+ for (const e of orderEntries) {
1498
+ if (!seen.has(e.field)) {
1499
+ seen.add(e.field);
1500
+ ordered.push(e.field);
1501
+ }
1502
+ }
1503
+ if (ordered.length === 0) throw new Error("cursor cte select list is empty");
1504
+ return ordered.map((f) => quoteColumn(model, f)).join(SQL_SEPARATORS.FIELD_LIST);
1505
+ }
1506
+ function truncateIdent(name, maxLen) {
1507
+ const s = String(name);
1508
+ if (s.length <= maxLen) return s;
1509
+ return s.slice(0, maxLen);
1510
+ }
1511
+ function buildCursorNames(outerAlias) {
1512
+ const maxLen = 63;
1513
+ const base = outerAlias.toLowerCase();
1514
+ const cteName = truncateIdent(`__tp_cursor_${base}`, maxLen);
1515
+ const srcAlias = truncateIdent(`__tp_cursor_src_${base}`, maxLen);
1516
+ if (cteName === outerAlias || srcAlias === outerAlias) {
1517
+ return {
1518
+ cteName: truncateIdent(`__tp_cursor_${base}_x`, maxLen),
1519
+ srcAlias: truncateIdent(`__tp_cursor_src_${base}_x`, maxLen)
1520
+ };
1521
+ }
1522
+ return { cteName, srcAlias };
1523
+ }
1524
+ function assertCursorAndOrderFieldsScalar(model, cursor, orderEntries) {
1525
+ if (!model) return;
1526
+ for (const k of Object.keys(cursor)) assertScalarField(model, k, "cursor");
1527
+ for (const e of orderEntries) assertScalarField(model, e.field, "orderBy");
1528
+ }
1450
1529
  function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect, model) {
1451
1530
  var _a;
1531
+ assertSafeTableRef(tableName);
1532
+ assertSafeAlias(alias);
1452
1533
  const d = dialect != null ? dialect : getGlobalDialect();
1453
1534
  const cursorEntries = Object.entries(cursor);
1454
- if (cursorEntries.length === 0) {
1535
+ if (cursorEntries.length === 0)
1455
1536
  throw new Error("cursor must have at least one field");
1456
- }
1457
- const cursorAlias = "__tp_cursor_src";
1458
- const { whereSql: cursorWhereSql, placeholdersByField } = buildCursorFilterParts(cursor, cursorAlias, params);
1459
- let orderEntries = buildOrderEntries(orderBy);
1537
+ const { cteName, srcAlias } = buildCursorNames(alias);
1538
+ assertSafeAlias(cteName);
1539
+ assertSafeAlias(srcAlias);
1540
+ const deterministicOrderBy = ensureDeterministicOrderByInput({
1541
+ orderBy,
1542
+ model,
1543
+ parseValue: parseOrderByValue
1544
+ });
1545
+ let orderEntries = buildOrderEntries(deterministicOrderBy);
1460
1546
  if (orderEntries.length === 0) {
1461
1547
  orderEntries = cursorEntries.map(([field]) => ({
1462
1548
  field,
@@ -1465,11 +1551,23 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
1465
1551
  } else {
1466
1552
  orderEntries = ensureCursorFieldsInOrder(orderEntries, cursorEntries);
1467
1553
  }
1468
- const existsExpr = buildCursorRowExistsExpr(
1469
- tableName,
1470
- cursorAlias,
1471
- cursorWhereSql
1554
+ assertCursorAndOrderFieldsScalar(model, cursor, orderEntries);
1555
+ const { whereSql: cursorWhereSql, placeholdersByField } = buildCursorFilterParts(cursor, srcAlias, params, model);
1556
+ const cursorOrderBy = orderEntries.map(
1557
+ (e) => `${srcAlias}.${quoteColumn(model, e.field)} ${e.direction.toUpperCase()}`
1558
+ ).join(", ");
1559
+ const selectList = buildCursorCteSelectList(
1560
+ cursorEntries,
1561
+ orderEntries,
1562
+ model
1472
1563
  );
1564
+ const cte = `${cteName} AS (
1565
+ SELECT ${selectList} FROM ${tableName} ${srcAlias}
1566
+ WHERE ${cursorWhereSql}
1567
+ ORDER BY ${cursorOrderBy}
1568
+ LIMIT 1
1569
+ )`;
1570
+ const existsExpr = `EXISTS (SELECT 1 FROM ${cteName})`;
1473
1571
  const outerCursorMatch = buildOuterCursorMatch(
1474
1572
  cursor,
1475
1573
  alias,
@@ -1477,34 +1575,29 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
1477
1575
  params,
1478
1576
  model
1479
1577
  );
1578
+ const getValueExpr = (field) => `(SELECT ${quoteColumn(model, field)} FROM ${cteName})`;
1480
1579
  const orClauses = [];
1481
1580
  for (let level = 0; level < orderEntries.length; level++) {
1482
1581
  const andParts = [];
1483
1582
  for (let i = 0; i < level; i++) {
1484
1583
  const e2 = orderEntries[i];
1485
1584
  const c2 = col(alias, e2.field, model);
1486
- const v2 = cursorValueExpr(
1487
- tableName,
1488
- cursorAlias,
1489
- cursorWhereSql,
1490
- e2.field);
1585
+ const v2 = getValueExpr(e2.field);
1491
1586
  andParts.push(buildCursorEqualityExpr(c2, v2));
1492
1587
  }
1493
1588
  const e = orderEntries[level];
1494
1589
  const c = col(alias, e.field, model);
1495
- const v = cursorValueExpr(
1496
- tableName,
1497
- cursorAlias,
1498
- cursorWhereSql,
1499
- e.field);
1590
+ const v = getValueExpr(e.field);
1500
1591
  const nulls = (_a = e.nulls) != null ? _a : defaultNullsFor(d, e.direction);
1501
1592
  andParts.push(buildCursorInequalityExpr(c, e.direction, nulls, v));
1502
1593
  orClauses.push(`(${andParts.join(SQL_SEPARATORS.CONDITION_AND)})`);
1503
1594
  }
1504
1595
  const exclusive = orClauses.join(SQL_SEPARATORS.CONDITION_OR);
1505
- return `(${existsExpr} ${SQL_SEPARATORS.CONDITION_AND} ((${exclusive})${SQL_SEPARATORS.CONDITION_OR}(${outerCursorMatch})))`;
1596
+ const condition = `(${existsExpr} ${SQL_SEPARATORS.CONDITION_AND} ((${exclusive})${SQL_SEPARATORS.CONDITION_OR}(${outerCursorMatch})))`;
1597
+ return { cte, condition };
1506
1598
  }
1507
1599
  function buildOrderBy(orderBy, alias, dialect, model) {
1600
+ assertSafeAlias(alias);
1508
1601
  const entries = buildOrderEntries(orderBy);
1509
1602
  if (entries.length === 0) return "";
1510
1603
  const d = dialect != null ? dialect : getGlobalDialect();
@@ -1526,9 +1619,7 @@ function normalizeTakeLike(v) {
1526
1619
  max: MAX_LIMIT_OFFSET,
1527
1620
  allowZero: true
1528
1621
  });
1529
- if (typeof n === "number") {
1530
- if (n === 0) return 0;
1531
- }
1622
+ if (typeof n === "number" && n === 0) return 0;
1532
1623
  return n;
1533
1624
  }
1534
1625
  function normalizeSkipLike(v) {
@@ -1604,6 +1695,11 @@ function buildScalarOperator(expr, op, val, params, mode, fieldType, dialect) {
1604
1695
  }
1605
1696
  return handleInOperator(expr, op, val, params, dialect);
1606
1697
  }
1698
+ if (op === Ops.EQUALS && mode === Modes.INSENSITIVE && !isNotNullish(dialect)) {
1699
+ throw createError(`Insensitive equals requires a SQL dialect`, {
1700
+ operator: op
1701
+ });
1702
+ }
1607
1703
  return handleComparisonOperator(expr, op, val, params);
1608
1704
  }
1609
1705
  function handleNullValue(expr, op) {
@@ -1619,6 +1715,28 @@ function normalizeMode(v) {
1619
1715
  function handleNotOperator(expr, val, params, outerMode, fieldType, dialect) {
1620
1716
  const innerMode = normalizeMode(val.mode);
1621
1717
  const effectiveMode = innerMode != null ? innerMode : outerMode;
1718
+ const entries = Object.entries(val).filter(
1719
+ ([k, v]) => k !== "mode" && v !== void 0
1720
+ );
1721
+ if (entries.length === 0) return "";
1722
+ if (!isNotNullish(dialect)) {
1723
+ const clauses = [];
1724
+ for (const [subOp, subVal] of entries) {
1725
+ const sub = buildScalarOperator(
1726
+ expr,
1727
+ subOp,
1728
+ subVal,
1729
+ params,
1730
+ effectiveMode,
1731
+ fieldType,
1732
+ void 0
1733
+ );
1734
+ if (sub && sub.trim().length > 0) clauses.push(`(${sub})`);
1735
+ }
1736
+ if (clauses.length === 0) return "";
1737
+ if (clauses.length === 1) return `${SQL_TEMPLATES.NOT} ${clauses[0]}`;
1738
+ return `${SQL_TEMPLATES.NOT} (${clauses.join(` ${SQL_TEMPLATES.AND} `)})`;
1739
+ }
1622
1740
  return buildNotComposite(
1623
1741
  expr,
1624
1742
  val,
@@ -1833,6 +1951,7 @@ function handleArrayIsEmpty(expr, val, dialect) {
1833
1951
 
1834
1952
  // src/builder/where/operators-json.ts
1835
1953
  var SAFE_JSON_PATH_SEGMENT = /^[a-zA-Z_]\w*$/;
1954
+ var MAX_PATH_SEGMENT_LENGTH = 255;
1836
1955
  function validateJsonPathSegments(segments) {
1837
1956
  for (const segment of segments) {
1838
1957
  if (typeof segment !== "string") {
@@ -1841,6 +1960,12 @@ function validateJsonPathSegments(segments) {
1841
1960
  value: segment
1842
1961
  });
1843
1962
  }
1963
+ if (segment.length > MAX_PATH_SEGMENT_LENGTH) {
1964
+ throw createError(
1965
+ `JSON path segment too long: max ${MAX_PATH_SEGMENT_LENGTH} characters`,
1966
+ { operator: Ops.PATH, value: `[${segment.length} chars]` }
1967
+ );
1968
+ }
1844
1969
  if (!SAFE_JSON_PATH_SEGMENT.test(segment)) {
1845
1970
  throw createError(
1846
1971
  `Invalid JSON path segment: '${segment}'. Must be alphanumeric with underscores, starting with letter or underscore.`,
@@ -1929,6 +2054,9 @@ function handleJsonWildcard(expr, op, val, params, wildcards, dialect) {
1929
2054
 
1930
2055
  // src/builder/where/relations.ts
1931
2056
  var NO_JOINS = Object.freeze([]);
2057
+ function freezeJoins(items) {
2058
+ return Object.freeze([...items]);
2059
+ }
1932
2060
  function isListRelation(fieldType) {
1933
2061
  return typeof fieldType === "string" && fieldType.endsWith("[]");
1934
2062
  }
@@ -1991,7 +2119,7 @@ function buildListRelationFilters(args) {
1991
2119
  const whereClause = `${relAlias}.${quote(checkField.name)} IS NULL`;
1992
2120
  return Object.freeze({
1993
2121
  clause: whereClause,
1994
- joins: [leftJoinSql]
2122
+ joins: freezeJoins([leftJoinSql])
1995
2123
  });
1996
2124
  }
1997
2125
  }
@@ -2196,7 +2324,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
2196
2324
  Ops.HAS_EVERY,
2197
2325
  Ops.IS_EMPTY
2198
2326
  ]);
2199
- const JSON_OPS = /* @__PURE__ */ new Set([
2327
+ const JSON_OPS2 = /* @__PURE__ */ new Set([
2200
2328
  Ops.PATH,
2201
2329
  Ops.STRING_CONTAINS,
2202
2330
  Ops.STRING_STARTS_WITH,
@@ -2213,7 +2341,7 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
2213
2341
  modelName
2214
2342
  });
2215
2343
  }
2216
- const isJsonOp = JSON_OPS.has(op);
2344
+ const isJsonOp = JSON_OPS2.has(op);
2217
2345
  const isFieldJson = isJsonType(fieldType);
2218
2346
  const jsonOpMismatch = isJsonOp && !isFieldJson;
2219
2347
  if (jsonOpMismatch) {
@@ -2227,6 +2355,14 @@ function assertValidOperator(fieldName, op, fieldType, path, modelName) {
2227
2355
  }
2228
2356
 
2229
2357
  // src/builder/where/builder.ts
2358
+ var MAX_QUERY_DEPTH = 50;
2359
+ var EMPTY_JOINS = Object.freeze([]);
2360
+ var JSON_OPS = /* @__PURE__ */ new Set([
2361
+ Ops.PATH,
2362
+ Ops.STRING_CONTAINS,
2363
+ Ops.STRING_STARTS_WITH,
2364
+ Ops.STRING_ENDS_WITH
2365
+ ]);
2230
2366
  var WhereBuilder = class {
2231
2367
  build(where, ctx) {
2232
2368
  if (!isPlainObject(where)) {
@@ -2238,8 +2374,6 @@ var WhereBuilder = class {
2238
2374
  return buildWhereInternal(where, ctx, this);
2239
2375
  }
2240
2376
  };
2241
- var MAX_QUERY_DEPTH = 50;
2242
- var EMPTY_JOINS = Object.freeze([]);
2243
2377
  var whereBuilderInstance = new WhereBuilder();
2244
2378
  function freezeResult(clause, joins = EMPTY_JOINS) {
2245
2379
  return Object.freeze({ clause, joins });
@@ -2416,16 +2550,8 @@ function buildOperator(expr, op, val, ctx, mode, fieldType) {
2416
2550
  if (fieldType && isArrayType(fieldType)) {
2417
2551
  return buildArrayOperator(expr, op, val, ctx.params, fieldType, ctx.dialect);
2418
2552
  }
2419
- if (fieldType && isJsonType(fieldType)) {
2420
- const JSON_OPS = /* @__PURE__ */ new Set([
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
- }
2553
+ if (fieldType && isJsonType(fieldType) && JSON_OPS.has(op)) {
2554
+ return buildJsonOperator(expr, op, val, ctx.params, ctx.dialect);
2429
2555
  }
2430
2556
  return buildScalarOperator(
2431
2557
  expr,
@@ -2446,7 +2572,7 @@ function toSafeSqlIdentifier(input) {
2446
2572
  const base = startsOk ? cleaned : `_${cleaned}`;
2447
2573
  const fallback = base.length > 0 ? base : "_t";
2448
2574
  const lowered = fallback.toLowerCase();
2449
- return SQL_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
2575
+ return ALIAS_FORBIDDEN_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
2450
2576
  }
2451
2577
  function createAliasGenerator(maxAliases = 1e4) {
2452
2578
  let counter = 0;
@@ -2656,6 +2782,7 @@ function toPublicResult(clause, joins, params) {
2656
2782
  // src/builder/where.ts
2657
2783
  function buildWhereClause(where, options) {
2658
2784
  var _a, _b, _c, _d, _e;
2785
+ assertSafeAlias(options.alias);
2659
2786
  const dialect = options.dialect || getGlobalDialect();
2660
2787
  const params = (_a = options.params) != null ? _a : createParamStore();
2661
2788
  const ctx = {
@@ -2671,22 +2798,6 @@ function buildWhereClause(where, options) {
2671
2798
  };
2672
2799
  const result = whereBuilderInstance.build(where, ctx);
2673
2800
  const publicResult = toPublicResult(result.clause, result.joins, params);
2674
- if (!options.isSubquery) {
2675
- const nums = [...publicResult.clause.matchAll(/\$(\d+)/g)].map(
2676
- (m) => parseInt(m[1], 10)
2677
- );
2678
- if (nums.length > 0) {
2679
- const min = Math.min(...nums);
2680
- if (min === 1) {
2681
- validateParamConsistency(publicResult.clause, publicResult.params);
2682
- } else {
2683
- validateParamConsistencyFragment(
2684
- publicResult.clause,
2685
- publicResult.params
2686
- );
2687
- }
2688
- }
2689
- }
2690
2801
  return publicResult;
2691
2802
  }
2692
2803
 
@@ -2824,6 +2935,9 @@ function buildRelationSelect(relArgs, relModel, relAlias) {
2824
2935
  }
2825
2936
 
2826
2937
  // src/builder/select/includes.ts
2938
+ var MAX_INCLUDE_DEPTH = 10;
2939
+ var MAX_TOTAL_SUBQUERIES = 100;
2940
+ var MAX_TOTAL_INCLUDES = 50;
2827
2941
  function getRelationTableReference(relModel, dialect) {
2828
2942
  return buildTableReference(
2829
2943
  SQL_TEMPLATES.PUBLIC_SCHEMA,
@@ -2869,107 +2983,23 @@ function relationEntriesFromArgs(args, model) {
2869
2983
  pushFrom(args.select);
2870
2984
  return out;
2871
2985
  }
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
2986
  function validateOrderByForModel(model, orderBy) {
2959
- const entries = getOrderByEntries(orderBy);
2960
- for (const [fieldName, v] of entries) {
2961
- assertScalarField(model, fieldName);
2962
- if (typeof v === "string") {
2963
- validateOrderByDirection(fieldName, v);
2964
- continue;
2987
+ if (!isNotNullish(orderBy)) return;
2988
+ const scalarSet = getScalarFieldSet(model);
2989
+ const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
2990
+ for (const item of normalized) {
2991
+ const entries = Object.entries(item);
2992
+ if (entries.length !== 1) {
2993
+ throw new Error("orderBy array entries must have exactly one field");
2965
2994
  }
2966
- if (isPlainObject(v)) {
2967
- validateOrderByObject(fieldName, v);
2968
- continue;
2995
+ const fieldName = String(entries[0][0]).trim();
2996
+ if (fieldName.length === 0)
2997
+ throw new Error("orderBy field name cannot be empty");
2998
+ if (!scalarSet.has(fieldName)) {
2999
+ throw new Error(
3000
+ `orderBy references unknown or non-scalar field '${fieldName}' on model ${model.name}`
3001
+ );
2969
3002
  }
2970
- throw new Error(
2971
- `orderBy for '${fieldName}' must be 'asc' | 'desc' or { sort, nulls? }`
2972
- );
2973
3003
  }
2974
3004
  }
2975
3005
  function appendLimitOffset(sql, dialect, params, takeVal, skipVal, scope) {
@@ -2998,7 +3028,10 @@ function readWhereInput(relArgs) {
2998
3028
  function readOrderByInput(relArgs) {
2999
3029
  if (!isPlainObject(relArgs)) return { hasOrderBy: false, orderBy: void 0 };
3000
3030
  if (!("orderBy" in relArgs)) return { hasOrderBy: false, orderBy: void 0 };
3001
- return { hasOrderBy: true, orderBy: relArgs.orderBy };
3031
+ return {
3032
+ hasOrderBy: true,
3033
+ orderBy: relArgs.orderBy
3034
+ };
3002
3035
  }
3003
3036
  function extractRelationPaginationConfig(relArgs) {
3004
3037
  const { hasOrderBy, orderBy: rawOrderByInput } = readOrderByInput(relArgs);
@@ -3020,44 +3053,25 @@ function extractRelationPaginationConfig(relArgs) {
3020
3053
  function maybeReverseNegativeTake(takeVal, hasOrderBy, orderByInput) {
3021
3054
  if (typeof takeVal !== "number") return { takeVal, orderByInput };
3022
3055
  if (takeVal >= 0) return { takeVal, orderByInput };
3023
- if (!hasOrderBy) {
3056
+ if (!hasOrderBy)
3024
3057
  throw new Error("Negative take requires orderBy for deterministic results");
3025
- }
3026
3058
  return {
3027
3059
  takeVal: Math.abs(takeVal),
3028
3060
  orderByInput: reverseOrderByInput(orderByInput)
3029
3061
  };
3030
3062
  }
3031
- function hasIdTiebreaker(orderByInput) {
3032
- const entries = Array.isArray(orderByInput) ? orderByInput : [orderByInput];
3033
- return entries.some(
3034
- (entry) => isPlainObject(entry) ? Object.prototype.hasOwnProperty.call(entry, "id") : false
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);
3049
- }
3050
- return orderByInput;
3051
- }
3052
- if (!hasOrderBy) {
3053
- return modelHasScalarId(relModel) ? { id: "asc" } : orderByInput;
3063
+ function finalizeOrderByForInclude(args) {
3064
+ if (args.hasOrderBy && isNotNullish(args.orderByInput)) {
3065
+ validateOrderByForModel(args.relModel, args.orderByInput);
3054
3066
  }
3055
- if (isNotNullish(orderByInput)) {
3056
- validateOrderByForModel(relModel, orderByInput);
3067
+ if (!args.hasPagination) {
3068
+ return args.orderByInput;
3057
3069
  }
3058
- if (!modelHasScalarId(relModel)) return orderByInput;
3059
- if (hasIdTiebreaker(orderByInput)) return orderByInput;
3060
- return addIdTiebreaker(orderByInput);
3070
+ return ensureDeterministicOrderByInput({
3071
+ orderBy: args.hasOrderBy ? args.orderByInput : void 0,
3072
+ model: args.relModel,
3073
+ parseValue: parseOrderByValue
3074
+ });
3061
3075
  }
3062
3076
  function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
3063
3077
  let relSelect = buildRelationSelect(relArgs, relModel, relAlias);
@@ -3068,7 +3082,10 @@ function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
3068
3082
  relAlias,
3069
3083
  ctx.aliasGen,
3070
3084
  ctx.params,
3071
- ctx.dialect
3085
+ ctx.dialect,
3086
+ ctx.visitPath || [],
3087
+ (ctx.depth || 0) + 1,
3088
+ ctx.stats
3072
3089
  ) : [];
3073
3090
  if (isNonEmptyArray(nestedIncludes)) {
3074
3091
  const emptyJson = ctx.dialect === "postgres" ? `'[]'::json` : `json('[]')`;
@@ -3155,11 +3172,7 @@ function buildListIncludeSpec(args) {
3155
3172
  joinPredicate: args.joinPredicate,
3156
3173
  whereClause: args.whereClause
3157
3174
  });
3158
- return Object.freeze({
3159
- name: args.relName,
3160
- sql: sql2,
3161
- isOneToOne: false
3162
- });
3175
+ return Object.freeze({ name: args.relName, sql: sql2, isOneToOne: false });
3163
3176
  }
3164
3177
  const rowAlias = args.ctx.aliasGen.next(`${args.relName}_row`);
3165
3178
  let base = buildBaseSql({
@@ -3170,9 +3183,7 @@ function buildListIncludeSpec(args) {
3170
3183
  joinPredicate: args.joinPredicate,
3171
3184
  whereClause: args.whereClause
3172
3185
  });
3173
- if (args.orderBySql) {
3174
- base += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
3175
- }
3186
+ if (args.orderBySql) base += ` ${SQL_TEMPLATES.ORDER_BY} ${args.orderBySql}`;
3176
3187
  base = appendLimitOffset(
3177
3188
  base,
3178
3189
  args.ctx.dialect,
@@ -3183,11 +3194,7 @@ function buildListIncludeSpec(args) {
3183
3194
  );
3184
3195
  const selectExpr = jsonAgg("row", args.ctx.dialect);
3185
3196
  const sql = `${SQL_TEMPLATES.SELECT} ${selectExpr} ${SQL_TEMPLATES.FROM} (${base}) ${rowAlias}`;
3186
- return Object.freeze({
3187
- name: args.relName,
3188
- sql,
3189
- isOneToOne: false
3190
- });
3197
+ return Object.freeze({ name: args.relName, sql, isOneToOne: false });
3191
3198
  }
3192
3199
  function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
3193
3200
  const relTable = getRelationTableReference(relModel, ctx.dialect);
@@ -3218,12 +3225,12 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
3218
3225
  paginationConfig.orderBy
3219
3226
  );
3220
3227
  const hasPagination = paginationConfig.hasSkip || paginationConfig.hasTake;
3221
- const finalOrderByInput = ensureDeterministicOrderBy(
3228
+ const finalOrderByInput = finalizeOrderByForInclude({
3222
3229
  relModel,
3223
- paginationConfig.hasOrderBy,
3224
- adjusted.orderByInput,
3230
+ hasOrderBy: paginationConfig.hasOrderBy,
3231
+ orderByInput: adjusted.orderByInput,
3225
3232
  hasPagination
3226
- );
3233
+ });
3227
3234
  const orderBySql = buildOrderBySql(
3228
3235
  finalOrderByInput,
3229
3236
  relAlias,
@@ -3244,11 +3251,7 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
3244
3251
  skipVal: paginationConfig.skipVal,
3245
3252
  ctx
3246
3253
  });
3247
- return Object.freeze({
3248
- name: relName,
3249
- sql,
3250
- isOneToOne: true
3251
- });
3254
+ return Object.freeze({ name: relName, sql, isOneToOne: true });
3252
3255
  }
3253
3256
  return buildListIncludeSpec({
3254
3257
  relName,
@@ -3264,32 +3267,69 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
3264
3267
  ctx
3265
3268
  });
3266
3269
  }
3267
- function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect) {
3270
+ function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect, visitPath = [], depth = 0, stats) {
3271
+ if (!stats) stats = { totalIncludes: 0, totalSubqueries: 0, maxDepth: 0 };
3272
+ if (depth > MAX_INCLUDE_DEPTH) {
3273
+ throw new Error(
3274
+ `Maximum include depth of ${MAX_INCLUDE_DEPTH} exceeded. Path: ${visitPath.join(" -> ")}. Deep includes cause exponential SQL complexity and performance issues.`
3275
+ );
3276
+ }
3277
+ stats.maxDepth = Math.max(stats.maxDepth, depth);
3268
3278
  const includes = [];
3269
3279
  const entries = relationEntriesFromArgs(args, model);
3270
3280
  for (const [relName, relArgs] of entries) {
3271
3281
  if (relArgs === false) continue;
3282
+ stats.totalIncludes++;
3283
+ if (stats.totalIncludes > MAX_TOTAL_INCLUDES) {
3284
+ throw new Error(
3285
+ `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.`
3286
+ );
3287
+ }
3288
+ stats.totalSubqueries++;
3289
+ if (stats.totalSubqueries > MAX_TOTAL_SUBQUERIES) {
3290
+ throw new Error(
3291
+ `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.`
3292
+ );
3293
+ }
3272
3294
  const resolved = resolveRelationOrThrow(model, schemas, relName);
3273
- const include = buildSingleInclude(
3274
- relName,
3275
- relArgs,
3276
- resolved.field,
3277
- resolved.relModel,
3278
- {
3295
+ const relationPath = `${model.name}.${relName}`;
3296
+ const currentPath = [...visitPath, relationPath];
3297
+ if (visitPath.includes(relationPath)) {
3298
+ throw new Error(
3299
+ `Circular include detected: ${currentPath.join(" -> ")}. Relation '${relationPath}' creates an infinite loop.`
3300
+ );
3301
+ }
3302
+ const modelOccurrences = currentPath.filter(
3303
+ (p) => p.startsWith(`${resolved.relModel.name}.`)
3304
+ ).length;
3305
+ if (modelOccurrences > 2) {
3306
+ throw new Error(
3307
+ `Include too deeply nested: model '${resolved.relModel.name}' appears ${modelOccurrences} times in path: ${currentPath.join(" -> ")}`
3308
+ );
3309
+ }
3310
+ includes.push(
3311
+ buildSingleInclude(relName, relArgs, resolved.field, resolved.relModel, {
3279
3312
  model,
3280
3313
  schemas,
3281
3314
  parentAlias,
3282
3315
  aliasGen,
3283
3316
  dialect,
3284
- params
3285
- }
3317
+ params,
3318
+ visitPath: currentPath,
3319
+ depth: depth + 1,
3320
+ stats
3321
+ })
3286
3322
  );
3287
- includes.push(include);
3288
3323
  }
3289
3324
  return includes;
3290
3325
  }
3291
3326
  function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
3292
3327
  const aliasGen = createAliasGenerator();
3328
+ const stats = {
3329
+ totalIncludes: 0,
3330
+ totalSubqueries: 0,
3331
+ maxDepth: 0
3332
+ };
3293
3333
  return buildIncludeSqlInternal(
3294
3334
  args,
3295
3335
  model,
@@ -3297,7 +3337,10 @@ function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
3297
3337
  parentAlias,
3298
3338
  aliasGen,
3299
3339
  params,
3300
- dialect
3340
+ dialect,
3341
+ [],
3342
+ 0,
3343
+ stats
3301
3344
  );
3302
3345
  }
3303
3346
  function resolveCountRelationOrThrow(relName, model, schemas) {
@@ -3308,10 +3351,14 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
3308
3351
  );
3309
3352
  }
3310
3353
  const field = model.fields.find((f) => f.name === relName);
3311
- if (!field) {
3354
+ if (!field)
3312
3355
  throw new Error(
3313
3356
  `_count.${relName} references unknown relation on model ${model.name}`
3314
3357
  );
3358
+ if (!isValidRelationField(field)) {
3359
+ throw new Error(
3360
+ `_count.${relName} has invalid relation metadata on model ${model.name}`
3361
+ );
3315
3362
  }
3316
3363
  const relModel = schemas.find((m) => m.name === field.relatedModel);
3317
3364
  if (!relModel) {
@@ -3321,31 +3368,78 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
3321
3368
  }
3322
3369
  return { field, relModel };
3323
3370
  }
3324
- function groupByColForCount(field, countAlias) {
3325
- const fkFields = normalizeKeyList(field.foreignKey);
3326
- const refFields = normalizeKeyList(field.references);
3327
- return field.isForeignKeyLocal ? `${countAlias}.${quote(refFields[0] || "id")}` : `${countAlias}.${quote(fkFields[0])}`;
3371
+ function defaultReferencesForCount(fkCount) {
3372
+ if (fkCount === 1) return ["id"];
3373
+ throw new Error(
3374
+ "Relation count for composite keys requires explicit references matching foreignKey length"
3375
+ );
3328
3376
  }
3329
- function leftJoinOnForCount(field, parentAlias, joinAlias) {
3377
+ function resolveCountKeyPairs(field) {
3330
3378
  const fkFields = normalizeKeyList(field.foreignKey);
3331
- const refFields = normalizeKeyList(field.references);
3332
- return field.isForeignKeyLocal ? `${joinAlias}.__fk = ${parentAlias}.${quote(fkFields[0])}` : `${joinAlias}.__fk = ${parentAlias}.${quote(refFields[0] || "id")}`;
3379
+ if (fkFields.length === 0)
3380
+ throw new Error("Relation count requires foreignKey");
3381
+ const refsRaw = field.references;
3382
+ const refs = normalizeKeyList(refsRaw);
3383
+ const refFields = refs.length > 0 ? refs : defaultReferencesForCount(fkFields.length);
3384
+ if (refFields.length !== fkFields.length) {
3385
+ throw new Error(
3386
+ "Relation count requires references count to match foreignKey count"
3387
+ );
3388
+ }
3389
+ const relKeyFields = field.isForeignKeyLocal ? refFields : fkFields;
3390
+ const parentKeyFields = field.isForeignKeyLocal ? fkFields : refFields;
3391
+ return { relKeyFields, parentKeyFields };
3392
+ }
3393
+ function aliasQualifiedColumn(alias, model, field) {
3394
+ return `${alias}.${quoteColumn(model, field)}`;
3395
+ }
3396
+ function subqueryForCount(args) {
3397
+ const selectKeys = args.relKeyFields.map(
3398
+ (f, i) => `${aliasQualifiedColumn(args.countAlias, args.relModel, f)} AS "__fk${i}"`
3399
+ ).join(SQL_SEPARATORS.FIELD_LIST);
3400
+ const groupByKeys = args.relKeyFields.map((f) => aliasQualifiedColumn(args.countAlias, args.relModel, f)).join(SQL_SEPARATORS.FIELD_LIST);
3401
+ const cntExpr = args.dialect === "postgres" ? "COUNT(*)::int AS __cnt" : "COUNT(*) AS __cnt";
3402
+ return `(SELECT ${selectKeys}${SQL_SEPARATORS.FIELD_LIST}${cntExpr} FROM ${args.relTable} ${args.countAlias} GROUP BY ${groupByKeys})`;
3403
+ }
3404
+ function leftJoinOnForCount(args) {
3405
+ const parts = args.parentKeyFields.map(
3406
+ (f, i) => `${args.joinAlias}."__fk${i}" = ${aliasQualifiedColumn(args.parentAlias, args.parentModel, f)}`
3407
+ );
3408
+ return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
3333
3409
  }
3334
- function subqueryForCount(dialect, relTable, countAlias, groupByCol) {
3335
- return dialect === "postgres" ? `(SELECT ${groupByCol} AS __fk, COUNT(*)::int AS __cnt FROM ${relTable} ${countAlias} GROUP BY ${groupByCol})` : `(SELECT ${groupByCol} AS __fk, COUNT(*) AS __cnt FROM ${relTable} ${countAlias} GROUP BY ${groupByCol})`;
3410
+ function nextAliasAvoiding(aliasGen, base, forbidden) {
3411
+ let a = aliasGen.next(base);
3412
+ while (forbidden.has(a)) a = aliasGen.next(base);
3413
+ return a;
3336
3414
  }
3337
3415
  function buildCountJoinAndPair(args) {
3338
3416
  const relTable = getRelationTableReference(args.relModel, args.dialect);
3339
- const countAlias = `__tp_cnt_${args.relName}`;
3340
- const groupByCol = groupByColForCount(args.field, countAlias);
3341
- const subquery = subqueryForCount(
3342
- args.dialect,
3417
+ const { relKeyFields, parentKeyFields } = resolveCountKeyPairs(args.field);
3418
+ const forbidden = /* @__PURE__ */ new Set([args.parentAlias]);
3419
+ const countAlias = nextAliasAvoiding(
3420
+ args.aliasGen,
3421
+ `__tp_cnt_${args.relName}`,
3422
+ forbidden
3423
+ );
3424
+ forbidden.add(countAlias);
3425
+ const subquery = subqueryForCount({
3426
+ dialect: args.dialect,
3343
3427
  relTable,
3344
3428
  countAlias,
3345
- groupByCol
3429
+ relModel: args.relModel,
3430
+ relKeyFields
3431
+ });
3432
+ const joinAlias = nextAliasAvoiding(
3433
+ args.aliasGen,
3434
+ `__tp_cnt_j_${args.relName}`,
3435
+ forbidden
3346
3436
  );
3347
- const joinAlias = `__tp_cnt_j_${args.relName}`;
3348
- const leftJoinOn = leftJoinOnForCount(args.field, args.parentAlias, joinAlias);
3437
+ const leftJoinOn = leftJoinOnForCount({
3438
+ joinAlias,
3439
+ parentAlias: args.parentAlias,
3440
+ parentModel: args.parentModel,
3441
+ parentKeyFields
3442
+ });
3349
3443
  return {
3350
3444
  joinSql: `LEFT JOIN ${subquery} ${joinAlias} ON ${leftJoinOn}`,
3351
3445
  pairSql: `${sqlStringLiteral(args.relName)}, COALESCE(${joinAlias}.__cnt, 0)`
@@ -3354,6 +3448,7 @@ function buildCountJoinAndPair(args) {
3354
3448
  function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params, dialect) {
3355
3449
  const joins = [];
3356
3450
  const pairs = [];
3451
+ const aliasGen = createAliasGenerator();
3357
3452
  for (const [relName, shouldCount] of Object.entries(countSelect)) {
3358
3453
  if (!shouldCount) continue;
3359
3454
  const resolved = resolveCountRelationOrThrow(relName, model, schemas);
@@ -3361,29 +3456,33 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
3361
3456
  relName,
3362
3457
  field: resolved.field,
3363
3458
  relModel: resolved.relModel,
3459
+ parentModel: model,
3364
3460
  parentAlias,
3365
- dialect
3461
+ dialect,
3462
+ aliasGen
3366
3463
  });
3367
3464
  joins.push(built.joinSql);
3368
3465
  pairs.push(built.pairSql);
3369
3466
  }
3370
- return {
3371
- joins,
3372
- jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST)
3373
- };
3467
+ return { joins, jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST) };
3374
3468
  }
3375
3469
 
3376
3470
  // src/builder/select/assembly.ts
3377
- var SIMPLE_SELECT_RE_CACHE = /* @__PURE__ */ new Map();
3378
- function normalizeFinalParams(params) {
3379
- return params.map(normalizeValue);
3380
- }
3471
+ var ALIAS_CAPTURE = "([A-Za-z_][A-Za-z0-9_]*)";
3472
+ var COLUMN_PART = '(?:"([^"]+)"|([a-z_][a-z0-9_]*))';
3473
+ var AS_PART = `(?:\\s+AS\\s+${COLUMN_PART})?`;
3474
+ var SIMPLE_COLUMN_PATTERN = `^${ALIAS_CAPTURE}\\.${COLUMN_PART}${AS_PART}$`;
3475
+ var SIMPLE_COLUMN_RE = new RegExp(SIMPLE_COLUMN_PATTERN, "i");
3381
3476
  function joinNonEmpty(parts, sep) {
3382
3477
  return parts.filter((s) => s.trim().length > 0).join(sep);
3383
3478
  }
3384
3479
  function buildWhereSql(conditions) {
3385
3480
  if (!isNonEmptyArray(conditions)) return "";
3386
- return ` ${SQL_TEMPLATES.WHERE} ${conditions.join(SQL_SEPARATORS.CONDITION_AND)}`;
3481
+ const parts = [
3482
+ SQL_TEMPLATES.WHERE,
3483
+ conditions.join(SQL_SEPARATORS.CONDITION_AND)
3484
+ ];
3485
+ return ` ${parts.join(" ")}`;
3387
3486
  }
3388
3487
  function buildJoinsSql(...joinGroups) {
3389
3488
  const all = [];
@@ -3398,37 +3497,43 @@ function buildSelectList(baseSelect, extraCols) {
3398
3497
  if (base && extra) return `${base}${SQL_SEPARATORS.FIELD_LIST}${extra}`;
3399
3498
  return base || extra;
3400
3499
  }
3401
- function finalizeSql(sql, params) {
3500
+ function finalizeSql(sql, params, dialect) {
3402
3501
  const snapshot = params.snapshot();
3403
3502
  validateSelectQuery(sql);
3404
- validateParamConsistency(sql, snapshot.params);
3503
+ validateParamConsistencyByDialect(
3504
+ sql,
3505
+ snapshot.params,
3506
+ dialect === "sqlite" ? "postgres" : dialect
3507
+ );
3405
3508
  return Object.freeze({
3406
3509
  sql,
3407
- params: normalizeFinalParams(snapshot.params),
3510
+ params: snapshot.params,
3408
3511
  paramMappings: Object.freeze(snapshot.mappings)
3409
3512
  });
3410
3513
  }
3411
- function parseSimpleScalarSelect(select, alias) {
3412
- var _a, _b;
3514
+ function parseSimpleScalarSelect(select, fromAlias) {
3515
+ var _a, _b, _c, _d;
3413
3516
  const raw = select.trim();
3414
3517
  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
3518
  const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
3422
3519
  const names = [];
3423
3520
  for (const part of parts) {
3424
3521
  const p = part.trim();
3425
- const m = p.match(re);
3522
+ const m = p.match(SIMPLE_COLUMN_RE);
3426
3523
  if (!m) {
3427
3524
  throw new Error(
3428
- `sqlite distinct emulation requires scalar select fields to be simple columns. Got: ${p}`
3525
+ `sqlite distinct emulation requires scalar select fields to be simple columns (optionally with AS). Got: ${p}`
3526
+ );
3527
+ }
3528
+ const actualAlias = m[1];
3529
+ if (actualAlias.toLowerCase() !== fromAlias.toLowerCase()) {
3530
+ throw new Error(
3531
+ `Expected alias '${fromAlias}', got '${actualAlias}' in: ${p}`
3429
3532
  );
3430
3533
  }
3431
- const name = ((_b = (_a = m[1]) != null ? _a : m[2]) != null ? _b : "").trim();
3534
+ const columnName = ((_b = (_a = m[2]) != null ? _a : m[3]) != null ? _b : "").trim();
3535
+ const outAlias = ((_d = (_c = m[4]) != null ? _c : m[5]) != null ? _d : "").trim();
3536
+ const name = outAlias.length > 0 ? outAlias : columnName;
3432
3537
  if (name.length === 0) {
3433
3538
  throw new Error(`Failed to parse selected column name from: ${p}`);
3434
3539
  }
@@ -3437,18 +3542,18 @@ function parseSimpleScalarSelect(select, alias) {
3437
3542
  return names;
3438
3543
  }
3439
3544
  function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
3440
- const needle = `${fromAlias}.`;
3441
- const replacement = `${outerAlias}.`;
3442
- return orderBy.split(needle).join(replacement);
3545
+ const src = String(fromAlias);
3546
+ if (src.length === 0) return orderBy;
3547
+ const escaped = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3548
+ const re = new RegExp(`\\b${escaped}\\.`, "gi");
3549
+ return orderBy.replace(re, `${outerAlias}.`);
3443
3550
  }
3444
3551
  function buildDistinctColumns(distinct, fromAlias, model) {
3445
3552
  return distinct.map((f) => col(fromAlias, f, model)).join(SQL_SEPARATORS.FIELD_LIST);
3446
3553
  }
3447
3554
  function buildOutputColumns(scalarNames, includeNames, hasCount) {
3448
3555
  const outputCols = [...scalarNames, ...includeNames];
3449
- if (hasCount) {
3450
- outputCols.push("_count");
3451
- }
3556
+ if (hasCount) outputCols.push("_count");
3452
3557
  const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
3453
3558
  if (!isNonEmptyString(formatted)) {
3454
3559
  throw new Error("distinct emulation requires at least one output column");
@@ -3457,9 +3562,10 @@ function buildOutputColumns(scalarNames, includeNames, hasCount) {
3457
3562
  }
3458
3563
  function buildWindowOrder(args) {
3459
3564
  const { baseOrder, idField, fromAlias, model } = args;
3565
+ const fromLower = String(fromAlias).toLowerCase();
3460
3566
  const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
3461
3567
  const hasIdInOrder = orderFields.some(
3462
- (f) => f.startsWith(`${fromAlias}.id `) || f.startsWith(`${fromAlias}."id" `)
3568
+ (f) => f.startsWith(`${fromLower}.id `) || f.startsWith(`${fromLower}."id" `)
3463
3569
  );
3464
3570
  if (hasIdInOrder) return baseOrder;
3465
3571
  const idTiebreaker = idField ? `, ${col(fromAlias, "id", model)} ASC` : "";
@@ -3494,15 +3600,37 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
3494
3600
  const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, `"__tp_distinct"`) : replaceOrderByAlias(fallbackOrder, from.alias, `"__tp_distinct"`);
3495
3601
  const joins = buildJoinsSql(whereJoins, countJoins);
3496
3602
  const conditions = [];
3497
- if (whereClause && whereClause !== "1=1") {
3498
- conditions.push(whereClause);
3499
- }
3603
+ if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
3500
3604
  const whereSql = buildWhereSql(conditions);
3501
3605
  const innerSelectList = selectWithIncludes.trim();
3502
3606
  const innerComma = innerSelectList.length > 0 ? SQL_SEPARATORS.FIELD_LIST : "";
3503
- const inner = `${SQL_TEMPLATES.SELECT} ${innerSelectList}${innerComma}ROW_NUMBER() OVER (PARTITION BY ${distinctCols} ORDER BY ${windowOrder}) ${SQL_TEMPLATES.AS} "__tp_rn" ${SQL_TEMPLATES.FROM} ${from.table} ${from.alias}${joins}${whereSql}`;
3504
- const outer = `${SQL_TEMPLATES.SELECT} ${outerSelectCols} ${SQL_TEMPLATES.FROM} (${inner}) ${SQL_TEMPLATES.AS} "__tp_distinct" ${SQL_TEMPLATES.WHERE} "__tp_rn" = 1` + (isNonEmptyString(outerOrder) ? ` ${SQL_TEMPLATES.ORDER_BY} ${outerOrder}` : "");
3505
- return outer;
3607
+ const innerParts = [
3608
+ SQL_TEMPLATES.SELECT,
3609
+ innerSelectList + innerComma,
3610
+ `ROW_NUMBER() OVER (PARTITION BY ${distinctCols} ORDER BY ${windowOrder})`,
3611
+ SQL_TEMPLATES.AS,
3612
+ '"__tp_rn"',
3613
+ SQL_TEMPLATES.FROM,
3614
+ from.table,
3615
+ from.alias
3616
+ ];
3617
+ if (joins) innerParts.push(joins);
3618
+ if (whereSql) innerParts.push(whereSql);
3619
+ const inner = innerParts.filter(Boolean).join(" ");
3620
+ const outerParts = [
3621
+ SQL_TEMPLATES.SELECT,
3622
+ outerSelectCols,
3623
+ SQL_TEMPLATES.FROM,
3624
+ `(${inner})`,
3625
+ SQL_TEMPLATES.AS,
3626
+ '"__tp_distinct"',
3627
+ SQL_TEMPLATES.WHERE,
3628
+ '"__tp_rn" = 1'
3629
+ ];
3630
+ if (isNonEmptyString(outerOrder)) {
3631
+ outerParts.push(SQL_TEMPLATES.ORDER_BY, outerOrder);
3632
+ }
3633
+ return outerParts.filter(Boolean).join(" ");
3506
3634
  }
3507
3635
  function buildIncludeColumns(spec) {
3508
3636
  var _a, _b;
@@ -3630,6 +3758,7 @@ function constructFinalSql(spec) {
3630
3758
  orderBy,
3631
3759
  distinct,
3632
3760
  method,
3761
+ cursorCte,
3633
3762
  cursorClause,
3634
3763
  params,
3635
3764
  dialect,
@@ -3644,9 +3773,13 @@ function constructFinalSql(spec) {
3644
3773
  const spec2 = withCountJoins(spec, countJoins, whereJoins);
3645
3774
  let sql2 = buildSqliteDistinctQuery(spec2, selectWithIncludes).trim();
3646
3775
  sql2 = appendPagination(sql2, spec);
3647
- return finalizeSql(sql2, params);
3776
+ return finalizeSql(sql2, params, dialect);
3777
+ }
3778
+ const parts = [];
3779
+ if (cursorCte) {
3780
+ parts.push(`WITH ${cursorCte}`);
3648
3781
  }
3649
- const parts = [SQL_TEMPLATES.SELECT];
3782
+ parts.push(SQL_TEMPLATES.SELECT);
3650
3783
  const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct, model) : null;
3651
3784
  if (distinctOn) parts.push(distinctOn);
3652
3785
  const baseSelect = (select != null ? select : "").trim();
@@ -3662,7 +3795,7 @@ function constructFinalSql(spec) {
3662
3795
  if (isNonEmptyString(orderBy)) parts.push(SQL_TEMPLATES.ORDER_BY, orderBy);
3663
3796
  let sql = parts.join(" ").trim();
3664
3797
  sql = appendPagination(sql, spec);
3665
- return finalizeSql(sql, params);
3798
+ return finalizeSql(sql, params, dialect);
3666
3799
  }
3667
3800
 
3668
3801
  // src/builder/select.ts
@@ -3695,7 +3828,7 @@ function buildPostgresDistinctOrderBy(distinctFields, existing) {
3695
3828
  }
3696
3829
  return next;
3697
3830
  }
3698
- function applyPostgresDistinctOrderBy(args, _model) {
3831
+ function applyPostgresDistinctOrderBy(args) {
3699
3832
  const distinctFields = normalizeDistinctFields(args.distinct);
3700
3833
  if (distinctFields.length === 0) return args;
3701
3834
  if (!isNotNullish(args.orderBy)) return args;
@@ -3705,19 +3838,6 @@ function applyPostgresDistinctOrderBy(args, _model) {
3705
3838
  orderBy: buildPostgresDistinctOrderBy(distinctFields, existing)
3706
3839
  });
3707
3840
  }
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
3841
  function validateDistinct(model, distinct) {
3722
3842
  if (!isNotNullish(distinct) || !isNonEmptyArray(distinct)) return;
3723
3843
  const seen = /* @__PURE__ */ new Set();
@@ -3728,24 +3848,24 @@ function validateDistinct(model, distinct) {
3728
3848
  throw new Error(`distinct must not contain duplicates (field: '${f}')`);
3729
3849
  }
3730
3850
  seen.add(f);
3731
- assertScalarFieldOnModel(model, f, "distinct");
3851
+ assertScalarField(model, f, "distinct");
3732
3852
  }
3733
3853
  }
3734
- function validateOrderByValue(fieldName, v) {
3735
- parseOrderByValue(v, fieldName);
3736
- }
3737
3854
  function validateOrderBy(model, orderBy) {
3738
3855
  if (!isNotNullish(orderBy)) return;
3739
3856
  const items = normalizeOrderByInput2(orderBy);
3740
3857
  if (items.length === 0) return;
3741
3858
  for (const it of items) {
3742
3859
  const entries = Object.entries(it);
3860
+ if (entries.length !== 1) {
3861
+ throw new Error("orderBy array entries must have exactly one field");
3862
+ }
3743
3863
  const fieldName = String(entries[0][0]).trim();
3744
3864
  if (fieldName.length === 0) {
3745
3865
  throw new Error("orderBy field name cannot be empty");
3746
3866
  }
3747
- assertScalarFieldOnModel(model, fieldName, "orderBy");
3748
- validateOrderByValue(fieldName, entries[0][1]);
3867
+ assertScalarField(model, fieldName, "orderBy");
3868
+ parseOrderByValue(entries[0][1], fieldName);
3749
3869
  }
3750
3870
  }
3751
3871
  function validateCursor(model, cursor) {
@@ -3762,7 +3882,7 @@ function validateCursor(model, cursor) {
3762
3882
  if (f.length === 0) {
3763
3883
  throw new Error("cursor field name cannot be empty");
3764
3884
  }
3765
- assertScalarFieldOnModel(model, f, "cursor");
3885
+ assertScalarField(model, f, "cursor");
3766
3886
  }
3767
3887
  }
3768
3888
  function resolveDialect(dialect) {
@@ -3781,20 +3901,21 @@ function normalizeArgsForNegativeTake(method, args) {
3781
3901
  orderBy: reverseOrderByInput(args.orderBy)
3782
3902
  });
3783
3903
  }
3784
- function normalizeArgsForDialect(dialect, args, model) {
3904
+ function normalizeArgsForDialect(dialect, args) {
3785
3905
  if (dialect !== "postgres") return args;
3786
3906
  return applyPostgresDistinctOrderBy(args);
3787
3907
  }
3788
3908
  function buildCursorClauseIfAny(input) {
3789
- const { cursor, orderBy, tableName, alias, params, dialect } = input;
3790
- if (!isNotNullish(cursor)) return void 0;
3909
+ const { cursor, orderBy, tableName, alias, params, dialect, model } = input;
3910
+ if (!isNotNullish(cursor)) return {};
3791
3911
  return buildCursorCondition(
3792
3912
  cursor,
3793
3913
  orderBy,
3794
3914
  tableName,
3795
3915
  alias,
3796
3916
  params,
3797
- dialect
3917
+ dialect,
3918
+ model
3798
3919
  );
3799
3920
  }
3800
3921
  function buildSelectSpec(input) {
@@ -3833,14 +3954,20 @@ function buildSelectSpec(input) {
3833
3954
  params,
3834
3955
  dialect
3835
3956
  );
3836
- const cursorClause = buildCursorClauseIfAny({
3957
+ const cursorResult = buildCursorClauseIfAny({
3837
3958
  cursor,
3838
3959
  orderBy: normalizedArgs.orderBy,
3839
3960
  tableName,
3840
3961
  alias,
3841
3962
  params,
3842
- dialect
3963
+ dialect,
3964
+ model
3843
3965
  });
3966
+ if (dialect === "sqlite" && isNonEmptyArray(normalizedArgs.distinct) && cursorResult.condition) {
3967
+ throw new Error(
3968
+ "Cursor pagination with distinct is not supported in SQLite due to window function limitations. Use findMany with skip/take instead, or remove distinct."
3969
+ );
3970
+ }
3844
3971
  return {
3845
3972
  select: selectFields,
3846
3973
  includes,
@@ -3851,7 +3978,8 @@ function buildSelectSpec(input) {
3851
3978
  pagination: { take, skip },
3852
3979
  distinct: normalizedArgs.distinct,
3853
3980
  method,
3854
- cursorClause,
3981
+ cursorCte: cursorResult.cte,
3982
+ cursorClause: cursorResult.condition,
3855
3983
  params,
3856
3984
  dialect,
3857
3985
  model,
@@ -3865,9 +3993,7 @@ function buildSelectSql(input) {
3865
3993
  assertSafeTableRef(from.tableName);
3866
3994
  const dialectToUse = resolveDialect(dialect);
3867
3995
  const argsForSql = normalizeArgsForNegativeTake(method, args);
3868
- const normalizedArgs = normalizeArgsForDialect(
3869
- dialectToUse,
3870
- argsForSql);
3996
+ const normalizedArgs = normalizeArgsForDialect(dialectToUse, argsForSql);
3871
3997
  validateDistinct(model, normalizedArgs.distinct);
3872
3998
  validateOrderBy(model, normalizedArgs.orderBy);
3873
3999
  validateCursor(model, normalizedArgs.cursor);
@@ -3883,8 +4009,21 @@ function buildSelectSql(input) {
3883
4009
  });
3884
4010
  return constructFinalSql(spec);
3885
4011
  }
3886
- var MODEL_FIELD_CACHE = /* @__PURE__ */ new WeakMap();
3887
- var NUMERIC_TYPES = /* @__PURE__ */ new Set(["Int", "Float", "Decimal", "BigInt"]);
4012
+
4013
+ // src/builder/shared/comparison-builder.ts
4014
+ function buildComparisons(expr, filter, params, dialect, builder, excludeKeys = /* @__PURE__ */ new Set(["mode"])) {
4015
+ const out = [];
4016
+ for (const [op, val] of Object.entries(filter)) {
4017
+ if (excludeKeys.has(op) || val === void 0) continue;
4018
+ const built = builder(expr, op, val, params, dialect);
4019
+ if (built && built.trim().length > 0) {
4020
+ out.push(built);
4021
+ }
4022
+ }
4023
+ return out;
4024
+ }
4025
+
4026
+ // src/builder/aggregates.ts
3888
4027
  var AGGREGATES = [
3889
4028
  ["_sum", "SUM"],
3890
4029
  ["_avg", "AVG"],
@@ -3899,22 +4038,32 @@ var COMPARISON_OPS = {
3899
4038
  [Ops.LT]: "<",
3900
4039
  [Ops.LTE]: "<="
3901
4040
  };
3902
- function normalizeFinalParams2(params) {
3903
- return params.map(normalizeValue);
3904
- }
3905
- function getModelFieldMap(model) {
3906
- const cached = MODEL_FIELD_CACHE.get(model);
3907
- if (cached) return cached;
3908
- const m = /* @__PURE__ */ new Map();
3909
- for (const f of model.fields) {
3910
- m.set(f.name, { name: f.name, type: f.type, isRelation: !!f.isRelation });
3911
- }
3912
- MODEL_FIELD_CACHE.set(model, m);
3913
- return m;
3914
- }
4041
+ var HAVING_ALLOWED_OPS = /* @__PURE__ */ new Set([
4042
+ Ops.EQUALS,
4043
+ Ops.NOT,
4044
+ Ops.GT,
4045
+ Ops.GTE,
4046
+ Ops.LT,
4047
+ Ops.LTE,
4048
+ Ops.IN,
4049
+ Ops.NOT_IN
4050
+ ]);
3915
4051
  function isTruthySelection(v) {
3916
4052
  return v === true;
3917
4053
  }
4054
+ function isLogicalKey(key) {
4055
+ return key === LogicalOps.AND || key === LogicalOps.OR || key === LogicalOps.NOT;
4056
+ }
4057
+ function isAggregateKey(key) {
4058
+ return key === "_count" || key === "_sum" || key === "_avg" || key === "_min" || key === "_max";
4059
+ }
4060
+ function assertHavingOp(op) {
4061
+ if (!HAVING_ALLOWED_OPS.has(op)) {
4062
+ throw new Error(
4063
+ `Unsupported HAVING operator '${op}'. Allowed: ${[...HAVING_ALLOWED_OPS].join(", ")}`
4064
+ );
4065
+ }
4066
+ }
3918
4067
  function aggExprForField(aggKey, field, alias, model) {
3919
4068
  if (aggKey === "_count") {
3920
4069
  return field === "_all" ? `COUNT(*)` : `COUNT(${col(alias, field, model)})`;
@@ -3948,32 +4097,9 @@ function normalizeLogicalValue2(operator, value) {
3948
4097
  }
3949
4098
  return out;
3950
4099
  }
3951
- if (isPlainObject(value)) {
3952
- return [value];
3953
- }
4100
+ if (isPlainObject(value)) return [value];
3954
4101
  throw new Error(`${operator} must be an object or array of objects in HAVING`);
3955
4102
  }
3956
- function assertScalarField2(model, fieldName, ctx) {
3957
- const m = getModelFieldMap(model);
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)) {
3972
- throw new Error(
3973
- `Cannot use ${aggKey} on non-numeric field '${fieldName}' (type: ${fieldType}) on model ${modelName}`
3974
- );
3975
- }
3976
- }
3977
4103
  function buildNullComparison(expr, op) {
3978
4104
  if (op === Ops.EQUALS) return `${expr} ${SQL_TEMPLATES.IS_NULL}`;
3979
4105
  if (op === Ops.NOT) return `${expr} ${SQL_TEMPLATES.IS_NOT_NULL}`;
@@ -4000,6 +4126,7 @@ function buildBinaryComparison(expr, op, val, params) {
4000
4126
  return `${expr} ${sqlOp} ${placeholder}`;
4001
4127
  }
4002
4128
  function buildSimpleComparison(expr, op, val, params, dialect) {
4129
+ assertHavingOp(op);
4003
4130
  if (val === null) return buildNullComparison(expr, op);
4004
4131
  if (op === Ops.NOT && isPlainObject(val)) {
4005
4132
  return buildNotComposite(
@@ -4016,12 +4143,6 @@ function buildSimpleComparison(expr, op, val, params, dialect) {
4016
4143
  }
4017
4144
  return buildBinaryComparison(expr, op, val, params);
4018
4145
  }
4019
- function isLogicalKey(key) {
4020
- return key === LogicalOps.AND || key === LogicalOps.OR || key === LogicalOps.NOT;
4021
- }
4022
- function isAggregateKey(key) {
4023
- return key === "_count" || key === "_sum" || key === "_avg" || key === "_min" || key === "_max";
4024
- }
4025
4146
  function negateClauses(subClauses) {
4026
4147
  if (subClauses.length === 1) return `${SQL_TEMPLATES.NOT} ${subClauses[0]}`;
4027
4148
  return `${SQL_TEMPLATES.NOT} (${subClauses.join(SQL_SEPARATORS.CONDITION_AND)})`;
@@ -4030,16 +4151,75 @@ function combineLogical(key, subClauses) {
4030
4151
  if (key === LogicalOps.NOT) return negateClauses(subClauses);
4031
4152
  return subClauses.join(` ${key} `);
4032
4153
  }
4154
+ function buildHavingNode(node, alias, params, dialect, model) {
4155
+ const clauses = [];
4156
+ const entries = Object.entries(node);
4157
+ for (const [key, value] of entries) {
4158
+ const built = buildHavingEntry(key, value, alias, params, dialect, model);
4159
+ for (const c of built) {
4160
+ if (c && c.trim().length > 0) clauses.push(c);
4161
+ }
4162
+ }
4163
+ return clauses.join(SQL_SEPARATORS.CONDITION_AND);
4164
+ }
4033
4165
  function buildLogicalClause2(key, value, alias, params, dialect, model) {
4034
4166
  const items = normalizeLogicalValue2(key, value);
4035
4167
  const subClauses = [];
4036
4168
  for (const it of items) {
4037
4169
  const c = buildHavingNode(it, alias, params, dialect, model);
4038
- if (c && c !== "") subClauses.push(`(${c})`);
4170
+ if (c && c.trim().length > 0) subClauses.push(`(${c})`);
4039
4171
  }
4040
4172
  if (subClauses.length === 0) return "";
4041
4173
  return combineLogical(key, subClauses);
4042
4174
  }
4175
+ function assertHavingAggTarget(aggKey, field, model) {
4176
+ if (field === "_all") {
4177
+ if (aggKey !== "_count")
4178
+ throw new Error(`HAVING '${aggKey}' does not support '_all'`);
4179
+ return;
4180
+ }
4181
+ if (aggKey === "_sum" || aggKey === "_avg") {
4182
+ assertNumericField(model, field, "HAVING");
4183
+ } else {
4184
+ assertScalarField(model, field, "HAVING");
4185
+ }
4186
+ }
4187
+ function buildHavingOpsForExpr(expr, filter, params, dialect) {
4188
+ return buildComparisons(expr, filter, params, dialect, buildSimpleComparison);
4189
+ }
4190
+ function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
4191
+ if (!isPlainObject(target)) {
4192
+ throw new Error(`HAVING '${aggKey}' must be an object`);
4193
+ }
4194
+ const out = [];
4195
+ for (const [field, filter] of Object.entries(target)) {
4196
+ assertHavingAggTarget(aggKey, field, model);
4197
+ if (!isPlainObject(filter) || Object.keys(filter).length === 0) continue;
4198
+ const expr = aggExprForField(aggKey, field, alias, model);
4199
+ out.push(...buildHavingOpsForExpr(expr, filter, params, dialect));
4200
+ }
4201
+ return out;
4202
+ }
4203
+ function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
4204
+ if (!isPlainObject(target)) {
4205
+ throw new Error(`HAVING '${fieldName}' must be an object`);
4206
+ }
4207
+ assertScalarField(model, fieldName, "HAVING");
4208
+ const out = [];
4209
+ const obj = target;
4210
+ const keys = ["_count", "_sum", "_avg", "_min", "_max"];
4211
+ for (const aggKey of keys) {
4212
+ const aggFilter = obj[aggKey];
4213
+ if (!isPlainObject(aggFilter)) continue;
4214
+ if (Object.keys(aggFilter).length === 0) continue;
4215
+ if (aggKey === "_sum" || aggKey === "_avg") {
4216
+ assertNumericField(model, fieldName, "HAVING");
4217
+ }
4218
+ const expr = aggExprForField(aggKey, fieldName, alias, model);
4219
+ out.push(...buildHavingOpsForExpr(expr, aggFilter, params, dialect));
4220
+ }
4221
+ return out;
4222
+ }
4043
4223
  function buildHavingEntry(key, value, alias, params, dialect, model) {
4044
4224
  if (isLogicalKey(key)) {
4045
4225
  const logical = buildLogicalClause2(
@@ -4071,71 +4251,10 @@ function buildHavingEntry(key, value, alias, params, dialect, model) {
4071
4251
  model
4072
4252
  );
4073
4253
  }
4074
- function buildHavingNode(node, alias, params, dialect, model) {
4075
- const clauses = [];
4076
- for (const [key, value] of Object.entries(node)) {
4077
- const built = buildHavingEntry(key, value, alias, params, dialect, model);
4078
- for (const c of built) {
4079
- if (c && c.trim().length > 0) clauses.push(c);
4080
- }
4081
- }
4082
- return clauses.join(SQL_SEPARATORS.CONDITION_AND);
4083
- }
4084
- function assertHavingAggTarget(aggKey, field, model) {
4085
- if (field === "_all") {
4086
- if (aggKey !== "_count") {
4087
- throw new Error(`HAVING '${aggKey}' does not support '_all'`);
4088
- }
4089
- return;
4090
- }
4091
- const f = assertScalarField2(model, field, "HAVING");
4092
- assertAggregateFieldType(aggKey, f.type, f.name, model.name);
4093
- }
4094
- function buildHavingOpsForExpr(expr, filter, params, dialect) {
4095
- const out = [];
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;
4102
- }
4103
- function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialect, model) {
4104
- if (!isPlainObject(target)) return [];
4105
- const out = [];
4106
- for (const [field, filter] of Object.entries(target)) {
4107
- assertHavingAggTarget(aggKey, field, model);
4108
- if (!isPlainObject(filter) || Object.keys(filter).length === 0) continue;
4109
- const expr = aggExprForField(aggKey, field, alias, model);
4110
- out.push(...buildHavingOpsForExpr(expr, filter, params, dialect));
4111
- }
4112
- return out;
4113
- }
4114
- function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect, model) {
4115
- if (!isPlainObject(target)) return [];
4116
- const field = assertScalarField2(model, fieldName, "HAVING");
4117
- const out = [];
4118
- const obj = target;
4119
- const keys = ["_count", "_sum", "_avg", "_min", "_max"];
4120
- for (const aggKey of keys) {
4121
- const aggFilter = obj[aggKey];
4122
- if (!isPlainObject(aggFilter)) continue;
4123
- assertAggregateFieldType(aggKey, field.type, field.name, model.name);
4124
- const entries = Object.entries(aggFilter);
4125
- if (entries.length === 0) continue;
4126
- const expr = aggExprForField(aggKey, fieldName, alias, model);
4127
- for (const [op, val] of entries) {
4128
- if (op === "mode") continue;
4129
- const built = buildSimpleComparison(expr, op, val, params, dialect);
4130
- if (built && built.trim().length > 0) out.push(built);
4131
- }
4132
- }
4133
- return out;
4134
- }
4135
4254
  function buildHavingClause(having, alias, params, model, dialect) {
4136
4255
  if (!isNotNullish(having)) return "";
4137
4256
  const d = dialect != null ? dialect : getGlobalDialect();
4138
- if (!isPlainObject(having)) return "";
4257
+ if (!isPlainObject(having)) throw new Error("having must be an object");
4139
4258
  return buildHavingNode(having, alias, params, d, model);
4140
4259
  }
4141
4260
  function normalizeCountArg(v) {
@@ -4149,26 +4268,13 @@ function pushCountAllField(fields) {
4149
4268
  `${SQL_TEMPLATES.COUNT_ALL} ${SQL_TEMPLATES.AS} ${quote("_count._all")}`
4150
4269
  );
4151
4270
  }
4152
- function assertCountableScalarField(fieldMap, model, fieldName) {
4153
- const field = fieldMap.get(fieldName);
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
- }
4164
- }
4165
4271
  function pushCountField(fields, alias, fieldName, model) {
4166
4272
  const outAlias = `_count.${fieldName}`;
4167
4273
  fields.push(
4168
4274
  `COUNT(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
4169
4275
  );
4170
4276
  }
4171
- function addCountFields(fields, countArg, alias, model, fieldMap) {
4277
+ function addCountFields(fields, countArg, alias, model) {
4172
4278
  if (!isNotNullish(countArg)) return;
4173
4279
  if (countArg === true) {
4174
4280
  pushCountAllField(fields);
@@ -4182,7 +4288,7 @@ function addCountFields(fields, countArg, alias, model, fieldMap) {
4182
4288
  ([f, v]) => f !== "_all" && isTruthySelection(v)
4183
4289
  );
4184
4290
  for (const [f] of selected) {
4185
- assertCountableScalarField(fieldMap, model, f);
4291
+ assertScalarField(model, f, "_count");
4186
4292
  pushCountField(fields, alias, f, model);
4187
4293
  }
4188
4294
  }
@@ -4190,19 +4296,12 @@ function getAggregateSelectionObject(args, agg) {
4190
4296
  const obj = args[agg];
4191
4297
  return isPlainObject(obj) ? obj : void 0;
4192
4298
  }
4193
- function assertAggregatableScalarField(fieldMap, model, agg, fieldName) {
4194
- const field = fieldMap.get(fieldName);
4195
- if (!field) {
4196
- throw new Error(
4197
- `Field '${fieldName}' does not exist on model ${model.name}`
4198
- );
4199
- }
4200
- if (field.isRelation) {
4201
- throw new Error(
4202
- `Cannot use ${agg} on relation field '${fieldName}' on model ${model.name}`
4203
- );
4299
+ function assertAggregatableScalarField(model, agg, fieldName) {
4300
+ if (agg === "_sum" || agg === "_avg") {
4301
+ assertNumericField(model, fieldName, agg);
4302
+ } else {
4303
+ assertScalarField(model, fieldName, agg);
4204
4304
  }
4205
- return field;
4206
4305
  }
4207
4306
  function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
4208
4307
  const outAlias = `${agg}.${fieldName}`;
@@ -4210,7 +4309,7 @@ function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
4210
4309
  `${aggFn}(${col(alias, fieldName, model)}) ${SQL_TEMPLATES.AS} ${quote(outAlias)}`
4211
4310
  );
4212
4311
  }
4213
- function addAggregateFields(fields, args, alias, model, fieldMap) {
4312
+ function addAggregateFields(fields, args, alias, model) {
4214
4313
  for (const [agg, aggFn] of AGGREGATES) {
4215
4314
  const obj = getAggregateSelectionObject(args, agg);
4216
4315
  if (!obj) continue;
@@ -4218,23 +4317,16 @@ function addAggregateFields(fields, args, alias, model, fieldMap) {
4218
4317
  if (fieldName === "_all")
4219
4318
  throw new Error(`'${agg}' does not support '_all'`);
4220
4319
  if (!isTruthySelection(selection)) continue;
4221
- const field = assertAggregatableScalarField(
4222
- fieldMap,
4223
- model,
4224
- agg,
4225
- fieldName
4226
- );
4227
- assertAggregateFieldType(agg, field.type, fieldName, model.name);
4320
+ assertAggregatableScalarField(model, agg, fieldName);
4228
4321
  pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model);
4229
4322
  }
4230
4323
  }
4231
4324
  }
4232
4325
  function buildAggregateFields(args, alias, model) {
4233
4326
  const fields = [];
4234
- const fieldMap = getModelFieldMap(model);
4235
4327
  const countArg = normalizeCountArg(args._count);
4236
- addCountFields(fields, countArg, alias, model, fieldMap);
4237
- addAggregateFields(fields, args, alias, model, fieldMap);
4328
+ addCountFields(fields, countArg, alias, model);
4329
+ addAggregateFields(fields, args, alias, model);
4238
4330
  return fields;
4239
4331
  }
4240
4332
  function buildAggregateSql(args, whereResult, tableName, alias, model) {
@@ -4258,7 +4350,7 @@ function buildAggregateSql(args, whereResult, tableName, alias, model) {
4258
4350
  validateParamConsistency(sql, whereResult.params);
4259
4351
  return Object.freeze({
4260
4352
  sql,
4261
- params: Object.freeze(normalizeFinalParams2([...whereResult.params])),
4353
+ params: Object.freeze([...whereResult.params]),
4262
4354
  paramMappings: Object.freeze([...whereResult.paramMappings])
4263
4355
  });
4264
4356
  }
@@ -4271,32 +4363,22 @@ function assertGroupByBy(args, model) {
4271
4363
  if (bySet.size !== byFields.length) {
4272
4364
  throw new Error("buildGroupBySql: by must not contain duplicates");
4273
4365
  }
4274
- const modelFieldMap = getModelFieldMap(model);
4275
4366
  for (const f of byFields) {
4276
- const field = modelFieldMap.get(f);
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
- }
4367
+ assertScalarField(model, f, "groupBy.by");
4287
4368
  }
4288
4369
  return byFields;
4289
4370
  }
4290
4371
  function buildGroupBySelectParts(args, alias, model, byFields) {
4291
4372
  const groupCols = byFields.map((f) => col(alias, f, model));
4373
+ const selectCols = byFields.map((f) => colWithAlias(alias, f, model));
4292
4374
  const groupFields = groupCols.join(SQL_SEPARATORS.FIELD_LIST);
4293
4375
  const aggFields = buildAggregateFields(args, alias, model);
4294
- const selectFields = isNonEmptyArray(aggFields) ? groupCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : groupCols.join(SQL_SEPARATORS.FIELD_LIST);
4376
+ const selectFields = isNonEmptyArray(aggFields) ? selectCols.concat(aggFields).join(SQL_SEPARATORS.FIELD_LIST) : selectCols.join(SQL_SEPARATORS.FIELD_LIST);
4295
4377
  return { groupCols, groupFields, selectFields };
4296
4378
  }
4297
4379
  function buildGroupByHaving(args, alias, params, model, dialect) {
4298
4380
  if (!isNotNullish(args.having)) return "";
4299
- if (!isPlainObject(args.having)) return "";
4381
+ if (!isPlainObject(args.having)) throw new Error("having must be an object");
4300
4382
  const h = buildHavingClause(args.having, alias, params, model, dialect);
4301
4383
  if (!h || h.trim().length === 0) return "";
4302
4384
  return `${SQL_TEMPLATES.HAVING} ${h}`;
@@ -4329,64 +4411,60 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
4329
4411
  const snapshot = params.snapshot();
4330
4412
  validateSelectQuery(sql);
4331
4413
  validateParamConsistency(sql, [...whereResult.params, ...snapshot.params]);
4332
- const mergedParams = [...whereResult.params, ...snapshot.params];
4333
4414
  return Object.freeze({
4334
4415
  sql,
4335
- params: Object.freeze(normalizeFinalParams2(mergedParams)),
4416
+ params: Object.freeze([...whereResult.params, ...snapshot.params]),
4336
4417
  paramMappings: Object.freeze([
4337
4418
  ...whereResult.paramMappings,
4338
4419
  ...snapshot.mappings
4339
4420
  ])
4340
4421
  });
4341
4422
  }
4342
- function buildCountSql(whereResult, tableName, alias, skip, dialect) {
4423
+ function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
4343
4424
  assertSafeAlias(alias);
4344
4425
  assertSafeTableRef(tableName);
4345
- const d = getGlobalDialect();
4426
+ if (skip !== void 0 && skip !== null) {
4427
+ if (isDynamicParameter(skip)) {
4428
+ throw new Error(
4429
+ "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."
4430
+ );
4431
+ }
4432
+ if (typeof skip === "string") {
4433
+ const s = skip.trim();
4434
+ if (s.length > 0) {
4435
+ const n = Number(s);
4436
+ if (Number.isFinite(n) && Number.isInteger(n) && n > 0) {
4437
+ throw new Error(
4438
+ "count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
4439
+ );
4440
+ }
4441
+ }
4442
+ }
4443
+ if (typeof skip === "number" && Number.isFinite(skip) && Number.isInteger(skip) && skip > 0) {
4444
+ throw new Error(
4445
+ "count() with skip is not supported because it produces nondeterministic results. Use findMany().length or add explicit orderBy to ensure deterministic behavior."
4446
+ );
4447
+ }
4448
+ }
4346
4449
  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
4450
  const sql = [
4359
4451
  SQL_TEMPLATES.SELECT,
4360
4452
  SQL_TEMPLATES.COUNT_ALL,
4361
4453
  SQL_TEMPLATES.AS,
4362
4454
  quote("_count._all"),
4363
4455
  SQL_TEMPLATES.FROM,
4364
- `(${subSelect})`,
4365
- SQL_TEMPLATES.AS,
4366
- `"sub"`
4456
+ tableName,
4457
+ alias,
4458
+ whereClause
4367
4459
  ].filter((x) => x && String(x).trim().length > 0).join(" ").trim();
4368
4460
  validateSelectQuery(sql);
4369
- const snapshot = params.snapshot();
4370
- const mergedParams = [...whereResult.params, ...snapshot.params];
4371
- validateParamConsistency(sql, mergedParams);
4461
+ validateParamConsistency(sql, whereResult.params);
4372
4462
  return Object.freeze({
4373
4463
  sql,
4374
- params: Object.freeze(normalizeFinalParams2(mergedParams)),
4375
- paramMappings: Object.freeze([
4376
- ...whereResult.paramMappings,
4377
- ...snapshot.mappings
4378
- ])
4464
+ params: Object.freeze([...whereResult.params]),
4465
+ paramMappings: Object.freeze([...whereResult.paramMappings])
4379
4466
  });
4380
4467
  }
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
4468
  function safeAlias(input) {
4391
4469
  const raw = String(input).toLowerCase();
4392
4470
  const cleaned = raw.replace(/[^a-z0-9_]/g, "_");
@@ -4536,8 +4614,12 @@ function buildAndNormalizeSql(args) {
4536
4614
  );
4537
4615
  }
4538
4616
  function finalizeDirective(args) {
4539
- const { directive, normalizedSql, normalizedMappings } = args;
4540
- validateSqlPositions(normalizedSql, normalizedMappings, getGlobalDialect());
4617
+ const { directive, normalizedSql, normalizedMappings, dialect } = args;
4618
+ const params = normalizedMappings.map((m) => {
4619
+ var _a;
4620
+ return (_a = m.value) != null ? _a : void 0;
4621
+ });
4622
+ validateParamConsistencyByDialect(normalizedSql, params, dialect);
4541
4623
  const { staticParams, dynamicKeys } = buildParamsFromMappings(normalizedMappings);
4542
4624
  return {
4543
4625
  method: directive.method,
@@ -4574,7 +4656,8 @@ function generateSQL(directive) {
4574
4656
  return finalizeDirective({
4575
4657
  directive,
4576
4658
  normalizedSql: normalized.sql,
4577
- normalizedMappings: normalized.paramMappings
4659
+ normalizedMappings: normalized.paramMappings,
4660
+ dialect
4578
4661
  });
4579
4662
  }
4580
4663
  function generateSQL2(directive) {
@@ -4682,18 +4765,57 @@ function generateCode(models, queries, dialect, datamodel) {
4682
4765
  return `// Generated by @prisma-sql/generator - DO NOT EDIT
4683
4766
  import { buildSQL, transformQueryResults, type PrismaMethod, type Model } from 'prisma-sql'
4684
4767
 
4685
- function normalizeValue(value: unknown): unknown {
4768
+ /**
4769
+ * Normalize values for SQL params.
4770
+ * Synced from src/utils/normalize-value.ts
4771
+ */
4772
+ function normalizeValue(value: unknown, seen = new WeakSet<object>(), depth = 0): unknown {
4773
+ const MAX_DEPTH = 20
4774
+ if (depth > MAX_DEPTH) {
4775
+ throw new Error(\`Max normalization depth exceeded (\${MAX_DEPTH} levels)\`)
4776
+ }
4686
4777
  if (value instanceof Date) {
4778
+ const t = value.getTime()
4779
+ if (!Number.isFinite(t)) {
4780
+ throw new Error('Invalid Date value in SQL params')
4781
+ }
4687
4782
  return value.toISOString()
4688
4783
  }
4689
-
4784
+ if (typeof value === 'bigint') {
4785
+ return value.toString()
4786
+ }
4690
4787
  if (Array.isArray(value)) {
4691
- return value.map(normalizeValue)
4788
+ const arrRef = value as unknown as object
4789
+ if (seen.has(arrRef)) {
4790
+ throw new Error('Circular reference in SQL params')
4791
+ }
4792
+ seen.add(arrRef)
4793
+ const out = value.map((v) => normalizeValue(v, seen, depth + 1))
4794
+ seen.delete(arrRef)
4795
+ return out
4796
+ }
4797
+ if (value && typeof value === 'object') {
4798
+ if (value instanceof Uint8Array) return value
4799
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return value
4800
+ const proto = Object.getPrototypeOf(value)
4801
+ const isPlain = proto === Object.prototype || proto === null
4802
+ if (!isPlain) return value
4803
+ const obj = value as Record<string, unknown>
4804
+ if (seen.has(obj)) {
4805
+ throw new Error('Circular reference in SQL params')
4806
+ }
4807
+ seen.add(obj)
4808
+ const out: Record<string, unknown> = {}
4809
+ for (const [k, v] of Object.entries(obj)) {
4810
+ out[k] = normalizeValue(v, seen, depth + 1)
4811
+ }
4812
+ seen.delete(obj)
4813
+ return out
4692
4814
  }
4693
-
4694
4815
  return value
4695
4816
  }
4696
4817
 
4818
+
4697
4819
  export const MODELS: Model[] = ${JSON.stringify(cleanModels, null, 2)}
4698
4820
 
4699
4821
  const ENUM_MAPPINGS: Record<string, Record<string, string>> = ${JSON.stringify(mappings, null, 2)}
@@ -4833,34 +4955,39 @@ function normalizeQuery(args: any): string {
4833
4955
 
4834
4956
  function extractDynamicParams(args: any, dynamicKeys: string[]): unknown[] {
4835
4957
  const params: unknown[] = []
4836
-
4958
+
4837
4959
  for (const key of dynamicKeys) {
4838
4960
  const parts = key.split(':')
4839
4961
  const lookupKey = parts.length === 2 ? parts[1] : key
4840
- const value = args[lookupKey]
4841
-
4962
+
4963
+ const value =
4964
+ lookupKey.includes('.') ? getByPath(args, lookupKey) : args?.[lookupKey]
4965
+
4842
4966
  if (value === undefined) {
4843
4967
  throw new Error(\`Missing required parameter: \${key}\`)
4844
4968
  }
4845
-
4969
+
4846
4970
  params.push(normalizeValue(value))
4847
4971
  }
4848
-
4972
+
4849
4973
  return params
4850
4974
  }
4851
4975
 
4976
+
4852
4977
  async function executeQuery(client: any, sql: string, params: unknown[]): Promise<unknown[]> {
4978
+ const normalizedParams = normalizeParams(params)
4979
+
4853
4980
  if (DIALECT === 'postgres') {
4854
- return await client.unsafe(sql, params)
4981
+ return await client.unsafe(sql, normalizedParams)
4855
4982
  }
4856
-
4983
+
4857
4984
  const stmt = client.prepare(sql)
4858
-
4985
+
4859
4986
  if (sql.toUpperCase().includes('COUNT(*) AS')) {
4860
- return [stmt.get(...params)]
4987
+ return [stmt.get(...normalizedParams)]
4861
4988
  }
4862
-
4863
- return stmt.all(...params)
4989
+
4990
+ return stmt.all(...normalizedParams)
4864
4991
  }
4865
4992
 
4866
4993
  export function speedExtension(config: {
@@ -4905,18 +5032,21 @@ export function speedExtension(config: {
4905
5032
 
4906
5033
  if (prebakedQuery) {
4907
5034
  sql = prebakedQuery.sql
4908
- params = [...prebakedQuery.params, ...extractDynamicParams(transformedArgs, prebakedQuery.dynamicKeys)]
5035
+ params = normalizeParams([
5036
+ ...prebakedQuery.params,
5037
+ ...extractDynamicParams(transformedArgs, prebakedQuery.dynamicKeys),
5038
+ ])
4909
5039
  prebaked = true
4910
5040
  } else {
4911
5041
  const model = MODELS.find((m) => m.name === modelName)
4912
-
5042
+
4913
5043
  if (!model) {
4914
5044
  return this.$parent[modelName][method](args)
4915
5045
  }
4916
5046
 
4917
5047
  const result = buildSQL(model, MODELS, method, transformedArgs, DIALECT)
4918
5048
  sql = result.sql
4919
- params = result.params
5049
+ params = normalizeParams(result.params as unknown[])
4920
5050
  }
4921
5051
 
4922
5052
  if (debug) {