dbgate-sqlite-dumper 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,234 +1,234 @@
1
- # Restore API
2
-
3
- ```ts
4
- restoreSqlDump({ connection, source, options?, progress?, signal? }): Promise<SqlDumpRestoreResult>
5
- ```
6
-
7
- Restores a plain-SQL SQLite dump using only the `SqliteConnection` abstraction — no
8
- `sqlite3` shell, no external process. Works on dumps produced by this package **and** on
9
- dumps produced by the native `.dump`, and on any other script the shell would accept.
10
-
11
- ```ts
12
- import { createReadStream } from 'node:fs';
13
- import { restoreSqlDump } from 'dbgate-sqlite-dumper';
14
-
15
- const result = await restoreSqlDump({
16
- connection,
17
- source: createReadStream('shop.sql'),
18
- progress: event => {
19
- if (event.phase === 'executing' && event.executionState === 'finished') {
20
- console.log(`${event.statementsProcessed} statements, ${event.rowsRestored} rows`);
21
- }
22
- },
23
- });
24
- ```
25
-
26
- ## `source`
27
-
28
- ```ts
29
- type SqlDumpSource =
30
- string | Buffer | Uint8Array | Readable | AsyncIterable<string | Buffer | Uint8Array>;
31
- ```
32
-
33
- Parsed incrementally: only the current statement's text plus a few carried bytes are held
34
- at a time, so a multi-gigabyte dump never lands in memory.
35
-
36
- `Buffer` is accepted alongside `string` for a reason that matters: the native `.dump`
37
- writes a `TEXT` value's stored bytes verbatim, so a database holding text that is not valid
38
- UTF-8 produces a dump that is not valid UTF-8 either. Pass the bytes; forcing them through
39
- `.toString()` would replace every invalid sequence with U+FFFD.
40
-
41
- ## How a script is split
42
-
43
- Exactly where the `sqlite3` shell splits it. Two layers, both ported from SQLite's sources:
44
-
45
- | Construct | Behaviour |
46
- | ------------------------------- | --------------------------------------------------------------------------------- |
47
- | `'…'` | String; `''` is a quote. **No** backslash escapes — SQLite has none. |
48
- | `"…"`, `` `…` ``, `[…]` | Quoted identifiers; a `;` inside never splits. |
49
- | `-- …`, `/* … */` | Comments (block comments do not nest). A block comment may run to end of input. |
50
- | `CREATE TRIGGER … BEGIN … END;` | One statement, inner `;`s included — the `sqlite3_complete()` state machine. |
51
- | `GO` / `/` alone on a line | Ends the pending statement, as in the shell. |
52
- | `.command` at column 0 | A shell dot-command; see below. |
53
- | `# …` at column 0 | A comment line, as in the shell. |
54
- | `\r\n` | The `\r` is dropped — everywhere, literals included — as the shell's reader does. |
55
- | UTF-8 byte-order mark | Whitespace. |
56
- | missing final `;` | The last statement still runs, as in the shell. |
57
-
58
- The dot-command and `#` rules apply only when no SQL is pending — `SELECT 1\n.5;` is one
59
- statement — exactly as the shell decides it.
60
-
61
- `tests/statementParser.test.ts` asserts identical output at **every** chunk size and
62
- **every** single split point; `integration/scripts.integration.test.ts` runs a set of
63
- hand-written scripts through both the shell and this package and requires identical
64
- databases afterwards.
65
-
66
- ### Dot-commands
67
-
68
- The shell runs lines beginning with `.` itself. Under the default
69
- `dotCommands: 'skip-presentational'`:
70
-
71
- - commands that only affect the shell's own output (`.mode`, `.headers`, `.print`, `.echo`,
72
- `.timer`, `.output`, …) are skipped and reported as `dot-command-skipped`;
73
- - `.quit` and `.exit` end the input, as in the shell;
74
- - every other command (`.read`, `.import`, `.open`, `.load`, `.shell`, `.parameter`, …) is
75
- refused with `UnsupportedClientCommandError`. Silently skipping a `.read` would leave the
76
- referenced file's objects missing — a corrupted restore that looks like a successful one.
77
-
78
- `dotCommands: 'error'` refuses every dot-command.
79
-
80
- ### Text that is not valid UTF-8
81
-
82
- A statement whose string literal holds such bytes is executed with that literal rewritten
83
- to `(CAST(X'…' AS TEXT))`, which stores the identical bytes, and a `text-literal-rewritten`
84
- warning says how many. Such bytes anywhere else — in an identifier — cannot be represented
85
- and raise `InvalidTextEncodingError`.
86
-
87
- ## `RestoreOptions`
88
-
89
- | Option | Default | Meaning |
90
- | --------------------- | ----------------------- | ---------------------------------------------------------------------------- |
91
- | `stopOnError` | `true` | Stop at the first failing statement. |
92
- | `restoreSessionState` | `true` | Put the connection back as it was; see below. |
93
- | `schemaWrites` | `'allow'` | Virtual-table dumps write `sqlite_schema`; `'refuse'` fails such statements. |
94
- | `transaction` | `'script'` | `'wrap'`: run everything in one transaction of the restore's own. |
95
- | `disableForeignKeys` | `false` | `PRAGMA foreign_keys=OFF` for the restore (data-only dumps). |
96
- | `verifyForeignKeys` | `false` | Run `PRAGMA foreign_key_check` afterwards and report violations. |
97
- | `maxStatementBytes` | 256 MiB | Bound on one statement's buffered text. |
98
- | `dotCommands` | `'skip-presentational'` | See above. |
99
-
100
- ### `restoreSessionState`
101
-
102
- A native dump is written for a shell that exits when it is done. Run through a long-lived
103
- handle, three things would otherwise leak into the caller's session:
104
-
105
- - **`foreign_keys`.** A dump's first line turns enforcement off, and nothing turns it back
106
- on. It is returned to its value before the restore.
107
- - **An open transaction.** A restore that stops at a failing statement, is cancelled, or
108
- reads a dump truncated before its `COMMIT` would hand the handle back mid-transaction,
109
- holding a write lock on the file. The transaction the _script_ opened is rolled back —
110
- as the shell's exit would roll it back — and reported as `transaction-rolled-back`. A
111
- transaction the caller opened before the restore is never touched.
112
- - **`writable_schema`.** Turned back off if the script left it on.
113
-
114
- ### `schemaWrites`
115
-
116
- A dump of a database with virtual tables recreates them by inserting into `sqlite_schema`
117
- under `PRAGMA writable_schema=ON` — the native `.dump` does exactly this, and opens with a
118
- comment saying it requires `SQLITE_DBCONFIG_DEFENSIVE` to be off. Under `'allow'`, when the
119
- adapter can (`setDefensive`), defensive mode is lifted at the `writable_schema=ON`
120
- statement, restored afterwards, and reported as `defensive-mode-disabled`. The schema is
121
- then reloaded (`PRAGMA writable_schema=RESET`), so the restored virtual tables work on the
122
- same handle — without it they would only appear on the next connection.
123
-
124
- ### `transaction: 'wrap'`
125
-
126
- The restore opens its own transaction before the first statement that is not a `PRAGMA` —
127
- so the dump's leading `PRAGMA foreign_keys=OFF` still takes effect — skips the script's own
128
- `BEGIN`/`COMMIT`/`ROLLBACK`, and commits at the end, or rolls back everything on failure.
129
- Meant for data-only dumps, which have no transaction of their own and would otherwise sync
130
- to disk once per row.
131
-
132
- ## `SqlDumpRestoreResult`
133
-
134
- ```ts
135
- {
136
- statementsExecuted: number;
137
- statementsFailed: number;
138
- rowsRestored: number; // sum of rows changed
139
- bytesConsumed: number;
140
- errors: readonly RestoreStatementError[];
141
- warnings: readonly RestoreWarning[];
142
- cancelled: boolean;
143
- }
144
- ```
145
-
146
- ## Errors
147
-
148
- ### Parse errors — always fatal, thrown
149
-
150
- The statement boundaries themselves cannot be trusted past the failure point.
151
-
152
- | Error | Cause |
153
- | ------------------------------- | --------------------------------------------------------------------- |
154
- | `MalformedSqlDumpError` | Input ends inside a string or quoted identifier — usually truncation. |
155
- | `StatementTooLargeError` | One statement exceeded `maxStatementBytes`. |
156
- | `UnsupportedClientCommandError` | A dot-command that would change the database. |
157
- | `InvalidTextEncodingError` | Bytes that are not valid UTF-8 outside any string literal. |
158
-
159
- All carry `line`, and all extend `SqlParseError` → `RestoreError` → `SqliteDumperError`
160
- (which has a stable `code`).
161
-
162
- ### Execution errors — scoped to one statement
163
-
164
- Recorded in `result.errors`; with `stopOnError: false` the restore continues.
165
-
166
- ```ts
167
- {
168
- statementIndex: number;
169
- location: { startLine: number; endLine: number };
170
- sqlPreview: string; // ≤200 chars, whitespace-collapsed, keys redacted
171
- message: string;
172
- sqliteError?: { code?: string; errno?: number; message: string };
173
- }
174
- ```
175
-
176
- `sqliteError.code` is SQLite's extended result code (`SQLITE_CONSTRAINT_UNIQUE`), so a
177
- caller can branch on it instead of matching message text. `location.startLine` points at
178
- the first real SQL character, not at the comments before it.
179
-
180
- **No error or preview ever contains an encryption key.** `redactSecrets` covers the syntax
181
- SQLCipher and the SQLite Encryption Extension use — `PRAGMA key`/`rekey`/`hexkey`/`textkey`
182
- and `ATTACH … KEY` — and is applied to driver messages too.
183
-
184
- ## Progress
185
-
186
- ```ts
187
- progress: event => {
188
- // 'connecting' | 'parsing' | 'executing' | 'finalizing'
189
- console.log(event.phase, event.statementIndex, event.currentObject, event.bytesConsumed);
190
- };
191
- ```
192
-
193
- `currentObject` is read from the statements themselves (`CREATE TABLE t`, `INSERT INTO t`),
194
- since a native dump carries no section banners.
195
-
196
- ## Preflight
197
-
198
- ```ts
199
- import { introspectSqlite, preflightRestore } from 'dbgate-sqlite-dumper';
200
-
201
- const { database } = await introspectSqlite(source);
202
- const report = await preflightRestore({ connection: target, database });
203
- if (report.diagnostics.some(diagnostic => diagnostic.severity === 'error')) {
204
- // e.g. object-already-exists, unsupported-target-feature (STRICT tables on 3.31),
205
- // virtual-table-module-unavailable, statistics-table-unsupported
206
- }
207
- ```
208
-
209
- Turns a failure part-way through a restore into an up-front report. Also reports the
210
- target's version, encoding, `foreign_keys`, open transaction, available modules and compile
211
- options, and whether defensive mode can be lifted. Never writes to the target.
212
-
213
- ## Using the parser on its own
214
-
215
- ```ts
216
- import {
217
- parseSqlStatements,
218
- streamSqlStatements,
219
- isSqliteDump,
220
- isCompleteStatement,
221
- } from 'dbgate-sqlite-dumper';
222
-
223
- if (!isSqliteDump(head)) throw new Error('not a SQLite dump');
224
-
225
- for (const statement of parseSqlStatements(sql)) {
226
- console.log(statement.statementIndex, statement.info.verb, statement.location.startLine);
227
- }
228
-
229
- for await (const statement of streamSqlStatements(createReadStream('big.sql'))) {
230
- // constant memory
231
- }
232
-
233
- isCompleteStatement('CREATE TRIGGER t AFTER INSERT ON x BEGIN SELECT 1;'); // false
234
- ```
1
+ # Restore API
2
+
3
+ ```ts
4
+ restoreSqlDump({ connection, source, options?, progress?, signal? }): Promise<SqlDumpRestoreResult>
5
+ ```
6
+
7
+ Restores a plain-SQL SQLite dump using only the `SqliteConnection` abstraction — no
8
+ `sqlite3` shell, no external process. Works on dumps produced by this package **and** on
9
+ dumps produced by the native `.dump`, and on any other script the shell would accept.
10
+
11
+ ```ts
12
+ import { createReadStream } from 'node:fs';
13
+ import { restoreSqlDump } from 'dbgate-sqlite-dumper';
14
+
15
+ const result = await restoreSqlDump({
16
+ connection,
17
+ source: createReadStream('shop.sql'),
18
+ progress: event => {
19
+ if (event.phase === 'executing' && event.executionState === 'finished') {
20
+ console.log(`${event.statementsProcessed} statements, ${event.rowsRestored} rows`);
21
+ }
22
+ },
23
+ });
24
+ ```
25
+
26
+ ## `source`
27
+
28
+ ```ts
29
+ type SqlDumpSource =
30
+ string | Buffer | Uint8Array | Readable | AsyncIterable<string | Buffer | Uint8Array>;
31
+ ```
32
+
33
+ Parsed incrementally: only the current statement's text plus a few carried bytes are held
34
+ at a time, so a multi-gigabyte dump never lands in memory.
35
+
36
+ `Buffer` is accepted alongside `string` for a reason that matters: the native `.dump`
37
+ writes a `TEXT` value's stored bytes verbatim, so a database holding text that is not valid
38
+ UTF-8 produces a dump that is not valid UTF-8 either. Pass the bytes; forcing them through
39
+ `.toString()` would replace every invalid sequence with U+FFFD.
40
+
41
+ ## How a script is split
42
+
43
+ Exactly where the `sqlite3` shell splits it. Two layers, both ported from SQLite's sources:
44
+
45
+ | Construct | Behaviour |
46
+ | ------------------------------- | --------------------------------------------------------------------------------- |
47
+ | `'…'` | String; `''` is a quote. **No** backslash escapes — SQLite has none. |
48
+ | `"…"`, `` `…` ``, `[…]` | Quoted identifiers; a `;` inside never splits. |
49
+ | `-- …`, `/* … */` | Comments (block comments do not nest). A block comment may run to end of input. |
50
+ | `CREATE TRIGGER … BEGIN … END;` | One statement, inner `;`s included — the `sqlite3_complete()` state machine. |
51
+ | `GO` / `/` alone on a line | Ends the pending statement, as in the shell. |
52
+ | `.command` at column 0 | A shell dot-command; see below. |
53
+ | `# …` at column 0 | A comment line, as in the shell. |
54
+ | `\r\n` | The `\r` is dropped — everywhere, literals included — as the shell's reader does. |
55
+ | UTF-8 byte-order mark | Whitespace. |
56
+ | missing final `;` | The last statement still runs, as in the shell. |
57
+
58
+ The dot-command and `#` rules apply only when no SQL is pending — `SELECT 1\n.5;` is one
59
+ statement — exactly as the shell decides it.
60
+
61
+ `tests/statementParser.test.ts` asserts identical output at **every** chunk size and
62
+ **every** single split point; `integration/scripts.integration.test.ts` runs a set of
63
+ hand-written scripts through both the shell and this package and requires identical
64
+ databases afterwards.
65
+
66
+ ### Dot-commands
67
+
68
+ The shell runs lines beginning with `.` itself. Under the default
69
+ `dotCommands: 'skip-presentational'`:
70
+
71
+ - commands that only affect the shell's own output (`.mode`, `.headers`, `.print`, `.echo`,
72
+ `.timer`, `.output`, …) are skipped and reported as `dot-command-skipped`;
73
+ - `.quit` and `.exit` end the input, as in the shell;
74
+ - every other command (`.read`, `.import`, `.open`, `.load`, `.shell`, `.parameter`, …) is
75
+ refused with `UnsupportedClientCommandError`. Silently skipping a `.read` would leave the
76
+ referenced file's objects missing — a corrupted restore that looks like a successful one.
77
+
78
+ `dotCommands: 'error'` refuses every dot-command.
79
+
80
+ ### Text that is not valid UTF-8
81
+
82
+ A statement whose string literal holds such bytes is executed with that literal rewritten
83
+ to `(CAST(X'…' AS TEXT))`, which stores the identical bytes, and a `text-literal-rewritten`
84
+ warning says how many. Such bytes anywhere else — in an identifier — cannot be represented
85
+ and raise `InvalidTextEncodingError`.
86
+
87
+ ## `RestoreOptions`
88
+
89
+ | Option | Default | Meaning |
90
+ | --------------------- | ----------------------- | ---------------------------------------------------------------------------- |
91
+ | `stopOnError` | `true` | Stop at the first failing statement. |
92
+ | `restoreSessionState` | `true` | Put the connection back as it was; see below. |
93
+ | `schemaWrites` | `'allow'` | Virtual-table dumps write `sqlite_schema`; `'refuse'` fails such statements. |
94
+ | `transaction` | `'script'` | `'wrap'`: run everything in one transaction of the restore's own. |
95
+ | `disableForeignKeys` | `false` | `PRAGMA foreign_keys=OFF` for the restore (data-only dumps). |
96
+ | `verifyForeignKeys` | `false` | Run `PRAGMA foreign_key_check` afterwards and report violations. |
97
+ | `maxStatementBytes` | 256 MiB | Bound on one statement's buffered text. |
98
+ | `dotCommands` | `'skip-presentational'` | See above. |
99
+
100
+ ### `restoreSessionState`
101
+
102
+ A native dump is written for a shell that exits when it is done. Run through a long-lived
103
+ handle, three things would otherwise leak into the caller's session:
104
+
105
+ - **`foreign_keys`.** A dump's first line turns enforcement off, and nothing turns it back
106
+ on. It is returned to its value before the restore.
107
+ - **An open transaction.** A restore that stops at a failing statement, is cancelled, or
108
+ reads a dump truncated before its `COMMIT` would hand the handle back mid-transaction,
109
+ holding a write lock on the file. The transaction the _script_ opened is rolled back —
110
+ as the shell's exit would roll it back — and reported as `transaction-rolled-back`. A
111
+ transaction the caller opened before the restore is never touched.
112
+ - **`writable_schema`.** Turned back off if the script left it on.
113
+
114
+ ### `schemaWrites`
115
+
116
+ A dump of a database with virtual tables recreates them by inserting into `sqlite_schema`
117
+ under `PRAGMA writable_schema=ON` — the native `.dump` does exactly this, and opens with a
118
+ comment saying it requires `SQLITE_DBCONFIG_DEFENSIVE` to be off. Under `'allow'`, when the
119
+ adapter can (`setDefensive`), defensive mode is lifted at the `writable_schema=ON`
120
+ statement, restored afterwards, and reported as `defensive-mode-disabled`. The schema is
121
+ then reloaded (`PRAGMA writable_schema=RESET`), so the restored virtual tables work on the
122
+ same handle — without it they would only appear on the next connection.
123
+
124
+ ### `transaction: 'wrap'`
125
+
126
+ The restore opens its own transaction before the first statement that is not a `PRAGMA` —
127
+ so the dump's leading `PRAGMA foreign_keys=OFF` still takes effect — skips the script's own
128
+ `BEGIN`/`COMMIT`/`ROLLBACK`, and commits at the end, or rolls back everything on failure.
129
+ Meant for data-only dumps, which have no transaction of their own and would otherwise sync
130
+ to disk once per row.
131
+
132
+ ## `SqlDumpRestoreResult`
133
+
134
+ ```ts
135
+ {
136
+ statementsExecuted: number;
137
+ statementsFailed: number;
138
+ rowsRestored: number; // sum of rows changed
139
+ bytesConsumed: number;
140
+ errors: readonly RestoreStatementError[];
141
+ warnings: readonly RestoreWarning[];
142
+ cancelled: boolean;
143
+ }
144
+ ```
145
+
146
+ ## Errors
147
+
148
+ ### Parse errors — always fatal, thrown
149
+
150
+ The statement boundaries themselves cannot be trusted past the failure point.
151
+
152
+ | Error | Cause |
153
+ | ------------------------------- | --------------------------------------------------------------------- |
154
+ | `MalformedSqlDumpError` | Input ends inside a string or quoted identifier — usually truncation. |
155
+ | `StatementTooLargeError` | One statement exceeded `maxStatementBytes`. |
156
+ | `UnsupportedClientCommandError` | A dot-command that would change the database. |
157
+ | `InvalidTextEncodingError` | Bytes that are not valid UTF-8 outside any string literal. |
158
+
159
+ All carry `line`, and all extend `SqlParseError` → `RestoreError` → `SqliteDumperError`
160
+ (which has a stable `code`).
161
+
162
+ ### Execution errors — scoped to one statement
163
+
164
+ Recorded in `result.errors`; with `stopOnError: false` the restore continues.
165
+
166
+ ```ts
167
+ {
168
+ statementIndex: number;
169
+ location: { startLine: number; endLine: number };
170
+ sqlPreview: string; // ≤200 chars, whitespace-collapsed, keys redacted
171
+ message: string;
172
+ sqliteError?: { code?: string; errno?: number; message: string };
173
+ }
174
+ ```
175
+
176
+ `sqliteError.code` is SQLite's extended result code (`SQLITE_CONSTRAINT_UNIQUE`), so a
177
+ caller can branch on it instead of matching message text. `location.startLine` points at
178
+ the first real SQL character, not at the comments before it.
179
+
180
+ **No error or preview ever contains an encryption key.** `redactSecrets` covers the syntax
181
+ SQLCipher and the SQLite Encryption Extension use — `PRAGMA key`/`rekey`/`hexkey`/`textkey`
182
+ and `ATTACH … KEY` — and is applied to driver messages too.
183
+
184
+ ## Progress
185
+
186
+ ```ts
187
+ progress: event => {
188
+ // 'connecting' | 'parsing' | 'executing' | 'finalizing'
189
+ console.log(event.phase, event.statementIndex, event.currentObject, event.bytesConsumed);
190
+ };
191
+ ```
192
+
193
+ `currentObject` is read from the statements themselves (`CREATE TABLE t`, `INSERT INTO t`),
194
+ since a native dump carries no section banners.
195
+
196
+ ## Preflight
197
+
198
+ ```ts
199
+ import { introspectSqlite, preflightRestore } from 'dbgate-sqlite-dumper';
200
+
201
+ const { database } = await introspectSqlite(source);
202
+ const report = await preflightRestore({ connection: target, database });
203
+ if (report.diagnostics.some(diagnostic => diagnostic.severity === 'error')) {
204
+ // e.g. object-already-exists, unsupported-target-feature (STRICT tables on 3.31),
205
+ // virtual-table-module-unavailable, statistics-table-unsupported
206
+ }
207
+ ```
208
+
209
+ Turns a failure part-way through a restore into an up-front report. Also reports the
210
+ target's version, encoding, `foreign_keys`, open transaction, available modules and compile
211
+ options, and whether defensive mode can be lifted. Never writes to the target.
212
+
213
+ ## Using the parser on its own
214
+
215
+ ```ts
216
+ import {
217
+ parseSqlStatements,
218
+ streamSqlStatements,
219
+ isSqliteDump,
220
+ isCompleteStatement,
221
+ } from 'dbgate-sqlite-dumper';
222
+
223
+ if (!isSqliteDump(head)) throw new Error('not a SQLite dump');
224
+
225
+ for (const statement of parseSqlStatements(sql)) {
226
+ console.log(statement.statementIndex, statement.info.verb, statement.location.startLine);
227
+ }
228
+
229
+ for await (const statement of streamSqlStatements(createReadStream('big.sql'))) {
230
+ // constant memory
231
+ }
232
+
233
+ isCompleteStatement('CREATE TRIGGER t AFTER INSERT ON x BEGIN SELECT 1;'); // false
234
+ ```
@@ -1,84 +1,84 @@
1
- # Round-trip testing
2
-
3
- Two suites, kept apart on purpose.
4
-
5
- | Suite | Command | Needs | What it proves |
6
- | -------------- | -------------------------- | -------------------------- | ----------------------------------------------- |
7
- | `tests/` | `npm test` | nothing but `node_modules` | Every layer, against in-memory databases. |
8
- | `integration/` | `npm run test:integration` | the `sqlite3` shell | Two-way interoperability with the native shell. |
9
-
10
- The unit suite uses `better-sqlite3` in-process, so it needs no server, no network and no
11
- binary beyond `node_modules`. The integration suite runs the real shell.
12
-
13
- ## Running the integration suite
14
-
15
- ```sh
16
- sudo apt-get install sqlite3 # or brew install sqlite
17
- npm run test:integration
18
- ```
19
-
20
- - When the shell is not installed the suites skip themselves with a message.
21
- `SQLITE_TEST_REQUIRED=1` (set in CI) turns that into a hard failure, so the suite can
22
- never silently no-op where it was supposed to run.
23
- - `SQLITE3_BIN=/path/to/sqlite3` tests a specific shell.
24
- - `KEEP_TEST_OUTPUT=1` keeps every dump and database under `test-output/` for inspection;
25
- CI uploads that directory when a run fails.
26
-
27
- ## What is tested
28
-
29
- ### Byte identity (`interop.integration.test.ts`)
30
-
31
- The fixture is dumped by this package and by the shell, with each native switch and its
32
- equivalent option (`.dump`, `--data-only`, `--preserve-rowids`, `--newlines`, `--nosys`).
33
- Every line must be identical; a line may differ only in the digits of a `REAL` literal, and
34
- is then compared again with each literal parsed as a double.
35
-
36
- ### The interoperability matrix
37
-
38
- | Path |
39
- | ------------------------------------------ |
40
- | this package → native `sqlite3` restore |
41
- | native `.dump` → this package's restore |
42
- | this package → this package |
43
- | native `.dump` → native restore (baseline) |
44
-
45
- Each ends by snapshotting the restored database — every `sqlite_schema` row except
46
- `rootpage`, and every row of every table with text compared as bytes — and deep-comparing
47
- it with the source. The baseline proves the fixture itself round-trips natively, so a
48
- failure elsewhere is this package's.
49
-
50
- On top of the matrix: dumping the restored database reproduces the first dump byte for byte,
51
- restored virtual tables answer FTS5 and R-tree queries, `NUL` characters survive through
52
- both restores, `user_version` is carried on request, and a data-only dump loads into an
53
- existing schema with `transaction: 'wrap'`.
54
-
55
- ### Scripts beyond `.dump` (`scripts.integration.test.ts`)
56
-
57
- Hand-written scripts in shapes the shell accepts — trigger bodies with `;`, `END` and
58
- `CASE`; comments everywhere; `GO` and `/` terminator lines; CRLF files and carriage returns
59
- inside literals; `#` comments and dot-commands; every kind of quoted identifier; several
60
- statements on a line. Each runs through the shell and through this package, and the two
61
- databases must be identical — including where both stop on the same error.
62
-
63
- ### Streaming (`streaming.integration.test.ts`)
64
-
65
- A 200 000-row table (over 30 MB of SQL) is dumped with bounded heap growth, compared with
66
- the shell's `.dump`, and restored both ways.
67
-
68
- ## The fixture
69
-
70
- `integration/fixture/schema.ts`: one statement per array element, executed one at a time
71
- with `better-sqlite3` — **never through this package's parser**, so a statement-splitting
72
- bug cannot corrupt the fixture and hide itself.
73
-
74
- It is deliberately unfriendly: a view created before the table it reads; two tables
75
- referencing each other; deleted rows leaving gaps in hidden rowids; names that are
76
- keywords, contain spaces and quotes, or are quoted with `'`, `"` and `[…]`; `WITHOUT ROWID`,
77
- `STRICT` and generated columns; partial, expression and `DESC` indexes; triggers whose
78
- bodies contain `; END`; an FTS5 and an R-tree table; statistics from `ANALYZE`; 64-bit
79
- extremes, infinities, `REAL`s without short representations, empty blobs, text with CRLF,
80
- emoji, and bytes that are not valid UTF-8.
81
-
82
- `sqlite_stat4` is dropped after `ANALYZE`: `better-sqlite3` builds with STAT4 and the
83
- Ubuntu shell does not, and a dump carrying it cannot restore there — natively either (see
84
- [known-limitations.md](known-limitations.md#sqlite_stat4-across-builds)).
1
+ # Round-trip testing
2
+
3
+ Two suites, kept apart on purpose.
4
+
5
+ | Suite | Command | Needs | What it proves |
6
+ | -------------- | -------------------------- | -------------------------- | ----------------------------------------------- |
7
+ | `tests/` | `npm test` | nothing but `node_modules` | Every layer, against in-memory databases. |
8
+ | `integration/` | `npm run test:integration` | the `sqlite3` shell | Two-way interoperability with the native shell. |
9
+
10
+ The unit suite uses `better-sqlite3` in-process, so it needs no server, no network and no
11
+ binary beyond `node_modules`. The integration suite runs the real shell.
12
+
13
+ ## Running the integration suite
14
+
15
+ ```sh
16
+ sudo apt-get install sqlite3 # or brew install sqlite
17
+ npm run test:integration
18
+ ```
19
+
20
+ - When the shell is not installed the suites skip themselves with a message.
21
+ `SQLITE_TEST_REQUIRED=1` (set in CI) turns that into a hard failure, so the suite can
22
+ never silently no-op where it was supposed to run.
23
+ - `SQLITE3_BIN=/path/to/sqlite3` tests a specific shell.
24
+ - `KEEP_TEST_OUTPUT=1` keeps every dump and database under `test-output/` for inspection;
25
+ CI uploads that directory when a run fails.
26
+
27
+ ## What is tested
28
+
29
+ ### Byte identity (`interop.integration.test.ts`)
30
+
31
+ The fixture is dumped by this package and by the shell, with each native switch and its
32
+ equivalent option (`.dump`, `--data-only`, `--preserve-rowids`, `--newlines`, `--nosys`).
33
+ Every line must be identical; a line may differ only in the digits of a `REAL` literal, and
34
+ is then compared again with each literal parsed as a double.
35
+
36
+ ### The interoperability matrix
37
+
38
+ | Path |
39
+ | ------------------------------------------ |
40
+ | this package → native `sqlite3` restore |
41
+ | native `.dump` → this package's restore |
42
+ | this package → this package |
43
+ | native `.dump` → native restore (baseline) |
44
+
45
+ Each ends by snapshotting the restored database — every `sqlite_schema` row except
46
+ `rootpage`, and every row of every table with text compared as bytes — and deep-comparing
47
+ it with the source. The baseline proves the fixture itself round-trips natively, so a
48
+ failure elsewhere is this package's.
49
+
50
+ On top of the matrix: dumping the restored database reproduces the first dump byte for byte,
51
+ restored virtual tables answer FTS5 and R-tree queries, `NUL` characters survive through
52
+ both restores, `user_version` is carried on request, and a data-only dump loads into an
53
+ existing schema with `transaction: 'wrap'`.
54
+
55
+ ### Scripts beyond `.dump` (`scripts.integration.test.ts`)
56
+
57
+ Hand-written scripts in shapes the shell accepts — trigger bodies with `;`, `END` and
58
+ `CASE`; comments everywhere; `GO` and `/` terminator lines; CRLF files and carriage returns
59
+ inside literals; `#` comments and dot-commands; every kind of quoted identifier; several
60
+ statements on a line. Each runs through the shell and through this package, and the two
61
+ databases must be identical — including where both stop on the same error.
62
+
63
+ ### Streaming (`streaming.integration.test.ts`)
64
+
65
+ A 200 000-row table (over 30 MB of SQL) is dumped with bounded heap growth, compared with
66
+ the shell's `.dump`, and restored both ways.
67
+
68
+ ## The fixture
69
+
70
+ `integration/fixture/schema.ts`: one statement per array element, executed one at a time
71
+ with `better-sqlite3` — **never through this package's parser**, so a statement-splitting
72
+ bug cannot corrupt the fixture and hide itself.
73
+
74
+ It is deliberately unfriendly: a view created before the table it reads; two tables
75
+ referencing each other; deleted rows leaving gaps in hidden rowids; names that are
76
+ keywords, contain spaces and quotes, or are quoted with `'`, `"` and `[…]`; `WITHOUT ROWID`,
77
+ `STRICT` and generated columns; partial, expression and `DESC` indexes; triggers whose
78
+ bodies contain `; END`; an FTS5 and an R-tree table; statistics from `ANALYZE`; 64-bit
79
+ extremes, infinities, `REAL`s without short representations, empty blobs, text with CRLF,
80
+ emoji, and bytes that are not valid UTF-8.
81
+
82
+ `sqlite_stat4` is dropped after `ANALYZE`: `better-sqlite3` builds with STAT4 and the
83
+ Ubuntu shell does not, and a dump carrying it cannot restore there — natively either (see
84
+ [known-limitations.md](known-limitations.md#sqlite_stat4-across-builds)).