feat(db): the db module — SQLite behind a five-op spec, sim host, pocket-db reference core - #231
feat(db): the db module — SQLite behind a five-op spec, sim host, pocket-db reference core#231siwei-yuan wants to merge 3 commits into
Conversation
…ket-db reference core
The fourth module-shaped vertical slice (after ui, strike and audio),
built spec-first per the module discipline: contracts/spec/db.ts pins
five synchronous ops (open/close/exec/query/lastError), the JSON value
encoding (blobs as {"$b": base64}, integers past 2^53-1 fail loudly),
logical database names the host maps under the app's own data root, and
the resource ceilings (4 databases, 4096 result rows per query).
Statement caching is host-side, keyed by the sql string — the guest
holds no statement handles, so there is nothing to finalize and nothing
to leak. The module owns no clock and emits no events: every op
completes inside the guest's single per-tick turn, and golden-tested
apps must not depend on random()/'now'-relative SQL (the Date.now rule
applied to the dialect).
ATTACH — the one SQL statement that names a file — is refused so the
app's data root stays the sandbox boundary; load_extension stays off.
- gen-rust emits pub mod db into engine/core/src/spec.rs (drift-guarded)
- data.sqlite capability registered ahead of any stock TARGET
advertising it, the audio.pcm precedent: the sim host and the
reference core implement and test the whole contract
- @pocketjs/framework/db SDK: the bun:sqlite shape (Database, cached
Statement .get/.all/.values/.run, transaction with savepoint
nesting), throwing where the namespace is unmounted — data code that
silently drops writes is a corruption bug, not a missing enhancement
- hosts/sim/db.ts: bun:sqlite behind the op namespace, injected via
bootWorld extraGlobals; tests/db.test.ts runs the op contract, the
SDK, and an oracle comparison against bun:sqlite directly
- engine/crates/pocket-db: the reference core over rusqlite (bundled),
a real SQLite authorizer for the ATTACH refusal, Storage::Memory/Dir,
mountable as globalThis.db on any pocket-mod guest — the adoption
path a device host copies, with SQLite's own VFS as the port point
- docs/DB.md maps the boundary and the three-move adoption path
Verified: bun run test 11/11 stages green (tests/db.test.ts 17 pass;
suite 313+ pass), cargo test -p pocket-db 9/9 including a live QuickJS
guest round-trip, cargo check --workspace clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11709ad to
bbd5317
Compare
|
Pushed a second commit: ESP-IDF support in-crate, and databases as ordinary files in the app root.
On-device conformance for both data modules lives in #238's 🤖 Generated with Claude Code |
f0211a8 to
2dd2211
Compare
…he app root Two changes from the ESP32-P4 bring-up, both invisible on desktop: - cfg(target_os = "espidf") support ships in the crate: the newlib shims SQLite's syscall table references (geteuid/fchmod/fchown/utimes/readlink no-ops — honest on a filesystem with no users or symlinks — and nanosleep routed through usleep for the busy handler), plus the unix-none VFS on open (LittleFS has no fcntl locks; a module instance is its files' only writer) with the flash-friendly pragmas (journal_mode=TRUNCATE, synchronous=NORMAL, cache_size=-32). `mount` is now a default feature — default-features = false drops the pocket-mod/rquickjs dependency for firmware with its own QuickJS wiring, so an MCU build compiles only the module core plus SQLite. - Storage::Dir creates the data root on first open and maps a name to <dir>/<name>.sqlite — an ORDINARY file in the app's own home, the same root the fs module is typically bound to. The database is the app's own asset, deliberately visible and touchable like any of its files (backup = a file copy); overwriting it corrupts the app's own data, the same trust class as deleting its own files, and SQLite fails loudly on a corrupt image. The build-environment half a firmware must supply (LIBSQLITE3_FLAGS, the empty sys/ioctl.h shim, arch CFLAGS) is documented in docs/DB.md — values validated on a Waveshare ESP32-P4 over a LittleFS workspace: open 15 ms, 288-row transaction ~0.4 s, ~70–80 KB heap, data intact across reopen and power cycling. Verified: cargo test -p pocket-db 9/9, clippy clean, cargo check --no-default-features clean, cargo check --target riscv32imafc-esp-espidf (no default features) clean incl. the bundled libsqlite3.a. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2dd2211 to
8a1407a
Compare
…th the reference core Review findings on pocket-stack#231, verified by probing both hosts with the same binding/ATTACH matrix: - ATTACH <expr> AS x bypassed BOTH refusals: rusqlite maps a NULL filename (any non-literal ATTACH argument) to AuthAction::Unknown, so the authorizer's catch-all allowed it, and the sim regex only matched the DATABASE-keyword and string-literal spellings — the probe left a real file on disk from each host. The core now also sets SQLITE_LIMIT_ATTACHED=0 (rusqlite "limits" feature) so every spelling is refused at the engine level; the sim matches the word "attach" anywhere (the documented false-positive trade widens accordingly). - The sim silently accepted named parameters without the $/:/@ prefix (bun binds bare keys) where the reference core fails with "unknown parameter" — an app developed on the sim would break on device. The sim now refuses them with the core's message; the remaining leniency (a PREFIXED key the statement never names is ignored by bun, loud on the core) is documented, since bun exposes no parameter-name introspection. - The sim host's header claimed named databases persist "the way a device keeps its files", but close() dropped the data that a Storage::Dir host keeps. Closed named databases are now stashed with serialize() and restored on reopen, and the close/reopen path is pinned in the SDK test. - DB_NAME_PATTERN tightened from 64 to 57 chars so the reference mapping <name>.sqlite (+7 bytes) stays within the fs module's 64-byte segment ceiling (pocket-stack#238) — without this, a max-length database file is invisible to a co-mounted fs module, contradicting the "visible like any of its files" contract both PRs document. cargo test -p pocket-db 10/10 (new expression-ATTACH test), clippy clean, tests/db.test.ts 18/18, bunx tsc --noEmit clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doodlewind
left a comment
There was a problem hiding this comment.
Verdict: approved. The module is well-built — the spec boundary is clean, the SDK is a faithful bun:sqlite shape, and the reference core's tests are real. I probed both hosts with the same binding/ATTACH matrix rather than reading the diff, found four issues, and pushed the fixes as 6dbb0f4 on this branch.
What the probe found (and the commit fixes)
ATTACH <expr> AS xbypassed both refusals — a real sandbox escape. rusqlite maps a NULL filename (any non-literal ATTACH argument) toAuthAction::Unknown, so an authorizer matching onlyAuthAction::Attachallows it, and the sim regex only matched theDATABASE-keyword and string-literal spellings.ATTACH hex('2f746d702f78') AS osailed through both hosts and left a real file on disk in each. The core now also setsSQLITE_LIMIT_ATTACHED=0(rusqlitelimitsfeature) so every spelling is refused at the engine level — the authorizer stays for the clearernot authorizedon the literal form — and the sim matches\battach\banywhere (the documented false-positive trade widens accordingly). Both sides gained tests.- The sim was more lenient than the core on named parameters. bun binds unprefixed keys (
{a: 1}) and silently ignores unknown ones ({$zzz: 2}); the core fails both withunknown parameter. An app developed on the sim would break on device. The sim now refuses unprefixed keys with the core's message; the residual leniency (a prefixed key the statement never names is ignored by bun — no parameter-name introspection to close it) is documented in the header. - close() lost data the device host keeps. The sim header claims named databases persist "the way a device keeps its files", but
close()then reopen produced a fresh empty database whereStorage::Dirkeeps the file. Closed named databases are now stashed viaserialize()and restored on reopen (Database.deserialize(image, {safeIntegers: true})), and the close→reopen path is pinned in the SDK test. DB_NAME_PATTERNtightened 64 → 57 chars. A 64-char name +.sqlite= 71 bytes, past the fs module's 64-byte segment ceiling (#238) — such a database file is invisible to a co-mounted fs module (pocket-fs deliberately skips unaddressable names inlist()), contradicting the "visible like any of its files" contract both PRs document. 57 + 7 = exactly 64.
Consistent-and-fine, verified explicitly: missing named params bind NULL identically on both hosts (rusqlite clears cached-statement bindings, so no stale-binding leak); positional count mismatches fail loudly on both; big-integer and blob round-trips agree.
Verified
cargo test -p pocket-db10/10 (incl. the new expression-ATTACH test), clippy clean,cargo check --workspacecleantests/db.test.ts18/18; full gate green except one pre-existing flake:symbian-runtime.test.ts"E7 scan matrix" times out intermittently on macOS (Gatekeeper first-launch stall on a freshly linked temp executable — same code passes/fails across runs, file identical to main, flakes on main too). Not this PR's fault; follow-up below.bunx tsc --noEmitclean; contract byte-compare green (the name pattern isn't part of the generated surface).- The ESP-IDF claims (unix-none VFS, newlib shims,
default-features = falseforriscv32imafc-esp-espidf) are taken as hardware-verified per the ppa-smoke precedent — reproducing them locally means pulling the full ESP-IDF toolchain; the on-device evidence in #238's data-smoke is convincing.
Left alone, deliberately
- SQL
random()/'now' are unpinned in the sim (bun exposes no VFS hook) — already documented, spec already forbids golden-tested apps from relying on them. - The
nanosleepshim's 64-bittime_tassumesespidf_time64(documented in the recipe; matches the validated IDF v5.5.x config). - Follow-up (not this PR): bump the symbian-runtime test's 5s timeout or pre-warm the executable — it flakes on managed macOS hosts.
🤖 Generated with Claude Code
What
The fourth module-shaped vertical slice (after
ui,strikeandaudio), built spec-first per the RUNTIMES.md §5 discipline: SQLite mounted asglobalThis.dbbehind a five-op, append-only spec.contracts/spec/db.tspins five synchronous ops (open/close/exec/query/lastError), the JSON value encoding (blobs as{"$b": base64}, integers past 2^53−1 fail loudly instead of losing precision), logical database names the host maps under the app's own data root, and the resource ceilings (4 databases, 4096 result rows per query).random()or 'now'-relative SQL — theDate.nowrule applied to the dialect.ATTACHis refused (the one SQL statement that names a file) so the app's data root stays the sandbox boundary;load_extensionstays off.data.sqlitecapability registered ahead of any stock TARGET advertising it — theaudio.pcmprecedent: the sim host and the reference core implement and test the whole contract.@pocketjs/framework/dbSDK: the bun:sqlite shape (Database, cachedStatement.get/.all/.values/.run,transactionwith savepoint nesting), throwing where the namespace is unmounted — data code that silently drops writes is a corruption bug, not a missing enhancement.hosts/sim/db.ts: bun:sqlite behind the op namespace, injected viabootWorldextraGlobals.engine/crates/pocket-db: the reference core over rusqlite (bundled), a real SQLite authorizer for the ATTACH refusal,Storage::Memory/Dir, mountable on any pocket-mod guest — the adoption path a device host copies, with SQLite's own VFS as the port point.docs/DB.mdmaps the boundary and the three-move adoption path.Verification
bun run test: 11/11 stages green (tests/db.test.ts17 pass — op contract, SDK, and an oracle comparison running identical statements through the op namespace and bun:sqlite directly; full suite 313+ pass, all existing goldens byte-identical).cargo test -p pocket-db: 9/9, including a live QuickJS guest round-trip through pocket-mod.cargo check --workspaceclean.SQLITE_OMIT_WAL,TEMP_STORE=3,lstat=stat, six newlib shims) is exactly the port-point work docs/DB.md assigns to a device host — the spec, SDK and core needed no changes.Scope
No stock target advertises
data.sqliteyet, matching howaudio.pcmlanded before its first console adoption. Device hosts adopt by the three moves in docs/DB.md; the ESP32-P4 spike above is the first external consumer of that path.🤖 Generated with Claude Code