@statewalker/webrun-files-sqlite 0.10.3 → 0.11.1
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 +59 -55
- package/dist/blocks/sqlite-files-api.d.ts +3 -3
- package/dist/blocks/sqlite-files-api.d.ts.map +1 -1
- package/dist/bytes.d.ts +1 -1
- package/dist/bytes.d.ts.map +1 -1
- package/dist/cjs/index.cjs +81 -112
- package/dist/esm/index.js +82 -110
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/run-statements.d.ts +15 -0
- package/dist/run-statements.d.ts.map +1 -0
- package/dist/sqlar-files-api.d.ts +2 -2
- package/dist/sqlar-files-api.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/blocks/sqlite-files-api.ts +40 -31
- package/src/bytes.ts +1 -3
- package/src/index.ts +0 -9
- package/src/run-statements.ts +24 -0
- package/src/sqlar-files-api.ts +20 -25
- package/dist/drivers.d.ts +0 -73
- package/dist/drivers.d.ts.map +0 -1
- package/dist/sql.types.d.ts +0 -25
- package/dist/sql.types.d.ts.map +0 -1
- package/src/drivers.ts +0 -133
- package/src/sql.types.ts +0 -25
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Db } from "@statewalker/db-api";
|
|
2
|
+
import { inTransaction } from "@statewalker/db-sqlite";
|
|
1
3
|
import type {
|
|
2
4
|
FileInfo,
|
|
3
5
|
FileStats,
|
|
@@ -7,13 +9,13 @@ import type {
|
|
|
7
9
|
} from "@statewalker/webrun-files";
|
|
8
10
|
import { normalizePath } from "@statewalker/webrun-files";
|
|
9
11
|
import { toBytes } from "../bytes.js";
|
|
10
|
-
import
|
|
12
|
+
import { runStatements, type SqlStatement } from "../run-statements.js";
|
|
11
13
|
import { ByteReader, collectBlock } from "./byte-reader.js";
|
|
12
14
|
import { Sha256 } from "./sha256.js";
|
|
13
15
|
import { type StreamCodec, webDeflateCodec } from "./stream-codec.js";
|
|
14
16
|
|
|
15
17
|
const MiB = 1024 * 1024;
|
|
16
|
-
/** Rows on Durable Objects
|
|
18
|
+
/** Rows on Durable Objects are capped at 2 MB; leave room for deflate's worst case. */
|
|
17
19
|
const MAX_BLOCK_SIZE_LIMIT = 1.5 * MiB;
|
|
18
20
|
const LIST_PAGE = 256;
|
|
19
21
|
|
|
@@ -49,14 +51,14 @@ interface ListRow extends EntryRow {
|
|
|
49
51
|
* A FilesApi over three SQLite tables: paths point at immutable contents, and
|
|
50
52
|
* a content is a sequence of bounded, optionally compressed blocks. Reads and
|
|
51
53
|
* writes stream one block at a time, so no file is ever held whole and no row
|
|
52
|
-
* outgrows the 2 MB limit of Durable Objects
|
|
54
|
+
* outgrows the 2 MB row limit of Durable Objects.
|
|
53
55
|
*
|
|
54
56
|
* Streaming happens outside transactions; every step that changes what a path
|
|
55
57
|
* shows is one transaction, so an interruption leaves at most an unreferenced
|
|
56
58
|
* content, which {@link sweep} removes.
|
|
57
59
|
*/
|
|
58
60
|
export class SqliteFilesApi implements FilesApi {
|
|
59
|
-
readonly #
|
|
61
|
+
readonly #db: Db;
|
|
60
62
|
readonly #codec: StreamCodec | null;
|
|
61
63
|
readonly #blockSize: number;
|
|
62
64
|
readonly #paths: string;
|
|
@@ -64,7 +66,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
64
66
|
readonly #blocks: string;
|
|
65
67
|
readonly #prefix: string;
|
|
66
68
|
|
|
67
|
-
constructor(
|
|
69
|
+
constructor(db: Db, opts: SqliteFilesApiOptions = {}) {
|
|
68
70
|
const prefix = opts.tablePrefix ?? "fs_";
|
|
69
71
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) {
|
|
70
72
|
throw new Error(`SqliteFilesApi: tablePrefix must be a plain SQL identifier, got ${prefix}`);
|
|
@@ -79,7 +81,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
79
81
|
`SqliteFilesApi: blockSize must be an integer from 1 to ${MAX_BLOCK_SIZE_LIMIT}, got ${this.#blockSize}`,
|
|
80
82
|
);
|
|
81
83
|
}
|
|
82
|
-
this.#
|
|
84
|
+
this.#db = db;
|
|
83
85
|
this.#codec = opts.compression === undefined ? availableWebCodec() : opts.compression;
|
|
84
86
|
this.#prefix = prefix;
|
|
85
87
|
this.#paths = `${prefix}paths`;
|
|
@@ -113,7 +115,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
113
115
|
`CREATE INDEX IF NOT EXISTS ${this.#prefix}paths_fid ON ${this.#paths}(fid)`,
|
|
114
116
|
`CREATE INDEX IF NOT EXISTS ${this.#prefix}files_hash ON ${this.#files}(hash, size)`,
|
|
115
117
|
];
|
|
116
|
-
for (const statement of statements) await this.#
|
|
118
|
+
for (const statement of statements) await this.#db.query(statement);
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
// ---------------------------------------------------------------- read
|
|
@@ -179,14 +181,18 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
179
181
|
): Promise<void> {
|
|
180
182
|
const name = normalizePath(path);
|
|
181
183
|
const compression = this.#codec?.name ?? "none";
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
// In a transaction too, so a Db without db-sqlite's hook fails before writing anything.
|
|
185
|
+
const fid = await inTransaction(
|
|
186
|
+
this.#db,
|
|
187
|
+
async (tx) =>
|
|
188
|
+
(
|
|
189
|
+
await tx.query<{ fid: number }>(
|
|
190
|
+
`INSERT INTO ${this.#files}(size, compression, block_size, hash, updated)
|
|
191
|
+
VALUES (NULL, ?, ?, NULL, ?) RETURNING fid`,
|
|
192
|
+
[compression, this.#blockSize, Date.now()],
|
|
193
|
+
)
|
|
194
|
+
)[0].fid,
|
|
188
195
|
);
|
|
189
|
-
const fid = created[0].fid;
|
|
190
196
|
|
|
191
197
|
const limit = this.#blockSize;
|
|
192
198
|
const reader = new ByteReader(content);
|
|
@@ -209,7 +215,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
209
215
|
}
|
|
210
216
|
};
|
|
211
217
|
const block = await collectBlock(this.#codec ? this.#codec.compress(pieces()) : pieces());
|
|
212
|
-
const [alive] = await this.#
|
|
218
|
+
const [alive] = await this.#transaction([
|
|
213
219
|
{
|
|
214
220
|
sql: `UPDATE ${this.#files} SET updated = ? WHERE fid = ? AND size IS NULL`,
|
|
215
221
|
params: [Date.now(), fid],
|
|
@@ -225,7 +231,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
const previous = await this.#entry(name);
|
|
228
|
-
const [finished] = await this.#
|
|
234
|
+
const [finished] = await this.#transaction(
|
|
229
235
|
this.#finishWrite(name, fid, size, hash.digestHex(), compression, previous?.fid ?? null),
|
|
230
236
|
);
|
|
231
237
|
if (finished === 0) throw sweptDuringWrite(name);
|
|
@@ -234,9 +240,9 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
234
240
|
await reader.close();
|
|
235
241
|
} catch {}
|
|
236
242
|
// Best effort, and never allowed to replace the error that brought us here —
|
|
237
|
-
// including a
|
|
243
|
+
// including a Db without db-sqlite's transaction hook.
|
|
238
244
|
try {
|
|
239
|
-
await this.#
|
|
245
|
+
await this.#transaction([
|
|
240
246
|
{ sql: `DELETE FROM ${this.#files} WHERE fid = ? AND size IS NULL`, params: [fid] },
|
|
241
247
|
...this.#deleteBlocksOfMissing(fid),
|
|
242
248
|
]);
|
|
@@ -292,10 +298,9 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
292
298
|
async mkdir(path: string): Promise<void> {
|
|
293
299
|
const now = Date.now();
|
|
294
300
|
for (const dir of ancestorsAndSelf(normalizePath(path))) {
|
|
295
|
-
await this.#
|
|
301
|
+
await this.#db.query(
|
|
296
302
|
`INSERT INTO ${this.#paths}(path, fid, mtime) VALUES (?, NULL, ?) ON CONFLICT(path) DO NOTHING`,
|
|
297
|
-
dir,
|
|
298
|
-
now,
|
|
303
|
+
[dir, now],
|
|
299
304
|
);
|
|
300
305
|
}
|
|
301
306
|
}
|
|
@@ -326,12 +331,12 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
326
331
|
for (;;) {
|
|
327
332
|
const params: unknown[] = [...range.params, cursor];
|
|
328
333
|
if (!options.recursive) params.push(prefixLength + 1);
|
|
329
|
-
const rows = await this.#
|
|
334
|
+
const rows = await this.#db.query<ListRow>(
|
|
330
335
|
`SELECT p.path, p.fid, p.mtime, f.size, f.compression, f.block_size
|
|
331
336
|
FROM ${this.#paths} p LEFT JOIN ${this.#files} f ON f.fid = p.fid
|
|
332
337
|
WHERE ${range.where} AND p.path > ? ${directOnly}
|
|
333
338
|
ORDER BY p.path LIMIT ${LIST_PAGE}`,
|
|
334
|
-
|
|
339
|
+
params,
|
|
335
340
|
);
|
|
336
341
|
for (const row of rows) {
|
|
337
342
|
const stats = toStats(row);
|
|
@@ -346,7 +351,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
346
351
|
// ------------------------------------------------------------- mutation
|
|
347
352
|
|
|
348
353
|
async remove(path: string): Promise<boolean> {
|
|
349
|
-
const counts = await this.#
|
|
354
|
+
const counts = await this.#transaction(this.#removeTree(normalizePath(path)));
|
|
350
355
|
return counts[counts.length - 1] > 0;
|
|
351
356
|
}
|
|
352
357
|
|
|
@@ -427,7 +432,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
427
432
|
params: [to, from.length + 1, now, ...range.params],
|
|
428
433
|
},
|
|
429
434
|
];
|
|
430
|
-
const counts = await this.#
|
|
435
|
+
const counts = await this.#transaction(statements);
|
|
431
436
|
return counts[counts.length - 1] > 0;
|
|
432
437
|
}
|
|
433
438
|
|
|
@@ -453,7 +458,7 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
453
458
|
const eligible = `f.updated < ? AND NOT EXISTS (
|
|
454
459
|
SELECT 1 FROM ${this.#paths} p WHERE p.fid = f.fid
|
|
455
460
|
)`;
|
|
456
|
-
const [, contents] = await this.#
|
|
461
|
+
const [, contents] = await this.#transaction([
|
|
457
462
|
{
|
|
458
463
|
sql: `DELETE FROM ${this.#blocks} WHERE fid IN (SELECT f.fid FROM ${this.#files} f WHERE ${eligible})`,
|
|
459
464
|
params: [cutoff],
|
|
@@ -465,12 +470,17 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
465
470
|
|
|
466
471
|
// -------------------------------------------------------------- private
|
|
467
472
|
|
|
473
|
+
/** The statements as one transaction; per statement, the rows it changed. */
|
|
474
|
+
#transaction(statements: SqlStatement[]): Promise<number[]> {
|
|
475
|
+
return inTransaction(this.#db, (tx) => runStatements(tx, statements));
|
|
476
|
+
}
|
|
477
|
+
|
|
468
478
|
async #entry(name: string): Promise<EntryRow | undefined> {
|
|
469
|
-
const rows = await this.#
|
|
479
|
+
const rows = await this.#db.query<EntryRow>(
|
|
470
480
|
`SELECT p.fid, p.mtime, f.size, f.compression, f.block_size
|
|
471
481
|
FROM ${this.#paths} p LEFT JOIN ${this.#files} f ON f.fid = p.fid
|
|
472
482
|
WHERE p.path = ?`,
|
|
473
|
-
name,
|
|
483
|
+
[name],
|
|
474
484
|
);
|
|
475
485
|
return rows[0];
|
|
476
486
|
}
|
|
@@ -488,10 +498,9 @@ export class SqliteFilesApi implements FilesApi {
|
|
|
488
498
|
}
|
|
489
499
|
|
|
490
500
|
async #block(fid: number, shift: number) {
|
|
491
|
-
const rows = await this.#
|
|
501
|
+
const rows = await this.#db.query<{ shift: number; block: unknown }>(
|
|
492
502
|
`SELECT shift, block FROM ${this.#blocks} WHERE fid = ? AND shift = ?`,
|
|
493
|
-
fid,
|
|
494
|
-
shift,
|
|
503
|
+
[fid, shift],
|
|
495
504
|
);
|
|
496
505
|
return rows[0];
|
|
497
506
|
}
|
package/src/bytes.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** BLOBs arrive as Uint8Array from every @statewalker/db-sqlite driver; SQLAR may hold TEXT. */
|
|
2
2
|
export function toBytes(value: unknown): Uint8Array {
|
|
3
3
|
if (value instanceof Uint8Array) return value;
|
|
4
|
-
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
5
|
-
if (Array.isArray(value)) return Uint8Array.from(value);
|
|
6
4
|
if (typeof value === "string") return new TextEncoder().encode(value);
|
|
7
5
|
return new Uint8Array(0);
|
|
8
6
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,13 +13,4 @@ export {
|
|
|
13
13
|
rawCodec,
|
|
14
14
|
webCodec,
|
|
15
15
|
} from "./codec.js";
|
|
16
|
-
export {
|
|
17
|
-
type D1Database,
|
|
18
|
-
D1SqlDriver,
|
|
19
|
-
DoSqlDriver,
|
|
20
|
-
type DoStorage,
|
|
21
|
-
NodeSqlDriver,
|
|
22
|
-
type NodeSqliteDatabase,
|
|
23
|
-
} from "./drivers.js";
|
|
24
|
-
export type { SqlDriver, SqlStatement } from "./sql.types.js";
|
|
25
16
|
export { SqlarFilesApi, type SqlarFilesApiOptions } from "./sqlar-files-api.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Db } from "@statewalker/db-api";
|
|
2
|
+
|
|
3
|
+
/** One statement of a transaction. */
|
|
4
|
+
export interface SqlStatement {
|
|
5
|
+
sql: string;
|
|
6
|
+
params?: unknown[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs the statements in order on a transaction's Db and returns, per
|
|
11
|
+
* statement, the rows it changed — zero exactly when it changed nothing. The
|
|
12
|
+
* count is SQLite's `changes()` on the same connection, so index writes are
|
|
13
|
+
* not counted. Call it only inside `inTransaction`; it does nothing but SQL,
|
|
14
|
+
* so a retried transaction callback is safe.
|
|
15
|
+
*/
|
|
16
|
+
export async function runStatements(tx: Db, statements: SqlStatement[]): Promise<number[]> {
|
|
17
|
+
const counts: number[] = [];
|
|
18
|
+
for (const { sql, params = [] } of statements) {
|
|
19
|
+
await tx.query(sql, params);
|
|
20
|
+
const [row] = await tx.query<{ n: number }>("SELECT changes() AS n");
|
|
21
|
+
counts.push(Number(row.n));
|
|
22
|
+
}
|
|
23
|
+
return counts;
|
|
24
|
+
}
|
package/src/sqlar-files-api.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Db } from "@statewalker/db-api";
|
|
1
2
|
import type {
|
|
2
3
|
FileInfo,
|
|
3
4
|
FileStats,
|
|
@@ -8,7 +9,6 @@ import type {
|
|
|
8
9
|
import { comparePaths, normalizePath } from "@statewalker/webrun-files";
|
|
9
10
|
import { toBytes } from "./bytes.js";
|
|
10
11
|
import { type Codec, defaultCodec } from "./codec.js";
|
|
11
|
-
import type { SqlDriver } from "./sql.types.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* The table, verbatim from the SQLite Archive specification. Five columns,
|
|
@@ -44,17 +44,17 @@ export interface SqlarFilesApiOptions {
|
|
|
44
44
|
* missing path — and {@link symlink} / {@link readlink} expose the link itself.
|
|
45
45
|
*/
|
|
46
46
|
export class SqlarFilesApi implements FilesApi {
|
|
47
|
-
readonly #
|
|
47
|
+
readonly #db: Db;
|
|
48
48
|
readonly #codec: Codec;
|
|
49
49
|
|
|
50
|
-
constructor(
|
|
51
|
-
this.#
|
|
50
|
+
constructor(db: Db, opts: SqlarFilesApiOptions = {}) {
|
|
51
|
+
this.#db = db;
|
|
52
52
|
this.#codec = opts.codec ?? defaultCodec();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Creates the table if it is missing. Await it before any other call. */
|
|
56
56
|
async init(): Promise<void> {
|
|
57
|
-
await this.#
|
|
57
|
+
await this.#db.query(SCHEMA);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** A missing path or a directory yields nothing; a file yields one chunk. */
|
|
@@ -63,10 +63,9 @@ export class SqlarFilesApi implements FilesApi {
|
|
|
63
63
|
const resolved = await this.#resolve(toName(path));
|
|
64
64
|
if (resolved?.stats.kind !== "file") return;
|
|
65
65
|
|
|
66
|
-
const rows = await this.#
|
|
67
|
-
"SELECT data FROM sqlar WHERE name = ?",
|
|
66
|
+
const rows = await this.#db.query<{ data: unknown }>("SELECT data FROM sqlar WHERE name = ?", [
|
|
68
67
|
resolved.name,
|
|
69
|
-
);
|
|
68
|
+
]);
|
|
70
69
|
const content = await this.#content(resolved.name, resolved.stats.size, toBytes(rows[0]?.data));
|
|
71
70
|
options.signal?.throwIfAborted();
|
|
72
71
|
const start = options.start ?? 0;
|
|
@@ -150,9 +149,9 @@ export class SqlarFilesApi implements FilesApi {
|
|
|
150
149
|
// Paths are reported under the requested name even when it is a link.
|
|
151
150
|
const base = resolved.name;
|
|
152
151
|
const range = descendants(base);
|
|
153
|
-
const rows = await this.#
|
|
152
|
+
const rows = await this.#db.query<StoredRow>(
|
|
154
153
|
`SELECT ${ROW_COLUMNS} FROM sqlar WHERE ${range.where} ORDER BY name`,
|
|
155
|
-
|
|
154
|
+
range.params,
|
|
156
155
|
);
|
|
157
156
|
const prefixLength = base === "" ? 0 : base.length + 1;
|
|
158
157
|
const seen = new Set<string>();
|
|
@@ -183,12 +182,12 @@ export class SqlarFilesApi implements FilesApi {
|
|
|
183
182
|
async remove(path: string): Promise<boolean> {
|
|
184
183
|
const name = toName(path);
|
|
185
184
|
const range = selfAndDescendants(name);
|
|
186
|
-
const found = await this.#
|
|
185
|
+
const found = await this.#db.query(
|
|
187
186
|
`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`,
|
|
188
|
-
|
|
187
|
+
range.params,
|
|
189
188
|
);
|
|
190
189
|
if (found.length === 0) return false;
|
|
191
|
-
await this.#
|
|
190
|
+
await this.#db.query(`DELETE FROM sqlar WHERE ${range.where}`, range.params);
|
|
192
191
|
return true;
|
|
193
192
|
}
|
|
194
193
|
|
|
@@ -200,7 +199,7 @@ export class SqlarFilesApi implements FilesApi {
|
|
|
200
199
|
const from = toName(source);
|
|
201
200
|
const to = toName(target);
|
|
202
201
|
const range = selfAndDescendants(from);
|
|
203
|
-
const rows = await this.#
|
|
202
|
+
const rows = await this.#db.query<{
|
|
204
203
|
name: string;
|
|
205
204
|
mode: number;
|
|
206
205
|
sz: number;
|
|
@@ -208,7 +207,7 @@ export class SqlarFilesApi implements FilesApi {
|
|
|
208
207
|
data: unknown;
|
|
209
208
|
}>(
|
|
210
209
|
`SELECT name, mode, sz, typeof(data) AS dtype, data FROM sqlar WHERE ${range.where}`,
|
|
211
|
-
|
|
210
|
+
range.params,
|
|
212
211
|
);
|
|
213
212
|
if (rows.length === 0) return false;
|
|
214
213
|
|
|
@@ -268,32 +267,28 @@ export class SqlarFilesApi implements FilesApi {
|
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
async #row(name: string): Promise<StoredRow | undefined> {
|
|
271
|
-
const rows = await this.#
|
|
270
|
+
const rows = await this.#db.query<StoredRow>(
|
|
272
271
|
`SELECT ${ROW_COLUMNS} FROM sqlar WHERE name = ?`,
|
|
273
|
-
name,
|
|
272
|
+
[name],
|
|
274
273
|
);
|
|
275
274
|
return rows[0];
|
|
276
275
|
}
|
|
277
276
|
|
|
278
277
|
async #hasDescendants(name: string): Promise<boolean> {
|
|
279
278
|
const range = descendants(name);
|
|
280
|
-
const rows = await this.#
|
|
279
|
+
const rows = await this.#db.query(
|
|
281
280
|
`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`,
|
|
282
|
-
|
|
281
|
+
range.params,
|
|
283
282
|
);
|
|
284
283
|
return rows.length > 0;
|
|
285
284
|
}
|
|
286
285
|
|
|
287
286
|
async #put(name: string, mode: number, sz: number, data: Uint8Array | string | null) {
|
|
288
|
-
await this.#
|
|
287
|
+
await this.#db.query(
|
|
289
288
|
`INSERT INTO sqlar (name, mode, mtime, sz, data) VALUES (?, ?, ?, ?, ?)
|
|
290
289
|
ON CONFLICT(name) DO UPDATE SET
|
|
291
290
|
mode = excluded.mode, mtime = excluded.mtime, sz = excluded.sz, data = excluded.data`,
|
|
292
|
-
name,
|
|
293
|
-
mode,
|
|
294
|
-
Math.floor(Date.now() / 1000), // SQLAR mtime is in seconds
|
|
295
|
-
sz,
|
|
296
|
-
data,
|
|
291
|
+
[name, mode, Math.floor(Date.now() / 1000), sz, data], // SQLAR mtime is in seconds
|
|
297
292
|
);
|
|
298
293
|
}
|
|
299
294
|
}
|
package/dist/drivers.d.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { SqlDriver, SqlStatement } from "./sql.types.js";
|
|
2
|
-
/** The subset of node:sqlite's `DatabaseSync` this driver uses. */
|
|
3
|
-
export interface NodeSqliteDatabase {
|
|
4
|
-
exec(sql: string): void;
|
|
5
|
-
prepare(sql: string): {
|
|
6
|
-
all(...params: never[]): unknown[];
|
|
7
|
-
run(...params: never[]): {
|
|
8
|
-
changes: number | bigint;
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
/** node:sqlite — local development, tests and any Node deployment. */
|
|
13
|
-
export declare class NodeSqlDriver implements SqlDriver {
|
|
14
|
-
#private;
|
|
15
|
-
constructor(db: NodeSqliteDatabase);
|
|
16
|
-
all<T>(sql: string, ...params: unknown[]): T[];
|
|
17
|
-
run(sql: string, ...params: unknown[]): void;
|
|
18
|
-
/** Synchronous from BEGIN to COMMIT, so no other call can interleave on the connection. */
|
|
19
|
-
transaction(statements: SqlStatement[]): number[];
|
|
20
|
-
}
|
|
21
|
-
/** The subset of a Durable Object's `ctx.storage` this driver uses. */
|
|
22
|
-
export interface DoStorage {
|
|
23
|
-
sql: {
|
|
24
|
-
exec(query: string, ...bindings: unknown[]): {
|
|
25
|
-
toArray(): unknown[];
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
transactionSync<T>(fn: () => T): T;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Durable Object SQLite storage: pass `ctx.storage`. Synchronous; BLOBs come
|
|
32
|
-
* back as ArrayBuffer. Transactions use `transactionSync`, since `sql.exec`
|
|
33
|
-
* rejects `BEGIN`.
|
|
34
|
-
*/
|
|
35
|
-
export declare class DoSqlDriver implements SqlDriver {
|
|
36
|
-
#private;
|
|
37
|
-
constructor(storage: DoStorage);
|
|
38
|
-
all<T>(sql: string, ...params: unknown[]): T[];
|
|
39
|
-
run(sql: string, ...params: unknown[]): void;
|
|
40
|
-
/**
|
|
41
|
-
* Counts come from SQLite's `changes()` on the same connection, not from the
|
|
42
|
-
* cursor's `rowsWritten`, which also counts every index row a write touches.
|
|
43
|
-
*/
|
|
44
|
-
transaction(statements: SqlStatement[]): number[];
|
|
45
|
-
}
|
|
46
|
-
/** The subset of a `D1Database` this driver uses. */
|
|
47
|
-
export interface D1Database {
|
|
48
|
-
prepare(query: string): D1PreparedStatement;
|
|
49
|
-
batch(statements: D1BoundStatement[]): Promise<{
|
|
50
|
-
meta: {
|
|
51
|
-
changes: number;
|
|
52
|
-
};
|
|
53
|
-
}[]>;
|
|
54
|
-
}
|
|
55
|
-
interface D1PreparedStatement {
|
|
56
|
-
bind(...values: unknown[]): D1BoundStatement;
|
|
57
|
-
}
|
|
58
|
-
interface D1BoundStatement {
|
|
59
|
-
all(): Promise<{
|
|
60
|
-
results: unknown[];
|
|
61
|
-
}>;
|
|
62
|
-
run(): Promise<unknown>;
|
|
63
|
-
}
|
|
64
|
-
/** Cloudflare D1. Asynchronous; BLOBs come back as number[]; transactions are `batch()`. */
|
|
65
|
-
export declare class D1SqlDriver implements SqlDriver {
|
|
66
|
-
#private;
|
|
67
|
-
constructor(db: D1Database);
|
|
68
|
-
all<T>(sql: string, ...params: unknown[]): Promise<T[]>;
|
|
69
|
-
run(sql: string, ...params: unknown[]): Promise<void>;
|
|
70
|
-
transaction(statements: SqlStatement[]): Promise<number[]>;
|
|
71
|
-
}
|
|
72
|
-
export {};
|
|
73
|
-
//# sourceMappingURL=drivers.d.ts.map
|
package/dist/drivers.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"drivers.d.ts","sourceRoot":"","sources":["../src/drivers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9D,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QACpB,GAAG,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC;QACnC,GAAG,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG;YAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAC;KACvD,CAAC;CACH;AAED,sEAAsE;AACtE,qBAAa,aAAc,YAAW,SAAS;;IAG7C,YAAY,EAAE,EAAE,kBAAkB,EAEjC;IAED,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAE7C;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAE3C;IAED,2FAA2F;IAC3F,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,CAYhD;CACF;AAED,uEAAuE;AACvE,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE;QACH,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,OAAO,EAAE,GAAG;YAAE,OAAO,IAAI,OAAO,EAAE,CAAA;SAAE,CAAC;KACvE,CAAC;IACF,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;CACpC;AAED;;;;GAIG;AACH,qBAAa,WAAY,YAAW,SAAS;;IAG3C,YAAY,OAAO,EAAE,SAAS,EAE7B;IAED,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAE7C;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAG3C;IAED;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,CAQhD;CACF;AAED,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC5C,KAAK,CAAC,UAAU,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,EAAE,CAAC,CAAC;CACjF;AAED,UAAU,mBAAmB;IAC3B,IAAI,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC;CAC9C;AAED,UAAU,gBAAgB;IACxB,GAAG,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;IACvC,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,4FAA4F;AAC5F,qBAAa,WAAY,YAAW,SAAS;;IAG3C,YAAY,EAAE,EAAE,UAAU,EAEzB;IAEK,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAM5D;IAEK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAK1D;IAEK,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAM/D;CACF"}
|
package/dist/sql.types.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/** One statement of a transaction. */
|
|
2
|
-
export interface SqlStatement {
|
|
3
|
-
sql: string;
|
|
4
|
-
params?: unknown[];
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* The SQL port. Each method may be sync or async, so node:sqlite, a Durable
|
|
8
|
-
* Object's storage and D1 all satisfy it through a thin driver.
|
|
9
|
-
*/
|
|
10
|
-
export interface SqlDriver {
|
|
11
|
-
/** Rows as objects keyed by column name. */
|
|
12
|
-
all<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[] | Promise<T[]>;
|
|
13
|
-
/** Execute one statement for its effect. */
|
|
14
|
-
run(sql: string, ...params: unknown[]): void | Promise<void>;
|
|
15
|
-
/**
|
|
16
|
-
* Run the statements in order as one transaction: all apply or none do, and
|
|
17
|
-
* a failure rethrows after rolling back. Returns, per statement, a count that
|
|
18
|
-
* is zero exactly when that statement changed nothing.
|
|
19
|
-
*
|
|
20
|
-
* No statement may depend on reading another's result — D1's `batch()` cannot
|
|
21
|
-
* pass results along — so conditions belong in SQL (`WHERE EXISTS …`).
|
|
22
|
-
*/
|
|
23
|
-
transaction(statements: SqlStatement[]): number[] | Promise<number[]>;
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=sql.types.d.ts.map
|
package/dist/sql.types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sql.types.d.ts","sourceRoot":"","sources":["../src/sql.types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,4CAA4C;IAC5C,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACxF,4CAA4C;IAC5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D;;;;;;;OAOG;IACH,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACvE"}
|
package/src/drivers.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type { SqlDriver, SqlStatement } from "./sql.types.js";
|
|
2
|
-
|
|
3
|
-
/** The subset of node:sqlite's `DatabaseSync` this driver uses. */
|
|
4
|
-
export interface NodeSqliteDatabase {
|
|
5
|
-
exec(sql: string): void;
|
|
6
|
-
prepare(sql: string): {
|
|
7
|
-
all(...params: never[]): unknown[];
|
|
8
|
-
run(...params: never[]): { changes: number | bigint };
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** node:sqlite — local development, tests and any Node deployment. */
|
|
13
|
-
export class NodeSqlDriver implements SqlDriver {
|
|
14
|
-
readonly #db: NodeSqliteDatabase;
|
|
15
|
-
|
|
16
|
-
constructor(db: NodeSqliteDatabase) {
|
|
17
|
-
this.#db = db;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
all<T>(sql: string, ...params: unknown[]): T[] {
|
|
21
|
-
return this.#db.prepare(sql).all(...(params as never[])) as T[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
run(sql: string, ...params: unknown[]): void {
|
|
25
|
-
this.#db.prepare(sql).run(...(params as never[]));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Synchronous from BEGIN to COMMIT, so no other call can interleave on the connection. */
|
|
29
|
-
transaction(statements: SqlStatement[]): number[] {
|
|
30
|
-
this.#db.exec("BEGIN IMMEDIATE");
|
|
31
|
-
try {
|
|
32
|
-
const counts = statements.map(({ sql, params = [] }) =>
|
|
33
|
-
Number(this.#db.prepare(sql).run(...(params as never[])).changes),
|
|
34
|
-
);
|
|
35
|
-
this.#db.exec("COMMIT");
|
|
36
|
-
return counts;
|
|
37
|
-
} catch (error) {
|
|
38
|
-
this.#db.exec("ROLLBACK");
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The subset of a Durable Object's `ctx.storage` this driver uses. */
|
|
45
|
-
export interface DoStorage {
|
|
46
|
-
sql: {
|
|
47
|
-
exec(query: string, ...bindings: unknown[]): { toArray(): unknown[] };
|
|
48
|
-
};
|
|
49
|
-
transactionSync<T>(fn: () => T): T;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Durable Object SQLite storage: pass `ctx.storage`. Synchronous; BLOBs come
|
|
54
|
-
* back as ArrayBuffer. Transactions use `transactionSync`, since `sql.exec`
|
|
55
|
-
* rejects `BEGIN`.
|
|
56
|
-
*/
|
|
57
|
-
export class DoSqlDriver implements SqlDriver {
|
|
58
|
-
readonly #storage: DoStorage;
|
|
59
|
-
|
|
60
|
-
constructor(storage: DoStorage) {
|
|
61
|
-
this.#storage = storage;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
all<T>(sql: string, ...params: unknown[]): T[] {
|
|
65
|
-
return this.#storage.sql.exec(sql, ...params).toArray() as T[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
run(sql: string, ...params: unknown[]): void {
|
|
69
|
-
// The cursor is lazy; draining it makes sure the statement has run.
|
|
70
|
-
this.#storage.sql.exec(sql, ...params).toArray();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Counts come from SQLite's `changes()` on the same connection, not from the
|
|
75
|
-
* cursor's `rowsWritten`, which also counts every index row a write touches.
|
|
76
|
-
*/
|
|
77
|
-
transaction(statements: SqlStatement[]): number[] {
|
|
78
|
-
return this.#storage.transactionSync(() =>
|
|
79
|
-
statements.map(({ sql, params = [] }) => {
|
|
80
|
-
this.#storage.sql.exec(sql, ...params).toArray();
|
|
81
|
-
const [row] = this.#storage.sql.exec("SELECT changes() AS n").toArray() as { n: number }[];
|
|
82
|
-
return row.n;
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The subset of a `D1Database` this driver uses. */
|
|
89
|
-
export interface D1Database {
|
|
90
|
-
prepare(query: string): D1PreparedStatement;
|
|
91
|
-
batch(statements: D1BoundStatement[]): Promise<{ meta: { changes: number } }[]>;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface D1PreparedStatement {
|
|
95
|
-
bind(...values: unknown[]): D1BoundStatement;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface D1BoundStatement {
|
|
99
|
-
all(): Promise<{ results: unknown[] }>;
|
|
100
|
-
run(): Promise<unknown>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Cloudflare D1. Asynchronous; BLOBs come back as number[]; transactions are `batch()`. */
|
|
104
|
-
export class D1SqlDriver implements SqlDriver {
|
|
105
|
-
readonly #db: D1Database;
|
|
106
|
-
|
|
107
|
-
constructor(db: D1Database) {
|
|
108
|
-
this.#db = db;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async all<T>(sql: string, ...params: unknown[]): Promise<T[]> {
|
|
112
|
-
const { results } = await this.#db
|
|
113
|
-
.prepare(sql)
|
|
114
|
-
.bind(...params)
|
|
115
|
-
.all();
|
|
116
|
-
return results as T[];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async run(sql: string, ...params: unknown[]): Promise<void> {
|
|
120
|
-
await this.#db
|
|
121
|
-
.prepare(sql)
|
|
122
|
-
.bind(...params)
|
|
123
|
-
.run();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async transaction(statements: SqlStatement[]): Promise<number[]> {
|
|
127
|
-
if (statements.length === 0) return [];
|
|
128
|
-
const results = await this.#db.batch(
|
|
129
|
-
statements.map(({ sql, params = [] }) => this.#db.prepare(sql).bind(...params)),
|
|
130
|
-
);
|
|
131
|
-
return results.map((result) => result.meta.changes);
|
|
132
|
-
}
|
|
133
|
-
}
|
package/src/sql.types.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/** One statement of a transaction. */
|
|
2
|
-
export interface SqlStatement {
|
|
3
|
-
sql: string;
|
|
4
|
-
params?: unknown[];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The SQL port. Each method may be sync or async, so node:sqlite, a Durable
|
|
9
|
-
* Object's storage and D1 all satisfy it through a thin driver.
|
|
10
|
-
*/
|
|
11
|
-
export interface SqlDriver {
|
|
12
|
-
/** Rows as objects keyed by column name. */
|
|
13
|
-
all<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[] | Promise<T[]>;
|
|
14
|
-
/** Execute one statement for its effect. */
|
|
15
|
-
run(sql: string, ...params: unknown[]): void | Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Run the statements in order as one transaction: all apply or none do, and
|
|
18
|
-
* a failure rethrows after rolling back. Returns, per statement, a count that
|
|
19
|
-
* is zero exactly when that statement changed nothing.
|
|
20
|
-
*
|
|
21
|
-
* No statement may depend on reading another's result — D1's `batch()` cannot
|
|
22
|
-
* pass results along — so conditions belong in SQL (`WHERE EXISTS …`).
|
|
23
|
-
*/
|
|
24
|
-
transaction(statements: SqlStatement[]): number[] | Promise<number[]>;
|
|
25
|
-
}
|