@di-framework/sqlite-component 0.0.1 → 6.0.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/README.md +250 -0
- package/package.json +16 -10
- package/wit/world.wit +211 -0
- package/src/index.ts +0 -2
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# di-framework:sqlite — SQLite WebAssembly component
|
|
2
|
+
|
|
3
|
+
A Rust WebAssembly component that bundles SQLite (amalgamation 3.53.2) and
|
|
4
|
+
exposes it to other components through the `di-framework:sqlite@0.1.0` WIT
|
|
5
|
+
package. Database files live on the WASI filesystem the host preopens for the
|
|
6
|
+
component (a mounted volume), using rollback journals with `synchronous=FULL`.
|
|
7
|
+
|
|
8
|
+
It exists so the QuickJS-based di-framework application component (built by
|
|
9
|
+
`componentize-qjs` in `@di-framework/cli-plugin-platform`) can persist actor
|
|
10
|
+
state, queue jobs and migration bookkeeping without a native SQLite in the JS
|
|
11
|
+
runtime: the JS component *imports* `di-framework:sqlite/database`, this
|
|
12
|
+
component *exports* it, and `wac plug` wires them into one component.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌──────────────────────────────┐ di-framework:sqlite/database ┌────────────────────────────┐
|
|
16
|
+
│ app.wasm (componentize-qjs) │ ───────── import ──────────────▶ │ di-framework-sqlite.wasm │
|
|
17
|
+
│ imports wasi:http, ... │ │ exports database, types │
|
|
18
|
+
└──────────────────────────────┘ │ imports wasi:filesystem, │
|
|
19
|
+
└────────────── wac plug ────────────────────────┤ wasi:clocks, cli │
|
|
20
|
+
│ └────────────────────────────┘
|
|
21
|
+
▼
|
|
22
|
+
composed.wasm (only wasi:* / wasmcloud:* imports remain)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Layout
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
packages/di-framework-sqlite-component/
|
|
29
|
+
├── Cargo.toml, Cargo.lock pinned crates (rusqlite 0.40.2, libsqlite3-sys 0.38.2, wit-bindgen 0.61.1, cc 1.4.5)
|
|
30
|
+
├── rust-toolchain.toml Rust 1.97.1 + wasm32-wasip2 (honoured by rustup)
|
|
31
|
+
├── .cargo/config.toml default target + C toolchain env + SQLite compile flags
|
|
32
|
+
├── build.rs compiles csrc/wasi-vfs.c with the same wasi-sdk clang
|
|
33
|
+
├── csrc/wasi-vfs.c SQLite VFS over wasi-libc (see "Persistence model")
|
|
34
|
+
├── wit/world.wit di-framework:sqlite@0.1.0
|
|
35
|
+
├── src/lib.rs the component (exports database + types)
|
|
36
|
+
├── scripts/
|
|
37
|
+
│ ├── tool-versions.env every pinned version + SHA-256
|
|
38
|
+
│ ├── install-tools.sh checksum-verified installer for wasm-tools, wac, wasi-sdk (+ optional rustup)
|
|
39
|
+
│ ├── env.sh puts .tools/ on PATH and exports build env (sourceable)
|
|
40
|
+
│ ├── build.sh cargo build + validate + stage dist/
|
|
41
|
+
│ ├── compose.sh wac plug <consumer> with the provider
|
|
42
|
+
│ └── smoke.sh end-to-end test under wasmtime
|
|
43
|
+
├── tests/smoke-consumer/ wasip2 command component importing the interface (used by smoke.sh)
|
|
44
|
+
├── Makefile thin wrapper around the scripts
|
|
45
|
+
└── dist/ (generated) di-framework-sqlite.wasm, .wit, BUILD-INFO.txt, SHA256SUMS
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Building
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
cd packages/di-framework-sqlite-component
|
|
52
|
+
|
|
53
|
+
make tools # wasm-tools 1.258.0, wac 0.11.0, wasi-sdk 34.0 -> .tools/ (SHA-256 verified)
|
|
54
|
+
make tools-rust # only if you have no rustup: hermetic rustup + Rust 1.97.1 + wasm32-wasip2 -> .tools/
|
|
55
|
+
make build # -> dist/di-framework-sqlite.wasm (~1.2 MB)
|
|
56
|
+
make smoke # compose with tests/smoke-consumer and run under wasmtime (needs `wasmtime` on PATH)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`make build` runs `scripts/build.sh`, which is:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
. scripts/env.sh # PATH, CC_wasm32_wasip2, LIBSQLITE3_FLAGS, RUSTFLAGS
|
|
63
|
+
cargo build --locked --release --target wasm32-wasip2 -p di-framework-sqlite-component
|
|
64
|
+
wasm-tools validate --features all dist/di-framework-sqlite.wasm
|
|
65
|
+
wasm-tools component wit dist/di-framework-sqlite.wasm > dist/di-framework-sqlite.wit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requirements: a C compiler that targets `wasm32-wasip2` with wasi-libc headers
|
|
69
|
+
(the pinned wasi-sdk; Apple clang has no wasm backend), Rust 1.97.1 with the
|
|
70
|
+
`wasm32-wasip2` std, `wasm-tools`. `rustc` on `wasm32-wasip2` emits a component
|
|
71
|
+
directly (via `wasm-component-ld`), so no `wasm-tools component new` step is
|
|
72
|
+
needed. Builds are deterministic: `--remap-path-prefix` strips absolute paths
|
|
73
|
+
and `dist/SHA256SUMS` is identical across clean rebuilds on the same toolchain.
|
|
74
|
+
|
|
75
|
+
### Pinning and checksums
|
|
76
|
+
|
|
77
|
+
`scripts/tool-versions.env` is the single source of truth. `install-tools.sh`
|
|
78
|
+
downloads release assets from GitHub / static.rust-lang.org, compares the
|
|
79
|
+
SHA-256 against the pinned value **before** unpacking or executing anything,
|
|
80
|
+
and aborts on mismatch. Hashes are recorded for macOS (arm64, x86_64) and Linux
|
|
81
|
+
(x86_64, aarch64). To upgrade: bump the version, download the new assets,
|
|
82
|
+
`shasum -a 256`, update the file, rebuild, run `make smoke`.
|
|
83
|
+
|
|
84
|
+
Rust itself is pinned by `rust-toolchain.toml` (rustup verifies components
|
|
85
|
+
against the signed channel manifest). Crates are pinned with `=` in
|
|
86
|
+
`Cargo.toml` and locked in `Cargo.lock`; `--locked` refuses to drift.
|
|
87
|
+
|
|
88
|
+
### Environment overrides
|
|
89
|
+
|
|
90
|
+
| Variable | Meaning |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `DF_SQLITE_TOOLS_DIR` | where `install-tools.sh` installs (default `.tools/`) |
|
|
93
|
+
| `WASI_SDK_PATH` | use an existing wasi-sdk instead of downloading |
|
|
94
|
+
| `CC_wasm32_wasip2`, `AR_wasm32_wasip2`, `CFLAGS_wasm32_wasip2` | C toolchain for `sqlite3.c` + `wasi-vfs.c` |
|
|
95
|
+
| `LIBSQLITE3_FLAGS` | extra `-D` flags for the amalgamation (see `.cargo/config.toml`) |
|
|
96
|
+
|
|
97
|
+
## WIT interface (`di-framework:sqlite@0.1.0`)
|
|
98
|
+
|
|
99
|
+
Full text in [`wit/world.wit`](wit/world.wit). Summary:
|
|
100
|
+
|
|
101
|
+
**`types`**
|
|
102
|
+
- `value`: `null | integer(s64) | real(f64) | text(string) | blob(list<u8>)`
|
|
103
|
+
- `row = list<tuple<string, value>>` — column name / value pairs, in column order
|
|
104
|
+
- `error`: `open-failed(string) | closed | invalid-sql(string) | invalid-params(string) | execution-failed(sqlite-error) | value-conversion-failed(string) | busy | invalid-transaction-state(string) | other(string)`
|
|
105
|
+
- `sqlite-error { code, extended-code, message }` — e.g. `19`/`1555` for a primary-key violation
|
|
106
|
+
- `open-options { create?, read-only?, synchronous?, journal-mode?, busy-timeout-ms?, foreign-keys? }`
|
|
107
|
+
- enums `sync-mode { off, normal, full }`, `journal-mode { delete, persist, memory }`, `transaction-behavior { deferred, immediate, exclusive }`
|
|
108
|
+
|
|
109
|
+
**`database`**
|
|
110
|
+
- `open(path, option<open-options>) -> result<connection, error>`
|
|
111
|
+
- `sqlite-version() -> string`
|
|
112
|
+
- `resource connection`
|
|
113
|
+
- `exec(sql)` — multi-statement batch, no params, no results (DDL / migrations / pragmas)
|
|
114
|
+
- `run(sql, params) -> u64` — one statement, returns `changes`
|
|
115
|
+
- `query(sql, params) -> list<row>`
|
|
116
|
+
- `first(sql, params) -> option<row>`
|
|
117
|
+
- `begin(option<transaction-behavior>)` (default `immediate`), `commit()`, `rollback()`
|
|
118
|
+
- `savepoint(name)`, `release-savepoint(name)`, `rollback-to-savepoint(name)` — nesting
|
|
119
|
+
- `in-transaction() -> bool`, `changes() -> u64`, `last-insert-rowid() -> s64`
|
|
120
|
+
- `close()`
|
|
121
|
+
|
|
122
|
+
**Worlds**: `sqlite-provider { export types; export database; }` (this component)
|
|
123
|
+
and `imports { import types; import database; }` (consumers).
|
|
124
|
+
|
|
125
|
+
Defaults applied by `open` when options are omitted: `journal_mode=DELETE`,
|
|
126
|
+
`synchronous=FULL`, `foreign_keys=ON`, `busy_timeout=5000`. `open` verifies the
|
|
127
|
+
journal mode SQLite reports matches the request and fails otherwise.
|
|
128
|
+
|
|
129
|
+
## Persistence model
|
|
130
|
+
|
|
131
|
+
- **Rollback journal, not WAL.** WAL requires shared memory and file locks; the
|
|
132
|
+
WASI filesystem offers neither. `sqlite3.c` is compiled with
|
|
133
|
+
`-DSQLITE_OMIT_WAL`, so `PRAGMA journal_mode=WAL` is a silent no-op and the
|
|
134
|
+
connection stays on `delete`. Offered modes: `delete` (default), `persist`,
|
|
135
|
+
`memory` (tests only).
|
|
136
|
+
- **`synchronous=FULL` by default** (`-DSQLITE_DEFAULT_SYNCHRONOUS=2` and set
|
|
137
|
+
again at open). Every commit fsyncs the journal and the database through
|
|
138
|
+
`wasi:filesystem/types.descriptor.sync`. `normal` is available via
|
|
139
|
+
`open-options` for lower-durability workloads.
|
|
140
|
+
- **Single writer.** The VFS does no locking. One connection per file inside
|
|
141
|
+
the component, and one component instance per file on the host — the
|
|
142
|
+
wasmCloud plugin enforces `replicas: 1` for actor workloads
|
|
143
|
+
(`WASMCLOUD_ACTORS_REPLICA_CONSTRAINT`).
|
|
144
|
+
- **Paths are guest paths inside a preopen.** The Kubernetes manifest mounts a
|
|
145
|
+
PVC at `mountPath` (default `/data/actors`) and exposes the same path to the
|
|
146
|
+
guest; open `/data/actors/<db>.sqlite`. Paths outside any preopen fail with
|
|
147
|
+
`open-failed`.
|
|
148
|
+
- **No temp files.** `-DSQLITE_TEMP_STORE=3` and `-DSQLITE_STMTJRNL_SPILL=-1`
|
|
149
|
+
keep temp tables, sorters and statement journals in memory.
|
|
150
|
+
|
|
151
|
+
### The VFS (`csrc/wasi-vfs.c`)
|
|
152
|
+
|
|
153
|
+
`libsqlite3-sys` ships a `wasm32-wasi-vfs` feature whose C file is a 2010-era
|
|
154
|
+
copy of SQLite's `demovfs.c`. It is **not** used here because:
|
|
155
|
+
|
|
156
|
+
1. its `xFileControl` returns `SQLITE_OK` for every opcode; since SQLite 3.7.x
|
|
157
|
+
`PRAGMA` first asks the VFS via `SQLITE_FCNTL_PRAGMA`, and an `OK` answer
|
|
158
|
+
means "handled" — so *every PRAGMA becomes a no-op* (`journal_mode`,
|
|
159
|
+
`synchronous`, `user_version`, ...);
|
|
160
|
+
2. `xTruncate` is a no-op (unsafe for any journal mode but DELETE; VACUUM never
|
|
161
|
+
shrinks the file);
|
|
162
|
+
3. `xRandomness` returns no entropy; `xDelete` scans past the end of its buffer.
|
|
163
|
+
|
|
164
|
+
`csrc/wasi-vfs.c` is the same public-domain design with those fixed
|
|
165
|
+
(`SQLITE_NOTFOUND`, `ftruncate`, `getentropy`, `pread`/`pwrite`, ms-precision
|
|
166
|
+
`xCurrentTimeInt64`) and is compiled by `build.rs` against
|
|
167
|
+
`libsqlite3-sys`'s exported `sqlite3.h`. `sqlite3.c` is built with
|
|
168
|
+
`-DSQLITE_OS_OTHER=1` so it calls our `sqlite3_os_init`. `make smoke` asserts
|
|
169
|
+
that `PRAGMA journal_mode` really returns `delete` and that no `-wal` or stale
|
|
170
|
+
`-journal` file is left behind.
|
|
171
|
+
|
|
172
|
+
## Consuming from the JS component
|
|
173
|
+
|
|
174
|
+
1. **WIT**: copy `wit/world.wit` to
|
|
175
|
+
`cli-extensions/packages/di-framework-cli-plugin-platform/assets/wit/deps/di-framework-sqlite/package.wit`
|
|
176
|
+
and add `import di-framework:sqlite/database@0.1.0;` to the generated guest
|
|
177
|
+
world (`sqliteProjectRequirements()` in `src/wit.ts` does this).
|
|
178
|
+
2. **JS**: `componentize-qjs`/jco expose the import as an ES module:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { open } from 'di-framework:sqlite/database@0.1.0';
|
|
182
|
+
|
|
183
|
+
const db = open('/data/actors/orders.sqlite', undefined); // options optional
|
|
184
|
+
db.exec('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL)');
|
|
185
|
+
db.run('INSERT OR REPLACE INTO kv VALUES (?1, ?2)', [
|
|
186
|
+
{ tag: 'text', val: 'a' },
|
|
187
|
+
{ tag: 'text', val: JSON.stringify({ n: 1 }) },
|
|
188
|
+
]);
|
|
189
|
+
const row = db.first('SELECT v FROM kv WHERE k = ?1', [{ tag: 'text', val: 'a' }]);
|
|
190
|
+
// row: [['v', { tag: 'text', val: '{"n":1}' }]] | undefined
|
|
191
|
+
|
|
192
|
+
db.begin(undefined); // BEGIN IMMEDIATE
|
|
193
|
+
try { /* ... */ db.commit(); } catch (e) { db.rollback(); throw e; }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`result<_, error>` failures surface as thrown `ComponentError`s whose
|
|
197
|
+
`payload` is the `error` variant (`{ tag: 'execution-failed', val: { code, extendedCode, message } }`).
|
|
198
|
+
`integer` values are `bigint`s. A `MigrationDatabase.transaction(fn)` wrapper
|
|
199
|
+
maps to `begin`/`commit`/`rollback` (or `savepoint` when already inside one).
|
|
200
|
+
3. **Compose** after componentize:
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
scripts/compose.sh path/to/app.wasm path/to/app.composed.wasm # = wac plug --plug dist/di-framework-sqlite.wasm app.wasm -o ...
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`wac plug` connects every export of the provider to the matching import of
|
|
207
|
+
the app; the composed component keeps only `wasi:*`/`wasmcloud:*` imports
|
|
208
|
+
for the host. `compose.sh` fails if `di-framework:sqlite/database` is still
|
|
209
|
+
imported afterwards.
|
|
210
|
+
4. **Ship the artifact**: the plugin package build runs `make publish-asset`,
|
|
211
|
+
which builds the provider from source and copies the WASM, recovered WIT,
|
|
212
|
+
build metadata, and checksums into
|
|
213
|
+
`cli-extensions/packages/di-framework-cli-plugin-platform/dist/assets/sqlite/`. These generated
|
|
214
|
+
files ship in the npm package and are ignored by Git. No prebuilt provider is
|
|
215
|
+
checked into the source tree.
|
|
216
|
+
|
|
217
|
+
### WASI version note
|
|
218
|
+
|
|
219
|
+
This component targets `wasm32-wasip2` (imports `wasi:*@0.2.9`), the only
|
|
220
|
+
stable Rust component target. The plugin's JS component targets WASI 0.3.0.
|
|
221
|
+
`wac plug` composes them fine — the composed component then imports both
|
|
222
|
+
`wasi:filesystem@0.2.9` (for SQLite) and the 0.3.0 interfaces (for the app) —
|
|
223
|
+
but the *host* must provide both. wasmtime does (p2 and p3 linkers coexist);
|
|
224
|
+
verify this on the wasmCloud host version you deploy to. When a stable
|
|
225
|
+
`wasm32-wasip3` Rust target lands, changing `RUST_TARGET` in
|
|
226
|
+
`scripts/tool-versions.env` and `.cargo/config.toml` is the only change needed
|
|
227
|
+
here.
|
|
228
|
+
|
|
229
|
+
## Verification
|
|
230
|
+
|
|
231
|
+
`make smoke` builds `tests/smoke-consumer` (a `wasip2` command component that
|
|
232
|
+
imports the interface), composes it with the provider using the pinned `wac`,
|
|
233
|
+
and runs it under `wasmtime run --dir <tmp>::/data`. It checks: pragma
|
|
234
|
+
defaults (`delete`/FULL/foreign keys), WAL is compiled out, DDL via `exec`,
|
|
235
|
+
all `value` kinds round-trip, `changes`/`last-insert-rowid`, error mapping
|
|
236
|
+
(constraint code 19/1555, syntax → `invalid-sql`, arity → `invalid-params`,
|
|
237
|
+
multi-statement → `invalid-sql`, `commit` without `begin` →
|
|
238
|
+
`invalid-transaction-state`), transactions, savepoints and rollback, `close`,
|
|
239
|
+
reopening read-only (`SQLITE_READONLY` = 8), `open-failed` for missing
|
|
240
|
+
directories and paths outside the preopen, `:memory:`, and finally that the
|
|
241
|
+
`.sqlite` file exists on the host with no `-journal`/`-wal` left over.
|
|
242
|
+
|
|
243
|
+
## Known limitations / follow-ups
|
|
244
|
+
|
|
245
|
+
- Synchronous interface (no `async func`/streams); fine for the small result
|
|
246
|
+
sets the framework issues, and matches `bun:sqlite`-style drivers.
|
|
247
|
+
- `row` repeats column names per row; switch to a `{columns, rows}` record if
|
|
248
|
+
large result sets ever matter.
|
|
249
|
+
- No prepared-statement cache across calls (each `run`/`query` prepares once).
|
|
250
|
+
- `wasmtime` used by `make smoke` is not pinned by this package.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@di-framework/sqlite-component",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "6.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "SQLite WebAssembly component (di-framework:sqlite@0.1.0) for platform guests.",
|
|
6
6
|
"license": "(MIT OR Apache-2.0)",
|
|
7
7
|
"author": "@di-framework Contributors",
|
|
8
8
|
"repository": {
|
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
"url": "https://github.com/di-framework/platform",
|
|
11
11
|
"directory": "platform/sqlite-component"
|
|
12
12
|
},
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"exports": {
|
|
16
|
-
".": "./src/index.ts"
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/di-framework/platform/issues"
|
|
17
15
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
16
|
+
"homepage": "https://github.com/di-framework/platform",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"wit"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "make build",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"smoke": "make smoke",
|
|
26
|
+
"publish-asset": "make publish-asset"
|
|
21
27
|
}
|
|
22
28
|
}
|
package/wit/world.wit
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
package di-framework:sqlite@0.1.0;
|
|
2
|
+
|
|
3
|
+
/// Types shared by the `database` interface.
|
|
4
|
+
///
|
|
5
|
+
/// Kept in a separate interface (mirroring `wasmcloud:postgres/types`) so that
|
|
6
|
+
/// values and errors unify when a consumer and a provider are composed with
|
|
7
|
+
/// WAC, and so a future `prepared` or `async` interface can reuse them.
|
|
8
|
+
interface types {
|
|
9
|
+
/// A structured error reported by SQLite itself.
|
|
10
|
+
record sqlite-error {
|
|
11
|
+
/// Primary SQLite result code (e.g. `19` = SQLITE_CONSTRAINT).
|
|
12
|
+
code: s32,
|
|
13
|
+
/// Extended result code (e.g. `2067` = SQLITE_CONSTRAINT_UNIQUE).
|
|
14
|
+
/// Equal to `code` when SQLite provides no extended information.
|
|
15
|
+
extended-code: s32,
|
|
16
|
+
/// Human-readable message from `sqlite3_errmsg`.
|
|
17
|
+
message: string,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Errors raised by functions in this package. Intentionally non-exhaustive:
|
|
21
|
+
/// consumers must keep a catch-all arm.
|
|
22
|
+
variant error {
|
|
23
|
+
/// The database file could not be opened or created. The guest path must
|
|
24
|
+
/// live under a directory the host has preopened (a mounted volume).
|
|
25
|
+
open-failed(string),
|
|
26
|
+
/// The connection has been closed with `close` and cannot be used again.
|
|
27
|
+
closed,
|
|
28
|
+
/// The statement could not be prepared: syntax error, unknown table or
|
|
29
|
+
/// column, or more than one statement passed to `run`/`query`/`first`.
|
|
30
|
+
invalid-sql(string),
|
|
31
|
+
/// Wrong number of parameters or a value that could not be bound.
|
|
32
|
+
invalid-params(string),
|
|
33
|
+
/// SQLite failed while executing a prepared statement (constraint
|
|
34
|
+
/// violation, read-only database, I/O error, ...). Branch on
|
|
35
|
+
/// `sqlite-error.code` / `extended-code` for machine-readable handling.
|
|
36
|
+
execution-failed(sqlite-error),
|
|
37
|
+
/// A column value could not be converted into a `value` (e.g. TEXT that is
|
|
38
|
+
/// not valid UTF-8).
|
|
39
|
+
value-conversion-failed(string),
|
|
40
|
+
/// The database file is locked or busy after the configured busy timeout.
|
|
41
|
+
/// With the WASI VFS this can only arise from another connection *inside
|
|
42
|
+
/// the same component instance*; the VFS does not perform file locking.
|
|
43
|
+
busy,
|
|
44
|
+
/// `begin`, `commit`, `rollback` (or a savepoint call) was issued in a
|
|
45
|
+
/// state that does not permit it (e.g. `commit` with no open transaction).
|
|
46
|
+
invalid-transaction-state(string),
|
|
47
|
+
/// Any other implementation-specific failure.
|
|
48
|
+
other(string),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A dynamically-typed SQLite value, used for parameters and results.
|
|
52
|
+
variant value {
|
|
53
|
+
null,
|
|
54
|
+
integer(s64),
|
|
55
|
+
real(f64),
|
|
56
|
+
text(string),
|
|
57
|
+
blob(list<u8>),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// A result row: `(column-name, value)` pairs in column order. Column names
|
|
61
|
+
/// are repeated per row on purpose: result sets in the framework (actor
|
|
62
|
+
/// state, queue jobs, migration bookkeeping) are small and JS consumers turn
|
|
63
|
+
/// each row straight into an object.
|
|
64
|
+
type row = list<tuple<string, value>>;
|
|
65
|
+
|
|
66
|
+
/// `PRAGMA synchronous` level. `full` is the default and is what the
|
|
67
|
+
/// framework expects for durable actor / queue state; `normal` trades a
|
|
68
|
+
/// small durability window (power loss mid-commit) for fewer fsyncs. `off`
|
|
69
|
+
/// exists for tests only.
|
|
70
|
+
enum sync-mode {
|
|
71
|
+
off,
|
|
72
|
+
normal,
|
|
73
|
+
full,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Rollback-journal mode. WAL is intentionally not offered: the WASI VFS
|
|
77
|
+
/// has neither file locking nor shared memory, both of which WAL needs, and
|
|
78
|
+
/// SQLite is compiled with `SQLITE_OMIT_WAL`. `delete` is the default and
|
|
79
|
+
/// the safest choice on WASI preopens (the journal is unlinked after every
|
|
80
|
+
/// commit); `persist` keeps the journal file and zeroes its header; `memory`
|
|
81
|
+
/// is for tests only. TRUNCATE is deliberately left out of the surface.
|
|
82
|
+
enum journal-mode {
|
|
83
|
+
delete,
|
|
84
|
+
persist,
|
|
85
|
+
memory,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// How `begin` acquires its lock. Maps to `BEGIN DEFERRED|IMMEDIATE|EXCLUSIVE`.
|
|
89
|
+
enum transaction-behavior {
|
|
90
|
+
deferred,
|
|
91
|
+
immediate,
|
|
92
|
+
exclusive,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Options accepted by `open`. Every field is optional; `none` means the
|
|
96
|
+
/// documented default.
|
|
97
|
+
record open-options {
|
|
98
|
+
/// Create the database file if it does not exist. Default: true.
|
|
99
|
+
create: option<bool>,
|
|
100
|
+
/// Open read-only. Default: false.
|
|
101
|
+
read-only: option<bool>,
|
|
102
|
+
/// `PRAGMA synchronous`. Default: `full`.
|
|
103
|
+
synchronous: option<sync-mode>,
|
|
104
|
+
/// `PRAGMA journal_mode`. Default: `delete`.
|
|
105
|
+
journal-mode: option<journal-mode>,
|
|
106
|
+
/// `PRAGMA busy_timeout` in milliseconds. Default: 5000.
|
|
107
|
+
busy-timeout-ms: option<u32>,
|
|
108
|
+
/// `PRAGMA foreign_keys`. Default: true.
|
|
109
|
+
foreign-keys: option<bool>,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Synchronous, single-writer SQLite access backed by the WASI filesystem.
|
|
114
|
+
///
|
|
115
|
+
/// The implementer of this interface is the `di-framework:sqlite` Rust
|
|
116
|
+
/// component. It bundles SQLite (amalgamation) with a minimal WASI VFS: no
|
|
117
|
+
/// file locking, no mmap, no WAL, in-memory temp store. Consequently exactly
|
|
118
|
+
/// one component instance may own a given database file at a time; the
|
|
119
|
+
/// framework enforces `replicas: 1` for actor workloads for this reason.
|
|
120
|
+
///
|
|
121
|
+
/// Paths are guest paths. The host mounts a volume at `mountPath` and the same
|
|
122
|
+
/// string is the guest-visible directory (a WASI preopen), e.g.
|
|
123
|
+
/// `/data/actors/orders/order-42.sqlite`.
|
|
124
|
+
interface database {
|
|
125
|
+
use types.{error, value, row, open-options, transaction-behavior};
|
|
126
|
+
|
|
127
|
+
/// Open (and by default create) the database at `path`, applying the
|
|
128
|
+
/// pragmas described in `open-options` (rollback journal, `synchronous=FULL`,
|
|
129
|
+
/// `foreign_keys=ON`, 5 s busy timeout when `options` is `none`). Use
|
|
130
|
+
/// `:memory:` for an in-memory database.
|
|
131
|
+
open: func(path: string, options: option<open-options>) -> result<connection, error>;
|
|
132
|
+
|
|
133
|
+
/// The bundled SQLite library version, e.g. `3.53.2`.
|
|
134
|
+
sqlite-version: func() -> string;
|
|
135
|
+
|
|
136
|
+
/// An open SQLite connection. Dropping the resource closes it; an open
|
|
137
|
+
/// transaction is rolled back by SQLite in that case.
|
|
138
|
+
resource connection {
|
|
139
|
+
/// Execute one or more `;`-separated statements without parameters and
|
|
140
|
+
/// discard any results. Intended for DDL / migrations / pragmas. Untrusted
|
|
141
|
+
/// input must never be interpolated into `sql`.
|
|
142
|
+
exec: func(sql: string) -> result<_, error>;
|
|
143
|
+
|
|
144
|
+
/// Execute a single parameterized statement and return the number of rows
|
|
145
|
+
/// changed (`sqlite3_changes`). Parameters bind positionally to `?`/`?NNN`.
|
|
146
|
+
/// Statements that return rows (e.g. `INSERT ... RETURNING`) run to
|
|
147
|
+
/// completion; their rows are discarded.
|
|
148
|
+
run: func(sql: string, params: list<value>) -> result<u64, error>;
|
|
149
|
+
|
|
150
|
+
/// Execute a single parameterized statement and return all rows.
|
|
151
|
+
query: func(sql: string, params: list<value>) -> result<list<row>, error>;
|
|
152
|
+
|
|
153
|
+
/// Execute a single parameterized statement and return only the first row,
|
|
154
|
+
/// or `none` when the statement produced no rows.
|
|
155
|
+
first: func(sql: string, params: list<value>) -> result<option<row>, error>;
|
|
156
|
+
|
|
157
|
+
/// `BEGIN [DEFERRED|IMMEDIATE|EXCLUSIVE]`. Default: `immediate`, which is
|
|
158
|
+
/// the right choice for a single-writer file with no lock arbitration.
|
|
159
|
+
/// Fails with `invalid-transaction-state` if a transaction is already open;
|
|
160
|
+
/// nest with `savepoint` instead.
|
|
161
|
+
begin: func(behavior: option<transaction-behavior>) -> result<_, error>;
|
|
162
|
+
|
|
163
|
+
/// `COMMIT` the open transaction.
|
|
164
|
+
commit: func() -> result<_, error>;
|
|
165
|
+
|
|
166
|
+
/// `ROLLBACK` the open transaction.
|
|
167
|
+
rollback: func() -> result<_, error>;
|
|
168
|
+
|
|
169
|
+
/// `SAVEPOINT <name>`; usable for nested transactions. `name` must match
|
|
170
|
+
/// `[A-Za-z_][A-Za-z0-9_]*`.
|
|
171
|
+
savepoint: func(name: string) -> result<_, error>;
|
|
172
|
+
|
|
173
|
+
/// `RELEASE SAVEPOINT <name>`.
|
|
174
|
+
release-savepoint: func(name: string) -> result<_, error>;
|
|
175
|
+
|
|
176
|
+
/// `ROLLBACK TO SAVEPOINT <name>` followed by `RELEASE`, so the savepoint
|
|
177
|
+
/// is fully unwound.
|
|
178
|
+
rollback-to-savepoint: func(name: string) -> result<_, error>;
|
|
179
|
+
|
|
180
|
+
/// True while a transaction (or savepoint) is open on this connection.
|
|
181
|
+
in-transaction: func() -> bool;
|
|
182
|
+
|
|
183
|
+
/// Rows changed by the most recent `run`/`exec`.
|
|
184
|
+
changes: func() -> u64;
|
|
185
|
+
|
|
186
|
+
/// `sqlite3_last_insert_rowid`.
|
|
187
|
+
last-insert-rowid: func() -> s64;
|
|
188
|
+
|
|
189
|
+
/// Explicitly close the connection. Subsequent calls fail with `closed`.
|
|
190
|
+
close: func() -> result<_, error>;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// The world implemented by the Rust component. WASI imports (filesystem,
|
|
195
|
+
/// clocks, cli, io) are pulled in by the Rust `wasm32-wasip2` target and
|
|
196
|
+
/// therefore do not need to be listed here. `types` is exported explicitly so
|
|
197
|
+
/// the component is self-contained: without it WIT elaboration would turn the
|
|
198
|
+
/// `use types.{...}` into an *import* of `types`, which `wac plug` could not
|
|
199
|
+
/// satisfy from this component.
|
|
200
|
+
world sqlite-provider {
|
|
201
|
+
export types;
|
|
202
|
+
export database;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// The world a consumer (e.g. the componentize-qjs JS component) targets when
|
|
206
|
+
/// it wants a SQLite database. `wac plug` satisfies these imports with the
|
|
207
|
+
/// `sqlite-provider` component's exports.
|
|
208
|
+
world imports {
|
|
209
|
+
import types;
|
|
210
|
+
import database;
|
|
211
|
+
}
|
package/src/index.ts
DELETED