deepbase-sqlite 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,7 +82,45 @@ const db2 = new DeepBase(new SqliteDriver({ name: 'mydb' }));
82
82
 
83
83
  SQLite still permits only one writer at a time. Keep write transactions short and use a client-server database when sustained write contention or multiple hosts are required. WAL requires a local filesystem shared by processes on the same host; do not place the database on NFS.
84
84
 
85
- When upgrading from a version that used the in-memory sequence counter, stop all old writer processes before starting the new version. The schema migration is automatic, but old and new sequence allocators must not write concurrently during a rolling deployment.
85
+ When upgrading from a version that used the in-memory sequence counter, stop all old writer processes before starting the new version. Old and new sequence allocators must not write concurrently during a rolling deployment.
86
+
87
+ ### Maintenance
88
+
89
+ The driver owns the `better-sqlite3` connection and exposes maintenance through
90
+ its own API, so a backup or vacuum job needs no `better-sqlite3` entry in your
91
+ own `package.json`. That keeps a single native binding in the tree and removes
92
+ any chance of a version mismatch between your copy and the driver's.
93
+
94
+ ```javascript
95
+ const driver = db.getDriver(0);
96
+
97
+ const status = await driver.checkIntegrity(); // 'ok', or what SQLite found
98
+ await driver.backup('./backups/mydb-2026-07-28.db');
99
+ await driver.vacuum();
100
+ await driver.checkpoint('TRUNCATE');
101
+ ```
102
+
103
+ | Method | Behaviour |
104
+ |--------|-----------|
105
+ | `checkIntegrity()` | Runs `PRAGMA integrity_check` and returns its status verbatim. |
106
+ | `backup(destination)` | Online copy, verified before it resolves. Returns `destination`. |
107
+ | `vacuum()` | Rebuilds the file to reclaim free pages. |
108
+ | `checkpoint(mode)` | `PASSIVE` (default), `FULL`, `RESTART` or `TRUNCATE`. Returns `{ busy, log, checkpointed }`. |
109
+
110
+ `backup()` creates parent directories as needed, then reopens the result and
111
+ verifies it with `integrity_check` — a backup nobody validated is not a backup.
112
+ It rejects with code `DEEPBASE_SQLITE_BACKUP_CORRUPT` when the copy fails to
113
+ verify, leaving the bad file on disk for inspection, so a restore routine must
114
+ never pick a backup by timestamp alone. The copy is consistent with writes that
115
+ land while it runs, and it does not block the driver's write queue.
116
+
117
+ `vacuum()` and `checkpoint()` do take the write lock, so they queue behind the
118
+ driver's own writes and honour `busyRetry`. Run them when the application is
119
+ idle. `checkpoint()` rejects with code `DEEPBASE_SQLITE_NOT_WAL` on databases
120
+ opened with `pragma: 'none'`, which use a rollback journal and have no WAL.
121
+
122
+ Retention, rotation and scheduling stay in your application — the driver takes a
123
+ verified snapshot, nothing more.
86
124
 
87
125
  ### Nested Data Structure
88
126
 
@@ -92,7 +130,7 @@ Efficiently stores nested objects using a key-value schema:
92
130
  - Values are stored as JSON
93
131
  - Fast lookups for both exact keys and partial paths
94
132
 
95
- Each row also stores a monotonic, database-assigned `seq` so reads that rebuild objects use `ORDER BY seq, key`. That matches JavaScript insertion order for sibling keys and keeps `shift()` / `pop()` aligned with `JsonDriver`. Existing databases migrate automatically inside an atomic `BEGIN IMMEDIATE` transaction. Legacy and duplicate sequence values are normalized while preserving their previous `ORDER BY seq, key` order.
133
+ Each row also stores a database-assigned `seq` so reads that rebuild objects use `ORDER BY seq, key`. That matches JavaScript insertion order for sibling keys and keeps `shift()` / `pop()` aligned with `JsonDriver`. For legacy databases, the driver only adds the missing column and index; it does not renumber existing rows. Historical ties remain deterministic through the `key` fallback order.
96
134
 
97
135
  ### ACID Compliance
