turbine-orm 0.49.0 → 0.50.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 (157) hide show
  1. package/README.md +122 -39
  2. package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
  3. package/dist/cjs/adapters/index.d.ts +110 -0
  4. package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
  5. package/dist/cjs/cli/config.d.ts +181 -0
  6. package/dist/cjs/cli/config.js +32 -6
  7. package/dist/cjs/cli/destructive.d.ts +38 -0
  8. package/dist/cjs/cli/index.d.ts +359 -0
  9. package/dist/cjs/cli/index.js +228 -56
  10. package/dist/cjs/cli/loader.d.ts +61 -0
  11. package/dist/cjs/cli/mcp.d.ts +42 -0
  12. package/dist/cjs/cli/migrate.d.ts +356 -0
  13. package/dist/cjs/cli/migrate.js +131 -40
  14. package/dist/cjs/cli/observe-ui.d.ts +1 -0
  15. package/dist/cjs/cli/observe-ui.js +14 -5
  16. package/dist/cjs/cli/observe.d.ts +25 -0
  17. package/dist/cjs/cli/observe.js +49 -12
  18. package/dist/cjs/cli/pii-tags.d.ts +53 -0
  19. package/dist/cjs/cli/prisma-report.d.ts +33 -0
  20. package/dist/cjs/cli/prisma-report.js +73 -0
  21. package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
  22. package/dist/cjs/cli/prisma-resolve.js +1 -0
  23. package/dist/cjs/cli/prisma-schema.d.ts +176 -0
  24. package/dist/cjs/cli/prisma-schema.js +82 -4
  25. package/dist/cjs/cli/rate-limit.d.ts +32 -0
  26. package/dist/cjs/cli/rate-limit.js +45 -0
  27. package/dist/cjs/cli/studio-demo.d.ts +43 -0
  28. package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
  29. package/dist/cjs/cli/studio.d.ts +207 -0
  30. package/dist/cjs/cli/studio.js +136 -71
  31. package/dist/cjs/cli/ui.d.ts +73 -0
  32. package/dist/cjs/cli/ui.js +51 -9
  33. package/dist/cjs/client.d.ts +837 -0
  34. package/dist/cjs/client.js +3 -0
  35. package/dist/cjs/dialect.d.ts +516 -0
  36. package/dist/cjs/dialect.js +37 -12
  37. package/dist/cjs/errors.d.ts +370 -0
  38. package/dist/cjs/generate.d.ts +137 -0
  39. package/dist/cjs/generate.js +39 -6
  40. package/dist/cjs/index-advisor.d.ts +153 -0
  41. package/dist/cjs/index-stats.d.ts +384 -0
  42. package/dist/cjs/index.d.ts +55 -0
  43. package/dist/cjs/index.js +7 -2
  44. package/dist/cjs/introspect.d.ts +269 -0
  45. package/dist/cjs/mssql.d.ts +232 -0
  46. package/dist/cjs/mssql.js +6 -0
  47. package/dist/cjs/mysql.d.ts +173 -0
  48. package/dist/cjs/mysql.js +16 -0
  49. package/dist/cjs/nested-write.d.ts +96 -0
  50. package/dist/cjs/nested-write.js +414 -24
  51. package/dist/cjs/observe.d.ts +115 -0
  52. package/dist/cjs/optional-peer-import.d.cts +72 -0
  53. package/dist/cjs/pipeline-submittable.d.ts +93 -0
  54. package/dist/cjs/pipeline.d.ts +71 -0
  55. package/dist/cjs/powdb-introspect.d.ts +84 -0
  56. package/dist/cjs/powdb.d.ts +931 -0
  57. package/dist/cjs/powdb.js +106 -21
  58. package/dist/cjs/powql.d.ts +592 -0
  59. package/dist/cjs/powql.js +42 -6
  60. package/dist/cjs/prisma-compat.d.ts +283 -0
  61. package/dist/cjs/prisma-compat.js +167 -9
  62. package/dist/cjs/query/aggregates.d.ts +92 -0
  63. package/dist/cjs/query/aggregates.js +7 -3
  64. package/dist/cjs/query/batched-loader.d.ts +193 -0
  65. package/dist/cjs/query/builder.d.ts +849 -0
  66. package/dist/cjs/query/builder.js +571 -65
  67. package/dist/cjs/query/compound-unique.d.ts +51 -0
  68. package/dist/cjs/query/deferred.d.ts +223 -0
  69. package/dist/cjs/query/filters.d.ts +201 -0
  70. package/dist/cjs/query/index.d.ts +14 -0
  71. package/dist/cjs/query/index.js +6 -1
  72. package/dist/cjs/query/relations.d.ts +609 -0
  73. package/dist/cjs/query/relations.js +693 -46
  74. package/dist/cjs/query/types.d.ts +1300 -0
  75. package/dist/cjs/query/utils.d.ts +209 -0
  76. package/dist/cjs/query/utils.js +208 -1
  77. package/dist/cjs/query/warn-registry.d.ts +68 -0
  78. package/dist/cjs/query/warn-registry.js +9 -0
  79. package/dist/cjs/query/where-compile.d.ts +139 -0
  80. package/dist/cjs/query/where.d.ts +548 -0
  81. package/dist/cjs/query/where.js +58 -22
  82. package/dist/cjs/query/writes.d.ts +172 -0
  83. package/dist/cjs/query/writes.js +105 -12
  84. package/dist/cjs/realtime.d.ts +70 -0
  85. package/dist/cjs/schema-builder.d.ts +354 -0
  86. package/dist/cjs/schema-metadata.d.ts +83 -0
  87. package/dist/cjs/schema-sql.d.ts +217 -0
  88. package/dist/cjs/schema-sql.js +23 -5
  89. package/dist/cjs/schema.d.ts +356 -0
  90. package/dist/cjs/schema.js +125 -0
  91. package/dist/cjs/seed.d.ts +15 -0
  92. package/dist/cjs/serverless.d.ts +142 -0
  93. package/dist/cjs/sqlite.d.ts +143 -0
  94. package/dist/cjs/sqlite.js +4 -0
  95. package/dist/cjs/typed-sql.d.ts +102 -0
  96. package/dist/cli/config.d.ts +18 -4
  97. package/dist/cli/config.js +31 -6
  98. package/dist/cli/index.d.ts +123 -0
  99. package/dist/cli/index.js +223 -58
  100. package/dist/cli/migrate.d.ts +59 -10
  101. package/dist/cli/migrate.js +128 -41
  102. package/dist/cli/observe-ui.d.ts +1 -1
  103. package/dist/cli/observe-ui.js +14 -5
  104. package/dist/cli/observe.d.ts +7 -1
  105. package/dist/cli/observe.js +48 -12
  106. package/dist/cli/prisma-report.d.ts +14 -0
  107. package/dist/cli/prisma-report.js +72 -0
  108. package/dist/cli/prisma-resolve.d.ts +6 -0
  109. package/dist/cli/prisma-resolve.js +1 -0
  110. package/dist/cli/prisma-schema.d.ts +62 -2
  111. package/dist/cli/prisma-schema.js +81 -4
  112. package/dist/cli/rate-limit.d.ts +32 -0
  113. package/dist/cli/rate-limit.js +40 -0
  114. package/dist/cli/studio.d.ts +5 -5
  115. package/dist/cli/studio.js +135 -70
  116. package/dist/cli/ui.d.ts +1 -1
  117. package/dist/cli/ui.js +51 -9
  118. package/dist/client.d.ts +40 -0
  119. package/dist/client.js +3 -0
  120. package/dist/dialect.d.ts +17 -1
  121. package/dist/dialect.js +37 -12
  122. package/dist/generate.js +40 -7
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/mssql.js +6 -0
  126. package/dist/mysql.js +16 -0
  127. package/dist/nested-write.d.ts +2 -0
  128. package/dist/nested-write.js +415 -25
  129. package/dist/powdb.d.ts +4 -2
  130. package/dist/powdb.js +106 -21
  131. package/dist/powql.d.ts +5 -0
  132. package/dist/powql.js +42 -6
  133. package/dist/prisma-compat.d.ts +2 -0
  134. package/dist/prisma-compat.js +166 -8
  135. package/dist/query/aggregates.js +7 -3
  136. package/dist/query/builder.d.ts +292 -21
  137. package/dist/query/builder.js +570 -64
  138. package/dist/query/deferred.d.ts +39 -0
  139. package/dist/query/index.d.ts +1 -1
  140. package/dist/query/index.js +1 -1
  141. package/dist/query/relations.d.ts +173 -5
  142. package/dist/query/relations.js +688 -47
  143. package/dist/query/types.d.ts +123 -39
  144. package/dist/query/utils.d.ts +116 -0
  145. package/dist/query/utils.js +198 -0
  146. package/dist/query/warn-registry.d.ts +9 -0
  147. package/dist/query/warn-registry.js +9 -0
  148. package/dist/query/where.d.ts +38 -1
  149. package/dist/query/where.js +58 -23
  150. package/dist/query/writes.d.ts +42 -1
  151. package/dist/query/writes.js +104 -13
  152. package/dist/schema-sql.d.ts +14 -0
  153. package/dist/schema-sql.js +23 -5
  154. package/dist/schema.d.ts +38 -0
  155. package/dist/schema.js +123 -0
  156. package/dist/sqlite.js +4 -0
  157. package/package.json +77 -28
