deepbase-sqlite 3.7.0 → 3.8.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 CHANGED
@@ -82,7 +82,49 @@ 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()` | Read-only `PRAGMA integrity_check`; returns its status verbatim. |
106
+ | `backup(destination)` | Read-only source, 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
+ `checkIntegrity()` and `backup()` open a temporary read-only connection with
111
+ `fileMustExist: true`, then close it. They never create, migrate, or change
112
+ PRAGMAs on the source database; a missing or mistyped source path fails instead
113
+ of producing an empty database. `backup()` creates destination parent
114
+ directories only after opening the source, then reopens the result read-only and
115
+ verifies it with `integrity_check` — a backup nobody validated is not a backup.
116
+ It rejects with code `DEEPBASE_SQLITE_BACKUP_CORRUPT` when the copy fails to
117
+ verify, leaving the bad file on disk for inspection, so a restore routine must
118
+ never pick a backup by timestamp alone. The copy is consistent with writes that
119
+ land while it runs, and it does not block the driver's write queue.
120
+
121
+ `vacuum()` and `checkpoint()` do take the write lock, so they queue behind the
122
+ driver's own writes and honour `busyRetry`. Run them when the application is
123
+ idle. `checkpoint()` rejects with code `DEEPBASE_SQLITE_NOT_WAL` on databases
124
+ opened with `pragma: 'none'`, which use a rollback journal and have no WAL.
125
+
126
+ Retention, rotation and scheduling stay in your application — the driver takes a
127
+ verified snapshot, nothing more.
86
128
 
87
129
  ### Nested Data Structure
88
130
 
@@ -92,7 +134,7 @@ Efficiently stores nested objects using a key-value schema:
92
134
  - Values are stored as JSON
93
135
  - Fast lookups for both exact keys and partial paths
94
136
 
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.
137
+ 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
138
 
97
139
  ### ACID Compliance
98
140
 
@@ -148,15 +190,10 @@ CREATE TABLE deepbase (
148
190
  seq INTEGER NOT NULL
149
191
  );
150
192
 
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
- );
193
+ CREATE INDEX deepbase_seq_idx ON deepbase(seq);
157
194
  ```
