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 +42 -9
- package/package.json +2 -2
- package/src/SqliteDriver.js +21 -2
- package/src/config.js +0 -1
- package/src/index.d.ts +32 -0
- package/src/maintenance.js +58 -0
- package/src/schema.js +15 -74
- package/test/fixtures.js +2 -18
- package/test/test-multiprocess.js +10 -39
- package/test/test.js +122 -0
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
20
|
+
"deepbase": "^3.8.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"mocha": "^10.8.2"
|
package/src/SqliteDriver.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
)
|
|
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
|
|
16
|
+
db.exec('CREATE INDEX IF NOT EXISTS deepbase_seq_idx ON deepbase(seq)');
|
|
76
17
|
});
|
|
77
18
|
|
|
78
|
-
|
|
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
|
|
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
|
|
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('
|
|
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:
|
|
108
|
-
{ key: 'b', seq:
|
|
106
|
+
{ key: 'a', seq: 0 },
|
|
107
|
+
{ key: 'b', seq: 0 },
|
|
109
108
|
]);
|
|
110
|
-
assert.
|
|
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('
|
|
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
|
-
|
|
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
|
|
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.
|
|
180
|
-
assert.
|
|
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
|
+
});
|