neoorm 0.4.7 → 0.5.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.
Files changed (90) hide show
  1. package/README.md +6 -6
  2. package/dist/codegen/emit-includes.d.ts.map +1 -1
  3. package/dist/codegen/emit-includes.js +3 -1
  4. package/dist/codegen/emit-includes.js.map +1 -1
  5. package/dist/dialect/postgres.d.ts.map +1 -1
  6. package/dist/dialect/postgres.js +8 -4
  7. package/dist/dialect/postgres.js.map +1 -1
  8. package/dist/dialect/sqlite.d.ts.map +1 -1
  9. package/dist/dialect/sqlite.js +14 -4
  10. package/dist/dialect/sqlite.js.map +1 -1
  11. package/dist/dialect/types.d.ts +4 -1
  12. package/dist/dialect/types.d.ts.map +1 -1
  13. package/dist/plugins/builtin.d.ts +6 -2
  14. package/dist/plugins/builtin.d.ts.map +1 -1
  15. package/dist/plugins/builtin.js.map +1 -1
  16. package/dist/runtime/client.d.ts +44 -6
  17. package/dist/runtime/client.d.ts.map +1 -1
  18. package/dist/runtime/client.js +13 -10
  19. package/dist/runtime/client.js.map +1 -1
  20. package/dist/runtime/query/aggregate.d.ts +3 -1
  21. package/dist/runtime/query/aggregate.d.ts.map +1 -1
  22. package/dist/runtime/query/aggregate.js +19 -5
  23. package/dist/runtime/query/aggregate.js.map +1 -1
  24. package/dist/runtime/query/compile.d.ts +49 -10
  25. package/dist/runtime/query/compile.d.ts.map +1 -1
  26. package/dist/runtime/query/compile.js +666 -68
  27. package/dist/runtime/query/compile.js.map +1 -1
  28. package/dist/runtime/query/count.d.ts +10 -2
  29. package/dist/runtime/query/count.d.ts.map +1 -1
  30. package/dist/runtime/query/count.js +39 -3
  31. package/dist/runtime/query/count.js.map +1 -1
  32. package/dist/runtime/query/create.d.ts +2 -0
  33. package/dist/runtime/query/create.d.ts.map +1 -1
  34. package/dist/runtime/query/create.js +7 -4
  35. package/dist/runtime/query/create.js.map +1 -1
  36. package/dist/runtime/query/cursor.d.ts +2 -1
  37. package/dist/runtime/query/cursor.d.ts.map +1 -1
  38. package/dist/runtime/query/cursor.js +21 -2
  39. package/dist/runtime/query/cursor.js.map +1 -1
  40. package/dist/runtime/query/delete.d.ts +3 -0
  41. package/dist/runtime/query/delete.d.ts.map +1 -1
  42. package/dist/runtime/query/delete.js +18 -3
  43. package/dist/runtime/query/delete.js.map +1 -1
  44. package/dist/runtime/query/find-or-create.d.ts.map +1 -1
  45. package/dist/runtime/query/find-or-create.js +3 -3
  46. package/dist/runtime/query/find-or-create.js.map +1 -1
  47. package/dist/runtime/query/find.d.ts +17 -9
  48. package/dist/runtime/query/find.d.ts.map +1 -1
  49. package/dist/runtime/query/find.js +118 -53
  50. package/dist/runtime/query/find.js.map +1 -1
  51. package/dist/runtime/query/group-by.d.ts +23 -0
  52. package/dist/runtime/query/group-by.d.ts.map +1 -0
  53. package/dist/runtime/query/group-by.js +54 -0
  54. package/dist/runtime/query/group-by.js.map +1 -0
  55. package/dist/runtime/query/paginate.d.ts +3 -0
  56. package/dist/runtime/query/paginate.d.ts.map +1 -1
  57. package/dist/runtime/query/paginate.js +34 -12
  58. package/dist/runtime/query/paginate.js.map +1 -1
  59. package/dist/runtime/query/projection.d.ts +19 -0
  60. package/dist/runtime/query/projection.d.ts.map +1 -0
  61. package/dist/runtime/query/projection.js +125 -0
  62. package/dist/runtime/query/projection.js.map +1 -0
  63. package/dist/runtime/query/relation-planner.d.ts +15 -5
  64. package/dist/runtime/query/relation-planner.d.ts.map +1 -1
  65. package/dist/runtime/query/relation-planner.js +103 -43
  66. package/dist/runtime/query/relation-planner.js.map +1 -1
  67. package/dist/runtime/query/table-index.d.ts +1 -0
  68. package/dist/runtime/query/table-index.d.ts.map +1 -1
  69. package/dist/runtime/query/table-index.js +1 -0
  70. package/dist/runtime/query/table-index.js.map +1 -1
  71. package/dist/runtime/query/update.d.ts +4 -0
  72. package/dist/runtime/query/update.d.ts.map +1 -1
  73. package/dist/runtime/query/update.js +55 -25
  74. package/dist/runtime/query/update.js.map +1 -1
  75. package/dist/runtime/query/upsert.d.ts.map +1 -1
  76. package/dist/runtime/query/upsert.js +8 -9
  77. package/dist/runtime/query/upsert.js.map +1 -1
  78. package/dist/runtime/types.d.ts +23 -11
  79. package/dist/runtime/types.d.ts.map +1 -1
  80. package/dist/schema/column-where.d.ts +3 -0
  81. package/dist/schema/column-where.d.ts.map +1 -1
  82. package/dist/schema/index.d.ts +2 -2
  83. package/dist/schema/index.d.ts.map +1 -1
  84. package/dist/schema/index.js +1 -1
  85. package/dist/schema/index.js.map +1 -1
  86. package/dist/schema/relation-types.d.ts +17 -8
  87. package/dist/schema/relation-types.d.ts.map +1 -1
  88. package/dist/schema/types.d.ts +117 -16
  89. package/dist/schema/types.d.ts.map +1 -1
  90. package/package.json +5 -5
@@ -1,5 +1,5 @@
1
1
  import { effectiveRelations } from "../../codegen/manifest-relations.js";
2
- import { postgresDialect, quoteIdentifier, tableRef } from "../../dialect/postgres.js";
2
+ import { postgresDialect, quoteIdentifier, tableRef, } from "../../dialect/postgres.js";
3
3
  import { getColumnType } from "../../plugins/registry.js";
4
4
  import { rebaseParamRefs } from "../../sql/template.js";
5
5
  import { findM2M } from "./manifest-lookup.js";
@@ -20,6 +20,35 @@ const operatorParamTransform = {
20
20
  startsWith: (v) => `${String(v)}%`,
21
21
  endsWith: (v) => `%${String(v)}`,
22
22
  };