158
195
 
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`.
196
+ Optimized PRAGMA profiles create `deepbase` `WITHOUT ROWID`.
160
197
 
161
198
  Example data:
162
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepbase-sqlite",
3
- "version": "3.7.0",
3
+ "version": "3.8.1",
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.1"
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 { backupFrom, checkIntegrityAt, 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,22 @@ export class SqliteDriver extends DeepBaseDriver {
122
123
  this._connected = true;
123
124
  }
124
125
 
126
+ async checkIntegrity() {
127
+ return checkIntegrityAt(this.fileName, this.busyTimeoutMs);
128
+ }
129
+
130
+ async backup(destination) {
131
+ return backupFrom(this.fileName, destination, this.busyTimeoutMs);
132
+ }
133
+
134
+ async vacuum() {
135
+ return this._runWrite(() => vacuum(this.db));
136
+ }
137
+
138
+ async checkpoint(mode = 'PASSIVE') {
139
+ return this._runWrite(() => checkpoint(this.db, mode));
140
+ }
141
+
125
142
  async connect() {
126
143
  if (this._connected) return;
127
144
  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
+ /** Opens the existing database read-only, runs `PRAGMA integrity_check`,
39
+ * then closes it. Does not connect or mutate the driver database. */
40
+ checkIntegrity(): Promise<string>;
41
+
42
+ /**
43
+ * Opens the existing source database read-only, writes an online backup to
44
+ * `destination`, then verifies the copy with `integrity_check`. Resolves
45
+ * with `destination`, or rejects with code
46
+ * `DEEPBASE_SQLITE_BACKUP_CORRUPT` when 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,72 @@
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
+ function checkIntegrity(db) {
8
+ return db.pragma('integrity_check', { simple: true });
9
+ }
10
+
11
+ function openReadonly(fileName, timeout) {
12
+ return new Database(fileName, {
13
+ readonly: true,
14
+ fileMustExist: true,
15
+ timeout,
16
+ });
17
+ }
18
+
19
+ export function checkIntegrityAt(fileName, timeout) {
20
+ const db = openReadonly(fileName, timeout);
21
+ try {
22
+ return checkIntegrity(db);
23
+ } finally {
24
+ db.close();
25
+ }
26
+ }
27
+
28
+ export async function backupFrom(fileName, destination, timeout) {
29
+ const source = openReadonly(fileName, timeout);
30
+ try {
31
+ fs.mkdirSync(pathModule.dirname(pathModule.resolve(destination)), { recursive: true });
32
+ await source.backup(destination);
33
+ } finally {
34
+ source.close();
35
+ }
36
+
37
+ const status = checkIntegrityAt(destination, timeout);
38
+ if (status !== 'ok') {
39
+ const error = new Error(
40
+ `deepbase-sqlite: backup verification failed for ${destination}: ${status}. ` +
41
+ 'The corrupt file was left in place for inspection; do not restore from it.',
42
+ );
43
+ error.code = 'DEEPBASE_SQLITE_BACKUP_CORRUPT';
44
+ throw error;
45
+ }
46
+
47
+ return destination;
48
+ }
49
+
50
+ export function vacuum(db) {
51
+ db.exec('VACUUM');
52
+ }
53
+
54
+ export function checkpoint(db, mode) {
55
+ if (!CHECKPOINT_MODES.includes(mode)) {
56
+ throw new TypeError(
57
+ `deepbase-sqlite: checkpoint mode must be one of ${CHECKPOINT_MODES.join(', ')}; received ${mode}`,
58
+ );
59
+ }
60
+
61
+ if (db.pragma('journal_mode', { simple: true }) !== 'wal') {
62
+ const error = new Error(
63
+ "deepbase-sqlite: checkpoint requires journal_mode=WAL. Databases opened with pragma: 'none' " +
64
+ 'use a rollback journal and have no WAL to checkpoint.',
65
+ );
66
+ error.code = 'DEEPBASE_SQLITE_NOT_WAL';
67
+ throw error;
68
+ }
69
+
70
+ const [result] = db.pragma(`wal_checkpoint(${mode})`);
71
+ return result;
72
+ }
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
@@ -1,4 +1,5 @@
1
1
  import assert from 'assert';
2
+ import Database from 'better-sqlite3';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
4
5
  import { fileURLToPath } from 'url';
@@ -728,5 +729,150 @@ for (const pragma of PRAGMA_MODES) {
728
729
  assert.strictEqual(await db.get('inventory'), 500);
729
730
  });
730
731
  });
732
+
733
+ describe('Maintenance', function () {
734
+ it('reports a healthy database as ok', async function () {
735
+ await db.set('key', 'value');
736
+ assert.strictEqual(await db.getDriver(0).checkIntegrity(), 'ok');
737
+ });
738
+
739
+ it('backs up to a verified copy containing the data', async function () {
740
+ await db.set('users', 'alice', { name: 'Alice' });
741
+ const destination = path.join(testDataPath, 'backups', `backup-${pragma}.db`);
742
+
743
+ assert.strictEqual(await db.getDriver(0).backup(destination), destination);
744
+
745
+ const copy = new DeepBase(new SqliteDriver({
746
+ name: `backup-${pragma}`,
747
+ path: path.join(testDataPath, 'backups'),
748
+ pragma,
749
+ }));
750
+ assert.deepStrictEqual(await copy.get('users', 'alice'), { name: 'Alice' });
751
+ await copy.disconnect();
752
+ });
753
+
754
+ it('creates missing parent directories for the destination', async function () {
755
+ const destination = path.join(testDataPath, 'deep', 'nested', 'backup.db');
756
+ await db.getDriver(0).backup(destination);
757
+ assert.ok(fs.existsSync(destination));
758
+ });
759
+
760
+ it('includes writes made after the driver opened the connection', async function () {
761
+ await db.set('counter', 41);
762
+ await db.inc('counter', 1);
763
+ const destination = path.join(testDataPath, 'backups', `late-${pragma}.db`);
764
+ await db.getDriver(0).backup(destination);
765
+
766
+ const copy = new DeepBase(new SqliteDriver({
767
+ name: `late-${pragma}`,
768
+ path: path.join(testDataPath, 'backups'),
769
+ pragma,
770
+ }));
771
+ assert.strictEqual(await copy.get('counter'), 42);
772
+ await copy.disconnect();
773
+ });
774
+
775
+ it('uses a temporary readonly connection', async function () {
776
+ await db.set('key', 'value');
777
+ const driver = db.getDriver(0);
778
+ await driver.disconnect();
779
+
780
+ assert.strictEqual(await driver.checkIntegrity(), 'ok');
781
+ assert.strictEqual(driver._connected, false);
782
+ assert.strictEqual(driver.db, null);
783
+ });
784
+
785
+ it('does not create a missing source database or backup directory', async function () {
786
+ const driver = new SqliteDriver({
787
+ name: `missing-${pragma}`,
788
+ path: path.join(testDataPath, 'missing'),
789
+ pragma,
790
+ });
791
+ const destination = path.join(testDataPath, 'should-not-exist', 'backup.db');
792
+
793
+ await assert.rejects(driver.checkIntegrity());
794
+ await assert.rejects(driver.backup(destination));
795
+
796
+ assert.strictEqual(fs.existsSync(driver.fileName), false);
797
+ assert.strictEqual(fs.existsSync(path.dirname(destination)), false);
798
+ });
799
+
800
+ it('vacuums without losing data', async function () {
801
+ for (let i = 0; i < 50; i++) {
802
+ await db.set('users', `user${i}`, { name: `User ${i}` });
803
+ }
804
+ await db.del('users', 'user0');
805
+
806
+ await db.getDriver(0).vacuum();
807
+
808
+ assert.strictEqual(await db.get('users', 'user0'), null);
809
+ assert.deepStrictEqual(await db.get('users', 'user49'), { name: 'User 49' });
810
+ assert.strictEqual(await db.getDriver(0).checkIntegrity(), 'ok');
811
+ });
812
+
813
+ it('rejects an unknown checkpoint mode', async function () {
814
+ await assert.rejects(db.getDriver(0).checkpoint('SOMETIMES'), TypeError);
815
+ });
816
+
817
+ if (pragma === 'none') {
818
+ it('refuses to checkpoint a rollback-journal database', async function () {
819
+ await db.set('key', 'value');
820
+ await assert.rejects(
821
+ db.getDriver(0).checkpoint(),
822
+ error => error.code === 'DEEPBASE_SQLITE_NOT_WAL',
823
+ );
824
+ });
825
+ } else {
826
+ it('checkpoints the WAL back into the database file', async function () {
827
+ await db.set('key', 'value');
828
+ const result = await db.getDriver(0).checkpoint('TRUNCATE');
829
+ assert.strictEqual(result.busy, 0);
830
+ assert.strictEqual(result.log, 0, 'TRUNCATE leaves an empty WAL');
831
+ assert.strictEqual(await db.get('key'), 'value');
832
+ });
833
+ }
834
+ });
731
835
  });
732
836
  }
837
+
838
+ describe('SqliteDriver backup verification', function () {
839
+ const corruptPath = path.join(testDataPath, 'corrupt');
840
+
841
+ afterEach(function () {
842
+ if (fs.existsSync(testDataPath)) {
843
+ fs.rmSync(testDataPath, { recursive: true, force: true });
844
+ }
845
+ });
846
+
847
+ it('rejects when the written copy does not verify', async function () {
848
+ const driver = new SqliteDriver({ name: 'source', path: corruptPath });
849
+ for (let i = 0; i < 200; i++) {
850
+ await driver.set('users', `user${i}`, { name: `User ${i}` });
851
+ }
852
+
853
+ const destination = path.join(corruptPath, 'backup.db');
854
+ const originalBackup = Database.prototype.backup;
855
+ Database.prototype.backup = async function (dest, ...args) {
856
+ const result = await originalBackup.call(this, dest, ...args);
857
+ // Scribble over every page but the header, so the file still opens as a
858
+ // database yet fails integrity_check.
859
+ const handle = fs.openSync(dest, 'r+');
860
+ const damaged = fs.fstatSync(handle).size - 4096;
861
+ fs.writeSync(handle, Buffer.alloc(damaged, 0xff), 0, damaged, 4096);
862
+ fs.closeSync(handle);
863
+ return result;
864
+ };
865
+
866
+ try {
867
+ await assert.rejects(
868
+ driver.backup(destination),
869
+ error => error.code === 'DEEPBASE_SQLITE_BACKUP_CORRUPT',
870
+ );
871
+ } finally {
872
+ Database.prototype.backup = originalBackup;
873
+ }
874
+ assert.ok(fs.existsSync(destination), 'corrupt copy is left in place for inspection');
875
+
876
+ await driver.disconnect();
877
+ });
878
+ });