@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.
@@ -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 type { SqlDriver, SqlStatement } from "../sql.types.js";
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 and D1 are capped at 2 MB; leave room for deflate's worst case. */
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 and D1.
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 #sql: SqlDriver;
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(sql: SqlDriver, opts: SqliteFilesApiOptions = {}) {
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.#sql = sql;
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.#sql.run(statement);
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
- const created = await this.#sql.all<{ fid: number }>(
183
- `INSERT INTO ${this.#files}(size, compression, block_size, hash, updated)
184
- VALUES (NULL, ?, ?, NULL, ?) RETURNING fid`,
185
- compression,
186
- this.#blockSize,
187
- Date.now(),
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.#sql.transaction([
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.#sql.transaction(
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 synchronous throw from a synchronous driver.
243
+ // including a Db without db-sqlite's transaction hook.
238
244
  try {
239
- await this.#sql.transaction([
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.#sql.run(
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.#sql.all<ListRow>(
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
- ...params,
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.#sql.transaction(this.#removeTree(normalizePath(path)));
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.#sql.transaction(statements);
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.#sql.transaction([
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.#sql.all<EntryRow>(
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.#sql.all<{ shift: number; block: unknown }>(
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
- /** Blobs arrive as Uint8Array (node:sqlite), ArrayBuffer (Durable Objects) or number[] (D1). */
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
+ }
@@ -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 #sql: SqlDriver;
47
+ readonly #db: Db;
48
48
  readonly #codec: Codec;
49
49
 
50
- constructor(sql: SqlDriver, opts: SqlarFilesApiOptions = {}) {
51
- this.#sql = sql;
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.#sql.run(SCHEMA);
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.#sql.all<{ data: unknown }>(
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.#sql.all<StoredRow>(
152
+ const rows = await this.#db.query<StoredRow>(
154
153
  `SELECT ${ROW_COLUMNS} FROM sqlar WHERE ${range.where} ORDER BY name`,
155
- ...range.params,
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.#sql.all(
185
+ const found = await this.#db.query(
187
186
  `SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`,
188
- ...range.params,
187
+ range.params,
189
188
  );
190
189
  if (found.length === 0) return false;
191
- await this.#sql.run(`DELETE FROM sqlar WHERE ${range.where}`, ...range.params);
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.#sql.all<{
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
- ...range.params,
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.#sql.all<StoredRow>(
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.#sql.all(
279
+ const rows = await this.#db.query(
281
280
  `SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`,
282
- ...range.params,
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.#sql.run(
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
@@ -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"}
@@ -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
@@ -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
- }