23
+ function parseQueryMode(value) {
24
+ if (value === undefined || value === "default")
25
+ return "default";
26
+ if (value === "insensitive")
27
+ return "insensitive";
28
+ throw new Error(`unsupported query mode: ${String(value)}`);
29
+ }
30
+ function isStringPatternOp(op) {
31
+ return (op === "contains" ||
32
+ op === "startsWith" ||
33
+ op === "endsWith" ||
34
+ op === "search");
35
+ }
36
+ function stringFilterSql(op, sqlCol, paramIndex, mode, dialect) {
37
+ switch (op) {
38
+ case "contains":
39
+ case "startsWith":
40
+ case "endsWith":
41
+ return mode === "insensitive"
42
+ ? dialect.ilike(sqlCol, paramIndex)
43
+ : dialect.whereOperators[op](sqlCol, paramIndex);
44
+ case "search":
45
+ return dialect.regex(sqlCol, paramIndex, mode === "insensitive");
46
+ default: {
47
+ const _never = op;
48
+ throw new Error(`unsupported string filter: ${_never}`);
49
+ }
50
+ }
51
+ }
23
52
  function pluginWhereOperators(col) {
24
53
  if (col.kind === "fk")
25
54
  return {};
@@ -60,6 +89,7 @@ function compileColumnCondition(col, rawValue, dialect, paramIndex, columnRef) {
60
89
  nextParamIndex++;
61
90
  return { sql: conditions.join(" AND "), params, nextParamIndex };
62
91
  }
92
+ const queryMode = parseQueryMode(rawValue.mode);
63
93
  const hasOperator = Object.keys(rawValue).some((k) => k in dialect.whereOperators || k in spatialOps);
64
94
  if (!hasOperator) {
65
95
  conditions.push(dialect.whereOperators.equals(sqlCol, nextParamIndex));
@@ -68,6 +98,8 @@ function compileColumnCondition(col, rawValue, dialect, paramIndex, columnRef) {
68
98
  return { sql: conditions.join(" AND "), params, nextParamIndex };
69
99
  }
70
100
  for (const [op, value] of Object.entries(rawValue)) {
101
+ if (op === "mode")
102
+ continue;
71
103
  if (op in spatialOps) {
72
104
  const operator = spatialOps[op];
73
105
  if (!operator)
@@ -102,7 +134,9 @@ function compileColumnCondition(col, rawValue, dialect, paramIndex, columnRef) {
102
134
  : transform
103
135
  ? transform(value)
104
136
  : serializeColumnValue(col, value);
105
- conditions.push(dialect.whereOperators[operator](sqlCol, nextParamIndex));
137
+ conditions.push(isStringPatternOp(operator)
138
+ ? stringFilterSql(operator, sqlCol, nextParamIndex, queryMode, dialect)
139
+ : dialect.whereOperators[operator](sqlCol, nextParamIndex));
106
140
  params.push(paramValue);
107
141
  nextParamIndex++;
108
142
  }
@@ -282,13 +316,15 @@ function compileWhereNode(manifest, table, where, dialect, startParamIndex, colu
282
316
  nextParamIndex: paramIndex,
283
317
  };
284
318
  }
285
- export function compileWhere(manifest, table, where, dialect, startParamIndex = 1, manifestIndex, qualifyColumns = false) {
319
+ export function compileWhere(manifest, table, where, dialect, startParamIndex = 1, manifestIndex, qualifyColumns = false, tableAlias) {
286
320
  if (!where || Object.keys(where).length === 0) {
287
321
  return { sql: "", params: [] };
288
322
  }
289
- const columnRef = qualifyColumns
290
- ? qualifiedColumnRefForTable(table)
291
- : defaultColumnRef;
323
+ const columnRef = tableAlias
324
+ ? (col) => `${quoteIdentifier(tableAlias)}.${quoteIdentifier(col.sqlName)}`
325
+ : qualifyColumns
326
+ ? qualifiedColumnRefForTable(table)
327
+ : defaultColumnRef;
292
328
  const result = compileWhereNode(manifest, table, where, dialect, startParamIndex, columnRef, manifestIndex);
293
329
  const impossible = isImpossibleWhereSql(result.sql);
294
330
  return {
@@ -297,19 +333,23 @@ export function compileWhere(manifest, table, where, dialect, startParamIndex =
297
333
  ...(impossible ? { impossible: true } : {}),
298
334
  };
299
335
  }
300
- function whereShapeKey(where) {
336
+ export function whereShapeKey(where) {
301
337
  const parts = [];
302
338
  for (const [key, value] of Object.entries(where)) {
303
339
  if (key === "AND" && Array.isArray(value)) {
304
340
  parts.push(`AND:${value
305
- .filter((item) => !!item && typeof item === "object" && !Array.isArray(item))
341
+ .filter((item) => !!item &&
342
+ typeof item === "object" &&
343
+ !Array.isArray(item))
306
344
  .map((item) => whereShapeKey(item))
307
345
  .join(",")}`);
308
346
  continue;
309
347
  }
310
348
  if (key === "OR" && Array.isArray(value)) {
311
349
  parts.push(`OR:${value
312
- .filter((item) => !!item && typeof item === "object" && !Array.isArray(item))
350
+ .filter((item) => !!item &&
351
+ typeof item === "object" &&
352
+ !Array.isArray(item))
313
353
  .map((item) => whereShapeKey(item))
314
354
  .join(",")}`);
315
355
  continue;
@@ -332,7 +372,9 @@ function whereShapeKey(where) {
332
372
  parts.push(`${key}:${op}:${len}`);
333
373
  }
334
374
  else {
335
- parts.push(`${key}:${ops.join(",")}`);
375
+ const modePart = typeof value.mode === "string" ? `:mode:${value.mode}` : "";
376
+ const opsForKey = ops.filter((op) => op !== "mode");
377
+ parts.push(`${key}:${opsForKey.join(",")}${modePart}`);
336
378
  }
337
379
  continue;
338
380
  }
@@ -345,12 +387,17 @@ function collectWhereParams(manifest, table, where, dialect, manifestIndex) {
345
387
  function walk(node, columnRef) {
346
388
  const tableIndex = getTableIndex(manifestIndex, table.accessor);
347
389
  const relations = tableIndex?.effectiveRelationsByName ??
348
- new Map(effectiveRelations(manifest, table).map((rel) => [rel.name, rel]));
390
+ new Map(effectiveRelations(manifest, table).map((rel) => [
391
+ rel.name,
392
+ rel,
393
+ ]));
349
394
  for (const [key, value] of Object.entries(node)) {
350
395
  if (key === "AND" || key === "OR") {
351
396
  if (Array.isArray(value)) {
352
397
  for (const item of value) {
353
- if (item && typeof item === "object" && !Array.isArray(item)) {
398
+ if (item &&
399
+ typeof item === "object" &&
400
+ !Array.isArray(item)) {
354
401
  walk(item, columnRef);
355
402
  }
356
403
  }
@@ -413,7 +460,9 @@ export function orderByShapeKey(orderBy, tableAlias) {
413
460
  return "";
414
461
  const entries = Object.entries(orderBy)
415
462
  .filter(([key]) => key !== "_count")
416
- .map(([key, direction]) => typeof direction === "string" ? `${key}:${direction.toUpperCase()}` : "")
463
+ .map(([key, direction]) => typeof direction === "string"
464
+ ? `${key}:${direction.toUpperCase()}`
465
+ : "")
417
466
  .filter(Boolean)
418
467
  .sort((a, b) => a.localeCompare(b));
419
468
  if (entries.length === 0)
@@ -458,9 +507,108 @@ function buildValuePlaceholder(col, paramIndex) {
458
507
  }
459
508
  return `$${paramIndex}`;
460
509
  }
461
- function buildSetExpression(col, paramIndex) {
510
+ function isBinaryValue(value) {
511
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value))
512
+ return true;
513
+ return value instanceof Uint8Array;
514
+ }
515
+ const ATOMIC_UPDATE_OPS = new Set([
516
+ "increment",
517
+ "decrement",
518
+ "multiply",
519
+ "set",
520
+ ]);
521
+ const NUMERIC_UPDATE_KINDS = new Set(["int", "serial", "decimal", "bigint"]);
522
+ function isNumericUpdateKind(kind) {
523
+ return NUMERIC_UPDATE_KINDS.has(kind);
524
+ }
525
+ function needsNumericCast(col, dialect) {
526
+ return (col.kind === "decimal" ||
527
+ (dialect.name === "sqlite" && col.kind === "bigint"));
528
+ }
529
+ function isAtomicOp(op) {
530
+ return ATOMIC_UPDATE_OPS.has(op);
531
+ }
532
+ function arithmeticSql(op, left, right) {
533
+ switch (op) {
534
+ case "increment":
535
+ return `${left} + ${right}`;
536
+ case "decrement":
537
+ return `${left} - ${right}`;
538
+ case "multiply":
539
+ return `${left} * ${right}`;
540
+ default: {
541
+ const _never = op;
542
+ throw new Error(`unsupported atomic update: ${_never}`);
543
+ }
544
+ }
545
+ }
546
+ export function parseAtomicUpdate(col, value) {
547
+ if (!isOperatorObject(value) || isBinaryValue(value)) {
548
+ return { op: "set", value };
549
+ }
550
+ const keys = Object.keys(value);
551
+ const opKeys = keys.filter(isAtomicOp);
552
+ if (opKeys.length === 0) {
553
+ if (isNumericUpdateKind(col.kind)) {
554
+ throw new Error(`update on ${col.tsName} requires increment, decrement, multiply, or set`);
555
+ }
556
+ return { op: "set", value };
557
+ }
558
+ if (opKeys.length !== keys.length) {
559
+ throw new Error(`update on ${col.tsName} cannot mix operators with other keys`);
560
+ }
561
+ if (opKeys.length !== 1) {
562
+ throw new Error(`update on ${col.tsName} allows only one of increment, decrement, multiply, set`);
563
+ }
564
+ const op = opKeys[0];
565
+ if (op === undefined) {
566
+ throw new Error(`update on ${col.tsName} requires an operator`);
567
+ }
568
+ if (value[op] === undefined) {
569
+ throw new Error(`update ${op} on ${col.tsName} requires a value`);
570
+ }
571
+ if (op !== "set" && !isNumericUpdateKind(col.kind)) {
572
+ throw new Error(`${op} is not supported on ${col.kind} column ${col.tsName}`);
573
+ }
574
+ return { op, value: value[op] };
575
+ }
576
+ function orderUpdateAssignments(dataKeys, ops) {
577
+ const pairs = dataKeys.map((key, i) => ({
578
+ key,
579
+ op: ops?.[i] ?? "set",
580
+ }));
581
+ pairs.sort((a, b) => a.key.localeCompare(b.key));
582
+ return {
583
+ keys: pairs.map((pair) => pair.key),
584
+ ops: pairs.map((pair) => pair.op),
585
+ };
586
+ }
587
+ function buildSetExpression(col, paramIndex, op = "set", dialect = postgresDialect) {
462
588
  const sqlCol = quoteIdentifier(col?.sqlName ?? "");
463
- return `${sqlCol} = ${buildValuePlaceholder(col, paramIndex)}`;
589
+ const placeholder = buildValuePlaceholder(col, paramIndex);
590
+ switch (op) {
591
+ case "set":
592
+ return `${sqlCol} = ${placeholder}`;
593
+ case "increment":
594
+ case "decrement":
595
+ case "multiply": {
596
+ if (!col) {
597
+ throw new Error("atomic update requires a column");
598
+ }
599
+ const left = needsNumericCast(col, dialect)
600
+ ? dialect.castToNumeric(sqlCol)
601
+ : sqlCol;
602
+ const right = needsNumericCast(col, dialect)
603
+ ? dialect.castToNumeric(placeholder)
604
+ : placeholder;
605
+ return `${sqlCol} = ${arithmeticSql(op, left, right)}`;
606
+ }
607
+ default: {
608
+ const _never = op;
609
+ throw new Error(`unsupported atomic update: ${_never}`);
610
+ }
611
+ }
464
612
  }
465
613
  export function compileOrderBy(table, orderBy, tableAlias, manifestIndex) {
466
614
  if (!orderBy || Object.keys(orderBy).length === 0)
@@ -518,10 +666,10 @@ export function buildQualifiedSelectColumns(table, select, manifestIndex) {
518
666
  : table.columns;
519
667
  return cols.map((c) => `${ref}.${selectExpression(c)}`).join(", ");
520
668
  }
521
- export function buildFindByIdQuery(table) {
669
+ export function buildFindByIdQuery(table, select, manifestIndex) {
522
670
  const { sqlName } = requireScalarPrimaryKey(table);
523
671
  const sqlCol = quoteIdentifier(sqlName);
524
- const selectCols = buildSelectColumns(table);
672
+ const selectCols = buildSelectColumns(table, select, manifestIndex);
525
673
  return `SELECT ${selectCols} FROM ${tableRef(table)} WHERE ${sqlCol} = $1`;
526
674
  }
527
675
  export function buildFindAllQuery(table) {
@@ -533,12 +681,12 @@ export function normalizeLimitOffset(value, label) {
533
681
  }
534
682
  return value;
535
683
  }
536
- export function buildFindManyQuery(table, whereSql, orderSql, limit, offset, distinctOn, extraSelectCols, joinClauses, manifestIndex, groupBySql) {
684
+ export function buildFindManyQuery(table, whereSql, orderSql, take, skip, distinctOn, extraSelectCols, joinClauses, manifestIndex, groupBySql, select) {
537
685
  const hasJoins = Boolean(joinClauses && joinClauses.length > 0);
538
686
  const tableIndex = getTableIndex(manifestIndex, table.accessor);
539
687
  const selectCols = hasJoins
540
- ? buildQualifiedSelectColumns(table, undefined, manifestIndex)
541
- : buildSelectColumns(table, undefined, manifestIndex);
688
+ ? buildQualifiedSelectColumns(table, select, manifestIndex)
689
+ : buildSelectColumns(table, select, manifestIndex);
542
690
  let sql = "SELECT ";
543
691
  if (distinctOn && distinctOn.length > 0) {
544
692
  const distinctCols = columnsByTsNames(tableIndex, table, distinctOn)
@@ -562,23 +710,99 @@ export function buildFindManyQuery(table, whereSql, orderSql, limit, offset, dis
562
710
  sql += ` ${groupBySql}`;
563
711
  if (orderSql)
564
712
  sql += ` ${orderSql}`;
565
- if (limit !== undefined) {
566
- sql += ` LIMIT ${normalizeLimitOffset(limit, "limit")}`;
713
+ if (take !== undefined) {
714
+ sql += ` LIMIT ${normalizeLimitOffset(take, "take")}`;
567
715
  }
568
- if (offset !== undefined) {
569
- sql += ` OFFSET ${normalizeLimitOffset(offset, "offset")}`;
716
+ if (skip !== undefined) {
717
+ sql += ` OFFSET ${normalizeLimitOffset(skip, "skip")}`;
570
718
  }
571
719
  return sql;
572
720
  }
573
721
  export function buildPaginateQuery(table, whereSql, orderSql, take, extraSelectCols, joinClauses, manifestIndex) {
574
722
  return buildFindManyQuery(table, whereSql, orderSql, take + 1, undefined, undefined, extraSelectCols, joinClauses, manifestIndex);
575
723
  }
576
- export function buildCountQuery(table, whereSql, dialect = postgresDialect) {
577
- let sql = `SELECT ${dialect.castToInt("COUNT(*)")} AS count FROM ${tableRef(table)}`;
724
+ export function buildCountQuery(table, whereSql, dialect = postgresDialect, distinct, select, manifestIndex) {
725
+ if (select !== undefined) {
726
+ if (distinct) {
727
+ throw new Error("count cannot combine distinct and select");
728
+ }
729
+ const parts = countSelectParts(table, select, dialect, manifestIndex);
730
+ if (parts.length === 0) {
731
+ throw new Error("count select requires at least one field");
732
+ }
733
+ let sql = `SELECT ${parts.join(", ")} FROM ${tableRef(table)}`;
734
+ if (whereSql)
735
+ sql += ` ${whereSql}`;
736
+ return sql;
737
+ }
738
+ let expr = "COUNT(*)";
739
+ if (distinct) {
740
+ const sqlCol = requireCountSqlCol(table, distinct, manifestIndex);
741
+ expr = `COUNT(DISTINCT ${sqlCol})`;
742
+ }
743
+ let sql = `SELECT ${dialect.castToInt(expr)} AS count FROM ${tableRef(table)}`;
744
+ if (whereSql)
745
+ sql += ` ${whereSql}`;
746
+ return sql;
747
+ }
748
+ export function buildExistsQuery(table, whereSql) {
749
+ let sql = `SELECT 1 FROM ${tableRef(table)}`;
578
750
  if (whereSql)
579
751
  sql += ` ${whereSql}`;
752
+ sql += " LIMIT 1";
580
753
  return sql;
581
754
  }
755
+ function countSqlCol(table, tsName, manifestIndex) {
756
+ const tableIndex = getTableIndex(manifestIndex, table.accessor);
757
+ const col = columnByTsName(tableIndex, table, tsName);
758
+ if (!col)
759
+ return undefined;
760
+ return quoteIdentifier(col.sqlName);
761
+ }
762
+ export function requireCountSqlCol(table, tsName, manifestIndex) {
763
+ const sqlCol = countSqlCol(table, tsName, manifestIndex);
764
+ if (!sqlCol) {
765
+ throw new Error(`Unknown count column: ${tsName}`);
766
+ }
767
+ return sqlCol;
768
+ }
769
+ export function normalizeCountMap(select) {
770
+ const map = {};
771
+ for (const [key, value] of Object.entries(select)) {
772
+ if (value === true)
773
+ map[key] = true;
774
+ }
775
+ return map;
776
+ }
777
+ export function toCountSelector(value) {
778
+ if (value === true)
779
+ return true;
780
+ const map = normalizeCountMap(value);
781
+ if (Object.keys(map).length === 0) {
782
+ throw new Error("_count requires at least one field");
783
+ }
784
+ return map;
785
+ }
786
+ export function hasStarCount(selectors) {
787
+ return (selectors._count === true ||
788
+ (typeof selectors._count === "object" && selectors._count._all === true));
789
+ }
790
+ export function hasCountField(selectors, field) {
791
+ return (typeof selectors._count === "object" && selectors._count[field] === true);
792
+ }
793
+ export function countSelectParts(table, select, dialect = postgresDialect, manifestIndex) {
794
+ const parts = [];
795
+ if (select._all) {
796
+ parts.push(`${dialect.castToInt("COUNT(*)")} AS "_all"`);
797
+ }
798
+ for (const key of Object.keys(select).sort()) {
799
+ if (key === "_all")
800
+ continue;
801
+ const sqlCol = requireCountSqlCol(table, key, manifestIndex);
802
+ parts.push(`${dialect.castToInt(`COUNT(${sqlCol})`)} AS ${quoteIdentifier(key)}`);
803
+ }
804
+ return parts;
805
+ }
582
806
  function aggregateSqlCol(table, tsName, dialect, manifestIndex) {
583
807
  const tableIndex = getTableIndex(manifestIndex, table.accessor);
584
808
  const col = columnByTsName(tableIndex, table, tsName);
@@ -589,31 +813,58 @@ function aggregateSqlCol(table, tsName, dialect, manifestIndex) {
589
813
  return dialect.castToNumeric(sqlCol);
590
814
  return sqlCol;
591
815
  }
592
- export function buildAggregateQuery(table, selectors, whereSql, manifestIndex, dialect = postgresDialect) {
816
+ const FIELD_AGG_KEYS = ["_avg", "_sum", "_min", "_max"];
817
+ function sqlFnForFieldAgg(key) {
818
+ switch (key) {
819
+ case "_avg":
820
+ return "AVG";
821
+ case "_sum":
822
+ return "SUM";
823
+ case "_min":
824
+ return "MIN";
825
+ case "_max":
826
+ return "MAX";
827
+ default: {
828
+ const _never = key;
829
+ throw new Error(`unsupported aggregate: ${_never}`);
830
+ }
831
+ }
832
+ }
833
+ function fieldAggExpression(key, table, colName, dialect, manifestIndex) {
834
+ const sqlCol = aggregateSqlCol(table, colName, dialect, manifestIndex);
835
+ if (!sqlCol)
836
+ return undefined;
837
+ return `${sqlFnForFieldAgg(key)}(${sqlCol})`;
838
+ }
839
+ export function aggregateSelectParts(table, selectors, dialect = postgresDialect, manifestIndex) {
593
840
  const parts = [];
594
- if (selectors._count) {
841
+ if (selectors._count === true) {
595
842
  parts.push(`${dialect.castToInt("COUNT(*)")} AS "__count"`);
596
843
  }
597
- for (const colName of Object.keys(selectors._avg ?? {})) {
598
- const sqlCol = aggregateSqlCol(table, colName, dialect, manifestIndex);
599
- if (sqlCol)
600
- parts.push(`AVG(${sqlCol}) AS "_avg_${colName}"`);
601
- }
602
- for (const colName of Object.keys(selectors._sum ?? {})) {
603
- const sqlCol = aggregateSqlCol(table, colName, dialect, manifestIndex);
604
- if (sqlCol)
605
- parts.push(`SUM(${sqlCol}) AS "_sum_${colName}"`);
606
- }
607
- for (const colName of Object.keys(selectors._min ?? {})) {
608
- const sqlCol = aggregateSqlCol(table, colName, dialect, manifestIndex);
609
- if (sqlCol)
610
- parts.push(`MIN(${sqlCol}) AS "_min_${colName}"`);
844
+ else if (selectors._count) {
845
+ for (const key of Object.keys(selectors._count).sort()) {
846
+ if (key === "_all") {
847
+ parts.push(`${dialect.castToInt("COUNT(*)")} AS "__count_all"`);
848
+ continue;
849
+ }
850
+ const sqlCol = requireCountSqlCol(table, key, manifestIndex);
851
+ parts.push(`${dialect.castToInt(`COUNT(${sqlCol})`)} AS "__count_${key}"`);
852
+ }
611
853
  }
612
- for (const colName of Object.keys(selectors._max ?? {})) {
613
- const sqlCol = aggregateSqlCol(table, colName, dialect, manifestIndex);
614
- if (sqlCol)
615
- parts.push(`MAX(${sqlCol}) AS "_max_${colName}"`);
854
+ for (const key of FIELD_AGG_KEYS) {
855
+ const fieldMap = selectors[key];
856
+ if (!fieldMap)
857
+ continue;
858
+ for (const colName of Object.keys(fieldMap)) {
859
+ const expr = fieldAggExpression(key, table, colName, dialect, manifestIndex);
860
+ if (expr)
861
+ parts.push(`${expr} AS "${key}_${colName}"`);
862
+ }
616
863
  }
864
+ return parts;
865
+ }
866
+ export function buildAggregateQuery(table, selectors, whereSql, manifestIndex, dialect = postgresDialect) {
867
+ const parts = aggregateSelectParts(table, selectors, dialect, manifestIndex);
617
868
  if (parts.length === 0) {
618
869
  throw new Error("aggregate requires at least one selector");
619
870
  }
@@ -624,8 +875,12 @@ export function buildAggregateQuery(table, selectors, whereSql, manifestIndex, d
624
875
  }
625
876
  export function aggregateSelectorCacheKey(selectors) {
626
877
  const parts = [];
627
- if (selectors._count)
878
+ if (selectors._count === true) {
628
879
  parts.push("_count");
880
+ }
881
+ else if (selectors._count) {
882
+ parts.push(`_count:${Object.keys(selectors._count).sort().join(",")}`);
883
+ }
629
884
  for (const key of ["_avg", "_sum", "_min", "_max"]) {
630
885
  const fieldMap = selectors[key];
631
886
  if (!fieldMap)
@@ -641,7 +896,297 @@ export function getCachedAggregateQuery(tableIndex, table, selectors, whereSql,
641
896
  }
642
897
  return getOrSetSqlCache(tableIndex.aggregateSqlBySelector, cacheKey, () => buildAggregateQuery(table, selectors, whereSql, manifestIndex, dialect));
643
898
  }
644
- export function buildUpsertQuery(table, insertKeys, updateKeys, conflictSqlColumns, exprSets = [], manifestIndex) {
899
+ function isHavingOperator(op) {
900
+ return (op === "equals" ||
901
+ op === "gt" ||
902
+ op === "gte" ||
903
+ op === "lt" ||
904
+ op === "lte" ||
905
+ op === "in" ||
906
+ op === "notIn");
907
+ }
908
+ function requireStarCount(selectors, context) {
909
+ if (!hasStarCount(selectors)) {
910
+ throw new Error(`${context} requires _count: true or _count: { _all: true }`);
911
+ }
912
+ }
913
+ function requireCountMapField(selectors, field, context) {
914
+ if (field === "_all") {
915
+ requireStarCount(selectors, context);
916
+ return;
917
+ }
918
+ if (!hasCountField(selectors, field)) {
919
+ throw new Error(`${context} requires _count: { ${field}: true }`);
920
+ }
921
+ }
922
+ function isStarHavingSpec(spec) {
923
+ if (typeof spec === "number")
924
+ return true;
925
+ const keys = Object.keys(spec);
926
+ return keys.every((key) => isHavingOperator(key));
927
+ }
928
+ function isMixedCountHaving(spec) {
929
+ const keys = Object.keys(spec);
930
+ const hasOps = keys.some((key) => isHavingOperator(key));
931
+ const hasFields = keys.some((key) => !isHavingOperator(key));
932
+ return hasOps && hasFields;
933
+ }
934
+ function countStarExpr() {
935
+ return "COUNT(*)";
936
+ }
937
+ function countFieldExpr(table, field, manifestIndex) {
938
+ if (field === "_all")
939
+ return countStarExpr();
940
+ return `COUNT(${requireCountSqlCol(table, field, manifestIndex)})`;
941
+ }
942
+ function requireSelectedFieldAgg(selectors, key, colName) {
943
+ if (!selectors[key]?.[colName]) {
944
+ throw new Error(`having.${key}.${colName} requires ${key}: { ${colName}: true }`);
945
+ }
946
+ }
947
+ function compileHavingCompare(expr, spec, dialect, paramIndex) {
948
+ const conditions = [];
949
+ const params = [];
950
+ let nextParamIndex = paramIndex;
951
+ let impossible = false;
952
+ const ops = typeof spec === "number" ? { equals: spec } : spec;
953
+ for (const [op, value] of Object.entries(ops)) {
954
+ if (!isHavingOperator(op)) {
955
+ throw new Error(`unsupported having operator: ${op}`);
956
+ }
957
+ switch (op) {
958
+ case "in":
959
+ case "notIn": {
960
+ if (Array.isArray(value) && value.length === 0) {
961
+ if (op === "in") {
962
+ conditions.push("1=0");
963
+ impossible = true;
964
+ }
965
+ else {
966
+ conditions.push("1=1");
967
+ }
968
+ break;
969
+ }
970
+ conditions.push(dialect.whereOperators[op](expr, nextParamIndex));
971
+ params.push(value);
972
+ nextParamIndex++;
973
+ break;
974
+ }
975
+ case "equals":
976
+ case "gt":
977
+ case "gte":
978
+ case "lt":
979
+ case "lte": {
980
+ conditions.push(dialect.whereOperators[op](expr, nextParamIndex));
981
+ params.push(value);
982
+ nextParamIndex++;
983
+ break;
984
+ }
985
+ default: {
986
+ const _never = op;
987
+ throw new Error(`unsupported having operator: ${_never}`);
988
+ }
989
+ }
990
+ }
991
+ return {
992
+ sql: conditions.join(" AND "),
993
+ params,
994
+ nextParamIndex,
995
+ ...(impossible ? { impossible: true } : {}),
996
+ };
997
+ }
998
+ function requireFieldAggExpression(key, table, colName, dialect, manifestIndex) {
999
+ const expr = fieldAggExpression(key, table, colName, dialect, manifestIndex);
1000
+ if (!expr) {
1001
+ throw new Error(`Unknown aggregate column: ${colName}`);
1002
+ }
1003
+ return expr;
1004
+ }
1005
+ export function compileHaving(table, selectors, having, dialect, startParamIndex = 1, manifestIndex) {
1006
+ if (!having || Object.keys(having).length === 0) {
1007
+ return { sql: "", params: [] };
1008
+ }
1009
+ for (const key of Object.keys(having)) {
1010
+ if (key !== "_count" &&
1011
+ key !== "_avg" &&
1012
+ key !== "_sum" &&
1013
+ key !== "_min" &&
1014
+ key !== "_max") {
1015
+ throw new Error(`unsupported having key: ${key}`);
1016
+ }
1017
+ }
1018
+ const conditions = [];
1019
+ const params = [];
1020
+ let paramIndex = startParamIndex;
1021
+ let impossible = false;
1022
+ const pushCompare = (expr, spec) => {
1023
+ const compiled = compileHavingCompare(expr, spec, dialect, paramIndex);
1024
+ if (compiled.sql)
1025
+ conditions.push(compiled.sql);
1026
+ params.push(...compiled.params);
1027
+ paramIndex = compiled.nextParamIndex;
1028
+ if (compiled.impossible)
1029
+ impossible = true;
1030
+ };
1031
+ if (having._count !== undefined) {
1032
+ const spec = having._count;
1033
+ if (typeof spec === "number") {
1034
+ requireStarCount(selectors, "having._count");
1035
+ pushCompare(countStarExpr(), spec);
1036
+ }
1037
+ else if (typeof spec === "object" && spec !== null) {
1038
+ if (isMixedCountHaving(spec)) {
1039
+ throw new Error("having._count cannot mix comparison operators with field keys");
1040
+ }
1041
+ if (isStarHavingSpec(spec)) {
1042
+ requireStarCount(selectors, "having._count");
1043
+ pushCompare(countStarExpr(), spec);
1044
+ }
1045
+ else {
1046
+ for (const [field, fieldSpec] of Object.entries(spec)) {
1047
+ if (typeof fieldSpec === "number") {
1048
+ requireCountMapField(selectors, field, `having._count.${field}`);
1049
+ pushCompare(countFieldExpr(table, field, manifestIndex), fieldSpec);
1050
+ continue;
1051
+ }
1052
+ if (typeof fieldSpec !== "object" || fieldSpec === null) {
1053
+ throw new Error(`invalid having._count.${field} predicate`);
1054
+ }
1055
+ requireCountMapField(selectors, field, `having._count.${field}`);
1056
+ pushCompare(countFieldExpr(table, field, manifestIndex), fieldSpec);
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ for (const key of FIELD_AGG_KEYS) {
1062
+ const fieldMap = having[key];
1063
+ if (!fieldMap)
1064
+ continue;
1065
+ for (const [colName, spec] of Object.entries(fieldMap)) {
1066
+ requireSelectedFieldAgg(selectors, key, colName);
1067
+ if (typeof spec !== "number" &&
1068
+ (typeof spec !== "object" || spec === null)) {
1069
+ throw new Error(`invalid having.${key}.${colName} predicate`);
1070
+ }
1071
+ const expr = requireFieldAggExpression(key, table, colName, dialect, manifestIndex);
1072
+ pushCompare(expr, spec);
1073
+ }
1074
+ }
1075
+ if (conditions.length === 0)
1076
+ return { sql: "", params: [] };
1077
+ return {
1078
+ sql: `HAVING ${conditions.join(" AND ")}`,
1079
+ params,
1080
+ ...(impossible ? { impossible: true } : {}),
1081
+ };
1082
+ }
1083
+ export function compileGroupByOrderBy(table, byKeys, selectors, orderBy, dialect = postgresDialect, manifestIndex) {
1084
+ if (!orderBy || Object.keys(orderBy).length === 0)
1085
+ return "";
1086
+ const tableIndex = getTableIndex(manifestIndex, table.accessor);
1087
+ const bySet = new Set(byKeys);
1088
+ const parts = [];
1089
+ for (const [tsKey, direction] of Object.entries(orderBy)) {
1090
+ if (tsKey === "_count") {
1091
+ if (typeof direction === "string") {
1092
+ requireStarCount(selectors, "orderBy._count");
1093
+ const dir = direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
1094
+ parts.push(`${countStarExpr()} ${dir}`);
1095
+ continue;
1096
+ }
1097
+ if (typeof direction !== "object" || direction === null) {
1098
+ throw new Error('orderBy._count must be "asc" or "desc" or a field map');
1099
+ }
1100
+ for (const [field, colDir] of Object.entries(direction)) {
1101
+ if (typeof colDir !== "string")
1102
+ continue;
1103
+ requireCountMapField(selectors, field, `orderBy._count.${field}`);
1104
+ const dir = colDir.toUpperCase() === "DESC" ? "DESC" : "ASC";
1105
+ parts.push(`${countFieldExpr(table, field, manifestIndex)} ${dir}`);
1106
+ }
1107
+ continue;
1108
+ }
1109
+ if (tsKey === "_avg" ||
1110
+ tsKey === "_sum" ||
1111
+ tsKey === "_min" ||
1112
+ tsKey === "_max") {
1113
+ const key = tsKey;
1114
+ if (typeof direction !== "object" || direction === null) {
1115
+ throw new Error(`orderBy.${key} must be a column map`);
1116
+ }
1117
+ for (const [colName, colDir] of Object.entries(direction)) {
1118
+ if (typeof colDir !== "string")
1119
+ continue;
1120
+ if (!selectors[key]?.[colName]) {
1121
+ throw new Error(`orderBy.${key}.${colName} requires ${key}: { ${colName}: true }`);
1122
+ }
1123
+ const expr = requireFieldAggExpression(key, table, colName, dialect, manifestIndex);
1124
+ const dir = colDir.toUpperCase() === "DESC" ? "DESC" : "ASC";
1125
+ parts.push(`${expr} ${dir}`);
1126
+ }
1127
+ continue;
1128
+ }
1129
+ if (typeof direction !== "string")
1130
+ continue;
1131
+ if (!bySet.has(tsKey)) {
1132
+ throw new Error(`orderBy column "${tsKey}" is not in groupBy by`);
1133
+ }
1134
+ const col = columnByTsName(tableIndex, table, tsKey);
1135
+ if (!col) {
1136
+ throw new Error(`Unknown groupBy column: ${tsKey}`);
1137
+ }
1138
+ const dir = direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
1139
+ parts.push(`${quoteIdentifier(col.sqlName)} ${dir}`);
1140
+ }
1141
+ return parts.length > 0 ? `ORDER BY ${parts.join(", ")}` : "";
1142
+ }
1143
+ export function resolveGroupByColumns(table, byKeys, manifestIndex) {
1144
+ if (byKeys.length === 0) {
1145
+ throw new Error("groupBy requires at least one column");
1146
+ }
1147
+ const tableIndex = getTableIndex(manifestIndex, table.accessor);
1148
+ const cols = [];
1149
+ for (const key of byKeys) {
1150
+ const col = columnByTsName(tableIndex, table, key);
1151
+ if (!col) {
1152
+ throw new Error(`Unknown groupBy column: ${key}`);
1153
+ }
1154
+ cols.push(col);
1155
+ }
1156
+ return cols;
1157
+ }
1158
+ export function buildGroupByQuery(table, byKeys, selectors, whereSql, havingSql, orderSql, take, skip, manifestIndex, dialect = postgresDialect) {
1159
+ const byCols = resolveGroupByColumns(table, byKeys, manifestIndex);
1160
+ const selectBy = buildSelectColumns(table, byKeys, manifestIndex);
1161
+ const aggParts = aggregateSelectParts(table, selectors, dialect, manifestIndex);
1162
+ const selectList = aggParts.length > 0 ? `${selectBy}, ${aggParts.join(", ")}` : selectBy;
1163
+ const groupList = byCols
1164
+ .map((col) => quoteIdentifier(col.sqlName))
1165
+ .join(", ");
1166
+ let sql = `SELECT ${selectList} FROM ${tableRef(table)}`;
1167
+ if (whereSql)
1168
+ sql += ` ${whereSql}`;
1169
+ sql += ` GROUP BY ${groupList}`;
1170
+ if (havingSql)
1171
+ sql += ` ${havingSql}`;
1172
+ if (orderSql)
1173
+ sql += ` ${orderSql}`;
1174
+ if (take !== undefined) {
1175
+ sql += ` LIMIT ${normalizeLimitOffset(take, "take")}`;
1176
+ }
1177
+ if (skip !== undefined) {
1178
+ sql += ` OFFSET ${normalizeLimitOffset(skip, "skip")}`;
1179
+ }
1180
+ return sql;
1181
+ }
1182
+ export function getCachedGroupByQuery(tableIndex, table, byKeys, selectors, whereSql, havingSql, orderSql, take, skip, manifestIndex, dialect = postgresDialect) {
1183
+ const cacheKey = `${dialect.name}|${byKeys.join(",")}|${aggregateSelectorCacheKey(selectors)}|${whereSql}|${havingSql}|${orderSql}|${take ?? ""}|${skip ?? ""}`;
1184
+ if (!tableIndex) {
1185
+ return buildGroupByQuery(table, byKeys, selectors, whereSql, havingSql, orderSql, take, skip, manifestIndex, dialect);
1186
+ }
1187
+ return getOrSetSqlCache(tableIndex.groupBySqlBySignature, cacheKey, () => buildGroupByQuery(table, byKeys, selectors, whereSql, havingSql, orderSql, take, skip, manifestIndex, dialect));
1188
+ }
1189
+ export function buildUpsertQuery(table, insertKeys, updateKeys, conflictSqlColumns, exprSets = [], manifestIndex, dialect = postgresDialect, updateOps) {
645
1190
  const insertCols = insertKeys.map((k) => {
646
1191
  const col = colByTs(table, k, manifestIndex);
647
1192
  return quoteIdentifier(col?.sqlName ?? k);
@@ -656,11 +1201,18 @@ export function buildUpsertQuery(table, insertKeys, updateKeys, conflictSqlColum
656
1201
  const conflictCols = conflictSqlColumns
657
1202
  .map((c) => quoteIdentifier(c))
658
1203
  .join(", ");
1204
+ let nextParam = insertKeys.length + 1;
659
1205
  const updateSets = updateKeys.length > 0
660
- ? updateKeys.map((k) => {
1206
+ ? updateKeys.map((k, i) => {
661
1207
  const col = colByTs(table, k, manifestIndex);
662
1208
  const sqlCol = quoteIdentifier(col?.sqlName ?? k);
663
- return `${sqlCol} = excluded.${sqlCol}`;
1209
+ const op = updateOps?.[i] ?? "set";
1210
+ if (op === "set") {
1211
+ return `${sqlCol} = excluded.${sqlCol}`;
1212
+ }
1213
+ const expr = buildSetExpression(col, nextParam, op, dialect);
1214
+ nextParam++;
1215
+ return expr;
664
1216
  })
665
1217
  : exprSets.length === 0
666
1218
  ? conflictSqlColumns.map((c) => {
@@ -728,7 +1280,7 @@ export function buildInsertQuery(table, dataKeys, manifestIndex, returning = "pk
728
1280
  return buildValuePlaceholder(col, i + 1);
729
1281
  })
730
1282
  .join(", ");
731
- let sql = `INSERT INTO ${tableRef(table)} (${cols.join(", ")}) VALUES (${placeholders})`;
1283
+ const sql = `INSERT INTO ${tableRef(table)} (${cols.join(", ")}) VALUES (${placeholders})`;
732
1284
  if (returning === "none")
733
1285
  return sql;
734
1286
  const effectiveReturning = resolveReturning(table, returning);
@@ -774,7 +1326,7 @@ export function buildInsertManyValueRows(table, dataKeys, rows, manifestIndex) {
774
1326
  }
775
1327
  return { valueRows, values };
776
1328
  }
777
- export function buildInsertManyQuery(table, dataKeys, valueRows, manifestIndex) {
1329
+ export function buildInsertManyQuery(table, dataKeys, valueRows, manifestIndex, skipDuplicates = false, dialect = postgresDialect) {
778
1330
  if (dataKeys.length === 0) {
779
1331
  throw new Error("Cannot build INSERT many query with no columns");
780
1332
  }
@@ -783,16 +1335,18 @@ export function buildInsertManyQuery(table, dataKeys, valueRows, manifestIndex)
783
1335
  return quoteIdentifier(col?.sqlName ?? k);
784
1336
  });
785
1337
  const selectCols = buildSelectColumns(table, undefined, manifestIndex);
786
- return `INSERT INTO ${tableRef(table)} (${cols.join(", ")}) VALUES ${valueRows.join(", ")} RETURNING ${selectCols}`;
1338
+ const conflict = skipDuplicates ? ` ${dialect.onConflictDoNothing()}` : "";
1339
+ return `INSERT INTO ${tableRef(table)} (${cols.join(", ")}) VALUES ${valueRows.join(", ")}${conflict} RETURNING ${selectCols}`;
787
1340
  }
788
- export function buildUpdateQuery(table, dataKeys, whereSql, exprSets = [], manifestIndex, returning = "full") {
789
- const orderedKeys = [...dataKeys].sort();
790
- const paramSets = orderedKeys.map((k, i) => {
1341
+ export function buildUpdateQuery(table, dataKeys, whereSql, exprSets = [], manifestIndex, returning = "full", dialect = postgresDialect, ops) {
1342
+ const ordered = orderUpdateAssignments(dataKeys, ops);
1343
+ const paramSets = ordered.keys.map((k, i) => {
791
1344
  const col = colByTs(table, k, manifestIndex);
792
- return buildSetExpression(col, i + 1);
1345
+ const op = ordered.ops[i] ?? "set";
1346
+ return buildSetExpression(col, i + 1, op, dialect);
793
1347
  });
794
1348
  const sets = [...paramSets, ...exprSets];
795
- const whereOffset = orderedKeys.length;
1349
+ const whereOffset = ordered.keys.length;
796
1350
  let sql = `UPDATE ${tableRef(table)} SET ${sets.join(", ")}`;
797
1351
  if (whereSql) {
798
1352
  const adjustedWhere = rebaseParamRefs(whereSql, whereOffset);
@@ -832,14 +1386,15 @@ export function buildDeleteManyQuery(table, whereSql) {
832
1386
  sql += ` ${whereSql}`;
833
1387
  return sql;
834
1388
  }
835
- export function buildUpdateManyQuery(table, dataKeys, whereSql, exprSets = [], manifestIndex) {
836
- const orderedKeys = [...dataKeys].sort();
837
- const paramSets = orderedKeys.map((k, i) => {
1389
+ export function buildUpdateManyQuery(table, dataKeys, whereSql, exprSets = [], manifestIndex, dialect = postgresDialect, ops) {
1390
+ const ordered = orderUpdateAssignments(dataKeys, ops);
1391
+ const paramSets = ordered.keys.map((k, i) => {
838
1392
  const col = colByTs(table, k, manifestIndex);
839
- return buildSetExpression(col, i + 1);
1393
+ const op = ordered.ops[i] ?? "set";
1394
+ return buildSetExpression(col, i + 1, op, dialect);
840
1395
  });
841
1396
  const sets = [...paramSets, ...exprSets];
842
- const whereOffset = orderedKeys.length;
1397
+ const whereOffset = ordered.keys.length;
843
1398
  let sql = `UPDATE ${tableRef(table)} SET ${sets.join(", ")}`;
844
1399
  if (whereSql) {
845
1400
  const adjustedWhere = rebaseParamRefs(whereSql, whereOffset);
@@ -847,13 +1402,16 @@ export function buildUpdateManyQuery(table, dataKeys, whereSql, exprSets = [], m
847
1402
  }
848
1403
  return sql;
849
1404
  }
850
- export function getCachedUpdateManyQuery(tableIndex, table, dataKeys, whereSql, exprSets, manifestIndex) {
851
- const orderedKeys = [...dataKeys].sort();
852
- const cacheKey = `${sortedKeysCacheKey(orderedKeys)}|${exprSets.length}|${whereSql}`;
1405
+ export function getCachedUpdateManyQuery(tableIndex, table, dataKeys, whereSql, exprSets, manifestIndex, dialect = postgresDialect, ops) {
1406
+ const ordered = orderUpdateAssignments(dataKeys, ops);
1407
+ const opKey = ordered.keys
1408
+ .map((key, i) => `${key}:${ordered.ops[i] ?? "set"}`)
1409
+ .join(",");
1410
+ const cacheKey = `${dialect.name}|${opKey}|${exprSets.length}|${whereSql}`;
853
1411
  if (!tableIndex) {
854
- return buildUpdateManyQuery(table, orderedKeys, whereSql, exprSets, manifestIndex);
1412
+ return buildUpdateManyQuery(table, ordered.keys, whereSql, exprSets, manifestIndex, dialect, ordered.ops);
855
1413
  }
856
- return getOrSetSqlCache(tableIndex.updateManySqlByKeys, cacheKey, () => buildUpdateManyQuery(table, orderedKeys, whereSql, exprSets, manifestIndex));
1414
+ return getOrSetSqlCache(tableIndex.updateManySqlByKeys, cacheKey, () => buildUpdateManyQuery(table, ordered.keys, whereSql, exprSets, manifestIndex, dialect, ordered.ops));
857
1415
  }
858
1416
  export function getCachedFindManyQuery(tableIndex, signature, build) {
859
1417
  if (!tableIndex)
@@ -877,6 +1435,46 @@ export function dataToSqlValues(table, data, options, manifestIndex, dialect = p
877
1435
  }
878
1436
  return reorderKeyValues(keys, values);
879
1437
  }
1438
+ export function dataToUpdateAssignments(table, data, options, manifestIndex, dialect = postgresDialect) {
1439
+ const tableIndex = getTableIndex(manifestIndex, table.accessor);
1440
+ const keys = [];
1441
+ const ops = [];
1442
+ const values = [];
1443
+ for (const [key, raw] of Object.entries(data)) {
1444
+ const col = columnByTsName(tableIndex, table, key);
1445
+ if (!col)
1446
+ continue;
1447
+ if (options?.excludePrimary && col.primary)
1448
+ continue;
1449
+ if (raw === undefined)
1450
+ continue;
1451
+ const parsed = parseAtomicUpdate(col, raw);
1452
+ keys.push(key);
1453
+ ops.push(parsed.op);
1454
+ values.push(serializeColumnValue(col, parsed.value, dialect));
1455
+ }
1456
+ if (keys.length <= 1)
1457
+ return { keys, ops, values };
1458
+ const pairs = keys.map((key, index) => ({
1459
+ key,
1460
+ op: ops[index] ?? "set",
1461
+ value: values[index],
1462
+ }));
1463
+ pairs.sort((a, b) => a.key.localeCompare(b.key));
1464
+ return {
1465
+ keys: pairs.map((pair) => pair.key),
1466
+ ops: pairs.map((pair) => pair.op),
1467
+ values: pairs.map((pair) => pair.value),
1468
+ };
1469
+ }
1470
+ export function upsertAtomicValues(ops, values) {
1471
+ const extra = [];
1472
+ for (let i = 0; i < ops.length; i++) {
1473
+ if (ops[i] !== "set")
1474
+ extra.push(values[i]);
1475
+ }
1476
+ return extra;
1477
+ }
880
1478
  export function rowToTs(table, row) {
881
1479
  const result = {};
882
1480
  for (const col of table.columns) {