@delali/sirannon-db 0.1.6 → 0.1.7

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.
Files changed (54) hide show
  1. package/README.md +134 -19
  2. package/dist/backup-scheduler/index.d.ts +18 -3
  3. package/dist/backup-scheduler/index.mjs +2 -2
  4. package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
  5. package/dist/chunk-4IGMIJQK.mjs +318 -0
  6. package/dist/chunk-BNUTBHHH.mjs +22 -0
  7. package/dist/chunk-CJLYFDP5.mjs +26 -0
  8. package/dist/chunk-CW6S3WL5.mjs +222 -0
  9. package/dist/chunk-DJLX6CAE.mjs +20 -0
  10. package/dist/chunk-DVWQD3GF.mjs +49 -0
  11. package/dist/chunk-GEZUUIKV.mjs +268 -0
  12. package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
  13. package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
  14. package/dist/chunk-TKGHYWQ6.mjs +35 -0
  15. package/dist/chunk-VLTICJOD.mjs +470 -0
  16. package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
  17. package/dist/client/index.d.ts +107 -15
  18. package/dist/client/index.mjs +348 -37
  19. package/dist/core/index.d.ts +70 -13
  20. package/dist/core/index.mjs +635 -192
  21. package/dist/core/writer-worker.d.ts +2 -0
  22. package/dist/core/writer-worker.mjs +107 -0
  23. package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
  24. package/dist/driver/better-sqlite3.d.ts +1 -1
  25. package/dist/driver/better-sqlite3.mjs +44 -6
  26. package/dist/driver/bun.mjs +35 -6
  27. package/dist/driver/expo.mjs +2 -2
  28. package/dist/driver/node.d.ts +1 -1
  29. package/dist/driver/node.mjs +48 -5
  30. package/dist/driver/wa-sqlite.d.ts +109 -0
  31. package/dist/driver/wa-sqlite.mjs +30 -2
  32. package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
  33. package/dist/file-migrations/index.d.ts +2 -3
  34. package/dist/file-migrations/index.mjs +1 -1
  35. package/dist/replication/coordinator/etcd.mjs +2 -2
  36. package/dist/replication/index.d.ts +7 -8
  37. package/dist/replication/index.mjs +79 -78
  38. package/dist/server/index.d.ts +111 -28
  39. package/dist/server/index.mjs +1016 -384
  40. package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
  41. package/dist/transport/grpc.d.ts +3 -4
  42. package/dist/transport/grpc.mjs +2 -2
  43. package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
  44. package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
  45. package/dist/types-D_hQW1hr.d.ts +494 -0
  46. package/package.json +3 -23
  47. package/dist/chunk-3MCMONVP.mjs +0 -115
  48. package/dist/chunk-74UN4DIE.mjs +0 -14
  49. package/dist/chunk-FB2U2Q3Y.mjs +0 -21
  50. package/dist/chunk-GS7T5YMI.mjs +0 -51
  51. package/dist/chunk-PXKAKK2V.mjs +0 -124
  52. package/dist/index-CLdNrcPz.d.ts +0 -16
  53. package/dist/types-BFSsG77t.d.ts +0 -29
  54. package/dist/types-D-74JiXb.d.ts +0 -265
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,107 @@
1
+ import { serializeError } from '../chunk-DJLX6CAE.mjs';
2
+ import { executeGroup } from '../chunk-CW6S3WL5.mjs';
3
+ import '../chunk-GEZUUIKV.mjs';
4
+ import { SirannonError } from '../chunk-YPYVQJ4C.mjs';
5
+ import { parentPort } from 'worker_threads';
6
+
7
+ var STATEMENT_CACHE_CAPACITY = 128;
8
+ if (!parentPort) {
9
+ throw new SirannonError("Writer worker started outside a worker thread", "WRITER_WORKER_NO_PORT");
10
+ }
11
+ var port = parentPort;
12
+ var connection = null;
13
+ var statements = /* @__PURE__ */ new Map();
14
+ function requireConnection() {
15
+ if (!connection) {
16
+ throw new SirannonError(
17
+ "Writer worker received a command before the connection was opened",
18
+ "WRITER_WORKER_NOT_OPEN"
19
+ );
20
+ }
21
+ return connection;
22
+ }
23
+ async function statementFor(sql) {
24
+ const conn = requireConnection();
25
+ const cached = statements.get(sql);
26
+ if (cached) {
27
+ statements.delete(sql);
28
+ statements.set(sql, cached);
29
+ return cached;
30
+ }
31
+ const stmt = await conn.prepare(sql);
32
+ statements.set(sql, stmt);
33
+ if (statements.size > STATEMENT_CACHE_CAPACITY) {
34
+ const oldest = statements.keys().next().value;
35
+ if (oldest !== void 0) statements.delete(oldest);
36
+ }
37
+ return stmt;
38
+ }
39
+ async function open(entry, path, options) {
40
+ const module = await import(entry.specifier);
41
+ const factory = module[entry.exportName ?? "default"];
42
+ if (typeof factory !== "function") {
43
+ throw new SirannonError(
44
+ `Driver worker entry '${entry.specifier}' has no callable export '${entry.exportName ?? "default"}'`,
45
+ "WRITER_WORKER_BAD_ENTRY"
46
+ );
47
+ }
48
+ const driver = factory(entry.config);
49
+ connection = await driver.open(path, options);
50
+ }
51
+ async function dispatch(req) {
52
+ switch (req.kind) {
53
+ case "open":
54
+ await open(req.entry, req.path, req.options);
55
+ return void 0;
56
+ case "exec":
57
+ await requireConnection().exec(req.sql);
58
+ return void 0;
59
+ case "run":
60
+ return (await statementFor(req.sql)).run(...req.params);
61
+ case "get":
62
+ return (await statementFor(req.sql)).get(...req.params);
63
+ case "all":
64
+ return (await statementFor(req.sql)).all(...req.params);
65
+ case "allRaw": {
66
+ const stmt = await statementFor(req.sql);
67
+ return stmt.allRaw ? stmt.allRaw(...req.params) : stmt.all(...req.params);
68
+ }
69
+ case "runBatch": {
70
+ const conn = requireConnection();
71
+ if (conn.runBatch) return conn.runBatch(req.sql, req.paramsBatch);
72
+ const stmt = await statementFor(req.sql);
73
+ return Promise.all(req.paramsBatch.map((params) => stmt.run(...params)));
74
+ }
75
+ case "runBatchSummary": {
76
+ const conn = requireConnection();
77
+ if (conn.runBatchSummary) return conn.runBatchSummary(req.sql, req.paramsBatch);
78
+ const stmt = await statementFor(req.sql);
79
+ let changes = 0;
80
+ for (const params of req.paramsBatch) changes += (await stmt.run(...params)).changes;
81
+ return { rowsLoaded: req.paramsBatch.length, changes };
82
+ }
83
+ case "runGroup": {
84
+ const outcomes = await executeGroup(requireConnection(), req.units);
85
+ return outcomes.map(
86
+ (outcome) => outcome.ok ? { ok: true, results: outcome.values } : { ok: false, error: serializeError(outcome.error) }
87
+ );
88
+ }
89
+ case "close":
90
+ if (connection) await connection.close();
91
+ connection = null;
92
+ statements.clear();
93
+ return void 0;
94
+ }
95
+ }
96
+ async function handle(req) {
97
+ try {
98
+ const value = await dispatch(req);
99
+ port.postMessage({ id: req.id, ok: true, value });
100
+ } catch (err) {
101
+ port.postMessage({ id: req.id, ok: false, error: serializeError(err) });
102
+ }
103
+ }
104
+ var tail = Promise.resolve();
105
+ port.on("message", (req) => {
106
+ tail = tail.then(() => handle(req));
107
+ });
@@ -1,6 +1,5 @@
1
- import { S as SQLiteDriver } from './types-BFSsG77t.js';
2
- import { Q as QueryHookContext, f as ConnectionHookContext, g as BeforeSubscribeHook, H as HookConfig, M as MetricsConfig, h as QueryMetrics, i as ConnectionMetrics, j as CDCMetrics, D as DatabaseOptions, P as Params, k as QueryOptions, E as ExecuteResult, T as Transaction, l as SubscriptionBuilder, m as BackupScheduleOptions, B as BeforeQueryHook, A as AfterQueryHook } from './types-D-74JiXb.js';
3
- import { M as Migration, a as MigrationResult, R as RollbackResult } from './types-BeozgNPr.js';
1
+ import { Q as QueryHookContext, k as ConnectionHookContext, l as BeforeSubscribeHook, H as HookConfig, M as MetricsConfig, m as QueryMetrics, n as ConnectionMetrics, o as CDCMetrics, a as SQLiteDriver, D as DatabaseOptions, P as Params, p as QueryOptions, E as ExecuteResult, q as BulkLoadOptions, h as BulkLoadResult, T as Transaction, e as SQLiteConnection, r as SubscriptionBuilder, f as BackupScheduleOptions, B as BeforeQueryHook, A as AfterQueryHook } from './types-D_hQW1hr.js';
2
+ import { M as Migration, a as MigrationResult, R as RollbackResult } from './types-D4p4UyDK.js';
4
3
 