98
136
 
@@ -148,15 +186,10 @@ CREATE TABLE deepbase (
148
186
  seq INTEGER NOT NULL
149
187
  );
150
188
 
151
- CREATE UNIQUE INDEX deepbase_seq_unique ON deepbase(seq);
152
-
153
- CREATE TABLE deepbase_meta (
154
- key TEXT PRIMARY KEY,
155
- value INTEGER NOT NULL
156
- );
189
+ CREATE INDEX deepbase_seq_idx ON deepbase(seq);
157
190
  ```
158
191
 
159
- `deepbase_meta` is always created `WITHOUT ROWID`; optimized PRAGMA profiles do the same for `deepbase`. The metadata table tracks the internal schema version, while user data remains exclusively in `deepbase`.
192
+ Optimized PRAGMA profiles create `deepbase` `WITHOUT ROWID`.
160
193
 
161
194
  Example data:
162
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepbase-sqlite",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "⚡ DeepBase SQLite - SQLite database driver",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -17,7 +17,7 @@
17
17
  "better-sqlite3": "^11.8.1"
18
18
  },
19
19
  "peerDependencies": {
20
- "deepbase": "^3.7.0"
20
+ "deepbase": "^3.8.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "mocha": "^10.8.2"
@@ -4,7 +4,8 @@ import fs from 'fs';
4
4
  import * as pathModule from 'path';
5
5
  import { withBusyRetry } from './busy.js';
6
6
  import { resolveSqliteConfig } from './config.js';
7
- import { migrateSchema } from './schema.js';
7
+ import { backupTo, checkIntegrity, checkpoint, vacuum } from './maintenance.js';
8
+ import { ensureSchema } from './schema.js';
8
9
 
9
10
  export class SqliteDriver extends DeepBaseDriver {
10
11
  constructor({ name, path, pragma, busyTimeoutMs, busyRetry, ...opts } = {}) {
@@ -61,7 +62,7 @@ export class SqliteDriver extends DeepBaseDriver {
61
62
  }
62
63
 
63
64
  const withoutRowid = cfg ? ' WITHOUT ROWID' : '';
64
- migrateSchema(this.db, { withoutRowid });
65
+ ensureSchema(this.db, { withoutRowid });
65
66
 
66
67
  this.getStmt = this.db.prepare('SELECT value FROM deepbase WHERE key = ?');
67
68
  this.setStmt = this.db.prepare(`
@@ -122,6 +123,24 @@ export class SqliteDriver extends DeepBaseDriver {
122
123
  this._connected = true;
123
124
  }
124
125
 
