tempest-db-js 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -40,10 +40,14 @@ var OPERATORS = [
40
40
  "lte",
41
41
  "like",
42
42
  "ilike",
43
+ "ieq",
43
44
  "in",
44
45
  "notIn",
45
46
  "between",
46
- "isNull"
47
+ "isNull",
48
+ "contains",
49
+ "containedBy",
50
+ "overlaps"
47
51
  ];
48
52
  var Agg = class {
49
53
  constructor(fn, column2) {
@@ -127,7 +131,56 @@ var SelectBuilder = class _SelectBuilder {
127
131
  offset(n) {
128
132
  return this.with({ offset: n });
129
133
  }
134
+ /**
135
+ * Lock the selected rows for update (`SELECT ... FOR UPDATE`), à la
136
+ * SQLAlchemy's `with_for_update()`.
137
+ *
138
+ * `{ skipLocked: true }` is the job-queue claim: competing workers each take a
139
+ * disjoint batch instead of blocking on — or worse, double-processing — the
140
+ * same rows.
141
+ *
142
+ * PostgreSQL and MySQL 8.0+ only. SQLite has no row-level locking, and its
143
+ * dialect throws rather than emitting a `SELECT` that silently locks nothing —
144
+ * a lock that does not exist only fails under production concurrency.
145
+ *
146
+ * @param options `skipLocked` / `noWait` wait behavior, and `of` to restrict
147
+ * the lock to specific tables.
148
+ * @returns A builder carrying the locking clause.
149
+ * @throws Error When both `skipLocked` and `noWait` are set.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const batch = await session.execute(
154
+ * select(Outbound)
155
+ * .where({ status: "queued" })
156
+ * .orderBy("nextAttemptAt")
157
+ * .limit(10)
158
+ * .forUpdate({ skipLocked: true }),
159
+ * ).all();
160
+ * ```
161
+ */
162
+ forUpdate(options) {
163
+ return this.with({ lock: buildLock("update", options) });
164
+ }
165
+ /**
166
+ * Take a shared read lock on the selected rows (`SELECT ... FOR SHARE`), the
167
+ * weaker counterpart of {@link SelectBuilder.forUpdate}.
168
+ *
169
+ * @param options `skipLocked` / `noWait` wait behavior, and `of` tables.
170
+ * @returns A builder carrying the locking clause.
171
+ * @throws Error When both `skipLocked` and `noWait` are set.
172
+ */
173
+ forShare(options) {
174
+ return this.with({ lock: buildLock("share", options) });
175
+ }
130
176
  };
177
+ function buildLock(strength, options) {
178
+ if (options?.skipLocked && options?.noWait) {
179
+ throw new Error("forUpdate/forShare accept skipLocked or noWait, not both.");
180
+ }
181
+ const wait = options?.skipLocked ? "skipLocked" : options?.noWait ? "noWait" : "block";
182
+ return { strength, wait, of: options?.of ?? [] };
183
+ }
131
184
  function select(model, columns) {
132
185
  return new SelectBuilder(
133
186
  {
@@ -140,13 +193,223 @@ function select(model, columns) {
140
193
  where: void 0,
141
194
  orderBy: [],
142
195
  limit: void 0,
143
- offset: void 0
196
+ offset: void 0,
197
+ names: columnNamesOf(model) ?? void 0
144
198
  },
145
199
  model
146
200
  );
147
201
  }
148
202
 
203
+ // src/serialize.ts
204
+ var ValidationError = class extends Error {
205
+ constructor(table, issues) {
206
+ super(`Validation failed for ${table}:
207
+ - ${issues.join("\n - ")}`);
208
+ this.table = table;
209
+ this.issues = issues;
210
+ this.name = "ValidationError";
211
+ }
212
+ table;
213
+ issues;
214
+ };
215
+ function toBase64(bytes) {
216
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
217
+ let binary = "";
218
+ for (const byte of bytes) binary += String.fromCharCode(byte);
219
+ return btoa(binary);
220
+ }
221
+ function fromBase64(value) {
222
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64"));
223
+ const binary = atob(value);
224
+ const bytes = new Uint8Array(binary.length);
225
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
226
+ return bytes;
227
+ }
228
+ function encodeValue(column2, value) {
229
+ if (value === null || value === void 0) return null;
230
+ switch (column2.type.kind) {
231
+ case "bigint":
232
+ return typeof value === "bigint" ? value.toString() : value;
233
+ case "date":
234
+ case "datetime":
235
+ case "timestamp":
236
+ return value instanceof Date ? value.toISOString() : value;
237
+ case "blob":
238
+ return value instanceof Uint8Array ? toBase64(value) : value;
239
+ default:
240
+ return value;
241
+ }
242
+ }
243
+ function decodeValue(column2, value) {
244
+ if (value === null || value === void 0) return null;
245
+ switch (column2.type.kind) {
246
+ case "bigint":
247
+ return typeof value === "bigint" ? value : BigInt(value);
248
+ case "date":
249
+ case "datetime":
250
+ case "timestamp":
251
+ return value instanceof Date ? value : new Date(value);
252
+ case "blob":
253
+ return value instanceof Uint8Array ? value : fromBase64(value);
254
+ case "json":
255
+ return typeof value === "string" ? JSON.parse(value) : value;
256
+ case "array": {
257
+ const element = column2.type.meta.element;
258
+ const items = typeof value === "string" ? JSON.parse(value) : value;
259
+ if (!Array.isArray(items)) return items;
260
+ if (!element) return items;
261
+ return items.map((item) => decodeValue({ type: element }, item));
262
+ }
263
+ case "numeric":
264
+ return typeof value === "string" ? value : String(value);
265
+ case "boolean":
266
+ return typeof value === "boolean" ? value : value === 1 || value === "true";
267
+ case "smallint":
268
+ case "integer":
269
+ case "real":
270
+ case "double":
271
+ return typeof value === "number" ? value : Number(value);
272
+ default:
273
+ return value;
274
+ }
275
+ }
276
+ function toDict(model, row) {
277
+ const columns = columnsOf(model);
278
+ const out = {};
279
+ for (const name of Object.keys(columns)) {
280
+ out[name] = row[name] ?? null;
281
+ }
282
+ return out;
283
+ }
284
+ function toJSON(model, row) {
285
+ const columns = columnsOf(model);
286
+ const out = {};
287
+ for (const [name, col] of Object.entries(columns)) {
288
+ out[name] = encodeValue(col, row[name] ?? null);
289
+ }
290
+ return out;
291
+ }
292
+ function stringify(model, row) {
293
+ return JSON.stringify(toJSON(model, row));
294
+ }
295
+ function fromDict(model, data) {
296
+ const columns = columnsOf(model);
297
+ const out = {};
298
+ const issues = [];
299
+ for (const [name, col] of Object.entries(columns)) {
300
+ const present = name in data && data[name] !== void 0 && data[name] !== null;
301
+ if (!present) {
302
+ const required = col.flags.notNull && !col.flags.hasDefault;
303
+ if (required) {
304
+ issues.push(`missing required column "${name}"`);
305
+ continue;
306
+ }
307
+ out[name] = null;
308
+ continue;
309
+ }
310
+ try {
311
+ out[name] = decodeValue(col, data[name]);
312
+ } catch (error) {
313
+ issues.push(`column "${name}": ${error.message}`);
314
+ }
315
+ }
316
+ if (issues.length > 0) {
317
+ throw new ValidationError(model.tablename, issues);
318
+ }
319
+ return out;
320
+ }
321
+ function parse(model, json) {
322
+ return fromDict(model, JSON.parse(json));
323
+ }
324
+ function decoderFor(type) {
325
+ switch (type.kind) {
326
+ case "bigint":
327
+ return (v) => v == null ? null : typeof v === "bigint" ? v : BigInt(v);
328
+ case "date":
329
+ case "datetime":
330
+ case "timestamp":
331
+ return (v) => v == null ? null : v instanceof Date ? v : new Date(v);
332
+ case "blob":
333
+ return (v) => v == null ? null : v instanceof Uint8Array ? v : fromBase64(v);
334
+ case "json":
335
+ return (v) => v == null ? null : typeof v === "string" ? JSON.parse(v) : v;
336
+ case "array": {
337
+ const element = type.meta.element;
338
+ const inner = element ? decoderFor(element) : null;
339
+ if (!inner) return null;
340
+ return (v) => v == null ? null : Array.isArray(v) ? v.map(inner) : v;
341
+ }
342
+ case "numeric":
343
+ return (v) => v == null ? null : typeof v === "string" ? v : String(v);
344
+ case "boolean":
345
+ return (v) => v == null ? null : typeof v === "boolean" ? v : v === 1 || v === "true";
346
+ case "smallint":
347
+ case "integer":
348
+ case "real":
349
+ case "double":
350
+ return (v) => v == null ? null : typeof v === "number" ? v : Number(v);
351
+ default:
352
+ return null;
353
+ }
354
+ }
355
+ var mapperCache = /* @__PURE__ */ new WeakMap();
356
+ function mapperFor(model) {
357
+ const cached = mapperCache.get(model);
358
+ if (cached) return cached;
359
+ const props = columnPropsOf(model);
360
+ const names = props ? Object.fromEntries(Object.entries(props).map(([db, prop]) => [prop, db])) : null;
361
+ const decoders = /* @__PURE__ */ new Map();
362
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
363
+ const decoder = decoderFor(col.type);
364
+ if (decoder) decoders.set(names?.[prop] ?? prop, decoder);
365
+ }
366
+ const mapper = { props, decoders };
367
+ mapperCache.set(model, mapper);
368
+ return mapper;
369
+ }
370
+ function coerceRow(model, raw) {
371
+ const { props, decoders } = mapperFor(model);
372
+ const out = {};
373
+ for (const name of Object.keys(raw)) {
374
+ const decode2 = decoders.get(name);
375
+ out[props?.[name] ?? name] = decode2 ? decode2(raw[name]) : raw[name];
376
+ }
377
+ return out;
378
+ }
379
+
149
380
  // src/mutations.ts
381
+ var STRUCTURED_KINDS = /* @__PURE__ */ new Set(["json", "array", "blob"]);
382
+ function isBindableScalar(value) {
383
+ if (value === null || value === void 0) return true;
384
+ const type = typeof value;
385
+ if (type === "string" || type === "number" || type === "bigint" || type === "boolean") {
386
+ return true;
387
+ }
388
+ return value instanceof Date || value instanceof Uint8Array;
389
+ }
390
+ function assertWritableValues(model, values, clause) {
391
+ const columns = columnsOf(model);
392
+ const issues = [];
393
+ for (const [key, value] of Object.entries(values)) {
394
+ const col = columns[key];
395
+ if (!col) {
396
+ issues.push(`${clause}: "${key}" is not a column of ${model.tablename}`);
397
+ continue;
398
+ }
399
+ if (isSqlExpression(value) || isBindableScalar(value)) continue;
400
+ if (typeof value === "object" && STRUCTURED_KINDS.has(col.type.kind)) continue;
401
+ issues.push(
402
+ `${clause}: "${key}" got ${describeValue(value)}, which cannot be bound to a ${col.type.kind} column \u2014 use sql.raw()/sql.expr\`...\` for a SQL expression`
403
+ );
404
+ }
405
+ if (issues.length > 0) throw new ValidationError(model.tablename, issues);
406
+ }
407
+ function describeValue(value) {
408
+ if (typeof value === "function") return "a function";
409
+ if (Array.isArray(value)) return "an array";
410
+ if (typeof value === "symbol") return "a symbol";
411
+ return "an object";
412
+ }
150
413
  var InsertBuilder = class _InsertBuilder {
151
414
  constructor(node, source) {
152
415
  this.node = node;
@@ -157,28 +420,66 @@ var InsertBuilder = class _InsertBuilder {
157
420
  with(patch) {
158
421
  return new _InsertBuilder({ ...this.node, ...patch }, this.source);
159
422
  }
160
- /** Provide one row or many rows to insert, typed by the insert shape. */
423
+ /**
424
+ * Provide one row or many rows to insert, typed by the insert shape.
425
+ *
426
+ * @param rows One row, or an array of rows.
427
+ * @returns A builder carrying the rows.
428
+ * @throws ValidationError When a value is not a column value the dialect can
429
+ * bind (see the `sql` helpers for writing an expression instead).
430
+ */
161
431
  values(rows) {
162
432
  const list = Array.isArray(rows) ? rows : [rows];
433
+ for (const row of list) assertWritableValues(this.source, row, "values");
163
434
  return this.with({ values: list });
164
435
  }
165
436
  /**
166
437
  * On a unique/PK conflict on `target`, do nothing (skip the row).
167
438
  *
168
439
  * @param target The conflicting column(s) — a unique or primary key.
440
+ * @param options Pass `where` to name the predicate of a **partial** unique
441
+ * index, which PostgreSQL requires in order to match it as a conflict target.
442
+ * @returns A builder carrying the conflict clause.
443
+ *
444
+ * @example
445
+ * ```ts
446
+ * insert(Outbound)
447
+ * .values(data)
448
+ * .onConflictDoNothing(["consumer", "idempotencyKey"], {
449
+ * where: { idempotencyKey: { isNull: false } },
450
+ * })
451
+ * .returning();
452
+ * ```
169
453
  */
170
- onConflictDoNothing(target) {
171
- return this.with({ onConflict: { target, update: "nothing" } });
454
+ onConflictDoNothing(target, options) {
455
+ return this.with({
456
+ onConflict: {
457
+ target,
458
+ update: "nothing",
459
+ targetWhere: options?.where ? toCondNode(options.where) : void 0
460
+ }
461
+ });
172
462
  }
173
463
  /**
174
464
  * On a unique/PK conflict on `target`, overwrite the given columns (upsert).
175
465
  *
176
466
  * @param target The conflicting column(s) — a unique or primary key.
177
467
  * @param set The columns to update with new values.
468
+ * @param options `indexWhere` names the predicate of a partial unique index
469
+ * (the conflict target); `updateWhere` further restricts which conflicting
470
+ * rows are rewritten.
471
+ * @returns A builder carrying the conflict clause.
472
+ * @throws ValidationError When a `set` value cannot be bound.
178
473
  */
179
- onConflictDoUpdate(target, set) {
474
+ onConflictDoUpdate(target, set, options) {
475
+ assertWritableValues(this.source, set, "set");
180
476
  return this.with({
181
- onConflict: { target, update: set }
477
+ onConflict: {
478
+ target,
479
+ update: set,
480
+ targetWhere: options?.indexWhere ? toCondNode(options.indexWhere) : void 0,
481
+ updateWhere: options?.updateWhere ? toCondNode(options.updateWhere) : void 0
482
+ }
182
483
  });
183
484
  }
184
485
  returning(columns) {
@@ -191,7 +492,8 @@ function insert(model) {
191
492
  kind: "insert",
192
493
  table: model.tablename,
193
494
  values: [],
194
- returning: null
495
+ returning: null,
496
+ names: columnNamesOf(model) ?? void 0
195
497
  },
196
498
  model
197
499
  );
@@ -206,8 +508,27 @@ var UpdateBuilder = class _UpdateBuilder {
206
508
  with(patch) {
207
509
  return new _UpdateBuilder({ ...this.node, ...patch }, this.source);
208
510
  }
209
- /** The columns to write. Partial — only the given columns change. */
511
+ /**
512
+ * The columns to write. Partial — only the given columns change.
513
+ *
514
+ * A value is bound as a parameter unless it is a {@link sql} expression, which
515
+ * is rendered inline instead — that is how a counter is written without a
516
+ * read-modify-write race.
517
+ *
518
+ * @param values The column → value map.
519
+ * @returns A builder carrying the assignments.
520
+ * @throws ValidationError When a value is not a column value the dialect can
521
+ * bind (a bare object, an array on a scalar column, a function).
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * update(Outbound)
526
+ * .set({ attempts: sql.raw("attempts + 1"), updatedAt: sql.now() })
527
+ * .where({ id });
528
+ * ```
529
+ */
210
530
  set(values) {
531
+ assertWritableValues(this.source, values, "set");
211
532
  return this.with({ set: values });
212
533
  }
213
534
  /** Restrict the rows to update. Marks the builder safe to execute. */
@@ -233,7 +554,8 @@ function update(model) {
233
554
  set: {},
234
555
  where: void 0,
235
556
  guarded: false,
236
- returning: null
557
+ returning: null,
558
+ names: columnNamesOf(model) ?? void 0
237
559
  },
238
560
  model
239
561
  );
@@ -270,7 +592,8 @@ function del(model) {
270
592
  table: model.tablename,
271
593
  where: void 0,
272
594
  guarded: false,
273
- returning: null
595
+ returning: null,
596
+ names: columnNamesOf(model) ?? void 0
274
597
  },
275
598
  model
276
599
  );
@@ -373,165 +696,20 @@ function detectDialect(url) {
373
696
  return parseDatabaseUrl(url).dialect;
374
697
  }
375
698
 
376
- // src/serialize.ts
377
- var ValidationError = class extends Error {
378
- constructor(table, issues) {
379
- super(`Validation failed for ${table}:
380
- - ${issues.join("\n - ")}`);
381
- this.table = table;
382
- this.issues = issues;
383
- this.name = "ValidationError";
384
- }
385
- table;
386
- issues;
387
- };
388
- function toBase64(bytes) {
389
- if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
390
- let binary = "";
391
- for (const byte of bytes) binary += String.fromCharCode(byte);
392
- return btoa(binary);
393
- }
394
- function fromBase64(value) {
395
- if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64"));
396
- const binary = atob(value);
397
- const bytes = new Uint8Array(binary.length);
398
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
399
- return bytes;
400
- }
401
- function encodeValue(column2, value) {
402
- if (value === null || value === void 0) return null;
403
- switch (column2.type.kind) {
404
- case "bigint":
405
- return typeof value === "bigint" ? value.toString() : value;
406
- case "date":
407
- case "datetime":
408
- case "timestamp":
409
- return value instanceof Date ? value.toISOString() : value;
410
- case "blob":
411
- return value instanceof Uint8Array ? toBase64(value) : value;
412
- default:
413
- return value;
414
- }
415
- }
416
- function decodeValue(column2, value) {
417
- if (value === null || value === void 0) return null;
418
- switch (column2.type.kind) {
419
- case "bigint":
420
- return typeof value === "bigint" ? value : BigInt(value);
421
- case "date":
422
- case "datetime":
423
- case "timestamp":
424
- return value instanceof Date ? value : new Date(value);
425
- case "blob":
426
- return value instanceof Uint8Array ? value : fromBase64(value);
427
- case "json":
428
- return typeof value === "string" ? JSON.parse(value) : value;
429
- case "numeric":
430
- return typeof value === "string" ? value : String(value);
431
- case "boolean":
432
- return typeof value === "boolean" ? value : value === 1 || value === "true";
433
- case "smallint":
434
- case "integer":
435
- case "real":
436
- case "double":
437
- return typeof value === "number" ? value : Number(value);
438
- default:
439
- return value;
440
- }
441
- }
442
- function toDict(model, row) {
443
- const columns = columnsOf(model);
444
- const out = {};
445
- for (const name of Object.keys(columns)) {
446
- out[name] = row[name] ?? null;
447
- }
448
- return out;
449
- }
450
- function toJSON(model, row) {
451
- const columns = columnsOf(model);
452
- const out = {};
453
- for (const [name, col] of Object.entries(columns)) {
454
- out[name] = encodeValue(col, row[name] ?? null);
455
- }
456
- return out;
457
- }
458
- function stringify(model, row) {
459
- return JSON.stringify(toJSON(model, row));
460
- }
461
- function fromDict(model, data) {
462
- const columns = columnsOf(model);
463
- const out = {};
464
- const issues = [];
465
- for (const [name, col] of Object.entries(columns)) {
466
- const present = name in data && data[name] !== void 0 && data[name] !== null;
467
- if (!present) {
468
- const required = col.flags.notNull && !col.flags.hasDefault;
469
- if (required) {
470
- issues.push(`missing required column "${name}"`);
471
- continue;
472
- }
473
- out[name] = null;
474
- continue;
475
- }
476
- try {
477
- out[name] = decodeValue(col, data[name]);
478
- } catch (error) {
479
- issues.push(`column "${name}": ${error.message}`);
480
- }
481
- }
482
- if (issues.length > 0) {
483
- throw new ValidationError(model.tablename, issues);
699
+ // src/expressions.ts
700
+ function renderPortableToken(token, dialect) {
701
+ switch (token) {
702
+ case "now":
703
+ return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
704
+ case "current_date":
705
+ return "CURRENT_DATE";
706
+ case "current_time":
707
+ return "CURRENT_TIME";
708
+ case "uuidv4":
709
+ if (dialect === "postgresql") return "gen_random_uuid()";
710
+ if (dialect === "mysql") return "(UUID())";
711
+ return "(lower(hex(randomblob(16))))";
484
712
  }
485
- return out;
486
- }
487
- function parse(model, json) {
488
- return fromDict(model, JSON.parse(json));
489
- }
490
- function decoderForKind(kind) {
491
- switch (kind) {
492
- case "bigint":
493
- return (v) => v == null ? null : typeof v === "bigint" ? v : BigInt(v);
494
- case "date":
495
- case "datetime":
496
- case "timestamp":
497
- return (v) => v == null ? null : v instanceof Date ? v : new Date(v);
498
- case "blob":
499
- return (v) => v == null ? null : v instanceof Uint8Array ? v : fromBase64(v);
500
- case "json":
501
- return (v) => v == null ? null : typeof v === "string" ? JSON.parse(v) : v;
502
- case "numeric":
503
- return (v) => v == null ? null : typeof v === "string" ? v : String(v);
504
- case "boolean":
505
- return (v) => v == null ? null : typeof v === "boolean" ? v : v === 1 || v === "true";
506
- case "smallint":
507
- case "integer":
508
- case "real":
509
- case "double":
510
- return (v) => v == null ? null : typeof v === "number" ? v : Number(v);
511
- default:
512
- return null;
513
- }
514
- }
515
- var decoderCache = /* @__PURE__ */ new WeakMap();
516
- function decodersFor(model) {
517
- const cached = decoderCache.get(model);
518
- if (cached) return cached;
519
- const map = /* @__PURE__ */ new Map();
520
- for (const [name, col] of Object.entries(columnsOf(model))) {
521
- const decoder = decoderForKind(col.type.kind);
522
- if (decoder) map.set(name, decoder);
523
- }
524
- decoderCache.set(model, map);
525
- return map;
526
- }
527
- function coerceRow(model, raw) {
528
- const decoders = decodersFor(model);
529
- const out = {};
530
- for (const name of Object.keys(raw)) {
531
- const decode2 = decoders.get(name);
532
- out[name] = decode2 ? decode2(raw[name]) : raw[name];
533
- }
534
- return out;
535
713
  }
536
714
 
537
715
  // src/dialect.ts
@@ -554,6 +732,20 @@ var Params = class {
554
732
  return this.placeholder(this.values.length);
555
733
  }
556
734
  };
735
+ function insertHasExpression(node) {
736
+ for (const row of node.values) {
737
+ for (const value of Object.values(row)) {
738
+ if (isSqlExpression(value)) return true;
739
+ }
740
+ }
741
+ const update2 = node.onConflict?.update;
742
+ if (update2 && update2 !== "nothing") {
743
+ for (const value of Object.values(update2)) {
744
+ if (isSqlExpression(value)) return true;
745
+ }
746
+ }
747
+ return false;
748
+ }
557
749
  var BaseDialect = class _BaseDialect {
558
750
  /**
559
751
  * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
@@ -563,6 +755,21 @@ var BaseDialect = class _BaseDialect {
563
755
  static insertTemplates = /* @__PURE__ */ new Map();
564
756
  /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
565
757
  static quotedIds = /* @__PURE__ */ new Map();
758
+ /**
759
+ * The SQL operator for an array containment/overlap test.
760
+ *
761
+ * Only PostgreSQL has native arrays; the other dialects throw rather than
762
+ * emitting an operator that means something else there.
763
+ *
764
+ * @param op The array operator name.
765
+ * @returns The SQL operator text.
766
+ * @throws Error On a dialect without native array support.
767
+ */
768
+ arrayOperator(op) {
769
+ throw new Error(
770
+ `The "${op}" operator needs native array support, which ${this.name} does not have.`
771
+ );
772
+ }
566
773
  /**
567
774
  * Quote an identifier (column/table) for the active dialect.
568
775
  *
@@ -601,51 +808,175 @@ var BaseDialect = class _BaseDialect {
601
808
  }
602
809
  return { sql: sql2, params: params.values };
603
810
  }
604
- /** Render a qualified `alias.column` ref as `"alias"."column"`. */
605
- qualify(ref) {
811
+ /**
812
+ * Render a qualified `alias.column` ref as `"alias"."column"`, translating the
813
+ * property name to the real column name for that alias's model.
814
+ *
815
+ * @param ref The `alias.property` reference (a bare name is left unqualified).
816
+ * @param names The node's per-alias name maps, if any source renames columns.
817
+ * @returns The quoted, qualified identifier.
818
+ */
819
+ qualify(ref, names) {
606
820
  const dot = ref.indexOf(".");
607
821
  if (dot === -1) return this.quoteId(ref);
608
- return `${this.quoteId(ref.slice(0, dot))}.${this.quoteId(ref.slice(dot + 1))}`;
822
+ const alias = ref.slice(0, dot);
823
+ const prop = ref.slice(dot + 1);
824
+ return `${this.quoteId(alias)}.${this.columnId(prop, names?.[alias])}`;
825
+ }
826
+ /**
827
+ * Quote a column identifier, translating the model property name to the real
828
+ * database column name first.
829
+ *
830
+ * `names` is `undefined` for a model that renames nothing — the overwhelmingly
831
+ * common case — so this stays a single lookup plus the memoized quote.
832
+ *
833
+ * @param prop The model property name as written in the builder.
834
+ * @param names The node's property → column map, if any.
835
+ * @returns The quoted database identifier.
836
+ */
837
+ columnId(prop, names) {
838
+ return this.quoteId(names?.[prop] ?? prop);
839
+ }
840
+ /**
841
+ * Render a {@link SqlExpression} inline, binding the parameters it carries.
842
+ *
843
+ * This is what keeps `set({ attempts: sql.raw("attempts + 1") })` an expression
844
+ * instead of a bound object: the fragment goes into the statement text, and
845
+ * only a `sql.expr` template's interpolations become parameters.
846
+ *
847
+ * @param expr The branded expression.
848
+ * @param params The parameter collector for the statement being compiled.
849
+ * @returns The SQL text of the expression.
850
+ */
851
+ renderExpression(expr, params) {
852
+ const token = expr.expression;
853
+ if (typeof token === "string") return renderPortableToken(token, this.name);
854
+ if ("raw" in token) return token.raw;
855
+ const parts = token.parts;
856
+ let sql2 = parts[0] ?? "";
857
+ for (let i = 1; i < parts.length; i++) {
858
+ sql2 += `${params.bind(expr.params[i - 1])}${parts[i]}`;
859
+ }
860
+ return sql2;
861
+ }
862
+ /** Render one write value: a SQL expression inline, anything else as a parameter. */
863
+ renderValue(value, params) {
864
+ return isSqlExpression(value) ? this.renderExpression(value, params) : params.bind(value);
865
+ }
866
+ /**
867
+ * Render a row-level locking clause (`FOR UPDATE ...`).
868
+ *
869
+ * Standard on PostgreSQL and MySQL 8.0+; SQLite overrides it to throw.
870
+ *
871
+ * @param lock The locking clause from the node.
872
+ * @returns The SQL text, leading space included.
873
+ */
874
+ renderLock(lock) {
875
+ const strength = lock.strength === "update" ? "FOR UPDATE" : "FOR SHARE";
876
+ const of = lock.of.length > 0 ? ` OF ${lock.of.map((t) => this.quoteId(t)).join(", ")}` : "";
877
+ const wait = lock.wait === "skipLocked" ? " SKIP LOCKED" : lock.wait === "noWait" ? " NOWAIT" : "";
878
+ return ` ${strength}${of}${wait}`;
609
879
  }
610
880
  // ---- statements -------------------------------------------------------
611
881
  compileSelect(node, params) {
882
+ const names = node.names;
612
883
  let cols;
613
884
  if (node.aggregates.length > 0) {
614
- const groupSel = node.groupBy.map((c) => this.quoteId(c));
885
+ const groupSel = node.groupBy.map((c) => this.columnId(c, names));
615
886
  const aggSel = node.aggregates.map((a) => {
616
- const inner = a.column === "*" ? "*" : this.quoteId(a.column);
887
+ const inner = a.column === "*" ? "*" : this.columnId(a.column, names);
617
888
  return `${a.fn.toUpperCase()}(${inner}) AS ${this.quoteId(a.alias)}`;
618
889
  });
619
890
  cols = [...groupSel, ...aggSel].join(", ");
620
891
  } else {
621
- cols = node.columns === "*" ? "*" : node.columns.map((c) => this.quoteId(c)).join(", ");
892
+ cols = node.columns === "*" ? "*" : node.columns.map((c) => this.columnId(c, names)).join(", ");
622
893
  }
623
894
  let sql2 = `SELECT ${node.distinct ? "DISTINCT " : ""}${cols} FROM ${this.quoteId(node.table)}`;
624
- const where = this.compileCondition(node.where, params, (k) => this.quoteId(k));
895
+ const where = this.compileCondition(
896
+ node.where,
897
+ params,
898
+ (k) => this.columnId(k, names)
899
+ );
625
900
  if (where) sql2 += ` WHERE ${where}`;
626
901
  if (node.groupBy.length > 0) {
627
- sql2 += ` GROUP BY ${node.groupBy.map((c) => this.quoteId(c)).join(", ")}`;
902
+ sql2 += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
628
903
  }
629
904
  if (node.orderBy.length > 0) {
630
905
  const terms = node.orderBy.map(
631
- (t) => `${this.quoteId(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`
906
+ (t) => `${this.columnId(t.column, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
632
907
  ).join(", ");
633
908
  sql2 += ` ORDER BY ${terms}`;
634
909
  }
635
910
  if (node.limit !== void 0) sql2 += ` LIMIT ${params.bind(node.limit)}`;
636
911
  if (node.offset !== void 0) sql2 += ` OFFSET ${params.bind(node.offset)}`;
912
+ if (node.lock) {
913
+ if (node.distinct || node.groupBy.length > 0 || node.aggregates.length > 0) {
914
+ throw new Error(
915
+ "FOR UPDATE / FOR SHARE cannot be combined with DISTINCT or an aggregate query \u2014 lock the underlying rows in a separate SELECT."
916
+ );
917
+ }
918
+ sql2 += this.renderLock(node.lock);
919
+ }
637
920
  return sql2;
638
921
  }
922
+ /**
923
+ * Compile an INSERT.
924
+ *
925
+ * Takes the cached fast path only when the statement text is a pure function of
926
+ * its structure. A SQL expression among the values, or a conflict predicate,
927
+ * makes the text depend on the values themselves — those compile uncached, in
928
+ * SQL order, so placeholder positions stay correct.
929
+ */
639
930
  compileInsert(node, params) {
640
931
  const columns = node.values.length > 0 ? Object.keys(node.values[0]) : [];
932
+ const conflict = node.onConflict;
933
+ const cacheable = conflict?.targetWhere === void 0 && conflict?.updateWhere === void 0 && !insertHasExpression(node);
934
+ if (!cacheable) return this.compileInsertDirect(node, columns, params);
641
935
  for (const row of node.values) {
642
936
  for (const c of columns) params.bind(row[c] ?? null);
643
937
  }
644
- const conflictCols = node.onConflict && node.onConflict.update !== "nothing" ? Object.keys(node.onConflict.update) : [];
938
+ const conflictCols = conflict && conflict.update !== "nothing" ? Object.keys(conflict.update) : [];
645
939
  for (const c of conflictCols) {
646
- params.bind((node.onConflict?.update)[c]);
940
+ params.bind((conflict?.update)[c]);
647
941
  }
648
- return this.insertTemplate(node, columns, conflictCols);
942
+ return this.insertTemplate(node, columns, conflictCols, params);
943
+ }
944
+ /**
945
+ * Compile an INSERT without the template cache, rendering clauses in statement
946
+ * order so every parameter is bound at the position it appears.
947
+ *
948
+ * @param node The insert node.
949
+ * @param columns The column keys shared by every row.
950
+ * @param params The parameter collector.
951
+ * @returns The SQL text.
952
+ */
953
+ compileInsertDirect(node, columns, params) {
954
+ const names = node.names;
955
+ const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
956
+ const rowsSql = node.values.map((row) => {
957
+ const cells = columns.map(
958
+ (c) => this.renderValue(row[c] ?? null, params)
959
+ );
960
+ return `(${cells.join(", ")})`;
961
+ }).join(", ");
962
+ let sql2 = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
963
+ if (node.onConflict) {
964
+ const update2 = node.onConflict.update;
965
+ const conflictCols = update2 === "nothing" ? [] : Object.keys(update2);
966
+ let cursor = 0;
967
+ sql2 += this.renderConflict(
968
+ node.onConflict,
969
+ conflictCols,
970
+ () => {
971
+ const key = conflictCols[cursor++];
972
+ return this.renderValue(update2[key], params);
973
+ },
974
+ names,
975
+ params
976
+ );
977
+ }
978
+ sql2 += this.compileReturning(node.returning, names);
979
+ return sql2;
649
980
  }
650
981
  /**
651
982
  * The INSERT SQL template for a given structure, cached across calls.
@@ -655,13 +986,14 @@ var BaseDialect = class _BaseDialect {
655
986
  * deterministic from the counts (a fresh statement always starts binding at 1).
656
987
  * So a per-row insert loop compiles the string once and reuses it every row.
657
988
  */
658
- insertTemplate(node, columns, conflictCols) {
989
+ insertTemplate(node, columns, conflictCols, params) {
659
990
  const returningKey = node.returning === null ? "" : node.returning === "*" ? "*" : node.returning.join(",");
660
991
  const conflictKey = node.onConflict ? `${node.onConflict.target.join(",")}>${node.onConflict.update === "nothing" ? "nothing" : conflictCols.join(",")}` : "";
661
992
  const key = `${this.name}|${node.table}|${columns.join(",")}|${node.values.length}|${returningKey}|${conflictKey}`;
662
993
  const cached = _BaseDialect.insertTemplates.get(key);
663
994
  if (cached !== void 0) return cached;
664
- const colSql = columns.map((c) => this.quoteId(c)).join(", ");
995
+ const names = node.names;
996
+ const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
665
997
  let position = 0;
666
998
  const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
667
999
  let sql2 = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
@@ -669,57 +1001,91 @@ var BaseDialect = class _BaseDialect {
669
1001
  sql2 += this.renderConflict(
670
1002
  node.onConflict,
671
1003
  conflictCols,
672
- () => this.placeholder(++position)
1004
+ () => this.placeholder(++position),
1005
+ names,
1006
+ params
673
1007
  );
674
1008
  }
675
- sql2 += this.compileReturning(node.returning);
1009
+ sql2 += this.compileReturning(node.returning, names);
676
1010
  _BaseDialect.insertTemplates.set(key, sql2);
677
1011
  return sql2;
678
1012
  }
679
1013
  /**
680
1014
  * Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
681
- * `ON CONFLICT (...) DO NOTHING | DO UPDATE SET ...`; MySQL overrides this.
1015
+ * `ON CONFLICT (...) [WHERE predicate] DO NOTHING | DO UPDATE SET ... [WHERE ...]`;
1016
+ * MySQL overrides this.
1017
+ *
1018
+ * The index predicate is rendered before the `DO UPDATE` assignments because
1019
+ * that is where it sits in the statement, so its parameters bind first.
682
1020
  *
683
1021
  * @param onConflict The conflict clause from the node.
684
1022
  * @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
685
- * @param nextPlaceholder Yields the next positional placeholder (advances the count).
1023
+ * @param nextValue Yields the SQL for the next `DO UPDATE` assignment value.
1024
+ * @param names The node's property → column map, if any.
1025
+ * @param params The parameter collector, for the predicates.
1026
+ * @returns The SQL text, leading space included.
686
1027
  */
687
- renderConflict(onConflict, conflictCols, nextPlaceholder) {
688
- const target = onConflict.target.map((c) => this.quoteId(c)).join(", ");
689
- if (onConflict.update === "nothing") return ` ON CONFLICT (${target}) DO NOTHING`;
690
- const assignments = conflictCols.map((c) => `${this.quoteId(c)} = ${nextPlaceholder()}`).join(", ");
691
- return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
1028
+ renderConflict(onConflict, conflictCols, nextValue, names, params) {
1029
+ const idFor = (key) => this.columnId(key, names);
1030
+ const target = onConflict.target.map(idFor).join(", ");
1031
+ const indexWhere = this.compileCondition(onConflict.targetWhere, params, idFor);
1032
+ const targetSql = indexWhere ? `(${target}) WHERE ${indexWhere}` : `(${target})`;
1033
+ if (onConflict.update === "nothing") return ` ON CONFLICT ${targetSql} DO NOTHING`;
1034
+ const assignments = conflictCols.map((c) => `${idFor(c)} = ${nextValue()}`).join(", ");
1035
+ let sql2 = ` ON CONFLICT ${targetSql} DO UPDATE SET ${assignments}`;
1036
+ const updateWhere = this.compileCondition(onConflict.updateWhere, params, idFor);
1037
+ if (updateWhere) sql2 += ` WHERE ${updateWhere}`;
1038
+ return sql2;
692
1039
  }
693
1040
  compileUpdate(node, params) {
694
- const sets = Object.entries(node.set).map(([col, value]) => `${this.quoteId(col)} = ${params.bind(value)}`).join(", ");
1041
+ const names = node.names;
1042
+ const sets = Object.entries(node.set).map(
1043
+ ([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
1044
+ ).join(", ");
695
1045
  let sql2 = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
696
- const where = this.compileCondition(node.where, params, (k) => this.quoteId(k));
1046
+ const where = this.compileCondition(
1047
+ node.where,
1048
+ params,
1049
+ (k) => this.columnId(k, names)
1050
+ );
697
1051
  if (where) sql2 += ` WHERE ${where}`;
698
- sql2 += this.compileReturning(node.returning);
1052
+ sql2 += this.compileReturning(node.returning, names);
699
1053
  return sql2;
700
1054
  }
701
1055
  compileDelete(node, params) {
1056
+ const names = node.names;
702
1057
  let sql2 = `DELETE FROM ${this.quoteId(node.table)}`;
703
- const where = this.compileCondition(node.where, params, (k) => this.quoteId(k));
1058
+ const where = this.compileCondition(
1059
+ node.where,
1060
+ params,
1061
+ (k) => this.columnId(k, names)
1062
+ );
704
1063
  if (where) sql2 += ` WHERE ${where}`;
705
- sql2 += this.compileReturning(node.returning);
1064
+ sql2 += this.compileReturning(node.returning, names);
706
1065
  return sql2;
707
1066
  }
708
1067
  compileJoin(node, params) {
1068
+ const names = node.names;
709
1069
  const cols = node.selections.map((s) => {
710
1070
  const ref = `${s.alias}.${s.column}`;
711
- return `${this.qualify(ref)} AS ${this.quoteId(ref)}`;
1071
+ return `${this.qualify(ref, names)} AS ${this.quoteId(ref)}`;
712
1072
  }).join(", ");
713
1073
  let sql2 = `SELECT ${cols} FROM ${this.quoteId(node.base.table)} AS ${this.quoteId(node.base.alias)}`;
714
1074
  for (const j of node.joins) {
715
1075
  const kw = j.kind === "left" ? "LEFT JOIN" : "INNER JOIN";
716
- const on = j.on.map(([l, r]) => `${this.qualify(l)} = ${this.qualify(r)}`).join(" AND ");
1076
+ const on = j.on.map(([l, r]) => `${this.qualify(l, names)} = ${this.qualify(r, names)}`).join(" AND ");
717
1077
  sql2 += ` ${kw} ${this.quoteId(j.table)} AS ${this.quoteId(j.alias)} ON ${on}`;
718
1078
  }
719
- const where = this.compileCondition(node.where, params, (k) => this.qualify(k));
1079
+ const where = this.compileCondition(
1080
+ node.where,
1081
+ params,
1082
+ (k) => this.qualify(k, names)
1083
+ );
720
1084
  if (where) sql2 += ` WHERE ${where}`;
721
1085
  if (node.orderBy.length > 0) {
722
- const terms = node.orderBy.map((t) => `${this.qualify(t.ref)} ${t.direction === "desc" ? "DESC" : "ASC"}`).join(", ");
1086
+ const terms = node.orderBy.map(
1087
+ (t) => `${this.qualify(t.ref, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
1088
+ ).join(", ");
723
1089
  sql2 += ` ORDER BY ${terms}`;
724
1090
  }
725
1091
  if (node.limit !== void 0) sql2 += ` LIMIT ${params.bind(node.limit)}`;
@@ -727,10 +1093,10 @@ var BaseDialect = class _BaseDialect {
727
1093
  return sql2;
728
1094
  }
729
1095
  // ---- clauses ----------------------------------------------------------
730
- compileReturning(returning) {
1096
+ compileReturning(returning, names) {
731
1097
  if (returning === null) return "";
732
1098
  if (returning === "*") return " RETURNING *";
733
- return ` RETURNING ${returning.map((c) => this.quoteId(c)).join(", ")}`;
1099
+ return ` RETURNING ${returning.map((c) => this.columnId(c, names)).join(", ")}`;
734
1100
  }
735
1101
  /**
736
1102
  * Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
@@ -787,6 +1153,14 @@ var BaseDialect = class _BaseDialect {
787
1153
  return `${id} LIKE ${params.bind(operand)}`;
788
1154
  case "ilike":
789
1155
  return this.ilike(id, params.bind(operand));
1156
+ case "ieq":
1157
+ return operand === null ? `${id} IS NULL` : `lower(${id}) = lower(${params.bind(operand)})`;
1158
+ case "contains":
1159
+ return `${id} ${this.arrayOperator("contains")} ${params.bind(operand)}`;
1160
+ case "containedBy":
1161
+ return `${id} ${this.arrayOperator("containedBy")} ${params.bind(operand)}`;
1162
+ case "overlaps":
1163
+ return `${id} ${this.arrayOperator("overlaps")} ${params.bind(operand)}`;
790
1164
  case "in":
791
1165
  return this.compileIn(id, operand, params, false);
792
1166
  case "notIn":
@@ -817,6 +1191,16 @@ var SqliteDialect = class extends BaseDialect {
817
1191
  ilike(column2, param) {
818
1192
  return `${column2} LIKE ${param}`;
819
1193
  }
1194
+ /**
1195
+ * SQLite has no row-level locking, so a lock request is an error rather than a
1196
+ * silently unlocked `SELECT` — a lock that does not exist only shows up as
1197
+ * duplicated work under production concurrency.
1198
+ */
1199
+ renderLock() {
1200
+ throw new Error(
1201
+ "SQLite has no row-level locking \u2014 FOR UPDATE / FOR SHARE is unsupported. Serialize the claim inside a transaction instead."
1202
+ );
1203
+ }
820
1204
  };
821
1205
  var PostgresDialect = class extends BaseDialect {
822
1206
  name = "postgresql";
@@ -826,6 +1210,11 @@ var PostgresDialect = class extends BaseDialect {
826
1210
  ilike(column2, param) {
827
1211
  return `${column2} ILIKE ${param}`;
828
1212
  }
1213
+ arrayOperator(op) {
1214
+ if (op === "contains") return "@>";
1215
+ if (op === "containedBy") return "<@";
1216
+ return "&&";
1217
+ }
829
1218
  };
830
1219
  var mysqlQuotedIds = /* @__PURE__ */ new Map();
831
1220
  var MysqlDialect = class extends BaseDialect {
@@ -843,12 +1232,17 @@ var MysqlDialect = class extends BaseDialect {
843
1232
  mysqlQuotedIds.set(name, quoted);
844
1233
  return quoted;
845
1234
  }
846
- renderConflict(onConflict, conflictCols, nextPlaceholder) {
1235
+ renderConflict(onConflict, conflictCols, nextValue, names) {
1236
+ if (onConflict.targetWhere || onConflict.updateWhere) {
1237
+ throw new Error(
1238
+ "MySQL's ON DUPLICATE KEY UPDATE has no conflict-target predicate \u2014 a partial unique index is PostgreSQL/SQLite only."
1239
+ );
1240
+ }
847
1241
  if (onConflict.update === "nothing") {
848
- const col = this.quoteId(onConflict.target[0] ?? "id");
1242
+ const col = this.columnId(onConflict.target[0] ?? "id", names);
849
1243
  return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
850
1244
  }
851
- const assignments = conflictCols.map((c) => `${this.quoteId(c)} = ${nextPlaceholder()}`).join(", ");
1245
+ const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
852
1246
  return ` ON DUPLICATE KEY UPDATE ${assignments}`;
853
1247
  }
854
1248
  compileReturning(returning) {
@@ -873,6 +1267,11 @@ function getDialect(name) {
873
1267
  function selectionsFor(alias, model) {
874
1268
  return Object.keys(columnsOf(model)).map((column2) => ({ alias, column: column2 }));
875
1269
  }
1270
+ function withAliasNames(current, alias, model) {
1271
+ const names = columnNamesOf(model);
1272
+ if (!names) return current;
1273
+ return { ...current ?? {}, [alias]: names };
1274
+ }
876
1275
  var JoinBuilder = class _JoinBuilder {
877
1276
  constructor(node, sources) {
878
1277
  this.node = node;
@@ -885,7 +1284,8 @@ var JoinBuilder = class _JoinBuilder {
885
1284
  {
886
1285
  ...this.node,
887
1286
  joins: [...this.node.joins, clause],
888
- selections: [...this.node.selections, ...selectionsFor(clause.alias, model)]
1287
+ selections: [...this.node.selections, ...selectionsFor(clause.alias, model)],
1288
+ names: withAliasNames(this.node.names, clause.alias, model)
889
1289
  },
890
1290
  { ...this.sources, [clause.alias]: model }
891
1291
  );
@@ -946,7 +1346,8 @@ function join(model, alias) {
946
1346
  where: void 0,
947
1347
  orderBy: [],
948
1348
  limit: void 0,
949
- offset: void 0
1349
+ offset: void 0,
1350
+ names: withAliasNames(void 0, alias, model)
950
1351
  },
951
1352
  { [alias]: model }
952
1353
  );
@@ -1259,11 +1660,12 @@ function splitJoinRow(node, sources, raw) {
1259
1660
  const out = {};
1260
1661
  for (const [alias, model] of Object.entries(sources)) {
1261
1662
  const sub = {};
1663
+ const names = columnNamesOf(model);
1262
1664
  let allNull = true;
1263
1665
  for (const colName of Object.keys(columnsOf(model))) {
1264
1666
  const value = raw[`${alias}.${colName}`];
1265
1667
  if (value !== null && value !== void 0) allNull = false;
1266
- sub[colName] = value;
1668
+ sub[names?.[colName] ?? colName] = value;
1267
1669
  }
1268
1670
  out[alias] = leftAliases.has(alias) && allNull ? null : coerceRow(model, sub);
1269
1671
  }
@@ -1385,6 +1787,13 @@ var AsyncResult = class {
1385
1787
  }
1386
1788
  };
1387
1789
  var savepointCounter = 0;
1790
+ function assertRawParams(params) {
1791
+ if (!Array.isArray(params)) {
1792
+ throw new TypeError(
1793
+ "session.raw(sql, params) takes an array of bound parameters \u2014 never interpolate values into the SQL string."
1794
+ );
1795
+ }
1796
+ }
1388
1797
  var SyncSession = class {
1389
1798
  constructor(driver, dialect, logger) {
1390
1799
  this.driver = driver;
@@ -1403,6 +1812,44 @@ var SyncSession = class {
1403
1812
  throw new QueryExecutionError(error, sql2, params);
1404
1813
  }
1405
1814
  }
1815
+ /**
1816
+ * Run a raw, parameterized SQL statement (synchronous) — the runtime counterpart of the
1817
+ * migrations' `Op.execute`.
1818
+ *
1819
+ * A query builder never covers all of SQL, and without an escape hatch a single
1820
+ * unsupported query forces a whole second database stack alongside this one. Use
1821
+ * it for what the builder cannot yet express, and keep everything else typed.
1822
+ *
1823
+ * The statement goes through the same path as a compiled one: it is logged via
1824
+ * `onQuery`, wrapped in {@link QueryExecutionError} on failure, and runs on the
1825
+ * reserved connection inside `transaction()`.
1826
+ *
1827
+ * @param sql The statement text. Placeholders only (`$1` / `?` per dialect) —
1828
+ * never interpolate a value into this string.
1829
+ * @param params The bound parameters, in placeholder order.
1830
+ * @param options Pass `as` to coerce the returned rows with a model's column
1831
+ * types (and its column-name mapping).
1832
+ * @returns The result view over the returned rows.
1833
+ * @throws Error When `params` is not an array — the guard against calling this
1834
+ * with an interpolated string and no parameters by mistake.
1835
+ *
1836
+ * @example
1837
+ * ```ts
1838
+ * const claimed = await session.raw<OutboundRow>(
1839
+ * `UPDATE outbound_messages SET status = 'sending'
1840
+ * WHERE id = ANY($1) RETURNING *`,
1841
+ * [ids],
1842
+ * { as: Outbound },
1843
+ * ).all();
1844
+ * ```
1845
+ */
1846
+ raw(sql2, params = [], options) {
1847
+ assertRawParams(params);
1848
+ const result = this.exec(sql2, params);
1849
+ const model = options?.as;
1850
+ const rows = model ? result.rows.map((row) => coerceRow(model, row)) : result.rows;
1851
+ return new SyncResult(rows, result.changes);
1852
+ }
1406
1853
  /** Compile, run, and coerce a builder into a result. */
1407
1854
  execute(builder) {
1408
1855
  const node = builder.node;
@@ -1485,6 +1932,46 @@ var AsyncSession = class _AsyncSession {
1485
1932
  throw new QueryExecutionError(error, sql2, params);
1486
1933
  }
1487
1934
  }
1935
+ /**
1936
+ * Run a raw, parameterized SQL statement — the runtime counterpart of the
1937
+ * migrations' `Op.execute`.
1938
+ *
1939
+ * A query builder never covers all of SQL, and without an escape hatch a single
1940
+ * unsupported query forces a whole second database stack alongside this one. Use
1941
+ * it for what the builder cannot yet express, and keep everything else typed.
1942
+ *
1943
+ * The statement goes through the same path as a compiled one: it is logged via
1944
+ * `onQuery`, wrapped in {@link QueryExecutionError} on failure, and runs on the
1945
+ * reserved connection inside `transaction()`.
1946
+ *
1947
+ * @param sql The statement text. Placeholders only (`$1` / `?` per dialect) —
1948
+ * never interpolate a value into this string.
1949
+ * @param params The bound parameters, in placeholder order.
1950
+ * @param options Pass `as` to coerce the returned rows with a model's column
1951
+ * types (and its column-name mapping).
1952
+ * @returns The result view over the returned rows.
1953
+ * @throws Error When `params` is not an array — the guard against calling this
1954
+ * with an interpolated string and no parameters by mistake.
1955
+ *
1956
+ * @example
1957
+ * ```ts
1958
+ * const claimed = await session.raw<OutboundRow>(
1959
+ * `UPDATE outbound_messages SET status = 'sending'
1960
+ * WHERE id = ANY($1) RETURNING *`,
1961
+ * [ids],
1962
+ * { as: Outbound },
1963
+ * ).all();
1964
+ * ```
1965
+ */
1966
+ raw(sql2, params = [], options) {
1967
+ assertRawParams(params);
1968
+ const model = options?.as;
1969
+ const inner = this.exec(sql2, params).then((result) => {
1970
+ const rows = model ? result.rows.map((row) => coerceRow(model, row)) : result.rows;
1971
+ return new SyncResult(rows, result.changes);
1972
+ });
1973
+ return new AsyncResult(inner);
1974
+ }
1488
1975
  execute(builder) {
1489
1976
  const node = builder.node;
1490
1977
  const { sql: sql2, params } = this.dialect.compile(node);
@@ -1754,24 +2241,58 @@ var DEFAULT_FLAGS = {
1754
2241
  hasDefault: false,
1755
2242
  unique: false
1756
2243
  };
2244
+ var EXPRESSION = /* @__PURE__ */ Symbol.for("tempest-db-js.expression");
2245
+ function expression(token, params = []) {
2246
+ return { [EXPRESSION]: true, kind: "expression", expression: token, params };
2247
+ }
2248
+ function isSqlExpression(value) {
2249
+ return typeof value === "object" && value !== null && value[EXPRESSION] === true;
2250
+ }
1757
2251
  var sql = {
1758
2252
  /** Current timestamp at insert (`CURRENT_TIMESTAMP` / `now()`). */
1759
- now: () => ({ kind: "expression", expression: "now" }),
2253
+ now: () => expression("now"),
1760
2254
  /** Current date. */
1761
- currentDate: () => ({ kind: "expression", expression: "current_date" }),
2255
+ currentDate: () => expression("current_date"),
1762
2256
  /** Current time. */
1763
- currentTime: () => ({ kind: "expression", expression: "current_time" }),
2257
+ currentTime: () => expression("current_time"),
1764
2258
  /** A freshly generated UUID v4 (`gen_random_uuid()` / portable fallback). */
1765
- uuidv4: () => ({ kind: "expression", expression: "uuidv4" }),
1766
- /** Escape hatch: a verbatim SQL expression rendered as-is. */
1767
- raw: (expression) => ({
1768
- kind: "expression",
1769
- expression: { raw: expression }
1770
- })
2259
+ uuidv4: () => expression("uuidv4"),
2260
+ /**
2261
+ * Escape hatch: a verbatim SQL expression rendered as-is, with no parameters.
2262
+ *
2263
+ * The fragment is interpolated into the statement untouched, so it must never
2264
+ * carry user input — use {@link sql.expr} when a value has to be bound.
2265
+ *
2266
+ * @param fragment The SQL text (e.g. `"attempts + 1"`).
2267
+ * @returns The expression, usable as a default and as a write value.
2268
+ */
2269
+ raw: (fragment) => expression({ raw: fragment }),
2270
+ /**
2271
+ * A parameterized SQL expression, written as a tagged template. Static text is
2272
+ * SQL; every `${...}` interpolation becomes a bound parameter, so the fragment
2273
+ * is injection-safe by construction.
2274
+ *
2275
+ * Cannot be used as a column default — a `DEFAULT` clause has nowhere to bind
2276
+ * parameters; use {@link sql.raw} there.
2277
+ *
2278
+ * @param parts The static SQL segments supplied by the template tag.
2279
+ * @param values The interpolated values, bound in order.
2280
+ * @returns The expression, usable as a write value.
2281
+ *
2282
+ * @example
2283
+ * ```ts
2284
+ * update(Account).set({ balance: sql.expr`balance - ${amount}` }).where({ id });
2285
+ * // UPDATE "accounts" SET "balance" = balance - $1 WHERE "id" = $2
2286
+ * ```
2287
+ */
2288
+ expr: (parts, ...values) => expression({ parts: Array.from(parts) }, values)
1771
2289
  };
1772
2290
  function isDefaultValue(value) {
1773
2291
  return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
1774
2292
  }
2293
+ function bindsParameters(value) {
2294
+ return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
2295
+ }
1775
2296
  function parseReference(ref, options) {
1776
2297
  const dot = ref.lastIndexOf(".");
1777
2298
  if (dot <= 0 || dot === ref.length - 1) {
@@ -1785,48 +2306,71 @@ function parseReference(ref, options) {
1785
2306
  };
1786
2307
  }
1787
2308
  var Column = class _Column {
1788
- constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
2309
+ constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
1789
2310
  this.type = type;
1790
2311
  this.flags = flags;
1791
2312
  this.defaultValue = defaultValue;
1792
2313
  this.onUpdateValue = onUpdateValue;
1793
2314
  this.reference = reference;
2315
+ this.dbName = dbName;
1794
2316
  }
1795
2317
  type;
1796
2318
  flags;
1797
2319
  defaultValue;
1798
2320
  onUpdateValue;
1799
2321
  reference;
1800
- primaryKey() {
2322
+ dbName;
2323
+ /** Clone this column with one facet replaced, carrying every other over. */
2324
+ derive(patch) {
1801
2325
  return new _Column(
1802
2326
  this.type,
1803
- { ...this.flags, primaryKey: true, hasDefault: true },
1804
- this.defaultValue,
1805
- this.onUpdateValue,
1806
- this.reference
2327
+ patch.flags ?? this.flags,
2328
+ patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
2329
+ patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
2330
+ patch.reference !== void 0 ? patch.reference : this.reference,
2331
+ patch.dbName !== void 0 ? patch.dbName : this.dbName
1807
2332
  );
1808
2333
  }
2334
+ primaryKey() {
2335
+ return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
2336
+ }
1809
2337
  notNull() {
1810
- return new _Column(
1811
- this.type,
1812
- { ...this.flags, notNull: true },
1813
- this.defaultValue,
1814
- this.onUpdateValue,
1815
- this.reference
1816
- );
2338
+ return this.derive({ flags: { ...this.flags, notNull: true } });
1817
2339
  }
1818
2340
  /**
1819
2341
  * Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
1820
2342
  * `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
1821
2343
  */
1822
2344
  unique() {
1823
- return new _Column(
1824
- this.type,
1825
- { ...this.flags, unique: true },
1826
- this.defaultValue,
1827
- this.onUpdateValue,
1828
- this.reference
1829
- );
2345
+ return this.derive({ flags: { ...this.flags, unique: true } });
2346
+ }
2347
+ /**
2348
+ * Map this property to a differently-named database column, à la SQLAlchemy's
2349
+ * `mapped_column("consumer_name")` (Django's `db_column`, Prisma's `@map`).
2350
+ *
2351
+ * The override applies everywhere the name reaches SQL — select, insert,
2352
+ * update, delete, where, order by, group by, returning, conflict targets, the
2353
+ * migration IR and the drift check — while the TypeScript row keeps the
2354
+ * property name. Use it to keep a `snake_case` schema behind a `camelCase`
2355
+ * model; {@link Model.naming} does the same for a whole table at once.
2356
+ *
2357
+ * @param dbName The real column name in the database.
2358
+ * @returns A new column bound to that name.
2359
+ * @throws Error When `dbName` is empty.
2360
+ *
2361
+ * @example
2362
+ * ```ts
2363
+ * class ApiKey extends Model {
2364
+ * static tablename = "api_keys";
2365
+ * consumerName = column.text().name("consumer_name").notNull();
2366
+ * }
2367
+ * ```
2368
+ */
2369
+ name(dbName) {
2370
+ if (dbName.length === 0) {
2371
+ throw new Error("column.name() requires a non-empty database column name.");
2372
+ }
2373
+ return this.derive({ dbName });
1830
2374
  }
1831
2375
  /**
1832
2376
  * Declare a foreign-key reference to another table's column, à la SQLAlchemy's
@@ -1839,35 +2383,45 @@ var Column = class _Column {
1839
2383
  * @throws Error When `ref` is not a valid `"table.column"` string.
1840
2384
  */
1841
2385
  references(ref, options) {
1842
- return new _Column(
1843
- this.type,
1844
- this.flags,
1845
- this.defaultValue,
1846
- this.onUpdateValue,
1847
- parseReference(ref, options)
1848
- );
2386
+ return this.derive({ reference: parseReference(ref, options) });
1849
2387
  }
1850
2388
  /**
1851
2389
  * Set the insert-time default: a constant value of type `T`, or a portable
1852
2390
  * server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
2391
+ *
2392
+ * @param value The literal default, or a {@link sql} expression.
2393
+ * @returns A new column carrying the default.
2394
+ * @throws Error When given a `sql.expr` fragment — a `DEFAULT` clause has
2395
+ * nowhere to bind parameters; use `sql.raw()` for a verbatim expression.
1853
2396
  */
1854
2397
  default(value) {
1855
2398
  const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
1856
- return new _Column(
1857
- this.type,
1858
- { ...this.flags, hasDefault: true },
1859
- resolved,
1860
- this.onUpdateValue,
1861
- this.reference
1862
- );
2399
+ if (bindsParameters(resolved)) {
2400
+ throw new Error(
2401
+ "sql.expr`...` binds parameters and cannot be a column default \u2014 use sql.raw() for a verbatim DEFAULT expression."
2402
+ );
2403
+ }
2404
+ return this.derive({
2405
+ flags: { ...this.flags, hasDefault: true },
2406
+ defaultValue: resolved
2407
+ });
1863
2408
  }
1864
2409
  /**
1865
2410
  * Re-apply a value whenever the row is updated (e.g. an `updated_at` column
1866
2411
  * with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
2412
+ *
2413
+ * @param value The literal value, or a {@link sql} expression.
2414
+ * @returns A new column carrying the on-update value.
2415
+ * @throws Error When given a `sql.expr` fragment (see {@link Column.default}).
1867
2416
  */
1868
2417
  onUpdate(value) {
1869
2418
  const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
1870
- return new _Column(this.type, this.flags, this.defaultValue, resolved, this.reference);
2419
+ if (bindsParameters(resolved)) {
2420
+ throw new Error(
2421
+ "sql.expr`...` binds parameters and cannot be an onUpdate default \u2014 use sql.raw() for a verbatim expression."
2422
+ );
2423
+ }
2424
+ return this.derive({ onUpdateValue: resolved });
1871
2425
  }
1872
2426
  };
1873
2427
  function makeColumn(kind, meta = {}) {
@@ -1919,7 +2473,27 @@ var column = {
1919
2473
  /** `UUID` → `string`. */
1920
2474
  uuid: () => makeColumn("uuid"),
1921
2475
  /** `ENUM(...values)` → a string-literal union of the given values. */
1922
- enum: (...values) => makeColumn("enum", { values })
2476
+ enum: (...values) => makeColumn("enum", { values }),
2477
+ /**
2478
+ * A PostgreSQL array column (`text[]`, `integer[]`) → `T[]`.
2479
+ *
2480
+ * PostgreSQL only: SQLite and MySQL have no native array type, and rendering
2481
+ * one as JSON there would give the same model different semantics per dialect
2482
+ * (`@>` and `&&` work on one and not the other), so the DDL renderer throws
2483
+ * for those dialects instead of falling back silently.
2484
+ *
2485
+ * @param element The element column (its type, not its flags, is what is used).
2486
+ * @returns A column whose inferred type is an array of the element's type.
2487
+ *
2488
+ * @example
2489
+ * ```ts
2490
+ * class ApiKey extends Model {
2491
+ * static tablename = "api_keys";
2492
+ * scopes = column.array(column.text()).notNull().default(["send"]);
2493
+ * }
2494
+ * ```
2495
+ */
2496
+ array: (element) => makeColumn("array", { element: element.type })
1923
2497
  };
1924
2498
  function unique(...columns) {
1925
2499
  if (columns.length === 0) {
@@ -1943,6 +2517,9 @@ function foreignKey(columns, refTable, refColumns, options) {
1943
2517
  onUpdate: options?.onUpdate
1944
2518
  };
1945
2519
  }
2520
+ function toSnakeCase(name) {
2521
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
2522
+ }
1946
2523
  var Model = class {
1947
2524
  static tablename;
1948
2525
  /**
@@ -1951,6 +2528,12 @@ var Model = class {
1951
2528
  * `__table_args__`.
1952
2529
  */
1953
2530
  static tableArgs;
2531
+ /**
2532
+ * How to derive column names from property names (default `"preserve"`). Set
2533
+ * `"snake_case"` to keep a `snake_case` schema behind a `camelCase` model
2534
+ * without annotating every column; {@link Column.name} overrides it per column.
2535
+ */
2536
+ static naming;
1954
2537
  };
1955
2538
  var columnsCache = /* @__PURE__ */ new WeakMap();
1956
2539
  function columnsOf(model) {
@@ -1966,6 +2549,47 @@ function columnsOf(model) {
1966
2549
  columnsCache.set(model, out);
1967
2550
  return out;
1968
2551
  }
2552
+ var nameMapCache = /* @__PURE__ */ new WeakMap();
2553
+ var propMapCache = /* @__PURE__ */ new WeakMap();
2554
+ function columnNamesOf(model) {
2555
+ const cached = nameMapCache.get(model);
2556
+ if (cached !== void 0) return cached;
2557
+ const strategy = model.naming ?? "preserve";
2558
+ const map = {};
2559
+ const seen = /* @__PURE__ */ new Map();
2560
+ let renamed = false;
2561
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
2562
+ const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
2563
+ const collision = seen.get(dbName);
2564
+ if (collision !== void 0) {
2565
+ throw new Error(
2566
+ `${model.tablename}: properties "${collision}" and "${prop}" both map to column "${dbName}".`
2567
+ );
2568
+ }
2569
+ seen.set(dbName, prop);
2570
+ map[prop] = dbName;
2571
+ if (dbName !== prop) renamed = true;
2572
+ }
2573
+ const result = renamed ? map : null;
2574
+ nameMapCache.set(model, result);
2575
+ return result;
2576
+ }
2577
+ function columnPropsOf(model) {
2578
+ const cached = propMapCache.get(model);
2579
+ if (cached !== void 0) return cached;
2580
+ const forward = columnNamesOf(model);
2581
+ let result = null;
2582
+ if (forward) {
2583
+ const inverse = {};
2584
+ for (const [prop, dbName] of Object.entries(forward)) inverse[dbName] = prop;
2585
+ result = inverse;
2586
+ }
2587
+ propMapCache.set(model, result);
2588
+ return result;
2589
+ }
2590
+ function dbColumn(names, prop) {
2591
+ return names?.[prop] ?? prop;
2592
+ }
1969
2593
 
1970
2594
  exports.ActiveRecord = ActiveRecord;
1971
2595
  exports.Agg = Agg;
@@ -1984,6 +2608,7 @@ exports.MysqlDialect = MysqlDialect;
1984
2608
  exports.NoResultError = NoResultError;
1985
2609
  exports.NodeSqliteDriver = NodeSqliteDriver;
1986
2610
  exports.OPERATORS = OPERATORS;
2611
+ exports.Params = Params;
1987
2612
  exports.PostgresDialect = PostgresDialect;
1988
2613
  exports.QueryExecutionError = QueryExecutionError;
1989
2614
  exports.RecordNotFound = RecordNotFound;
@@ -1999,10 +2624,13 @@ exports.and = and;
1999
2624
  exports.avg = avg;
2000
2625
  exports.belongsTo = belongsTo;
2001
2626
  exports.column = column;
2627
+ exports.columnNamesOf = columnNamesOf;
2628
+ exports.columnPropsOf = columnPropsOf;
2002
2629
  exports.columnsOf = columnsOf;
2003
2630
  exports.count = count;
2004
2631
  exports.createEngine = createEngine;
2005
2632
  exports.createSyncEngine = createSyncEngine;
2633
+ exports.dbColumn = dbColumn;
2006
2634
  exports.del = del;
2007
2635
  exports.detectDialect = detectDialect;
2008
2636
  exports.foreignKey = foreignKey;
@@ -2011,6 +2639,7 @@ exports.getDialect = getDialect;
2011
2639
  exports.hasMany = hasMany;
2012
2640
  exports.insert = insert;
2013
2641
  exports.isCondition = isCondition;
2642
+ exports.isSqlExpression = isSqlExpression;
2014
2643
  exports.join = join;
2015
2644
  exports.loadRelations = loadRelations;
2016
2645
  exports.max = max;
@@ -2026,6 +2655,7 @@ exports.sum = sum;
2026
2655
  exports.toCondNode = toCondNode;
2027
2656
  exports.toDict = toDict;
2028
2657
  exports.toJSON = toJSON;
2658
+ exports.toSnakeCase = toSnakeCase;
2029
2659
  exports.unique = unique;
2030
2660
  exports.update = update;
2031
2661
  //# sourceMappingURL=index.cjs.map