oakbun 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +1 -1
- package/dist/db/index.d.ts +238 -26
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/migrations/diff.d.ts +1 -1
- package/dist/db/migrations/diff.d.ts.map +1 -1
- package/dist/db/migrations/generator.d.ts +1 -1
- package/dist/db/migrations/generator.d.ts.map +1 -1
- package/dist/db/sql.d.ts +70 -2
- package/dist/db/sql.d.ts.map +1 -1
- package/dist/{db-XTXH6OKV.js → db-YSUNURBB.js} +706 -45
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +900 -146
- package/dist/index.js.map +1 -1
- package/dist/schema/table.d.ts +108 -4
- package/dist/schema/table.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -28,6 +28,14 @@ var ValidationError = class extends VelnError {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
// src/db/sql.ts
|
|
31
|
+
function buildSubquery(sql, params, col) {
|
|
32
|
+
if (!sql) throw new Error("buildSubquery: sql must not be empty");
|
|
33
|
+
return {
|
|
34
|
+
_sql: `(${sql})`,
|
|
35
|
+
_params: params,
|
|
36
|
+
_phantom: { col, type: void 0 }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
31
39
|
var ON_CLAUSE_PATTERN = /^([\w]+)\.([\w]+)\s*=\s*([\w]+)\.([\w]+)$/;
|
|
32
40
|
function validateAndQuoteOnClause(on) {
|
|
33
41
|
const trimmed = on.trim();
|
|
@@ -50,6 +58,9 @@ function filterDefined(data) {
|
|
|
50
58
|
function isWhereOp(val) {
|
|
51
59
|
return typeof val === "object" && val !== null && "op" in val && typeof val["op"] === "string";
|
|
52
60
|
}
|
|
61
|
+
function isSubqueryResult(val) {
|
|
62
|
+
return typeof val === "object" && val !== null && "_sql" in val && "_params" in val;
|
|
63
|
+
}
|
|
53
64
|
function buildFieldCondition(key, condition, dialect) {
|
|
54
65
|
if (!isWhereOp(condition)) {
|
|
55
66
|
return { sql: `"${key}" = ?`, params: [condition] };
|
|
@@ -69,7 +80,11 @@ function buildFieldCondition(key, condition, dialect) {
|
|
|
69
80
|
case "<=":
|
|
70
81
|
return { sql: `"${key}" <= ?`, params: [op.value] };
|
|
71
82
|
case "IN": {
|
|
72
|
-
const
|
|
83
|
+
const inVal = op.value;
|
|
84
|
+
if (isSubqueryResult(inVal)) {
|
|
85
|
+
return { sql: `"${key}" IN ${inVal._sql}`, params: inVal._params };
|
|
86
|
+
}
|
|
87
|
+
const vals = inVal;
|
|
73
88
|
if (vals.length === 0) {
|
|
74
89
|
return { sql: "1 = 0", params: [] };
|
|
75
90
|
}
|
|
@@ -77,7 +92,11 @@ function buildFieldCondition(key, condition, dialect) {
|
|
|
77
92
|
return { sql: `"${key}" IN (${placeholders})`, params: vals };
|
|
78
93
|
}
|
|
79
94
|
case "NOT IN": {
|
|
80
|
-
const
|
|
95
|
+
const inVal = op.value;
|
|
96
|
+
if (isSubqueryResult(inVal)) {
|
|
97
|
+
return { sql: `"${key}" NOT IN ${inVal._sql}`, params: inVal._params };
|
|
98
|
+
}
|
|
99
|
+
const vals = inVal;
|
|
81
100
|
if (vals.length === 0) {
|
|
82
101
|
return { sql: "1 = 1", params: [] };
|
|
83
102
|
}
|
|
@@ -148,6 +167,37 @@ function buildInsert(tableName, data, returning = true) {
|
|
|
148
167
|
const sql = `INSERT INTO "${tableName}" (${cols}) VALUES (${placeholders})${returning_clause}`;
|
|
149
168
|
return { sql, params };
|
|
150
169
|
}
|
|
170
|
+
function buildInsertMany(tableName, rows, returning = true) {
|
|
171
|
+
if (rows.length === 0) {
|
|
172
|
+
throw new Error("insertMany: rows array must not be empty");
|
|
173
|
+
}
|
|
174
|
+
const columns = Object.keys(rows[0]);
|
|
175
|
+
const quotedCols = columns.map((c) => `"${c}"`).join(", ");
|
|
176
|
+
const params = [];
|
|
177
|
+
const valueClauses = [];
|
|
178
|
+
for (const row of rows) {
|
|
179
|
+
const placeholders = [];
|
|
180
|
+
for (const col of columns) {
|
|
181
|
+
const val = row[col];
|
|
182
|
+
if (val === void 0) {
|
|
183
|
+
throw new Error(`insertMany: column "${col}" has undefined value \u2014 apply defaults before calling buildInsertMany`);
|
|
184
|
+
}
|
|
185
|
+
params.push(val);
|
|
186
|
+
placeholders.push("?");
|
|
187
|
+
}
|
|
188
|
+
valueClauses.push(`(${placeholders.join(", ")})`);
|
|
189
|
+
}
|
|
190
|
+
const returning_clause = returning ? " RETURNING *" : "";
|
|
191
|
+
const sql = `INSERT INTO "${tableName}" (${quotedCols}) VALUES ${valueClauses.join(", ")}${returning_clause}`;
|
|
192
|
+
return { sql, params };
|
|
193
|
+
}
|
|
194
|
+
function buildSoftDeleteUpdate(tableName, col, value, where, dialect = "sqlite") {
|
|
195
|
+
const serialized = value instanceof Date ? value.toISOString() : null;
|
|
196
|
+
const { sql: whereSql, params: whereParams } = buildWhere(where, dialect);
|
|
197
|
+
const setSql = `"${col}" = ?`;
|
|
198
|
+
const sql = whereSql ? `UPDATE "${tableName}" SET ${setSql} WHERE ${whereSql}` : `UPDATE "${tableName}" SET ${setSql}`;
|
|
199
|
+
return { sql, params: [serialized, ...whereParams] };
|
|
200
|
+
}
|
|
151
201
|
function buildUpdate(tableName, patch, pk, pkValue) {
|
|
152
202
|
const entries = filterDefined(patch);
|
|
153
203
|
const sets = entries.map(([key]) => `"${key}" = ?`).join(", ");
|
|
@@ -203,11 +253,27 @@ function appendPaginationAndOrder(parts, options) {
|
|
|
203
253
|
}
|
|
204
254
|
}
|
|
205
255
|
}
|
|
256
|
+
function buildUnion(parts, kind, options = {}) {
|
|
257
|
+
if (parts.length < 2) {
|
|
258
|
+
throw new Error("buildUnion: at least 2 parts required");
|
|
259
|
+
}
|
|
260
|
+
const sqls = parts.map((p) => p.sql);
|
|
261
|
+
const params = parts.flatMap((p) => p.params);
|
|
262
|
+
let sql = sqls.join(` ${kind} `);
|
|
263
|
+
if (options.orderBy) {
|
|
264
|
+
sql += ` ORDER BY "${options.orderBy.col}" ${options.orderBy.dir ?? "ASC"}`;
|
|
265
|
+
}
|
|
266
|
+
if (options.limit !== void 0) {
|
|
267
|
+
sql += ` LIMIT ${Math.trunc(Math.max(0, options.limit))}`;
|
|
268
|
+
}
|
|
269
|
+
return { sql, params };
|
|
270
|
+
}
|
|
206
271
|
function buildSelect(tableName, conditions, options, dialect = "sqlite") {
|
|
207
272
|
const selectList = buildSelectListFromOptions(options);
|
|
208
273
|
const { sql: whereSql, params } = buildWhere(conditions, dialect);
|
|
274
|
+
const selectKeyword = options?.distinct ? "SELECT DISTINCT" : "SELECT";
|
|
209
275
|
const parts = [
|
|
210
|
-
whereSql ?
|
|
276
|
+
whereSql ? `${selectKeyword} ${selectList} FROM "${tableName}" WHERE ${whereSql}` : `${selectKeyword} ${selectList} FROM "${tableName}"`
|
|
211
277
|
];
|
|
212
278
|
if (options?.groupBy && options.groupBy.length > 0) {
|
|
213
279
|
parts.push(`GROUP BY ${options.groupBy.map((c) => `"${c}"`).join(", ")}`);
|
|
@@ -260,10 +326,11 @@ var VelnDB = class {
|
|
|
260
326
|
}
|
|
261
327
|
};
|
|
262
328
|
var BoundVelnDB = class _BoundVelnDB {
|
|
263
|
-
constructor(adapter, hooks, ctx, queue, queryLog) {
|
|
329
|
+
constructor(adapter, hooks, ctx, queue, queryLog, dialect = "sqlite") {
|
|
264
330
|
this.hooks = hooks;
|
|
265
331
|
this.ctx = ctx;
|
|
266
332
|
this.queue = queue;
|
|
333
|
+
this.dialect = dialect;
|
|
267
334
|
if (queryLog) {
|
|
268
335
|
const log = queryLog;
|
|
269
336
|
this.adapter = {
|
|
@@ -298,9 +365,11 @@ var BoundVelnDB = class _BoundVelnDB {
|
|
|
298
365
|
hooks;
|
|
299
366
|
ctx;
|
|
300
367
|
queue;
|
|
368
|
+
dialect;
|
|
301
369
|
/** Per-request query counter — incremented for every query() and execute() call on this instance. */
|
|
302
370
|
_queryCount = 0;
|
|
303
371
|
adapter;
|
|
372
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
304
373
|
from(table) {
|
|
305
374
|
return new SelectBuilder(this.adapter, this.hooks, this.ctx, this.queue, table, {});
|
|
306
375
|
}
|
|
@@ -320,20 +389,20 @@ var BoundVelnDB = class _BoundVelnDB {
|
|
|
320
389
|
return new JoinBuilder(this.adapter, tableName, [], [], "", []);
|
|
321
390
|
}
|
|
322
391
|
into(table) {
|
|
323
|
-
return new InsertBuilder(this.adapter, this.hooks, this.ctx, this.queue, table);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
392
|
+
return new InsertBuilder(this.adapter, this.hooks, this.ctx, this.queue, table, this.dialect);
|
|
393
|
+
}
|
|
394
|
+
// Implementation
|
|
395
|
+
async loadRelation(parents, keyOrRelationName, tableOrSource, primaryKey) {
|
|
396
|
+
if (primaryKey === void 0) {
|
|
397
|
+
return this._loadRelationByName(
|
|
398
|
+
parents,
|
|
399
|
+
keyOrRelationName,
|
|
400
|
+
tableOrSource,
|
|
401
|
+
"many"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const foreignKey = keyOrRelationName;
|
|
405
|
+
const childTable = tableOrSource;
|
|
337
406
|
const result = /* @__PURE__ */ new Map();
|
|
338
407
|
if (parents.length === 0) return result;
|
|
339
408
|
const ids = [...new Set(parents.map((p) => p[foreignKey]))];
|
|
@@ -349,15 +418,18 @@ var BoundVelnDB = class _BoundVelnDB {
|
|
|
349
418
|
}
|
|
350
419
|
return result;
|
|
351
420
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
421
|
+
// Implementation
|
|
422
|
+
async loadRelationOne(parents, keyOrRelationName, tableOrSource, primaryKey) {
|
|
423
|
+
if (primaryKey === void 0) {
|
|
424
|
+
return this._loadRelationByName(
|
|
425
|
+
parents,
|
|
426
|
+
keyOrRelationName,
|
|
427
|
+
tableOrSource,
|
|
428
|
+
"one"
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const foreignKey = keyOrRelationName;
|
|
432
|
+
const childTable = tableOrSource;
|
|
361
433
|
const result = /* @__PURE__ */ new Map();
|
|
362
434
|
if (parents.length === 0) return result;
|
|
363
435
|
const ids = [...new Set(parents.map((p) => p[foreignKey]))];
|
|
@@ -367,6 +439,55 @@ var BoundVelnDB = class _BoundVelnDB {
|
|
|
367
439
|
}
|
|
368
440
|
return result;
|
|
369
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Shared implementation for name-based loadRelation / loadRelationOne.
|
|
444
|
+
* Reads RelationMeta from sourceTable.relations, validates the kind,
|
|
445
|
+
* and delegates to the explicit overload.
|
|
446
|
+
*/
|
|
447
|
+
async _loadRelationByName(parents, relationName, sourceTable, mode) {
|
|
448
|
+
const rel = sourceTable.relations[relationName];
|
|
449
|
+
if (rel === void 0) {
|
|
450
|
+
const available = Object.keys(sourceTable.relations).join(", ") || "(none)";
|
|
451
|
+
throw new Error(
|
|
452
|
+
`Relation '${relationName}' is not defined on table '${sourceTable.name}'. Available relations: ${available}`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (rel.kind === "manyToMany") {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`manyToMany relations are not yet supported in loadRelation. Use a manual JOIN for relation '${relationName}' on table '${sourceTable.name}'.`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (mode === "one" && rel.kind === "hasMany") {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`loadRelationOne cannot be used with hasMany relation '${relationName}' on table '${sourceTable.name}'. Use loadRelation to get an array of results.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const foreignTable = rel.getTable();
|
|
466
|
+
const pk = foreignTable.primaryKey;
|
|
467
|
+
if (rel.kind === "belongsTo") {
|
|
468
|
+
const ft = foreignTable;
|
|
469
|
+
const fk = rel.foreignKey;
|
|
470
|
+
if (mode === "one") {
|
|
471
|
+
return this.loadRelationOne(parents, fk, ft, pk);
|
|
472
|
+
}
|
|
473
|
+
return this.loadRelation(parents, fk, ft, pk);
|
|
474
|
+
}
|
|
475
|
+
const parentPk = sourceTable.primaryKey;
|
|
476
|
+
const result = /* @__PURE__ */ new Map();
|
|
477
|
+
if (parents.length === 0) return result;
|
|
478
|
+
const ids = [...new Set(parents.map((p) => p[parentPk]))];
|
|
479
|
+
const children = await this.from(foreignTable).where({ [rel.foreignKey]: { op: "IN", value: ids } }).select();
|
|
480
|
+
for (const child of children) {
|
|
481
|
+
const key = child[rel.foreignKey];
|
|
482
|
+
const group = result.get(key);
|
|
483
|
+
if (group) {
|
|
484
|
+
group.push(child);
|
|
485
|
+
} else {
|
|
486
|
+
result.set(key, [child]);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
370
491
|
async transaction(fn) {
|
|
371
492
|
const txQueue = new RequestEventQueue();
|
|
372
493
|
const result = await this.adapter.transaction(async (txAdapter) => {
|
|
@@ -411,7 +532,7 @@ function mergeWhereAnd(a, b) {
|
|
|
411
532
|
return { AND: [a, b] };
|
|
412
533
|
}
|
|
413
534
|
var SelectBuilder = class _SelectBuilder {
|
|
414
|
-
constructor(adapter, hooks, ctx, queue, table, conditions, _options = {}, _rawWhere = [], _dialect = "sqlite") {
|
|
535
|
+
constructor(adapter, hooks, ctx, queue, table, conditions, _options = {}, _rawWhere = [], _dialect = "sqlite", _withRelations = [], _includeDeleted = false) {
|
|
415
536
|
this.adapter = adapter;
|
|
416
537
|
this.hooks = hooks;
|
|
417
538
|
this.ctx = ctx;
|
|
@@ -421,6 +542,8 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
421
542
|
this._options = _options;
|
|
422
543
|
this._rawWhere = _rawWhere;
|
|
423
544
|
this._dialect = _dialect;
|
|
545
|
+
this._withRelations = _withRelations;
|
|
546
|
+
this._includeDeleted = _includeDeleted;
|
|
424
547
|
}
|
|
425
548
|
adapter;
|
|
426
549
|
hooks;
|
|
@@ -431,6 +554,8 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
431
554
|
_options;
|
|
432
555
|
_rawWhere;
|
|
433
556
|
_dialect;
|
|
557
|
+
_withRelations;
|
|
558
|
+
_includeDeleted;
|
|
434
559
|
_cloneWith(conditions, rawWhere) {
|
|
435
560
|
return new _SelectBuilder(
|
|
436
561
|
this.adapter,
|
|
@@ -441,7 +566,9 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
441
566
|
conditions,
|
|
442
567
|
this._options,
|
|
443
568
|
rawWhere ?? this._rawWhere,
|
|
444
|
-
this._dialect
|
|
569
|
+
this._dialect,
|
|
570
|
+
this._withRelations,
|
|
571
|
+
this._includeDeleted
|
|
445
572
|
);
|
|
446
573
|
}
|
|
447
574
|
_clone(patch) {
|
|
@@ -454,7 +581,60 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
454
581
|
this.conditions,
|
|
455
582
|
{ ...this._options, ...patch },
|
|
456
583
|
this._rawWhere,
|
|
457
|
-
this._dialect
|
|
584
|
+
this._dialect,
|
|
585
|
+
this._withRelations,
|
|
586
|
+
this._includeDeleted
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Eager-load relations alongside the main query.
|
|
591
|
+
* One additional IN-query per relation — never N+1 regardless of row count.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* const posts = await db.from(postsTable).with({ author: true }).select()
|
|
595
|
+
* posts[0].author // → User | null (fully typed)
|
|
596
|
+
* posts[0].title // → string (original fields preserved)
|
|
597
|
+
*/
|
|
598
|
+
with(relations) {
|
|
599
|
+
const keys = Object.keys(relations);
|
|
600
|
+
return new _SelectBuilder(
|
|
601
|
+
this.adapter,
|
|
602
|
+
this.hooks,
|
|
603
|
+
this.ctx,
|
|
604
|
+
this.queue,
|
|
605
|
+
// table type cast: the schema/relations are unchanged; only T changes in the generic
|
|
606
|
+
this.table,
|
|
607
|
+
this.conditions,
|
|
608
|
+
this._options,
|
|
609
|
+
this._rawWhere,
|
|
610
|
+
this._dialect,
|
|
611
|
+
[...this._withRelations, ...keys],
|
|
612
|
+
this._includeDeleted
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Include soft-deleted rows in the query result.
|
|
617
|
+
* By default, tables with `.withSoftDelete()` automatically exclude rows
|
|
618
|
+
* where the soft-delete column is not null.
|
|
619
|
+
*
|
|
620
|
+
* Has no effect on tables without soft delete configured.
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* const allUsers = await db.from(usersTable).withDeleted().select()
|
|
624
|
+
*/
|
|
625
|
+
withDeleted() {
|
|
626
|
+
return new _SelectBuilder(
|
|
627
|
+
this.adapter,
|
|
628
|
+
this.hooks,
|
|
629
|
+
this.ctx,
|
|
630
|
+
this.queue,
|
|
631
|
+
this.table,
|
|
632
|
+
this.conditions,
|
|
633
|
+
this._options,
|
|
634
|
+
this._rawWhere,
|
|
635
|
+
this._dialect,
|
|
636
|
+
this._withRelations,
|
|
637
|
+
true
|
|
458
638
|
);
|
|
459
639
|
}
|
|
460
640
|
/**
|
|
@@ -481,6 +661,17 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
481
661
|
whereRaw(sql, params) {
|
|
482
662
|
return this._cloneWith(this.conditions, [...this._rawWhere, { sql, params }]);
|
|
483
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Apply SELECT DISTINCT — deduplicate rows in the result set.
|
|
666
|
+
* Combine with `.columns()` to deduplicate on specific columns.
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* await db.from(usersTable).columns('name').distinct().select()
|
|
670
|
+
* // → SELECT DISTINCT "name" FROM "users"
|
|
671
|
+
*/
|
|
672
|
+
distinct() {
|
|
673
|
+
return this._clone({ distinct: true });
|
|
674
|
+
}
|
|
484
675
|
/** Limit the number of rows returned. Bound as a parameter — never interpolated. */
|
|
485
676
|
limit(n) {
|
|
486
677
|
return this._clone({ limit: n });
|
|
@@ -502,14 +693,74 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
502
693
|
page(page, size) {
|
|
503
694
|
return this._clone({ limit: size, offset: (page - 1) * size });
|
|
504
695
|
}
|
|
696
|
+
columns(...cols) {
|
|
697
|
+
const cloned = this._clone({ columns: cols });
|
|
698
|
+
if (cols.length === 1) {
|
|
699
|
+
return new ColumnRestrictedBuilder(cloned, cols[0]);
|
|
700
|
+
}
|
|
701
|
+
return cloned;
|
|
702
|
+
}
|
|
505
703
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* Return type is narrowed to Pick<T, K> for full type safety.
|
|
704
|
+
* Build SELECT SQL + params without executing the query.
|
|
705
|
+
* Used internally by ColumnRestrictedBuilder.subquery().
|
|
510
706
|
*/
|
|
511
|
-
|
|
512
|
-
|
|
707
|
+
/** Internal accessor for ColumnRestrictedBuilder / UnionBuilder — returns the adapter. */
|
|
708
|
+
_getAdapter() {
|
|
709
|
+
return this.adapter;
|
|
710
|
+
}
|
|
711
|
+
/** Internal accessor for ColumnRestrictedBuilder / UnionBuilder — returns the SQL dialect. */
|
|
712
|
+
_getDialect() {
|
|
713
|
+
return this._dialect;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Returns the effective WHERE conditions, injecting the soft-delete IS NULL
|
|
717
|
+
* filter when the table has a soft-delete column and .withDeleted() was not called.
|
|
718
|
+
*/
|
|
719
|
+
_effectiveConditions() {
|
|
720
|
+
const col = this.table.softDeleteColumn;
|
|
721
|
+
if (col === null || this._includeDeleted) return this.conditions;
|
|
722
|
+
const softFilter = { [col]: { op: "IS NULL" } };
|
|
723
|
+
return mergeWhereAnd(this.conditions, softFilter);
|
|
724
|
+
}
|
|
725
|
+
_buildSelectSQL() {
|
|
726
|
+
const conditions = this._effectiveConditions();
|
|
727
|
+
if (this._rawWhere.length === 0) {
|
|
728
|
+
return buildSelect(
|
|
729
|
+
this.table.name,
|
|
730
|
+
conditions,
|
|
731
|
+
this._options,
|
|
732
|
+
this._dialect
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const { sql: whereSql, params: whereParams } = buildWhere(
|
|
736
|
+
conditions,
|
|
737
|
+
this._dialect
|
|
738
|
+
);
|
|
739
|
+
const allWhereParts = [];
|
|
740
|
+
const allParams = [...whereParams];
|
|
741
|
+
if (whereSql) allWhereParts.push(whereSql);
|
|
742
|
+
for (const raw of this._rawWhere) {
|
|
743
|
+
allWhereParts.push(raw.sql);
|
|
744
|
+
allParams.push(...raw.params);
|
|
745
|
+
}
|
|
746
|
+
const combinedWhere = allWhereParts.join(" AND ");
|
|
747
|
+
const selectList = buildSelectListFromOptions(this._options);
|
|
748
|
+
const selectKeyword = this._options.distinct ? "SELECT DISTINCT" : "SELECT";
|
|
749
|
+
const parts = [
|
|
750
|
+
combinedWhere ? `${selectKeyword} ${selectList} FROM "${this.table.name}" WHERE ${combinedWhere}` : `${selectKeyword} ${selectList} FROM "${this.table.name}"`
|
|
751
|
+
];
|
|
752
|
+
if (this._options.orderBy && this._options.orderBy.length > 0) {
|
|
753
|
+
const clause = this._options.orderBy.map(({ col, dir }) => `"${col}" ${dir}`).join(", ");
|
|
754
|
+
parts.push(`ORDER BY ${clause}`);
|
|
755
|
+
}
|
|
756
|
+
if (this._options.limit !== void 0 || this._options.offset !== void 0) {
|
|
757
|
+
const limitVal = this._options.limit !== void 0 ? Math.trunc(Math.max(0, this._options.limit)) : -1;
|
|
758
|
+
parts.push(`LIMIT ${limitVal}`);
|
|
759
|
+
if (this._options.offset !== void 0) {
|
|
760
|
+
parts.push(`OFFSET ${Math.trunc(Math.max(0, this._options.offset))}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return { sql: parts.join(" "), params: allParams };
|
|
513
764
|
}
|
|
514
765
|
/**
|
|
515
766
|
* Add a GROUP BY clause. Multiple columns are comma-separated.
|
|
@@ -546,7 +797,7 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
546
797
|
}));
|
|
547
798
|
const { sql, params } = buildSelect(
|
|
548
799
|
this.table.name,
|
|
549
|
-
this.
|
|
800
|
+
this._effectiveConditions(),
|
|
550
801
|
{ ...this._options, aggregates: aggClauses },
|
|
551
802
|
this._dialect
|
|
552
803
|
);
|
|
@@ -584,7 +835,7 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
584
835
|
const alias = "_agg";
|
|
585
836
|
const colExpr = col ? `"${col}"` : "*";
|
|
586
837
|
const { sql: whereSql, params } = buildWhere(
|
|
587
|
-
this.
|
|
838
|
+
this._effectiveConditions(),
|
|
588
839
|
this._dialect
|
|
589
840
|
);
|
|
590
841
|
let sqlStr;
|
|
@@ -610,10 +861,11 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
610
861
|
async select() {
|
|
611
862
|
let finalSql;
|
|
612
863
|
let finalParams;
|
|
864
|
+
const effectiveConditions = this._effectiveConditions();
|
|
613
865
|
if (this._rawWhere.length === 0) {
|
|
614
866
|
const { sql, params } = buildSelect(
|
|
615
867
|
this.table.name,
|
|
616
|
-
|
|
868
|
+
effectiveConditions,
|
|
617
869
|
this._options,
|
|
618
870
|
this._dialect
|
|
619
871
|
);
|
|
@@ -621,7 +873,7 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
621
873
|
finalParams = params;
|
|
622
874
|
} else {
|
|
623
875
|
const { sql: whereSql, params: whereParams } = buildWhere(
|
|
624
|
-
|
|
876
|
+
effectiveConditions,
|
|
625
877
|
this._dialect
|
|
626
878
|
);
|
|
627
879
|
const allWhereParts = [];
|
|
@@ -660,16 +912,103 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
660
912
|
finalSql = parts.join(" ");
|
|
661
913
|
finalParams = allParams;
|
|
662
914
|
}
|
|
663
|
-
const
|
|
915
|
+
const rawRows = await this.adapter.query(finalSql, finalParams);
|
|
916
|
+
let rows;
|
|
664
917
|
if (this._options.columns && this._options.columns.length > 0) {
|
|
665
|
-
|
|
918
|
+
rows = rawRows;
|
|
919
|
+
} else {
|
|
920
|
+
rows = rawRows.map((row) => deserializeRow(this.table, row));
|
|
666
921
|
}
|
|
667
|
-
|
|
922
|
+
if (this._withRelations.length === 0) return rows;
|
|
923
|
+
return this._executeWith(rows);
|
|
668
924
|
}
|
|
669
925
|
async first() {
|
|
670
926
|
const rows = await this.select();
|
|
671
927
|
return rows[0] ?? null;
|
|
672
928
|
}
|
|
929
|
+
// ── Eager loading — _executeWith ────────────────────────────────────────
|
|
930
|
+
async _executeWith(rows) {
|
|
931
|
+
if (rows.length === 0) return rows;
|
|
932
|
+
const mutableRows = rows.map((r) => ({ ...r }));
|
|
933
|
+
for (const relationName of this._withRelations) {
|
|
934
|
+
const meta = this.table.relations[relationName];
|
|
935
|
+
if (!meta) continue;
|
|
936
|
+
if (meta.kind === "manyToMany") {
|
|
937
|
+
throw new Error(
|
|
938
|
+
`manyToMany eager loading is not yet supported. Use loadRelation manually for relation '${relationName}'.`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (meta.kind === "belongsTo") {
|
|
942
|
+
await this._attachBelongsTo(mutableRows, relationName, meta);
|
|
943
|
+
} else if (meta.kind === "hasMany") {
|
|
944
|
+
await this._attachHasMany(mutableRows, relationName, meta);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return mutableRows;
|
|
948
|
+
}
|
|
949
|
+
async _attachBelongsTo(rows, relationName, meta) {
|
|
950
|
+
const foreignTable = meta.getTable();
|
|
951
|
+
const fkValues = rows.map((r) => r[meta.foreignKey]).filter((v) => v !== null && v !== void 0);
|
|
952
|
+
if (fkValues.length === 0) {
|
|
953
|
+
for (const r of rows) r[relationName] = null;
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const uniqueFkValues = [...new Set(fkValues)];
|
|
957
|
+
const pk = foreignTable.primaryKey;
|
|
958
|
+
const baseConditions = { [pk]: { op: "IN", value: uniqueFkValues } };
|
|
959
|
+
if (foreignTable.softDeleteColumn !== null) {
|
|
960
|
+
baseConditions[foreignTable.softDeleteColumn] = { op: "IS NULL" };
|
|
961
|
+
}
|
|
962
|
+
const { sql, params } = buildSelect(
|
|
963
|
+
foreignTable.name,
|
|
964
|
+
baseConditions,
|
|
965
|
+
{},
|
|
966
|
+
this._dialect
|
|
967
|
+
);
|
|
968
|
+
const related = await this.adapter.query(sql, params);
|
|
969
|
+
const relatedMap = /* @__PURE__ */ new Map();
|
|
970
|
+
for (const r of related) {
|
|
971
|
+
relatedMap.set(r[pk], deserializeRow(foreignTable, r));
|
|
972
|
+
}
|
|
973
|
+
for (const r of rows) {
|
|
974
|
+
r[relationName] = relatedMap.get(r[meta.foreignKey]) ?? null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async _attachHasMany(rows, relationName, meta) {
|
|
978
|
+
const foreignTable = meta.getTable();
|
|
979
|
+
const pk = this.table.primaryKey;
|
|
980
|
+
const pkValues = rows.map((r) => r[pk]).filter((v) => v !== null && v !== void 0);
|
|
981
|
+
if (pkValues.length === 0) {
|
|
982
|
+
for (const r of rows) r[relationName] = [];
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const hasManyConditions = { [meta.foreignKey]: { op: "IN", value: pkValues } };
|
|
986
|
+
if (foreignTable.softDeleteColumn !== null) {
|
|
987
|
+
hasManyConditions[foreignTable.softDeleteColumn] = { op: "IS NULL" };
|
|
988
|
+
}
|
|
989
|
+
const { sql, params } = buildSelect(
|
|
990
|
+
foreignTable.name,
|
|
991
|
+
hasManyConditions,
|
|
992
|
+
{},
|
|
993
|
+
this._dialect
|
|
994
|
+
);
|
|
995
|
+
const related = await this.adapter.query(sql, params);
|
|
996
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
997
|
+
for (const pkVal of pkValues) grouped.set(pkVal, []);
|
|
998
|
+
for (const r of related) {
|
|
999
|
+
const fkVal = r[meta.foreignKey];
|
|
1000
|
+
const deserialized = deserializeRow(foreignTable, r);
|
|
1001
|
+
const group = grouped.get(fkVal);
|
|
1002
|
+
if (group) {
|
|
1003
|
+
group.push(deserialized);
|
|
1004
|
+
} else {
|
|
1005
|
+
grouped.set(fkVal, [deserialized]);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
for (const r of rows) {
|
|
1009
|
+
r[relationName] = grouped.get(r[pk]) ?? [];
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
673
1012
|
async update(patch) {
|
|
674
1013
|
const hasConditions = !(Object.keys(this.conditions).length === 0 && this._rawWhere.length === 0);
|
|
675
1014
|
if (!hasConditions) {
|
|
@@ -697,6 +1036,58 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
697
1036
|
await this.hooks.runAfterUpdate(this.table, this.ctx, result, current, this.queue);
|
|
698
1037
|
return result;
|
|
699
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Update multiple rows atomically inside a single transaction.
|
|
1041
|
+
* Each row must include the primary key. beforeUpdate and afterUpdate hooks
|
|
1042
|
+
* run per row. If any row fails, the entire transaction rolls back.
|
|
1043
|
+
*
|
|
1044
|
+
* @example
|
|
1045
|
+
* const updated = await db.from(usersTable).updateMany([
|
|
1046
|
+
* { id: 1, name: 'Alice Updated' },
|
|
1047
|
+
* { id: 2, role: 'admin' },
|
|
1048
|
+
* ])
|
|
1049
|
+
*/
|
|
1050
|
+
async updateMany(rows) {
|
|
1051
|
+
if (rows.length === 0) return [];
|
|
1052
|
+
const pk = this.table.primaryKey;
|
|
1053
|
+
const results = await this.adapter.transaction(async (txAdapter) => {
|
|
1054
|
+
const txQueue = new RequestEventQueue();
|
|
1055
|
+
const inner = [];
|
|
1056
|
+
for (const row of rows) {
|
|
1057
|
+
const pkValue = row[pk];
|
|
1058
|
+
if (pkValue === void 0 || pkValue === null) {
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
`updateMany: row is missing primary key "${pk}" \u2014 every row must include the PK`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const selectSql = `SELECT * FROM "${this.table.name}" WHERE "${pk}" = ?`;
|
|
1064
|
+
const currentRows = await txAdapter.query(selectSql, [pkValue]);
|
|
1065
|
+
if (currentRows.length === 0) {
|
|
1066
|
+
throw new Error(`updateMany: record with ${pk}=${String(pkValue)} not found`);
|
|
1067
|
+
}
|
|
1068
|
+
const current = deserializeRow(this.table, currentRows[0]);
|
|
1069
|
+
const { [pk]: _pk, ...patchWithoutPk } = row;
|
|
1070
|
+
const patch = patchWithoutPk;
|
|
1071
|
+
const finalPatch = await this.hooks.runBeforeUpdate(this.table, this.ctx, current, patch);
|
|
1072
|
+
const { sql, params } = buildUpdate(
|
|
1073
|
+
this.table.name,
|
|
1074
|
+
finalPatch,
|
|
1075
|
+
pk,
|
|
1076
|
+
pkValue
|
|
1077
|
+
);
|
|
1078
|
+
await txAdapter.execute(sql, params);
|
|
1079
|
+
const updatedRow = {
|
|
1080
|
+
...current,
|
|
1081
|
+
...finalPatch
|
|
1082
|
+
};
|
|
1083
|
+
const result = deserializeRow(this.table, updatedRow);
|
|
1084
|
+
await this.hooks.runAfterUpdate(this.table, this.ctx, result, current, txQueue);
|
|
1085
|
+
inner.push(result);
|
|
1086
|
+
}
|
|
1087
|
+
return inner;
|
|
1088
|
+
});
|
|
1089
|
+
return results;
|
|
1090
|
+
}
|
|
700
1091
|
async delete() {
|
|
701
1092
|
const hasConditions = !(Object.keys(this.conditions).length === 0 && this._rawWhere.length === 0);
|
|
702
1093
|
if (!hasConditions) {
|
|
@@ -714,20 +1105,235 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
714
1105
|
await this.hooks.runAfterDelete(this.table, this.ctx, current, this.queue);
|
|
715
1106
|
return current;
|
|
716
1107
|
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Soft-delete rows by setting the soft-delete column to the current timestamp.
|
|
1110
|
+
* The table must have `.withSoftDelete()` configured — throws otherwise (at execute() time).
|
|
1111
|
+
*
|
|
1112
|
+
* Does NOT call beforeUpdate/afterUpdate hooks.
|
|
1113
|
+
* Without .where(), all rows in the table are soft-deleted.
|
|
1114
|
+
*
|
|
1115
|
+
* @example
|
|
1116
|
+
* await db.from(usersTable).softDelete().where({ id: 1 }).execute()
|
|
1117
|
+
*/
|
|
1118
|
+
softDelete() {
|
|
1119
|
+
return new SoftDeleteBuilder(this.adapter, this.table, /* @__PURE__ */ new Date(), this._dialect);
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Restore soft-deleted rows by setting the soft-delete column back to null.
|
|
1123
|
+
* The table must have `.withSoftDelete()` configured — throws otherwise (at execute() time).
|
|
1124
|
+
*
|
|
1125
|
+
* @example
|
|
1126
|
+
* await db.from(usersTable).restore().where({ id: 1 }).execute()
|
|
1127
|
+
*/
|
|
1128
|
+
restore() {
|
|
1129
|
+
return new SoftDeleteBuilder(this.adapter, this.table, null, this._dialect);
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
var ColumnRestrictedBuilder = class _ColumnRestrictedBuilder {
|
|
1133
|
+
// _builder is typed as SelectBuilder<unknown, ...> to avoid variance issues
|
|
1134
|
+
// when constructing from SelectBuilder<Pick<T,K>, ...> — the runtime shape is identical.
|
|
1135
|
+
constructor(_builder, _col) {
|
|
1136
|
+
this._builder = _builder;
|
|
1137
|
+
this._col = _col;
|
|
1138
|
+
}
|
|
1139
|
+
_builder;
|
|
1140
|
+
_col;
|
|
1141
|
+
where(conditions) {
|
|
1142
|
+
return new _ColumnRestrictedBuilder(
|
|
1143
|
+
this._builder.where(conditions),
|
|
1144
|
+
this._col
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
limit(n) {
|
|
1148
|
+
return new _ColumnRestrictedBuilder(
|
|
1149
|
+
this._builder.limit(n),
|
|
1150
|
+
this._col
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
orderBy(col, dir = "ASC") {
|
|
1154
|
+
return new _ColumnRestrictedBuilder(
|
|
1155
|
+
// _builder is SelectBuilder<unknown,...> so keyof unknown = never;
|
|
1156
|
+
// col is a valid schema key at runtime — cast is safe.
|
|
1157
|
+
this._builder.orderBy(col, dir),
|
|
1158
|
+
this._col
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Build the SQL for this query as a subquery fragment.
|
|
1163
|
+
* The result can be used directly in WHERE IN / NOT IN conditions.
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* const activeIds = db.from(usersTable).columns('id').where({ active: true }).subquery()
|
|
1167
|
+
* // → SubqueryResult<'id', number>
|
|
1168
|
+
*
|
|
1169
|
+
* const posts = await db.from(postsTable)
|
|
1170
|
+
* .where({ authorId: { op: 'IN', value: activeIds } })
|
|
1171
|
+
* .select()
|
|
1172
|
+
*/
|
|
1173
|
+
subquery() {
|
|
1174
|
+
const { sql, params } = this._builder._buildSelectSQL();
|
|
1175
|
+
return buildSubquery(sql, params, this._col);
|
|
1176
|
+
}
|
|
1177
|
+
/** Build raw SELECT SQL + params without parentheses (for UNION). */
|
|
1178
|
+
_buildRawSQL() {
|
|
1179
|
+
return this._builder._buildSelectSQL();
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Combine this query with another same-type column query via UNION (deduplicates).
|
|
1183
|
+
* Both sides must produce the same column type — enforced at compile time.
|
|
1184
|
+
*
|
|
1185
|
+
* @example
|
|
1186
|
+
* db.from(usersTable).columns('id')
|
|
1187
|
+
* .union(db.from(adminsTable).columns('id'))
|
|
1188
|
+
* .select()
|
|
1189
|
+
*/
|
|
1190
|
+
union(other) {
|
|
1191
|
+
return new UnionBuilder(
|
|
1192
|
+
[this._buildRawSQL(), other._buildRawSQL()],
|
|
1193
|
+
"UNION",
|
|
1194
|
+
this._builder._getAdapter(),
|
|
1195
|
+
this._builder._getDialect()
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Combine via UNION ALL — keeps duplicate rows.
|
|
1200
|
+
*/
|
|
1201
|
+
unionAll(other) {
|
|
1202
|
+
return new UnionBuilder(
|
|
1203
|
+
[this._buildRawSQL(), other._buildRawSQL()],
|
|
1204
|
+
"UNION ALL",
|
|
1205
|
+
this._builder._getAdapter(),
|
|
1206
|
+
this._builder._getDialect()
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
var SoftDeleteBuilder = class {
|
|
1211
|
+
constructor(adapter, table, _value, _dialect = "sqlite") {
|
|
1212
|
+
this.adapter = adapter;
|
|
1213
|
+
this.table = table;
|
|
1214
|
+
this._value = _value;
|
|
1215
|
+
this._dialect = _dialect;
|
|
1216
|
+
}
|
|
1217
|
+
adapter;
|
|
1218
|
+
table;
|
|
1219
|
+
_value;
|
|
1220
|
+
_dialect;
|
|
1221
|
+
_conditions = {};
|
|
1222
|
+
/**
|
|
1223
|
+
* Add WHERE conditions to scope which rows are soft-deleted / restored.
|
|
1224
|
+
* Multiple calls accumulate with AND.
|
|
1225
|
+
* Without .where(), all rows in the table are affected.
|
|
1226
|
+
*/
|
|
1227
|
+
where(conditions) {
|
|
1228
|
+
this._conditions = mergeWhereAnd(this._conditions, conditions);
|
|
1229
|
+
return this;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Execute the soft-delete or restore UPDATE.
|
|
1233
|
+
* Throws if the table has no softDeleteColumn configured.
|
|
1234
|
+
*/
|
|
1235
|
+
async execute() {
|
|
1236
|
+
const col = this.table.softDeleteColumn;
|
|
1237
|
+
if (col === null) {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`softDelete() called on table '${this.table.name}' which has no soft delete column. Add .withSoftDelete('deletedAt') to the table definition.`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const { sql, params } = buildSoftDeleteUpdate(
|
|
1243
|
+
this.table.name,
|
|
1244
|
+
col,
|
|
1245
|
+
this._value,
|
|
1246
|
+
this._conditions,
|
|
1247
|
+
this._dialect
|
|
1248
|
+
);
|
|
1249
|
+
await this.adapter.execute(sql, params);
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
var UnionBuilder = class _UnionBuilder {
|
|
1253
|
+
constructor(_parts, _kind, _adapter, _dialect) {
|
|
1254
|
+
this._parts = _parts;
|
|
1255
|
+
this._kind = _kind;
|
|
1256
|
+
this._adapter = _adapter;
|
|
1257
|
+
this._dialect = _dialect;
|
|
1258
|
+
}
|
|
1259
|
+
_parts;
|
|
1260
|
+
_kind;
|
|
1261
|
+
_adapter;
|
|
1262
|
+
_dialect;
|
|
1263
|
+
_orderBy;
|
|
1264
|
+
_limit;
|
|
1265
|
+
/** Append another UNION (deduplicating) leg. */
|
|
1266
|
+
union(other) {
|
|
1267
|
+
return new _UnionBuilder(
|
|
1268
|
+
[...this._parts, other._buildRawSQL()],
|
|
1269
|
+
"UNION",
|
|
1270
|
+
this._adapter,
|
|
1271
|
+
this._dialect
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
/** Append another UNION ALL (keep duplicates) leg. */
|
|
1275
|
+
unionAll(other) {
|
|
1276
|
+
return new _UnionBuilder(
|
|
1277
|
+
[...this._parts, other._buildRawSQL()],
|
|
1278
|
+
"UNION ALL",
|
|
1279
|
+
this._adapter,
|
|
1280
|
+
this._dialect
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
/** Add ORDER BY to the entire UNION result. */
|
|
1284
|
+
orderBy(col, dir = "ASC") {
|
|
1285
|
+
const next = new _UnionBuilder(this._parts, this._kind, this._adapter, this._dialect);
|
|
1286
|
+
next._orderBy = { col, dir };
|
|
1287
|
+
next._limit = this._limit;
|
|
1288
|
+
return next;
|
|
1289
|
+
}
|
|
1290
|
+
/** Add LIMIT to the entire UNION result. */
|
|
1291
|
+
limit(n) {
|
|
1292
|
+
const next = new _UnionBuilder(this._parts, this._kind, this._adapter, this._dialect);
|
|
1293
|
+
next._orderBy = this._orderBy;
|
|
1294
|
+
next._limit = n;
|
|
1295
|
+
return next;
|
|
1296
|
+
}
|
|
1297
|
+
/** Execute the UNION query and return typed rows. */
|
|
1298
|
+
async select() {
|
|
1299
|
+
const { sql, params } = buildUnion(this._parts, this._kind, {
|
|
1300
|
+
orderBy: this._orderBy,
|
|
1301
|
+
limit: this._limit
|
|
1302
|
+
});
|
|
1303
|
+
return this._adapter.query(sql, params);
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Build the UNION as a subquery — wrapped in parentheses.
|
|
1307
|
+
* Usable in WHERE IN / NOT IN conditions.
|
|
1308
|
+
*
|
|
1309
|
+
* @example
|
|
1310
|
+
* const adminOrModIds = db.from(usersTable).columns('id').where({ role: 'admin' })
|
|
1311
|
+
* .union(db.from(usersTable).columns('id').where({ role: 'mod' }))
|
|
1312
|
+
* .subquery()
|
|
1313
|
+
*/
|
|
1314
|
+
subquery() {
|
|
1315
|
+
const { sql, params } = buildUnion(this._parts, this._kind, {
|
|
1316
|
+
orderBy: this._orderBy,
|
|
1317
|
+
limit: this._limit
|
|
1318
|
+
});
|
|
1319
|
+
return buildSubquery(sql, params, "");
|
|
1320
|
+
}
|
|
717
1321
|
};
|
|
718
1322
|
var InsertBuilder = class {
|
|
719
|
-
constructor(adapter, hooks, ctx, queue, table) {
|
|
1323
|
+
constructor(adapter, hooks, ctx, queue, table, dialect = "sqlite") {
|
|
720
1324
|
this.adapter = adapter;
|
|
721
1325
|
this.hooks = hooks;
|
|
722
1326
|
this.ctx = ctx;
|
|
723
1327
|
this.queue = queue;
|
|
724
1328
|
this.table = table;
|
|
1329
|
+
this.dialect = dialect;
|
|
725
1330
|
}
|
|
726
1331
|
adapter;
|
|
727
1332
|
hooks;
|
|
728
1333
|
ctx;
|
|
729
1334
|
queue;
|
|
730
1335
|
table;
|
|
1336
|
+
dialect;
|
|
731
1337
|
async insert(data) {
|
|
732
1338
|
const originalInput = { ...data };
|
|
733
1339
|
let current = { ...data };
|
|
@@ -751,7 +1357,59 @@ var InsertBuilder = class {
|
|
|
751
1357
|
await this.hooks.runAfterInsert(this.table, this.ctx, result, originalInput, this.queue);
|
|
752
1358
|
return result;
|
|
753
1359
|
}
|
|
754
|
-
/**
|
|
1360
|
+
/**
|
|
1361
|
+
* Insert multiple rows in a single SQL statement.
|
|
1362
|
+
* beforeInsert and afterInsert hooks run per row.
|
|
1363
|
+
* Defaults (defaultFn / defaultValue) are applied per row.
|
|
1364
|
+
*
|
|
1365
|
+
* MySQL is not yet supported (no RETURNING *) — throws an informative error.
|
|
1366
|
+
*
|
|
1367
|
+
* @example
|
|
1368
|
+
* const users = await db.into(usersTable).insertMany([
|
|
1369
|
+
* { name: 'Alice', email: 'alice@example.com' },
|
|
1370
|
+
* { name: 'Bob', email: 'bob@example.com' },
|
|
1371
|
+
* ])
|
|
1372
|
+
*/
|
|
1373
|
+
async insertMany(data) {
|
|
1374
|
+
if (data.length === 0) return [];
|
|
1375
|
+
if (this.dialect === "mysql") {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
"insertMany is not yet supported for MySQL \u2014 MySQL does not support RETURNING *. Use individual insert() calls inside a transaction() instead."
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
const serializedRows = [];
|
|
1381
|
+
for (const row of data) {
|
|
1382
|
+
let processed = await this.hooks.runBeforeInsert(
|
|
1383
|
+
this.table,
|
|
1384
|
+
this.ctx,
|
|
1385
|
+
{ ...row }
|
|
1386
|
+
);
|
|
1387
|
+
for (const [field, col] of Object.entries(this.table.schema)) {
|
|
1388
|
+
if (col.def.primaryKey && col.def.autoIncrement) continue;
|
|
1389
|
+
if (processed[field] === void 0) {
|
|
1390
|
+
if (col.def.defaultFn !== void 0) {
|
|
1391
|
+
;
|
|
1392
|
+
processed[field] = col.def.defaultFn();
|
|
1393
|
+
} else if (col.def.defaultValue !== void 0) {
|
|
1394
|
+
;
|
|
1395
|
+
processed[field] = col.def.defaultValue;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
const serialized = this._serializeForInsert(processed);
|
|
1400
|
+
serializedRows.push(serialized);
|
|
1401
|
+
}
|
|
1402
|
+
const { sql, params } = buildInsertMany(this.table.name, serializedRows, true);
|
|
1403
|
+
const rawRows = await this.adapter.query(sql, params);
|
|
1404
|
+
const results = [];
|
|
1405
|
+
for (const rawRow of rawRows) {
|
|
1406
|
+
const deserialized = deserializeRow(this.table, rawRow);
|
|
1407
|
+
await this.hooks.runAfterInsert(this.table, this.ctx, deserialized, {}, this.queue);
|
|
1408
|
+
results.push(deserialized);
|
|
1409
|
+
}
|
|
1410
|
+
return results;
|
|
1411
|
+
}
|
|
1412
|
+
/** Serialize values for storage. Date → ISO string. Drops undefined values. */
|
|
755
1413
|
_serializeForInsert(data) {
|
|
756
1414
|
const result = {};
|
|
757
1415
|
for (const [key, val] of Object.entries(data)) {
|
|
@@ -882,8 +1540,11 @@ var JoinBuilder = class _JoinBuilder {
|
|
|
882
1540
|
};
|
|
883
1541
|
export {
|
|
884
1542
|
BoundVelnDB,
|
|
1543
|
+
ColumnRestrictedBuilder,
|
|
885
1544
|
InsertBuilder,
|
|
886
1545
|
JoinBuilder,
|
|
887
1546
|
SelectBuilder,
|
|
1547
|
+
SoftDeleteBuilder,
|
|
1548
|
+
UnionBuilder,
|
|
888
1549
|
VelnDB
|
|
889
1550
|
};
|