126
+ async checkIntegrity() {
127
+ await this.connect();
128
+ return checkIntegrity(this.db);
129
+ }
130
+
131
+ async backup(destination) {
132
+ await this.connect();
133
+ return backupTo(this.db, destination);
134
+ }
135
+
136
+ async vacuum() {
137
+ return this._runWrite(() => vacuum(this.db));
138
+ }
139
+
140
+ async checkpoint(mode = 'PASSIVE') {
141
+ return this._runWrite(() => checkpoint(this.db, mode));
142
+ }
143
+
125
144
  async connect() {
126
145
  if (this._connected) return;
127
146
  if (!this._connectPromise) {
package/src/config.js CHANGED
@@ -25,7 +25,6 @@ const PRAGMA_PROFILES = Object.freeze({
25
25
 
26
26
  export const SQLITE_CONFIG = Object.freeze({
27
27
  defaultPragma: 'balanced',
28
- schemaVersion: 1,
29
28
  busyTimeoutMs: 5000,
30
29
  busyRetry: Object.freeze({
31
30
  maxAttempts: 2,
package/src/index.d.ts CHANGED
@@ -6,6 +6,17 @@ export interface SqliteBusyRetryOptions {
6
6
  maxDelayMs?: number;
7
7
  }
8
8
 
9
+ export type SqliteCheckpointMode = 'PASSIVE' | 'FULL' | 'RESTART' | 'TRUNCATE';
10
+
11
+ export interface SqliteCheckpointResult {
12
+ /** `1` when readers or writers prevented the checkpoint from completing. */
13
+ busy: number;
14
+ /** Pages in the WAL file. */
15
+ log: number;
16
+ /** Pages moved into the database file. */
17
+ checkpointed: number;
18
+ }
19
+
9
20
  export interface SqliteDriverOptions extends DeepBaseDriverOptions {
10
21
  name?: string;
11
22
  path?: string;
@@ -23,6 +34,27 @@ export class SqliteDriver extends DeepBaseDriver {
23
34
  pragma: string;
24
35
  busyTimeoutMs: number;
25
36
  busyRetry: Required<SqliteBusyRetryOptions>;
37
+
38
+ /** Runs `PRAGMA integrity_check` and returns its status: `'ok'` when the
39
+ * database is sound, otherwise SQLite's description of the damage. */
40
+ checkIntegrity(): Promise<string>;
41
+
42
+ /**
43
+ * Writes an online backup to `destination`, creating parent directories as
44
+ * needed, then verifies the copy with `integrity_check`. Resolves with
45
+ * `destination`, or rejects with code `DEEPBASE_SQLITE_BACKUP_CORRUPT` when
46
+ * the copy does not verify.
47
+ */
48
+ backup(destination: string): Promise<string>;
49
+
50
+ /** Rebuilds the database file to reclaim free pages. Takes the write lock. */
51
+ vacuum(): Promise<void>;
52
+
53
+ /**
54
+ * Runs `PRAGMA wal_checkpoint`. Rejects with code `DEEPBASE_SQLITE_NOT_WAL`
55
+ * when the database uses a rollback journal (`pragma: 'none'`).
56
+ */
57
+ checkpoint(mode?: SqliteCheckpointMode): Promise<SqliteCheckpointResult>;
26
58
  }
27
59
 
28
60
  export { SqliteDriver as SqliteFastDriver };
@@ -0,0 +1,58 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs';
3
+ import * as pathModule from 'path';
4
+
5
+ const CHECKPOINT_MODES = ['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE'];
6
+
7
+ export function checkIntegrity(db) {
8
+ return db.pragma('integrity_check', { simple: true });
9
+ }
10
+
11
+ export async function backupTo(db, destination) {
12
+ fs.mkdirSync(pathModule.dirname(pathModule.resolve(destination)), { recursive: true });
13
+
14
+ await db.backup(destination);
15
+
16
+ const copy = new Database(destination, { readonly: true });
17
+ let status;
18
+ try {
19
+ status = checkIntegrity(copy);
20
+ } finally {
21
+ copy.close();
22
+ }
23
+
24
+ if (status !== 'ok') {
25
+ const error = new Error(
26
+ `deepbase-sqlite: backup verification failed for ${destination}: ${status}. ` +
27
+ 'The corrupt file was left in place for inspection; do not restore from it.',
28
+ );
29
+ error.code = 'DEEPBASE_SQLITE_BACKUP_CORRUPT';
30
+ throw error;
31
+ }
32
+
33
+ return destination;
34
+ }
35
+
36
+ export function vacuum(db) {
37
+ db.exec('VACUUM');
38
+ }
39
+
40
+ export function checkpoint(db, mode) {
41
+ if (!CHECKPOINT_MODES.includes(mode)) {
42
+ throw new TypeError(
43
+ `deepbase-sqlite: checkpoint mode must be one of ${CHECKPOINT_MODES.join(', ')}; received ${mode}`,
44
+ );
45
+ }
46
+
47
+ if (db.pragma('journal_mode', { simple: true }) !== 'wal') {
48
+ const error = new Error(
49
+ "deepbase-sqlite: checkpoint requires journal_mode=WAL. Databases opened with pragma: 'none' " +
50
+ 'use a rollback journal and have no WAL to checkpoint.',
51
+ );
52
+ error.code = 'DEEPBASE_SQLITE_NOT_WAL';
53
+ throw error;
54
+ }
55
+
56
+ const [result] = db.pragma(`wal_checkpoint(${mode})`);
57
+ return result;
58
+ }
package/src/schema.js CHANGED
@@ -1,79 +1,20 @@
1
- import { SQLITE_CONFIG } from './config.js';
2
-
3
- const SCHEMA_VERSION_KEY = 'schema_version';
4
-
5
- function createBaseSchema(db, withoutRowid) {
6
- db.exec(`
7
- CREATE TABLE IF NOT EXISTS deepbase (
8
- key TEXT PRIMARY KEY,
9
- value TEXT NOT NULL,
10
- seq INTEGER NOT NULL DEFAULT 0
11
- )${withoutRowid}
12
- `);
13
-
14
- const columns = db.prepare('PRAGMA table_info(deepbase)').all();
15
- if (!columns.some(column => column.name === 'seq')) {
16
- db.exec('ALTER TABLE deepbase ADD COLUMN seq INTEGER NOT NULL DEFAULT 0');
17
- }
18
-
19
- db.exec(`
20
- CREATE TABLE IF NOT EXISTS deepbase_meta (
21
- key TEXT PRIMARY KEY,
22
- value INTEGER NOT NULL
23
- ) WITHOUT ROWID
24
- `);
25
- }
26
-
27
- function readSchemaVersion(db) {
28
- const row = db.prepare('SELECT value FROM deepbase_meta WHERE key = ?').get(SCHEMA_VERSION_KEY);
29
- return Number(row?.value ?? 0);
30
- }
31
-
32
- function normalizeSequence(db) {
33
- const nonPositive = db.prepare('SELECT 1 FROM deepbase WHERE seq < 1 LIMIT 1').get();
34
- const needsNormalization = nonPositive || db.prepare(`
35
- SELECT 1
36
- FROM deepbase
37
- GROUP BY seq
38
- HAVING COUNT(*) > 1
39
- LIMIT 1
40
- `).get();
41
- if (!needsNormalization) return;
42
-
43
- const rows = db.prepare('SELECT key FROM deepbase ORDER BY seq, key').all();
44
- const update = db.prepare('UPDATE deepbase SET seq = ? WHERE key = ?');
45
-
46
- rows.forEach((row, index) => {
47
- update.run(index + 1, row.key);
48
- });
49
- }
50
-
51
- function writeSchemaVersion(db) {
52
- db.prepare(`
53
- INSERT INTO deepbase_meta (key, value)
54
- VALUES (?, ?)
55
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
56
- `).run(SCHEMA_VERSION_KEY, SQLITE_CONFIG.schemaVersion);
57
- }
58
-
59
- export function migrateSchema(db, { withoutRowid = '' } = {}) {
60
- const migration = db.transaction(() => {
61
- createBaseSchema(db, withoutRowid);
62
-
63
- const version = readSchemaVersion(db);
64
- if (version > SQLITE_CONFIG.schemaVersion) {
65
- throw new Error(
66
- `deepbase-sqlite schema version ${version} is newer than supported version ${SQLITE_CONFIG.schemaVersion}`,
67
- );
68
- }
69
-
70
- if (version < SQLITE_CONFIG.schemaVersion) {
71
- normalizeSequence(db);
72
- writeSchemaVersion(db);
1
+ export function ensureSchema(db, { withoutRowid = '' } = {}) {
2
+ const setup = db.transaction(() => {
3
+ db.exec(`
4
+ CREATE TABLE IF NOT EXISTS deepbase (
5
+ key TEXT PRIMARY KEY,
6
+ value TEXT NOT NULL,
7
+ seq INTEGER NOT NULL DEFAULT 0
8
+ )${withoutRowid}
9
+ `);
10
+
11
+ const columns = db.prepare('PRAGMA table_info(deepbase)').all();
12
+ if (!columns.some(column => column.name === 'seq')) {
13
+ db.exec('ALTER TABLE deepbase ADD COLUMN seq INTEGER NOT NULL DEFAULT 0');
73
14
  }
74
15
 
75
- db.exec('CREATE UNIQUE INDEX IF NOT EXISTS deepbase_seq_unique ON deepbase(seq)');
16
+ db.exec('CREATE INDEX IF NOT EXISTS deepbase_seq_idx ON deepbase(seq)');
76
17
  });
77
18
 
78
- migration.immediate();
19
+ setup.immediate();
79
20
  }
package/test/fixtures.js CHANGED
@@ -13,7 +13,7 @@ export function createLegacyDatabase(fileName, rows) {
13
13
  db.close();
14
14
  }
15
15
 
16
- export function createSequencedDatabase(fileName, rows, { failMigration = false } = {}) {
16
+ export function createSequencedDatabase(fileName, rows) {
17
17
  const db = new Database(fileName);
18
18
  db.exec(`
19
19
  CREATE TABLE deepbase (
@@ -29,16 +29,6 @@ export function createSequencedDatabase(fileName, rows, { failMigration = false
29
29
  }
30
30
  });
31
31
  insertRows(rows);
32
-
33
- if (failMigration) {
34
- db.exec(`
35
- CREATE TRIGGER fail_seq_migration
36
- BEFORE UPDATE OF seq ON deepbase
37
- BEGIN
38
- SELECT RAISE(ABORT, 'forced migration failure');
39
- END
40
- `);
41
- }
42
32
  db.close();
43
33
  }
44
34
 
@@ -46,12 +36,6 @@ export function inspectDatabase(fileName) {
46
36
  const db = new Database(fileName);
47
37
  const rows = db.prepare('SELECT key, seq FROM deepbase ORDER BY seq, key').all();
48
38
  const indexes = db.prepare("PRAGMA index_list('deepbase')").all();
49
- const metaTable = db.prepare(
50
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'deepbase_meta'",
51
- ).get();
52
- const schemaVersion = metaTable
53
- ? db.prepare("SELECT value FROM deepbase_meta WHERE key = 'schema_version'").get()?.value
54
- : undefined;
55
39
  db.close();
56
- return { rows, indexes, schemaVersion };
40
+ return { rows, indexes };
57
41
  }
@@ -3,7 +3,6 @@ import { fork } from 'child_process';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import Database from 'better-sqlite3';
7
6
  import { SqliteDriver } from '../src/SqliteDriver.js';
8
7
  import { createLegacyDatabase, createSequencedDatabase, inspectDatabase } from './fixtures.js';
9
8
 
@@ -93,7 +92,7 @@ describe('SqliteDriver multi-process safety', function () {
93
92
  await driver.disconnect();
94
93
  });
95
94
 
96
- it('migrates a legacy database without seq and preserves observable order', async function () {
95
+ it('adds a missing seq column without rewriting legacy rows', async function () {
97
96
  const name = 'legacy-no-seq';
98
97
  const fileName = path.join(testDataPath, `${name}.db`);
99
98
  createLegacyDatabase(fileName, [['b', 2], ['a', 1]]);
@@ -104,14 +103,13 @@ describe('SqliteDriver multi-process safety', function () {
104
103
 
105
104
  const state = inspectDatabase(fileName);
106
105
  assert.deepStrictEqual(state.rows, [
107
- { key: 'a', seq: 1 },
108
- { key: 'b', seq: 2 },
106
+ { key: 'a', seq: 0 },
107
+ { key: 'b', seq: 0 },
109
108
  ]);
110
- assert.strictEqual(state.schemaVersion, 1);
111
- assert.ok(state.indexes.some(index => index.name === 'deepbase_seq_unique' && index.unique === 1));
109
+ assert.ok(state.indexes.some(index => index.name === 'deepbase_seq_idx' && index.unique === 0));
112
110
  });
113
111
 
114
- it('normalizes duplicate seq values without changing their current order', async function () {
112
+ it('preserves duplicate historical seq values and their stable key order', async function () {
115
113
  const name = 'duplicate-seq';
116
114
  const fileName = path.join(testDataPath, `${name}.db`);
117
115
  createSequencedDatabase(fileName, [
@@ -126,41 +124,14 @@ describe('SqliteDriver multi-process safety', function () {
126
124
  await driver.disconnect();
127
125
 
128
126
  assert.deepStrictEqual(inspectDatabase(fileName).rows, [
129
- { key: 'a', seq: 1 },
130
- { key: 'b', seq: 2 },
131
- { key: 'c', seq: 3 },
132
- { key: 'd', seq: 4 },
133
- ]);
134
- });
135
-
136
- it('rolls back the complete migration when normalization fails', async function () {
137
- const name = 'migration-rollback';
138
- const fileName = path.join(testDataPath, `${name}.db`);
139
- createSequencedDatabase(fileName, [['a', 1, 0], ['b', 2, 0]], { failMigration: true });
140
-
141
- const driver = new SqliteDriver({ name, path: testDataPath, ...retryOptions });
142
- await assert.rejects(driver.connect(), /forced migration failure/);
143
-
144
- const failedState = inspectDatabase(fileName);
145
- assert.deepStrictEqual(failedState.rows, [
146
127
  { key: 'a', seq: 0 },
147
128
  { key: 'b', seq: 0 },
148
- ]);
149
- assert.strictEqual(failedState.schemaVersion, undefined);
150
-
151
- const raw = new Database(fileName);
152
- raw.exec('DROP TRIGGER fail_seq_migration');
153
- raw.close();
154
-
155
- await driver.connect();
156
- await driver.disconnect();
157
- assert.deepStrictEqual(inspectDatabase(fileName).rows, [
158
- { key: 'a', seq: 1 },
159
- { key: 'b', seq: 2 },
129
+ { key: 'c', seq: 2 },
130
+ { key: 'd', seq: 2 },
160
131
  ]);
161
132
  });
162
133
 
163
- it('serializes concurrent schema migration across processes', async function () {
134
+ it('serializes concurrent schema setup across processes', async function () {
164
135
  const name = 'concurrent-migration';
165
136
  const fileName = path.join(testDataPath, `${name}.db`);
166
137
  createLegacyDatabase(fileName, [['value', 1]]);
@@ -176,8 +147,8 @@ describe('SqliteDriver multi-process safety', function () {
176
147
  ));
177
148
 
178
149
  const state = inspectDatabase(fileName);
179
- assert.strictEqual(state.schemaVersion, 1);
180
- assert.deepStrictEqual(state.rows, [{ key: 'value', seq: 1 }]);
150
+ assert.deepStrictEqual(state.rows, [{ key: 'value', seq: 0 }]);
151
+ assert.ok(state.indexes.some(index => index.name === 'deepbase_seq_idx'));
181
152
  });
182
153
 
183
154
  it('keeps increments atomic across processes', async function () {
package/test/test.js CHANGED
@@ -728,5 +728,127 @@ for (const pragma of PRAGMA_MODES) {
728
728
  assert.strictEqual(await db.get('inventory'), 500);
729
729
  });
730
730
  });
731
+
732
+ describe('Maintenance', function () {
733
+ it('reports a healthy database as ok', async function () {
734
+ await db.set('key', 'value');
735
+ assert.strictEqual(await db.getDriver(0).checkIntegrity(), 'ok');
736
+ });
737
+
738
+ it('backs up to a verified copy containing the data', async function () {
739
+ await db.set('users', 'alice', { name: 'Alice' });
740
+ const destination = path.join(testDataPath, 'backups', `backup-${pragma}.db`);
741
+
742
+ assert.strictEqual(await db.getDriver(0).backup(destination), destination);
743
+
744
+ const copy = new DeepBase(new SqliteDriver({
745
+ name: `backup-${pragma}`,
746
+ path: path.join(testDataPath, 'backups'),
747
+ pragma,
748
+ }));
749
+ assert.deepStrictEqual(await copy.get('users', 'alice'), { name: 'Alice' });
750
+ await copy.disconnect();
751
+ });
752
+
753
+ it('creates missing parent directories for the destination', async function () {
754
+ const destination = path.join(testDataPath, 'deep', 'nested', 'backup.db');
755
+ await db.getDriver(0).backup(destination);
756
+ assert.ok(fs.existsSync(destination));
757
+ });
758
+
759
+ it('includes writes made after the driver opened the connection', async function () {
760
+ await db.set('counter', 41);
761
+ await db.inc('counter', 1);
762
+ const destination = path.join(testDataPath, 'backups', `late-${pragma}.db`);
763
+ await db.getDriver(0).backup(destination);
764
+
765
+ const copy = new DeepBase(new SqliteDriver({
766
+ name: `late-${pragma}`,
767
+ path: path.join(testDataPath, 'backups'),
768
+ pragma,
769
+ }));
770
+ assert.strictEqual(await copy.get('counter'), 42);
771
+ await copy.disconnect();
772
+ });
773
+
774
+ it('opens the connection on demand', async function () {
775
+ const driver = new SqliteDriver({ name: `lazy-${pragma}`, path: testDataPath, pragma });
776
+ assert.strictEqual(await driver.checkIntegrity(), 'ok');
777
+ await driver.disconnect();
778
+ });
779
+
780
+ it('vacuums without losing data', async function () {
781
+ for (let i = 0; i < 50; i++) {
782
+ await db.set('users', `user${i}`, { name: `User ${i}` });
783
+ }
784
+ await db.del('users', 'user0');
785
+
786
+ await db.getDriver(0).vacuum();
787
+
788
+ assert.strictEqual(await db.get('users', 'user0'), null);
789
+ assert.deepStrictEqual(await db.get('users', 'user49'), { name: 'User 49' });
790
+ assert.strictEqual(await db.getDriver(0).checkIntegrity(), 'ok');
791
+ });
792
+
793
+ it('rejects an unknown checkpoint mode', async function () {
794
+ await assert.rejects(db.getDriver(0).checkpoint('SOMETIMES'), TypeError);
795
+ });
796
+
797
+ if (pragma === 'none') {
798
+ it('refuses to checkpoint a rollback-journal database', async function () {
799
+ await db.set('key', 'value');
800
+ await assert.rejects(
801
+ db.getDriver(0).checkpoint(),
802
+ error => error.code === 'DEEPBASE_SQLITE_NOT_WAL',
803
+ );
804
+ });
805
+ } else {
806
+ it('checkpoints the WAL back into the database file', async function () {
807
+ await db.set('key', 'value');
808
+ const result = await db.getDriver(0).checkpoint('TRUNCATE');
809
+ assert.strictEqual(result.busy, 0);
810
+ assert.strictEqual(result.log, 0, 'TRUNCATE leaves an empty WAL');
811
+ assert.strictEqual(await db.get('key'), 'value');
812
+ });
813
+ }
814
+ });
731
815
  });
732
816
  }
817
+
818
+ describe('SqliteDriver backup verification', function () {
819
+ const corruptPath = path.join(testDataPath, 'corrupt');
820
+
821
+ afterEach(function () {
822
+ if (fs.existsSync(testDataPath)) {
823
+ fs.rmSync(testDataPath, { recursive: true, force: true });
824
+ }
825
+ });
826
+
827
+ it('rejects when the written copy does not verify', async function () {
828
+ const driver = new SqliteDriver({ name: 'source', path: corruptPath });
829
+ for (let i = 0; i < 200; i++) {
830
+ await driver.set('users', `user${i}`, { name: `User ${i}` });
831
+ }
832
+
833
+ const destination = path.join(corruptPath, 'backup.db');
834
+ const originalBackup = driver.db.backup.bind(driver.db);
835
+ driver.db.backup = async dest => {
836
+ const result = await originalBackup(dest);
837
+ // Scribble over every page but the header, so the file still opens as a
838
+ // database yet fails integrity_check.
839
+ const handle = fs.openSync(dest, 'r+');
840
+ const damaged = fs.fstatSync(handle).size - 4096;
841
+ fs.writeSync(handle, Buffer.alloc(damaged, 0xff), 0, damaged, 4096);
842
+ fs.closeSync(handle);
843
+ return result;
844
+ };
845
+
846
+ await assert.rejects(
847
+ driver.backup(destination),
848
+ error => error.code === 'DEEPBASE_SQLITE_BACKUP_CORRUPT',
849
+ );
850
+ assert.ok(fs.existsSync(destination), 'corrupt copy is left in place for inspection');
851
+
852
+ await driver.disconnect();
853
+ });
854
+ });