picovolt 1.6.0 → 1.7.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 +42 -47
- package/browser.js +47 -0
- package/package.json +4 -3
- package/picovolt.d.ts +24 -0
- package/picovolt.js +1 -1
- package/picovolt_bg.js +86 -0
- package/picovolt_bg.wasm +0 -0
- package/sqlite.js +38 -4
- package/worker.js +26 -0
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# PicoVolt (PVDB)
|
|
2
2
|
|
|
3
3
|
[](https://github.com/MiniJe/picovolt/actions/workflows/ci.yml)
|
|
4
|
-
[](https://crates.io/crates/picovolt)
|
|
5
5
|
[](LICENSE)
|
|
6
|
-

|
|
7
7
|
[](https://github.com/MiniJe/picovolt)
|
|
8
8
|
|
|
9
|
-
PicoVolt is an embedded database engine written
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
PicoVolt is an embedded database engine written in Rust. Its 1.x public API and
|
|
10
|
+
on-disk format are stable under Semantic Versioning. It is young software and
|
|
11
|
+
has not had an external security audit, so review it and keep backups before
|
|
12
|
+
trusting it with data you cannot regenerate.
|
|
13
13
|
|
|
14
14
|
If PicoVolt is useful to you, consider starring the repository on GitHub. It is
|
|
15
15
|
the simplest way to help others discover the project.
|
|
@@ -18,26 +18,20 @@ The engine decouples query logic from storage representation through a
|
|
|
18
18
|
Virtualization Layer Engine (VLE) that shifts between two on-disk shapes:
|
|
19
19
|
|
|
20
20
|
- **Development mode:** a `.pv/` workspace of mutable, append-only chunk files
|
|
21
|
-
plus a content-addressed blob store
|
|
21
|
+
plus a content-addressed blob store and inspectable manifest.
|
|
22
22
|
- **Production mode:** a single contiguous, memory-mappable `.pvdb` file produced
|
|
23
23
|
by `pv_bake()`.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
cache efficiency.
|
|
25
|
+
New records use a slotted row layout for O(1) appends. Idle pages can be
|
|
26
|
+
transposed into a packed columnar layout for compression and cache efficiency.
|
|
28
27
|
|
|
29
28
|
## Status
|
|
30
29
|
|
|
31
|
-
The
|
|
32
|
-
integration tests
|
|
33
|
-
Linux and Windows.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|-------|-------|--------|
|
|
37
|
-
| 1 | Core memory layouts and error taxonomy | Done |
|
|
38
|
-
| 2 | Page engine, CAS dedup, compression, VLE router | Done |
|
|
39
|
-
| 3 | MVCC and snapshot isolation, WASM runtime | Done |
|
|
40
|
-
| 4 | Public surface (`pv_open_dev` / `pv_open_prod` / `query` / `pv_bake`) | Done |
|
|
30
|
+
The current stable release is exercised by a 240+ test Rust suite plus doctests
|
|
31
|
+
and maintained-binding integration tests. CI also enforces formatting and
|
|
32
|
+
warning-free Clippy builds on Linux and Windows. Shipped changes are tracked in
|
|
33
|
+
[CHANGELOG.md](CHANGELOG.md), and the remaining work toward 2.0 is tracked in
|
|
34
|
+
[ROADMAP.md](ROADMAP.md).
|
|
41
35
|
|
|
42
36
|
### Module map
|
|
43
37
|
|
|
@@ -50,7 +44,7 @@ Linux and Windows. Changes are tracked in [CHANGELOG.md](CHANGELOG.md).
|
|
|
50
44
|
| [`storage/cache.rs`](src/storage/cache.rs) | bounded LRU buffer pool (enables larger-than-RAM reads) |
|
|
51
45
|
| [`storage/cas.rs`](src/storage/cas.rs) | BLAKE3 content-addressable dedup (memory, dev-files, mmap) |
|
|
52
46
|
| [`storage/compress.rs`](src/storage/compress.rs) | Delta-Z, LEB128 varints, dictionary bit-packing |
|
|
53
|
-
| [`storage/index.rs`](src/storage/index.rs) |
|
|
47
|
+
| [`storage/index.rs`](src/storage/index.rs) | ordered secondary-index query structure and its persisted value/address encoding (point and range) |
|
|
54
48
|
| [`storage/record.rs`](src/storage/record.rs) | row and record-body serialization with CAS interception |
|
|
55
49
|
| [`storage/vle.rs`](src/storage/vle.rs) | dev directory store, owned prod snapshot, streamed reads, `bake` |
|
|
56
50
|
| [`engine/mvcc.rs`](src/engine/mvcc.rs) | transaction clock and snapshot visibility |
|
|
@@ -122,33 +116,32 @@ Install the first-class CLI with `cargo install picovolt`, then use `pv query`,
|
|
|
122
116
|
Rust, Python, Go, Node, and browser projects are in [`starters/`](starters/README.md); supported adapters
|
|
123
117
|
are catalogued in [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md).
|
|
124
118
|
|
|
125
|
-
SQL
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
filesystem and in-memory databases.
|
|
119
|
+
SQL supports the normal PicoVolt CRUD and schema statements plus projection,
|
|
120
|
+
filters, aggregates, grouping, time travel, ordering, and pagination. The
|
|
121
|
+
current query surface includes `AS`/bare table aliases, N-table equality
|
|
122
|
+
`INNER`/`LEFT` joins, searched `CASE WHEN`, and the focused `LOWER`, `UPPER`,
|
|
123
|
+
`TRIM`, `LENGTH`, `ABS`, `COALESCE`, and `NULLIF` scalar functions. Schema-light
|
|
124
|
+
types, literal defaults, named inserts, and persisted `CHECK` constraints cover
|
|
125
|
+
common adapter DDL. See the precise syntax, examples, type behavior, and
|
|
126
|
+
deliberate limits in
|
|
127
|
+
[`docs/SQL.md`](docs/SQL.md). Rust callers can cache `Database::prepare(...)`
|
|
128
|
+
templates; C, WebAssembly, JavaScript, Python, and Go expose the same reusable
|
|
129
|
+
prepared-statement lifecycle. Callers can use explicit transactions or atomic
|
|
130
|
+
`Database::transaction(...)` closures with both filesystem and in-memory
|
|
131
|
+
databases.
|
|
139
132
|
Durability is selectable via `Database::set_durability` (`Fast` OS-cache default,
|
|
140
133
|
or crash-safe `Sync` with fsync and an atomic manifest).
|
|
141
134
|
|
|
142
135
|
Measured results and the methodology are in [BENCHMARKS.md](BENCHMARKS.md). In
|
|
143
|
-
short, PicoVolt is a page-backed engine with O(1)
|
|
136
|
+
short, PicoVolt is a page-backed engine with O(1) filesystem appends (autocommit
|
|
144
137
|
around 33k rows/s, linear), larger-than-RAM reads through a bounded buffer pool (a
|
|
145
138
|
667-page dataset serves from a 16-page pool), ordered secondary indexes (point
|
|
146
|
-
lookups roughly
|
|
139
|
+
lookups roughly 6,100 times faster than a scan, plus range predicates), MVCC
|
|
147
140
|
time-travel, opt-in crash-safe durability (`Durability::Sync`), and a fast
|
|
148
141
|
compile-and-publish path (CAS dedup, columnar compression, memory-mappable
|
|
149
142
|
single-file artifacts). Current limits include full-workspace transaction
|
|
150
|
-
backups rather than an incremental WAL,
|
|
151
|
-
and no concurrent writers.
|
|
143
|
+
backups rather than an incremental WAL, left-deep equality joins rather than a
|
|
144
|
+
general SQL planner, and no concurrent writers.
|
|
152
145
|
|
|
153
146
|
## Install and distribution
|
|
154
147
|
|
|
@@ -157,7 +150,8 @@ and no concurrent writers.
|
|
|
157
150
|
| **Rust** (crates.io) | `cargo add picovolt` |
|
|
158
151
|
| **JavaScript / npm** (WebAssembly, browser and Node) | `npm install picovolt` |
|
|
159
152
|
| **Python** (native wheels) | `python -m pip install picovolt` |
|
|
160
|
-
| **
|
|
153
|
+
| **Go** (`database/sql` and direct API) | `go get github.com/MiniJe/picovolt/bindings/go@latest`, then provide the matching native C ABI library described in [`bindings/go/`](bindings/go) |
|
|
154
|
+
| **C** | Download the matching `picovolt-capi-*` bundle from the [latest release](https://github.com/MiniJe/picovolt/releases/latest), or run `cargo build --release --features capi` |
|
|
161
155
|
| **In-memory** (native, no filesystem) | `Database::open_memory()`, export with `bake_to_bytes()` |
|
|
162
156
|
|
|
163
157
|
PicoVolt runs in the browser through its in-memory backend plus an OPFS persistence
|
|
@@ -174,13 +168,14 @@ binding. The bindings suit embedded use, not a concurrent server's primary store
|
|
|
174
168
|
|
|
175
169
|
All bindings accept positional `?` parameters
|
|
176
170
|
(`db.query("... WHERE id = ?", [1])`), bound as safely-escaped SQL literals. For
|
|
177
|
-
a familiar surface,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
([`bindings/go/pvsql`](bindings/go/pvsql)).
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
171
|
+
a familiar surface, PicoVolt provides a `better-sqlite3`-inspired JavaScript API
|
|
172
|
+
(`import Database from "picovolt/sqlite"`), a Python DB-API 2.0 module
|
|
173
|
+
(`import picovolt.dbapi2 as sqlite`), and a Go `database/sql` driver
|
|
174
|
+
([`bindings/go/pvsql`](bindings/go/pvsql)). These are interface adapters, not
|
|
175
|
+
drop-in compatibility layers: shared limits include positional `?` only and the
|
|
176
|
+
intentionally compact SQL grammar. JavaScript and in-memory Rust also expose
|
|
177
|
+
rollback-capable transaction wrappers. Native bindings expose the same
|
|
178
|
+
transaction lifecycle through the C ABI.
|
|
184
179
|
|
|
185
180
|
## Server mode
|
|
186
181
|
|
package/browser.js
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
// Durable browser helper backed by the Origin Private File System (OPFS).
|
|
2
2
|
import { Db } from "./picovolt.js";
|
|
3
3
|
|
|
4
|
+
export class PersistentStatement {
|
|
5
|
+
constructor(database, source) {
|
|
6
|
+
this.database = database;
|
|
7
|
+
this.source = source;
|
|
8
|
+
this._prepared = database.db.prepare(source);
|
|
9
|
+
this.parameterCount = this._prepared.parameterCount;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
query(params = []) {
|
|
13
|
+
if (!this._prepared) throw new Error("PicoVolt prepared statement is closed");
|
|
14
|
+
this.database._assertOpen();
|
|
15
|
+
return JSON.parse(this._prepared.execute(this.database.db, params));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
close() {
|
|
19
|
+
if (!this._prepared) return false;
|
|
20
|
+
const prepared = this._prepared;
|
|
21
|
+
this._prepared = undefined;
|
|
22
|
+
this.database._statements.delete(this);
|
|
23
|
+
prepared.free();
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
finalize() {
|
|
28
|
+
return this.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
4
32
|
export class PersistentDb {
|
|
5
33
|
constructor(name, db) {
|
|
6
34
|
this.name = name;
|
|
7
35
|
this.db = db;
|
|
36
|
+
this._closed = false;
|
|
37
|
+
this._statements = new Set();
|
|
8
38
|
}
|
|
9
39
|
|
|
10
40
|
static async open(name = "picovolt.pvdb") {
|
|
@@ -20,11 +50,20 @@ export class PersistentDb {
|
|
|
20
50
|
}
|
|
21
51
|
|
|
22
52
|
query(sql, params) {
|
|
53
|
+
this._assertOpen();
|
|
23
54
|
const json = params === undefined ? this.db.query(sql) : this.db.query(sql, params);
|
|
24
55
|
return JSON.parse(json);
|
|
25
56
|
}
|
|
26
57
|
|
|
58
|
+
prepare(sql) {
|
|
59
|
+
this._assertOpen();
|
|
60
|
+
const statement = new PersistentStatement(this, sql);
|
|
61
|
+
this._statements.add(statement);
|
|
62
|
+
return statement;
|
|
63
|
+
}
|
|
64
|
+
|
|
27
65
|
async save() {
|
|
66
|
+
this._assertOpen();
|
|
28
67
|
const root = await navigator.storage.getDirectory();
|
|
29
68
|
const handle = await root.getFileHandle(this.name, { create: true });
|
|
30
69
|
const writable = await handle.createWritable();
|
|
@@ -33,7 +72,15 @@ export class PersistentDb {
|
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
async close() {
|
|
75
|
+
if (this._closed) return;
|
|
36
76
|
await this.save();
|
|
77
|
+
for (const statement of [...this._statements]) statement.close();
|
|
78
|
+
this.db.free();
|
|
79
|
+
this._closed = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_assertOpen() {
|
|
83
|
+
if (this._closed) throw new Error("PicoVolt database is closed");
|
|
37
84
|
}
|
|
38
85
|
}
|
|
39
86
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picovolt",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "1.
|
|
4
|
+
"description": "Embedded SQL database with MVCC history and single-file deployment",
|
|
5
|
+
"version": "1.7.1",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/MiniJe/picovolt"
|
|
9
|
+
"url": "git+https://github.com/MiniJe/picovolt.git"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"picovolt_bg.wasm",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"worker.js"
|
|
19
19
|
],
|
|
20
20
|
"main": "picovolt.js",
|
|
21
|
+
"homepage": "https://github.com/MiniJe/picovolt",
|
|
21
22
|
"types": "picovolt.d.ts",
|
|
22
23
|
"sideEffects": [
|
|
23
24
|
"./picovolt.js",
|
package/picovolt.d.ts
CHANGED
|
@@ -44,6 +44,12 @@ export class Db {
|
|
|
44
44
|
* as queries touch them. `totalSize` is the image's byte length.
|
|
45
45
|
*/
|
|
46
46
|
static openRemote(read: Function, total_size: number): Db;
|
|
47
|
+
/**
|
|
48
|
+
* Validate and retain a reusable SQL template. Preparation verifies the
|
|
49
|
+
* syntax and records the exact positional-parameter count without running
|
|
50
|
+
* the statement.
|
|
51
|
+
*/
|
|
52
|
+
prepare(sql: string): PreparedStatement;
|
|
47
53
|
/**
|
|
48
54
|
* Run one SQL statement, optionally binding `?` placeholders to `params` (a
|
|
49
55
|
* JS array, e.g. `db.query("SELECT * FROM t WHERE id = ?", [1])`). Returns a
|
|
@@ -62,3 +68,21 @@ export class Db {
|
|
|
62
68
|
*/
|
|
63
69
|
tables(): string;
|
|
64
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A validated, reusable SQL template for the raw WebAssembly API.
|
|
74
|
+
*/
|
|
75
|
+
export class PreparedStatement {
|
|
76
|
+
private constructor();
|
|
77
|
+
free(): void;
|
|
78
|
+
[Symbol.dispose](): void;
|
|
79
|
+
/**
|
|
80
|
+
* Execute this statement against `db`, returning the same JSON string as
|
|
81
|
+
* [`Db::query`]. The statement can be reused with different parameters.
|
|
82
|
+
*/
|
|
83
|
+
execute(db: Db, params: any): string;
|
|
84
|
+
/**
|
|
85
|
+
* Number of positional `?` values required by this statement.
|
|
86
|
+
*/
|
|
87
|
+
readonly parameterCount: number;
|
|
88
|
+
}
|
package/picovolt.js
CHANGED
package/picovolt_bg.js
CHANGED
|
@@ -106,6 +106,22 @@ export class Db {
|
|
|
106
106
|
}
|
|
107
107
|
return Db.__wrap(ret[0]);
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate and retain a reusable SQL template. Preparation verifies the
|
|
111
|
+
* syntax and records the exact positional-parameter count without running
|
|
112
|
+
* the statement.
|
|
113
|
+
* @param {string} sql
|
|
114
|
+
* @returns {PreparedStatement}
|
|
115
|
+
*/
|
|
116
|
+
prepare(sql) {
|
|
117
|
+
const ptr0 = passStringToWasm0(sql, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
118
|
+
const len0 = WASM_VECTOR_LEN;
|
|
119
|
+
const ret = wasm.db_prepare(this.__wbg_ptr, ptr0, len0);
|
|
120
|
+
if (ret[2]) {
|
|
121
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
122
|
+
}
|
|
123
|
+
return PreparedStatement.__wrap(ret[0]);
|
|
124
|
+
}
|
|
109
125
|
/**
|
|
110
126
|
* Run one SQL statement, optionally binding `?` placeholders to `params` (a
|
|
111
127
|
* JS array, e.g. `db.query("SELECT * FROM t WHERE id = ?", [1])`). Returns a
|
|
@@ -170,6 +186,63 @@ export class Db {
|
|
|
170
186
|
}
|
|
171
187
|
}
|
|
172
188
|
if (Symbol.dispose) Db.prototype[Symbol.dispose] = Db.prototype.free;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A validated, reusable SQL template for the raw WebAssembly API.
|
|
192
|
+
*/
|
|
193
|
+
export class PreparedStatement {
|
|
194
|
+
static __wrap(ptr) {
|
|
195
|
+
const obj = Object.create(PreparedStatement.prototype);
|
|
196
|
+
obj.__wbg_ptr = ptr;
|
|
197
|
+
PreparedStatementFinalization.register(obj, obj.__wbg_ptr, obj);
|
|
198
|
+
return obj;
|
|
199
|
+
}
|
|
200
|
+
__destroy_into_raw() {
|
|
201
|
+
const ptr = this.__wbg_ptr;
|
|
202
|
+
this.__wbg_ptr = 0;
|
|
203
|
+
PreparedStatementFinalization.unregister(this);
|
|
204
|
+
return ptr;
|
|
205
|
+
}
|
|
206
|
+
free() {
|
|
207
|
+
const ptr = this.__destroy_into_raw();
|
|
208
|
+
wasm.__wbg_preparedstatement_free(ptr, 0);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Execute this statement against `db`, returning the same JSON string as
|
|
212
|
+
* [`Db::query`]. The statement can be reused with different parameters.
|
|
213
|
+
* @param {Db} db
|
|
214
|
+
* @param {any} params
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
execute(db, params) {
|
|
218
|
+
let deferred2_0;
|
|
219
|
+
let deferred2_1;
|
|
220
|
+
try {
|
|
221
|
+
_assertClass(db, Db);
|
|
222
|
+
const ret = wasm.preparedstatement_execute(this.__wbg_ptr, db.__wbg_ptr, params);
|
|
223
|
+
var ptr1 = ret[0];
|
|
224
|
+
var len1 = ret[1];
|
|
225
|
+
if (ret[3]) {
|
|
226
|
+
ptr1 = 0; len1 = 0;
|
|
227
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
228
|
+
}
|
|
229
|
+
deferred2_0 = ptr1;
|
|
230
|
+
deferred2_1 = len1;
|
|
231
|
+
return getStringFromWasm0(ptr1, len1);
|
|
232
|
+
} finally {
|
|
233
|
+
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Number of positional `?` values required by this statement.
|
|
238
|
+
* @returns {number}
|
|
239
|
+
*/
|
|
240
|
+
get parameterCount() {
|
|
241
|
+
const ret = wasm.preparedstatement_parameterCount(this.__wbg_ptr);
|
|
242
|
+
return ret >>> 0;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (Symbol.dispose) PreparedStatement.prototype[Symbol.dispose] = PreparedStatement.prototype.free;
|
|
173
246
|
export function __wbg___wbindgen_boolean_get_c9c83ebd41b34df3(arg0) {
|
|
174
247
|
const v = arg0;
|
|
175
248
|
const ret = typeof(v) === 'boolean' ? v : undefined;
|
|
@@ -240,6 +313,10 @@ export function __wbg_instanceof_Uint8Array_f935dbb0aa7cdeed(arg0) {
|
|
|
240
313
|
const ret = result;
|
|
241
314
|
return ret;
|
|
242
315
|
}
|
|
316
|
+
export function __wbg_isArray_6339f732981044bf(arg0) {
|
|
317
|
+
const ret = Array.isArray(arg0);
|
|
318
|
+
return ret;
|
|
319
|
+
}
|
|
243
320
|
export function __wbg_length_36bd29c6848c2144(arg0) {
|
|
244
321
|
const ret = arg0.length;
|
|
245
322
|
return ret;
|
|
@@ -284,6 +361,9 @@ export function __wbindgen_init_externref_table() {
|
|
|
284
361
|
const DbFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
285
362
|
? { register: () => {}, unregister: () => {} }
|
|
286
363
|
: new FinalizationRegistry(ptr => wasm.__wbg_db_free(ptr, 1));
|
|
364
|
+
const PreparedStatementFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
365
|
+
? { register: () => {}, unregister: () => {} }
|
|
366
|
+
: new FinalizationRegistry(ptr => wasm.__wbg_preparedstatement_free(ptr, 1));
|
|
287
367
|
|
|
288
368
|
function addToExternrefTable0(obj) {
|
|
289
369
|
const idx = wasm.__externref_table_alloc();
|
|
@@ -291,6 +371,12 @@ function addToExternrefTable0(obj) {
|
|
|
291
371
|
return idx;
|
|
292
372
|
}
|
|
293
373
|
|
|
374
|
+
function _assertClass(instance, klass) {
|
|
375
|
+
if (!(instance instanceof klass)) {
|
|
376
|
+
throw new Error(`expected instance of ${klass.name}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
294
380
|
function debugString(val) {
|
|
295
381
|
// primitive types
|
|
296
382
|
const type = typeof val;
|
package/picovolt_bg.wasm
CHANGED
|
Binary file
|
package/sqlite.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// A better-sqlite3-
|
|
2
|
-
//
|
|
1
|
+
// A better-sqlite3-inspired synchronous API over PicoVolt's WebAssembly engine.
|
|
2
|
+
// It follows the familiar prepare/run/get/all shape while retaining PicoVolt's
|
|
3
|
+
// focused SQL surface:
|
|
3
4
|
//
|
|
4
5
|
// import Database from "picovolt/sqlite";
|
|
5
6
|
// const db = new Database();
|
|
@@ -30,13 +31,19 @@ function normalizeParams(args) {
|
|
|
30
31
|
|
|
31
32
|
class Statement {
|
|
32
33
|
constructor(db, sql) {
|
|
34
|
+
db._assertOpen();
|
|
33
35
|
this._db = db;
|
|
34
36
|
this.source = sql;
|
|
37
|
+
this._prepared = db._db.prepare(sql);
|
|
38
|
+
this.parameterCount = this._prepared.parameterCount;
|
|
39
|
+
db._statements.add(this);
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
_exec(args) {
|
|
43
|
+
if (!this._prepared) throw new Error("PicoVolt prepared statement is closed");
|
|
44
|
+
this._db._assertOpen();
|
|
38
45
|
const params = normalizeParams(args);
|
|
39
|
-
const json =
|
|
46
|
+
const json = this._prepared.execute(this._db._db, params);
|
|
40
47
|
return JSON.parse(json);
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -60,12 +67,27 @@ class Statement {
|
|
|
60
67
|
*iterate(...args) {
|
|
61
68
|
yield* this.all(...args);
|
|
62
69
|
}
|
|
70
|
+
|
|
71
|
+
close() {
|
|
72
|
+
if (!this._prepared) return false;
|
|
73
|
+
const prepared = this._prepared;
|
|
74
|
+
this._prepared = undefined;
|
|
75
|
+
this._db._statements.delete(this);
|
|
76
|
+
prepared.free();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
finalize() {
|
|
81
|
+
return this.close();
|
|
82
|
+
}
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
class Database {
|
|
66
86
|
constructor() {
|
|
67
87
|
this._db = new Db();
|
|
68
88
|
this._inTransaction = false;
|
|
89
|
+
this._closed = false;
|
|
90
|
+
this._statements = new Set();
|
|
69
91
|
}
|
|
70
92
|
|
|
71
93
|
prepare(sql) {
|
|
@@ -74,6 +96,7 @@ class Database {
|
|
|
74
96
|
|
|
75
97
|
// Run one or more `;`-separated statements with no bound parameters.
|
|
76
98
|
exec(sql) {
|
|
99
|
+
this._assertOpen();
|
|
77
100
|
for (const stmt of sql.split(";").map((s) => s.trim()).filter(Boolean)) {
|
|
78
101
|
this._db.query(stmt);
|
|
79
102
|
}
|
|
@@ -82,11 +105,13 @@ class Database {
|
|
|
82
105
|
|
|
83
106
|
// The most recent committed transaction id (upper bound for `... BEFORE tx`).
|
|
84
107
|
get currentTx() {
|
|
108
|
+
this._assertOpen();
|
|
85
109
|
return this._db.currentTx();
|
|
86
110
|
}
|
|
87
111
|
|
|
88
112
|
// Export the database as a `.pvdb` byte image (Uint8Array).
|
|
89
113
|
serialize() {
|
|
114
|
+
this._assertOpen();
|
|
90
115
|
return this._db.export();
|
|
91
116
|
}
|
|
92
117
|
|
|
@@ -98,8 +123,10 @@ class Database {
|
|
|
98
123
|
// better-sqlite3's common pattern.
|
|
99
124
|
transaction(fn) {
|
|
100
125
|
if (typeof fn !== "function") throw new TypeError("transaction expects a function");
|
|
126
|
+
this._assertOpen();
|
|
101
127
|
const db = this;
|
|
102
128
|
function wrapped(...args) {
|
|
129
|
+
db._assertOpen();
|
|
103
130
|
if (db._inTransaction) return fn(...args);
|
|
104
131
|
db._db.beginTransaction();
|
|
105
132
|
db._inTransaction = true;
|
|
@@ -121,7 +148,14 @@ class Database {
|
|
|
121
148
|
}
|
|
122
149
|
|
|
123
150
|
close() {
|
|
124
|
-
|
|
151
|
+
if (this._closed) return;
|
|
152
|
+
for (const statement of [...this._statements]) statement.close();
|
|
153
|
+
this._db.free();
|
|
154
|
+
this._closed = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_assertOpen() {
|
|
158
|
+
if (this._closed) throw new Error("PicoVolt database is closed");
|
|
125
159
|
}
|
|
126
160
|
}
|
|
127
161
|
|
package/worker.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { PersistentDb } from "./browser.js";
|
|
4
4
|
|
|
5
5
|
let database;
|
|
6
|
+
let nextStatementId = 1;
|
|
7
|
+
const statements = new Map();
|
|
6
8
|
|
|
7
9
|
self.addEventListener("message", async ({ data }) => {
|
|
8
10
|
const { id, method } = data ?? {};
|
|
@@ -10,6 +12,9 @@ self.addEventListener("message", async ({ data }) => {
|
|
|
10
12
|
let result;
|
|
11
13
|
switch (method) {
|
|
12
14
|
case "open":
|
|
15
|
+
if (database) await database.close();
|
|
16
|
+
database = undefined;
|
|
17
|
+
statements.clear();
|
|
13
18
|
database = await PersistentDb.open(data.name);
|
|
14
19
|
result = true;
|
|
15
20
|
break;
|
|
@@ -17,6 +22,26 @@ self.addEventListener("message", async ({ data }) => {
|
|
|
17
22
|
if (!database) throw new Error("open the database first");
|
|
18
23
|
result = database.query(data.sql, data.params);
|
|
19
24
|
break;
|
|
25
|
+
case "prepare": {
|
|
26
|
+
if (!database) throw new Error("open the database first");
|
|
27
|
+
const statement = database.prepare(data.sql);
|
|
28
|
+
const statementId = nextStatementId++;
|
|
29
|
+
statements.set(statementId, statement);
|
|
30
|
+
result = { statementId, parameterCount: statement.parameterCount };
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "execute": {
|
|
34
|
+
const statement = statements.get(data.statementId);
|
|
35
|
+
if (!statement) throw new Error("unknown PicoVolt prepared statement");
|
|
36
|
+
result = statement.query(data.params ?? []);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "finalize": {
|
|
40
|
+
const statement = statements.get(data.statementId);
|
|
41
|
+
result = statement ? statement.close() : false;
|
|
42
|
+
statements.delete(data.statementId);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
20
45
|
case "save":
|
|
21
46
|
if (!database) throw new Error("open the database first");
|
|
22
47
|
await database.save();
|
|
@@ -25,6 +50,7 @@ self.addEventListener("message", async ({ data }) => {
|
|
|
25
50
|
case "close":
|
|
26
51
|
if (database) await database.close();
|
|
27
52
|
database = undefined;
|
|
53
|
+
statements.clear();
|
|
28
54
|
result = true;
|
|
29
55
|
break;
|
|
30
56
|
default:
|