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