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.
- package/LICENSE +674 -674
- package/README.md +247 -230
- package/dist/better-sqlite3.cjs.map +1 -1
- package/dist/better-sqlite3.d.cts +1 -1
- package/dist/better-sqlite3.d.ts +1 -1
- package/dist/better-sqlite3.js.map +1 -1
- package/dist/d1.cjs +260 -0
- package/dist/d1.cjs.map +1 -0
- package/dist/d1.d.cts +101 -0
- package/dist/d1.d.ts +101 -0
- package/dist/d1.js +229 -0
- package/dist/d1.js.map +1 -0
- package/dist/errors-CplNzuYs.d.cts +20 -0
- package/dist/errors-CplNzuYs.d.ts +20 -0
- package/dist/index.cjs +405 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -24
- package/dist/index.d.ts +58 -24
- package/dist/index.js +400 -89
- package/dist/index.js.map +1 -1
- package/dist/{types-CTyJTzB6.d.cts → types-DziV5ysl.d.cts} +67 -2
- package/dist/{types-CTyJTzB6.d.ts → types-DziV5ysl.d.ts} +67 -2
- package/docs/architecture.md +180 -157
- package/docs/better-sqlite3-adapter.md +83 -83
- package/docs/d1-adapter.md +98 -0
- package/docs/dump-api.md +253 -253
- package/docs/known-limitations.md +100 -88
- package/docs/native-compatibility.md +124 -124
- package/docs/restore-api.md +234 -234
- package/docs/round-trip-testing.md +84 -84
- package/docs/supported-data-types.md +74 -74
- package/docs/supported-objects.md +85 -85
- package/package.json +96 -89
|
@@ -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.
|