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

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/db-now.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  /** The database-clock sentinel produced by {@link dbNow}. */
19
19
  export type DbNow = {
20
20
  readonly __vibeDbNow: true;
21
- /** Whole seconds added to the database's now() (negative = past). */
21
+ /** Seconds added to the database's now() (fractional OK; negative = past). */
22
22
  readonly offsetSeconds: number;
23
23
  };
24
24
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"db-now.d.ts","sourceRoot":"","sources":["../src/db-now.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,6DAA6D;AAC7D,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAUxE;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAYtD"}
1
+ {"version":3,"file":"db-now.d.ts","sourceRoot":"","sources":["../src/db-now.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,6DAA6D;AAC7D,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAUxE;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAYtD"}
package/dist/index.js CHANGED
@@ -14,10 +14,10 @@ function toSqlExecutor(params) {
14
14
  import { VibeError } from "@vibeorm/schema";
15
15
  function dbNow(options) {
16
16
  const offset = options?.plusSeconds ?? 0;
17
- if (typeof offset !== "number" || !Number.isInteger(offset)) {
17
+ if (typeof offset !== "number" || !Number.isFinite(offset)) {
18
18
  throw new VibeError({
19
19
  code: "VIBE_VALIDATION",
20
- message: `dbNow plusSeconds must be a whole number of seconds, got ${String(offset)}`,
20
+ message: `dbNow plusSeconds must be a finite number of seconds (fractional is fine), got ${String(offset)}`,
21
21
  meta: { plusSeconds: offset }
22
22
  });
23
23
  }
@@ -31,7 +31,7 @@ function isDbNow(value) {
31
31
  return false;
32
32
  }
33
33
  const candidate = value;
34
- return candidate.__vibeDbNow === true && typeof candidate.offsetSeconds === "number" && Number.isInteger(candidate.offsetSeconds);
34
+ return candidate.__vibeDbNow === true && typeof candidate.offsetSeconds === "number" && Number.isFinite(candidate.offsetSeconds);
35
35
  }
36
36
  // packages/runtime/src/model-meta.ts
37
37
  import { VibeError as VibeError2, isEnumType, validateComputedConfig } from "@vibeorm/schema";
@@ -167,26 +167,45 @@ function buildModelMeta(params) {
167
167
  primaryKey.push(...fields.filter((field) => field.isId));
168
168
  }
169
169
  const compoundUniques = new Map;
170
- const addCompound = (names) => {
171
- if (names.length < 2)
172
- return;
173
- const key = names.join("_");
174
- if (compoundUniques.has(key))
175
- return;
170
+ const singleIndexUniques = [];
171
+ const resolveFields = (names) => {
176
172
  const resolved = [];
177
173
  for (const name of names) {
178
174
  const field = fieldByName.get(name);
179
175
  if (field === undefined)
180
- return;
176
+ return null;
181
177
  resolved.push(field);
182
178
  }
183
- compoundUniques.set(key, resolved);
179
+ return resolved;
184
180
  };
185
- if (primaryKey.length > 1)
186
- addCompound(primaryKey.map((field) => field.name));
181
+ const addCompound = (names, nullsNotDistinct, clientName) => {
182
+ if (names.length < 2)
183
+ return;
184
+ const key = clientName ?? 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 });
191
+ };
192
+ if (primaryKey.length > 1) {
193
+ addCompound(primaryKey.map((field) => field.name), false, model.primaryKey?.clientName);
194
+ }
187
195
  for (const index of model.indexes) {
188
- if (index.kind === "unique" && index.where === undefined) {
189
- addCompound(index.fields.map((entry) => entry.name));
196
+ if (index.kind !== "unique" || index.where !== undefined)
197
+ continue;
198
+ const names = index.fields.map((entry) => entry.name);
199
+ const nullsNotDistinct = index.nullsNotDistinct === true;
200
+ if (names.length === 1) {
201
+ const field = resolveFields(names)?.[0];
202
+ if (field === undefined || field.isId || field.isUnique)
203
+ continue;
204
+ if (singleIndexUniques.some((entry) => entry.fields[0]?.name === field.name))
205
+ continue;
206
+ singleIndexUniques.push({ fields: [field], nullsNotDistinct });
207
+ } else {
208
+ addCompound(names, nullsNotDistinct, index.clientName);
190
209
  }
191
210
  }
192
211
  const computedSpecs = params.computed;
@@ -204,6 +223,7 @@ function buildModelMeta(params) {
204
223
  primaryKey,
205
224
  uniqueFields: fields.filter((field) => field.isId || field.isUnique),
206
225
  compoundUniques,
226
+ singleIndexUniques,
207
227
  relations: model.relations,
208
228
  relationByName: new Map(model.relations.map((relation) => [relation.name, relation])),
209
229
  isView: model.isView,
@@ -2096,7 +2116,7 @@ function expandCompoundWhere(params) {
2096
2116
  for (const [key, value] of Object.entries(where)) {
2097
2117
  if (value === undefined)
2098
2118
  continue;
2099
- const fields = model.compoundUniques.get(key);
2119
+ const fields = model.compoundUniques.get(key)?.fields;
2100
2120
  if (fields === undefined)
2101
2121
  continue;
2102
2122
  const spec = requireObject({ value, what: `${what}.${key}`, model });
@@ -2115,33 +2135,52 @@ function expandCompoundWhere(params) {
2115
2135
  }
2116
2136
  return expanded ?? where;
2117
2137
  }
2138
+ function isAddressableMember(params) {
2139
+ return isDirectValue(params.value) || params.nullsNotDistinct && params.value === null;
2140
+ }
2118
2141
  function assertUniqueWhere(params) {
2119
2142
  const { model, where, method } = params;
2120
2143
  for (const field of model.uniqueFields) {
2121
2144
  if (isDirectValue(where[field.name]))
2122
2145
  return;
2123
2146
  }
2124
- for (const fields of model.compoundUniques.values()) {
2125
- if (fields.every((field) => isDirectValue(where[field.name])))
2147
+ for (const single of model.singleIndexUniques) {
2148
+ const field = single.fields[0];
2149
+ if (isAddressableMember({ value: where[field.name], nullsNotDistinct: single.nullsNotDistinct }))
2150
+ return;
2151
+ }
2152
+ for (const group of model.compoundUniques.values()) {
2153
+ if (group.fields.every((field) => isAddressableMember({ value: where[field.name], nullsNotDistinct: group.nullsNotDistinct })))
2126
2154
  return;
2127
2155
  }
2128
- const candidates = [...model.uniqueFields.map((field) => field.name), ...model.compoundUniques.keys()];
2156
+ const candidates = [
2157
+ ...model.uniqueFields.map((field) => field.name),
2158
+ ...model.singleIndexUniques.map((single) => single.fields[0].name),
2159
+ ...model.compoundUniques.keys()
2160
+ ];
2129
2161
  throw validationError2({
2130
2162
  message: `${model.clientName}.${method}() needs a unique where \u2014 one of: ${candidates.join(", ")}`,
2131
2163
  meta: { model: model.name, method, candidates }
2132
2164
  });
2133
2165
  }
2134
- function conflictTargetFields(params) {
2166
+ function conflictTargetGroup(params) {
2135
2167
  const { model, where } = params;
2136
2168
  for (const field of model.uniqueFields) {
2137
2169
  if (isDirectValue(where[field.name]))
2138
- return [field];
2170
+ return { fields: [field], nullsNotDistinct: false };
2171
+ }
2172
+ for (const single of model.singleIndexUniques) {
2173
+ const field = single.fields[0];
2174
+ if (isAddressableMember({ value: where[field.name], nullsNotDistinct: single.nullsNotDistinct })) {
2175
+ return single;
2176
+ }
2139
2177
  }
2140
- for (const fields of model.compoundUniques.values()) {
2141
- if (fields.every((field) => isDirectValue(where[field.name])))
2142
- return fields;
2178
+ for (const group of model.compoundUniques.values()) {
2179
+ if (group.fields.every((field) => isAddressableMember({ value: where[field.name], nullsNotDistinct: group.nullsNotDistinct }))) {
2180
+ return group;
2181
+ }
2143
2182
  }
2144
- return model.primaryKey;
2183
+ return { fields: model.primaryKey, nullsNotDistinct: false };
2145
2184
  }
2146
2185
  function sameDirectValue(a, b) {
2147
2186
  if (a === b)
@@ -2161,15 +2200,21 @@ function upsertUsesNativeStatement(params) {
2161
2200
  });
2162
2201
  assertUniqueWhere({ model, where, method: "upsert" });
2163
2202
  const create = requireObject({ value: args.create, what: "create", model });
2164
- const targets = conflictTargetFields({ model, where });
2165
- const targetNames = new Set(targets.map((field) => field.name));
2203
+ const target = conflictTargetGroup({ model, where });
2204
+ const targetNames = new Set(target.fields.map((field) => field.name));
2166
2205
  for (const [key, value] of Object.entries(where)) {
2167
2206
  if (value === undefined)
2168
2207
  continue;
2169
2208
  if (!targetNames.has(key))
2170
2209
  return false;
2171
2210
  }
2172
- return targets.every((field) => isDirectValue(create[field.name]) && sameDirectValue(create[field.name], where[field.name]));
2211
+ return target.fields.every((field) => {
2212
+ const createValue = create[field.name];
2213
+ const whereValue = where[field.name];
2214
+ if (target.nullsNotDistinct && createValue === null && whereValue === null)
2215
+ return true;
2216
+ return isDirectValue(createValue) && sameDirectValue(createValue, whereValue);
2217
+ });
2173
2218
  }
2174
2219
  var writePlanCache = new WeakMap;
2175
2220
  function getWritePlan(params) {
@@ -2451,7 +2496,8 @@ function parseCursor(params) {
2451
2496
  }
2452
2497
  if (fields.length === 1) {
2453
2498
  const field = fields[0];
2454
- if (!field.isId && !field.isUnique) {
2499
+ const isSingleIndexUnique = model.singleIndexUniques.some((single) => single.fields[0].name === field.name);
2500
+ if (!field.isId && !field.isUnique && !isSingleIndexUnique) {
2455
2501
  throw validationError2({
2456
2502
  message: `cursor field ${model.name}.${field.name} must be @id or @unique \u2014 a non-unique cursor cannot identify a position`,
2457
2503
  meta: { model: model.name, field: field.name }
@@ -2460,7 +2506,7 @@ function parseCursor(params) {
2460
2506
  } else {
2461
2507
  const names = new Set(fields.map((field) => field.name));
2462
2508
  const matchesGroup = (group) => group.length === names.size && group.every((field) => names.has(field.name));
2463
- const matches = matchesGroup(model.primaryKey) || [...model.compoundUniques.values()].some(matchesGroup);
2509
+ const matches = matchesGroup(model.primaryKey) || [...model.compoundUniques.values()].some((group) => matchesGroup(group.fields));
2464
2510
  if (!matches) {
2465
2511
  const candidates = [
2466
2512
  model.primaryKey.map((field) => field.name).join(" + ") || "none",
@@ -3662,7 +3708,7 @@ function buildQuery(params) {
3662
3708
  assertUniqueWhere({ model, where, method });
3663
3709
  const row = compileCreateRow({ dialect, model, data: args.create });
3664
3710
  const columns = orderedColumns({ model, values: row.values });
3665
- const targetFields = conflictTargetFields({ model, where });
3711
+ const targetFields = conflictTargetGroup({ model, where }).fields;
3666
3712
  for (const field of targetFields) {
3667
3713
  if (!(field.columnName in row.values)) {
3668
3714
  throw new VibeError5({
@@ -6984,4 +7030,4 @@ export {
6984
7030
  COUNT_ALIAS
6985
7031
  };
6986
7032
 
6987
- //# debugId=DDDCD7D2210268EA64756E2164756E21
7033
+ //# debugId=4B6B11C516D4A81A64756E2164756E21