@ix-xs/node-comfort 2.0.0 → 2.1.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/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/SQLite.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
const nodePath = require("node:path");
|
|
25
25
|
const fs = require("node:fs");
|
|
26
26
|
const { SQLiteError } = require("./errors.js");
|
|
27
|
+
const { toMs } = require("./internal/shared.js");
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* A column type. `JSON` and `BOOLEAN` columns give you back objects and
|
|
@@ -44,7 +45,8 @@ const { SQLiteError } = require("./errors.js");
|
|
|
44
45
|
* @property {boolean} [autoincrement] With an `INTEGER` primary key: ids are never reused.
|
|
45
46
|
* @property {boolean} [notNull]
|
|
46
47
|
* @property {boolean} [unique]
|
|
47
|
-
* @property {unknown} [defaultValue] Objects work for JSON columns, booleans for BOOLEAN ones.
|
|
48
|
+
* @property {unknown} [defaultValue] Objects work for JSON columns, booleans for BOOLEAN ones. `"CURRENT_TIMESTAMP"`, `"CURRENT_DATE"` and `"CURRENT_TIME"` mean the moment of the insert.
|
|
49
|
+
* @property {string} [defaultExpression] A default computed by SQLite, like `"(unixepoch())"` or `"(lower(hex(randomblob(16))))"`. Written into the table as is: never build it from user input.
|
|
48
50
|
* @property {unknown[]} [values] Allowed values.
|
|
49
51
|
* @property {string} [check] A `CHECK` expression, like `"price >= 0"`.
|
|
50
52
|
* @property {{ table: string, column: string }} [references] Foreign key target.
|
|
@@ -99,12 +101,16 @@ const { SQLiteError } = require("./errors.js");
|
|
|
99
101
|
* `IS NULL`, an array means "one of", and an object uses operators. Keys
|
|
100
102
|
* combine with AND; use `$or` and `$and` for the rest.
|
|
101
103
|
*
|
|
104
|
+
* A key with dots looks inside a JSON column: `"settings.theme"` is the
|
|
105
|
+
* `theme` key of the `settings` column, `"tags.0"` the first item of `tags`.
|
|
106
|
+
*
|
|
102
107
|
* @example
|
|
103
108
|
* { status: "active", age: { gte: 18, lt: 65 }, role: ["admin", "editor"], deleted_at: null }
|
|
104
109
|
* { $or: [{ email: { like: "%@acme.com" } }, { vip: true }] }
|
|
110
|
+
* { "settings.theme": "dark", "settings.beta": true }
|
|
105
111
|
*
|
|
106
112
|
* @template [R=Record<string, any>] Row type; its keys are suggested as columns.
|
|
107
|
-
* @typedef {{ [K in keyof R]?: WhereValue } & { $or?: Where<R>[], $and?: Where<R>[] }} Where
|
|
113
|
+
* @typedef {{ [K in keyof R]?: WhereValue } & { [path: `${string}.${string}`]: WhereValue } & { $or?: Where<R>[], $and?: Where<R>[] }} Where
|
|
108
114
|
*/
|
|
109
115
|
|
|
110
116
|
/**
|
|
@@ -135,7 +141,8 @@ const { SQLiteError } = require("./errors.js");
|
|
|
135
141
|
* @property {boolean} [dates] Return `DATE`, `DATETIME` and `TIMESTAMP` columns as `Date`s.
|
|
136
142
|
* @property {boolean} [wal] Write-ahead logging: faster, and reads don't wait for writes. Defaults to `true`.
|
|
137
143
|
* @property {boolean} [foreignKeys] Enforce foreign keys. Defaults to `true`.
|
|
138
|
-
* @property {number} [busyTimeout] How long to wait for a lock held by another connection, in ms
|
|
144
|
+
* @property {number | string} [busyTimeout] How long to wait for a lock held by another connection, in ms or like `"5s"`. Defaults to 5 seconds.
|
|
145
|
+
* @property {"OFF" | "NORMAL" | "FULL" | "EXTRA"} [synchronous] How hard SQLite pushes each commit to the disk. With `wal`, `"NORMAL"` writes several times faster and can't corrupt the file; a power cut may undo the last commits. Defaults to SQLite's own setting, `"FULL"`.
|
|
139
146
|
* @property {boolean} [readOnly]
|
|
140
147
|
*/
|
|
141
148
|
|
|
@@ -172,9 +179,11 @@ const { SQLiteError } = require("./errors.js");
|
|
|
172
179
|
* @property {string} name
|
|
173
180
|
* @property {(where?: Where<R>, options?: Pick<QueryOptions, "columns" | "orderBy" | "direction">) => R | undefined} get The first matching row.
|
|
174
181
|
* @property {(where?: Where<R>, options?: QueryOptions) => R[]} getAll All matching rows.
|
|
175
|
-
* @property {(
|
|
176
|
-
* @property {(
|
|
182
|
+
* @property {(where?: Where<R>, options?: PaginateOptions) => import("./Arr").Page<R>} paginate One page of matching rows.
|
|
183
|
+
* @property {(data: Partial<R>, options?: InsertOptions) => InsertResult} insert Inserts a row.
|
|
184
|
+
* @property {(rows: Iterable<Partial<R>>, options?: InsertOptions) => ChangeResult} insertMany Inserts rows in one transaction.
|
|
177
185
|
* @property {(data: Partial<R>, where: Where<R>) => ChangeResult} update Updates matching rows.
|
|
186
|
+
* @property {(amounts: Partial<Record<keyof R & string, number>>, where: Where<R>) => ChangeResult} increment Adds to number columns.
|
|
178
187
|
* @property {(data: Partial<R>, conflict: string[]) => InsertResult} upsert Inserts, or updates on conflict.
|
|
179
188
|
* @property {(where: Where<R>) => ChangeResult} delete Deletes matching rows.
|
|
180
189
|
* @property {(where?: Where<R>) => number} count Counts matching rows.
|
|
@@ -182,7 +191,44 @@ const { SQLiteError } = require("./errors.js");
|
|
|
182
191
|
* @property {() => { ok: true }} clear Deletes every row.
|
|
183
192
|
*/
|
|
184
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Options for `insert()` and `insertMany()`.
|
|
196
|
+
* @typedef {object} InsertOptions
|
|
197
|
+
* @property {"ignore" | "replace"} [onConflict] What to do when a row clashes with an existing one: `"ignore"` skips it, `"replace"` deletes the old row first. By default, the insert fails.
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Options for `paginate()`.
|
|
202
|
+
* @typedef {object} PaginateOptions
|
|
203
|
+
* @property {number} [page] Starts at 1, which is the default.
|
|
204
|
+
* @property {number} [perPage] Defaults to `20`.
|
|
205
|
+
* @property {string | Array<string | [string, SortDirection]>} [orderBy] Give one: without it, SQLite is free to return the rows in any order.
|
|
206
|
+
* @property {SortDirection} [direction]
|
|
207
|
+
* @property {string[]} [columns]
|
|
208
|
+
*/
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Options for `transaction()`.
|
|
212
|
+
* @typedef {object} TransactionOptions
|
|
213
|
+
* @property {boolean} [immediate] Take the write lock right away, instead of on the first write. Use it for read-then-write work when several processes share the database: they then wait for each other instead of failing with `SQLITE_BUSY`.
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A prepared query and what's needed to turn its rows into objects.
|
|
218
|
+
* @typedef {{ stmt: import("node:sqlite").StatementSync, names: string[] | undefined, kinds: Uint8Array | undefined, table: string | undefined }} _Reader
|
|
219
|
+
*/
|
|
220
|
+
|
|
185
221
|
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
222
|
+
const PATH_PART = /^(?:[A-Za-z_][A-Za-z0-9_]*|\d+)$/;
|
|
223
|
+
const CURRENT = /^CURRENT_(?:TIMESTAMP|DATE|TIME)$/i;
|
|
224
|
+
const EXISTS = (/** @type {string} */ types) =>
|
|
225
|
+
`SELECT 1 FROM (SELECT name, type FROM sqlite_master UNION ALL SELECT name, type FROM sqlite_temp_master) WHERE type IN (${types}) AND name = ?`;
|
|
226
|
+
const TABLE_EXISTS = EXISTS("'table'");
|
|
227
|
+
const SOURCE_EXISTS = EXISTS("'table', 'view'");
|
|
228
|
+
// how a column is converted when read
|
|
229
|
+
const JSON_KIND = 1;
|
|
230
|
+
const BOOLEAN_KIND = 2;
|
|
231
|
+
const DATE_KIND = 3;
|
|
186
232
|
const OPERATORS = new Set(["eq", "ne", "gt", "gte", "lt", "lte", "in", "notIn", "like", "notLike", "glob", "between", "isNull"]);
|
|
187
233
|
const COMPARATORS = /** @type {Record<string, string>} */ ({ gt: ">", gte: ">=", lt: "<", lte: "<=", like: "LIKE", notLike: "NOT LIKE", glob: "GLOB" });
|
|
188
234
|
const ACTIONS = ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"];
|
|
@@ -291,6 +337,76 @@ const _wrap = (error, sql) => {
|
|
|
291
337
|
return new SQLiteError(sql ? `${message}\n in: ${sql.replace(/\s+/g, " ").trim()}` : message, { sql, sqliteCode, cause: error });
|
|
292
338
|
};
|
|
293
339
|
|
|
340
|
+
/**
|
|
341
|
+
* How a column of this declared type is converted when read, or 0.
|
|
342
|
+
* @param {string} type Upper case.
|
|
343
|
+
* @param {{ json: boolean, booleans: boolean, dates: boolean }} read
|
|
344
|
+
* @returns {number}
|
|
345
|
+
*/
|
|
346
|
+
const _kindOf = (type, read) => {
|
|
347
|
+
if (type === "JSON") return read.json ? JSON_KIND : 0;
|
|
348
|
+
if (type === "BOOLEAN" || type === "BOOL") return read.booleans ? BOOLEAN_KIND : 0;
|
|
349
|
+
return read.dates && (type === "DATE" || type === "DATETIME" || type === "TIMESTAMP") ? DATE_KIND : 0;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* @param {number} kind
|
|
354
|
+
* @param {unknown} value Not `null`.
|
|
355
|
+
* @returns {unknown}
|
|
356
|
+
*/
|
|
357
|
+
const _convert = (kind, value) => {
|
|
358
|
+
if (kind === JSON_KIND) {
|
|
359
|
+
if (typeof value !== "string") return value;
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(value);
|
|
362
|
+
} catch {
|
|
363
|
+
// not valid JSON: keep the text
|
|
364
|
+
return value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (kind === BOOLEAN_KIND) return typeof value === "number" || typeof value === "bigint" ? Number(value) !== 0 : value;
|
|
368
|
+
if (typeof value !== "string" && typeof value !== "number") return value;
|
|
369
|
+
// SQLite writes its own dates (CURRENT_TIMESTAMP, datetime('now')) in UTC and
|
|
370
|
+
// without saying so; read as they are, JavaScript would take them for local time.
|
|
371
|
+
const date = new Date(typeof value === "string" && SQLITE_DATETIME.test(value) ? `${value.replace(" ", "T")}Z` : value);
|
|
372
|
+
return Number.isNaN(date.getTime()) ? value : date;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const SQLITE_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?$/;
|
|
376
|
+
|
|
377
|
+
/** @type {Map<string, string>} */
|
|
378
|
+
const _columns = new Map();
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* A column in a filter or a sort: a name, or a path into a JSON column
|
|
382
|
+
* like `settings.theme`.
|
|
383
|
+
* @param {string} key
|
|
384
|
+
* @returns {string}
|
|
385
|
+
*/
|
|
386
|
+
const _column = (key) => {
|
|
387
|
+
const known = _columns.get(key);
|
|
388
|
+
if (known) return known;
|
|
389
|
+
let sql;
|
|
390
|
+
if (typeof key === "string" && key.includes(".")) {
|
|
391
|
+
const [name, ...path] = key.split(".");
|
|
392
|
+
if (!path.every((part) => PATH_PART.test(part))) throw new SQLiteError(`Invalid JSON path "${key}" (letters, digits and _ only between the dots)`);
|
|
393
|
+
sql = `json_extract(${_id(name, "column name")}, '$${path.map((part) => (/^\d+$/.test(part) ? `[${part}]` : `.${part}`)).join("")}')`;
|
|
394
|
+
} else {
|
|
395
|
+
sql = _id(key, "column name");
|
|
396
|
+
}
|
|
397
|
+
if (_columns.size >= 1000) _columns.clear();
|
|
398
|
+
_columns.set(key, sql);
|
|
399
|
+
return sql;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/** @param {InsertOptions | undefined} options */
|
|
403
|
+
const _insertVerb = (options) => {
|
|
404
|
+
const mode = options?.onConflict;
|
|
405
|
+
if (mode === undefined) return "INSERT";
|
|
406
|
+
if (mode !== "ignore" && mode !== "replace") throw new SQLiteError(`Invalid onConflict "${String(mode)}" (use "ignore" or "replace")`);
|
|
407
|
+
return mode === "ignore" ? "INSERT OR IGNORE" : "INSERT OR REPLACE";
|
|
408
|
+
};
|
|
409
|
+
|
|
294
410
|
/**
|
|
295
411
|
* Removes quoted strings and names, to look for `;` safely.
|
|
296
412
|
* @param {string} sql
|
|
@@ -308,8 +424,15 @@ class SQLite {
|
|
|
308
424
|
#db;
|
|
309
425
|
/** @type {Map<string, import("node:sqlite").StatementSync>} */
|
|
310
426
|
#statements = new Map();
|
|
311
|
-
/** @type {Map<string,
|
|
312
|
-
#
|
|
427
|
+
/** @type {Map<string, _Reader>} */
|
|
428
|
+
#readers = new Map();
|
|
429
|
+
// Tables and views known to exist, so that each call doesn't ask SQLite again.
|
|
430
|
+
/** @type {Set<string>} */
|
|
431
|
+
#known = new Set();
|
|
432
|
+
/** @type {Map<string, Map<string, number>>} */
|
|
433
|
+
#columnKinds = new Map();
|
|
434
|
+
/** @type {Map<string, string | undefined>} */
|
|
435
|
+
#sources = new Map();
|
|
313
436
|
/** @type {{ json: boolean, booleans: boolean, dates: boolean }} */
|
|
314
437
|
#read;
|
|
315
438
|
#depth = 0;
|
|
@@ -337,8 +460,13 @@ class SQLite {
|
|
|
337
460
|
if (!memory && !options.readOnly) fs.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
338
461
|
try {
|
|
339
462
|
this.#db = new DatabaseSync(target, { readOnly: options.readOnly ?? false });
|
|
340
|
-
this.#db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(
|
|
463
|
+
this.#db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(toMs(options.busyTimeout ?? 5000, "busyTimeout")) || 0)}`);
|
|
341
464
|
if (!memory && !options.readOnly && (options.wal ?? true)) this.#db.exec("PRAGMA journal_mode = WAL");
|
|
465
|
+
if (options.synchronous !== undefined) {
|
|
466
|
+
const level = String(options.synchronous).toUpperCase();
|
|
467
|
+
if (!["OFF", "NORMAL", "FULL", "EXTRA"].includes(level)) throw new SQLiteError(`Invalid synchronous "${options.synchronous}" (use "OFF", "NORMAL", "FULL" or "EXTRA")`);
|
|
468
|
+
this.#db.exec(`PRAGMA synchronous = ${level}`);
|
|
469
|
+
}
|
|
342
470
|
this.#db.exec(`PRAGMA foreign_keys = ${options.foreignKeys === false ? "OFF" : "ON"}`);
|
|
343
471
|
} catch (error) {
|
|
344
472
|
throw _wrap(error);
|
|
@@ -348,6 +476,7 @@ class SQLite {
|
|
|
348
476
|
}
|
|
349
477
|
|
|
350
478
|
/**
|
|
479
|
+
* A prepared statement, kept for the next call.
|
|
351
480
|
* @param {string} sql
|
|
352
481
|
* @returns {import("node:sqlite").StatementSync}
|
|
353
482
|
*/
|
|
@@ -364,6 +493,118 @@ class SQLite {
|
|
|
364
493
|
return stmt;
|
|
365
494
|
}
|
|
366
495
|
|
|
496
|
+
/**
|
|
497
|
+
* A prepared query. Where Node allows it, rows are fetched as arrays and
|
|
498
|
+
* turned into objects here: it's faster than copying the objects Node
|
|
499
|
+
* returns, and Node says which column each value comes from.
|
|
500
|
+
* @param {string} sql
|
|
501
|
+
* @param {boolean} [keep=true] `false` for a statement used once, like an iteration.
|
|
502
|
+
* @returns {_Reader}
|
|
503
|
+
*/
|
|
504
|
+
#reader(sql, keep = true) {
|
|
505
|
+
let reader = keep ? this.#readers.get(sql) : undefined;
|
|
506
|
+
if (reader) return reader;
|
|
507
|
+
let stmt;
|
|
508
|
+
try {
|
|
509
|
+
stmt = this.#db.prepare(sql);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
throw _wrap(error, sql);
|
|
512
|
+
}
|
|
513
|
+
reader = { stmt, names: undefined, kinds: undefined, table: undefined };
|
|
514
|
+
const modern = /** @type {any} */ (stmt);
|
|
515
|
+
if (typeof modern.setReturnArrays === "function" && typeof modern.columns === "function") {
|
|
516
|
+
modern.setReturnArrays(true);
|
|
517
|
+
this.#describe(reader);
|
|
518
|
+
} else {
|
|
519
|
+
reader.table = this.#sourceTable(sql);
|
|
520
|
+
}
|
|
521
|
+
if (keep) {
|
|
522
|
+
if (this.#readers.size >= 500) this.#readers.delete(/** @type {string} */ (this.#readers.keys().next().value));
|
|
523
|
+
this.#readers.set(sql, reader);
|
|
524
|
+
}
|
|
525
|
+
return reader;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Reads the column names of a query, and how to convert each one.
|
|
530
|
+
* @param {_Reader} reader
|
|
531
|
+
*/
|
|
532
|
+
#describe(reader) {
|
|
533
|
+
/** @type {Array<{ name: string, type: string | null }>} */
|
|
534
|
+
const columns = /** @type {any} */ (reader.stmt).columns();
|
|
535
|
+
reader.names = columns.map((column) => column.name);
|
|
536
|
+
reader.kinds = undefined;
|
|
537
|
+
for (let i = 0; i < columns.length; i++) {
|
|
538
|
+
const type = columns[i].type;
|
|
539
|
+
const kind = type ? _kindOf(String(type).toUpperCase(), this.#read) : 0;
|
|
540
|
+
if (kind) (reader.kinds ??= new Uint8Array(columns.length))[i] = kind;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @param {_Reader} reader
|
|
546
|
+
* @param {any} raw What the statement returned for one row.
|
|
547
|
+
* @returns {any}
|
|
548
|
+
*/
|
|
549
|
+
#row(reader, raw) {
|
|
550
|
+
/** @type {Record<string, unknown>} */
|
|
551
|
+
const row = {};
|
|
552
|
+
if (reader.names === undefined) {
|
|
553
|
+
// Node before 22.16: a row is an object without a prototype, copied into a plain one.
|
|
554
|
+
for (const key in raw) row[key] = raw[key];
|
|
555
|
+
if (reader.table === undefined) return row;
|
|
556
|
+
for (const [key, kind] of this.#kinds(reader.table)) {
|
|
557
|
+
const value = row[key];
|
|
558
|
+
if (value !== null && value !== undefined) row[key] = _convert(kind, value);
|
|
559
|
+
}
|
|
560
|
+
return row;
|
|
561
|
+
}
|
|
562
|
+
// A statement prepared before the table changed can come back with other columns.
|
|
563
|
+
if (raw.length !== reader.names.length) this.#describe(reader);
|
|
564
|
+
const names = reader.names;
|
|
565
|
+
const kinds = reader.kinds;
|
|
566
|
+
if (kinds === undefined) {
|
|
567
|
+
for (let i = 0; i < names.length; i++) row[names[i]] = raw[i];
|
|
568
|
+
return row;
|
|
569
|
+
}
|
|
570
|
+
for (let i = 0; i < names.length; i++) {
|
|
571
|
+
const value = raw[i];
|
|
572
|
+
row[names[i]] = kinds[i] === 0 || value === null ? value : _convert(kinds[i], value);
|
|
573
|
+
}
|
|
574
|
+
return row;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* @param {string} sql
|
|
579
|
+
* @param {any[]} params
|
|
580
|
+
* @returns {any}
|
|
581
|
+
*/
|
|
582
|
+
#one(sql, params) {
|
|
583
|
+
const reader = this.#reader(sql);
|
|
584
|
+
try {
|
|
585
|
+
const raw = reader.stmt.get(...params);
|
|
586
|
+
return raw === undefined ? undefined : this.#row(reader, raw);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
throw _wrap(error, sql);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* @param {string} sql
|
|
594
|
+
* @param {any[]} params
|
|
595
|
+
* @returns {any[]}
|
|
596
|
+
*/
|
|
597
|
+
#all(sql, params) {
|
|
598
|
+
const reader = this.#reader(sql);
|
|
599
|
+
try {
|
|
600
|
+
const rows = reader.stmt.all(...params);
|
|
601
|
+
for (let i = 0; i < rows.length; i++) rows[i] = this.#row(reader, rows[i]);
|
|
602
|
+
return rows;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
throw _wrap(error, sql);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
367
608
|
/**
|
|
368
609
|
* @param {unknown} params
|
|
369
610
|
* @returns {any[]}
|
|
@@ -376,60 +617,34 @@ class SQLite {
|
|
|
376
617
|
}
|
|
377
618
|
|
|
378
619
|
/**
|
|
620
|
+
* Forgets what was learned about the schema, after it changed.
|
|
379
621
|
* @returns {void}
|
|
380
622
|
*/
|
|
381
623
|
#invalidate() {
|
|
382
|
-
this.#
|
|
624
|
+
this.#columnKinds.clear();
|
|
383
625
|
this.#statements.clear();
|
|
626
|
+
this.#readers.clear();
|
|
627
|
+
this.#known.clear();
|
|
628
|
+
this.#sources.clear();
|
|
384
629
|
}
|
|
385
630
|
|
|
386
631
|
/**
|
|
387
|
-
*
|
|
632
|
+
* The columns of a table that are converted when read. Only used on Node
|
|
633
|
+
* versions that don't describe the columns of a query.
|
|
388
634
|
* @param {string} table
|
|
389
|
-
* @returns {Map<string,
|
|
635
|
+
* @returns {Map<string, number>}
|
|
390
636
|
*/
|
|
391
|
-
#
|
|
392
|
-
let
|
|
393
|
-
if (!
|
|
394
|
-
|
|
637
|
+
#kinds(table) {
|
|
638
|
+
let kinds = this.#columnKinds.get(table);
|
|
639
|
+
if (!kinds) {
|
|
640
|
+
kinds = new Map();
|
|
395
641
|
for (const col of /** @type {Array<{ name: string, type: string }>} */ (/** @type {unknown} */ (this.#prepare(`PRAGMA table_info(${_id(table, "table name")})`).all()))) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
this.#columnTypes.set(table, types);
|
|
399
|
-
}
|
|
400
|
-
return types;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* @param {unknown} raw
|
|
405
|
-
* @param {string | undefined} table
|
|
406
|
-
* @returns {any}
|
|
407
|
-
*/
|
|
408
|
-
#row(raw, table) {
|
|
409
|
-
if (!raw) return undefined;
|
|
410
|
-
/** @type {Record<string, unknown>} */
|
|
411
|
-
const row = { ...raw };
|
|
412
|
-
if (!table || !(this.#read.json || this.#read.booleans || this.#read.dates)) return row;
|
|
413
|
-
const types = this.#types(table);
|
|
414
|
-
for (const [key, value] of Object.entries(row)) {
|
|
415
|
-
const type = types.get(key);
|
|
416
|
-
if (!type || value === null) continue;
|
|
417
|
-
if (type === "JSON") {
|
|
418
|
-
if (this.#read.json && typeof value === "string") {
|
|
419
|
-
try {
|
|
420
|
-
row[key] = JSON.parse(value);
|
|
421
|
-
} catch {
|
|
422
|
-
// not valid JSON: keep the text
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
} else if (type === "BOOLEAN" || type === "BOOL") {
|
|
426
|
-
if (this.#read.booleans && (typeof value === "number" || typeof value === "bigint")) row[key] = Number(value) !== 0;
|
|
427
|
-
} else if (this.#read.dates && (type === "DATE" || type === "DATETIME" || type === "TIMESTAMP") && (typeof value === "string" || typeof value === "number")) {
|
|
428
|
-
const date = new Date(value);
|
|
429
|
-
if (!Number.isNaN(date.getTime())) row[key] = date;
|
|
642
|
+
const kind = _kindOf(String(col.type).toUpperCase(), this.#read);
|
|
643
|
+
if (kind) kinds.set(col.name, kind);
|
|
430
644
|
}
|
|
645
|
+
this.#columnKinds.set(table, kinds);
|
|
431
646
|
}
|
|
432
|
-
return
|
|
647
|
+
return kinds;
|
|
433
648
|
}
|
|
434
649
|
|
|
435
650
|
/**
|
|
@@ -456,7 +671,7 @@ class SQLite {
|
|
|
456
671
|
parts.push(subs.length ? `(${subs.map((s) => `(${s})`).join(key === "$or" ? " OR " : " AND ")})` : key === "$or" ? "0" : "1");
|
|
457
672
|
continue;
|
|
458
673
|
}
|
|
459
|
-
const col =
|
|
674
|
+
const col = _column(key);
|
|
460
675
|
if (value === null) parts.push(`${col} IS NULL`);
|
|
461
676
|
else if (Array.isArray(value)) parts.push(this.#op(col, "in", value, params));
|
|
462
677
|
else if (_isPlain(value) && Object.keys(value).some((k) => OPERATORS.has(k))) {
|
|
@@ -531,8 +746,8 @@ class SQLite {
|
|
|
531
746
|
if (upper !== "ASC" && upper !== "DESC") throw new SQLiteError(`Invalid sort direction "${String(direction)}" (use "ASC" or "DESC")`);
|
|
532
747
|
return upper;
|
|
533
748
|
};
|
|
534
|
-
if (!Array.isArray(options.orderBy)) return ` ORDER BY ${
|
|
535
|
-
return ` ORDER BY ${options.orderBy.map((item) => (Array.isArray(item) ? `${
|
|
749
|
+
if (!Array.isArray(options.orderBy)) return ` ORDER BY ${_column(options.orderBy)} ${dir(options.direction)}`;
|
|
750
|
+
return ` ORDER BY ${options.orderBy.map((item) => (Array.isArray(item) ? `${_column(item[0])} ${dir(item[1])}` : `${_column(item)} ASC`)).join(", ")}`;
|
|
536
751
|
}
|
|
537
752
|
|
|
538
753
|
/**
|
|
@@ -552,7 +767,10 @@ class SQLite {
|
|
|
552
767
|
*/
|
|
553
768
|
#table(table) {
|
|
554
769
|
const quoted = _id(table, "table name");
|
|
555
|
-
if (!this.
|
|
770
|
+
if (!this.#known.has(table)) {
|
|
771
|
+
if (this.#prepare(SOURCE_EXISTS).get(table) === undefined) throw new SQLiteError(`Table "${table}" does not exist`);
|
|
772
|
+
this.#known.add(table);
|
|
773
|
+
}
|
|
556
774
|
return quoted;
|
|
557
775
|
}
|
|
558
776
|
|
|
@@ -577,9 +795,17 @@ class SQLite {
|
|
|
577
795
|
* @returns {string | undefined}
|
|
578
796
|
*/
|
|
579
797
|
#sourceTable(sql) {
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
798
|
+
if (this.#sources.has(sql)) return this.#sources.get(sql);
|
|
799
|
+
/** @type {string | undefined} */
|
|
800
|
+
let table;
|
|
801
|
+
if (!/\bjoin\b/i.test(sql)) {
|
|
802
|
+
const match = /^\s*select\b[\s\S]*?\bfrom\s+"?([A-Za-z_][A-Za-z0-9_]*)"?(?:\s+(?:as\s+)?(?!where\b|order\b|limit\b|group\b)[A-Za-z_]\w*)?\s*(?:where\b|order\b|limit\b|group\b|;|$)/i.exec(sql);
|
|
803
|
+
// A view has column types too: SQLite takes them from the tables it reads.
|
|
804
|
+
if (match && (this.#known.has(match[1]) || this.#prepare(SOURCE_EXISTS).get(match[1]) !== undefined)) table = match[1];
|
|
805
|
+
}
|
|
806
|
+
if (this.#sources.size >= 500) this.#sources.clear();
|
|
807
|
+
this.#sources.set(sql, table);
|
|
808
|
+
return table;
|
|
583
809
|
}
|
|
584
810
|
|
|
585
811
|
/**
|
|
@@ -617,8 +843,9 @@ class SQLite {
|
|
|
617
843
|
}
|
|
618
844
|
|
|
619
845
|
/**
|
|
620
|
-
* Runs a query and returns the first row.
|
|
621
|
-
*
|
|
846
|
+
* Runs a query and returns the first row. JSON and boolean columns are
|
|
847
|
+
* converted like in `get()`, joins and renamed columns included. On Node.js
|
|
848
|
+
* before 22.16, that only works for a query on a single table.
|
|
622
849
|
*
|
|
623
850
|
* @example
|
|
624
851
|
* const user = db.queryOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
@@ -630,12 +857,7 @@ class SQLite {
|
|
|
630
857
|
* @throws {SQLiteError}
|
|
631
858
|
*/
|
|
632
859
|
queryOne(sql, params) {
|
|
633
|
-
|
|
634
|
-
try {
|
|
635
|
-
return this.#row(stmt.get(...this.#params(params)), this.#sourceTable(sql));
|
|
636
|
-
} catch (error) {
|
|
637
|
-
throw _wrap(error, sql);
|
|
638
|
-
}
|
|
860
|
+
return this.#one(sql, this.#params(params));
|
|
639
861
|
}
|
|
640
862
|
|
|
641
863
|
/**
|
|
@@ -651,13 +873,7 @@ class SQLite {
|
|
|
651
873
|
* @throws {SQLiteError}
|
|
652
874
|
*/
|
|
653
875
|
queryAll(sql, params) {
|
|
654
|
-
|
|
655
|
-
try {
|
|
656
|
-
const table = this.#sourceTable(sql);
|
|
657
|
-
return stmt.all(...this.#params(params)).map((row) => this.#row(row, table));
|
|
658
|
-
} catch (error) {
|
|
659
|
-
throw _wrap(error, sql);
|
|
660
|
-
}
|
|
876
|
+
return this.#all(sql, this.#params(params));
|
|
661
877
|
}
|
|
662
878
|
|
|
663
879
|
/**
|
|
@@ -676,14 +892,9 @@ class SQLite {
|
|
|
676
892
|
* @throws {SQLiteError}
|
|
677
893
|
*/
|
|
678
894
|
*iterate(sql, params) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
stmt = this.#db.prepare(sql);
|
|
683
|
-
} catch (error) {
|
|
684
|
-
throw _wrap(error, sql);
|
|
685
|
-
}
|
|
686
|
-
for (const row of stmt.iterate(...this.#params(params))) yield this.#row(row, table);
|
|
895
|
+
// Not kept: a statement being walked through can't serve another call at the same time.
|
|
896
|
+
const reader = this.#reader(sql, false);
|
|
897
|
+
for (const raw of reader.stmt.iterate(...this.#params(params))) yield this.#row(reader, raw);
|
|
687
898
|
}
|
|
688
899
|
|
|
689
900
|
/**
|
|
@@ -718,7 +929,11 @@ class SQLite {
|
|
|
718
929
|
if (def.primaryKey) sql += ` PRIMARY KEY${def.autoincrement ? " AUTOINCREMENT" : ""}`;
|
|
719
930
|
if (def.notNull) sql += " NOT NULL";
|
|
720
931
|
if (def.unique) sql += " UNIQUE";
|
|
721
|
-
if (def.
|
|
932
|
+
if (def.defaultExpression !== undefined) sql += ` DEFAULT (${def.defaultExpression})`;
|
|
933
|
+
else if (def.defaultValue !== undefined) {
|
|
934
|
+
// Written bare, these are the clock of the insert. Quoted, they would be plain text.
|
|
935
|
+
sql += ` DEFAULT ${typeof def.defaultValue === "string" && CURRENT.test(def.defaultValue) ? def.defaultValue.toUpperCase() : _literal(def.defaultValue)}`;
|
|
936
|
+
}
|
|
722
937
|
if (def.values?.length) sql += ` CHECK (${col} IN (${def.values.map(_literal).join(", ")}))`;
|
|
723
938
|
if (def.check) sql += ` CHECK (${def.check})`;
|
|
724
939
|
if (def.references) {
|
|
@@ -825,7 +1040,7 @@ class SQLite {
|
|
|
825
1040
|
*/
|
|
826
1041
|
hasTable(tableName) {
|
|
827
1042
|
if (typeof tableName !== "string" || !IDENTIFIER.test(tableName)) return false;
|
|
828
|
-
return this.#prepare(
|
|
1043
|
+
return this.#prepare(TABLE_EXISTS).get(tableName) !== undefined;
|
|
829
1044
|
}
|
|
830
1045
|
|
|
831
1046
|
/**
|
|
@@ -873,7 +1088,8 @@ class SQLite {
|
|
|
873
1088
|
* @throws {SQLiteError}
|
|
874
1089
|
*/
|
|
875
1090
|
get(tableName, where = {}, options = {}) {
|
|
876
|
-
|
|
1091
|
+
const query = this.#select(tableName, where, options, true);
|
|
1092
|
+
return this.#one(query.sql, query.params);
|
|
877
1093
|
}
|
|
878
1094
|
|
|
879
1095
|
/**
|
|
@@ -896,21 +1112,56 @@ class SQLite {
|
|
|
896
1112
|
* @throws {SQLiteError}
|
|
897
1113
|
*/
|
|
898
1114
|
getAll(tableName, where = {}, options = {}) {
|
|
1115
|
+
const query = this.#select(tableName, where, options, false);
|
|
1116
|
+
return this.#all(query.sql, query.params);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* @param {string} tableName
|
|
1121
|
+
* @param {Where<any>} where
|
|
1122
|
+
* @param {QueryOptions} options
|
|
1123
|
+
* @param {boolean} single
|
|
1124
|
+
* @returns {{ sql: string, params: any[] }}
|
|
1125
|
+
*/
|
|
1126
|
+
#select(tableName, where, options, single) {
|
|
899
1127
|
const table = this.#table(tableName);
|
|
900
1128
|
const columns = options.columns?.length ? options.columns.map((c) => _id(c, "column name")).join(", ") : "*";
|
|
901
1129
|
const clause = this.#where(where);
|
|
902
1130
|
let sql = `SELECT ${columns} FROM ${table}${clause.sql}${this.#orderBy(options)}`;
|
|
903
|
-
|
|
904
|
-
|
|
1131
|
+
if (single) {
|
|
1132
|
+
sql += " LIMIT 1";
|
|
1133
|
+
} else if (options.limit !== undefined || options.offset !== undefined) {
|
|
905
1134
|
sql += " LIMIT ? OFFSET ?";
|
|
906
|
-
params.push(options.limit === undefined ? -1 : Math.max(0, Math.floor(options.limit)), Math.max(0, Math.floor(options.offset ?? 0)));
|
|
907
|
-
}
|
|
908
|
-
const stmt = this.#prepare(sql);
|
|
909
|
-
try {
|
|
910
|
-
return stmt.all(...params).map((row) => this.#row(row, tableName));
|
|
911
|
-
} catch (error) {
|
|
912
|
-
throw _wrap(error, sql);
|
|
1135
|
+
clause.params.push(options.limit === undefined ? -1 : Math.max(0, Math.floor(options.limit)), Math.max(0, Math.floor(options.offset ?? 0)));
|
|
913
1136
|
}
|
|
1137
|
+
return { sql, params: clause.params };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* One page of the rows that match, with what a pager needs to draw itself.
|
|
1142
|
+
*
|
|
1143
|
+
* @example
|
|
1144
|
+
* const { items, pages, hasNext } = db.paginate("posts", { status: "published" }, {
|
|
1145
|
+
* page: 3,
|
|
1146
|
+
* perPage: 20,
|
|
1147
|
+
* orderBy: "created_at",
|
|
1148
|
+
* direction: "DESC",
|
|
1149
|
+
* });
|
|
1150
|
+
*
|
|
1151
|
+
* @template [T=R]
|
|
1152
|
+
* @param {string} tableName
|
|
1153
|
+
* @param {Where<R>} [where]
|
|
1154
|
+
* @param {PaginateOptions} [options]
|
|
1155
|
+
* @returns {import("./Arr").Page<T>} A page past the end gives the last one.
|
|
1156
|
+
* @throws {SQLiteError}
|
|
1157
|
+
*/
|
|
1158
|
+
paginate(tableName, where = {}, options = {}) {
|
|
1159
|
+
const perPage = Math.max(1, Math.floor(options.perPage ?? 20) || 20);
|
|
1160
|
+
const total = this.count(tableName, where);
|
|
1161
|
+
const pages = Math.max(1, Math.ceil(total / perPage));
|
|
1162
|
+
const page = Math.min(Math.max(1, Math.floor(options.page ?? 1) || 1), pages);
|
|
1163
|
+
const items = total ? this.getAll(tableName, where, { orderBy: options.orderBy, direction: options.direction, columns: options.columns, limit: perPage, offset: (page - 1) * perPage }) : [];
|
|
1164
|
+
return { items: /** @type {any[]} */ (items), page, perPage, total, pages, hasPrev: page > 1, hasNext: page < pages };
|
|
914
1165
|
}
|
|
915
1166
|
|
|
916
1167
|
/**
|
|
@@ -964,17 +1215,19 @@ class SQLite {
|
|
|
964
1215
|
*
|
|
965
1216
|
* @example
|
|
966
1217
|
* const { lastInsertRowid } = db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
|
|
1218
|
+
* db.insert("tags", { label: "news" }, { onConflict: "ignore" }); // no error if it's already there
|
|
967
1219
|
*
|
|
968
1220
|
* @param {string} tableName
|
|
969
1221
|
* @param {Partial<R> & Record<string, unknown>} data
|
|
1222
|
+
* @param {InsertOptions} [options]
|
|
970
1223
|
* @returns {InsertResult}
|
|
971
1224
|
* @throws {SQLiteError} On a constraint failure; `sqliteCode` tells you which, like `"SQLITE_CONSTRAINT_UNIQUE"`.
|
|
972
1225
|
*/
|
|
973
|
-
insert(tableName, data) {
|
|
1226
|
+
insert(tableName, data, options) {
|
|
974
1227
|
const table = this.#table(tableName);
|
|
975
1228
|
const [cols, values] = this.#values(data);
|
|
976
|
-
const
|
|
977
|
-
return this.#run(
|
|
1229
|
+
const verb = _insertVerb(options);
|
|
1230
|
+
return this.#run(cols.length ? `${verb} INTO ${table} (${cols.join(", ")}) VALUES (${cols.map(() => "?").join(", ")})` : `${verb} INTO ${table} DEFAULT VALUES`, values);
|
|
978
1231
|
}
|
|
979
1232
|
|
|
980
1233
|
/**
|
|
@@ -983,17 +1236,47 @@ class SQLite {
|
|
|
983
1236
|
*
|
|
984
1237
|
* @example
|
|
985
1238
|
* db.insertMany("products", rowsFromCsv);
|
|
1239
|
+
* db.insertMany("tags", labels.map((label) => ({ label })), { onConflict: "ignore" });
|
|
986
1240
|
*
|
|
987
1241
|
* @param {string} tableName
|
|
988
|
-
* @param {
|
|
1242
|
+
* @param {Iterable<Partial<R> & Record<string, unknown>>} rows
|
|
1243
|
+
* @param {InsertOptions} [options]
|
|
989
1244
|
* @returns {ChangeResult}
|
|
990
1245
|
* @throws {SQLiteError} If a row fails; nothing is inserted then.
|
|
991
1246
|
*/
|
|
992
|
-
insertMany(tableName, rows) {
|
|
993
|
-
this.#table(tableName);
|
|
1247
|
+
insertMany(tableName, rows, options) {
|
|
1248
|
+
const table = this.#table(tableName);
|
|
1249
|
+
const verb = _insertVerb(options);
|
|
994
1250
|
return this.transaction(() => {
|
|
995
1251
|
let changes = 0;
|
|
996
|
-
|
|
1252
|
+
let shape = "";
|
|
1253
|
+
let sql = "";
|
|
1254
|
+
/** @type {import("node:sqlite").StatementSync | undefined} */
|
|
1255
|
+
let stmt;
|
|
1256
|
+
for (const row of rows) {
|
|
1257
|
+
if (!_isPlain(row)) throw new SQLiteError("Row data must be a plain object");
|
|
1258
|
+
const keys = [];
|
|
1259
|
+
const values = [];
|
|
1260
|
+
for (const key of Object.keys(row)) {
|
|
1261
|
+
const value = /** @type {Record<string, unknown>} */ (row)[key];
|
|
1262
|
+
if (value === undefined) continue;
|
|
1263
|
+
keys.push(key);
|
|
1264
|
+
values.push(_toSQL(value));
|
|
1265
|
+
}
|
|
1266
|
+
// Rows of one import nearly always have the same columns: the statement is prepared once for all of them.
|
|
1267
|
+
const current = keys.join(",");
|
|
1268
|
+
if (stmt === undefined || current !== shape) {
|
|
1269
|
+
shape = current;
|
|
1270
|
+
const cols = keys.map((key) => _id(key, "column name"));
|
|
1271
|
+
sql = cols.length ? `${verb} INTO ${table} (${cols.join(", ")}) VALUES (${cols.map(() => "?").join(", ")})` : `${verb} INTO ${table} DEFAULT VALUES`;
|
|
1272
|
+
stmt = this.#prepare(sql);
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
changes += Number(stmt.run(...values).changes);
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
throw _wrap(error, sql);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
997
1280
|
return { ok: /** @type {const} */ (true), changes };
|
|
998
1281
|
});
|
|
999
1282
|
}
|
|
@@ -1086,6 +1369,38 @@ class SQLite {
|
|
|
1086
1369
|
return { ok: true, changes };
|
|
1087
1370
|
}
|
|
1088
1371
|
|
|
1372
|
+
/**
|
|
1373
|
+
* Adds to number columns in one statement, so two requests at the same
|
|
1374
|
+
* moment both count. Reading the value, adding to it and writing it back
|
|
1375
|
+
* would lose one of them. Negative numbers subtract; an empty column
|
|
1376
|
+
* counts as 0.
|
|
1377
|
+
*
|
|
1378
|
+
* @example
|
|
1379
|
+
* db.increment("posts", { views: 1 }, { id: post.id });
|
|
1380
|
+
* db.increment("accounts", { balance: -25, withdrawals: 1 }, { id: account.id });
|
|
1381
|
+
*
|
|
1382
|
+
* @param {string} tableName
|
|
1383
|
+
* @param {Partial<Record<keyof R & string, number>> & Record<string, number>} amounts Column names and how much to add.
|
|
1384
|
+
* @param {Where<R>} where
|
|
1385
|
+
* @returns {ChangeResult}
|
|
1386
|
+
* @throws {SQLiteError}
|
|
1387
|
+
*/
|
|
1388
|
+
increment(tableName, amounts, where) {
|
|
1389
|
+
const table = this.#table(tableName);
|
|
1390
|
+
if (!_isPlain(where) || !Object.keys(where).length) throw new SQLiteError("increment() requires a non-empty where filter");
|
|
1391
|
+
if (!_isPlain(amounts)) throw new SQLiteError("increment() expects an object of columns and amounts");
|
|
1392
|
+
const entries = Object.entries(amounts).filter(([, amount]) => amount !== undefined);
|
|
1393
|
+
if (!entries.length) throw new SQLiteError("increment() requires at least one column");
|
|
1394
|
+
const set = entries.map(([column, amount]) => {
|
|
1395
|
+
if (typeof amount !== "number" || !Number.isFinite(amount)) throw new SQLiteError(`increment() expects a number for "${column}", got ${String(amount)}`);
|
|
1396
|
+
const col = _id(column, "column name");
|
|
1397
|
+
return `${col} = COALESCE(${col}, 0) + ?`;
|
|
1398
|
+
});
|
|
1399
|
+
const clause = this.#where(where);
|
|
1400
|
+
const { changes } = this.#run(`UPDATE ${table} SET ${set.join(", ")}${clause.sql}`, [...entries.map(([, amount]) => amount), ...clause.params]);
|
|
1401
|
+
return { ok: true, changes };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1089
1404
|
/**
|
|
1090
1405
|
* Shortcuts bound to one table, so you don't repeat its name.
|
|
1091
1406
|
*
|
|
@@ -1104,9 +1419,11 @@ class SQLite {
|
|
|
1104
1419
|
name: tableName,
|
|
1105
1420
|
get: (where, options) => this.get(tableName, where, options),
|
|
1106
1421
|
getAll: (where, options) => this.getAll(tableName, where, options),
|
|
1107
|
-
|
|
1108
|
-
|
|
1422
|
+
paginate: (where, options) => this.paginate(tableName, where, options),
|
|
1423
|
+
insert: (data, options) => this.insert(tableName, /** @type {any} */ (data), options),
|
|
1424
|
+
insertMany: (rows, options) => this.insertMany(tableName, /** @type {any} */ (rows), options),
|
|
1109
1425
|
update: (data, where) => this.update(tableName, /** @type {any} */ (data), where),
|
|
1426
|
+
increment: (amounts, where) => this.increment(tableName, /** @type {any} */ (amounts), where),
|
|
1110
1427
|
upsert: (data, conflict) => this.upsert(tableName, /** @type {any} */ (data), conflict),
|
|
1111
1428
|
delete: (where) => this.delete(tableName, where),
|
|
1112
1429
|
count: (where) => this.count(tableName, where),
|
|
@@ -1122,18 +1439,19 @@ class SQLite {
|
|
|
1122
1439
|
*
|
|
1123
1440
|
* @example
|
|
1124
1441
|
* db.transaction(() => {
|
|
1125
|
-
* db.
|
|
1126
|
-
* db.
|
|
1442
|
+
* db.increment("accounts", { balance: -amount }, { id: from.id });
|
|
1443
|
+
* db.increment("accounts", { balance: amount }, { id: to.id });
|
|
1127
1444
|
* });
|
|
1128
1445
|
*
|
|
1129
1446
|
* @template T
|
|
1130
1447
|
* @param {() => T} callback Can be async; the commit then waits for it.
|
|
1448
|
+
* @param {TransactionOptions} [options]
|
|
1131
1449
|
* @returns {T}
|
|
1132
1450
|
* @throws Whatever `callback` threw, after rolling back.
|
|
1133
1451
|
*/
|
|
1134
|
-
transaction(callback) {
|
|
1452
|
+
transaction(callback, options) {
|
|
1135
1453
|
const savepoint = this.#depth > 0 ? `nc_savepoint_${this.#depth}` : null;
|
|
1136
|
-
this.#db.exec(savepoint ? `SAVEPOINT ${savepoint}` : "BEGIN");
|
|
1454
|
+
this.#db.exec(savepoint ? `SAVEPOINT ${savepoint}` : options?.immediate ? "BEGIN IMMEDIATE" : "BEGIN");
|
|
1137
1455
|
this.#depth++;
|
|
1138
1456
|
const commit = () => {
|
|
1139
1457
|
this.#depth--;
|
|
@@ -1141,6 +1459,8 @@ class SQLite {
|
|
|
1141
1459
|
};
|
|
1142
1460
|
const rollback = () => {
|
|
1143
1461
|
this.#depth--;
|
|
1462
|
+
// A table created in there is gone again.
|
|
1463
|
+
this.#known.clear();
|
|
1144
1464
|
try {
|
|
1145
1465
|
this.#db.exec(savepoint ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : "ROLLBACK");
|
|
1146
1466
|
} catch {
|
|
@@ -1291,6 +1611,7 @@ class SQLite {
|
|
|
1291
1611
|
*/
|
|
1292
1612
|
close() {
|
|
1293
1613
|
this.#statements.clear();
|
|
1614
|
+
this.#readers.clear();
|
|
1294
1615
|
if (!this.#open) return;
|
|
1295
1616
|
this.#open = false;
|
|
1296
1617
|
this.#db.close();
|