5
4
  type HookEvent = 'beforeQuery' | 'afterQuery' | 'beforeConnect' | 'databaseOpen' | 'databaseClose' | 'beforeSubscribe';
6
5
  type SubscribeHookContext = Parameters<BeforeSubscribeHook>[0];
@@ -49,28 +48,66 @@ declare class Database {
49
48
  readonly readOnly: boolean;
50
49
  private readonly pool;
51
50
  private readonly driver;
51
+ private readonly synchronous;
52
+ private readonly walMode;
53
+ private readonly writerLock;
54
+ private readonly writeGate;
55
+ private readonly groupCommitter;
52
56
  private readonly closeListeners;
53
57
  private _closed;
54
- private changeTracker;
55
- private subscriptionManager;
56
- private stopCdcPolling;
57
- private readonly cdcPollInterval;
58
- private readonly cdcRetention;
58
+ private readonly cdc;
59
59
  private readonly hookRegistry;
60
- private readonly parentHooks;
61
- private readonly metricsCollector;
62
- private readonly backupManager;
63
- private readonly backupScheduler;
64
- private readonly scheduledBackupCancellers;
60
+ private readonly observer;
61
+ private readonly backups;
65
62
  private constructor();
66
63
  static create(id: string, path: string, driver: SQLiteDriver, options?: DatabaseOptions, internals?: DatabaseInternals): Promise<Database>;
67
64
  query<T = Record<string, unknown>>(sql: string, params?: Params, options?: QueryOptions): Promise<T[]>;
65
+ /**
66
+ * Returns query rows already encoded for the wire (safe-range integers as
67
+ * plain numbers, larger integers and BLOBs as tagged envelopes) in a single
68
+ * pass. The server response path uses this so a read walks its values once
69
+ * rather than narrowing on the driver and re-scanning to tag.
70
+ */
71
+ queryForWire(sql: string, params?: Params, options?: QueryOptions): Promise<unknown[]>;
68
72
  queryOne<T = Record<string, unknown>>(sql: string, params?: Params, options?: QueryOptions): Promise<T | undefined>;
73
+ /**
74
+ * On a single-connection driver the reader is the writer, so serialise the
75
+ * read through the writer lock to avoid a bulk load's uncommitted rows.
76
+ */
77
+ private runRead;
69
78
  execute(sql: string, params?: Params, options?: QueryOptions): Promise<ExecuteResult>;
70
79
  executeBatch(sql: string, paramsBatch: Params[], options?: QueryOptions): Promise<ExecuteResult[]>;
80
+ private runInTransaction;
81
+ /**
82
+ * Load rows with relaxed writer durability. The load holds the writer lock
83
+ * for its whole duration, so no other write commits under the relaxed level
84
+ * and no two loads race on the shared `synchronous` setting; the configured
85
+ * level is restored before this resolves, whether the load succeeds or
86
+ * fails. The load runs in one transaction, so one commit and one durability
87
+ * barrier cover the whole batch. Rows are summed rather than returned
88
+ * per-row to bound memory on large loads. Like `execute` and `transaction`,
89
+ * this writes only to the local database; under replication the server routes
90
+ * loads through the engine, not through this method.
91
+ */
92
+ bulkLoad(sql: string, paramsBatch: Params[], options?: BulkLoadOptions): Promise<BulkLoadResult>;
93
+ /**
94
+ * Takes the statements up front rather than a callback, since a group cannot wait on an
95
+ * arbitrary caller-supplied callback without delaying every transaction beside it.
96
+ */
97
+ executeTransaction(statements: readonly {
98
+ sql: string;
99
+ params?: Params;
100
+ }[]): Promise<ExecuteResult[]>;
101
+ private runStatementsAlone;
71
102
  transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
72
- private maybeApplyDdlSideEffects;
73
103
  watch(table: string): Promise<void>;
104
+ /**
105
+ * Runs a CDC maintenance write (change-log pruning) on the shared writer
106
+ * under the writer lock. Serialising it with application writes keeps it
107
+ * from becoming a second writer that contends for SQLite's single write
108
+ * lock and stalls the event loop on `busy_timeout`.
109
+ */
110
+ runCdcMaintenance(op: (writer: SQLiteConnection) => Promise<unknown>): Promise<void>;
74
111
  unwatch(table: string): Promise<void>;
75
112
  on(table: string): SubscriptionBuilder;
76
113
  migrate(migrations: Migration[]): Promise<MigrationResult>;
@@ -85,11 +122,6 @@ declare class Database {
85
122
  get closed(): boolean;
86
123
  get readerCount(): number;
87
124
  private ensureOpen;
88
- private ensureCdc;
89
- private ensureCdcPolling;
90
- private stopCdcPollingLoop;
91
- private fireBeforeQueryHooks;
92
- private fireAfterQueryHooks;
93
125
  }
94
126
 
95
127
  export { Database as D, type HookDispose as H, MetricsCollector as M, type SubscribeHookContext as S, type HookEvent as a, type HookEventContextMap as b, type HookHandler as c, HookRegistry as d };
@@ -1,4 +1,4 @@
1
- import { S as SQLiteDriver } from '../types-BFSsG77t.js';
1
+ import { a as SQLiteDriver } from '../types-D_hQW1hr.js';
2
2
 
3
3
  interface BetterSqlite3Options {
4
4
  busyTimeout?: number;
@@ -1,8 +1,13 @@
1
- import { defineDriver } from '../chunk-74UN4DIE.mjs';
2
- import '../chunk-O7BHI3CF.mjs';
1
+ import { nodeResolveExtensionPath, nodeBackupEngine, nodeWriterContext, WriterWorker, createStatementCache, narrowSafeBigInt, narrowRowIntegers, narrowRowsIntegers } from '../chunk-4IGMIJQK.mjs';
2
+ import { defineDriver } from '../chunk-BNUTBHHH.mjs';
3
+ import '../chunk-DJLX6CAE.mjs';
4
+ import { synchronousPragmaValue } from '../chunk-CJLYFDP5.mjs';
5
+ import '../chunk-VLTICJOD.mjs';
6
+ import '../chunk-YPYVQJ4C.mjs';
3
7
 
4
8
  // src/drivers/better-sqlite3/index.ts
5
9
  function createConnection(db) {
10
+ const batchStatementFor = createStatementCache((sql) => db.prepare(sql));
6
11
  const conn = {
7
12
  async exec(sql) {
8
13
  db.exec(sql);
@@ -11,20 +16,43 @@ function createConnection(db) {
11
16
  const stmt = db.prepare(sql);
12
17
  return {
13
18
  async all(...params) {
19
+ return narrowRowsIntegers(stmt.all(...params));
20
+ },
21
+ async allRaw(...params) {
14
22
  return stmt.all(...params);
15
23
  },
16
24
  async get(...params) {
17
- return stmt.get(...params);
25
+ return narrowRowIntegers(stmt.get(...params));
18
26
  },
19
27
  async run(...params) {
20
28
  const result = stmt.run(...params);
21
29
  return {
22
- changes: result.changes,
23
- lastInsertRowId: result.lastInsertRowid
30
+ changes: Number(result.changes),
31
+ lastInsertRowId: narrowSafeBigInt(result.lastInsertRowid)
24
32
  };
25
33
  }
26
34
  };
27
35
  },
36
+ async runBatch(sql, paramsBatch) {
37
+ const stmt = batchStatementFor(sql);
38
+ const results = new Array(paramsBatch.length);
39
+ for (let i = 0; i < paramsBatch.length; i++) {
40
+ const result = stmt.run(...paramsBatch[i]);
41
+ results[i] = {
42
+ changes: Number(result.changes),
43
+ lastInsertRowId: narrowSafeBigInt(result.lastInsertRowid)
44
+ };
45
+ }
46
+ return results;
47
+ },
48
+ async runBatchSummary(sql, paramsBatch) {
49
+ const stmt = batchStatementFor(sql);
50
+ let changes = 0;
51
+ for (let i = 0; i < paramsBatch.length; i++) {
52
+ changes += Number(stmt.run(...paramsBatch[i]).changes);
53
+ }
54
+ return { rowsLoaded: paramsBatch.length, changes };
55
+ },
28
56
  async transaction(fn) {
29
57
  await conn.exec("BEGIN");
30
58
  try {
@@ -46,13 +74,23 @@ function createConnection(db) {
46
74
  return conn;
47
75
  }
48
76
  function betterSqlite3(driverOptions) {
77
+ const workerEntry = { specifier: import.meta.url, exportName: "betterSqlite3", config: driverOptions };
49
78
  return defineDriver({
50
79
  capabilities: { multipleConnections: true, extensions: true },
80
+ worker: workerEntry,
81
+ startWriterHost: async (path, options, hostOptions) => {
82
+ const host = await WriterWorker.start(workerEntry, path, options, hostOptions);
83
+ return host.connection;
84
+ },
85
+ createWriterContext: nodeWriterContext,
86
+ createBackupEngine: nodeBackupEngine,
87
+ resolveExtensionPath: nodeResolveExtensionPath,
51
88
  async open(path, options) {
52
89
  const Database = (await import('better-sqlite3')).default;
53
90
  const db = new Database(path, { readonly: options?.readonly ?? false });
91
+ db.defaultSafeIntegers(true);
54
92
  if (options?.walMode !== false) db.pragma("journal_mode = WAL");
55
- db.pragma("synchronous = NORMAL");
93
+ db.pragma(`synchronous = ${synchronousPragmaValue(options?.synchronous)}`);
56
94
  db.pragma("foreign_keys = ON");
57
95
  db.pragma(`busy_timeout = ${driverOptions?.busyTimeout ?? 5e3}`);
58
96
  return createConnection(db);
@@ -1,4 +1,30 @@
1
- import { defineDriver } from '../chunk-FB2U2Q3Y.mjs';
1
+ import { defineDriver, synchronousPragmaValue } from '../chunk-DVWQD3GF.mjs';
2
+
3
+ // src/core/driver/values.ts
4
+ var SAFE_INT_BOUND = 9007199254740991n;
5
+ function narrowSafeBigInt(value) {
6
+ if (typeof value === "bigint" && value >= -SAFE_INT_BOUND && value <= SAFE_INT_BOUND) {
7
+ return Number(value);
8
+ }
9
+ return value;
10
+ }
11
+ function narrowRowIntegers(row) {
12
+ if (row === null || typeof row !== "object") return row;
13
+ const obj = row;
14
+ for (const key of Object.keys(obj)) {
15
+ const value = obj[key];
16
+ if (typeof value === "bigint" && value >= -SAFE_INT_BOUND && value <= SAFE_INT_BOUND) {
17
+ obj[key] = Number(value);
18
+ }
19
+ }
20
+ return row;
21
+ }
22
+ function narrowRowsIntegers(rows) {
23
+ for (const row of rows) {
24
+ narrowRowIntegers(row);
25
+ }
26
+ return rows;
27
+ }
2
28
 
3
29
  // src/drivers/bun/index.ts
4
30
  function bunSqlite(driverOptions) {
@@ -6,9 +32,9 @@ function bunSqlite(driverOptions) {
6
32
  capabilities: { multipleConnections: true, extensions: true },
7
33
  async open(path, options) {
8
34
  const { Database } = await import('bun:sqlite');
9
- const db = new Database(path, { readonly: options?.readonly ?? false });
35
+ const db = new Database(path, { readonly: options?.readonly ?? false, safeIntegers: true });
10
36
  if (options?.walMode !== false) db.run("PRAGMA journal_mode = WAL");
11
- db.run("PRAGMA synchronous = NORMAL");
37
+ db.run(`PRAGMA synchronous = ${synchronousPragmaValue(options?.synchronous)}`);
12
38
  db.run("PRAGMA foreign_keys = ON");
13
39
  db.run(`PRAGMA busy_timeout = ${driverOptions?.busyTimeout ?? 5e3}`);
14
40
  const conn = {
@@ -19,18 +45,21 @@ function bunSqlite(driverOptions) {
19
45
  const stmt = db.query(sql);
20
46
  return {
21
47
  async all(...params) {
48
+ return narrowRowsIntegers(stmt.all(...params));
49
+ },
50
+ async allRaw(...params) {
22
51
  return stmt.all(...params);
23
52
  },
24
53
  async get(...params) {
25
- return stmt.get(...params) ?? void 0;
54
+ return narrowRowIntegers(stmt.get(...params) ?? void 0);
26
55
  },
27
56
  async run(...params) {
28
57
  stmt.run(...params);
29
58
  const changesStmt = db.query("SELECT changes() AS changes, last_insert_rowid() AS lastId");
30
59
  const info = changesStmt.get();
31
60
  return {
32
- changes: info.changes,
33
- lastInsertRowId: info.lastId
61
+ changes: Number(info.changes),
62
+ lastInsertRowId: narrowSafeBigInt(info.lastId)
34
63
  };
35
64
  }
36
65
  };
@@ -1,4 +1,4 @@
1
- import { defineDriver } from '../chunk-FB2U2Q3Y.mjs';
1
+ import { defineDriver, synchronousPragmaValue } from '../chunk-DVWQD3GF.mjs';
2
2
 
3
3
  // src/drivers/expo/index.ts
4
4
  function expoSqlite() {
@@ -10,7 +10,7 @@ function expoSqlite() {
10
10
  readOnly: options?.readonly
11
11
  });
12
12
  if (options?.walMode !== false) await db.execAsync("PRAGMA journal_mode = WAL");
13
- await db.execAsync("PRAGMA synchronous = NORMAL");
13
+ await db.execAsync(`PRAGMA synchronous = ${synchronousPragmaValue(options?.synchronous)}`);
14
14
  await db.execAsync("PRAGMA foreign_keys = ON");
15
15
  const buildConnectionFromHandle = (dbHandle) => ({
16
16
  async exec(sql) {
@@ -1,4 +1,4 @@
1
- import { S as SQLiteDriver } from '../types-BFSsG77t.js';
1
+ import { a as SQLiteDriver } from '../types-D_hQW1hr.js';
2
2
 
3
3
  interface NodeSqliteOptions {
4
4
  busyTimeout?: number;
@@ -1,39 +1,82 @@
1
- import { defineDriver } from '../chunk-74UN4DIE.mjs';
2
- import '../chunk-O7BHI3CF.mjs';
1
+ import { nodeResolveExtensionPath, nodeBackupEngine, nodeWriterContext, createStatementCache, WriterWorker, narrowSafeBigInt, narrowRowIntegers, narrowRowsIntegers } from '../chunk-4IGMIJQK.mjs';
2
+ import { defineDriver } from '../chunk-BNUTBHHH.mjs';
3
+ import '../chunk-DJLX6CAE.mjs';
4
+ import { synchronousPragmaValue } from '../chunk-CJLYFDP5.mjs';
5
+ import '../chunk-VLTICJOD.mjs';
6
+ import '../chunk-YPYVQJ4C.mjs';
3
7
 
4
8
  // src/drivers/node/index.ts
5
9
  function nodeSqlite(driverOptions) {
10
+ const workerEntry = { specifier: import.meta.url, exportName: "nodeSqlite", config: driverOptions };
6
11
  return defineDriver({
7
12
  capabilities: { multipleConnections: true, extensions: true },
13
+ worker: workerEntry,
14
+ startWriterHost: async (path, options, hostOptions) => {
15
+ const host = await WriterWorker.start(workerEntry, path, options, hostOptions);
16
+ return host.connection;
17
+ },
18
+ createWriterContext: nodeWriterContext,
19
+ createBackupEngine: nodeBackupEngine,
20
+ resolveExtensionPath: nodeResolveExtensionPath,
8
21
  async open(path, options) {
9
22
  const { DatabaseSync } = await import('node:sqlite');
10
23
  const db = new DatabaseSync(path, { readOnly: options?.readonly ?? false });
11
24
  if (options?.walMode !== false) db.exec("PRAGMA journal_mode = WAL");
12
- db.exec("PRAGMA synchronous = NORMAL");
25
+ db.exec(`PRAGMA synchronous = ${synchronousPragmaValue(options?.synchronous)}`);
13
26
  db.exec("PRAGMA foreign_keys = ON");
14
27
  db.exec(`PRAGMA busy_timeout = ${driverOptions?.busyTimeout ?? 5e3}`);
28
+ const batchStatementFor = createStatementCache((sql) => {
29
+ const stmt = db.prepare(sql);
30
+ stmt.setReadBigInts(true);
31
+ return stmt;
32
+ });
15
33
  const conn = {
16
34
  async exec(sql) {
17
35
  db.exec(sql);
18
36
  },
19
37
  async prepare(sql) {
20
38
  const stmt = db.prepare(sql);
39
+ stmt.setReadBigInts(true);
21
40
  return {
22
41
  async all(...params) {
42
+ return narrowRowsIntegers(stmt.all(...params));
43
+ },
44
+ async allRaw(...params) {
23
45
  return stmt.all(...params);
24
46
  },
25
47
  async get(...params) {
26
- return stmt.get(...params);
48
+ return narrowRowIntegers(stmt.get(...params));
27
49
  },
28
50
  async run(...params) {
29
51
  const result = stmt.run(...params);
30
52
  return {
31
53
  changes: Number(result.changes),
32
- lastInsertRowId: result.lastInsertRowid
54
+ lastInsertRowId: narrowSafeBigInt(result.lastInsertRowid)
33
55
  };
34
56
  }
35
57
  };
36
58
  },
59
+ async runBatch(sql, paramsBatch) {
60
+ const stmt = batchStatementFor(sql);
61
+ const results = new Array(paramsBatch.length);
62
+ for (let i = 0; i < paramsBatch.length; i++) {
63
+ const result = stmt.run(...paramsBatch[i]);
64
+ results[i] = {
65
+ changes: Number(result.changes),
66
+ lastInsertRowId: narrowSafeBigInt(result.lastInsertRowid)
67
+ };
68
+ }
69
+ return results;
70
+ },
71
+ async runBatchSummary(sql, paramsBatch) {
72
+ const stmt = batchStatementFor(sql);
73
+ let changes = 0;
74
+ for (let i = 0; i < paramsBatch.length; i++) {
75
+ const result = stmt.run(...paramsBatch[i]);
76
+ changes += Number(result.changes);
77
+ }
78
+ return { rowsLoaded: paramsBatch.length, changes };
79
+ },
37
80
  async transaction(fn) {
38
81
  await conn.exec("BEGIN");
39
82
  try {
@@ -1,29 +1,138 @@
1
+ /** Options for scheduled backups. */
2
+ interface BackupScheduleOptions {
3
+ /** Cron expression (e.g., '0 * * * *' for hourly). */
4
+ cron: string;
5
+ /** Directory to store backup files. */
6
+ destDir: string;
7
+ /** Maximum number of backup files to keep. Default: 5. */
8
+ maxFiles?: number;
9
+ /**
10
+ * Sirannon evaluates the cron expression in this IANA time zone (e.g. 'America/New_York').
11
+ * When omitted, it uses the host's local time zone, which also sets the daylight saving rules that apply.
12
+ */
13
+ timezone?: string;
14
+ /** Called when a scheduled backup fails. Without this, errors are silently discarded. */
15
+ onError?: (error: Error) => void;
16
+ }
17
+
18
+ interface WorkerHostOptions {
19
+ writeTimeoutMs?: number;
20
+ maxRestarts?: number;
21
+ }
22
+
1
23
  interface RunResult {
2
24
  changes: number;
3
25
  lastInsertRowId: number | bigint;
4
26
  }
27
+ /**
28
+ * Tells a caller running inside the operation that holds the writer from one
29
+ * merely waiting on it. A runtime without async context tracking cannot answer
30
+ * this, and answering it wrongly runs one caller's writes inside another
31
+ * caller's transaction.
32
+ */
33
+ interface WriterContext {
34
+ run<T>(operation: () => T): T;
35
+ isActive(): boolean;
36
+ exit<T>(operation: () => T): T;
37
+ }
38
+ interface BackupEngine {
39
+ backup(conn: SQLiteConnection, destPath: string): Promise<void>;
40
+ schedule(conn: SQLiteConnection, options: BackupScheduleOptions, runExclusive: (op: () => Promise<void>) => Promise<void>): () => void;
41
+ }
42
+ interface BatchSummary {
43
+ rowsLoaded: number;
44
+ changes: number;
45
+ }
5
46
  interface SQLiteStatement {
6
47
  all<T = unknown>(...params: unknown[]): Promise<T[]>;
7
48
  get<T = unknown>(...params: unknown[]): Promise<T | undefined>;
8
49
  run(...params: unknown[]): Promise<RunResult>;
50
+ /**
51
+ * Like {@link all} but skips the safe-range BigInt narrowing, leaving every
52
+ * integer as a BigInt. The server wire path narrows and tags in one pass, so
53
+ * feeding it raw rows avoids a second walk. Optional: a driver that omits it
54
+ * falls back to {@link all}, still correct but with the extra narrowing walk.
55
+ */
56
+ allRaw?<T = unknown>(...params: unknown[]): Promise<T[]>;
9
57
  }
58
+ interface GroupRunError {
59
+ message: string;
60
+ name?: string;
61
+ code?: string;
62
+ }
63
+ type GroupRunOutcome = {
64
+ ok: true;
65
+ results: RunResult[];
66
+ } | {
67
+ ok: false;
68
+ error: GroupRunError;
69
+ };
10
70
  interface SQLiteConnection {
11
71
  exec(sql: string): Promise<void>;
12
72
  prepare(sql: string): Promise<SQLiteStatement>;
13
73
  transaction<T>(fn: (conn: SQLiteConnection) => Promise<T>): Promise<T>;
14
74
  close(): Promise<void>;
75
+ runBatch?(sql: string, paramsBatch: readonly unknown[][]): Promise<RunResult[]>;
76
+ runBatchSummary?(sql: string, paramsBatch: readonly unknown[][]): Promise<BatchSummary>;
77
+ /**
78
+ * Runs several independent units in one transaction, one outcome per unit in
79
+ * order. A unit is one write or one whole transaction, and a unit that fails
80
+ * must not disturb the others.
81
+ */
82
+ runGroup?(units: readonly {
83
+ statements: readonly {
84
+ sql: string;
85
+ params?: readonly unknown[];
86
+ }[];
87
+ }[]): Promise<GroupRunOutcome[]>;
15
88
  }
89
+ /**
90
+ * SQLite `PRAGMA synchronous` level applied to a connection. `normal` is safe
91
+ * from corruption in WAL mode but can lose the most recent commits on power
92
+ * loss; `full` fsyncs every commit; `extra` adds a directory sync after the
93
+ * rollback journal is unlinked in DELETE journal mode and equals `full` in
94
+ * WAL mode; `off` hands writes to the OS without syncing and is sanctioned
95
+ * only for re-runnable bulk loads.
96
+ */
97
+ type SynchronousLevel = 'off' | 'normal' | 'full' | 'extra';
16
98
  interface OpenOptions {
17
99
  readonly?: boolean;
18
100
  walMode?: boolean;
101
+ synchronous?: SynchronousLevel;
19
102
  }
20
103
  interface DriverCapabilities {
21
104
  multipleConnections: boolean;
22
105
  extensions: boolean;
23
106
  }
107
+ /**
108
+ * Lets a worker thread rebuild the driver, since the driver's `open` function
109
+ * cannot cross the thread boundary. `specifier` must be importable from the
110
+ * worker and `config` must survive a structured clone; the worker imports the
111
+ * module and calls its `exportName` factory (default export otherwise) with it.
112
+ */
113
+ interface DriverWorkerEntry {
114
+ specifier: string;
115
+ exportName?: string;
116
+ config?: unknown;
117
+ }
24
118
  interface SQLiteDriver {
25
119
  readonly capabilities: DriverCapabilities;
26
120
  open(path: string, options?: OpenOptions): Promise<SQLiteConnection>;
121
+ readonly worker?: DriverWorkerEntry;
122
+ /**
123
+ * Offloads writes to a worker thread. Only a driver whose runtime has
124
+ * threads implements this, which is what keeps the thread machinery out of
125
+ * bundles built for runtimes that do not.
126
+ */
127
+ startWriterHost?(path: string, options: OpenOptions, hostOptions?: WorkerHostOptions): Promise<SQLiteConnection>;
128
+ createWriterContext?(): WriterContext;
129
+ createBackupEngine?(): BackupEngine;
130
+ /**
131
+ * Makes an extension path absolute. Passing a bare name to `load_extension`
132
+ * would let the dynamic linker search its own paths and open a different
133
+ * library than the operator named.
134
+ */
135
+ resolveExtensionPath?(extensionPath: string): string;
27
136
  }
28
137
 
29
138
  interface WaSqliteOptions {
@@ -12,12 +12,40 @@ function defineDriver(config) {
12
12
  if (!config.capabilities || typeof config.open !== "function") {
13
13
  throw new SirannonError("Driver must define capabilities and open()", "INVALID_DRIVER");
14
14
  }
15
+ if (config.worker !== void 0 && typeof config.worker.specifier !== "string") {
16
+ throw new SirannonError("Driver worker entry must define a string specifier", "INVALID_DRIVER");
17
+ }
15
18
  return Object.freeze({
16
19
  capabilities: Object.freeze({ ...config.capabilities }),
17
- open: config.open
20
+ open: config.open,
21
+ ...config.worker ? { worker: Object.freeze({ ...config.worker }) } : {},
22
+ ...config.startWriterHost ? { startWriterHost: config.startWriterHost } : {},
23
+ ...config.createWriterContext ? { createWriterContext: config.createWriterContext } : {},
24
+ ...config.createBackupEngine ? { createBackupEngine: config.createBackupEngine } : {},
25
+ ...config.resolveExtensionPath ? { resolveExtensionPath: config.resolveExtensionPath } : {}
18
26
  });
19
27
  }
20
28
 
29
+ // src/core/driver/synchronous.ts
30
+ var DEFAULT_SYNCHRONOUS = "normal";
31
+ var SYNCHRONOUS_PRAGMA_VALUES = {
32
+ off: "OFF",
33
+ normal: "NORMAL",
34
+ full: "FULL",
35
+ extra: "EXTRA"
36
+ };
37
+ function synchronousPragmaValue(level) {
38
+ const resolved = level ?? DEFAULT_SYNCHRONOUS;
39
+ const value = SYNCHRONOUS_PRAGMA_VALUES[resolved];
40
+ if (value === void 0) {
41
+ throw new SirannonError(
42
+ `Invalid synchronous level '${String(resolved)}': expected 'off', 'normal', 'full', or 'extra'`,
43
+ "INVALID_SYNCHRONOUS"
44
+ );
45
+ }
46
+ return value;
47
+ }
48
+
21
49
  // src/drivers/wa-sqlite/index.ts
22
50
  function waSqlite(driverOptions) {
23
51
  return defineDriver({
@@ -46,7 +74,7 @@ function waSqlite(driverOptions) {
46
74
  } catch {
47
75
  }
48
76
  }
49
- await sqlite3.exec(db, "PRAGMA synchronous = NORMAL");
77
+ await sqlite3.exec(db, `PRAGMA synchronous = ${synchronousPragmaValue(options?.synchronous)}`);
50
78
  await sqlite3.exec(db, "PRAGMA foreign_keys = ON");
51
79
  async function execStatements(sql, params) {
52
80
  const columns = [];