@@ -35,6 +35,14 @@ export interface BuilderCtx {
35
35
  readonly scopedHostCache: Map<string, WhereHost>;
36
36
  readonly columnPgTypeMap: Map<string, string>;
37
37
  readonly columnArrayTypeMap: Map<string, string>;
38
+ /**
39
+ * The client's `utcTimestamps` setting, when the owning QueryInterface
40
+ * supplies it. Optional so an older ctx literal (and every test that builds
41
+ * one by hand) keeps the default. `false` opts out of BOTH the UTC read
42
+ * parsing and the symmetric UTC bind rewriting for zone-less `date` /
43
+ * `timestamp` columns (see `coerceWriteValue` in writes.ts).
44
+ */
45
+ readonly utcTimestamps?: boolean;
38
46
  readonly crossSchemaTypeColumns: Set<string>;
39
47
  /**
40
48
  * The active query's `skipGlobalFilters` opt-out. A live getter/setter over
@@ -62,7 +70,13 @@ export interface BuilderCtx {
62
70
  readonly camelDateFieldCache: Map<string, Set<string>>;
63
71
  limitOneClause(): string;
64
72
  buildPagination(limitPh: string | undefined, offsetPh: string | undefined, hasOrderBy: boolean): string;
65
- paginationRef(value: unknown, params: unknown[]): string;
73
+ paginationRef(value: unknown, params: unknown[], arg?: string): string;
74
+ /**
75
+ * Coerce + validate a LIMIT/OFFSET argument (non-negative safe integer).
76
+ * The cache-hit param-collect paths call it directly, so a warmed template
77
+ * can never bind an unvalidated NaN (which Postgres reads as "no limit").
78
+ */
79
+ paginationValue(value: unknown, arg?: string): number;
66
80
  }
