picovolt 1.3.0 → 1.5.0
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 +46 -20
- package/browser.js +40 -0
- package/package.json +7 -3
- package/picovolt_bg.js +15 -15
- package/picovolt_bg.wasm +0 -0
- package/sqlite.js +27 -4
- package/worker.js +37 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# PicoVolt (PVDB)
|
|
2
2
|
|
|
3
3
|
[](https://github.com/MiniJe/picovolt/actions/workflows/ci.yml)
|
|
4
|
-
[](CHANGELOG.md)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
7
|
[](https://github.com/MiniJe/picovolt)
|
|
@@ -28,7 +28,7 @@ cache efficiency.
|
|
|
28
28
|
|
|
29
29
|
## Status
|
|
30
30
|
|
|
31
|
-
The engine is built out across four phases, all implemented, with
|
|
31
|
+
The engine is built out across four phases, all implemented, with over 180 unit and
|
|
32
32
|
integration tests plus doctests passing and a clean `cargo clippy -D warnings` on
|
|
33
33
|
Linux and Windows. Changes are tracked in [CHANGELOG.md](CHANGELOG.md).
|
|
34
34
|
|
|
@@ -52,11 +52,11 @@ Linux and Windows. Changes are tracked in [CHANGELOG.md](CHANGELOG.md).
|
|
|
52
52
|
| [`storage/compress.rs`](src/storage/compress.rs) | Delta-Z, LEB128 varints, dictionary bit-packing |
|
|
53
53
|
| [`storage/index.rs`](src/storage/index.rs) | in-memory ordered secondary index (value to record addresses; point and range) |
|
|
54
54
|
| [`storage/record.rs`](src/storage/record.rs) | row and record-body serialization with CAS interception |
|
|
55
|
-
| [`storage/vle.rs`](src/storage/vle.rs) | dev directory store, prod
|
|
55
|
+
| [`storage/vle.rs`](src/storage/vle.rs) | dev directory store, owned prod snapshot, streamed reads, `bake` |
|
|
56
56
|
| [`engine/mvcc.rs`](src/engine/mvcc.rs) | transaction clock and snapshot visibility |
|
|
57
57
|
| [`engine/wasm.rs`](src/engine/wasm.rs) | sandboxed `wasmi` extension runtime and the `WasmExec` backend trait |
|
|
58
58
|
| [`engine/interp.rs`](src/engine/interp.rs) | `pv-wasm`: a from-scratch WASM interpreter (integer subset) |
|
|
59
|
-
| [`engine/query.rs`](src/engine/query.rs) | SQL front-end (CREATE/INSERT/UPDATE/DELETE/DROP, `SELECT` with projection, `AS` aliases, `DISTINCT`, aggregates, `GROUP BY`/`HAVING`, `WHERE` predicates incl. `IN`/`BETWEEN`/`IS NULL`/`LIKE`, `BEFORE`, multi-column `ORDER BY`, `LIMIT`) |
|
|
59
|
+
| [`engine/query.rs`](src/engine/query.rs) | SQL front-end (CREATE/INSERT/UPDATE/DELETE/DROP, `SELECT` with projection, `AS` aliases, `DISTINCT`, aggregates, `GROUP BY`/`HAVING`, `WHERE` predicates incl. `IN`/`BETWEEN`/`IS NULL`/`LIKE`, `BEFORE`, multi-column `ORDER BY`, `LIMIT`/`OFFSET`) |
|
|
60
60
|
| [`engine/compliance.rs`](src/engine/compliance.rs) | optional, app-driven usage-policy hook (not a license requirement) |
|
|
61
61
|
| [`db.rs`](src/db.rs) | the `Database` surface that ties it together |
|
|
62
62
|
| [`ffi.rs`](src/ffi.rs) | C ABI (the `capi` feature): a panic-safe, C-callable surface wrapping the engine for Go, Python, and C bindings |
|
|
@@ -89,12 +89,15 @@ Linux and Windows. Changes are tracked in [CHANGELOG.md](CHANGELOG.md).
|
|
|
89
89
|
- **Hardened against untrusted input.** Opening a `.pvdb` or workspace, or running
|
|
90
90
|
a WASM module, validates manifest hashes (no path traversal), bounds-checks CAS
|
|
91
91
|
offsets and page chains (no out-of-bounds reads or infinite loops on a crafted
|
|
92
|
-
file), and
|
|
92
|
+
file), and meters WASM instructions, memory, and output. The decoders are fuzzed (a cross-platform
|
|
93
93
|
fuzz-lite test and a [`fuzz/`](fuzz) cargo-fuzz crate), and `cargo audit`
|
|
94
|
-
reports no
|
|
94
|
+
currently reports no vulnerability failures. Both run in CI. See
|
|
95
|
+
[SECURITY.md](SECURITY.md).
|
|
95
96
|
|
|
96
97
|
## Build
|
|
97
98
|
|
|
99
|
+
Rust 1.86 or newer is required.
|
|
100
|
+
|
|
98
101
|
```sh
|
|
99
102
|
cargo build
|
|
100
103
|
cargo test
|
|
@@ -109,14 +112,24 @@ cargo run --release --example repl # interactive SQL shell (pvsql)
|
|
|
109
112
|
cargo run --release --example bench # evaluation harness across modes and workloads
|
|
110
113
|
```
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
`
|
|
115
|
+
Install the first-class CLI with `cargo install picovolt`, then use `pv query`,
|
|
116
|
+
`pv inspect`, `pv history`, `pv import`, `pv export`, and `pv bake`. Copyable
|
|
117
|
+
Rust, Python, Go, Node, and browser projects are in [`starters/`](starters/README.md); supported adapters
|
|
118
|
+
are catalogued in [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md).
|
|
119
|
+
|
|
120
|
+
SQL supported: `CREATE TABLE [IF NOT EXISTS]` with `PRIMARY KEY`, `UNIQUE`, and `NOT NULL`,
|
|
121
|
+
`CREATE [UNIQUE] INDEX ON t (col)`, single- and multi-row `INSERT`,
|
|
122
|
+
`UPDATE ... SET ... WHERE`, `DELETE ... WHERE`, `DROP TABLE [IF EXISTS]`, and
|
|
114
123
|
`SELECT [DISTINCT] {* | col [AS alias], ... | COUNT/SUM/MIN/MAX/AVG(...) [AS alias]}
|
|
115
124
|
FROM t [WHERE <pred>] [GROUP BY cols] [HAVING <pred>] [BEFORE tx]
|
|
116
|
-
[ORDER BY col [ASC|DESC], ...] [LIMIT n]`, where `<pred>` combines
|
|
125
|
+
[ORDER BY col [ASC|DESC], ...] [LIMIT n] [OFFSET n]`, where `<pred>` combines
|
|
117
126
|
`col <op> value` (`=`, `!=`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`),
|
|
118
127
|
`col [NOT] IN (...)`, `col [NOT] BETWEEN a AND b`, and `col IS [NOT] NULL` with
|
|
119
128
|
`AND`, `OR`, and parentheses. Integer and decimal values compare by magnitude.
|
|
129
|
+
Two-table equality `INNER`/`LEFT JOIN` queries support qualified references,
|
|
130
|
+
projection, aliases, `DISTINCT`, filters, ordering, and pagination. Rust callers
|
|
131
|
+
can cache `Database::prepare(...)` templates and use
|
|
132
|
+
atomic `Database::transaction(...)` closures with in-memory databases.
|
|
120
133
|
Durability is selectable via `Database::set_durability` (`Fast` OS-cache default,
|
|
121
134
|
or crash-safe `Sync` with fsync and an atomic manifest).
|
|
122
135
|
|
|
@@ -126,9 +139,9 @@ around 33k rows/s, linear), larger-than-RAM reads through a bounded buffer pool
|
|
|
126
139
|
667-page dataset serves from a 16-page pool), ordered secondary indexes (point
|
|
127
140
|
lookups roughly 11,000 times faster than a scan, plus range predicates), MVCC
|
|
128
141
|
time-travel, opt-in crash-safe durability (`Durability::Sync`), and a fast
|
|
129
|
-
compile-and-publish path (CAS dedup, columnar compression,
|
|
130
|
-
artifacts). Current limits
|
|
131
|
-
no
|
|
142
|
+
compile-and-publish path (CAS dedup, columnar compression, memory-mappable
|
|
143
|
+
single-file artifacts). Current limits include no filesystem `BEGIN`/`COMMIT`
|
|
144
|
+
transactions, only basic two-table equality joins, and no concurrent writers.
|
|
132
145
|
|
|
133
146
|
## Install and distribution
|
|
134
147
|
|
|
@@ -136,10 +149,12 @@ no concurrency.
|
|
|
136
149
|
|--------|-----|
|
|
137
150
|
| **Rust** (crates.io) | `cargo add picovolt` |
|
|
138
151
|
| **JavaScript / npm** (WebAssembly, browser and Node) | `npm install picovolt` |
|
|
139
|
-
| **
|
|
152
|
+
| **Python** (native wheels) | `python -m pip install picovolt` (after the first PyPI release) |
|
|
153
|
+
| **C / Go** (native, via the C ABI) | `cargo build --release --features capi`, then see [`bindings/`](bindings) |
|
|
140
154
|
| **In-memory** (native, no filesystem) | `Database::open_memory()`, export with `bake_to_bytes()` |
|
|
141
155
|
|
|
142
|
-
PicoVolt runs in the browser through its in-memory backend
|
|
156
|
+
PicoVolt runs in the browser through its in-memory backend plus an OPFS persistence
|
|
157
|
+
wrapper and Web Worker endpoint. Build the WebAssembly
|
|
143
158
|
package with `wasm-pack build --target bundler --release -- --features wasm`, then
|
|
144
159
|
`import { Db } from "picovolt"` and run SQL with `db.query(...)`. See
|
|
145
160
|
[src/wasm_api.rs](src/wasm_api.rs) for the JavaScript surface.
|
|
@@ -155,9 +170,9 @@ All bindings accept positional `?` parameters
|
|
|
155
170
|
a familiar surface, drop-in adapters are provided: a `better-sqlite3`-style
|
|
156
171
|
JavaScript API (`import Database from "picovolt/sqlite"`), a Python DB-API 2.0
|
|
157
172
|
module (`import picovolt.dbapi2 as sqlite`), and the Go `database/sql` driver
|
|
158
|
-
([`bindings/go/pvsql`](bindings/go/pvsql)). Shared limits
|
|
159
|
-
|
|
160
|
-
|
|
173
|
+
([`bindings/go/pvsql`](bindings/go/pvsql)). Shared limits include positional `?`
|
|
174
|
+
only and the intentionally compact SQL grammar; JavaScript and in-memory Rust
|
|
175
|
+
also expose rollback-capable transaction wrappers.
|
|
161
176
|
|
|
162
177
|
## Server mode
|
|
163
178
|
|
|
@@ -169,13 +184,22 @@ thread over a channel, so the single-threaded core is unchanged.
|
|
|
169
184
|
```sh
|
|
170
185
|
cargo build --release --features server
|
|
171
186
|
./target/release/picovolt-server --memory --addr 127.0.0.1:8080
|
|
172
|
-
curl -s localhost:8080/v1/query
|
|
187
|
+
curl -s localhost:8080/v1/query \
|
|
188
|
+
-H 'Content-Type: application/json' \
|
|
189
|
+
-d '{"sql":"CREATE TABLE demo (value)","params":[]}'
|
|
173
190
|
```
|
|
174
191
|
|
|
175
|
-
Endpoints are `POST /v1/query`, `GET /v1/tx`, and `GET /v1/health`.
|
|
176
|
-
authentication
|
|
192
|
+
Endpoints are `POST /v1/query`, `GET /v1/tx`, and `GET /v1/health`. Loopback use
|
|
193
|
+
may omit authentication. A non-loopback bind is refused unless a bearer token is
|
|
194
|
+
provided with `--token-file` or `PICOVOLT_SERVER_TOKEN`; send it as
|
|
195
|
+
`Authorization: Bearer ...`. Query bodies, queues, execution time, rows scanned,
|
|
196
|
+
result rows, and response size are bounded. TLS is not built in, so network
|
|
197
|
+
deployments still belong behind a TLS-terminating reverse proxy. See
|
|
177
198
|
[src/bin/server.rs](src/bin/server.rs).
|
|
178
199
|
|
|
200
|
+
Applications accepting SQL from users can also call `Database::query_with_limits`
|
|
201
|
+
directly and choose their own scan, result, memory, and deadline budgets.
|
|
202
|
+
|
|
179
203
|
## Extending PicoVolt
|
|
180
204
|
|
|
181
205
|
There are two extension paths: sandboxed WebAssembly user-defined functions, and
|
|
@@ -187,6 +211,8 @@ native modules built on the public API. Both are documented in
|
|
|
187
211
|
| | |
|
|
188
212
|
|--|--|
|
|
189
213
|
| Roadmap | [ROADMAP.md](ROADMAP.md) |
|
|
214
|
+
| One-million-download plan | [docs/ROADMAP_1M_DOWNLOADS.md](docs/ROADMAP_1M_DOWNLOADS.md) |
|
|
215
|
+
| Platform and file support | [docs/SUPPORT.md](docs/SUPPORT.md) |
|
|
190
216
|
| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
|
|
191
217
|
| Code of conduct | [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) |
|
|
192
218
|
| Changelog | [CHANGELOG.md](CHANGELOG.md) |
|
package/browser.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Durable browser helper backed by the Origin Private File System (OPFS).
|
|
2
|
+
import { Db } from "./picovolt.js";
|
|
3
|
+
|
|
4
|
+
export class PersistentDb {
|
|
5
|
+
constructor(name, db) {
|
|
6
|
+
this.name = name;
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static async open(name = "picovolt.pvdb") {
|
|
11
|
+
if (!globalThis.navigator?.storage?.getDirectory) {
|
|
12
|
+
throw new Error("PicoVolt OPFS persistence is unavailable in this browser/context");
|
|
13
|
+
}
|
|
14
|
+
const root = await navigator.storage.getDirectory();
|
|
15
|
+
const handle = await root.getFileHandle(name, { create: true });
|
|
16
|
+
const file = await handle.getFile();
|
|
17
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
18
|
+
const db = bytes.length ? Db.fromBytes(bytes) : new Db();
|
|
19
|
+
return new PersistentDb(name, db);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
query(sql, params) {
|
|
23
|
+
const json = params === undefined ? this.db.query(sql) : this.db.query(sql, params);
|
|
24
|
+
return JSON.parse(json);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async save() {
|
|
28
|
+
const root = await navigator.storage.getDirectory();
|
|
29
|
+
const handle = await root.getFileHandle(this.name, { create: true });
|
|
30
|
+
const writable = await handle.createWritable();
|
|
31
|
+
await writable.write(this.db.export());
|
|
32
|
+
await writable.close();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async close() {
|
|
36
|
+
await this.save();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default PersistentDb;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "picovolt",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "PicoVolt (PVDB): a polymorphic embedded database engine in Rust.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.5.0",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"picovolt.js",
|
|
14
14
|
"picovolt_bg.js",
|
|
15
15
|
"picovolt.d.ts",
|
|
16
|
-
"sqlite.js"
|
|
16
|
+
"sqlite.js",
|
|
17
|
+
"browser.js",
|
|
18
|
+
"worker.js"
|
|
17
19
|
],
|
|
18
20
|
"main": "picovolt.js",
|
|
19
21
|
"types": "picovolt.d.ts",
|
|
@@ -30,6 +32,8 @@
|
|
|
30
32
|
],
|
|
31
33
|
"exports": {
|
|
32
34
|
".": "./picovolt.js",
|
|
33
|
-
"./sqlite": "./sqlite.js"
|
|
35
|
+
"./sqlite": "./sqlite.js",
|
|
36
|
+
"./browser": "./browser.js",
|
|
37
|
+
"./worker": "./worker.js"
|
|
34
38
|
}
|
|
35
39
|
}
|
package/picovolt_bg.js
CHANGED
|
@@ -135,33 +135,33 @@ export class Db {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
if (Symbol.dispose) Db.prototype[Symbol.dispose] = Db.prototype.free;
|
|
138
|
-
export function
|
|
138
|
+
export function __wbg___wbindgen_boolean_get_c9c83ebd41b34df3(arg0) {
|
|
139
139
|
const v = arg0;
|
|
140
140
|
const ret = typeof(v) === 'boolean' ? v : undefined;
|
|
141
141
|
return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
|
|
142
142
|
}
|
|
143
|
-
export function
|
|
143
|
+
export function __wbg___wbindgen_debug_string_a57024b9c6e4a48b(arg0, arg1) {
|
|
144
144
|
const ret = debugString(arg1);
|
|
145
145
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
146
146
|
const len1 = WASM_VECTOR_LEN;
|
|
147
147
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
148
148
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
149
149
|
}
|
|
150
|
-
export function
|
|
150
|
+
export function __wbg___wbindgen_is_null_7d13f41e1a2d5140(arg0) {
|
|
151
151
|
const ret = arg0 === null;
|
|
152
152
|
return ret;
|
|
153
153
|
}
|
|
154
|
-
export function
|
|
154
|
+
export function __wbg___wbindgen_is_undefined_6cff064c44e0d823(arg0) {
|
|
155
155
|
const ret = arg0 === undefined;
|
|
156
156
|
return ret;
|
|
157
157
|
}
|
|
158
|
-
export function
|
|
158
|
+
export function __wbg___wbindgen_number_get_136b9679cab35cfb(arg0, arg1) {
|
|
159
159
|
const obj = arg1;
|
|
160
160
|
const ret = typeof(obj) === 'number' ? obj : undefined;
|
|
161
161
|
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
|
162
162
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
|
163
163
|
}
|
|
164
|
-
export function
|
|
164
|
+
export function __wbg___wbindgen_string_get_d154f1e671052120(arg0, arg1) {
|
|
165
165
|
const obj = arg1;
|
|
166
166
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
|
167
167
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
@@ -169,14 +169,14 @@ export function __wbg___wbindgen_string_get_b0ca35b86a603356(arg0, arg1) {
|
|
|
169
169
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
170
170
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
171
171
|
}
|
|
172
|
-
export function
|
|
172
|
+
export function __wbg___wbindgen_throw_bb96b2010945f0bc(arg0, arg1) {
|
|
173
173
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
174
174
|
}
|
|
175
|
-
export function
|
|
175
|
+
export function __wbg_call_0f2a9af232c18fd2() { return handleError(function (arg0, arg1, arg2, arg3) {
|
|
176
176
|
const ret = arg0.call(arg1, arg2, arg3);
|
|
177
177
|
return ret;
|
|
178
178
|
}, arguments); }
|
|
179
|
-
export function
|
|
179
|
+
export function __wbg_error_757e9472f8410341(arg0, arg1) {
|
|
180
180
|
let deferred0_0;
|
|
181
181
|
let deferred0_1;
|
|
182
182
|
try {
|
|
@@ -187,15 +187,15 @@ export function __wbg_error_a6fa202b58aa1cd3(arg0, arg1) {
|
|
|
187
187
|
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
export function
|
|
190
|
+
export function __wbg_from_74f3d90e0ff11240(arg0) {
|
|
191
191
|
const ret = Array.from(arg0);
|
|
192
192
|
return ret;
|
|
193
193
|
}
|
|
194
|
-
export function
|
|
194
|
+
export function __wbg_get_unchecked_e20b893aeafc3fca(arg0, arg1) {
|
|
195
195
|
const ret = arg0[arg1 >>> 0];
|
|
196
196
|
return ret;
|
|
197
197
|
}
|
|
198
|
-
export function
|
|
198
|
+
export function __wbg_instanceof_Uint8Array_f935dbb0aa7cdeed(arg0) {
|
|
199
199
|
let result;
|
|
200
200
|
try {
|
|
201
201
|
result = arg0 instanceof Uint8Array;
|
|
@@ -205,11 +205,11 @@ export function __wbg_instanceof_Uint8Array_309b927aaf7a3fc7(arg0) {
|
|
|
205
205
|
const ret = result;
|
|
206
206
|
return ret;
|
|
207
207
|
}
|
|
208
|
-
export function
|
|
208
|
+
export function __wbg_length_36bd29c6848c2144(arg0) {
|
|
209
209
|
const ret = arg0.length;
|
|
210
210
|
return ret;
|
|
211
211
|
}
|
|
212
|
-
export function
|
|
212
|
+
export function __wbg_length_ecfa2c63d3d0d82c(arg0) {
|
|
213
213
|
const ret = arg0.length;
|
|
214
214
|
return ret;
|
|
215
215
|
}
|
|
@@ -217,7 +217,7 @@ export function __wbg_new_227d7c05414eb861() {
|
|
|
217
217
|
const ret = new Error();
|
|
218
218
|
return ret;
|
|
219
219
|
}
|
|
220
|
-
export function
|
|
220
|
+
export function __wbg_prototypesetcall_de8e0d9553586985(arg0, arg1, arg2) {
|
|
221
221
|
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
|
|
222
222
|
}
|
|
223
223
|
export function __wbg_stack_3b0d974bbf31e44f(arg0, arg1) {
|
package/picovolt_bg.wasm
CHANGED
|
Binary file
|
package/sqlite.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
// // [ { id: 1, name: "alice" } ]
|
|
10
10
|
//
|
|
11
11
|
// Limitations: parameters are positional `?` only (named `:id` params are not
|
|
12
|
-
// supported);
|
|
12
|
+
// supported); blob parameters are unsupported.
|
|
13
13
|
|
|
14
14
|
import { Db } from "./picovolt.js";
|
|
15
15
|
|
|
16
16
|
function rowToObject(columns, row) {
|
|
17
|
-
|
|
17
|
+
// A null prototype prevents hostile column names such as `__proto__` from
|
|
18
|
+
// changing the shape or prototype of the returned record.
|
|
19
|
+
const obj = Object.create(null);
|
|
18
20
|
for (let i = 0; i < columns.length; i++) obj[columns[i]] = row[i];
|
|
19
21
|
return obj;
|
|
20
22
|
}
|
|
@@ -63,6 +65,7 @@ class Statement {
|
|
|
63
65
|
class Database {
|
|
64
66
|
constructor() {
|
|
65
67
|
this._db = new Db();
|
|
68
|
+
this._inTransaction = false;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
prepare(sql) {
|
|
@@ -91,8 +94,28 @@ class Database {
|
|
|
91
94
|
throw new Error("picovolt: pragma is not supported");
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
// Wrap a synchronous callback in an atomic unit. A compact PVDB snapshot is
|
|
98
|
+
// restored if the callback throws, matching better-sqlite3's common pattern.
|
|
99
|
+
transaction(fn) {
|
|
100
|
+
if (typeof fn !== "function") throw new TypeError("transaction expects a function");
|
|
101
|
+
const db = this;
|
|
102
|
+
function wrapped(...args) {
|
|
103
|
+
if (db._inTransaction) return fn(...args);
|
|
104
|
+
const snapshot = db.serialize();
|
|
105
|
+
db._inTransaction = true;
|
|
106
|
+
try {
|
|
107
|
+
return fn(...args);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
db._db = Db.fromBytes(snapshot);
|
|
110
|
+
throw error;
|
|
111
|
+
} finally {
|
|
112
|
+
db._inTransaction = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
wrapped.deferred = wrapped;
|
|
116
|
+
wrapped.immediate = wrapped;
|
|
117
|
+
wrapped.exclusive = wrapped;
|
|
118
|
+
return wrapped;
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
close() {
|
package/worker.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Module-worker RPC endpoint. Use `new Worker(url, { type: "module" })` and
|
|
2
|
+
// send `{ id, method, ... }`; every response echoes `id` and carries result/error.
|
|
3
|
+
import { PersistentDb } from "./browser.js";
|
|
4
|
+
|
|
5
|
+
let database;
|
|
6
|
+
|
|
7
|
+
self.addEventListener("message", async ({ data }) => {
|
|
8
|
+
const { id, method } = data ?? {};
|
|
9
|
+
try {
|
|
10
|
+
let result;
|
|
11
|
+
switch (method) {
|
|
12
|
+
case "open":
|
|
13
|
+
database = await PersistentDb.open(data.name);
|
|
14
|
+
result = true;
|
|
15
|
+
break;
|
|
16
|
+
case "query":
|
|
17
|
+
if (!database) throw new Error("open the database first");
|
|
18
|
+
result = database.query(data.sql, data.params);
|
|
19
|
+
break;
|
|
20
|
+
case "save":
|
|
21
|
+
if (!database) throw new Error("open the database first");
|
|
22
|
+
await database.save();
|
|
23
|
+
result = true;
|
|
24
|
+
break;
|
|
25
|
+
case "close":
|
|
26
|
+
if (database) await database.close();
|
|
27
|
+
database = undefined;
|
|
28
|
+
result = true;
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`unknown PicoVolt worker method: ${method}`);
|
|
32
|
+
}
|
|
33
|
+
self.postMessage({ id, result });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
self.postMessage({ id, error: error instanceof Error ? error.message : String(error) });
|
|
36
|
+
}
|
|
37
|
+
});
|