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,83 +1,83 @@
1
- # The `better-sqlite3` adapter
2
-
3
- ```ts
4
- import { fromBetterSqlite3, connectBetterSqlite3 } from 'dbgate-sqlite-dumper/better-sqlite3';
5
- ```
6
-
7
- The only module that knows `better-sqlite3` exists, reachable only through this separate
8
- entry point. `better-sqlite3` is an optional peer dependency: the type import costs nothing
9
- at runtime and the value import is dynamic, so the core package — and this module — load
10
- without it installed. `better-sqlite3` is the driver DbGate's own SQLite plugin uses.
11
-
12
- ## Ownership
13
-
14
- | Function | Who closes the database |
15
- | --------------------------------------- | ----------------------------------------- |
16
- | `fromBetterSqlite3(database)` | The caller. The database is **borrowed**. |
17
- | `connectBetterSqlite3(filename, opts?)` | The returned `close()`. Idempotent. |
18
-
19
- `connectBetterSqlite3` passes `opts` straight to the `Database` constructor, so
20
- `{ readonly: true }` for a dump source, `{ fileMustExist: true }` and the rest all work.
21
-
22
- ## Behaviour
23
-
24
- - **Integers are read as `bigint`** (`safeIntegers(true)`) for every catalog query, so no
25
- value is ever rounded. Row data does not depend on this: it is read through SQL that has
26
- SQLite render every value as text or bytes (see
27
- [supported-data-types.md](supported-data-types.md)).
28
- - **`stream()` steps the statement with `iterate()`**, one row at a time, yielding between
29
- rows — constant memory for a table of any size. While a stream is open, the database
30
- cannot run another statement; this package never asks it to.
31
- - **`execute()` reports a statement's own row count** (`changes`), 0 for DDL — not the
32
- count left over from the previous `INSERT`. Text holding several statements, which the
33
- parser does not produce but other callers might, falls back to `exec()`.
34
- - **`isInTransaction()`** is `database.inTransaction`, which is what lets a restore roll
35
- back exactly the transaction the script opened.
36
- - **`setDefensive()`** is `unsafeMode()`, which is how `better-sqlite3` exposes
37
- `SQLITE_DBCONFIG_DEFENSIVE` — on by default in `better-sqlite3`. A restore lifts it only
38
- for a dump that writes `sqlite_schema` (a virtual-table dump), and puts it back. Unsafe
39
- mode also relaxes the driver's own guard against writing while iterating; this package
40
- never does that.
41
- - **`cancel()` is a no-op.** `better-sqlite3` runs each statement synchronously to
42
- completion, so cancellation takes effect between rows and between statements — prompt,
43
- since every statement a dump or a restore runs is short.
44
-
45
- ## Foreign keys
46
-
47
- `better-sqlite3` turns `foreign_keys` **on** for every database it opens; the `sqlite3`
48
- shell leaves it off. A full dump turns enforcement off itself, but a data-only dump does
49
- not, so loading one whose tables reference each other needs `disableForeignKeys: true`
50
- (see [restore-api.md](restore-api.md)).
51
-
52
- ## Statistics tables
53
-
54
- `better-sqlite3` is built with `SQLITE_ENABLE_STAT4`, so `ANALYZE` — including the
55
- `ANALYZE sqlite_schema;` a dump runs — creates a `sqlite_stat4` table. A dump of a database
56
- that has one does not restore on a build without STAT4 (the native `.dump` of it fails the
57
- same way); `preflightRestore` reports it as `statistics-table-unsupported`.
58
-
59
- ## Other drivers
60
-
61
- Anything that can run SQL can be adapted by implementing `SqliteConnection`:
62
-
63
- ```ts
64
- interface SqliteConnection {
65
- query(
66
- query: { sql: string; parameters?: readonly unknown[] },
67
- signal?,
68
- ): Promise<{ rows: readonly Row[] }>;
69
- stream(query, options?): AsyncIterable<Row>;
70
- execute?(sql: string, signal?): Promise<{ changes: number }>; // run text verbatim, no parameter parsing
71
- describeError?(error): { code?: string; errno?: number; message: string } | undefined;
72
- isInTransaction?(): boolean;
73
- setDefensive?(enabled: boolean): Promise<void>;
74
- cancel(): Promise<void>;
75
- }
76
- ```
77
-
78
- `query` and `stream` return rows as objects keyed by column name. Values may be any of
79
- SQLite's storage classes as the driver represents them; the core only relies on text,
80
- bytes and `null`, which every driver delivers exactly. The optional members degrade
81
- gracefully: without `execute`, statements go through `query`; without `isInTransaction`,
82
- restore tracks the script's own `BEGIN`/`COMMIT`; without `setDefensive`, a virtual-table
83
- dump restores only onto a handle that is not in defensive mode, and preflight says so.
1
+ # The `better-sqlite3` adapter
2
+
3
+ ```ts
4
+ import { fromBetterSqlite3, connectBetterSqlite3 } from 'dbgate-sqlite-dumper/better-sqlite3';
5
+ ```
6
+
7
+ The only module that knows `better-sqlite3` exists, reachable only through this separate
8
+ entry point. `better-sqlite3` is an optional peer dependency: the type import costs nothing
9
+ at runtime and the value import is dynamic, so the core package — and this module — load
10
+ without it installed. `better-sqlite3` is the driver DbGate's own SQLite plugin uses.
11
+
12
+ ## Ownership
13
+
14
+ | Function | Who closes the database |
15
+ | --------------------------------------- | ----------------------------------------- |
16
+ | `fromBetterSqlite3(database)` | The caller. The database is **borrowed**. |
17
+ | `connectBetterSqlite3(filename, opts?)` | The returned `close()`. Idempotent. |
18
+
19
+ `connectBetterSqlite3` passes `opts` straight to the `Database` constructor, so
20
+ `{ readonly: true }` for a dump source, `{ fileMustExist: true }` and the rest all work.
21
+
22
+ ## Behaviour
23
+
24
+ - **Integers are read as `bigint`** (`safeIntegers(true)`) for every catalog query, so no
25
+ value is ever rounded. Row data does not depend on this: it is read through SQL that has
26
+ SQLite render every value as text or bytes (see
27
+ [supported-data-types.md](supported-data-types.md)).
28
+ - **`stream()` steps the statement with `iterate()`**, one row at a time, yielding between
29
+ rows — constant memory for a table of any size. While a stream is open, the database
30
+ cannot run another statement; this package never asks it to.
31
+ - **`execute()` reports a statement's own row count** (`changes`), 0 for DDL — not the
32
+ count left over from the previous `INSERT`. Text holding several statements, which the
33
+ parser does not produce but other callers might, falls back to `exec()`.
34
+ - **`isInTransaction()`** is `database.inTransaction`, which is what lets a restore roll
35
+ back exactly the transaction the script opened.
36
+ - **`setDefensive()`** is `unsafeMode()`, which is how `better-sqlite3` exposes
37
+ `SQLITE_DBCONFIG_DEFENSIVE` — on by default in `better-sqlite3`. A restore lifts it only
38
+ for a dump that writes `sqlite_schema` (a virtual-table dump), and puts it back. Unsafe
39
+ mode also relaxes the driver's own guard against writing while iterating; this package
40
+ never does that.
41
+ - **`cancel()` is a no-op.** `better-sqlite3` runs each statement synchronously to
42
+ completion, so cancellation takes effect between rows and between statements — prompt,
43
+ since every statement a dump or a restore runs is short.
44
+
45
+ ## Foreign keys
46
+
47
+ `better-sqlite3` turns `foreign_keys` **on** for every database it opens; the `sqlite3`
48
+ shell leaves it off. A full dump turns enforcement off itself, but a data-only dump does
49
+ not, so loading one whose tables reference each other needs `disableForeignKeys: true`
50
+ (see [restore-api.md](restore-api.md)).
51
+
52
+ ## Statistics tables
53
+
54
+ `better-sqlite3` is built with `SQLITE_ENABLE_STAT4`, so `ANALYZE` — including the
55
+ `ANALYZE sqlite_schema;` a dump runs — creates a `sqlite_stat4` table. A dump of a database
56
+ that has one does not restore on a build without STAT4 (the native `.dump` of it fails the
57
+ same way); `preflightRestore` reports it as `statistics-table-unsupported`.
58
+
59
+ ## Other drivers
60
+
61
+ Anything that can run SQL can be adapted by implementing `SqliteConnection`:
62
+
63
+ ```ts
64
+ interface SqliteConnection {
65
+ query(
66
+ query: { sql: string; parameters?: readonly unknown[] },
67
+ signal?,
68
+ ): Promise<{ rows: readonly Row[] }>;
69
+ stream(query, options?): AsyncIterable<Row>;
70
+ execute?(sql: string, signal?): Promise<{ changes: number }>; // run text verbatim, no parameter parsing
71
+ describeError?(error): { code?: string; errno?: number; message: string } | undefined;
72
+ isInTransaction?(): boolean;
73
+ setDefensive?(enabled: boolean): Promise<void>;
74
+ cancel(): Promise<void>;
75
+ }
76
+ ```
77
+
78
+ `query` and `stream` return rows as objects keyed by column name. Values may be any of
79
+ SQLite's storage classes as the driver represents them; the core only relies on text,
80
+ bytes and `null`, which every driver delivers exactly. The optional members degrade
81
+ gracefully: without `execute`, statements go through `query`; without `isInTransaction`,
82
+ restore tracks the script's own `BEGIN`/`COMMIT`; without `setDefensive`, a virtual-table
83
+ dump restores only onto a handle that is not in defensive mode, and preflight says so.
@@ -0,0 +1,98 @@
1
+ # The Cloudflare D1 adapter
2
+
3
+ ```ts
4
+ import { fromD1Http, fromD1Binding } from 'dbgate-sqlite-dumper/d1';
5
+ ```
6
+
7
+ Dumps a [Cloudflare D1](https://developers.cloudflare.com/d1/) database into the same
8
+ plain-SQL file the native `sqlite3 .dump` would write for a local copy of it. The adapter
9
+ has no dependencies: it uses the global `fetch` (or the one you pass it).
10
+
11
+ **Dump only.** Restoring into D1 is not supported: D1 refuses the `BEGIN TRANSACTION` /
12
+ `COMMIT` and `PRAGMA writable_schema` statements a dump script relies on. To load a dump
13
+ into D1, use `wrangler d1 execute <db> --remote --file=dump.sql`, after removing those
14
+ statements as Cloudflare's import guide describes. A D1 dump restores into an ordinary
15
+ SQLite database with `restoreSqlDump` or the `sqlite3` shell as usual.
16
+
17
+ ## Over the REST API
18
+
19
+ ```ts
20
+ import fs from 'node:fs';
21
+ import { dumpSqlite } from 'dbgate-sqlite-dumper';
22
+ import { fromD1Http } from 'dbgate-sqlite-dumper/d1';
23
+
24
+ const connection = fromD1Http({
25
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
26
+ databaseId: 'c8a3...', // the UUID `wrangler d1 list` shows
27
+ apiToken: process.env.CLOUDFLARE_API_TOKEN, // needs D1 Read
28
+ });
29
+ const result = await dumpSqlite(connection, {}, fs.createWriteStream('backup.sql'));
30
+ ```
31
+
32
+ | Option | Default | Meaning |
33
+ | ------------ | -------------------------------------- | ---------------------------------------- |
34
+ | `accountId` | required | Cloudflare account ID |
35
+ | `databaseId` | required | The database's UUID |
36
+ | `apiToken` | required | API token with `D1 Read` (or `D1 Edit`) |
37
+ | `apiBaseUrl` | `https://api.cloudflare.com/client/v4` | For a proxy or a test server |
38
+ | `pageSize` | `1000` | Rows per request when table data is read |
39
+ | `fetch` | `globalThis.fetch` | Any `fetch`-compatible function |
40
+
41
+ Statements go to the `/raw` query endpoint. The catalog of every table is fetched in
42
+ `batch` requests of up to 50 statements, so introspection takes a handful of round trips
43
+ whatever the number of tables. The token is sent only in the `Authorization` header and is
44
+ never part of an error message. Errors are `D1Error`s carrying the message Cloudflare
45
+ returned, the HTTP `status`, and the `SQLITE_*` code when the message names one.
46
+
47
+ `connection.cancel()` (and the dump's `AbortSignal`) aborts the request in flight.
48
+
49
+ ## Inside a Worker
50
+
51
+ ```ts
52
+ import { dumpSqlite, BufferDumpWriter } from 'dbgate-sqlite-dumper';
53
+ import { fromD1Binding } from 'dbgate-sqlite-dumper/d1';
54
+
55
+ const connection = fromD1Binding(env.DB, { pageSize: 1000 });
56
+ ```
57
+
58
+ Uses `prepare().bind().raw({ columnNames: true })` and `batch()`. The package uses
59
+ `Buffer`, so the Worker needs the `nodejs_compat` compatibility flag.
60
+
61
+ ## How D1's restrictions are handled
62
+
63
+ The adapter declares what D1 does not allow as `connection.features`
64
+ (`d1ConnectionFeatures()`), and the core reads the database with queries D1 does accept.
65
+ None of this changes the dump; each is tested against an emulation of D1 that refuses what
66
+ D1 refuses, and must produce a dump byte-identical to the one of a local copy.
67
+
68
+ | D1 restriction | What the dump does instead |
69
+ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | No `BEGIN` / `SAVEPOINT` | Reads without a snapshot, and reports `snapshot-unavailable` (see below) |
71
+ | No table-valued `pragma_xxx()` functions | Uses the `PRAGMA table_info('t')` statement forms |
72
+ | Possibly no `table_xinfo` / `index_xinfo` | Falls back to `table_info` / `index_info` the first time an extended pragma is refused |
73
+ | One database, `main` | Names no schema in any statement |
74
+ | JSON results: no bytes, integers as doubles | Text and blobs are fetched as `hex()`; numbers already arrive as SQLite-rendered text |
75
+ | Whole results, no streaming | Table data is read in pages: `WHERE rowid > :last ORDER BY rowid LIMIT n`, or on the primary key of a `WITHOUT ROWID` table — an index seek per page |
76
+ | Reserved `_cf_` tables in `sqlite_schema`, unreadable | Left out, with their `sqlite_sequence` and `sqlite_stat*` rows; reported as `reserved-objects-skipped` |
77
+
78
+ ### Consistency
79
+
80
+ D1 has no read transactions a client can hold across requests, so a D1 dump is not a
81
+ snapshot: a write committed while the dump runs may be partly included. The result carries
82
+ a `snapshot-unavailable` warning. For a consistent copy, dump while nothing writes, or
83
+ create a [Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/)
84
+ bookmark first and dump a database restored from it.
85
+
86
+ ### Row order of `WITHOUT ROWID` tables
87
+
88
+ Pages follow the primary-key index, so rows come out in the order the native `.dump`
89
+ writes them. When D1 refuses `index_xinfo`, the key's direction and collation are not
90
+ known; pages then follow the declared key, ascending — the same rows, possibly in a
91
+ different order for a key declared `DESC` or with its own `COLLATE`.
92
+
93
+ ## Other engines
94
+
95
+ The features are not specific to D1. Any adapter for an engine that speaks SQLite's SQL
96
+ with similar restrictions — over HTTP, or behind an authorizer — can declare the
97
+ same `SqliteConnectionFeatures` and get the same treatment. See
98
+ [architecture.md](architecture.md) for the `SqliteConnection` contract.