@vibeorm/runtime 2.0.0-alpha.3 → 2.0.0-alpha.5

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/index.js CHANGED
@@ -10,8 +10,31 @@ function toSqlExecutor(params) {
10
10
  return result.rows;
11
11
  };
12
12
  }
13
+ // packages/runtime/src/db-now.ts
14
+ import { VibeError } from "@vibeorm/schema";
15
+ function dbNow(options) {
16
+ const offset = options?.plusSeconds ?? 0;
17
+ if (typeof offset !== "number" || !Number.isFinite(offset)) {
18
+ throw new VibeError({
19
+ code: "VIBE_VALIDATION",
20
+ message: `dbNow plusSeconds must be a finite number of seconds (fractional is fine), got ${String(offset)}`,
21
+ meta: { plusSeconds: offset }
22
+ });
23
+ }
24
+ return { __vibeDbNow: true, offsetSeconds: offset };
25
+ }
26
+ function isDbNow(value) {
27
+ if (typeof value !== "object" || value === null || Array.isArray(value))
28
+ return false;
29
+ const keys = Object.keys(value);
30
+ if (keys.length !== 2 || !keys.includes("__vibeDbNow") || !keys.includes("offsetSeconds")) {
31
+ return false;
32
+ }
33
+ const candidate = value;
34
+ return candidate.__vibeDbNow === true && typeof candidate.offsetSeconds === "number" && Number.isInteger(candidate.offsetSeconds);
35
+ }
13
36
  // packages/runtime/src/model-meta.ts
