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

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.isInteger(offset)) {
18
+ throw new VibeError({
19
+ code: "VIBE_VALIDATION",
20
+ message: `dbNow plusSeconds must be a whole number of seconds, 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 }
@@ -162,8 +185,9 @@ function buildModelMeta(params) {
162
185
  if (primaryKey.length > 1)
163
186
  addCompound(primaryKey.map((field) => field.name));
164
187
  for (const index of model.indexes) {
165
- if (index.kind === "unique")
188
+ if (index.kind === "unique" && index.where === undefined) {
166
189
  addCompound(index.fields.map((entry) => entry.name));
190
+ }
167
191
  }
168
192
  const computedSpecs = params.computed;
169
193
  const computed = computedSpecs === undefined || Object.keys(computedSpecs).length === 0 ? null : new Map(Object.entries(computedSpecs).map(([name, spec]) => [
@@ -210,7 +234,7 @@ function getModelMeta(params) {
210
234
  const { meta, model } = params;
211
235
  const found = meta.modelByName.get(model);
212
236
  if (found === undefined) {
213
- throw new VibeError({
237
+ throw new VibeError2({
214
238
  code: "VIBE_VALIDATION",
215
239
  message: `unknown model "${model}"`,
216
240
  meta: { model, known: [...new Set(meta.models.map((entry) => entry.name))] }
@@ -223,20 +247,20 @@ function getFieldMeta(params) {
223
247
  const found = model.fieldByName.get(field);
224
248
  if (found === undefined) {
225
249
  if (model.computed !== null && model.computed.has(field)) {
226
- throw new VibeError({
250
+ throw new VibeError2({
227
251
  code: "VIBE_VALIDATION",
228
252
  message: `"${field}" on model "${model.name}" is a computed field \u2014 computed fields appear in results (select/omit) only, not in ${context}`,
229
253
  meta: { model: model.name, field, context, computed: true }
230
254
  });
231
255
  }
232
256
  if (model.relationByName.has(field)) {
233
- throw new VibeError({
257
+ throw new VibeError2({
234
258
  code: "VIBE_VALIDATION",
235
259
  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
260
  meta: { model: model.name, field, context }
237
261
  });
238
262
  }
239
- throw new VibeError({
263
+ throw new VibeError2({
240
264
  code: "VIBE_VALIDATION",
241
265
  message: `unknown field "${field}" on model "${model.name}" in ${context}`,
242
266
  meta: { model: model.name, field, context }
@@ -245,7 +269,7 @@ function getFieldMeta(params) {
245
269
  return found;
246
270
  }
247
271
  // packages/runtime/src/codecs.ts
248
- import { VibeError as VibeError2, isEnumType as isEnumType2 } from "@vibeorm/schema";
272
+ import { VibeError as VibeError3, isEnumType as isEnumType2 } from "@vibeorm/schema";
249
273
  import { DIALECT_CODECS, decodeIsNoop } from "@vibeorm/sql";
250
274
  import {
251
275
  DIALECT_CODECS as DIALECT_CODECS2,
@@ -341,7 +365,7 @@ var DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
341
365
  function toRealNumber(params) {
342
366
  const { field, value } = params;
343
367
  const refuse = () => {
344
- throw new VibeError2({
368
+ throw new VibeError3({
345
369
  code: "VIBE_VALIDATION",
346
370
  message: `"${String(value)}" is not a numeric value for the Decimal field "${field.name}"`,
347
371
  meta: { field: field.name, value }
@@ -556,10 +580,11 @@ function decodeAggregateValue(params) {
556
580
  return field === null ? value : decodeValue({ dialect, field, value });
557
581
  }
558
582
  // packages/runtime/src/query-builder.ts
559
- import { VibeError as VibeError4, unsupportedCapability } from "@vibeorm/schema";
583
+ import { VibeError as VibeError5, unsupportedCapability } from "@vibeorm/schema";
560
584
  import {
561
585
  SQL_DEFAULT_CELL,
562
586
  containsPattern,
587
+ dbNowMarker,
563
588
  endsWithPattern,
564
589
  escapeLikePattern,
565
590
  jsonParamCell,
@@ -567,9 +592,9 @@ import {
567
592
  } from "@vibeorm/sql";
568
593
 
569
594
  // packages/runtime/src/relation-plan.ts
570
- import { VibeError as VibeError3, implicitJoinTable } from "@vibeorm/schema";
595
+ import { VibeError as VibeError4, implicitJoinTable } from "@vibeorm/schema";
571
596
  function validationError(params) {
572
- return new VibeError3({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
597
+ return new VibeError4({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
573
598
  }
574
599
  function isPlainObject(value) {
575
600
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
@@ -593,7 +618,7 @@ function singleField(params) {
593
618
  }
594
619
  const field = model.fieldByName.get(names[0]);
595
620
  if (field === undefined) {
596
- throw new VibeError3({
621
+ throw new VibeError4({
597
622
  code: "VIBE_SCHEMA_INVALID",
598
623
  message: `relation "${relation.name}" on ${relation.model} names unknown ${what} field "${String(names[0])}"`,
599
624
  meta: { model: relation.model, relation: relation.name }
@@ -662,7 +687,7 @@ function resolveRelationLinkUncached(params) {
662
687
  }
663
688
  const reverse = findReverseRelation({ relation, target });
664
689
  if (reverse === null || reverse.fields === undefined || reverse.fields.length === 0) {
665
- throw new VibeError3({
690
+ throw new VibeError4({
666
691
  code: "VIBE_SCHEMA_INVALID",
667
692
  message: `relation "${relation.name}" on ${relation.model} has no foreign-key counterpart on ${relation.targetModel}`,
668
693
  meta: { model: relation.model, relation: relation.name, target: relation.targetModel }
@@ -959,15 +984,15 @@ var ATOMIC_OPERATIONS = {
959
984
  divide: "divide"
960
985
  };
961
986
  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"],
987
+ findMany: ["where", "orderBy", "take", "skip", "cursor", "distinct", "select", "include", "omit", "relationStrategy", "lock"],
988
+ findFirst: ["where", "orderBy", "take", "skip", "cursor", "distinct", "select", "include", "omit", "relationStrategy", "lock"],
989
+ findUnique: ["where", "select", "include", "omit", "relationStrategy", "lock"],
990
+ create: ["data", "select", "include", "omit", "onConflict"],
966
991
  createMany: ["data", "skipDuplicates"],
967
992
  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"],
993
+ update: ["where", "data", "select", "include", "omit", "skipUpdatedAt"],
994
+ updateMany: ["where", "data", "skipUpdatedAt"],
995
+ upsert: ["where", "create", "update", "select", "include", "omit", "onConflict"],
971
996
  delete: ["where", "select", "include", "omit"],
972
997
  deleteMany: ["where"],
973
998
  count: ["where", "select", "take", "skip", "orderBy"],
@@ -983,7 +1008,7 @@ var AGG_FN_BY_KEY = {
983
1008
  _max: "max"
984
1009
  };
985
1010
  function validationError2(params) {
986
- return new VibeError4({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
1011
+ return new VibeError5({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
987
1012
  }
988
1013
  function isPlainObject2(value) {
989
1014
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Uint8Array);
@@ -1003,6 +1028,121 @@ function columnRef(params) {
1003
1028
  const { field, table } = params;
1004
1029
  return table === undefined ? { name: field.columnName } : { table, name: field.columnName };
1005
1030
  }
1031
+ function compileLock(params) {
1032
+ const { args, model, method, hasRelations } = params;
1033
+ if (args.lock === undefined)
1034
+ return false;
1035
+ if (model.isView) {
1036
+ throw validationError2({
1037
+ message: `lock on ${model.clientName}.${method}() \u2014 a view row cannot be locked; lock the underlying table's rows instead`,
1038
+ meta: { model: model.name, method }
1039
+ });
1040
+ }
1041
+ if (args.lock !== "update") {
1042
+ throw validationError2({
1043
+ message: `lock on ${model.clientName}.${method}() must be "update", got ${String(args.lock)}`,
1044
+ meta: { model: model.name, method, lock: args.lock }
1045
+ });
1046
+ }
1047
+ if (args.distinct !== undefined) {
1048
+ throw validationError2({
1049
+ message: `lock cannot combine with distinct on ${model.clientName}.${method}() \u2014 postgres rejects FOR UPDATE with DISTINCT`,
1050
+ meta: { model: model.name, method }
1051
+ });
1052
+ }
1053
+ if (hasRelations) {
1054
+ throw validationError2({
1055
+ message: `lock cannot combine with relation loading on ${model.clientName}.${method}() \u2014 lock the parent row alone, then read relations inside the same transaction`,
1056
+ meta: { model: model.name, method }
1057
+ });
1058
+ }
1059
+ return true;
1060
+ }
1061
+ function compileConflictTarget(params) {
1062
+ const { dialect, meta, model, method, target, where } = params;
1063
+ if ((target !== undefined || where !== undefined) && dialect.doNothingForm === "insertIgnore") {
1064
+ throw unsupportedCapability({
1065
+ dialect: dialect.name,
1066
+ feature: `a conflict target on ${model.clientName}.${method}()`,
1067
+ hint: "mysql's INSERT IGNORE / ON DUPLICATE KEY fire on ANY duplicate key \u2014 no target can be named; gate the call per dialect"
1068
+ });
1069
+ }
1070
+ const out = {};
1071
+ if (target !== undefined) {
1072
+ if (Array.isArray(target)) {
1073
+ if (target.length === 0) {
1074
+ throw validationError2({
1075
+ message: `onConflict target on ${model.clientName}.${method}() must name at least one field`,
1076
+ meta: { model: model.name, method }
1077
+ });
1078
+ }
1079
+ out.columns = target.map((name) => getFieldMeta({ model, field: String(name), context: "onConflict target" }).columnName);
1080
+ } else if (isPlainObject2(target) && typeof target.constraint === "string" && target.constraint.length > 0 && Object.keys(target).length === 1) {
1081
+ if (!dialect.capabilities.conflictTargetConstraint) {
1082
+ throw unsupportedCapability({
1083
+ dialect: dialect.name,
1084
+ feature: "a named-constraint conflict target (ON CONFLICT ON CONSTRAINT)",
1085
+ hint: "only postgres supports ON CONSTRAINT targets \u2014 name the indexed columns instead"
1086
+ });
1087
+ }
1088
+ out.constraint = target.constraint;
1089
+ } else {
1090
+ throw validationError2({
1091
+ message: `onConflict target on ${model.clientName}.${method}() must be a field-name array or { constraint: string }`,
1092
+ meta: { model: model.name, method }
1093
+ });
1094
+ }
1095
+ }
1096
+ if (where !== undefined) {
1097
+ if (out.columns === undefined) {
1098
+ throw validationError2({
1099
+ message: `onConflict where on ${model.clientName}.${method}() needs a column-list target \u2014 the predicate refines "(columns) WHERE \u2026"`,
1100
+ meta: { model: model.name, method }
1101
+ });
1102
+ }
1103
+ if (!dialect.capabilities.partialIndexes) {
1104
+ throw unsupportedCapability({
1105
+ dialect: dialect.name,
1106
+ feature: "a partial-index conflict target",
1107
+ hint: "this dialect has no partial indexes to target"
1108
+ });
1109
+ }
1110
+ if (typeof where !== "string" || where.trim().length === 0) {
1111
+ throw validationError2({
1112
+ message: `onConflict where on ${model.clientName}.${method}() must be the partial index's raw predicate string (the schema declaration's where: text)`,
1113
+ meta: { model: model.name, method }
1114
+ });
1115
+ }
1116
+ out.conflictWhere = where;
1117
+ }
1118
+ return out;
1119
+ }
1120
+ function compileDbNowWrite(params) {
1121
+ const { model, field, value } = params;
1122
+ if (field.scalarType !== "DateTime" || field.isList) {
1123
+ throw validationError2({
1124
+ message: `dbNow() writes the database clock into a DateTime field \u2014 ${model.name}.${field.name} is ${String(field.enumName ?? field.scalarType)}${field.isList ? "[]" : ""}`,
1125
+ meta: { model: model.name, field: field.name }
1126
+ });
1127
+ }
1128
+ if (field.isId || field.isUnique || model.primaryKey.some((candidate) => candidate.name === field.name)) {
1129
+ throw validationError2({
1130
+ 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`,
1131
+ meta: { model: model.name, field: field.name }
1132
+ });
1133
+ }
1134
+ return dbNowMarker({ offsetSeconds: value.offsetSeconds });
1135
+ }
1136
+ function compileDbNowFilter(params) {
1137
+ const { model, field, value } = params;
1138
+ if (field.scalarType !== "DateTime" || field.isList) {
1139
+ throw validationError2({
1140
+ message: `dbNow() compares against a DateTime field \u2014 ${model.name}.${field.name} is ${String(field.enumName ?? field.scalarType)}${field.isList ? "[]" : ""}`,
1141
+ meta: { model: model.name, field: field.name }
1142
+ });
1143
+ }
1144
+ return dbNowMarker({ offsetSeconds: value.offsetSeconds });
1145
+ }
1006
1146
  function requireObject(params) {
1007
1147
  const { value, what, model } = params;
1008
1148
  if (!isPlainObject2(value)) {
@@ -1319,7 +1459,7 @@ function compileOperatorFilter(params) {
1319
1459
  kind: "cmp",
1320
1460
  op: "eq",
1321
1461
  column,
1322
- value: encodeFilterValue({ dialect: dialect.name, field, value: operand })
1462
+ value: isDbNow(operand) ? compileDbNowFilter({ model, field, value: operand }) : encodeFilterValue({ dialect: dialect.name, field, value: operand })
1323
1463
  });
1324
1464
  break;
1325
1465
  }
@@ -1343,7 +1483,7 @@ function compileOperatorFilter(params) {
1343
1483
  kind: "cmp",
1344
1484
  op: "ne",
1345
1485
  column,
1346
- value: encodeFilterValue({ dialect: dialect.name, field, value: operand })
1486
+ value: isDbNow(operand) ? compileDbNowFilter({ model, field, value: operand }) : encodeFilterValue({ dialect: dialect.name, field, value: operand })
1347
1487
  });
1348
1488
  break;
1349
1489
  }
@@ -1355,6 +1495,14 @@ function compileOperatorFilter(params) {
1355
1495
  meta: { model: model.name, field: field.name, operator }
1356
1496
  });
1357
1497
  }
1498
+ for (const item of operand) {
1499
+ if (isDbNow(item)) {
1500
+ throw validationError2({
1501
+ message: `dbNow() cannot ride an "${operator}" list on ${model.name}.${field.name} \u2014 compare with lt/lte/gt/gte/equals instead`,
1502
+ meta: { model: model.name, field: field.name, operator }
1503
+ });
1504
+ }
1505
+ }
1358
1506
  conditions.push({
1359
1507
  kind: "inArray",
1360
1508
  column,
@@ -1371,7 +1519,7 @@ function compileOperatorFilter(params) {
1371
1519
  kind: "cmp",
1372
1520
  op: operator,
1373
1521
  column,
1374
- value: encodeFilterValue({ dialect: dialect.name, field, value: operand })
1522
+ value: isDbNow(operand) ? compileDbNowFilter({ model, field, value: operand }) : encodeFilterValue({ dialect: dialect.name, field, value: operand })
1375
1523
  });
1376
1524
  break;
1377
1525
  }
@@ -1427,6 +1575,9 @@ function compileFieldFilter(params) {
1427
1575
  if (isDirectValue(value)) {
1428
1576
  return { kind: "cmp", op: "eq", column, value: encodeFilterValue({ dialect: dialect.name, field, value }) };
1429
1577
  }
1578
+ if (isDbNow(value)) {
1579
+ return { kind: "cmp", op: "eq", column, value: compileDbNowFilter({ model, field, value }) };
1580
+ }
1430
1581
  if (Array.isArray(value)) {
1431
1582
  if (!field.isList) {
1432
1583
  throw validationError2({
@@ -2074,6 +2225,17 @@ function compileCreateRow(params) {
2074
2225
  }
2075
2226
  if (entry.listRefused)
2076
2227
  assertListWritable({ dialect, model, field: entry.field });
2228
+ if (isDbNow(value)) {
2229
+ raw.set(entry.field.name, value);
2230
+ values[entry.field.columnName] = compileDbNowWrite({ model, field: entry.field, value });
2231
+ continue;
2232
+ }
2233
+ if (Array.isArray(value) && value.some((item) => isDbNow(item))) {
2234
+ throw validationError2({
2235
+ message: `dbNow() cannot be a list element (${model.name}.${entry.field.name})`,
2236
+ meta: { model: model.name, field: entry.field.name }
2237
+ });
2238
+ }
2077
2239
  raw.set(entry.field.name, value);
2078
2240
  values[entry.field.columnName] = encodeWith(entry, value);
2079
2241
  }
@@ -2145,6 +2307,16 @@ function compileUpdateSet(params) {
2145
2307
  if (entry.listRefused)
2146
2308
  assertListWritable({ dialect, model, field });
2147
2309
  touched.add(field.name);
2310
+ if (isDbNow(value)) {
2311
+ set[field.columnName] = compileDbNowWrite({ model, field, value });
2312
+ continue;
2313
+ }
2314
+ if (Array.isArray(value) && value.some((item) => isDbNow(item))) {
2315
+ throw validationError2({
2316
+ message: `dbNow() cannot be a list element (${model.name}.${field.name})`,
2317
+ meta: { model: model.name, field: field.name }
2318
+ });
2319
+ }
2148
2320
  if (isPlainObject2(value) && field.scalarType !== "Json") {
2149
2321
  const entries = Object.entries(value).filter(([, operand2]) => operand2 !== undefined);
2150
2322
  if (entries.length !== 1) {
@@ -2155,7 +2327,7 @@ function compileUpdateSet(params) {
2155
2327
  }
2156
2328
  const [operator, operand] = entries[0];
2157
2329
  if (operator === "set") {
2158
- set[field.columnName] = encodeWith(entry, operand);
2330
+ set[field.columnName] = isDbNow(operand) ? compileDbNowWrite({ model, field, value: operand }) : encodeWith(entry, operand);
2159
2331
  continue;
2160
2332
  }
2161
2333
  if (operator === "push" && field.isList) {
@@ -2174,10 +2346,12 @@ function compileUpdateSet(params) {
2174
2346
  }
2175
2347
  set[field.columnName] = encodeWith(entry, value);
2176
2348
  }
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);
2349
+ if (params.skipUpdatedAt !== true) {
2350
+ for (const entry of plan.injected) {
2351
+ if (!entry.field.isUpdatedAt || touched.has(entry.field.name))
2352
+ continue;
2353
+ set[entry.field.columnName] = encodeWith(entry, new Date);
2354
+ }
2181
2355
  }
2182
2356
  if (Object.keys(set).length === 0) {
2183
2357
  throw validationError2({
@@ -3099,6 +3273,7 @@ function buildQuery(params) {
3099
3273
  switch (method) {
3100
3274
  case "findMany":
3101
3275
  case "findFirst": {
3276
+ const locked = compileLock({ args, model, method, hasRelations: relations !== null });
3102
3277
  const shape = buildFindShape({
3103
3278
  dialect,
3104
3279
  meta,
@@ -3111,12 +3286,13 @@ function buildQuery(params) {
3111
3286
  });
3112
3287
  return {
3113
3288
  ...base,
3114
- statement: shape.statement,
3289
+ statement: locked ? { ...shape.statement, lockForUpdate: true } : shape.statement,
3115
3290
  reverseRows: shape.reverseRows,
3116
3291
  resortBy: shape.resortBy
3117
3292
  };
3118
3293
  }
3119
3294
  case "findUnique": {
3295
+ const locked = compileLock({ args, model, method, hasRelations: relations !== null });
3120
3296
  const where = expandCompoundWhere({
3121
3297
  model,
3122
3298
  where: requireObject({ value: args.where, what: "where", model })
@@ -3131,7 +3307,10 @@ function buildQuery(params) {
3131
3307
  ...useLateral ? { selection: relations } : {},
3132
3308
  clientOptions: undefined
3133
3309
  });
3134
- return { ...base, statement: shape.statement };
3310
+ return {
3311
+ ...base,
3312
+ statement: locked ? { ...shape.statement, lockForUpdate: true } : shape.statement
3313
+ };
3135
3314
  }
3136
3315
  case "count": {
3137
3316
  const where = args.where === undefined ? undefined : compileWhere({ dialect, meta, model, where: requireObject({ value: args.where, what: "where", model }) });
@@ -3321,6 +3500,38 @@ function buildQuery(params) {
3321
3500
  };
3322
3501
  }
3323
3502
  case "create": {
3503
+ let conflictClause;
3504
+ if (args.onConflict !== undefined) {
3505
+ const spec = requireObject({ value: args.onConflict, what: "onConflict", model });
3506
+ for (const key of Object.keys(spec)) {
3507
+ if (key !== "action" && key !== "target" && key !== "where") {
3508
+ throw validationError2({
3509
+ message: `unknown onConflict key "${key}" on ${model.clientName}.create() \u2014 expected action/target/where`,
3510
+ meta: { model: model.name, key }
3511
+ });
3512
+ }
3513
+ }
3514
+ if (spec.action !== "nothing") {
3515
+ throw validationError2({
3516
+ message: `onConflict.action on ${model.clientName}.create() must be "nothing" (conflicting rows are skipped \u2014 to update instead, use upsert())`,
3517
+ meta: { model: model.name }
3518
+ });
3519
+ }
3520
+ const compiled = compileConflictTarget({
3521
+ dialect,
3522
+ meta,
3523
+ model,
3524
+ method,
3525
+ target: spec.target,
3526
+ where: spec.where
3527
+ });
3528
+ conflictClause = {
3529
+ doNothing: true,
3530
+ ...compiled.columns === undefined ? {} : { columns: compiled.columns },
3531
+ ...compiled.constraint === undefined ? {} : { constraint: compiled.constraint },
3532
+ ...compiled.conflictWhere === undefined ? {} : { conflictWhere: compiled.conflictWhere }
3533
+ };
3534
+ }
3324
3535
  const row = compileCreateRow({ dialect, model, data: args.data });
3325
3536
  const columns = orderedColumns({ model, values: row.values });
3326
3537
  if (columns.length === 0) {
@@ -3337,6 +3548,7 @@ function buildQuery(params) {
3337
3548
  table: { name: model.tableName },
3338
3549
  columns,
3339
3550
  rows: [columns.map((column) => row.values[column])],
3551
+ ...conflictClause === undefined ? {} : { onConflict: conflictClause },
3340
3552
  returning: returningColumns({ model, selectedFields })
3341
3553
  }
3342
3554
  };
@@ -3351,13 +3563,40 @@ function buildQuery(params) {
3351
3563
  });
3352
3564
  }
3353
3565
  const groups = compileCreateRows({ dialect, model, data: args.data });
3354
- const skipDuplicates = args.skipDuplicates === true;
3566
+ let manyConflict;
3567
+ if (args.skipDuplicates === true) {
3568
+ manyConflict = { doNothing: true };
3569
+ } else if (args.skipDuplicates !== undefined && args.skipDuplicates !== false) {
3570
+ const spec = requireObject({ value: args.skipDuplicates, what: "skipDuplicates", model });
3571
+ for (const key of Object.keys(spec)) {
3572
+ if (key !== "target" && key !== "where") {
3573
+ throw validationError2({
3574
+ message: `unknown skipDuplicates key "${key}" on ${model.clientName}.${method}() \u2014 expected target/where`,
3575
+ meta: { model: model.name, key }
3576
+ });
3577
+ }
3578
+ }
3579
+ const compiled = compileConflictTarget({
3580
+ dialect,
3581
+ meta,
3582
+ model,
3583
+ method,
3584
+ target: spec.target,
3585
+ where: spec.where
3586
+ });
3587
+ manyConflict = {
3588
+ doNothing: true,
3589
+ ...compiled.columns === undefined ? {} : { columns: compiled.columns },
3590
+ ...compiled.constraint === undefined ? {} : { constraint: compiled.constraint },
3591
+ ...compiled.conflictWhere === undefined ? {} : { conflictWhere: compiled.conflictWhere }
3592
+ };
3593
+ }
3355
3594
  const statements = groups.map((group) => ({
3356
3595
  kind: "insert",
3357
3596
  table: { name: model.tableName },
3358
3597
  columns: group.columns,
3359
3598
  rows: group.rows,
3360
- ...skipDuplicates ? { onConflict: { doNothing: true } } : {},
3599
+ ...manyConflict === undefined ? {} : { onConflict: manyConflict },
3361
3600
  ...method === "createManyAndReturn" ? { returning: returningColumns({ model, selectedFields }) } : {}
3362
3601
  }));
3363
3602
  return {
@@ -3369,6 +3608,12 @@ function buildQuery(params) {
3369
3608
  case "update":
3370
3609
  case "updateMany": {
3371
3610
  const isUnique = method === "update";
3611
+ if (args.skipUpdatedAt !== undefined && typeof args.skipUpdatedAt !== "boolean") {
3612
+ throw validationError2({
3613
+ message: `skipUpdatedAt on ${model.clientName}.${method}() must be a boolean, got ${String(args.skipUpdatedAt)}`,
3614
+ meta: { model: model.name, method }
3615
+ });
3616
+ }
3372
3617
  const raw = requireObject({ value: args.where, what: "where", model });
3373
3618
  const where = isUnique ? expandCompoundWhere({ model, where: raw }) : raw;
3374
3619
  if (isUnique)
@@ -3380,13 +3625,36 @@ function buildQuery(params) {
3380
3625
  statement: {
3381
3626
  kind: "update",
3382
3627
  table: { name: model.tableName },
3383
- set: compileUpdateSet({ dialect, model, data: args.data }),
3628
+ set: compileUpdateSet({
3629
+ dialect,
3630
+ model,
3631
+ data: args.data,
3632
+ ...args.skipUpdatedAt === true ? { skipUpdatedAt: true } : {}
3633
+ }),
3384
3634
  ...compiled === undefined ? {} : { where: compiled },
3385
3635
  ...isUnique ? { returning: returningColumns({ model, selectedFields }) } : {}
3386
3636
  }
3387
3637
  };
3388
3638
  }
3389
3639
  case "upsert": {
3640
+ let constraintTarget;
3641
+ if (args.onConflict !== undefined) {
3642
+ const spec = requireObject({ value: args.onConflict, what: "onConflict", model });
3643
+ if (typeof spec.constraint !== "string" || spec.constraint.length === 0 || Object.keys(spec).length !== 1) {
3644
+ throw validationError2({
3645
+ message: `onConflict on ${model.clientName}.upsert() must be { constraint: string } \u2014 the named unique constraint that arbitrates the conflict`,
3646
+ meta: { model: model.name }
3647
+ });
3648
+ }
3649
+ if (!dialect.capabilities.conflictTargetConstraint) {
3650
+ throw unsupportedCapability({
3651
+ dialect: dialect.name,
3652
+ feature: "a named-constraint conflict target on upsert",
3653
+ hint: "only postgres supports ON CONFLICT ON CONSTRAINT \u2014 rely on the where-derived column target instead"
3654
+ });
3655
+ }
3656
+ constraintTarget = spec.constraint;
3657
+ }
3390
3658
  const where = expandCompoundWhere({
3391
3659
  model,
3392
3660
  where: requireObject({ value: args.where, what: "where", model })
@@ -3397,7 +3665,7 @@ function buildQuery(params) {
3397
3665
  const targetFields = conflictTargetFields({ model, where });
3398
3666
  for (const field of targetFields) {
3399
3667
  if (!(field.columnName in row.values)) {
3400
- throw new VibeError4({
3668
+ throw new VibeError5({
3401
3669
  code: "VIBE_INTERNAL",
3402
3670
  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
3671
  meta: { model: model.name, column: field.columnName }
@@ -3408,7 +3676,7 @@ function buildQuery(params) {
3408
3676
  for (const [key, value] of Object.entries(where)) {
3409
3677
  if (value === undefined || nativeTargetNames.has(key))
3410
3678
  continue;
3411
- throw new VibeError4({
3679
+ throw new VibeError5({
3412
3680
  code: "VIBE_INTERNAL",
3413
3681
  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
3682
  meta: { model: model.name, field: key }
@@ -3428,6 +3696,7 @@ function buildQuery(params) {
3428
3696
  rows: [columns.map((column) => row.values[column])],
3429
3697
  onConflict: {
3430
3698
  columns: targetFields.map((field) => field.columnName),
3699
+ ...constraintTarget === undefined ? {} : { constraint: constraintTarget },
3431
3700
  update,
3432
3701
  ...odkuKeyColumn === undefined ? {} : { lastInsertIdColumn: odkuKeyColumn }
3433
3702
  },
@@ -3457,10 +3726,10 @@ function buildQuery(params) {
3457
3726
  }
3458
3727
  }
3459
3728
  // packages/runtime/src/relation-loader.ts
3460
- import { VibeError as VibeError6 } from "@vibeorm/schema";
3729
+ import { VibeError as VibeError7 } from "@vibeorm/schema";
3461
3730
 
3462
3731
  // packages/runtime/src/computed.ts
3463
- import { VibeError as VibeError5 } from "@vibeorm/schema";
3732
+ import { VibeError as VibeError6 } from "@vibeorm/schema";
3464
3733
  function sameNeeds(a, b) {
3465
3734
  if (a.length !== b.length)
3466
3735
  return false;
@@ -3496,7 +3765,7 @@ function assertComputedManifest(params) {
3496
3765
  problems.push(`"${key}" is configured but missing from the generated client`);
3497
3766
  }
3498
3767
  if (problems.length > 0) {
3499
- throw new VibeError5({
3768
+ throw new VibeError6({
3500
3769
  code: "VIBE_STALE_CLIENT",
3501
3770
  message: `computed-field config changed since generate; run vibeorm generate \u2014 ${problems.join("; ")}`,
3502
3771
  meta: { problems }
@@ -3660,7 +3929,7 @@ async function loadManyToMany(params) {
3660
3929
  const link = node.link;
3661
3930
  const join = link.join;
3662
3931
  if (join === null) {
3663
- throw new VibeError6({
3932
+ throw new VibeError7({
3664
3933
  code: "VIBE_INTERNAL",
3665
3934
  message: `manyToMany relation "${node.name}" resolved without join-table wiring`
3666
3935
  });
@@ -3858,7 +4127,7 @@ async function attachLateralRelations(params) {
3858
4127
  try {
3859
4128
  parsed = JSON.parse(raw);
3860
4129
  } catch (cause) {
3861
- throw new VibeError6({
4130
+ throw new VibeError7({
3862
4131
  code: "VIBE_INTERNAL",
3863
4132
  message: `relation column "${column}" returned text that is not valid JSON`,
3864
4133
  meta: { relation: node.name },
@@ -3895,12 +4164,12 @@ async function attachLateralRelations(params) {
3895
4164
  await loadCounts({ ctx, model, rows, counts: selection.counts });
3896
4165
  }
3897
4166
  // packages/runtime/src/nested-writes.ts
3898
- import { VibeError as VibeError7 } from "@vibeorm/schema";
4167
+ import { VibeError as VibeError8 } from "@vibeorm/schema";
3899
4168
  function validationError3(params) {
3900
- return new VibeError7({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
4169
+ return new VibeError8({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
3901
4170
  }
3902
4171
  function notFound(params) {
3903
- return new VibeError7({ code: "VIBE_NOT_FOUND", message: params.message, meta: params.meta });
4172
+ return new VibeError8({ code: "VIBE_NOT_FOUND", message: params.message, meta: params.meta });
3904
4173
  }
3905
4174
  function isPlainObject3(value) {
3906
4175
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Uint8Array);
@@ -4730,7 +4999,7 @@ async function updateWithNested(params) {
4730
4999
  });
4731
5000
  }
4732
5001
  // packages/runtime/src/extensions.ts
4733
- import { VibeError as VibeError8, extensionManifestEntry } from "@vibeorm/schema";
5002
+ import { VibeError as VibeError9, extensionManifestEntry } from "@vibeorm/schema";
4734
5003
  function assertExtensionManifest(params) {
4735
5004
  const { manifest } = params;
4736
5005
  const instances = params.instances ?? [];
@@ -4753,7 +5022,7 @@ function assertExtensionManifest(params) {
4753
5022
  problems.push(`"${name}" is configured but missing from the generated client`);
4754
5023
  }
4755
5024
  if (problems.length > 0) {
4756
- throw new VibeError8({
5025
+ throw new VibeError9({
4757
5026
  code: "VIBE_STALE_CLIENT",
4758
5027
  message: `extension list changed since generate; run vibeorm generate \u2014 ${problems.join("; ")}`,
4759
5028
  meta: { manifest, problems }
@@ -4768,7 +5037,7 @@ function buildExtensionOperatorMap(params) {
4768
5037
  for (const operator of extension.sql?.whereOperators ?? []) {
4769
5038
  const existing = owner.get(operator.name);
4770
5039
  if (existing !== undefined) {
4771
- throw new VibeError8({
5040
+ throw new VibeError9({
4772
5041
  code: "VIBE_CONFIG",
4773
5042
  message: `where operator "${operator.name}" is claimed by both "${existing}" and "${extension.name}"`,
4774
5043
  meta: { operator: operator.name }
@@ -4784,7 +5053,7 @@ function buildExtensionOperatorMap(params) {
4784
5053
  options: extension.options
4785
5054
  });
4786
5055
  if ("error" in compiled) {
4787
- throw new VibeError8({
5056
+ throw new VibeError9({
4788
5057
  code: "VIBE_VALIDATION",
4789
5058
  message: `"${operator.name}" on ${model}.${field}: ${compiled.error}`,
4790
5059
  meta: { model, field, operator: operator.name, extension: extension.name }
@@ -4816,9 +5085,9 @@ function buildConnectHooks(params) {
4816
5085
  return hooks;
4817
5086
  }
4818
5087
  // packages/runtime/src/views.ts
4819
- import { VibeError as VibeError9 } from "@vibeorm/schema";
5088
+ import { VibeError as VibeError10 } from "@vibeorm/schema";
4820
5089
  function validationError4(params) {
4821
- return new VibeError9({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
5090
+ return new VibeError10({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
4822
5091
  }
4823
5092
 
4824
5093
  class ViewResult {
@@ -5170,7 +5439,7 @@ function createView(params) {
5170
5439
  for (const [clientName, mask] of resolved) {
5171
5440
  const delegate = models[clientName];
5172
5441
  if (delegate === undefined) {
5173
- throw new VibeError9({
5442
+ throw new VibeError10({
5174
5443
  code: "VIBE_INTERNAL",
5175
5444
  message: `view "${name}": no delegate for model "${mask.model.name}"`,
5176
5445
  meta: { view: name, model: mask.model.name }
@@ -5186,11 +5455,11 @@ function attachDefineView(params) {
5186
5455
  Object.defineProperty(client, "$defineView", { value, enumerable: true });
5187
5456
  }
5188
5457
  // packages/runtime/src/client.ts
5189
- import { VibeError as VibeError10, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
5458
+ import { VibeError as VibeError11, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
5190
5459
  import { ParamCollector, getSqlDialect } from "@vibeorm/sql";
5191
5460
 
5192
5461
  // packages/runtime/src/render-cache.ts
5193
- import { isListSetOperation, isSetOperation, isSqlDefaultCell, renderStatement, strictMatchPattern } from "@vibeorm/sql";
5462
+ import { isDbNowMarker, isListSetOperation, isSetOperation, isSqlDefaultCell, renderStatement, strictMatchPattern } from "@vibeorm/sql";
5194
5463
  var MAX_ENTRIES = 512;
5195
5464
 
5196
5465
  class StatementRenderCache {
@@ -5240,6 +5509,13 @@ function addSetEntry(walk, name, value) {
5240
5509
  walk.values.push(value.value);
5241
5510
  return;
5242
5511
  }
5512
+ if (isDbNowMarker(value)) {
5513
+ walk.key += value.offsetSeconds === 0 ? "=N0" : "=N1";
5514
+ addIdent(walk, name);
5515
+ if (value.offsetSeconds !== 0)
5516
+ walk.values.push(value.offsetSeconds);
5517
+ return;
5518
+ }
5243
5519
  if (isListSetOperation(value)) {
5244
5520
  walk.key += `=l${value.__listOp}`;
5245
5521
  addIdent(walk, name);
@@ -5263,6 +5539,13 @@ function addReturning(walk, returning) {
5263
5539
  function walkExpr(walk, dialect, expr) {
5264
5540
  switch (expr.kind) {
5265
5541
  case "cmp":
5542
+ if (isDbNowMarker(expr.value)) {
5543
+ walk.key += `cN${expr.op}${expr.value.offsetSeconds === 0 ? "0" : "1"}`;
5544
+ addCol(walk, expr.column);
5545
+ if (expr.value.offsetSeconds !== 0)
5546
+ walk.values.push(expr.value.offsetSeconds);
5547
+ return true;
5548
+ }
5266
5549
  walk.key += `c${expr.op}`;
5267
5550
  addCol(walk, expr.column);
5268
5551
  walk.values.push(expr.value);
@@ -5478,7 +5761,7 @@ function walkSelect(walk, dialect, statement) {
5478
5761
  walk.key += "}";
5479
5762
  return true;
5480
5763
  }
5481
- function walkInsert(walk, statement) {
5764
+ function walkInsert(walk, dialect, statement) {
5482
5765
  walk.key += "I{";
5483
5766
  addTable(walk, statement.table);
5484
5767
  walk.key += "c(";
@@ -5494,7 +5777,11 @@ function walkInsert(walk, statement) {
5494
5777
  const value = row[cell];
5495
5778
  if (isSqlDefaultCell(value))
5496
5779
  walk.key += `D${rowIndex}:${cell};`;
5497
- else
5780
+ else if (isDbNowMarker(value)) {
5781
+ walk.key += `T${rowIndex}:${cell}:${value.offsetSeconds === 0 ? "0" : "1"};`;
5782
+ if (value.offsetSeconds !== 0)
5783
+ walk.values.push(value.offsetSeconds);
5784
+ } else
5498
5785
  walk.values.push(value);
5499
5786
  }
5500
5787
  }
@@ -5502,13 +5789,25 @@ function walkInsert(walk, statement) {
5502
5789
  if (onConflict !== undefined) {
5503
5790
  if ("doNothing" in onConflict) {
5504
5791
  walk.key += "N(";
5792
+ if (onConflict.constraint !== undefined) {
5793
+ walk.key += "K";
5794
+ addIdent(walk, onConflict.constraint);
5795
+ }
5505
5796
  if (onConflict.columns !== undefined) {
5506
5797
  for (const name of onConflict.columns)
5507
5798
  addIdent(walk, name);
5508
5799
  }
5800
+ if (onConflict.conflictWhere !== undefined) {
5801
+ walk.key += "w";
5802
+ addIdent(walk, onConflict.conflictWhere);
5803
+ }
5509
5804
  walk.key += ")";
5510
5805
  } else {
5511
5806
  walk.key += "U(";
5807
+ if (onConflict.constraint !== undefined) {
5808
+ walk.key += "K";
5809
+ addIdent(walk, onConflict.constraint);
5810
+ }
5512
5811
  for (const name of onConflict.columns)
5513
5812
  addIdent(walk, name);
5514
5813
  walk.key += ":";
@@ -5556,7 +5855,7 @@ function walkStatement(walk, dialect, statement) {
5556
5855
  case "select":
5557
5856
  return walkSelect(walk, dialect, statement);
5558
5857
  case "insert":
5559
- return walkInsert(walk, statement);
5858
+ return walkInsert(walk, dialect, statement);
5560
5859
  case "update":
5561
5860
  return walkUpdate(walk, dialect, statement);
5562
5861
  case "delete":
@@ -5606,7 +5905,7 @@ function renderStatementCached(params) {
5606
5905
  // packages/runtime/src/client.ts
5607
5906
  function notFound2(params) {
5608
5907
  const { model, method } = params;
5609
- return new VibeError10({
5908
+ return new VibeError11({
5610
5909
  code: "VIBE_NOT_FOUND",
5611
5910
  message: `no ${model} record matched the where clause of ${method}()`,
5612
5911
  meta: { model, method }
@@ -5614,7 +5913,7 @@ function notFound2(params) {
5614
5913
  }
5615
5914
  function viewIsReadOnly(params) {
5616
5915
  const { model, method } = params;
5617
- return new VibeError10({
5916
+ return new VibeError11({
5618
5917
  code: "VIBE_VALIDATION",
5619
5918
  message: `${model} is a view \u2014 ${method}() is not available on views (they are read-only); write to the underlying table instead`,
5620
5919
  meta: { model, method }
@@ -5743,7 +6042,7 @@ class QueryPromise extends Promise {
5743
6042
  }
5744
6043
  claim() {
5745
6044
  if (this.started) {
5746
- throw new VibeError10({
6045
+ throw new VibeError11({
5747
6046
  code: "VIBE_VALIDATION",
5748
6047
  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
6048
  });
@@ -5790,7 +6089,7 @@ async function runAffected(params) {
5790
6089
  if (plan.batch !== undefined && plan.batch.length > 1) {
5791
6090
  const batch = plan.batch;
5792
6091
  return await engine.adapter.transaction(async (txAdapter) => {
5793
- const txEngine = { ...engine, adapter: txAdapter };
6092
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
5794
6093
  let total = 0;
5795
6094
  for (const statement of batch) {
5796
6095
  total += await runAffected({ engine: txEngine, plan: { ...plan, statement, batch: undefined } });
@@ -5820,6 +6119,13 @@ async function runAffected(params) {
5820
6119
  }
5821
6120
  function plan(params) {
5822
6121
  const { engine, model, method, args } = params;
6122
+ if (args !== undefined && args["lock"] !== undefined && engine.inTransaction !== true) {
6123
+ throw new VibeError11({
6124
+ code: "VIBE_VALIDATION",
6125
+ 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)`,
6126
+ meta: { model, method }
6127
+ });
6128
+ }
5823
6129
  return buildQuery({
5824
6130
  meta: engine.meta,
5825
6131
  dialect: engine.dialect,
@@ -5887,7 +6193,7 @@ async function insertedKeyWhere(params) {
5887
6193
  });
5888
6194
  const id = result.rows[0]?.id;
5889
6195
  if (id === undefined || id === null) {
5890
- throw new VibeError10({
6196
+ throw new VibeError11({
5891
6197
  code: "VIBE_INTERNAL",
5892
6198
  message: `could not read back the generated key for ${source.model.name}`,
5893
6199
  meta: { model: source.model.name }
@@ -5902,8 +6208,11 @@ async function runSingleMutation(params) {
5902
6208
  if (requested !== undefined && requested.length > 0 && engine.dialect.capabilities.returning) {
5903
6209
  const rows = await runRows({ engine, plan: source });
5904
6210
  const row = rows[0];
5905
- if (row === undefined)
6211
+ if (row === undefined) {
6212
+ if (params.allowZeroRows === true)
6213
+ return null;
5906
6214
  throw notFound2({ model: source.model.name, method: source.method });
6215
+ }
5907
6216
  return row;
5908
6217
  }
5909
6218
  const rendered = renderStatementCached({ dialect: engine.dialect, statement, cache: engine.renderCache });
@@ -5928,7 +6237,7 @@ async function runSingleMutation(params) {
5928
6237
  if (where === null)
5929
6238
  throw notFound2({ model: source.model.name, method: source.method });
5930
6239
  return await engine.adapter.transaction(async (txAdapter) => {
5931
- const txEngine = { ...engine, adapter: txAdapter };
6240
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
5932
6241
  const before = await refetch({ engine: txEngine, source, where, lock: true });
5933
6242
  if (before === null)
5934
6243
  throw notFound2({ model: source.model.name, method: source.method });
@@ -5942,7 +6251,7 @@ async function runSingleMutation(params) {
5942
6251
  const knownWhere = source.refetchWhere;
5943
6252
  if (source.method === "upsert") {
5944
6253
  return await engine.adapter.transaction(async (txAdapter) => {
5945
- const txEngine = { ...engine, adapter: txAdapter };
6254
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
5946
6255
  await executeWrite(txEngine);
5947
6256
  const byWhere = await refetch({ engine: txEngine, source, where: knownWhere });
5948
6257
  if (byWhere !== null)
@@ -5966,9 +6275,9 @@ async function runSingleMutation(params) {
5966
6275
  }
5967
6276
  if (source.method === "update") {
5968
6277
  return await engine.adapter.transaction(async (txAdapter) => {
5969
- const txEngine = { ...engine, adapter: txAdapter };
5970
- const affected = await executeWrite(txEngine);
5971
- if (affected === 0)
6278
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6279
+ const affected2 = await executeWrite(txEngine);
6280
+ if (affected2 === 0)
5972
6281
  throw notFound2({ model: source.model.name, method: source.method });
5973
6282
  const after2 = await refetch({ engine: txEngine, source, where: knownWhere });
5974
6283
  if (after2 === null)
@@ -5976,15 +6285,19 @@ async function runSingleMutation(params) {
5976
6285
  return after2;
5977
6286
  });
5978
6287
  }
5979
- await executeWrite(engine);
6288
+ const affected = await executeWrite(engine);
6289
+ if (affected === 0 && params.allowZeroRows === true)
6290
+ return null;
5980
6291
  const after = await refetch({ engine, source, where: knownWhere });
5981
6292
  if (after === null)
5982
6293
  throw notFound2({ model: source.model.name, method: source.method });
5983
6294
  return after;
5984
6295
  }
5985
6296
  return await engine.adapter.transaction(async (txAdapter) => {
5986
- const txEngine = { ...engine, adapter: txAdapter };
5987
- await executeWrite(txEngine);
6297
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6298
+ const affected = await executeWrite(txEngine);
6299
+ if (affected === 0 && params.allowZeroRows === true)
6300
+ return null;
5988
6301
  const where = await insertedKeyWhere({ engine: txEngine, source });
5989
6302
  const after = await refetch({ engine: txEngine, source, where });
5990
6303
  if (after === null)
@@ -5992,6 +6305,11 @@ async function runSingleMutation(params) {
5992
6305
  return after;
5993
6306
  });
5994
6307
  }
6308
+ function requireRow(params) {
6309
+ if (params.row === null)
6310
+ throw notFound2({ model: params.model, method: params.method });
6311
+ return params.row;
6312
+ }
5995
6313
  async function runUpsertFallback(params) {
5996
6314
  const { engine, model, args } = params;
5997
6315
  const passthrough = {};
@@ -6019,10 +6337,14 @@ async function runUpsertFallback(params) {
6019
6337
  args: { ...passthrough, where: args.where, data: args.update }
6020
6338
  });
6021
6339
  return await engine.adapter.transaction(async (txAdapter) => {
6022
- const txEngine = { ...engine, adapter: txAdapter };
6340
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6023
6341
  const existing = (await runRows({ engine: txEngine, plan: probePlan }))[0];
6024
6342
  const source = existing === undefined ? createSource : updateSource;
6025
- return await runSingleMutation({ engine: txEngine, source, readBeforeWrite: false });
6343
+ return requireRow({
6344
+ row: await runSingleMutation({ engine: txEngine, source, readBeforeWrite: false }),
6345
+ model: model.name,
6346
+ method: "upsert"
6347
+ });
6026
6348
  });
6027
6349
  }
6028
6350
  function relationExecutor(params) {
@@ -6079,10 +6401,14 @@ async function finalizeRow(params) {
6079
6401
  function makeRunner(params) {
6080
6402
  const { engine } = params;
6081
6403
  return {
6082
- single: async ({ model, method, args }) => await runSingleMutation({
6083
- engine,
6084
- source: plan({ engine, model, method, args }),
6085
- readBeforeWrite: method === "delete"
6404
+ single: async ({ model, method, args }) => requireRow({
6405
+ row: await runSingleMutation({
6406
+ engine,
6407
+ source: plan({ engine, model, method, args }),
6408
+ readBeforeWrite: method === "delete"
6409
+ }),
6410
+ model,
6411
+ method
6086
6412
  }),
6087
6413
  rows: async ({ model, method, args }) => await runRows({ engine, plan: plan({ engine, model, method, args }) }),
6088
6414
  affected: async ({ model, method, args }) => await runAffected({ engine, plan: plan({ engine, model, method, args }) }),
@@ -6108,7 +6434,7 @@ function makeWriteContext(params) {
6108
6434
  meta: engine.meta,
6109
6435
  dialect: engine.dialect,
6110
6436
  run: makeRunner({ engine }),
6111
- transactional: (fn) => engine.adapter.transaction((txAdapter) => fn(makeRunner({ engine: { ...engine, adapter: txAdapter } })))
6437
+ transactional: (fn) => engine.adapter.transaction((txAdapter) => fn(makeRunner({ engine: { ...engine, adapter: txAdapter, inTransaction: true } })))
6112
6438
  };
6113
6439
  }
6114
6440
  function readOnlyDelegate(params) {
@@ -6166,7 +6492,15 @@ function createDelegate(params) {
6166
6492
  };
6167
6493
  const create = async (target, args) => {
6168
6494
  const modelMeta = getModelMeta({ meta: target.meta, model });
6495
+ const skipping = args.onConflict !== undefined;
6169
6496
  if (hasNestedWrites({ model: modelMeta, data: args.data })) {
6497
+ if (skipping) {
6498
+ throw new VibeError11({
6499
+ code: "VIBE_VALIDATION",
6500
+ message: `onConflict cannot combine with nested writes on ${modelMeta.name}.create() \u2014 insert the parent row alone, then connect relations`,
6501
+ meta: { model: modelMeta.name }
6502
+ });
6503
+ }
6170
6504
  const shape = resolveResultShape({ meta: target.meta, model: modelMeta, method: "create", args });
6171
6505
  const row2 = await createWithNested({
6172
6506
  ctx: makeWriteContext({ engine: target }),
@@ -6181,7 +6515,14 @@ function createDelegate(params) {
6181
6515
  });
6182
6516
  }
6183
6517
  const built = plan({ engine: target, model, method: "create", args });
6184
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: false });
6518
+ const row = await runSingleMutation({
6519
+ engine: target,
6520
+ source: built,
6521
+ readBeforeWrite: false,
6522
+ ...skipping ? { allowZeroRows: true } : {}
6523
+ });
6524
+ if (row === null)
6525
+ return null;
6185
6526
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6186
6527
  };
6187
6528
  const update = async (target, args) => {
@@ -6202,7 +6543,11 @@ function createDelegate(params) {
6202
6543
  });
6203
6544
  }
6204
6545
  const built = plan({ engine: target, model, method: "update", args });
6205
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: false });
6546
+ const row = requireRow({
6547
+ row: await runSingleMutation({ engine: target, source: built, readBeforeWrite: false }),
6548
+ model: built.model.name,
6549
+ method: "update"
6550
+ });
6206
6551
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6207
6552
  };
6208
6553
  const createMany = async (target, args) => {
@@ -6234,13 +6579,20 @@ function createDelegate(params) {
6234
6579
  const upsert = async (target, args) => {
6235
6580
  const modelMeta = getModelMeta({ meta: target.meta, model });
6236
6581
  if (hasNestedWrites({ model: modelMeta, data: args.create }) || hasNestedWrites({ model: modelMeta, data: args.update })) {
6237
- throw new VibeError10({
6582
+ throw new VibeError11({
6238
6583
  code: "VIBE_VALIDATION",
6239
6584
  message: `nested writes are not supported inside upsert arms on ${modelMeta.name} \u2014 use create()/update() with nested data`,
6240
6585
  meta: { model: modelMeta.name }
6241
6586
  });
6242
6587
  }
6243
6588
  if (!upsertUsesNativeStatement({ model: modelMeta, args })) {
6589
+ if (args.onConflict !== undefined) {
6590
+ throw new VibeError11({
6591
+ code: "VIBE_VALIDATION",
6592
+ 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`,
6593
+ meta: { model: modelMeta.name }
6594
+ });
6595
+ }
6244
6596
  const shape = resolveResultShape({
6245
6597
  meta: target.meta,
6246
6598
  model: modelMeta,
@@ -6256,12 +6608,20 @@ function createDelegate(params) {
6256
6608
  });
6257
6609
  }
6258
6610
  const built = plan({ engine: target, model, method: "upsert", args });
6259
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: false });
6611
+ const row = requireRow({
6612
+ row: await runSingleMutation({ engine: target, source: built, readBeforeWrite: false }),
6613
+ model: built.model.name,
6614
+ method: "upsert"
6615
+ });
6260
6616
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6261
6617
  };
6262
6618
  const deleteOne = async (target, args) => {
6263
6619
  const built = plan({ engine: target, model, method: "delete", args });
6264
- const row = await runSingleMutation({ engine: target, source: built, readBeforeWrite: true });
6620
+ const row = requireRow({
6621
+ row: await runSingleMutation({ engine: target, source: built, readBeforeWrite: true }),
6622
+ model: built.model.name,
6623
+ method: "delete"
6624
+ });
6265
6625
  return await finalizeRow({ engine: target, model: built.model, shape: shapeOf(built), row });
6266
6626
  };
6267
6627
  const deleteMany = async (target, args) => {
@@ -6389,7 +6749,7 @@ function buildClient(params) {
6389
6749
  let claimError = null;
6390
6750
  for (const operation of operations) {
6391
6751
  if (!(operation instanceof QueryPromise)) {
6392
- claimError = new VibeError10({
6752
+ claimError = new VibeError11({
6393
6753
  code: "VIBE_VALIDATION",
6394
6754
  message: "$transaction([...]) takes query promises from model delegate calls \u2014 a foreign promise is already running and cannot join the transaction"
6395
6755
  });
@@ -6408,7 +6768,7 @@ function buildClient(params) {
6408
6768
  return Promise.reject(claimError);
6409
6769
  }
6410
6770
  return engine.adapter.transaction(async (txAdapter) => {
6411
- const txEngine = { ...engine, adapter: txAdapter };
6771
+ const txEngine = { ...engine, adapter: txAdapter, inTransaction: true };
6412
6772
  const results = [];
6413
6773
  let index = 0;
6414
6774
  try {
@@ -6428,7 +6788,7 @@ function buildClient(params) {
6428
6788
  $models: models,
6429
6789
  $on: (event, listener) => {
6430
6790
  if (event !== "query") {
6431
- throw new VibeError10({
6791
+ throw new VibeError11({
6432
6792
  code: "VIBE_VALIDATION",
6433
6793
  message: `unknown client event "${String(event)}" \u2014 supported: "query"`,
6434
6794
  meta: { event }
@@ -6439,7 +6799,7 @@ function buildClient(params) {
6439
6799
  engine.listeners.delete(listener);
6440
6800
  };
6441
6801
  },
6442
- $transaction: (fnOrOperations, options) => Array.isArray(fnOrOperations) ? runOperations(fnOrOperations, options) : engine.adapter.transaction((txAdapter) => fnOrOperations(buildClient({ engine: { ...engine, adapter: txAdapter } })), options),
6802
+ $transaction: (fnOrOperations, options) => Array.isArray(fnOrOperations) ? runOperations(fnOrOperations, options) : engine.adapter.transaction((txAdapter) => fnOrOperations(buildClient({ engine: { ...engine, adapter: txAdapter, inTransaction: true } })), options),
6443
6803
  $queryRaw: async (strings, ...values) => (await runRaw("$queryRaw", buildRawStatement({ dialect: engine.dialect, strings, values }))).rows,
6444
6804
  $executeRaw: async (strings, ...values) => (await runRaw("$executeRaw", buildRawStatement({ dialect: engine.dialect, strings, values }))).affectedRows,
6445
6805
  $queryRawUnsafe: async (text, ...values) => (await runRaw("$queryRawUnsafe", { text, values })).rows,
@@ -6491,7 +6851,7 @@ function makeMountContext(params) {
6491
6851
  compileWhere: ({ where, model: modelName, qualify }) => {
6492
6852
  const target = modelName === undefined ? model : getModelMeta({ meta: engine.meta, model: modelName });
6493
6853
  if (target === null) {
6494
- throw new VibeError10({
6854
+ throw new VibeError11({
6495
6855
  code: "VIBE_INTERNAL",
6496
6856
  message: `extension "${extension}" compiled a where with no model in scope`,
6497
6857
  meta: { extension }
@@ -6517,7 +6877,7 @@ function attachExtensionMounts(params) {
6517
6877
  const model = getModelMeta({ meta: engine.meta, model: mount.model ?? "" });
6518
6878
  const delegate = models[model.name];
6519
6879
  if (delegate === undefined || mount.property in delegate) {
6520
- throw new VibeError10({
6880
+ throw new VibeError11({
6521
6881
  code: "VIBE_CONFIG",
6522
6882
  message: `extension "${mount.extension}" cannot mount "${mount.property}" on ${model.name} \u2014 the name is taken`,
6523
6883
  meta: { extension: mount.extension, model: model.name, property: mount.property }
@@ -6528,7 +6888,7 @@ function attachExtensionMounts(params) {
6528
6888
  continue;
6529
6889
  }
6530
6890
  if (!mount.property.startsWith("$") || mount.property in client) {
6531
- throw new VibeError10({
6891
+ throw new VibeError11({
6532
6892
  code: "VIBE_CONFIG",
6533
6893
  message: `extension "${mount.extension}" cannot mount client property "${mount.property}" \u2014 root mounts are $-prefixed and must not collide`,
6534
6894
  meta: { extension: mount.extension, property: mount.property }
@@ -6548,7 +6908,7 @@ function createClient(params) {
6548
6908
  assertComputedManifest({ manifest: computed.manifest, specs: computed.specs });
6549
6909
  }
6550
6910
  if (computed === undefined && options !== undefined && options["computed"] !== undefined) {
6551
- throw new VibeError10({
6911
+ throw new VibeError11({
6552
6912
  code: "VIBE_CONFIG",
6553
6913
  message: "this client was generated without computed fields \u2014 declare them in vibeorm.config.ts and re-run vibeorm generate",
6554
6914
  meta: {}
@@ -6583,6 +6943,7 @@ export {
6583
6943
  resolveRelationSelection,
6584
6944
  resolveRelationLink,
6585
6945
  lateralColumnAlias,
6946
+ isDbNow,
6586
6947
  hasNestedWrites,
6587
6948
  getModelMeta,
6588
6949
  getFieldMeta,
@@ -6598,6 +6959,7 @@ export {
6598
6959
  decodeRow,
6599
6960
  decodeIsNoop2 as decodeIsNoop,
6600
6961
  decodeAggregateValue,
6962
+ dbNow,
6601
6963
  createWithNested,
6602
6964
  createView,
6603
6965
  createClient,
@@ -6622,4 +6984,4 @@ export {
6622
6984
  COUNT_ALIAS
6623
6985
  };
6624
6986
 
6625
- //# debugId=D09B25CE72AA9DC064756E2164756E21
6987
+ //# debugId=DDDCD7D2210268EA64756E2164756E21