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.
@@ -29,7 +29,7 @@ interface SqliteQuery {
29
29
  * `data/valueQuery.ts`), so a 64-bit integer or a `REAL`'s exact digits
30
30
  * never pass through a JavaScript number on the way into the dump.
31
31
  */
32
- type SqliteColumnValue = string | number | bigint | Buffer | Uint8Array | null;
32
+ type SqliteColumnValue = string | number | bigint | Buffer | Uint8Array | ArrayBuffer | readonly number[] | null;
33
33
  /** A single result row, keyed by column name. */
34
34
  interface SqliteRow {
35
35
  readonly [column: string]: SqliteColumnValue;
@@ -66,6 +66,62 @@ interface SqliteErrorInfo {
66
66
  readonly errno?: number;
67
67
  readonly message: string;
68
68
  }
69
+ /**
70
+ * What a connection can and cannot do, for engines that speak SQLite's SQL
71
+ * but are not an embedded SQLite handle — Cloudflare D1, reached over HTTP,
72
+ * is the case this exists for. Every field is optional and defaults to what
73
+ * an ordinary SQLite handle does, so a local adapter declares nothing.
74
+ *
75
+ * The dump honours each restriction by choosing a different but equivalent
76
+ * query, so the output is the same dump an ordinary handle onto the same
77
+ * database would produce.
78
+ */
79
+ interface SqliteConnectionFeatures {
80
+ /**
81
+ * `false` when the engine accepts no `BEGIN` / `SAVEPOINT`. A dump then
82
+ * reads without a snapshot (`consistency: 'none'`) and reports
83
+ * `snapshot-unavailable`. Defaults to `true`.
84
+ */
85
+ readonly transactions?: boolean;
86
+ /**
87
+ * `false` when the table-valued `pragma_xxx()` functions are refused; the
88
+ * catalog is then read through the `PRAGMA xxx(arg)` statements instead.
89
+ * Defaults to `true` (for SQLite 3.16+).
90
+ */
91
+ readonly pragmaFunctions?: boolean;
92
+ /**
93
+ * `false` when only the classic introspection pragmas are allowed:
94
+ * `table_info` / `index_info` in place of `table_xinfo` / `index_xinfo`,
95
+ * and no `table_list`. The dump is the same; what is lost is detail only a
96
+ * diagnostic uses (generated columns, index key order and collations).
97
+ * Defaults to `true`.
98
+ */
99
+ readonly extendedPragmas?: boolean;
100
+ /**
101
+ * `false` when statements must not name a schema (`"main"."t"`). Only the
102
+ * `main` schema can be dumped then. Defaults to `true`.
103
+ */
104
+ readonly schemaQualifiedNames?: boolean;
105
+ /**
106
+ * How binary values cross the connection. `'hex'` has SQLite return them
107
+ * as `hex()` text, for transports — such as JSON — that cannot carry bytes
108
+ * exactly. Defaults to `'native'`.
109
+ */
110
+ readonly binaryTransport?: 'native' | 'hex';
111
+ /**
112
+ * Read table data in pages of this many rows through {@link
113
+ * SqliteConnection.query} instead of {@link SqliteConnection.stream}, for
114
+ * engines that return a whole result at once. Pages are keyed on the rowid
115
+ * or the primary key, so each one is an index seek.
116
+ */
117
+ readonly pagedReadSize?: number;
118
+ /**
119
+ * Name prefixes of objects the engine keeps for itself in `sqlite_schema`
120
+ * and refuses to let a client read — D1's `_cf_` tables. They are left out
121
+ * of the dump. Compared case-insensitively.
122
+ */
123
+ readonly reservedNamePrefixes?: readonly string[];
124
+ }
69
125
  /**
70
126
  * One open SQLite database handle.
71
127
  *
@@ -76,7 +132,16 @@ interface SqliteErrorInfo {
76
132
  * stepping.
77
133
  */
78
134
  interface SqliteConnection {
135
+ /** Restrictions of the engine behind this handle; see {@link SqliteConnectionFeatures}. */
136
+ readonly features?: SqliteConnectionFeatures;
79
137
  query<Row extends SqliteRow = SqliteRow>(query: SqliteQuery, signal?: AbortSignal): Promise<SqliteQueryResult<Row>>;
138
+ /**
139
+ * Runs several read-only queries in one round trip, returning one result
140
+ * per query in order. Optional: introspection uses it, when present, to
141
+ * fetch the catalog of every table in a few requests instead of several
142
+ * per table. If a batch fails as a whole, its queries are run one by one.
143
+ */
144
+ queryBatch?(queries: readonly SqliteQuery[], signal?: AbortSignal): Promise<readonly SqliteQueryResult[]>;
80
145
  /** Streams rows without buffering the full result set in memory. */
81
146
  stream<Row extends SqliteRow = SqliteRow>(query: SqliteQuery, options?: SqliteStreamOptions): AsyncIterable<Row>;
82
147
  /**
@@ -148,4 +213,4 @@ interface SqliteConnectionSource {
148
213
  type SqliteConnectionInput = SqliteConnection | SqliteConnectionSource;
149
214
  declare function isSqliteConnectionSource(input: SqliteConnectionInput): input is SqliteConnectionSource;
150
215
 
151
- export { type AcquiredSqliteConnection as A, type SqliteConnection as S, type SqliteConnectionInput as a, type SqliteExecResult as b, type SqliteRow as c, type SqliteErrorInfo as d, type SqliteColumnValue as e, type SqliteConnectionSource as f, type SqliteParameterValue as g, type SqliteQuery as h, type SqliteQueryResult as i, type SqliteStreamOptions as j, isSqliteConnectionSource as k };
216
+ export { type AcquiredSqliteConnection as A, type SqliteConnection as S, type SqliteConnectionFeatures as a, type SqliteConnectionInput as b, type SqliteExecResult as c, type SqliteRow as d, type SqliteErrorInfo as e, type SqliteColumnValue as f, type SqliteConnectionSource as g, type SqliteParameterValue as h, type SqliteQuery as i, type SqliteQueryResult as j, type SqliteStreamOptions as k, isSqliteConnectionSource as l };
@@ -29,7 +29,7 @@ interface SqliteQuery {
29
29
  * `data/valueQuery.ts`), so a 64-bit integer or a `REAL`'s exact digits
30
30
  * never pass through a JavaScript number on the way into the dump.
31
31
  */
32
- type SqliteColumnValue = string | number | bigint | Buffer | Uint8Array | null;
32
+ type SqliteColumnValue = string | number | bigint | Buffer | Uint8Array | ArrayBuffer | readonly number[] | null;
33
33
  /** A single result row, keyed by column name. */
34
34
  interface SqliteRow {
35
35
  readonly [column: string]: SqliteColumnValue;
@@ -66,6 +66,62 @@ interface SqliteErrorInfo {
66
66
  readonly errno?: number;
67
67
  readonly message: string;
68
68
  }
69
+ /**
70
+ * What a connection can and cannot do, for engines that speak SQLite's SQL
71
+ * but are not an embedded SQLite handle — Cloudflare D1, reached over HTTP,
72
+ * is the case this exists for. Every field is optional and defaults to what
73
+ * an ordinary SQLite handle does, so a local adapter declares nothing.
74
+ *
75
+ * The dump honours each restriction by choosing a different but equivalent
76
+ * query, so the output is the same dump an ordinary handle onto the same
77
+ * database would produce.
78
+ */
79
+ interface SqliteConnectionFeatures {
80
+ /**
81
+ * `false` when the engine accepts no `BEGIN` / `SAVEPOINT`. A dump then
82
+ * reads without a snapshot (`consistency: 'none'`) and reports
83
+ * `snapshot-unavailable`. Defaults to `true`.
84
+ */
85
+ readonly transactions?: boolean;
86
+ /**
87
+ * `false` when the table-valued `pragma_xxx()` functions are refused; the
88
+ * catalog is then read through the `PRAGMA xxx(arg)` statements instead.
89
+ * Defaults to `true` (for SQLite 3.16+).
90
+ */
91
+ readonly pragmaFunctions?: boolean;
92
+ /**
93
+ * `false` when only the classic introspection pragmas are allowed:
94
+ * `table_info` / `index_info` in place of `table_xinfo` / `index_xinfo`,
95
+ * and no `table_list`. The dump is the same; what is lost is detail only a
96
+ * diagnostic uses (generated columns, index key order and collations).
97
+ * Defaults to `true`.
98
+ */
99
+ readonly extendedPragmas?: boolean;
100
+ /**
101
+ * `false` when statements must not name a schema (`"main"."t"`). Only the
102
+ * `main` schema can be dumped then. Defaults to `true`.
103
+ */
104
+ readonly schemaQualifiedNames?: boolean;
105
+ /**
106
+ * How binary values cross the connection. `'hex'` has SQLite return them
107
+ * as `hex()` text, for transports — such as JSON — that cannot carry bytes
108
+ * exactly. Defaults to `'native'`.
109
+ */
110
+ readonly binaryTransport?: 'native' | 'hex';
111
+ /**
112
+ * Read table data in pages of this many rows through {@link
113
+ * SqliteConnection.query} instead of {@link SqliteConnection.stream}, for
114
+ * engines that return a whole result at once. Pages are keyed on the rowid
115
+ * or the primary key, so each one is an index seek.
116
+ */
117
+ readonly pagedReadSize?: number;
118
+ /**
119
+ * Name prefixes of objects the engine keeps for itself in `sqlite_schema`
120
+ * and refuses to let a client read — D1's `_cf_` tables. They are left out
121
+ * of the dump. Compared case-insensitively.
122
+ */
123
+ readonly reservedNamePrefixes?: readonly string[];
124
+ }
69
125
  /**
70
126
  * One open SQLite database handle.
71
127
  *
@@ -76,7 +132,16 @@ interface SqliteErrorInfo {
76
132
  * stepping.
77
133
  */
78
134
  interface SqliteConnection {
135
+ /** Restrictions of the engine behind this handle; see {@link SqliteConnectionFeatures}. */
136
+ readonly features?: SqliteConnectionFeatures;
79
137
  query<Row extends SqliteRow = SqliteRow>(query: SqliteQuery, signal?: AbortSignal): Promise<SqliteQueryResult<Row>>;
138
+ /**
139
+ * Runs several read-only queries in one round trip, returning one result
140
+ * per query in order. Optional: introspection uses it, when present, to
141
+ * fetch the catalog of every table in a few requests instead of several
142
+ * per table. If a batch fails as a whole, its queries are run one by one.
143
+ */
144
+ queryBatch?(queries: readonly SqliteQuery[], signal?: AbortSignal): Promise<readonly SqliteQueryResult[]>;
80
145
  /** Streams rows without buffering the full result set in memory. */
81
146
  stream<Row extends SqliteRow = SqliteRow>(query: SqliteQuery, options?: SqliteStreamOptions): AsyncIterable<Row>;
82
147
  /**
@@ -148,4 +213,4 @@ interface SqliteConnectionSource {
148
213
  type SqliteConnectionInput = SqliteConnection | SqliteConnectionSource;
149
214
  declare function isSqliteConnectionSource(input: SqliteConnectionInput): input is SqliteConnectionSource;
150
215
 
151
- export { type AcquiredSqliteConnection as A, type SqliteConnection as S, type SqliteConnectionInput as a, type SqliteExecResult as b, type SqliteRow as c, type SqliteErrorInfo as d, type SqliteColumnValue as e, type SqliteConnectionSource as f, type SqliteParameterValue as g, type SqliteQuery as h, type SqliteQueryResult as i, type SqliteStreamOptions as j, isSqliteConnectionSource as k };
216
+ export { type AcquiredSqliteConnection as A, type SqliteConnection as S, type SqliteConnectionFeatures as a, type SqliteConnectionInput as b, type SqliteExecResult as c, type SqliteRow as d, type SqliteErrorInfo as e, type SqliteColumnValue as f, type SqliteConnectionSource as g, type SqliteParameterValue as h, type SqliteQuery as i, type SqliteQueryResult as j, type SqliteStreamOptions as k, isSqliteConnectionSource as l };
@@ -1,157 +1,180 @@
1
- # Architecture
2
-
3
- The package is a stack of layers, each usable on its own. Nothing above the `connection`
4
- layer knows what driver is in use; nothing below the `api` layer knows about a dump as a
5
- whole.
6
-
7
- ```
8
- ┌──────────────────────────────────────────┐
9
- api/ │ dumpSqlite() · restoreSqlDump() │ orchestration
10
- └──────────────────────────────────────────┘
11
- │ │ │
12
- introspection/ ──────────┘ │ └────── restore/
13
- sqlite_schema + pragmas → model │ sqlite3_complete + shell
14
- │ line rules → statements
15
- archive/ ─────── plan: what & in what order
16
- renderer/ ────── model + plan → plain SQL text preflight/ compatibility/
17
- data/ ────────── rows → INSERT statements selection/ security/
18
- writer/ ──────── text/bytes → Writable version/ model/
19
- │
20
- ┌──────────────────────────────────────────┐
21
- connection/ │ SqliteConnection (driver-agnostic) │
22
- └──────────────────────────────────────────┘
23
- │
24
- better-sqlite3.ts ─ the only module that knows better-sqlite3 exists
25
- ```
26
-
27
- This mirrors `dbgate-mysql-dumper`, `dbgate-pg-dumper`, `dbgate-mssql-dumper` and
28
- `dbgate-redis-dumper`; the SQLite-specific differences are called out below.
29
-
30
- ## `connection/` — the driver boundary
31
-
32
- `SqliteConnection` is the whole contract between this package and a driver: `query()`,
33
- `stream()`, and optional `execute()`, `describeError()`, `isInTransaction()` and
34
- `setDefensive()`, plus `cancel()`. The core never imports a driver, and
35
- `tests/packageBoundaries.test.ts` fails if it starts to.
36
-
37
- SQLite is embedded, so a "connection" is one open database handle. The contract keeps the
38
- shape of the network drivers' in the sibling packages — including `SqliteConnectionSource`
39
- for pool-like inputs — so an application can drive every dumper the same way.
40
-
41
- ### Why one read transaction
42
-
43
- A dump runs entirely inside one transaction on one handle (`session.ts`), opened as a
44
- `SAVEPOINT` so that it nests inside a transaction the caller already has, and with one read
45
- issued immediately so the snapshot is taken at the start rather than by whichever catalog
46
- query happens to run first. In WAL mode that is a true snapshot; in rollback-journal mode it
47
- holds a `SHARED` lock. The native shell does the same (`SAVEPOINT dump`). It is released
48
- without the caller's `AbortSignal`, so a cancelled dump still ends it.
49
-
50
- ## `introspection/` — catalog to model
51
-
52
- Everything comes from `sqlite_schema` (read as `sqlite_master`, which every release
53
- accepts), in rowid order, plus the table-valued pragma functions — `table_xinfo`,
54
- `index_list`, `index_xinfo`, `foreign_key_list`, `table_list` — with a fallback to plain
55
- `PRAGMA` statements on libraries too old for them. Tables are classified as ordinary,
56
- virtual, shadow (a virtual table's storage) or system (`sqlite_*`).
57
-
58
- ### Why the DDL is not reconstructed
59
-
60
- SQLite keeps every `CREATE` statement verbatim, and the native `.dump` writes that text.
61
- Reconstructing DDL from the column model would lose comments, spelling, formatting and any
62
- clause the model does not know about, for no gain. The model exists for everything else —
63
- planning, selection, `INSERT` column lists, compatibility checks, diagnostics — which is why
64
- `renderPlainSql` is a pure function of it.
65
-
66
- ## `archive/` — the plan
67
-
68
- `inspectDumpArchive` turns a model into ordered `ArchiveEntry` objects. It is pure: no SQL
69
- text, no streams, no connection.
70
-
71
- The order is the native `.dump`'s — creation order — not a topological sort. Creation order
72
- is valid for everything SQLite checks at `CREATE` time (an index after its table, a trigger
73
- after its table or view), SQLite resolves view and trigger bodies lazily, and the dump's
74
- `PRAGMA foreign_keys=OFF` makes table order irrelevant to foreign keys. Dependencies are
75
- still recorded and then _verified_ against that order:
76
-
77
- - **`hard`** — a trigger depends on its table's data this way: created before the rows, it
78
- would fire for each one.
79
- - **`preference`** — every foreign key. Recording them as hard edges would report a false
80
- cycle for exactly the circular schemas that restore perfectly well.
81
-
82
- ## `renderer/` — model to text
83
-
84
- A port of the parts of `shell.c` that shape `.dump` output — `printSchemaLine()`,
85
- `run_table_dump_query()`, the virtual-table `INSERT INTO sqlite_schema` — driven by the
86
- archive, with row data arriving through an `onTableData` hook so the renderer never needs a
87
- connection. `sqlite3_complete()` itself is ported (in `restore/complete.ts`) because
88
- `printSchemaLine()` calls it to decide how to terminate DDL that ends in a comment.
89
-
90
- ## `data/` — rows to `INSERT`
91
-
92
- `exportTableDataAsInserts` streams one table in constant memory.
93
-
94
- ### Why SQLite renders the values
95
-
96
- The `SELECT` it runs does not fetch column values; it fetches each value's storage class and
97
- a representation computed _by SQLite_: integers as text, reals as the finished literal —
98
- using SQLite's own `printf('%!.20g')`, the formatter the shell uses — and text as its stored
99
- bytes. Three problems disappear at once: 64-bit integers never become JavaScript numbers;
100
- `REAL` digits match the shell's exactly, which no JavaScript formatting could; and text that
101
- is not valid UTF-8 is not "repaired" by the driver's decoder. The core is then exact with
102
- any driver that can return strings and bytes.
103
-
104
- `SqlChunkBuilder` keeps `(string | Buffer)[]` parts and only falls back to `Buffer.concat`
105
- once it has been handed bytes, so the all-text case stays on the string fast path.
106
-
107
- ## `restore/` — text to statements
108
-
109
- ### The parser
110
-
111
- `SqlStatementParser` is an incremental scanner over **bytes**: a dump is not necessarily
112
- valid UTF-8, and `latin1` is a bijection between bytes and code units, so nothing is lost
113
- and every character the scanner reacts to is ASCII.
114
-
115
- It has two layers, both ported: `sqlite3_complete()`'s eight-state machine, which decides
116
- when a `;` completes a statement (and keeps trigger bodies whole), and the shell's
117
- `process_input()` line rules — dot-commands and `#` comments at column 0 when nothing is
118
- pending, `GO`/`/` terminator lines checked with the shell's own `line_is_complete()`
119
- condition, and the `\r` its line reader drops before each `\n`.
120
-
121
- Anything whose meaning depends on the next chunk — a `-` or `/` that may open a comment, a
122
- `*` that may close one, a `\r` that may precede `\n`, a line start that may be a `GO` line,
123
- a word that may be a keyword — is carried or kept in state, never guessed.
124
- `tests/statementParser.test.ts` checks identical output at every chunk size and every split
125
- point.
126
-
127
- ### Session cleanup
128
-
129
- `RestoreSessionState` follows only classified top-level statements (transaction control,
130
- `PRAGMA foreign_keys`, `PRAGMA writable_schema`, writes to the schema table), never text
131
- inside rows or trigger bodies, and puts back what the script changed: it rolls back a
132
- transaction the script opened and left open, resets `writable_schema`, and returns
133
- `foreign_keys` to its previous value — which a native dump never does, because it is written
134
- for a shell that exits.
135
-
136
- ## `writer/` — text and bytes out
137
-
138
- `DumpWriter.write` accepts `string | Buffer` for the reason above. `StreamDumpWriter` honours
139
- backpressure by gating on `write()`'s return value with the `drain` listener attached in the
140
- same tick, and never calls `end()` on a caller-owned stream.
141
-
142
- ## `security/` — quoting and escaping
143
-
144
- Two quoting functions, for two jobs. `quoteIdentifier` always double-quotes, and is used for
145
- every query this package runs. `quoteIdentifierIfNeeded` reproduces the shell's
146
- `quoteChar()` — quote only non-identifiers and SQLite's 147 keywords — and is used only
147
- where output must match the native layout. Literal rendering ports
148
- `output_quoted_escaped_string()`, including `unused_string()`'s placeholder choice.
149
-
150
- ## Testing strategy
151
-
152
- | Suite | Needs | What it proves |
153
- | -------------- | --------------- | ------------------------------------------------------------------- |
154
- | `tests/` | nothing | Every layer, against in-memory `better-sqlite3` databases. |
155
- | `integration/` | `sqlite3` shell | Byte identity, the four-way matrix, shell-script parity, streaming. |
156
-
157
- See [round-trip-testing.md](round-trip-testing.md).
1
+ # Architecture
2
+
3
+ The package is a stack of layers, each usable on its own. Nothing above the `connection`
4
+ layer knows what driver is in use; nothing below the `api` layer knows about a dump as a
5
+ whole.
6
+
7
+ ```
8
+ ┌──────────────────────────────────────────┐
9
+ api/ │ dumpSqlite() · restoreSqlDump() │ orchestration
10
+ └──────────────────────────────────────────┘
11
+ │ │ │
12
+ introspection/ ──────────┘ │ └────── restore/
13
+ sqlite_schema + pragmas → model │ sqlite3_complete + shell
14
+ │ line rules → statements
15
+ archive/ ─────── plan: what & in what order
16
+ renderer/ ────── model + plan → plain SQL text preflight/ compatibility/
17
+ data/ ────────── rows → INSERT statements selection/ security/
18
+ writer/ ──────── text/bytes → Writable version/ model/
19
+ │
20
+ ┌──────────────────────────────────────────┐
21
+ connection/ │ SqliteConnection (driver-agnostic) │
22
+ └──────────────────────────────────────────┘
23
+ │
24
+ better-sqlite3.ts ─ the only module that knows better-sqlite3 exists
25
+ ```
26
+
27
+ This mirrors `dbgate-mysql-dumper`, `dbgate-pg-dumper`, `dbgate-mssql-dumper` and
28
+ `dbgate-redis-dumper`; the SQLite-specific differences are called out below.
29
+
30
+ ## `connection/` — the driver boundary
31
+
32
+ `SqliteConnection` is the whole contract between this package and a driver: `query()`,
33
+ `stream()`, and optional `execute()`, `describeError()`, `isInTransaction()` and
34
+ `setDefensive()`, plus `cancel()`. The core never imports a driver, and
35
+ `tests/packageBoundaries.test.ts` fails if it starts to.
36
+
37
+ SQLite is embedded, so a "connection" is one open database handle. The contract keeps the
38
+ shape of the network drivers' in the sibling packages — including `SqliteConnectionSource`
39
+ for pool-like inputs — so an application can drive every dumper the same way.
40
+
41
+ ### Connection features: engines that are not a SQLite handle
42
+
43
+ An adapter may declare `features` — restrictions of the engine behind it — and an optional
44
+ `queryBatch()`. Cloudflare D1 (`src/d1.ts`) is why they exist: SQLite's SQL, reached over
45
+ HTTP, behind an authorizer. Each feature makes the core choose an equivalent query rather
46
+ than a different dump:
47
+
48
+ - `transactions: false` — the session reads without a snapshot and says so
49
+ (`snapshot-unavailable`);
50
+ - `pragmaFunctions: false` / `extendedPragmas: false` — the catalog is read through the
51
+ classic `PRAGMA` statements (the reader also falls back on its own the first time an
52
+ extended pragma is refused);
53
+ - `schemaQualifiedNames: false` — no statement names a schema;
54
+ - `binaryTransport: 'hex'` — text and blob bytes are selected as `hex()`;
55
+ - `pagedReadSize` — table data is read through `query()` in pages keyed on the rowid or
56
+ the primary key, in natural scan order, instead of through `stream()`;
57
+ - `reservedNamePrefixes` — objects the engine keeps for itself are left out, together
58
+ with their counters and statistics.
59
+
60
+ `queryBatch()` lets introspection fetch every table's catalog in a few requests; a batch
61
+ that fails is not fatal, its queries simply run one by one. The tests run every feature
62
+ against an ordinary handle as well, where each must leave the dump byte-identical.
63
+
64
+ ### Why one read transaction
65
+
66
+ A dump runs entirely inside one transaction on one handle (`session.ts`), opened as a
67
+ `SAVEPOINT` so that it nests inside a transaction the caller already has, and with one read
68
+ issued immediately so the snapshot is taken at the start rather than by whichever catalog
69
+ query happens to run first. In WAL mode that is a true snapshot; in rollback-journal mode it
70
+ holds a `SHARED` lock. The native shell does the same (`SAVEPOINT dump`). It is released
71
+ without the caller's `AbortSignal`, so a cancelled dump still ends it.
72
+
73
+ ## `introspection/` — catalog to model
74
+
75
+ Everything comes from `sqlite_schema` (read as `sqlite_master`, which every release
76
+ accepts), in rowid order, plus the table-valued pragma functions — `table_xinfo`,
77
+ `index_list`, `index_xinfo`, `foreign_key_list`, `table_list` — with a fallback to plain
78
+ `PRAGMA` statements on libraries too old for them. Tables are classified as ordinary,
79
+ virtual, shadow (a virtual table's storage) or system (`sqlite_*`).
80
+
81
+ ### Why the DDL is not reconstructed
82
+
83
+ SQLite keeps every `CREATE` statement verbatim, and the native `.dump` writes that text.
84
+ Reconstructing DDL from the column model would lose comments, spelling, formatting and any
85
+ clause the model does not know about, for no gain. The model exists for everything else —
86
+ planning, selection, `INSERT` column lists, compatibility checks, diagnostics — which is why
87
+ `renderPlainSql` is a pure function of it.
88
+
89
+ ## `archive/` — the plan
90
+
91
+ `inspectDumpArchive` turns a model into ordered `ArchiveEntry` objects. It is pure: no SQL
92
+ text, no streams, no connection.
93
+
94
+ The order is the native `.dump`'s — creation order — not a topological sort. Creation order
95
+ is valid for everything SQLite checks at `CREATE` time (an index after its table, a trigger
96
+ after its table or view), SQLite resolves view and trigger bodies lazily, and the dump's
97
+ `PRAGMA foreign_keys=OFF` makes table order irrelevant to foreign keys. Dependencies are
98
+ still recorded and then _verified_ against that order:
99
+
100
+ - **`hard`** — a trigger depends on its table's data this way: created before the rows, it
101
+ would fire for each one.
102
+ - **`preference`** — every foreign key. Recording them as hard edges would report a false
103
+ cycle for exactly the circular schemas that restore perfectly well.
104
+
105
+ ## `renderer/` — model to text
106
+
107
+ A port of the parts of `shell.c` that shape `.dump` output — `printSchemaLine()`,
108
+ `run_table_dump_query()`, the virtual-table `INSERT INTO sqlite_schema` — driven by the
109
+ archive, with row data arriving through an `onTableData` hook so the renderer never needs a
110
+ connection. `sqlite3_complete()` itself is ported (in `restore/complete.ts`) because
111
+ `printSchemaLine()` calls it to decide how to terminate DDL that ends in a comment.
112
+
113
+ ## `data/` — rows to `INSERT`
114
+
115
+ `exportTableDataAsInserts` streams one table in constant memory.
116
+
117
+ ### Why SQLite renders the values
118
+
119
+ The `SELECT` it runs does not fetch column values; it fetches each value's storage class and
120
+ a representation computed _by SQLite_: integers as text, reals as the finished literal —
121
+ using SQLite's own `printf('%!.20g')`, the formatter the shell uses — and text as its stored
122
+ bytes. Three problems disappear at once: 64-bit integers never become JavaScript numbers;
123
+ `REAL` digits match the shell's exactly, which no JavaScript formatting could; and text that
124
+ is not valid UTF-8 is not "repaired" by the driver's decoder. The core is then exact with
125
+ any driver that can return strings and bytes.
126
+
127
+ `SqlChunkBuilder` keeps `(string | Buffer)[]` parts and only falls back to `Buffer.concat`
128
+ once it has been handed bytes, so the all-text case stays on the string fast path.
129
+
130
+ ## `restore/` — text to statements
131
+
132
+ ### The parser
133
+
134
+ `SqlStatementParser` is an incremental scanner over **bytes**: a dump is not necessarily
135
+ valid UTF-8, and `latin1` is a bijection between bytes and code units, so nothing is lost
136
+ and every character the scanner reacts to is ASCII.
137
+
138
+ It has two layers, both ported: `sqlite3_complete()`'s eight-state machine, which decides
139
+ when a `;` completes a statement (and keeps trigger bodies whole), and the shell's
140
+ `process_input()` line rules — dot-commands and `#` comments at column 0 when nothing is
141
+ pending, `GO`/`/` terminator lines checked with the shell's own `line_is_complete()`
142
+ condition, and the `\r` its line reader drops before each `\n`.
143
+
144
+ Anything whose meaning depends on the next chunk — a `-` or `/` that may open a comment, a
145
+ `*` that may close one, a `\r` that may precede `\n`, a line start that may be a `GO` line,
146
+ a word that may be a keyword — is carried or kept in state, never guessed.
147
+ `tests/statementParser.test.ts` checks identical output at every chunk size and every split
148
+ point.
149
+
150
+ ### Session cleanup
151
+
152
+ `RestoreSessionState` follows only classified top-level statements (transaction control,
153
+ `PRAGMA foreign_keys`, `PRAGMA writable_schema`, writes to the schema table), never text
154
+ inside rows or trigger bodies, and puts back what the script changed: it rolls back a
155
+ transaction the script opened and left open, resets `writable_schema`, and returns
156
+ `foreign_keys` to its previous value — which a native dump never does, because it is written
157
+ for a shell that exits.
158
+
159
+ ## `writer/` — text and bytes out
160
+
161
+ `DumpWriter.write` accepts `string | Buffer` for the reason above. `StreamDumpWriter` honours
162
+ backpressure by gating on `write()`'s return value with the `drain` listener attached in the
163
+ same tick, and never calls `end()` on a caller-owned stream.
164
+
165
+ ## `security/` — quoting and escaping
166
+
167
+ Two quoting functions, for two jobs. `quoteIdentifier` always double-quotes, and is used for
168
+ every query this package runs. `quoteIdentifierIfNeeded` reproduces the shell's
169
+ `quoteChar()` — quote only non-identifiers and SQLite's 147 keywords — and is used only
170
+ where output must match the native layout. Literal rendering ports
171
+ `output_quoted_escaped_string()`, including `unused_string()`'s placeholder choice.
172
+
173
+ ## Testing strategy
174
+
175
+ | Suite | Needs | What it proves |
176
+ | -------------- | --------------- | ------------------------------------------------------------------- |
177
+ | `tests/` | nothing | Every layer, against in-memory `better-sqlite3` databases. |
178
+ | `integration/` | `sqlite3` shell | Byte identity, the four-way matrix, shell-script parity, streaming. |
179
+
180
+ See [round-trip-testing.md](round-trip-testing.md).