67
81
  /**
68
82
  * Column-reference resolution context threaded into
@@ -76,6 +90,13 @@ interface ColumnRefContext {
76
90
  meta: TableMetadata;
77
91
  table: string;
78
92
  prefix: string;
93
+ /**
94
+ * The operator's own RAW (unquoted) column name. The `column` argument the
95
+ * operator builders receive is already quoted on the build side and raw on
96
+ * the collect side, so the temporal bind rewrite resolves the column's type
97
+ * from here instead — the one value both sides pass identically.
98
+ */
99
+ rawColumn: string;
79
100
  }
80
101
  /**
81
102
  * A table-scoped WHERE compilation context for a sub-where that is NOT the
@@ -310,6 +331,22 @@ export declare function buildSubWhereForRelation(qi: BuilderCtx, targetTable: st
310
331
  * (relation targets, not just `qi.table`).
311
332
  */
312
333
  export declare function pgTypeForColumn(_qi: BuilderCtx, meta: TableMetadata, column: string): string;
334
+ /**
335
+ * Rewrite a WHERE operand bound against `column` the same way the write path
336
+ * rewrites a `data` value ({@link coerceTemporalValue}): a JS `Date` on a
337
+ * `time` / `timetz` column becomes a time-of-day literal (Postgres otherwise
338
+ * answers `22007 invalid input syntax for type time` for the ISO timestamp the
339
+ * driver would send), and on a zone-less `date` / `timestamp` column it becomes
340
+ * the UTC-component literal, so a predicate matches the value a write of the
341
+ * same `Date` stored.
342
+ *
343
+ * This is a VALUE transform only — it never changes the emitted SQL — so the
344
+ * SQL-template cache is unaffected, and it is applied on the cache-hit
345
+ * param-collect path as well as the build path.
346
+ *
347
+ * `timestamptz` and every non-temporal column are returned by identity.
348
+ */
349
+ export declare function coerceWhereOperand(qi: BuilderCtx, meta: TableMetadata, column: string, value: unknown): unknown;
313
350
  /**
314
351
  * The Postgres enum type name for a column, when the schema knows one.
315
352
  *
@@ -13,7 +13,7 @@
13
13
  import { UnsupportedFeatureError, ValidationError } from '../errors.js';
14
14
  import { camelToSnake, normalizeKeyColumns } from '../schema.js';
15
15
  import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_RANGE_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
16
- import { escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
16
+ import { coerceTemporalValue, escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
17
17
  import { classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
18
18
  /**
19
19
  * Produce a value-invariant fingerprint of a where clause.
@@ -158,6 +158,7 @@ export function collectScalarParams(qi, key, value, params) {
158
158
  meta: qi.tableMeta,
159
159
  table: qi.table,
160
160
  prefix: '',
161
+ rawColumn,
161
162
  });
162
163
  return;
163
164
  default:
@@ -165,7 +166,7 @@ export function collectScalarParams(qi, key, value, params) {
165
166
  // the build path, so a cache hit can never silently bind a
166
167
  // misspelled-operator object.
167
168
  assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
168
- params.push(value);
169
+ params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
169
170
  return;
170
171
  }
171
172
  }
@@ -228,24 +229,26 @@ export function collectOperatorParams(qi, column, op, params, refCtx) {
228
229
  resolveColumnRef(qi, v, refCtx, op.mode);
229
230
  return true;
230
231
  };
232
+ // Mirrors buildOperatorClauses' temporal bind rewrite exactly.
233
+ const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
231
234
  if (op.equals !== undefined && op.equals !== null && !skipRef(op.equals)) {
232
235
  assertBindableEqualsOperand(op.equals, `"${column}"`);
233
- params.push(op.equals);
236
+ params.push(cv(op.equals));
234
237
  }
235
238
  if (op.gt !== undefined && !skipRef(op.gt))
236
- params.push(op.gt);
239
+ params.push(cv(op.gt));
237
240
  if (op.gte !== undefined && !skipRef(op.gte))
238
- params.push(op.gte);
241
+ params.push(cv(op.gte));
239
242
  if (op.lt !== undefined && !skipRef(op.lt))
240
- params.push(op.lt);
243
+ params.push(cv(op.lt));
241
244
  if (op.lte !== undefined && !skipRef(op.lte))
242
- params.push(op.lte);
245
+ params.push(cv(op.lte));
243
246
  if (op.not !== undefined && op.not !== null && !skipRef(op.not))
244
- params.push(op.not);
247
+ params.push(cv(op.not));
245
248
  if (op.in !== undefined)
246
- params.push(qi.inParam(op.in));
249
+ params.push(qi.inParam(cv(op.in)));
247
250
  if (op.notIn !== undefined)
248
- params.push(qi.inParam(op.notIn));
251
+ params.push(qi.inParam(cv(op.notIn)));
249
252
  if (op.contains !== undefined)
250
253
  params.push(`%${escapeLike(op.contains)}%`);
251
254
  if (op.startsWith !== undefined)
@@ -555,6 +558,7 @@ export function buildScalarClause(qi, key, value, params, andClauses) {
555
558
  meta: qi.tableMeta,
556
559
  table: qi.table,
557
560
  prefix: '',
561
+ rawColumn,
558
562
  }));
559
563
  return;
560
564
  default:
@@ -562,7 +566,7 @@ export function buildScalarClause(qi, key, value, params, andClauses) {
562
566
  // is almost always a misspelled operator (`startWith` for `startsWith`);
563
567
  // the guard also runs on the cache-hit param-collect path.
564
568
  assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
565
- params.push(value);
569
+ params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
566
570
  andClauses.push(`${column} = ${qi.p(params.length)}`);
567
571
  return;
568
572
  }
@@ -704,11 +708,12 @@ export function buildScopedScalarClause(qi, scope, field, value, params, clauses
704
708
  meta,
705
709
  table: scope.table,
706
710
  prefix: scope.qualifier,
711
+ rawColumn: col,
707
712
  }));
708
713
  return;
709
714
  }
710
715
  assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
711
- params.push(value);
716
+ params.push(coerceWhereOperand(qi, meta, col, value));
712
717
  clauses.push(`${qCol} = ${qi.p(params.length)}`);
713
718
  }
714
719
  /**
@@ -760,11 +765,11 @@ export function collectScopedScalarParams(qi, scope, field, value, params) {
760
765
  }
761
766
  }
762
767
  if (isWhereOperator(value)) {
763
- collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '' });
768
+ collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '', rawColumn: col });
764
769
  return;
765
770
  }
766
771
  assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
767
- params.push(value);
772
+ params.push(coerceWhereOperand(qi, meta, col, value));
768
773
  }
769
774
  /**
770
775
  * Value-invariant fingerprint of a scoped sub-where. Same canonical
@@ -944,6 +949,30 @@ export function buildSubWhereForRelation(qi, targetTable, subWhere, params) {
944
949
  export function pgTypeForColumn(_qi, meta, column) {
945
950
  return meta.dialectTypes?.[column] ?? meta.pgTypes?.[column] ?? 'text';
946
951
  }
952
+ /**
953
+ * Rewrite a WHERE operand bound against `column` the same way the write path
954
+ * rewrites a `data` value ({@link coerceTemporalValue}): a JS `Date` on a
955
+ * `time` / `timetz` column becomes a time-of-day literal (Postgres otherwise
956
+ * answers `22007 invalid input syntax for type time` for the ISO timestamp the
957
+ * driver would send), and on a zone-less `date` / `timestamp` column it becomes
958
+ * the UTC-component literal, so a predicate matches the value a write of the
959
+ * same `Date` stored.
960
+ *
961
+ * This is a VALUE transform only — it never changes the emitted SQL — so the
962
+ * SQL-template cache is unaffected, and it is applied on the cache-hit
963
+ * param-collect path as well as the build path.
964
+ *
965
+ * `timestamptz` and every non-temporal column are returned by identity.
966
+ */
967
+ export function coerceWhereOperand(qi, meta, column, value) {
968
+ if (!(value instanceof Date) && !Array.isArray(value))
969
+ return value;
970
+ return coerceTemporalValue(pgTypeForColumn(qi, meta, column), value,
971
+ // Same PostgreSQL + `utcTimestamps` gate as the write path (see
972
+ // `utcDateTimeWrites` in writes.ts): only the read/write-symmetric engine
973
+ // gets the zone-less rewrite. Time-of-day always rewrites.
974
+ qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false);
975
+ }
947
976
  /**
948
977
  * The Postgres enum type name for a column, when the schema knows one.
949
978
  *
@@ -1081,6 +1110,9 @@ export function columnRefSql(qi, ref, ctx, mode) {
1081
1110
  */