14
- import { VibeError, isEnumType, validateComputedConfig } from "@vibeorm/schema";
37
+ import { VibeError as VibeError2, isEnumType, validateComputedConfig } from "@vibeorm/schema";
15
38
  function toClientName(params) {
16
39
  const { name } = params;
17
40
  if (name.length === 0)
@@ -132,7 +155,7 @@ function buildModelMeta(params) {
132
155
  for (const name of model.primaryKey.fields) {
133
156
  const field = fieldByName.get(name);
134
157
  if (field === undefined) {
135
- throw new VibeError({
158
+ throw new VibeError2({
136
159
  code: "VIBE_SCHEMA_INVALID",
137
160
  message: `model "${model.name}" declares composite primary-key field "${name}", which does not exist`,
138
161
  meta: { model: model.name, field: name }
@@ -144,26 +167,45 @@ function buildModelMeta(params) {
144
167
  primaryKey.push(...fields.filter((field) => field.isId));
145
168
  }
146
169
  const compoundUniques = new Map;
147
- const addCompound = (names) => {
148
- if (names.length < 2)
149
- return;
150
- const key = names.join("_");
151
- if (compoundUniques.has(key))
152
- return;
170
+ const singleIndexUniques = [];
171
+ const resolveFields = (names) => {
153
172
  const resolved = [];
154
173
  for (const name of names) {
155
174
  const field = fieldByName.get(name);
156
175
  if (field === undefined)
157
- return;
176
+ return null;
158
177
  resolved.push(field);
159
178
  }
160
- compoundUniques.set(key, resolved);
179
+ return resolved;
180
+ };
181
+ const addCompound = (names, nullsNotDistinct) => {
182
+ if (names.length < 2)
183
+ return;
184
+ const key = names.join("_");
185
+ if (compoundUniques.has(key))
186
+ return;
187
+ const resolved = resolveFields(names);
188
+ if (resolved === null)
189
+ return;
190
+ compoundUniques.set(key, { fields: resolved, nullsNotDistinct });
161
191
  };
162
192
  if (primaryKey.length > 1)
163
- addCompound(primaryKey.map((field) => field.name));
193
+ addCompound(primaryKey.map((field) => field.name), false);
164
194
  for (const index of model.indexes) {
165
- if (index.kind === "unique")
166
- addCompound(index.fields.map((entry) => entry.name));
195
+ if (index.kind !== "unique" || index.where !== undefined)
196
+ continue;
197
+ const names = index.fields.map((entry) => entry.name);
198
+ const nullsNotDistinct = index.nullsNotDistinct === true;
199
+ if (names.length === 1) {
200
+ const field = resolveFields(names)?.[0];
201
+ if (field === undefined || field.isId || field.isUnique)
202
+ continue;
203
+ if (singleIndexUniques.some((entry) => entry.fields[0]?.name === field.name))
204
+ continue;
205
+ singleIndexUniques.push({ fields: [field], nullsNotDistinct });
206
+ } else {
207
+ addCompound(names, nullsNotDistinct);
208
+ }
167
209
  }
168
210
  const computedSpecs = params.computed;
169
211
  const computed = computedSpecs === undefined || Object.keys(computedSpecs).length === 0 ? null : new Map(Object.entries(computedSpecs).map(([name, spec]) => [
@@ -180,6 +222,7 @@ function buildModelMeta(params) {
180
222
  primaryKey,
181
223
  uniqueFields: fields.filter((field) => field.isId || field.isUnique),
182
224
  compoundUniques,
225
+ singleIndexUniques,
183
226
  relations: model.relations,
184
227
  relationByName: new Map(model.relations.map((relation) => [relation.name, relation])),
185
228
  isView: model.isView,
@@ -210,7 +253,7 @@ function getModelMeta(params) {
210
253
  const { meta, model } = params;
211
254
  const found = meta.modelByName.get(model);
212
255
  if (found === undefined) {
213
- throw new VibeError({
256
+ throw new VibeError2({
214
257
  code: "VIBE_VALIDATION",
215
258
  message: `unknown model "${model}"`,
216
259
  meta: { model, known: [...new Set(meta.models.map((entry) => entry.name))] }
@@ -223,20 +266,20 @@ function getFieldMeta(params) {
223
266
  const found = model.fieldByName.get(field);
224
267
  if (found === undefined) {
225
268
  if (model.computed !== null && model.computed.has(field)) {
226
- throw new VibeError({
269
+ throw new VibeError2({
227
270
  code: "VIBE_VALIDATION",
228
271
  message: `"${field}" on model "${model.name}" is a computed field \u2014 computed fields appear in results (select/omit) only, not in ${context}`,
229
272
  meta: { model: model.name, field, context, computed: true }
230
273
  });
231
274
  }
232
275
  if (model.relationByName.has(field)) {
233
- throw new VibeError({
276
+ throw new VibeError2({
234
277
  code: "VIBE_VALIDATION",
235
278
  message: `"${field}" on model "${model.name}" is a relation \u2014 not valid in ${context} here (relation writes use nested-write verbs, relation reads use select/include)`,
236
279
  meta: { model: model.name, field, context }
237
280
  });
238
281
  }
239
- throw new VibeError({
282
+ throw new VibeError2({
240
283
  code: "VIBE_VALIDATION",
241
284
  message: `unknown field "${field}" on model "${model.name}" in ${context}`,
242
285
  meta: { model: model.name, field, context }
@@ -245,7 +288,7 @@ function getFieldMeta(params) {
245
288
  return found;
246
289
  }
247
290
  // packages/runtime/src/codecs.ts
248
- import { VibeError as VibeError2, isEnumType as isEnumType2 } from "@vibeorm/schema";
291
+ import { VibeError as VibeError3, isEnumType as isEnumType2 } from "@vibeorm/schema";
249
292
  import { DIALECT_CODECS, decodeIsNoop } from "@vibeorm/sql";
250
293
  import {
251
294
  DIALECT_CODECS as DIALECT_CODECS2,
@@ -341,7 +384,7 @@ var DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
341
384
  function toRealNumber(params) {
342
385
  const { field, value } = params;
343
386
  const refuse = () => {
344
- throw new VibeError2({
387
+ throw new VibeError3({
345
388
  code: "VIBE_VALIDATION",
346
389
  message: `"${String(value)}" is not a numeric value for the Decimal field "${field.name}"`,
347
390
  meta: { field: field.name, value }
@@ -556,10 +599,11 @@ function decodeAggregateValue(params) {
556
599
  return field === null ? value : decodeValue({ dialect, field, value });
557
600
  }
558
601
  // packages/runtime/src/query-builder.ts
559
- import { VibeError as VibeError4, unsupportedCapability } from "@vibeorm/schema";
602
+ import { VibeError as VibeError5, unsupportedCapability } from "@vibeorm/schema";
560
603
  import {
561
604
  SQL_DEFAULT_CELL,
562
605
  containsPattern,
606
+ dbNowMarker,
563
607
  endsWithPattern,
564
608
  escapeLikePattern,
565
609
  jsonParamCell,
@@ -567,9 +611,9 @@ import {
567
611
  } from "@vibeorm/sql";
568
612
 
569
613
  // packages/runtime/src/relation-plan.ts
570
- import { VibeError as VibeError3, implicitJoinTable } from "@vibeorm/schema";
614
+ import { VibeError as VibeError4, implicitJoinTable } from "@vibeorm/schema";
571
615
  function validationError(params) {
572
- return new VibeError3({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
616
+ return new VibeError4({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
573
617
  }
574
618
  function isPlainObject(value) {
575
619
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
@@ -593,7 +637,7 @@ function singleField(params) {
593
637
  }
594
638
  const field = model.fieldByName.get(names[0]);
595
639
  if (field === undefined) {
596
- throw new VibeError3({
640
+ throw new VibeError4({
597
641
  code: "VIBE_SCHEMA_INVALID",
598
642
  message: `relation "${relation.name}" on ${relation.model} names unknown ${what} field "${String(names[0])}"`,
599
643
  meta: { model: relation.model, relation: relation.name }
@@ -662,7 +706,7 @@ function resolveRelationLinkUncached(params) {
662
706
  }
663
707
  const reverse = findReverseRelation({ relation, target });
664
708
  if (reverse === null || reverse.fields === undefined || reverse.fields.length === 0) {
665
- throw new VibeError3({
709
+ throw new VibeError4({
666
710
  code: "VIBE_SCHEMA_INVALID",
667
711
  message: `relation "${relation.name}" on ${relation.model} has no foreign-key counterpart on ${relation.targetModel}`,
668
712
  meta: { model: relation.model, relation: relation.name, target: relation.targetModel }
@@ -959,15 +1003,15 @@ var ATOMIC_OPERATIONS = {
959
1003
  divide: "divide"
960
1004
  };
961
1005
  var ALLOWED_ARGS = {
962
- findMany: ["where", "orderBy", "take", "skip", "cursor", "distinct", "select", "include", "omit", "relationStrategy"],
963
- findFirst: ["where", "orderBy", "take", "skip", "cursor", "distinct", "select", "include", "omit", "relationStrategy"],
964
- findUnique: ["where", "select", "include", "omit", "relationStrategy"],
965
- create: ["data", "select", "include", "omit"],
1006
+ findMany: ["where", "orderBy", "take", "skip", "cursor", "distinct", "select", "include", "omit", "relationStrategy", "lock"],
1007
+ findFirst: ["where", "orderBy", "take", "skip", "cursor", "distinct", "select", "include", "omit", "relationStrategy", "lock"],
1008
+ findUnique: ["where", "select", "include", "omit", "relationStrategy", "lock"],
1009
+ create: ["data", "select", "include", "omit", "onConflict"],
966
1010
  createMany: ["data", "skipDuplicates"],
967
1011
  createManyAndReturn: ["data", "skipDuplicates", "select", "include", "omit"],
968
- update: ["where", "data", "select", "include", "omit"],
969
- updateMany: ["where", "data"],
970
- upsert: ["where", "create", "update", "select", "include", "omit"],
1012
+ update: ["where", "data", "select", "include", "omit", "skipUpdatedAt"],
1013
+ updateMany: ["where", "data", "skipUpdatedAt"],
1014
+ upsert: ["where", "create", "update", "select", "include", "omit", "onConflict"],
971
1015
  delete: ["where", "select", "include", "omit"],
972
1016
  deleteMany: ["where"],
973
1017
  count: ["where", "select", "take", "skip", "orderBy"],
@@ -983,7 +1027,7 @@ var AGG_FN_BY_KEY = {
983
1027
  _max: "max"
984
1028
  };
985
1029
  function validationError2(params) {
986
- return new VibeError4({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
1030
+ return new VibeError5({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
987
1031
  }
988
1032
  function isPlainObject2(value) {
989
1033
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Uint8Array);
@@ -1003,6 +1047,121 @@ function columnRef(params) {
1003
1047
  const { field, table } = params;
1004
1048
  return table === undefined ? { name: field.columnName } : { table, name: field.columnName };
1005
1049
  }
1050
+ function compileLock(params) {
1051
+ const { args, model, method, hasRelations } = params;
1052
+ if (args.lock === undefined)
1053
+ return false;
1054
+ if (model.isView) {
1055
+ throw validationError2({
1056
+ message: `lock on ${model.clientName}.${method}() \u2014 a view row cannot be locked; lock the underlying table's rows instead`,
1057
+ meta: { model: model.name, method }
1058
+ });
1059
+ }
1060
+ if (args.lock !== "update") {
1061
+ throw validationError2({
1062
+ message: `lock on ${model.clientName}.${method}() must be "update", got ${String(args.lock)}`,
1063
+ meta: { model: model.name, method, lock: args.lock }
1064
+ });
1065
+ }
1066
+ if (args.distinct !== undefined) {
1067
+ throw validationError2({
1068
+ message: `lock cannot combine with distinct on ${model.clientName}.${method}() \u2014 postgres rejects FOR UPDATE with DISTINCT`,
1069
+ meta: { model: model.name, method }
1070
+ });
1071
+ }
1072
+ if (hasRelations) {
1073
+ throw validationError2({
1074
+ message: `lock cannot combine with relation loading on ${model.clientName}.${method}() \u2014 lock the parent row alone, then read relations inside the same transaction`,
1075
+ meta: { model: model.name, method }
1076
+ });
1077
+ }
1078
+ return true;
1079
+ }
1080
+ function compileConflictTarget(params) {
1081
+ const { dialect, meta, model, method, target, where } = params;
1082
+ if ((target !== undefined || where !== undefined) && dialect.doNothingForm === "insertIgnore") {
1083
+ throw unsupportedCapability({
1084
+ dialect: dialect.name,
1085
+ feature: `a conflict target on ${model.clientName}.${method}()`,
1086
+ hint: "mysql's INSERT IGNORE / ON DUPLICATE KEY fire on ANY duplicate key \u2014 no target can be named; gate the call per dialect"
1087
+ });
1088
+ }
1089
+ const out = {};
1090
+ if (target !== undefined) {
1091
+ if (Array.isArray(target)) {
1092
+ if (target.length === 0) {
1093
+ throw validationError2({
1094
+ message: `onConflict target on ${model.clientName}.${method}() must name at least one field`,
1095
+ meta: { model: model.name, method }
1096
+ });
1097
+ }
1098
+ out.columns = target.map((name) => getFieldMeta({ model, field: String(name), context: "onConflict target" }).columnName);
1099
+ } else if (isPlainObject2(target) && typeof target.constraint === "string" && target.constraint.length > 0 && Object.keys(target).length === 1) {
1100
+ if (!dialect.capabilities.conflictTargetConstraint) {
1101
+ throw unsupportedCapability({
1102
+ dialect: dialect.name,
1103
+ feature: "a named-constraint conflict target (ON CONFLICT ON CONSTRAINT)",
1104
+ hint: "only postgres supports ON CONSTRAINT targets \u2014 name the indexed columns instead"
1105
+ });
1106
+ }
1107
+ out.constraint = target.constraint;
1108
+ } else {
1109
+ throw validationError2({
1110
+ message: `onConflict target on ${model.clientName}.${method}() must be a field-name array or { constraint: string }`,
1111
+ meta: { model: model.name, method }
1112
+ });
1113
+ }
1114
+ }
1115
+ if (where !== undefined) {
1116
+ if (out.columns === undefined) {
1117
+ throw validationError2({
1118
+ message: `onConflict where on ${model.clientName}.${method}() needs a column-list target \u2014 the predicate refines "(columns) WHERE \u2026"`,
1119
+ meta: { model: model.name, method }
1120
+ });
1121
+ }
1122
+ if (!dialect.capabilities.partialIndexes) {
1123
+ throw unsupportedCapability({
1124
+ dialect: dialect.name,
1125
+ feature: "a partial-index conflict target",
1126
+ hint: "this dialect has no partial indexes to target"
1127
+ });
1128
+ }
1129
+ if (typeof where !== "string" || where.trim().length === 0) {
1130
+ throw validationError2({
1131
+ message: `onConflict where on ${model.clientName}.${method}() must be the partial index's raw predicate string (the schema declaration's where: text)`,
1132
+ meta: { model: model.name, method }
1133
+ });
1134
+ }
1135
+ out.conflictWhere = where;
1136
+ }
1137
+ return out;
1138
+ }
1139
+ function compileDbNowWrite(params) {
1140
+ const { model, field, value } = params;
1141
+ if (field.scalarType !== "DateTime" || field.isList) {
1142
+ throw validationError2({
1143
+ message: `dbNow() writes the database clock into a DateTime field \u2014 ${model.name}.${field.name} is ${String(field.enumName ?? field.scalarType)}${field.isList ? "[]" : ""}`,
1144
+ meta: { model: model.name, field: field.name }
1145
+ });
1146
+ }
1147
+ if (field.isId || field.isUnique || model.primaryKey.some((candidate) => candidate.name === field.name)) {
1148
+ throw validationError2({
1149
+ message: `dbNow() on ${model.name}.${field.name} \u2014 a key field cannot take a database-computed value; the client could not re-address the written row`,
1150
+ meta: { model: model.name, field: field.name }
1151
+ });
1152
+ }
1153
+ return dbNowMarker({ offsetSeconds: value.offsetSeconds });
1154
+ }
1155
+ function compileDbNowFilter(params) {
1156
+ const { model, field, value } = params;
1157
+ if (field.scalarType !== "DateTime" || field.isList) {
1158
+ throw validationError2({
1159
+ message: `dbNow() compares against a DateTime field \u2014 ${model.name}.${field.name} is ${String(field.enumName ?? field.scalarType)}${field.isList ? "[]" : ""}`,
1160
+ meta: { model: model.name, field: field.name }
1161
+ });
1162
+ }
1163
+ return dbNowMarker({ offsetSeconds: value.offsetSeconds });
1164
+ }
1006
1165
  function requireObject(params) {
1007
1166
  const { value, what, model } = params;
1008
1167
  if (!isPlainObject2(value)) {
@@ -1319,7 +1478,7 @@ function compileOperatorFilter(params) {
1319
1478
  kind: "cmp",
1320
1479
  op: "eq",
1321
1480
  column,
1322
- value: encodeFilterValue({ dialect: dialect.name, field, value: operand })
1481
+ value: isDbNow(operand) ? compileDbNowFilter({ model, field, value: operand }) : encodeFilterValue({ dialect: dialect.name, field, value: operand })
1323
1482
  });
1324
1483
  break;
1325
1484
  }
@@ -1343,7 +1502,7 @@ function compileOperatorFilter(params) {
1343
1502
  kind: "cmp",
1344
1503
  op: "ne",
1345
1504
  column,
1346
- value: encodeFilterValue({ dialect: dialect.name, field, value: operand })
1505
+ value: isDbNow(operand) ? compileDbNowFilter({ model, field, value: operand }) : encodeFilterValue({ dialect: dialect.name, field, value: operand })
1347
1506
  });
1348
1507
  break;
1349
1508
  }
@@ -1355,6 +1514,14 @@ function compileOperatorFilter(params) {
1355
1514
  meta: { model: model.name, field: field.name, operator }
1356
1515
  });
1357
1516
  }
1517
+ for (const item of operand) {
1518
+ if (isDbNow(item)) {
1519
+ throw validationError2({
1520
+ message: `dbNow() cannot ride an "${operator}" list on ${model.name}.${field.name} \u2014 compare with lt/lte/gt/gte/equals instead`,
1521
+ meta: { model: model.name, field: field.name, operator }
1522
+ });
1523
+ }
1524
+ }
1358
1525
  conditions.push({
1359
1526
  kind: "inArray",
1360
1527
  column,
@@ -1371,7 +1538,7 @@ function compileOperatorFilter(params) {
1371
1538
  kind: "cmp",
1372
1539
  op: operator,
1373
1540
  column,
1374
- value: encodeFilterValue({ dialect: dialect.name, field, value: operand })
1541
+ value: isDbNow(operand) ? compileDbNowFilter({ model, field, value: operand }) : encodeFilterValue({ dialect: dialect.name, field, value: operand })
1375
1542
  });
1376
1543
  break;
1377
1544
  }
@@ -1427,6 +1594,9 @@ function compileFieldFilter(params) {
1427
1594
  if (isDirectValue(value)) {
1428
1595
  return { kind: "cmp", op: "eq", column, value: encodeFilterValue({ dialect: dialect.name, field, value }) };
1429
1596
  }
1597
+ if (isDbNow(value)) {
1598
+ return { kind: "cmp", op: "eq", column, value: compileDbNowFilter({ model, field, value }) };
1599
+ }
1430
1600
  if (Array.isArray(value)) {
1431
1601
  if (!field.isList) {
1432
1602
  throw validationError2({
@@ -1945,7 +2115,7 @@ function expandCompoundWhere(params) {
1945
2115
  for (const [key, value] of Object.entries(where)) {
1946
2116
  if (value === undefined)
1947
2117
  continue;
1948
- const fields = model.compoundUniques.get(key);
2118
+ const fields = model.compoundUniques.get(key)?.fields;
1949
2119
  if (fields === undefined)
1950
2120
  continue;
1951
2121
  const spec = requireObject({ value, what: `${what}.${key}`, model });
@@ -1964,33 +2134,52 @@ function expandCompoundWhere(params) {
1964
2134
  }
1965
2135
  return expanded ?? where;
1966
2136
  }
2137
+ function isAddressableMember(params) {
2138
+ return isDirectValue(params.value) || params.nullsNotDistinct && params.value === null;
2139
+ }
1967
2140
  function assertUniqueWhere(params) {
1968
2141
  const { model, where, method } = params;
1969
2142
  for (const field of model.uniqueFields) {
1970
2143
  if (isDirectValue(where[field.name]))
1971
2144
  return;
1972
2145
  }
1973
- for (const fields of model.compoundUniques.values()) {
1974
- if (fields.every((field) => isDirectValue(where[field.name])))
2146
+ for (const single of model.singleIndexUniques) {
2147
+ const field = single.fields[0];
2148
+ if (isAddressableMember({ value: where[field.name], nullsNotDistinct: single.nullsNotDistinct }))
1975
2149
  return;
1976
2150
  }
1977
- const candidates = [...model.uniqueFields.map((field) => field.name), ...model.compoundUniques.keys()];
2151
+ for (const group of model.compoundUniques.values()) {
2152
+ if (group.fields.every((field) => isAddressableMember({ value: where[field.name], nullsNotDistinct: group.nullsNotDistinct })))
2153
+ return;
2154
+ }
2155
+ const candidates = [
2156
+ ...model.uniqueFields.map((field) => field.name),
2157
+ ...model.singleIndexUniques.map((single) => single.fields[0].name),
2158
+ ...model.compoundUniques.keys()
2159
+ ];
1978
2160
  throw validationError2({
1979
2161
  message: `${model.clientName}.${method}() needs a unique where \u2014 one of: ${candidates.join(", ")}`,
1980
2162
  meta: { model: model.name, method, candidates }
1981
2163
  });
1982
2164
  }
1983
- function conflictTargetFields(params) {
2165
+ function conflictTargetGroup(params) {
1984
2166
  const { model, where } = params;
1985
2167
  for (const field of model.uniqueFields) {
1986
2168
  if (isDirectValue(where[field.name]))
1987
- return [field];
2169
+ return { fields: [field], nullsNotDistinct: false };
1988
2170
  }
1989
- for (const fields of model.compoundUniques.values()) {
1990
- if (fields.every((field) => isDirectValue(where[field.name])))
1991
- return fields;
2171
+ for (const single of model.singleIndexUniques) {
2172
+ const field = single.fields[0];
2173
+ if (isAddressableMember({ value: where[field.name], nullsNotDistinct: single.nullsNotDistinct })) {
2174
+ return single;
2175
+ }
1992
2176
  }
1993
- return model.primaryKey;
2177
+ for (const group of model.compoundUniques.values()) {
2178
+ if (group.fields.every((field) => isAddressableMember({ value: where[field.name], nullsNotDistinct: group.nullsNotDistinct }))) {
2179
+ return group;
2180
+ }
2181
+ }
2182
+ return { fields: model.primaryKey, nullsNotDistinct: false };
1994
2183
  }
1995
2184
  function sameDirectValue(a, b) {
1996
2185
  if (a === b)
@@ -2010,15 +2199,21 @@ function upsertUsesNativeStatement(params) {
2010
2199
  });
2011
2200
  assertUniqueWhere({ model, where, method: "upsert" });
2012
2201
  const create = requireObject({ value: args.create, what: "create", model });
2013
- const targets = conflictTargetFields({ model, where });
2014
- const targetNames = new Set(targets.map((field) => field.name));
2202
+ const target = conflictTargetGroup({ model, where });
2203
+ const targetNames = new Set(target.fields.map((field) => field.name));
2015
2204
  for (const [key, value] of Object.entries(where)) {
2016
2205
  if (value === undefined)
2017
2206
  continue;
2018
2207
  if (!targetNames.has(key))
2019
2208
  return false;
2020
2209
  }
2021
- return targets.every((field) => isDirectValue(create[field.name]) && sameDirectValue(create[field.name], where[field.name]));
2210
+ return target.fields.every((field) => {
2211
+ const createValue = create[field.name];
2212
+ const whereValue = where[field.name];
2213
+ if (target.nullsNotDistinct && createValue === null && whereValue === null)
2214
+ return true;
2215
+ return isDirectValue(createValue) && sameDirectValue(createValue, whereValue);
2216
+ });
2022
2217
  }
2023
2218
  var writePlanCache = new WeakMap;
2024
2219
  function getWritePlan(params) {
@@ -2074,6 +2269,17 @@ function compileCreateRow(params) {
2074
2269
  }
2075
2270
  if (entry.listRefused)
2076
2271
  assertListWritable({ dialect, model, field: entry.field });
2272
+ if (isDbNow(value)) {
2273
+ raw.set(entry.field.name, value);
2274
+ values[entry.field.columnName] = compileDbNowWrite({ model, field: entry.field, value });
2275
+ continue;
2276
+ }
2277
+ if (Array.isArray(value) && value.some((item) => isDbNow(item))) {
2278
+ throw validationError2({
2279
+ message: `dbNow() cannot be a list element (${model.name}.${entry.field.name})`,
2280
+ meta: { model: model.name, field: entry.field.name }
2281
+ });
2282
+ }
2077
2283
  raw.set(entry.field.name, value);
2078
2284
  values[entry.field.columnName] = encodeWith(entry, value);
2079
2285
  }
@@ -2145,6 +2351,16 @@ function compileUpdateSet(params) {
2145
2351
  if (entry.listRefused)
2146
2352
  assertListWritable({ dialect, model, field });
2147
2353
  touched.add(field.name);
2354
+ if (isDbNow(value)) {
2355
+ set[field.columnName] = compileDbNowWrite({ model, field, value });
2356
+ continue;
2357
+ }
2358
+ if (Array.isArray(value) && value.some((item) => isDbNow(item))) {
2359
+ throw validationError2({
2360
+ message: `dbNow() cannot be a list element (${model.name}.${field.name})`,
2361
+ meta: { model: model.name, field: field.name }
2362
+ });
2363
+ }
2148
2364
  if (isPlainObject2(value) && field.scalarType !== "Json") {
2149
2365
  const entries = Object.entries(value).filter(([, operand2]) => operand2 !== undefined);
2150
2366
  if (entries.length !== 1) {
@@ -2155,7 +2371,7 @@ function compileUpdateSet(params) {
2155
2371
  }
2156
2372
  const [operator, operand] = entries[0];
2157
2373
  if (operator === "set") {
2158
- set[field.columnName] = encodeWith(entry, operand);
2374
+ set[field.columnName] = isDbNow(operand) ? compileDbNowWrite({ model, field, value: operand }) : encodeWith(entry, operand);
2159
2375
  continue;
2160
2376
  }
2161
2377
  if (operator === "push" && field.isList) {
@@ -2174,10 +2390,12 @@ function compileUpdateSet(params) {
2174
2390
  }
2175
2391
  set[field.columnName] = encodeWith(entry, value);
2176
2392
  }
2177
- for (const entry of plan.injected) {
2178
- if (!entry.field.isUpdatedAt || touched.has(entry.field.name))
2179
- continue;
2180
- set[entry.field.columnName] = encodeWith(entry, new Date);
2393
+ if (params.skipUpdatedAt !== true) {
2394
+ for (const entry of plan.injected) {
2395
+ if (!entry.field.isUpdatedAt || touched.has(entry.field.name))
2396
+ continue;
2397
+ set[entry.field.columnName] = encodeWith(entry, new Date);
2398
+ }
2181
2399
  }
2182
2400
  if (Object.keys(set).length === 0) {
2183
2401
  throw validationError2({
@@ -2277,7 +2495,8 @@ function parseCursor(params) {
2277
2495
  }
2278
2496
  if (fields.length === 1) {
2279
2497
  const field = fields[0];
2280
- if (!field.isId && !field.isUnique) {
2498
+ const isSingleIndexUnique = model.singleIndexUniques.some((single) => single.fields[0].name === field.name);
2499
+ if (!field.isId && !field.isUnique && !isSingleIndexUnique) {
2281
2500
  throw validationError2({
2282
2501
  message: `cursor field ${model.name}.${field.name} must be @id or @unique \u2014 a non-unique cursor cannot identify a position`,
2283
2502
  meta: { model: model.name, field: field.name }
@@ -2286,7 +2505,7 @@ function parseCursor(params) {
2286
2505
  } else {
2287
2506
  const names = new Set(fields.map((field) => field.name));
2288
2507
  const matchesGroup = (group) => group.length === names.size && group.every((field) => names.has(field.name));
2289
- const matches = matchesGroup(model.primaryKey) || [...model.compoundUniques.values()].some(matchesGroup);
2508
+ const matches = matchesGroup(model.primaryKey) || [...model.compoundUniques.values()].some((group) => matchesGroup(group.fields));
2290
2509
  if (!matches) {
2291
2510
  const candidates = [
2292
2511
  model.primaryKey.map((field) => field.name).join(" + ") || "none",
@@ -3099,6 +3318,7 @@ function buildQuery(params) {
3099
3318
  switch (method) {
3100
3319
  case "findMany":
3101
3320
  case "findFirst": {
3321
+ const locked = compileLock({ args, model, method, hasRelations: relations !== null });
3102
3322
  const shape = buildFindShape({
3103
3323
  dialect,
3104
3324
  meta,
@@ -3111,12 +3331,13 @@ function buildQuery(params) {
3111
3331
  });
3112
3332
  return {
3113
3333
  ...base,
3114
- statement: shape.statement,
3334
+ statement: locked ? { ...shape.statement, lockForUpdate: true } : shape.statement,
3115
3335
  reverseRows: shape.reverseRows,
3116
3336
  resortBy: shape.resortBy
3117
3337
  };
3118
3338
  }
3119
3339
  case "findUnique": {
3340
+ const locked = compileLock({ args, model, method, hasRelations: relations !== null });
3120
3341
  const where = expandCompoundWhere({
3121
3342
  model,
3122
3343
  where: requireObject({ value: args.where, what: "where", model })
@@ -3131,7 +3352,10 @@ function buildQuery(params) {
3131
3352
  ...useLateral ? { selection: relations } : {},
3132
3353
  clientOptions: undefined
3133
3354
  });
3134
- return { ...base, statement: shape.statement };
3355
+ return {
3356
+ ...base,
3357
+ statement: locked ? { ...shape.statement, lockForUpdate: true } : shape.statement
3358
+ };
3135
3359
  }
3136
3360
  case "count": {
3137
3361
  const where = args.where === undefined ? undefined : compileWhere({ dialect, meta, model, where: requireObject({ value: args.where, what: "where", model }) });
@@ -3321,6 +3545,38 @@ function buildQuery(params) {
3321
3545
  };
3322
3546
  }
3323
3547
  case "create": {
3548
+ let conflictClause;
3549
+ if (args.onConflict !== undefined) {
3550
+ const spec = requireObject({ value: args.onConflict, what: "onConflict", model });
3551
+ for (const key of Object.keys(spec)) {
3552
+ if (key !== "action" && key !== "target" && key !== "where") {
3553
+ throw validationError2({
3554
+ message: `unknown onConflict key "${key}" on ${model.clientName}.create() \u2014 expected action/target/where`,
3555
+ meta: { model: model.name, key }
3556
+ });
3557
+ }
3558
+ }
3559
+ if (spec.action !== "nothing") {
3560
+ throw validationError2({
3561
+ message: `onConflict.action on ${model.clientName}.create() must be "nothing" (conflicting rows are skipped \u2014 to update instead, use upsert())`,
3562
+ meta: { model: model.name }
3563
+ });
3564
+ }
3565
+ const compiled = compileConflictTarget({
3566
+ dialect,
3567
+ meta,
3568
+ model,
3569
+ method,
3570
+ target: spec.target,
3571
+ where: spec.where
3572
+ });
3573
+ conflictClause = {
3574
+ doNothing: true,
3575
+ ...compiled.columns === undefined ? {} : { columns: compiled.columns },
3576
+ ...compiled.constraint === undefined ? {} : { constraint: compiled.constraint },
3577
+ ...compiled.conflictWhere === undefined ? {} : { conflictWhere: compiled.conflictWhere }
3578
+ };
3579
+ }
3324
3580
  const row = compileCreateRow({ dialect, model, data: args.data });
3325
3581
  const columns = orderedColumns({ model, values: row.values });
3326
3582
  if (columns.length === 0) {
@@ -3337,6 +3593,7 @@ function buildQuery(params) {
3337
3593
  table: { name: model.tableName },
3338
3594
  columns,
3339
3595
  rows: [columns.map((column) => row.values[column])],
3596
+ ...conflictClause === undefined ? {} : { onConflict: conflictClause },
3340
3597
  returning: returningColumns({ model, selectedFields })
3341
3598
  }
3342
3599
  };
@@ -3351,13 +3608,40 @@ function buildQuery(params) {
3351
3608
  });
3352
3609
  }
3353
3610
  const groups = compileCreateRows({ dialect, model, data: args.data });
3354
- const skipDuplicates = args.skipDuplicates === true;
3611
+ let manyConflict;
3612
+ if (args.skipDuplicates === true) {
3613
+ manyConflict = { doNothing: true };
3614
+ } else if (args.skipDuplicates !== undefined && args.skipDuplicates !== false) {
3615
+ const spec = requireObject({ value: args.skipDuplicates, what: "skipDuplicates", model });
3616
+ for (const key of Object.keys(spec)) {
3617
+ if (key !== "target" && key !== "where") {
3618
+ throw validationError2({
3619
+ message: `unknown skipDuplicates key "${key}" on ${model.clientName}.${method}() \u2014 expected target/where`,
3620
+ meta: { model: model.name, key }
3621
+ });
3622
+ }
3623
+ }
3624
+ const compiled = compileConflictTarget({
3625
+ dialect,
3626
+ meta,
3627
+ model,
3628
+ method,
3629
+ target: spec.target,
3630
+ where: spec.where
3631
+ });
3632
+ manyConflict = {
3633
+ doNothing: true,
3634
+ ...compiled.columns === undefined ? {} : { columns: compiled.columns },
3635
+ ...compiled.constraint === undefined ? {} : { constraint: compiled.constraint },
3636
+ ...compiled.conflictWhere === undefined ? {} : { conflictWhere: compiled.conflictWhere }
3637
+ };
3638
+ }
3355
3639
  const statements = groups.map((group) => ({
3356
3640
  kind: "insert",
3357
3641
  table: { name: model.tableName },
3358
3642
  columns: group.columns,
3359
3643
  rows: group.rows,
3360
- ...skipDuplicates ? { onConflict: { doNothing: true } } : {},
3644
+ ...manyConflict === undefined ? {} : { onConflict: manyConflict },
3361
3645
  ...method === "createManyAndReturn" ? { returning: returningColumns({ model, selectedFields }) } : {}
3362
3646
  }));
3363
3647
  return {
@@ -3369,6 +3653,12 @@ function buildQuery(params) {
3369
3653
  case "update":
3370
3654
  case "updateMany": {
3371
3655
  const isUnique = method === "update";
3656
+ if (args.skipUpdatedAt !== undefined && typeof args.skipUpdatedAt !== "boolean") {
3657
+ throw validationError2({
3658
+ message: `skipUpdatedAt on ${model.clientName}.${method}() must be a boolean, got ${String(args.skipUpdatedAt)}`,
3659
+ meta: { model: model.name, method }
3660
+ });
3661
+ }
3372
3662
  const raw = requireObject({ value: args.where, what: "where", model });
3373
3663
  const where = isUnique ? expandCompoundWhere({ model, where: raw }) : raw;
3374
3664
  if (isUnique)
@@ -3380,13 +3670,36 @@ function buildQuery(params) {
3380
3670
  statement: {
3381
3671
  kind: "update",
3382
3672
  table: { name: model.tableName },
3383
- set: compileUpdateSet({ dialect, model, data: args.data }),
3673
+ set: compileUpdateSet({
3674
+ dialect,
3675
+ model,
3676
+ data: args.data,
3677
+ ...args.skipUpdatedAt === true ? { skipUpdatedAt: true } : {}
3678
+ }),
3384
3679
  ...compiled === undefined ? {} : { where: compiled },
3385
3680
  ...isUnique ? { returning: returningColumns({ model, selectedFields }) } : {}
3386
3681
  }
3387
3682
  };
3388
3683
  }
3389
3684
  case "upsert": {
3685
+ let constraintTarget;
3686
+ if (args.onConflict !== undefined) {
3687
+ const spec = requireObject({ value: args.onConflict, what: "onConflict", model });
3688
+ if (typeof spec.constraint !== "string" || spec.constraint.length === 0 || Object.keys(spec).length !== 1) {
3689
+ throw validationError2({
3690
+ message: `onConflict on ${model.clientName}.upsert() must be { constraint: string } \u2014 the named unique constraint that arbitrates the conflict`,
3691
+ meta: { model: model.name }
3692
+ });
3693
+ }
3694
+ if (!dialect.capabilities.conflictTargetConstraint) {
3695
+ throw unsupportedCapability({
3696
+ dialect: dialect.name,
3697
+ feature: "a named-constraint conflict target on upsert",
3698
+ hint: "only postgres supports ON CONFLICT ON CONSTRAINT \u2014 rely on the where-derived column target instead"
3699
+ });
3700
+ }
3701
+ constraintTarget = spec.constraint;
3702
+ }
3390
3703
  const where = expandCompoundWhere({
3391
3704
  model,
3392
3705
  where: requireObject({ value: args.where, what: "where", model })
@@ -3394,10 +3707,10 @@ function buildQuery(params) {
3394
3707
  assertUniqueWhere({ model, where, method });
3395
3708
  const row = compileCreateRow({ dialect, model, data: args.create });
3396
3709
  const columns = orderedColumns({ model, values: row.values });
3397
- const targetFields = conflictTargetFields({ model, where });
3710
+ const targetFields = conflictTargetGroup({ model, where }).fields;
3398
3711
  for (const field of targetFields) {
3399
3712
  if (!(field.columnName in row.values)) {
3400
- throw new VibeError4({
3713
+ throw new VibeError5({
3401
3714
  code: "VIBE_INTERNAL",
3402
3715
  message: `upsert on ${model.name}: create data does not bind conflict-target column "${field.columnName}" \u2014 this shape must run the find-then-write fallback flow`,
3403
3716
  meta: { model: model.name, column: field.columnName }
@@ -3408,7 +3721,7 @@ function buildQuery(params) {
3408
3721
  for (const [key, value] of Object.entries(where)) {
3409
3722
  if (value === undefined || nativeTargetNames.has(key))
3410
3723
  continue;
3411
- throw new VibeError4({
3724
+ throw new VibeError5({
3412
3725
  code: "VIBE_INTERNAL",
3413
3726
  message: `upsert on ${model.name}: where field "${key}" is not part of the conflict target \u2014 guarded upserts must run the find-then-write fallback flow`,
3414
3727
  meta: { model: model.name, field: key }
@@ -3428,6 +3741,7 @@ function buildQuery(params) {
3428
3741
  rows: [columns.map((column) => row.values[column])],
3429
3742
  onConflict: {
3430
3743
  columns: targetFields.map((field) => field.columnName),
3744
+ ...constraintTarget === undefined ? {} : { constraint: constraintTarget },
3431
3745
  update,
3432
3746
  ...odkuKeyColumn === undefined ? {} : { lastInsertIdColumn: odkuKeyColumn }
3433
3747
  },
@@ -3457,10 +3771,10 @@ function buildQuery(params) {
3457
3771
  }
3458
3772
  }
3459
3773
  // packages/runtime/src/relation-loader.ts
3460
- import { VibeError as VibeError6 } from "@vibeorm/schema";
3774
+ import { VibeError as VibeError7 } from "@vibeorm/schema";
3461
3775
 
3462
3776
  // packages/runtime/src/computed.ts
3463
- import { VibeError as VibeError5 } from "@vibeorm/schema";
3777
+ import { VibeError as VibeError6 } from "@vibeorm/schema";
3464
3778
  function sameNeeds(a, b) {
3465
3779
  if (a.length !== b.length)
3466
3780
  return false;
@@ -3496,7 +3810,7 @@ function assertComputedManifest(params) {
3496
3810
  problems.push(`"${key}" is configured but missing from the generated client`);
3497
3811
  }
3498
3812
  if (problems.length > 0) {
3499
- throw new VibeError5({
3813
+ throw new VibeError6({
3500
3814
  code: "VIBE_STALE_CLIENT",
3501
3815
  message: `computed-field config changed since generate; run vibeorm generate \u2014 ${problems.join("; ")}`,
3502
3816
  meta: { problems }
@@ -3660,7 +3974,7 @@ async function loadManyToMany(params) {
3660
3974
  const link = node.link;
3661
3975
  const join = link.join;
3662
3976
  if (join === null) {
3663
- throw new VibeError6({
3977
+ throw new VibeError7({
3664
3978
  code: "VIBE_INTERNAL",
3665
3979
  message: `manyToMany relation "${node.name}" resolved without join-table wiring`
3666
3980
  });
@@ -3858,7 +4172,7 @@ async function attachLateralRelations(params) {
3858
4172
  try {
3859
4173
  parsed = JSON.parse(raw);
3860
4174
  } catch (cause) {
3861
- throw new VibeError6({
4175
+ throw new VibeError7({
3862
4176
  code: "VIBE_INTERNAL",
3863
4177
  message: `relation column "${column}" returned text that is not valid JSON`,
3864
4178
  meta: { relation: node.name },
@@ -3895,12 +4209,12 @@ async function attachLateralRelations(params) {
3895
4209
  await loadCounts({ ctx, model, rows, counts: selection.counts });
3896
4210
  }
3897
4211
  // packages/runtime/src/nested-writes.ts
3898
- import { VibeError as VibeError7 } from "@vibeorm/schema";
4212
+ import { VibeError as VibeError8 } from "@vibeorm/schema";
3899
4213
  function validationError3(params) {
3900
- return new VibeError7({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
4214
+ return new VibeError8({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
3901
4215
  }
3902
4216
  function notFound(params) {
3903
- return new VibeError7({ code: "VIBE_NOT_FOUND", message: params.message, meta: params.meta });
4217
+ return new VibeError8({ code: "VIBE_NOT_FOUND", message: params.message, meta: params.meta });
3904
4218
  }
3905
4219
  function isPlainObject3(value) {
3906
4220
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Uint8Array);
@@ -4730,7 +5044,7 @@ async function updateWithNested(params) {
4730
5044
  });
4731
5045
  }
4732
5046
  // packages/runtime/src/extensions.ts
4733
- import { VibeError as VibeError8, extensionManifestEntry } from "@vibeorm/schema";
5047
+ import { VibeError as VibeError9, extensionManifestEntry } from "@vibeorm/schema";
4734
5048
  function assertExtensionManifest(params) {
4735
5049
  const { manifest } = params;
4736
5050
  const instances = params.instances ?? [];
@@ -4753,7 +5067,7 @@ function assertExtensionManifest(params) {
4753
5067
  problems.push(`"${name}" is configured but missing from the generated client`);
4754
5068
  }
4755
5069
  if (problems.length > 0) {
4756
- throw new VibeError8({
5070
+ throw new VibeError9({
4757
5071
  code: "VIBE_STALE_CLIENT",
4758
5072
  message: `extension list changed since generate; run vibeorm generate \u2014 ${problems.join("; ")}`,
4759
5073
  meta: { manifest, problems }
@@ -4768,7 +5082,7 @@ function buildExtensionOperatorMap(params) {
4768
5082
  for (const operator of extension.sql?.whereOperators ?? []) {
4769
5083
  const existing = owner.get(operator.name);
4770
5084
  if (existing !== undefined) {
4771
- throw new VibeError8({
5085
+ throw new VibeError9({
4772
5086
  code: "VIBE_CONFIG",
4773
5087
  message: `where operator "${operator.name}" is claimed by both "${existing}" and "${extension.name}"`,
4774
5088
  meta: { operator: operator.name }
@@ -4784,7 +5098,7 @@ function buildExtensionOperatorMap(params) {
4784
5098
  options: extension.options
4785
5099
  });
4786
5100
  if ("error" in compiled) {
4787
- throw new VibeError8({
5101
+ throw new VibeError9({
4788
5102
  code: "VIBE_VALIDATION",
4789
5103
  message: `"${operator.name}" on ${model}.${field}: ${compiled.error}`,
4790
5104
  meta: { model, field, operator: operator.name, extension: extension.name }
@@ -4816,9 +5130,9 @@ function buildConnectHooks(params) {
4816
5130
  return hooks;
4817
5131
  }
4818
5132
  // packages/runtime/src/views.ts
4819
- import { VibeError as VibeError9 } from "@vibeorm/schema";
5133
+ import { VibeError as VibeError10 } from "@vibeorm/schema";
4820
5134
  function validationError4(params) {
4821
- return new VibeError9({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
5135
+ return new VibeError10({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
4822
5136
  }
4823
5137
 
4824
5138
  class ViewResult {
@@ -5170,7 +5484,7 @@ function createView(params) {
5170
5484
  for (const [clientName, mask] of resolved) {
5171
5485
  const delegate = models[clientName];
5172
5486
  if (delegate === undefined) {
5173
- throw new VibeError9({
5487
+ throw new VibeError10({
5174
5488
  code: "VIBE_INTERNAL",
5175
5489
  message: `view "${name}": no delegate for model "${mask.model.name}"`,
5176
5490
  meta: { view: name, model: mask.model.name }
@@ -5186,11 +5500,11 @@ function attachDefineView(params) {
5186
5500
  Object.defineProperty(client, "$defineView", { value, enumerable: true });
5187
5501
  }
5188
5502
  // packages/runtime/src/client.ts
5189
- import { VibeError as VibeError10, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
5503
+ import { VibeError as VibeError11, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
5190
5504
  import { ParamCollector, getSqlDialect } from "@vibeorm/sql";
5191
5505
 
5192
5506
  // packages/runtime/src/render-cache.ts
5193
- import { isListSetOperation, isSetOperation, isSqlDefaultCell, renderStatement, strictMatchPattern } from "@vibeorm/sql";
5507
+ import { isDbNowMarker, isListSetOperation, isSetOperation, isSqlDefaultCell, renderStatement, strictMatchPattern } from "@vibeorm/sql";
5194
5508
  var MAX_ENTRIES = 512;
5195
5509
 
5196
5510
  class StatementRenderCache {
@@ -5240,6 +5554,13 @@ function addSetEntry(walk, name, value) {
5240
5554
  walk.values.push(value.value);
5241
5555
  return;
5242
5556
  }
5557
+ if (isDbNowMarker(value)) {
5558
+ walk.key += value.offsetSeconds === 0 ? "=N0" : "=N1";
5559
+ addIdent(walk, name);
5560
+ if (value.offsetSeconds !== 0)
5561
+ walk.values.push(value.offsetSeconds);
5562
+ return;
5563
+ }
5243
5564
  if (isListSetOperation(value)) {
5244
5565
  walk.key += `=l${value.__listOp}`;
5245
5566
  addIdent(walk, name);
@@ -5263,6 +5584,13 @@ function addReturning(walk, returning) {
5263
5584
  function walkExpr(walk, dialect, expr) {
5264
5585
  switch (expr.kind) {
5265
5586
  case "cmp":
5587
+ if (isDbNowMarker(expr.value)) {
5588
+ walk.key += `cN${expr.op}${expr.value.offsetSeconds === 0 ? "0" : "1"}`;
5589
+ addCol(walk, expr.column);
5590
+ if (expr.value.offsetSeconds !== 0)
5591
+ walk.values.push(expr.value.offsetSeconds);
5592
+ return true;
5593
+ }
5266
5594
  walk.key += `c${expr.op}`;
5267
5595
  addCol(walk, expr.column);
5268
5596
  walk.values.push(expr.value);
@@ -5478,7 +5806,7 @@ function walkSelect(walk, dialect, statement) {
5478
5806
  walk.key += "}";
5479
5807
  return true;
5480
5808
  }
5481
- function walkInsert(walk, statement) {
5809
+ function walkInsert(walk, dialect, statement) {
5482
5810
  walk.key += "I{";
5483
5811
  addTable(walk, statement.table);
5484
5812
  walk.key += "c(";
@@ -5494,7 +5822,11 @@ function walkInsert(walk, statement) {
5494
5822
  const value = row[cell];
5495
5823
  if (isSqlDefaultCell(value))
5496
5824
  walk.key += `D${rowIndex}:${cell};`;
5497
- else
5825
+ else if (isDbNowMarker(value)) {
5826
+ walk.key += `T${rowIndex}:${cell}:${value.offsetSeconds === 0 ? "0" : "1"};`;
5827
+ if (value.offsetSeconds !== 0)
5828
+ walk.values.push(value.offsetSeconds);
5829
+ } else
5498
5830
  walk.values.push(value);
5499
5831
  }
5500
5832
  }
@@ -5502,13 +5834,25 @@ function walkInsert(walk, statement) {
5502
5834
  if (onConflict !== undefined) {
5503
5835
  if ("doNothing" in onConflict) {
5504
5836
  walk.key += "N(";
5837
+ if (onConflict.constraint !== undefined) {
5838
+ walk.key += "K";
5839
+ addIdent(walk, onConflict.constraint);
5840
+ }
5505
5841
  if (onConflict.columns !== undefined) {
5506
5842
  for (const name of onConflict.columns)
5507
5843
  addIdent(walk, name);
5508
5844
  }
5845
+ if (onConflict.conflictWhere !== undefined) {
5846
+ walk.key += "w";
5847
+ addIdent(walk, onConflict.conflictWhere);
5848
+ }
5509
5849
  walk.key += ")";
5510
5850
  } else {
5511
5851
  walk.key += "U(";
5852
+ if (onConflict.constraint !== undefined) {
5853
+ walk.key += "K";
5854
+ addIdent(walk, onConflict.constraint);
5855
+ }
5512
5856
  for (const name of onConflict.columns)
5513
5857
  addIdent(walk, name);
5514
5858
  walk.key += ":";
@@ -5556,7 +5900,7 @@ function walkStatement(walk, dialect, statement) {
5556
5900
  case "select":
5557
5901
  return walkSelect(walk, dialect, statement);
5558
5902
  case "insert":
5559
- return walkInsert(walk, statement);
5903
+ return walkInsert(walk, dialect, statement);
5560
5904
  case "update":
5561
5905
  return walkUpdate(walk, dialect, statement);
5562
5906
  case "delete":
@@ -5606,7 +5950,7 @@ function renderStatementCached(params) {
5606
5950
  // packages/runtime/src/client.ts
5607
5951
  function notFound2(params) {
5608
5952
  const { model, method } = params;
5609
- return new VibeError10({
5953
+ return new VibeError11({
5610
5954
  code: "VIBE_NOT_FOUND",
5611
5955
  message: `no ${model} record matched the where clause of ${method}()`,
5612
5956
  meta: { model, method }
@@ -5614,7 +5958,7 @@ function notFound2(params) {
5614
5958
  }
5615
5959
  function viewIsReadOnly(params) {
5616
5960
  const { model, method } = params;
5617
- return new VibeError10({
5961
+ return new VibeError11({
5618
5962
  code: "VIBE_VALIDATION",
5619
5963
  message: `${model} is a view \u2014 ${method}() is not available on views (they are read-only); write to the underlying table instead`,
5620
5964
  meta: { model, method }
@@ -5743,7 +6087,7 @@ class QueryPromise extends Promise {
5743
6087
  }
5744
6088
  claim() {
5745
6089
  if (this.started) {
5746
- throw new VibeError10({
6090
+ throw new VibeError11({
5747
6091
  code: "VIBE_VALIDATION",
5748
6092
  message: "$transaction([...]) needs operations that have not dispatched \u2014 pass delegate calls in the same tick they are created (this one was awaited or created earlier and already executed outside the transaction)"
5749
6093
  });
@@ -5790,7 +6134,7 @@ async function runAffected(params) {
5790
6134
  if (plan.batch !== undefined && plan.batch.length > 1) {
5791
6135
  const batch = plan.batch;
5792
6136
  return await engine.adapter.transaction(async (txAdapter) => {
5793
- const txEngine = { ...engine, adapter: txAdapter };
6137
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
5794
6138
  let total = 0;
5795
6139
  for (const statement of batch) {
5796
6140
  total += await runAffected({ engine: txEngine, plan: { ...plan, statement, batch: undefined } });
@@ -5820,6 +6164,13 @@ async function runAffected(params) {
5820
6164
  }
5821
6165
  function plan(params) {
5822
6166
  const { engine, model, method, args } = params;
6167
+ if (args !== undefined && args["lock"] !== undefined && engine.inTransaction !== true) {
6168
+ throw new VibeError11({
6169
+ code: "VIBE_VALIDATION",
6170
+ message: `lock on ${model}.${method}() requires a transaction \u2014 call it on the client $transaction hands its callback (outside one, the lock would release at statement end)`,
6171
+ meta: { model, method }
6172
+ });
6173
+ }
5823
6174
  return buildQuery({
5824
6175
  meta: engine.meta,
5825
6176
  dialect: engine.dialect,
@@ -5887,7 +6238,7 @@ async function insertedKeyWhere(params) {
5887
6238
  });
5888
6239
  const id = result.rows[0]?.id;
5889
6240
  if (id === undefined || id === null) {
5890
- throw new VibeError10({
6241
+ throw new VibeError11({
5891
6242
  code: "VIBE_INTERNAL",
5892
6243
  message: `could not read back the generated key for ${source.model.name}`,
5893
6244
  meta: { model: source.model.name }
@@ -5902,8 +6253,11 @@ async function runSingleMutation(params) {
5902
6253
  if (requested !== undefined && requested.length > 0 && engine.dialect.capabilities.returning) {
5903
6254
  const rows = await runRows({ engine, plan: source });
5904
6255
  const row = rows[0];
5905
- if (row === undefined)
6256
+ if (row === undefined) {
6257
+ if (params.allowZeroRows === true)
6258
+ return null;
5906
6259
  throw notFound2({ model: source.model.name, method: source.method });
6260
+ }
5907
6261
  return row;
5908
6262
  }
5909
6263
  const rendered = renderStatementCached({ dialect: engine.dialect, statement, cache: engine.renderCache });
@@ -5928,7 +6282,7 @@ async function runSingleMutation(params) {
5928
6282
  if (where === null)
5929
6283
  throw notFound2({ model: source.model.name, method: source.method });
5930
6284
  return await engine.adapter.transaction(async (txAdapter) => {
5931
- const txEngine = { ...engine, adapter: txAdapter };
6285
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
5932
6286
  const before = await refetch({ engine: txEngine, source, where, lock: true });
5933
6287
  if (before === null)
5934
6288
  throw notFound2({ model: source.model.name, method: source.method });
@@ -5942,7 +6296,7 @@ async function runSingleMutation(params) {
5942
6296
  const knownWhere = source.refetchWhere;
5943
6297
  if (source.method === "upsert") {
5944
6298
  return await engine.adapter.transaction(async (txAdapter) => {
5945
- const txEngine = { ...engine, adapter: txAdapter };
6299
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
5946
6300
  await executeWrite(txEngine);
5947
6301
  const byWhere = await refetch({ engine: txEngine, source, where: knownWhere });
5948
6302
  if (byWhere !== null)
@@ -5966,9 +6320,9 @@ async function runSingleMutation(params) {
5966
6320
  }
5967
6321
  if (source.method === "update") {
5968
6322
  return await engine.adapter.transaction(async (txAdapter) => {
5969
- const txEngine = { ...engine, adapter: txAdapter };
5970
- const affected = await executeWrite(txEngine);
5971
- if (affected === 0)
6323
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6324
+ const affected2 = await executeWrite(txEngine);
6325
+ if (affected2 === 0)
5972
6326
  throw notFound2({ model: source.model.name, method: source.method });
5973
6327
  const after2 = await refetch({ engine: txEngine, source, where: knownWhere });
5974
6328
  if (after2 === null)
@@ -5976,15 +6330,19 @@ async function runSingleMutation(params) {
5976
6330
  return after2;
5977
6331
  });
5978
6332
  }
5979
- await executeWrite(engine);
6333
+ const affected = await executeWrite(engine);
6334
+ if (affected === 0 && params.allowZeroRows === true)
6335
+ return null;
5980
6336
  const after = await refetch({ engine, source, where: knownWhere });
5981
6337
  if (after === null)
5982
6338
  throw notFound2({ model: source.model.name, method: source.method });
5983
6339
  return after;
5984
6340
  }
5985
6341
  return await engine.adapter.transaction(async (txAdapter) => {
5986
- const txEngine = { ...engine, adapter: txAdapter };
5987
- await executeWrite(txEngine);
6342
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6343
+ const affected = await executeWrite(txEngine);
6344
+ if (affected === 0 && params.allowZeroRows === true)
6345
+ return null;
5988
6346
  const where = await insertedKeyWhere({ engine: txEngine, source });
5989
6347
  const after = await refetch({ engine: txEngine, source, where });
5990
6348
  if (after === null)
@@ -5992,6 +6350,11 @@ async function runSingleMutation(params) {
5992
6350
  return after;
5993
6351
  });
5994
6352
  }
6353
+ function requireRow(params) {
6354
+ if (params.row === null)
6355
+ throw notFound2({ model: params.model, method: params.method });
6356
+ return params.row;
6357
+ }
5995
6358
  async function runUpsertFallback(params) {
5996
6359
  const { engine, model, args } = params;
5997
6360
  const passthrough = {};
@@ -6019,10 +6382,14 @@ async function runUpsertFallback(params) {
6019
6382
  args: { ...passthrough, where: args.where, data: args.update }
6020
6383
  });
6021
6384
  return await engine.adapter.transaction(async (txAdapter) => {
6022
- const txEngine = { ...engine, adapter: txAdapter };
6385
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6023
6386
  const existing = (await runRows({ engine: txEngine, plan: probePlan }))[0];
6024
6387
  const source = existing === undefined ? createSource : updateSource;
6025
- return await runSingleMutation({ engine: txEngine, source, readBeforeWrite: false });
6388
+ return requireRow({
6389
+ row: await runSingleMutation({ engine: txEngine, source, readBeforeWrite: false }),
6390
+ model: model.name,
6391
+ method: "upsert"
6392
+ });
6026
6393
  });
6027
6394
  }
6028
6395
  function relationExecutor(params) {
@@ -6079,10 +6446,14 @@ async function finalizeRow(params) {
6079
6446
  function makeRunner(params) {
6080
6447
  const { engine } = params;
6081
6448
  return {
6082
- single: async ({ model, method, args }) => await runSingleMutation({
6083
- engine,
6084
- source: plan({ engine, model, method, args }),
6085
- readBeforeWrite: method === "delete"
6449
+ single: async ({ model, method, args }) => requireRow({
6450
+ row: await runSingleMutation({
6451
+ engine,
6452
+ source: plan({ engine, model, method, args }),
6453
+ readBeforeWrite: method === "delete"
6454
+ }),
6455
+ model,
6456
+ method
6086
6457
  }),
6087
6458
  rows: async ({ model, method, args }) => await runRows({ engine, plan: plan({ engine, model, method, args }) }),
6088
6459
  affected: async ({ model, method, args }) => await runAffected({ engine, plan: plan({ engine, model, method, args }) }),
@@ -6108,7 +6479,7 @@ function makeWriteContext(params) {
6108
6479
  meta: engine.meta,
6109
6480
  dialect: engine.dialect,
6110
6481
  run: makeRunner({ engine }),
6111
- transactional: (fn) => engine.adapter.transaction((txAdapter) => fn(makeRunner({ engine: { ...engine, adapter: txAdapter } })))
6482
+ transactional: (fn) => engine.adapter.transaction((txAdapter) => fn(makeRunner({ engine: { ...engine, adapter: txAdapter, inTransaction: true } })))
6112
6483
  };
6113
6484
  }
6114
6485
  function readOnlyDelegate(params) {
@@ -6166,7 +6537,15 @@ function createDelegate(params) {
6166
6537
  };
6167
6538
  const create = async (target, args) => {
6168
6539
  const modelMeta = getModelMeta({ meta: target.meta, model });
6540
+ const skipping = args.onConflict !== undefined;
6169
6541
  if (hasNestedWrites({ model: modelMeta, data: args.data })) {
6542
+ if (skipping) {
6543
+ throw new VibeError11({
6544
+ code: "VIBE_VALIDATION",
6545
+ message: `onConflict cannot combine with nested writes on ${modelMeta.name}.create() \u2014 insert the parent row alone, then connect relations`,
6546
+ meta: { model: modelMeta.name }
6547
+ });
6548
+ }
6170
6549
  const shape = resolveResultShape({ meta: target.meta, model: modelMeta, method: "create", args });
6171
6550
  const row2 = await createWithNested({
6172
6551
  ctx: makeWriteContext({ engine: target }),
@@ -6181,7 +6560,14 @@ function createDelegate(params) {
6181
6560
  });
6182
6561
  }
6183
6562
  const built = plan({ engine: target, model, method: "create", args });
6184
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: false });
6563
+ const row = await runSingleMutation({
6564
+ engine: target,
6565
+ source: built,
6566
+ readBeforeWrite: false,
6567
+ ...skipping ? { allowZeroRows: true } : {}
6568
+ });
6569
+ if (row === null)
6570
+ return null;
6185
6571
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6186
6572
  };
6187
6573
  const update = async (target, args) => {
@@ -6202,7 +6588,11 @@ function createDelegate(params) {
6202
6588
  });
6203
6589
  }
6204
6590
  const built = plan({ engine: target, model, method: "update", args });
6205
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: false });
6591
+ const row = requireRow({
6592
+ row: await runSingleMutation({ engine: target, source: built, readBeforeWrite: false }),
6593
+ model: built.model.name,
6594
+ method: "update"
6595
+ });
6206
6596
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6207
6597
  };
6208
6598
  const createMany = async (target, args) => {
@@ -6234,13 +6624,20 @@ function createDelegate(params) {
6234
6624
  const upsert = async (target, args) => {
6235
6625
  const modelMeta = getModelMeta({ meta: target.meta, model });
6236
6626
  if (hasNestedWrites({ model: modelMeta, data: args.create }) || hasNestedWrites({ model: modelMeta, data: args.update })) {
6237
- throw new VibeError10({
6627
+ throw new VibeError11({
6238
6628
  code: "VIBE_VALIDATION",
6239
6629
  message: `nested writes are not supported inside upsert arms on ${modelMeta.name} \u2014 use create()/update() with nested data`,
6240
6630
  meta: { model: modelMeta.name }
6241
6631
  });
6242
6632
  }
6243
6633
  if (!upsertUsesNativeStatement({ model: modelMeta, args })) {
6634
+ if (args.onConflict !== undefined) {
6635
+ throw new VibeError11({
6636
+ code: "VIBE_VALIDATION",
6637
+ message: `onConflict on ${modelMeta.name}.upsert() needs the native single-statement path \u2014 this shape (a guard field beyond the conflict target, or create data not binding the where's key) runs the find-then-write fallback, which cannot honor an explicit conflict target`,
6638
+ meta: { model: modelMeta.name }
6639
+ });
6640
+ }
6244
6641
  const shape = resolveResultShape({
6245
6642
  meta: target.meta,
6246
6643
  model: modelMeta,
@@ -6256,12 +6653,20 @@ function createDelegate(params) {
6256
6653
  });
6257
6654
  }
6258
6655
  const built = plan({ engine: target, model, method: "upsert", args });
6259
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: false });
6656
+ const row = requireRow({
6657
+ row: await runSingleMutation({ engine: target, source: built, readBeforeWrite: false }),
6658
+ model: built.model.name,
6659
+ method: "upsert"
6660
+ });
6260
6661
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6261
6662
  };
6262
6663
  const deleteOne = async (target, args) => {
6263
6664
  const built = plan({ engine: target, model, method: "delete", args });
6264
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: true });
6665
+ const row = requireRow({
6666
+ row: await runSingleMutation({ engine: target, source: built, readBeforeWrite: true }),
6667
+ model: built.model.name,
6668
+ method: "delete"
6669
+ });
6265
6670
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6266
6671
  };
6267
6672
  const deleteMany = async (target, args) => {
@@ -6389,7 +6794,7 @@ function buildClient(params) {
6389
6794
  let claimError = null;
6390
6795
  for (const operation of operations) {
6391
6796
  if (!(operation instanceof QueryPromise)) {
6392
- claimError = new VibeError10({
6797
+ claimError = new VibeError11({
6393
6798
  code: "VIBE_VALIDATION",
6394
6799
  message: "$transaction([...]) takes query promises from model delegate calls \u2014 a foreign promise is already running and cannot join the transaction"
6395
6800
  });
@@ -6408,7 +6813,7 @@ function buildClient(params) {
6408
6813
  return Promise.reject(claimError);
6409
6814
  }
6410
6815
  return engine.adapter.transaction(async (txAdapter) => {
6411
- const txEngine = { ...engine, adapter: txAdapter };
6816
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6412
6817
  const results = [];
6413
6818
  let index = 0;
6414
6819
  try {
@@ -6428,7 +6833,7 @@ function buildClient(params) {
6428
6833
  $models: models,
6429
6834
  $on: (event, listener) => {
6430
6835
  if (event !== "query") {
6431
- throw new VibeError10({
6836
+ throw new VibeError11({
6432
6837
  code: "VIBE_VALIDATION",
6433
6838
  message: `unknown client event "${String(event)}" \u2014 supported: "query"`,
6434
6839
  meta: { event }
@@ -6439,7 +6844,7 @@ function buildClient(params) {
6439
6844
  engine.listeners.delete(listener);
6440
6845
  };
6441
6846
  },
6442
- $transaction: (fnOrOperations, options) => Array.isArray(fnOrOperations) ? runOperations(fnOrOperations, options) : engine.adapter.transaction((txAdapter) => fnOrOperations(buildClient({ engine: { ...engine, adapter: txAdapter } })), options),
6847
+ $transaction: (fnOrOperations, options) => Array.isArray(fnOrOperations) ? runOperations(fnOrOperations, options) : engine.adapter.transaction((txAdapter) => fnOrOperations(buildClient({ engine: { ...engine, adapter: txAdapter, inTransaction: true } })), options),
6443
6848
  $queryRaw: async (strings, ...values) => (await runRaw("$queryRaw", buildRawStatement({ dialect: engine.dialect, strings, values }))).rows,
6444
6849
  $executeRaw: async (strings, ...values) => (await runRaw("$executeRaw", buildRawStatement({ dialect: engine.dialect, strings, values }))).affectedRows,
6445
6850
  $queryRawUnsafe: async (text, ...values) => (await runRaw("$queryRawUnsafe", { text, values })).rows,
@@ -6491,7 +6896,7 @@ function makeMountContext(params) {
6491
6896
  compileWhere: ({ where, model: modelName, qualify }) => {
6492
6897
  const target = modelName === undefined ? model : getModelMeta({ meta: engine.meta, model: modelName });
6493
6898
  if (target === null) {
6494
- throw new VibeError10({
6899
+ throw new VibeError11({
6495
6900
  code: "VIBE_INTERNAL",
6496
6901
  message: `extension "${extension}" compiled a where with no model in scope`,
6497
6902
  meta: { extension }
@@ -6517,7 +6922,7 @@ function attachExtensionMounts(params) {
6517
6922
  const model = getModelMeta({ meta: engine.meta, model: mount.model ?? "" });
6518
6923
  const delegate = models[model.name];
6519
6924
  if (delegate === undefined || mount.property in delegate) {
6520
- throw new VibeError10({
6925
+ throw new VibeError11({
6521
6926
  code: "VIBE_CONFIG",
6522
6927
  message: `extension "${mount.extension}" cannot mount "${mount.property}" on ${model.name} \u2014 the name is taken`,
6523
6928
  meta: { extension: mount.extension, model: model.name, property: mount.property }
@@ -6528,7 +6933,7 @@ function attachExtensionMounts(params) {
6528
6933
  continue;
6529
6934
  }
6530
6935
  if (!mount.property.startsWith("$") || mount.property in client) {
6531
- throw new VibeError10({
6936
+ throw new VibeError11({
6532
6937
  code: "VIBE_CONFIG",
6533
6938
  message: `extension "${mount.extension}" cannot mount client property "${mount.property}" \u2014 root mounts are $-prefixed and must not collide`,
6534
6939
  meta: { extension: mount.extension, property: mount.property }
@@ -6548,7 +6953,7 @@ function createClient(params) {
6548
6953
  assertComputedManifest({ manifest: computed.manifest, specs: computed.specs });
6549
6954
  }
6550
6955
  if (computed === undefined && options !== undefined && options["computed"] !== undefined) {
6551
- throw new VibeError10({
6956
+ throw new VibeError11({
6552
6957
  code: "VIBE_CONFIG",
6553
6958
  message: "this client was generated without computed fields \u2014 declare them in vibeorm.config.ts and re-run vibeorm generate",
6554
6959
  meta: {}
@@ -6583,6 +6988,7 @@ export {
6583
6988
  resolveRelationSelection,
6584
6989
  resolveRelationLink,
6585
6990
  lateralColumnAlias,
6991
+ isDbNow,
6586
6992
  hasNestedWrites,
6587
6993
  getModelMeta,
6588
6994
  getFieldMeta,
@@ -6598,6 +7004,7 @@ export {
6598
7004
  decodeRow,
6599
7005
  decodeIsNoop2 as decodeIsNoop,
6600
7006
  decodeAggregateValue,
7007
+ dbNow,
6601
7008
  createWithNested,
6602
7009
  createView,
6603
7010
  createClient,
@@ -6622,4 +7029,4 @@ export {
6622
7029
  COUNT_ALIAS
6623
7030
  };
6624
7031
 
6625
- //# debugId=D09B25CE72AA9DC064756E2164756E21
7032
+ //# debugId=0C003BB2F334C66D64756E2164756E21