tempest-db-js 0.3.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/README.md +9 -2
- package/dist/bin.cjs +475 -44
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/{chunk-Q32CBI2A.js → chunk-5QQMVTS5.js} +988 -286
- package/dist/chunk-5QQMVTS5.js.map +1 -0
- package/dist/{chunk-OP7FRDI5.js → chunk-EPMLFNFK.js} +448 -37
- package/dist/chunk-EPMLFNFK.js.map +1 -0
- package/dist/index.cjs +993 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +632 -30
- package/dist/index.d.ts +632 -30
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +591 -52
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +63 -14
- package/dist/migrations/index.d.ts +63 -14
- package/dist/migrations/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-OP7FRDI5.js.map +0 -1
- package/dist/chunk-Q32CBI2A.js.map +0 -1
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
|
-
/**
|
|
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({
|
|
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: {
|
|
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
|
-
/**
|
|
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
|
);
|
|
@@ -323,215 +646,70 @@ function parseSqlite(raw, driver, rest) {
|
|
|
323
646
|
driver,
|
|
324
647
|
host: null,
|
|
325
648
|
port: null,
|
|
326
|
-
user: null,
|
|
327
|
-
password: null,
|
|
328
|
-
database: decode(database),
|
|
329
|
-
options: {},
|
|
330
|
-
raw
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
function parseNetworkUrl(raw, driver, rest, dialect) {
|
|
334
|
-
let parsed;
|
|
335
|
-
try {
|
|
336
|
-
parsed = new URL(`${dialect}:${rest}`);
|
|
337
|
-
} catch {
|
|
338
|
-
throw new InvalidDatabaseUrl(raw, "could not parse host/credentials");
|
|
339
|
-
}
|
|
340
|
-
const database = decode(parsed.pathname.replace(/^\//, "")) || null;
|
|
341
|
-
const options = {};
|
|
342
|
-
for (const [key, value] of parsed.searchParams) options[key] = value;
|
|
343
|
-
return {
|
|
344
|
-
dialect,
|
|
345
|
-
driver,
|
|
346
|
-
host: parsed.hostname || null,
|
|
347
|
-
port: parsed.port ? Number(parsed.port) : null,
|
|
348
|
-
user: parsed.username ? decode(parsed.username) : null,
|
|
349
|
-
password: parsed.password ? decode(parsed.password) : null,
|
|
350
|
-
database,
|
|
351
|
-
options,
|
|
352
|
-
raw
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
function parseDatabaseUrl(url) {
|
|
356
|
-
const schemeEnd = url.indexOf(":");
|
|
357
|
-
if (schemeEnd === -1) {
|
|
358
|
-
throw new InvalidDatabaseUrl(
|
|
359
|
-
url,
|
|
360
|
-
"missing scheme (expected e.g. sqlite:// or postgresql://)"
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
const { base, driver } = splitScheme(url.slice(0, schemeEnd));
|
|
364
|
-
const dialect = DIALECT_ALIASES[base];
|
|
365
|
-
if (!dialect) {
|
|
366
|
-
throw new InvalidDatabaseUrl(url, `unknown dialect ${JSON.stringify(base)}`);
|
|
367
|
-
}
|
|
368
|
-
const rest = url.slice(schemeEnd + 1);
|
|
369
|
-
if (dialect === "sqlite") return parseSqlite(url, driver, rest);
|
|
370
|
-
return parseNetworkUrl(url, driver, rest, dialect);
|
|
371
|
-
}
|
|
372
|
-
function detectDialect(url) {
|
|
373
|
-
return parseDatabaseUrl(url).dialect;
|
|
374
|
-
}
|
|
375
|
-
|
|
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);
|
|
484
|
-
}
|
|
485
|
-
return out;
|
|
486
|
-
}
|
|
487
|
-
function parse(model, json) {
|
|
488
|
-
return fromDict(model, JSON.parse(json));
|
|
649
|
+
user: null,
|
|
650
|
+
password: null,
|
|
651
|
+
database: decode(database),
|
|
652
|
+
options: {},
|
|
653
|
+
raw
|
|
654
|
+
};
|
|
489
655
|
}
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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;
|
|
656
|
+
function parseNetworkUrl(raw, driver, rest, dialect) {
|
|
657
|
+
let parsed;
|
|
658
|
+
try {
|
|
659
|
+
parsed = new URL(`${dialect}:${rest}`);
|
|
660
|
+
} catch {
|
|
661
|
+
throw new InvalidDatabaseUrl(raw, "could not parse host/credentials");
|
|
513
662
|
}
|
|
663
|
+
const database = decode(parsed.pathname.replace(/^\//, "")) || null;
|
|
664
|
+
const options = {};
|
|
665
|
+
for (const [key, value] of parsed.searchParams) options[key] = value;
|
|
666
|
+
return {
|
|
667
|
+
dialect,
|
|
668
|
+
driver,
|
|
669
|
+
host: parsed.hostname || null,
|
|
670
|
+
port: parsed.port ? Number(parsed.port) : null,
|
|
671
|
+
user: parsed.username ? decode(parsed.username) : null,
|
|
672
|
+
password: parsed.password ? decode(parsed.password) : null,
|
|
673
|
+
database,
|
|
674
|
+
options,
|
|
675
|
+
raw
|
|
676
|
+
};
|
|
514
677
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (decoder) map.set(name, decoder);
|
|
678
|
+
function parseDatabaseUrl(url) {
|
|
679
|
+
const schemeEnd = url.indexOf(":");
|
|
680
|
+
if (schemeEnd === -1) {
|
|
681
|
+
throw new InvalidDatabaseUrl(
|
|
682
|
+
url,
|
|
683
|
+
"missing scheme (expected e.g. sqlite:// or postgresql://)"
|
|
684
|
+
);
|
|
523
685
|
}
|
|
524
|
-
|
|
525
|
-
|
|
686
|
+
const { base, driver } = splitScheme(url.slice(0, schemeEnd));
|
|
687
|
+
const dialect = DIALECT_ALIASES[base];
|
|
688
|
+
if (!dialect) {
|
|
689
|
+
throw new InvalidDatabaseUrl(url, `unknown dialect ${JSON.stringify(base)}`);
|
|
690
|
+
}
|
|
691
|
+
const rest = url.slice(schemeEnd + 1);
|
|
692
|
+
if (dialect === "sqlite") return parseSqlite(url, driver, rest);
|
|
693
|
+
return parseNetworkUrl(url, driver, rest, dialect);
|
|
526
694
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
695
|
+
function detectDialect(url) {
|
|
696
|
+
return parseDatabaseUrl(url).dialect;
|
|
697
|
+
}
|
|
698
|
+
|
|
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))))";
|
|
533
712
|
}
|
|
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
|
-
/**
|
|
605
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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 =
|
|
938
|
+
const conflictCols = conflict && conflict.update !== "nothing" ? Object.keys(conflict.update) : [];
|
|
645
939
|
for (const c of conflictCols) {
|
|
646
|
-
params.bind((
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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,
|
|
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.
|
|
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.
|
|
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);
|
|
@@ -1751,73 +2238,190 @@ function createPostgresDriver(url, pool) {
|
|
|
1751
2238
|
var DEFAULT_FLAGS = {
|
|
1752
2239
|
primaryKey: false,
|
|
1753
2240
|
notNull: false,
|
|
1754
|
-
hasDefault: false
|
|
2241
|
+
hasDefault: false,
|
|
2242
|
+
unique: false
|
|
1755
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
|
+
}
|
|
1756
2251
|
var sql = {
|
|
1757
2252
|
/** Current timestamp at insert (`CURRENT_TIMESTAMP` / `now()`). */
|
|
1758
|
-
now: () => (
|
|
2253
|
+
now: () => expression("now"),
|
|
1759
2254
|
/** Current date. */
|
|
1760
|
-
currentDate: () => (
|
|
2255
|
+
currentDate: () => expression("current_date"),
|
|
1761
2256
|
/** Current time. */
|
|
1762
|
-
currentTime: () => (
|
|
2257
|
+
currentTime: () => expression("current_time"),
|
|
1763
2258
|
/** A freshly generated UUID v4 (`gen_random_uuid()` / portable fallback). */
|
|
1764
|
-
uuidv4: () => (
|
|
1765
|
-
/**
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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)
|
|
1770
2289
|
};
|
|
1771
2290
|
function isDefaultValue(value) {
|
|
1772
2291
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
1773
2292
|
}
|
|
2293
|
+
function bindsParameters(value) {
|
|
2294
|
+
return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
|
|
2295
|
+
}
|
|
2296
|
+
function parseReference(ref, options) {
|
|
2297
|
+
const dot = ref.lastIndexOf(".");
|
|
2298
|
+
if (dot <= 0 || dot === ref.length - 1) {
|
|
2299
|
+
throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
|
|
2300
|
+
}
|
|
2301
|
+
return {
|
|
2302
|
+
table: ref.slice(0, dot),
|
|
2303
|
+
column: ref.slice(dot + 1),
|
|
2304
|
+
onDelete: options?.onDelete,
|
|
2305
|
+
onUpdate: options?.onUpdate
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
1774
2308
|
var Column = class _Column {
|
|
1775
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null) {
|
|
2309
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
|
|
1776
2310
|
this.type = type;
|
|
1777
2311
|
this.flags = flags;
|
|
1778
2312
|
this.defaultValue = defaultValue;
|
|
1779
2313
|
this.onUpdateValue = onUpdateValue;
|
|
2314
|
+
this.reference = reference;
|
|
2315
|
+
this.dbName = dbName;
|
|
1780
2316
|
}
|
|
1781
2317
|
type;
|
|
1782
2318
|
flags;
|
|
1783
2319
|
defaultValue;
|
|
1784
2320
|
onUpdateValue;
|
|
1785
|
-
|
|
2321
|
+
reference;
|
|
2322
|
+
dbName;
|
|
2323
|
+
/** Clone this column with one facet replaced, carrying every other over. */
|
|
2324
|
+
derive(patch) {
|
|
1786
2325
|
return new _Column(
|
|
1787
2326
|
this.type,
|
|
1788
|
-
|
|
1789
|
-
this.defaultValue,
|
|
1790
|
-
this.onUpdateValue
|
|
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
|
|
1791
2332
|
);
|
|
1792
2333
|
}
|
|
2334
|
+
primaryKey() {
|
|
2335
|
+
return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
|
|
2336
|
+
}
|
|
1793
2337
|
notNull() {
|
|
1794
|
-
return
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2338
|
+
return this.derive({ flags: { ...this.flags, notNull: true } });
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
|
|
2342
|
+
* `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
|
|
2343
|
+
*/
|
|
2344
|
+
unique() {
|
|
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 });
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Declare a foreign-key reference to another table's column, à la SQLAlchemy's
|
|
2377
|
+
* `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
|
|
2378
|
+
* not change the inferred type.
|
|
2379
|
+
*
|
|
2380
|
+
* @param ref The target as `"table.column"` (e.g. `"users.id"`).
|
|
2381
|
+
* @param options Optional `onDelete` / `onUpdate` referential actions.
|
|
2382
|
+
* @returns A new column carrying the reference.
|
|
2383
|
+
* @throws Error When `ref` is not a valid `"table.column"` string.
|
|
2384
|
+
*/
|
|
2385
|
+
references(ref, options) {
|
|
2386
|
+
return this.derive({ reference: parseReference(ref, options) });
|
|
1800
2387
|
}
|
|
1801
2388
|
/**
|
|
1802
2389
|
* Set the insert-time default: a constant value of type `T`, or a portable
|
|
1803
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.
|
|
1804
2396
|
*/
|
|
1805
2397
|
default(value) {
|
|
1806
2398
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
+
});
|
|
1813
2408
|
}
|
|
1814
2409
|
/**
|
|
1815
2410
|
* Re-apply a value whenever the row is updated (e.g. an `updated_at` column
|
|
1816
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}).
|
|
1817
2416
|
*/
|
|
1818
2417
|
onUpdate(value) {
|
|
1819
2418
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
1820
|
-
|
|
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 });
|
|
1821
2425
|
}
|
|
1822
2426
|
};
|
|
1823
2427
|
function makeColumn(kind, meta = {}) {
|
|
@@ -1869,10 +2473,67 @@ var column = {
|
|
|
1869
2473
|
/** `UUID` → `string`. */
|
|
1870
2474
|
uuid: () => makeColumn("uuid"),
|
|
1871
2475
|
/** `ENUM(...values)` → a string-literal union of the given values. */
|
|
1872
|
-
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 })
|
|
1873
2497
|
};
|
|
2498
|
+
function unique(...columns) {
|
|
2499
|
+
if (columns.length === 0) {
|
|
2500
|
+
throw new Error("unique() requires at least one column.");
|
|
2501
|
+
}
|
|
2502
|
+
return { kind: "unique", columns };
|
|
2503
|
+
}
|
|
2504
|
+
function foreignKey(columns, refTable, refColumns, options) {
|
|
2505
|
+
if (columns.length === 0 || columns.length !== refColumns.length) {
|
|
2506
|
+
throw new Error(
|
|
2507
|
+
"foreignKey() requires matching, non-empty local and referenced column lists."
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
kind: "foreignKey",
|
|
2512
|
+
name: options?.name,
|
|
2513
|
+
columns,
|
|
2514
|
+
refTable,
|
|
2515
|
+
refColumns,
|
|
2516
|
+
onDelete: options?.onDelete,
|
|
2517
|
+
onUpdate: options?.onUpdate
|
|
2518
|
+
};
|
|
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
|
+
}
|
|
1874
2523
|
var Model = class {
|
|
1875
2524
|
static tablename;
|
|
2525
|
+
/**
|
|
2526
|
+
* Optional table-level constraints (composite unique / foreign keys), returned
|
|
2527
|
+
* by a thunk so forward references resolve lazily. Mirrors SQLAlchemy's
|
|
2528
|
+
* `__table_args__`.
|
|
2529
|
+
*/
|
|
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;
|
|
1876
2537
|
};
|
|
1877
2538
|
var columnsCache = /* @__PURE__ */ new WeakMap();
|
|
1878
2539
|
function columnsOf(model) {
|
|
@@ -1888,6 +2549,47 @@ function columnsOf(model) {
|
|
|
1888
2549
|
columnsCache.set(model, out);
|
|
1889
2550
|
return out;
|
|
1890
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
|
+
}
|
|
1891
2593
|
|
|
1892
2594
|
exports.ActiveRecord = ActiveRecord;
|
|
1893
2595
|
exports.Agg = Agg;
|
|
@@ -1906,6 +2608,7 @@ exports.MysqlDialect = MysqlDialect;
|
|
|
1906
2608
|
exports.NoResultError = NoResultError;
|
|
1907
2609
|
exports.NodeSqliteDriver = NodeSqliteDriver;
|
|
1908
2610
|
exports.OPERATORS = OPERATORS;
|
|
2611
|
+
exports.Params = Params;
|
|
1909
2612
|
exports.PostgresDialect = PostgresDialect;
|
|
1910
2613
|
exports.QueryExecutionError = QueryExecutionError;
|
|
1911
2614
|
exports.RecordNotFound = RecordNotFound;
|
|
@@ -1921,17 +2624,22 @@ exports.and = and;
|
|
|
1921
2624
|
exports.avg = avg;
|
|
1922
2625
|
exports.belongsTo = belongsTo;
|
|
1923
2626
|
exports.column = column;
|
|
2627
|
+
exports.columnNamesOf = columnNamesOf;
|
|
2628
|
+
exports.columnPropsOf = columnPropsOf;
|
|
1924
2629
|
exports.columnsOf = columnsOf;
|
|
1925
2630
|
exports.count = count;
|
|
1926
2631
|
exports.createEngine = createEngine;
|
|
1927
2632
|
exports.createSyncEngine = createSyncEngine;
|
|
2633
|
+
exports.dbColumn = dbColumn;
|
|
1928
2634
|
exports.del = del;
|
|
1929
2635
|
exports.detectDialect = detectDialect;
|
|
2636
|
+
exports.foreignKey = foreignKey;
|
|
1930
2637
|
exports.fromDict = fromDict;
|
|
1931
2638
|
exports.getDialect = getDialect;
|
|
1932
2639
|
exports.hasMany = hasMany;
|
|
1933
2640
|
exports.insert = insert;
|
|
1934
2641
|
exports.isCondition = isCondition;
|
|
2642
|
+
exports.isSqlExpression = isSqlExpression;
|
|
1935
2643
|
exports.join = join;
|
|
1936
2644
|
exports.loadRelations = loadRelations;
|
|
1937
2645
|
exports.max = max;
|
|
@@ -1947,6 +2655,8 @@ exports.sum = sum;
|
|
|
1947
2655
|
exports.toCondNode = toCondNode;
|
|
1948
2656
|
exports.toDict = toDict;
|
|
1949
2657
|
exports.toJSON = toJSON;
|
|
2658
|
+
exports.toSnakeCase = toSnakeCase;
|
|
2659
|
+
exports.unique = unique;
|
|
1950
2660
|
exports.update = update;
|
|
1951
2661
|
//# sourceMappingURL=index.cjs.map
|
|
1952
2662
|
//# sourceMappingURL=index.cjs.map
|