1082
1111
  export function buildOperatorClauses(qi, column, op, params, refCtx) {
1083
1112
  const clauses = [];
1113
+ // Temporal bind rewrite, identical to `collectOperatorParams`. Value-only, so
1114
+ // the emitted SQL (and therefore the template cache) is untouched.
1115
+ const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
1084
1116
  if (op.equals !== undefined) {
1085
1117
  if (op.equals === null) {
1086
1118
  clauses.push(`${column} IS NULL`);
@@ -1090,7 +1122,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
1090
1122
  }
1091
1123
  else {
1092
1124
  assertBindableEqualsOperand(op.equals, column);
1093
- params.push(op.equals);
1125
+ params.push(cv(op.equals));
1094
1126
  clauses.push(`${column} = ${qi.p(params.length)}`);
1095
1127
  }
1096
1128
  }
@@ -1099,7 +1131,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
1099
1131
  clauses.push(`${column} > ${columnRefSql(qi, op.gt, refCtx, op.mode)}`);
1100
1132
  }
1101
1133
  else {
1102
- params.push(op.gt);
1134
+ params.push(cv(op.gt));
1103
1135
  clauses.push(`${column} > ${qi.p(params.length)}`);
1104
1136
  }
1105
1137
  }
@@ -1108,7 +1140,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
1108
1140
  clauses.push(`${column} >= ${columnRefSql(qi, op.gte, refCtx, op.mode)}`);
1109
1141
  }
1110
1142
  else {
1111
- params.push(op.gte);
1143
+ params.push(cv(op.gte));
1112
1144
  clauses.push(`${column} >= ${qi.p(params.length)}`);
1113
1145
  }
1114
1146
  }
@@ -1117,7 +1149,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
1117
1149
  clauses.push(`${column} < ${columnRefSql(qi, op.lt, refCtx, op.mode)}`);
1118
1150
  }
1119
1151
  else {
1120
- params.push(op.lt);
1152
+ params.push(cv(op.lt));
1121
1153
  clauses.push(`${column} < ${qi.p(params.length)}`);
1122
1154
  }
1123
1155
  }
@@ -1126,7 +1158,7 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
1126
1158
  clauses.push(`${column} <= ${columnRefSql(qi, op.lte, refCtx, op.mode)}`);
1127
1159
  }
1128
1160
  else {
1129
- params.push(op.lte);
1161
+ params.push(cv(op.lte));
1130
1162
  clauses.push(`${column} <= ${qi.p(params.length)}`);
1131
1163
  }
1132
1164
  }
@@ -1138,16 +1170,16 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
1138
1170
  clauses.push(`${column} != ${columnRefSql(qi, op.not, refCtx, op.mode)}`);
1139
1171
  }
1140
1172
  else {
1141
- params.push(op.not);
1173
+ params.push(cv(op.not));
1142
1174
  clauses.push(`${column} != ${qi.p(params.length)}`);
1143
1175
  }
1144
1176
  }
1145
1177
  if (op.in !== undefined) {
1146
- params.push(qi.inParam(op.in));
1178
+ params.push(qi.inParam(cv(op.in)));
1147
1179
  clauses.push(qi.inClause(column, qi.p(params.length), false));
1148
1180
  }
1149
1181
  if (op.notIn !== undefined) {
1150
- params.push(qi.inParam(op.notIn));
1182
+ params.push(qi.inParam(cv(op.notIn)));
1151
1183
  clauses.push(qi.inClause(column, qi.p(params.length), true));
1152
1184
  }
1153
1185
  const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
@@ -1207,7 +1239,10 @@ export function vectorOperator(qi, field, rawColumn, metric) {
1207
1239
  throw new ValidationError(`[turbine] Column "${field}" on table "${qi.table}" is not a vector column ` +
1208
1240
  `(actual type: ${colType}); cannot apply a vector distance operation.`);
1209
1241
  }
1210
- const op = VECTOR_METRIC_OPERATORS[metric];
1242
+ // ownLookup, not a bare index: an inherited Object.prototype member
1243
+ // ("constructor", "toString", …) would otherwise resolve to a truthy builtin
1244
+ // and be spliced into the ORDER BY / WHERE clause as its source text.
1245
+ const op = ownLookup(VECTOR_METRIC_OPERATORS, metric);
1211
1246
  if (!op) {
1212
1247
  throw new ValidationError(`[turbine] Unknown vector metric "${metric}" for column "${field}". ` +
1213
1248
  `Valid metrics: ${Object.keys(VECTOR_METRIC_OPERATORS).join(', ')}.`);
@@ -15,6 +15,29 @@ import type { TableMetadata } from '../schema.js';
15
15
  import type { DeferredQuery } from './deferred.js';
16
16
  import type { CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, UpdateArgs, UpdateManyArgs, UpsertArgs } from './types.js';
17
17
  import type { BuilderCtx } from './where.js';
18
+ /**
19
+ * Normalize one `data` value before it is bound as a write param.
20
+ *
21
+ * Two value shapes need rewriting, both a JS `Date` on a temporal column:
22
+ *
23
+ * 1. A time-of-day column (`time` / `timetz`) — the driver serializes a `Date`
24
+ * as a full ISO timestamp with the process offset and Postgres answers
25
+ * `22007 invalid input syntax for type time`. Rewritten on every engine.
26
+ * 2. A zone-less `date` / `timestamp` column on PostgreSQL — the driver's
27
+ * local-offset serialization stores the PROCESS's calendar fields, so in a
28
+ * non-UTC process the stored value is wrong and (because the read path
29
+ * interprets an offset-less value as UTC) does not round-trip. Rewritten to
30
+ * the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
31
+ * in query/utils.ts.
32
+ *
33
+ * `timestamptz` is deliberately untouched: it stores a real instant and the
34
+ * driver's local-offset string is already correct for it.
35
+ *
36
+ * Every other value, including a string that is already a valid literal, is
37
+ * returned by identity, so this is a byte-for-byte no-op for every non-temporal
38
+ * column.
39
+ */
40
+ export declare function coerceWriteValue(qi: BuilderCtx, key: string, value: unknown): unknown;
18
41
  /**
19
42
  * Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
20
43
  * by a write's `where` clause. Used by the `'reselect'` result strategy to
@@ -120,6 +143,24 @@ export declare function assertNoGeneratedColumns(qi: BuilderCtx, data: Record<st
120
143
  * clause numbering continues correctly afterward.
121
144
  */
122
145
  export declare function buildSetClause(qi: BuilderCtx, key: string, value: unknown, params: unknown[]): string;
146
+ /**
147
+ * Reject a `data` value that reached {@link buildSetClause}'s plain-value
148
+ * fallthrough as a plain object with exactly ONE key that is not a known
149
+ * atomic operator. On a scalar column that shape can only be a misspelled
150
+ * operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
151
+ * JSON text `{"bogus":"x"}` into the column and silently corrupts it.
152
+ *
153
+ * Deliberately narrow. Skipped for:
154
+ * - json / jsonb columns, which legitimately take arbitrary objects;
155
+ * - class instances (Buffer for bytea, Decimal wrappers, …), arrays and Dates,
156
+ * which {@link isUnmatchedPlainObject} already excludes;
157
+ * - multi-key objects, which are never operator-shaped (a real operator object
158
+ * has exactly one key) and are far more likely a deliberate payload.
159
+ *
160
+ * Shared by the SQL-build path and the cache-hit param-collect path, so a
161
+ * warmed template can never skip the check.
162
+ */
163
+ export declare function assertBindableSetValue(qi: BuilderCtx, key: string, value: unknown): void;
123
164
  /**
124
165
  * Fingerprint SET clauses for update/updateMany.
125
166
  * Captures key names + operator types (set/increment/etc) but not values.
@@ -128,4 +169,4 @@ export declare function fingerprintSet(_qi: BuilderCtx, data: Record<string, unk
128
169
  /**
129
170
  * Collect SET params for update/updateMany. Mirrors buildSetClause param order.
130
171
  */
131
- export declare function collectSetParams(_qi: BuilderCtx, data: Record<string, unknown>, params: unknown[]): void;
172
+ export declare function collectSetParams(qi: BuilderCtx, data: Record<string, unknown>, params: unknown[]): void;
@@ -13,8 +13,58 @@
13
13
  import { NotFoundError, OptimisticLockError, ValidationError } from '../errors.js';
14
14
  import { camelToSnake, snakeToCamel } from '../schema.js';
15
15
  import { expandCompoundUniqueWhere } from './compound-unique.js';
16
- import { UPDATE_OPERATOR_KEYS } from './filters.js';
16
+ import { isUnmatchedPlainObject, UPDATE_OPERATOR_KEYS } from './filters.js';
17
+ import { coerceTemporalValue, ownLookup } from './utils.js';
17
18
  import * as whereMod from './where.js';
19
+ /**
20
+ * Normalize one `data` value before it is bound as a write param.
21
+ *
22
+ * Two value shapes need rewriting, both a JS `Date` on a temporal column:
23
+ *
24
+ * 1. A time-of-day column (`time` / `timetz`) — the driver serializes a `Date`
25
+ * as a full ISO timestamp with the process offset and Postgres answers
26
+ * `22007 invalid input syntax for type time`. Rewritten on every engine.
27
+ * 2. A zone-less `date` / `timestamp` column on PostgreSQL — the driver's
28
+ * local-offset serialization stores the PROCESS's calendar fields, so in a
29
+ * non-UTC process the stored value is wrong and (because the read path
30
+ * interprets an offset-less value as UTC) does not round-trip. Rewritten to
31
+ * the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
32
+ * in query/utils.ts.
33
+ *
34
+ * `timestamptz` is deliberately untouched: it stores a real instant and the
35
+ * driver's local-offset string is already correct for it.
36
+ *
37
+ * Every other value, including a string that is already a valid literal, is
38
+ * returned by identity, so this is a byte-for-byte no-op for every non-temporal
39
+ * column.
40
+ */
41
+ export function coerceWriteValue(qi, key, value) {
42
+ // Cheap shape check first: the common path costs one check and no lookup.
43
+ if (!(value instanceof Date) && !Array.isArray(value))
44
+ return value;
45
+ // Non-throwing column resolution: this runs on the cache-HIT param-collect
46
+ // path too, where an unknown key must not turn into a different error than
47
+ // the build path already raises.
48
+ const column = ownLookup(qi.tableMeta.columnMap, key);
49
+ if (!column)
50
+ return value;
51
+ // Metadata generated by an older Turbine still carries per-column types
52
+ // (`pgTypes` has always been part of TableMetadata), and a column missing
53
+ // from the map resolves to undefined → no coercion, today's behavior.
54
+ return coerceTemporalValue(qi.columnPgTypeMap.get(column), value, utcDateTimeWrites(qi));
55
+ }
56
+ /**
57
+ * Whether the zone-less `date` / `timestamp` UTC rewrite applies.
58
+ *
59
+ * PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
60
+ * `datetime2` are converted or bound by their own drivers, and MySQL in
61
+ * particular reads a zone-less literal in the SESSION time zone, so a UTC
62
+ * literal would be misread there. `utcTimestamps: false` opts out — it is the
63
+ * same switch that turns off the UTC READ parsing, so the two stay symmetric.
64
+ */
65
+ function utcDateTimeWrites(qi) {
66
+ return qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false;
67
+ }
18
68
  /**
19
69
  * Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
20
70
  * by a write's `where` clause. Used by the `'reselect'` result strategy to
@@ -32,7 +82,7 @@ export function buildCreate(qi, args) {
32
82
  assertNoGeneratedColumns(qi, args.data, 'create');
33
83
  const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
34
84
  const columns = entries.map(([k]) => qi.toSqlColumn(k));
35
- const params = entries.map(([, v]) => v);
85
+ const params = entries.map(([k, v]) => coerceWriteValue(qi, k, v));
36
86
  // Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
37
87
  const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
38
88
  const sql = qi.dialect.buildInsertStatement({
@@ -78,7 +128,9 @@ export function makeCreateReselect(qi, insertSql, insertParams, data) {
78
128
  let idx = 1;
79
129
  for (const pk of qi.tableMeta.primaryKey) {
80
130
  const field = qi.tableMeta.reverseColumnMap[pk] ?? snakeToCamel(pk);
81
- selParams.push(data[field] ?? data[pk] ?? insertId);
131
+ // Same rewrite the INSERT's own params got: a temporal PK re-fetched with
132
+ // a raw Date would not match the literal that was just written.
133
+ selParams.push(coerceWriteValue(qi, field, data[field] ?? data[pk] ?? insertId));
82
134
  conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
83
135
  }
84
136
  const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
@@ -103,7 +155,7 @@ export function buildCreateMany(qi, args) {
103
155
  const columns = keys.map((k) => qi.toColumn(k));
104
156
  const rowValues = args.data.map((row) => {
105
157
  const record = row;
106
- return keys.map((key) => record[key]);
158
+ return keys.map((key) => coerceWriteValue(qi, key, record[key]));
107
159
  });
108
160
  // Use actual Postgres types for array casts in the default PostgreSQL dialect.
109
161
  // Enum columns cast to `"EnumName"[]` — the generic text[] fallback would
@@ -303,7 +355,7 @@ export function buildUpsert(qi, args) {
303
355
  // Build the INSERT part from create data
304
356
  const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
305
357
  const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
306
- const createParams = createEntries.map(([, v]) => v);
358
+ const createParams = createEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
307
359
  // Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
308
360
  const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
309
361
  // The conflict target comes from `where` keys — must be unique/PK columns
@@ -317,7 +369,7 @@ export function buildUpsert(qi, args) {
317
369
  paramIdx++;
318
370
  return clause;
319
371
  });
320
- const updateParams = updateEntries.map(([, v]) => v);
372
+ const updateParams = updateEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
321
373
  const params = [...createParams, ...updateParams];
322
374
  // Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
323
375
  // upsert never resurrects a soft-deleted row or writes across tenants. Only
@@ -557,7 +609,7 @@ export function buildSetClause(qi, key, value, params) {
557
609
  const op = keys[0];
558
610
  const opValue = v[op];
559
611
  if (op === 'set') {
560
- params.push(opValue);
612
+ params.push(coerceWriteValue(qi, key, opValue));
561
613
  return `${col} = ${qi.p(params.length)}${cast}`;
562
614
  }
563
615
  // Arithmetic operators: must be finite numbers
@@ -582,12 +634,44 @@ export function buildSetClause(qi, key, value, params) {
582
634
  }
583
635
  }
584
636
  // Fall through: multi-key objects or non-operator single-key objects
585
- // are treated as plain values (e.g., JSONB column payloads).
637
+ // are treated as plain values (e.g., JSONB column payloads), unless the
638
+ // shape can only be a misspelled atomic operator (see below).
639
+ assertBindableSetValue(qi, key, value);
586
640
  }
587
641
  // Plain value (including null, Date, Buffer, arrays, JSON objects)
588
- params.push(value);
642
+ params.push(coerceWriteValue(qi, key, value));
589
643
  return `${col} = ${qi.p(params.length)}${cast}`;
590
644
  }
645
+ /**
646
+ * Reject a `data` value that reached {@link buildSetClause}'s plain-value
647
+ * fallthrough as a plain object with exactly ONE key that is not a known
648
+ * atomic operator. On a scalar column that shape can only be a misspelled
649
+ * operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
650
+ * JSON text `{"bogus":"x"}` into the column and silently corrupts it.
651
+ *
652
+ * Deliberately narrow. Skipped for:
653
+ * - json / jsonb columns, which legitimately take arbitrary objects;
654
+ * - class instances (Buffer for bytea, Decimal wrappers, …), arrays and Dates,
655
+ * which {@link isUnmatchedPlainObject} already excludes;
656
+ * - multi-key objects, which are never operator-shaped (a real operator object
657
+ * has exactly one key) and are far more likely a deliberate payload.
658
+ *
659
+ * Shared by the SQL-build path and the cache-hit param-collect path, so a
660
+ * warmed template can never skip the check.
661
+ */
662
+ export function assertBindableSetValue(qi, key, value) {
663
+ if (!isUnmatchedPlainObject(value))
664
+ return;
665
+ const keys = Object.keys(value);
666
+ if (keys.length !== 1 || UPDATE_OPERATOR_KEYS.has(keys[0]))
667
+ return;
668
+ const column = qi.toColumn(key);
669
+ if (whereMod.isJsonColumnType(qi, whereMod.getColumnPgType(qi, column)))
670
+ return;
671
+ throw new ValidationError(`[turbine] Unknown update operator "${keys[0]}" on "${qi.table}.${key}". ` +
672
+ `Supported: ${[...UPDATE_OPERATOR_KEYS].join(', ')}. ` +
673
+ 'A plain object is only written as a value on a json/jsonb column.');
674
+ }
591
675
  /**
592
676
  * Fingerprint SET clauses for update/updateMany.
593
677
  * Captures key names + operator types (set/increment/etc) but not values.
@@ -614,9 +698,9 @@ export function fingerprintSet(_qi, data) {
614
698
  /**
615
699
  * Collect SET params for update/updateMany. Mirrors buildSetClause param order.
616
700
  */
617
- export function collectSetParams(_qi, data, params) {
701
+ export function collectSetParams(qi, data, params) {
618
702
  const entries = Object.entries(data).filter(([, v]) => v !== undefined);
619
- for (const [, v] of entries) {
703
+ for (const [k, v] of entries) {
620
704
  if (v !== null &&
621
705
  typeof v === 'object' &&
622
706
  !Array.isArray(v) &&
@@ -625,10 +709,17 @@ export function collectSetParams(_qi, data, params) {
625
709
  const obj = v;
626
710
  const keys = Object.keys(obj);
627
711
  if (keys.length === 1 && UPDATE_OPERATOR_KEYS.has(keys[0])) {
628
- params.push(obj[keys[0]]);
712
+ // `set` is the only operator that binds a user value of arbitrary type;
713
+ // the arithmetic operators are finite numbers, which coerceWriteValue
714
+ // returns untouched. Mirrors buildSetClause.
715
+ params.push(coerceWriteValue(qi, k, obj[keys[0]]));
629
716
  continue;
630
717
  }
718
+ // Mirror the build path's misspelled-operator guard: the SET fingerprint
719
+ // is `key:eq` for both a plain value and a bogus operator object, so a
720
+ // cache HIT would otherwise bind the object and skip the check.
721
+ assertBindableSetValue(qi, k, v);
631
722
  }
632
- params.push(v);
723
+ params.push(coerceWriteValue(qi, k, v));
633
724
  }
634
725
  }
@@ -126,7 +126,21 @@ export declare function diffEnumValues(enumName: string, schemaLabels: readonly
126
126
  };
127
127
  /** A check constraint as declared or read from the DB. */
128
128
  export interface CheckSpec {
129
+ /** Constraint name. Always emitted through `dialect.quoteIdentifier`. */
129
130
  name: string;
131
+ /**
132
+ * RAW SQL boolean expression (e.g. `price >= 0`), emitted VERBATIM into the
133
+ * `CHECK (...)` clause.
134
+ *
135
+ * This is a deliberate escape hatch: a CHECK body is an arbitrary SQL
136
+ * expression, so it cannot be quoted, parameterized, or validated by the DDL
137
+ * builder without reimplementing the server's expression parser. The contract
138
+ * is therefore on the caller: the expression MUST come from the project's own
139
+ * schema definition (a `defineSchema` literal or an introspected constraint),
140
+ * and must NEVER be built from user input, request data, or any other
141
+ * untrusted source. Anything interpolated into it runs with the privileges of
142
+ * the migration.
143
+ */
130
144
  expression: string;
131
145
  }
132
146
  /**
@@ -57,14 +57,23 @@ function generateCreateEnumType(enumName, labels, dialect) {
57
57
  /**
58
58
  * Resolve the DDL type token for a column: an enum type name, a `vector(n)`
59
59
  * literal, or the dialect's scalar type — with a trailing `[]` for arrays.
60
+ *
61
+ * `vectorDimensions` is the one number interpolated into the type token, so it
62
+ * is validated here as a positive integer within pgvector's limit rather than
63
+ * being trusted to be numeric.
60
64
  */
61
- function resolveDdlType(config, dialect) {
65
+ function resolveDdlType(config, dialect, columnName) {
62
66
  let base;
63
67
  if (config.enumName) {
64
68
  base = dialect.quoteIdentifier(config.enumName);
65
69
  }
66
70
  else if (config.vectorDimensions != null) {
67
- base = `vector(${config.vectorDimensions})`;
71
+ const dims = config.vectorDimensions;
72
+ // pgvector caps a `vector` column at 16000 dimensions.
73
+ if (typeof dims !== 'number' || !Number.isInteger(dims) || dims < 1 || dims > 16000) {
74
+ throw new ValidationError(`[turbine] Column "${columnName}": vector dimensions must be an integer between 1 and 16000, got ${String(dims)}.`);
75
+ }
76
+ base = `vector(${dims})`;
68
77
  }
69
78
  else {
70
79
  base = dialect.buildColumnType({ type: config.type, maxLength: config.maxLength });
@@ -223,6 +232,9 @@ function generateCreateTable(table, resolveRef, dialect = postgresDialect) {
223
232
  columnDefs.push(dialect.buildPrimaryKeyConstraint(cols));
224
233
  }
225
234
  // Table-level CHECK constraints (named → CONSTRAINT "name" CHECK (...)).
235
+ // The constraint NAME is quoted; the EXPRESSION is raw SQL emitted verbatim
236
+ // by design (see the CheckSpec.expression contract) and must never be built
237
+ // from untrusted input.
226
238
  for (const chk of table.checks ?? []) {
227
239
  columnDefs.push(chk.name
228
240
  ? `CONSTRAINT ${dialect.quoteIdentifier(chk.name)} CHECK (${chk.expression})`
@@ -269,7 +281,7 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
269
281
  // Resolve the DDL type (enum name / vector(n) / scalar, plus [] for arrays).
270
282
  // Passed as a fully-formed `type` token with no maxLength so the dialect
271
283
  // doesn't re-apply VARCHAR(n) on top of it.
272
- const ddlType = resolveDdlType(config, dialect);
284
+ const ddlType = resolveDdlType(config, dialect, snakeName);
273
285
  let def = dialect.buildColumnDefinition({
274
286
  name: dialect.quoteIdentifier(snakeName),
275
287
  type: ddlType,
@@ -288,7 +300,9 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
288
300
  if (config.onUpdate)
289
301
  def += ` ON UPDATE ${referentialActionToSql(config.onUpdate)}`;
290
302
  }
291
- // Column-level CHECK constraint (raw SQL expression, user-authored).
303
+ // Column-level CHECK constraint. The expression is RAW SQL authored in the
304
+ // schema and is emitted verbatim by design (a DDL builder cannot parse or
305
+ // parameterize a boolean expression); never build it from untrusted input.
292
306
  if (config.check) {
293
307
  def += ` CHECK (${config.check})`;
294
308
  }
@@ -529,6 +543,8 @@ export function diffCheckConstraints(table, schemaChecks, dbChecks, dialect = po
529
543
  const dbByName = new Map(dbChecks.map((c) => [c.name, c]));
530
544
  const schemaByName = new Map(schemaChecks.map((c) => [c.name, c]));
531
545
  const norm = (e) => e.replace(/\s+/g, ' ').trim();
546
+ // `c.expression` is raw SQL emitted verbatim by design (see CheckSpec):
547
+ // schema-authored only, never untrusted input.
532
548
  const addStmt = (c) => `ALTER TABLE ${q(table)} ADD CONSTRAINT ${q(c.name)} CHECK (${c.expression});`;
533
549
  const dropStmt = (name) => `ALTER TABLE ${q(table)} DROP CONSTRAINT ${q(name)};`;
534
550
  for (const sc of schemaChecks) {
@@ -741,7 +757,7 @@ export async function schemaDiff(schema, connectionString) {
741
757
  if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
742
758
  // resolveDdlType handles enum names, vector(n), arrays, and VARCHAR(n) —
743
759
  // config.type alone would emit the internal ENUM/VECTOR sentinels here.
744
- const sqlType = resolveDdlType(config, dialect);
760
+ const sqlType = resolveDdlType(config, dialect, snakeName);
745
761
  const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
746
762
  const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
747
763
  const reverseSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${oldSqlType} USING ${dialect.quoteIdentifier(snakeName)}::${oldSqlType};`;
@@ -879,6 +895,8 @@ export async function schemaDiff(schema, connectionString) {
879
895
  const normExpr = (e) => e.replace(/\s+/g, ' ').trim();
880
896
  for (const sc of namedSchemaChecks) {
881
897
  const existing = dbCheckByName.get(sc.name);
898
+ // The constraint name is quoted; `sc.expression` is raw SQL emitted
899
+ // verbatim by design (see CheckSpec) and must be schema-authored.
882
900
  const addSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ADD CONSTRAINT ${dialect.quoteIdentifier(sc.name)} CHECK (${sc.expression});`;
883
901
  const dropSql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP CONSTRAINT ${dialect.quoteIdentifier(sc.name)};`;
884
902
  if (!existing) {