picovolt 1.5.0 → 1.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # PicoVolt (PVDB)
2
2
 
3
3
  [![CI](https://github.com/MiniJe/picovolt/actions/workflows/ci.yml/badge.svg)](https://github.com/MiniJe/picovolt/actions/workflows/ci.yml)
4
- [![Version](https://img.shields.io/badge/version-1.5.0-blue.svg)](CHANGELOG.md)
4
+ [![Version](https://img.shields.io/badge/version-1.7.0-blue.svg)](CHANGELOG.md)
5
5
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
6
6
  ![Status: 1.0 stable](https://img.shields.io/badge/status-1.0%20stable-brightgreen.svg)
7
7
  [![GitHub stars](https://img.shields.io/github/stars/MiniJe/picovolt?style=social)](https://github.com/MiniJe/picovolt)
@@ -58,6 +58,7 @@ Linux and Windows. Changes are tracked in [CHANGELOG.md](CHANGELOG.md).
58
58
  | [`engine/interp.rs`](src/engine/interp.rs) | `pv-wasm`: a from-scratch WASM interpreter (integer subset) |
59
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
+ | [`enterprise.rs`](src/enterprise.rs) | optional, host-owned audit events and honest capability discovery for fleet integrations |
61
62
  | [`db.rs`](src/db.rs) | the `Database` surface that ties it together |
62
63
  | [`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 |
63
64
 
@@ -86,6 +87,10 @@ Linux and Windows. Changes are tracked in [CHANGELOG.md](CHANGELOG.md).
86
87
  each flush `fsync` the data and commit the manifest atomically (write to a temp
87
88
  file, `fsync`, then rename). The default `Fast` mode uses the OS cache only:
88
89
  fast and durable on a clean exit, but not power-loss-safe.
90
+ - **Crash-recoverable transactions.** Explicit `BEGIN`, `COMMIT`, and
91
+ `ROLLBACK` group filesystem or in-memory writes. Filesystem transactions keep
92
+ a synced rollback image and recovery marker; reopening after interruption
93
+ restores the last committed state before loading the workspace.
89
94
  - **Hardened against untrusted input.** Opening a `.pvdb` or workspace, or running
90
95
  a WASM module, validates manifest hashes (no path traversal), bounds-checks CAS
91
96
  offsets and page chains (no out-of-bounds reads or infinite loops on a crafted
@@ -117,19 +122,18 @@ Install the first-class CLI with `cargo install picovolt`, then use `pv query`,
117
122
  Rust, Python, Go, Node, and browser projects are in [`starters/`](starters/README.md); supported adapters
118
123
  are catalogued in [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md).
119
124
 
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
123
- `SELECT [DISTINCT] {* | col [AS alias], ... | COUNT/SUM/MIN/MAX/AVG(...) [AS alias]}
124
- FROM t [WHERE <pred>] [GROUP BY cols] [HAVING <pred>] [BEFORE tx]
125
- [ORDER BY col [ASC|DESC], ...] [LIMIT n] [OFFSET n]`, where `<pred>` combines
126
- `col <op> value` (`=`, `!=`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`),
127
- `col [NOT] IN (...)`, `col [NOT] BETWEEN a AND b`, and `col IS [NOT] NULL` with
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.
125
+ SQL supports the normal PicoVolt CRUD and schema statements plus projection,
126
+ filters, aggregates, grouping, time travel, ordering, and pagination. The 1.7
127
+ query surface adds `AS`/bare table aliases, N-table equality `INNER`/`LEFT`
128
+ joins, searched `CASE WHEN`, and the focused `LOWER`, `UPPER`, `TRIM`, `LENGTH`,
129
+ `ABS`, `COALESCE`, and `NULLIF` scalar functions. Schema-light types, literal
130
+ defaults, named inserts, and persisted `CHECK` constraints cover common adapter
131
+ DDL. See the precise syntax, examples, type behavior, and deliberate limits in
132
+ [`docs/SQL.md`](docs/SQL.md). Rust callers can cache `Database::prepare(...)`
133
+ templates; C, WebAssembly, JavaScript, Python, and Go expose the same reusable
134
+ prepared-statement lifecycle. Callers can use explicit transactions or atomic
135
+ `Database::transaction(...)` closures with both filesystem and in-memory
136
+ databases.
133
137
  Durability is selectable via `Database::set_durability` (`Fast` OS-cache default,
134
138
  or crash-safe `Sync` with fsync and an atomic manifest).
135
139
 
@@ -140,8 +144,9 @@ around 33k rows/s, linear), larger-than-RAM reads through a bounded buffer pool
140
144
  lookups roughly 11,000 times faster than a scan, plus range predicates), MVCC
141
145
  time-travel, opt-in crash-safe durability (`Durability::Sync`), and a fast
142
146
  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.
147
+ single-file artifacts). Current limits include full-workspace transaction
148
+ backups rather than an incremental WAL, left-deep equality joins rather than a
149
+ general SQL planner, and no concurrent writers.
145
150
 
146
151
  ## Install and distribution
147
152
 
@@ -149,7 +154,7 @@ transactions, only basic two-table equality joins, and no concurrent writers.
149
154
  |--------|-----|
150
155
  | **Rust** (crates.io) | `cargo add picovolt` |
151
156
  | **JavaScript / npm** (WebAssembly, browser and Node) | `npm install picovolt` |
152
- | **Python** (native wheels) | `python -m pip install picovolt` (after the first PyPI release) |
157
+ | **Python** (native wheels) | `python -m pip install picovolt` |
153
158
  | **C / Go** (native, via the C ABI) | `cargo build --release --features capi`, then see [`bindings/`](bindings) |
154
159
  | **In-memory** (native, no filesystem) | `Database::open_memory()`, export with `bake_to_bytes()` |
155
160
 
@@ -172,7 +177,8 @@ JavaScript API (`import Database from "picovolt/sqlite"`), a Python DB-API 2.0
172
177
  module (`import picovolt.dbapi2 as sqlite`), and the Go `database/sql` driver
173
178
  ([`bindings/go/pvsql`](bindings/go/pvsql)). Shared limits include positional `?`
174
179
  only and the intentionally compact SQL grammar; JavaScript and in-memory Rust
175
- also expose rollback-capable transaction wrappers.
180
+ also expose rollback-capable transaction wrappers. Native bindings expose the
181
+ same transaction lifecycle through the C ABI.
176
182
 
177
183
  ## Server mode
178
184
 
@@ -197,6 +203,11 @@ result rows, and response size are bounded. TLS is not built in, so network
197
203
  deployments still belong behind a TLS-terminating reverse proxy. See
198
204
  [src/bin/server.rs](src/bin/server.rs).
199
205
 
206
+ The HTTP API is sessionless, so each request is an atomic statement and explicit
207
+ transaction-control statements are rejected. Applications needing a
208
+ multi-statement transaction should use an embedded language binding, where the
209
+ transaction belongs to one database handle.
210
+
200
211
  Applications accepting SQL from users can also call `Database::query_with_limits`
201
212
  directly and choose their own scan, result, memory, and deadline budgets.
202
213
 
@@ -212,6 +223,8 @@ native modules built on the public API. Both are documented in
212
223
  |--|--|
213
224
  | Roadmap | [ROADMAP.md](ROADMAP.md) |
214
225
  | One-million-download plan | [docs/ROADMAP_1M_DOWNLOADS.md](docs/ROADMAP_1M_DOWNLOADS.md) |
226
+ | Monetization thesis | [docs/MONETIZATION.md](docs/MONETIZATION.md) |
227
+ | Enterprise integration foundation | [docs/ENTERPRISE.md](docs/ENTERPRISE.md) |
215
228
  | Platform and file support | [docs/SUPPORT.md](docs/SUPPORT.md) |
216
229
  | Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
217
230
  | Code of conduct | [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) |
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
@@ -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.0",
5
+ "version": "1.7.0",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
package/picovolt.d.ts CHANGED
@@ -7,6 +7,14 @@
7
7
  export class Db {
8
8
  free(): void;
9
9
  [Symbol.dispose](): void;
10
+ /**
11
+ * Begin an explicit multi-statement in-memory transaction.
12
+ */
13
+ beginTransaction(): void;
14
+ /**
15
+ * Commit the active transaction.
16
+ */
17
+ commitTransaction(): void;
10
18
  /**
11
19
  * The most recently committed transaction id, the upper bound for a
12
20
  * `... BEFORE tx` time-travel query.
@@ -21,6 +29,10 @@ export class Db {
21
29
  * [`export`](Db::export)). Writable, with full time-travel history intact.
22
30
  */
23
31
  static fromBytes(bytes: Uint8Array): Db;
32
+ /**
33
+ * Whether an explicit transaction is active.
34
+ */
35
+ inTransaction(): boolean;
24
36
  /**
25
37
  * Create a new, empty in-memory database.
26
38
  */
@@ -32,6 +44,12 @@ export class Db {
32
44
  * as queries touch them. `totalSize` is the image's byte length.
33
45
  */
34
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;
35
53
  /**
36
54
  * Run one SQL statement, optionally binding `?` placeholders to `params` (a
37
55
  * JS array, e.g. `db.query("SELECT * FROM t WHERE id = ?", [1])`). Returns a
@@ -40,9 +58,31 @@ export class Db {
40
58
  * `{"done":true}` otherwise. Throws the error message (a string) on failure.
41
59
  */
42
60
  query(sql: string, params: any): string;
61
+ /**
62
+ * Roll back the active transaction.
63
+ */
64
+ rollbackTransaction(): void;
43
65
  /**
44
66
  * A JSON array of the table names in this database (for introspecting an
45
67
  * uploaded `.pvdb` whose schema is unknown).
46
68
  */
47
69
  tables(): string;
48
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
@@ -5,5 +5,5 @@ import { __wbg_set_wasm } from "./picovolt_bg.js";
5
5
  __wbg_set_wasm(wasm);
6
6
  wasm.__wbindgen_start();
7
7
  export {
8
- Db
8
+ Db, PreparedStatement
9
9
  } from "./picovolt_bg.js";
package/picovolt_bg.js CHANGED
@@ -18,6 +18,24 @@ export class Db {
18
18
  const ptr = this.__destroy_into_raw();
19
19
  wasm.__wbg_db_free(ptr, 0);
20
20
  }
21
+ /**
22
+ * Begin an explicit multi-statement in-memory transaction.
23
+ */
24
+ beginTransaction() {
25
+ const ret = wasm.db_beginTransaction(this.__wbg_ptr);
26
+ if (ret[1]) {
27
+ throw takeFromExternrefTable0(ret[0]);
28
+ }
29
+ }
30
+ /**
31
+ * Commit the active transaction.
32
+ */
33
+ commitTransaction() {
34
+ const ret = wasm.db_commitTransaction(this.__wbg_ptr);
35
+ if (ret[1]) {
36
+ throw takeFromExternrefTable0(ret[0]);
37
+ }
38
+ }
21
39
  /**
22
40
  * The most recently committed transaction id, the upper bound for a
23
41
  * `... BEFORE tx` time-travel query.
@@ -55,6 +73,14 @@ export class Db {
55
73
  }
56
74
  return Db.__wrap(ret[0]);
57
75
  }
76
+ /**
77
+ * Whether an explicit transaction is active.
78
+ * @returns {boolean}
79
+ */
80
+ inTransaction() {
81
+ const ret = wasm.db_inTransaction(this.__wbg_ptr);
82
+ return ret !== 0;
83
+ }
58
84
  /**
59
85
  * Create a new, empty in-memory database.
60
86
  */
@@ -80,6 +106,22 @@ export class Db {
80
106
  }
81
107
  return Db.__wrap(ret[0]);
82
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
+ }
83
125
  /**
84
126
  * Run one SQL statement, optionally binding `?` placeholders to `params` (a
85
127
  * JS array, e.g. `db.query("SELECT * FROM t WHERE id = ?", [1])`). Returns a
@@ -110,6 +152,15 @@ export class Db {
110
152
  wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
111
153
  }
112
154
  }
155
+ /**
156
+ * Roll back the active transaction.
157
+ */
158
+ rollbackTransaction() {
159
+ const ret = wasm.db_rollbackTransaction(this.__wbg_ptr);
160
+ if (ret[1]) {
161
+ throw takeFromExternrefTable0(ret[0]);
162
+ }
163
+ }
113
164
  /**
114
165
  * A JSON array of the table names in this database (for introspecting an
115
166
  * uploaded `.pvdb` whose schema is unknown).
@@ -135,6 +186,63 @@ export class Db {
135
186
  }
136
187
  }
137
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;
138
246
  export function __wbg___wbindgen_boolean_get_c9c83ebd41b34df3(arg0) {
139
247
  const v = arg0;
140
248
  const ret = typeof(v) === 'boolean' ? v : undefined;
@@ -205,6 +313,10 @@ export function __wbg_instanceof_Uint8Array_f935dbb0aa7cdeed(arg0) {
205
313
  const ret = result;
206
314
  return ret;
207
315
  }
316
+ export function __wbg_isArray_6339f732981044bf(arg0) {
317
+ const ret = Array.isArray(arg0);
318
+ return ret;
319
+ }
208
320
  export function __wbg_length_36bd29c6848c2144(arg0) {
209
321
  const ret = arg0.length;
210
322
  return ret;
@@ -249,6 +361,9 @@ export function __wbindgen_init_externref_table() {
249
361
  const DbFinalization = (typeof FinalizationRegistry === 'undefined')
250
362
  ? { register: () => {}, unregister: () => {} }
251
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));
252
367
 
253
368
  function addToExternrefTable0(obj) {
254
369
  const idx = wasm.__externref_table_alloc();
@@ -256,6 +371,12 @@ function addToExternrefTable0(obj) {
256
371
  return idx;
257
372
  }
258
373
 
374
+ function _assertClass(instance, klass) {
375
+ if (!(instance instanceof klass)) {
376
+ throw new Error(`expected instance of ${klass.name}`);
377
+ }
378
+ }
379
+
259
380
  function debugString(val) {
260
381
  // primitive types
261
382
  const type = typeof val;
package/picovolt_bg.wasm CHANGED
Binary file
package/sqlite.js CHANGED
@@ -30,13 +30,19 @@ function normalizeParams(args) {
30
30
 
31
31
  class Statement {
32
32
  constructor(db, sql) {
33
+ db._assertOpen();
33
34
  this._db = db;
34
35
  this.source = sql;
36
+ this._prepared = db._db.prepare(sql);
37
+ this.parameterCount = this._prepared.parameterCount;
38
+ db._statements.add(this);
35
39
  }
36
40
 
37
41
  _exec(args) {
42
+ if (!this._prepared) throw new Error("PicoVolt prepared statement is closed");
43
+ this._db._assertOpen();
38
44
  const params = normalizeParams(args);
39
- const json = params.length ? this._db._db.query(this.source, params) : this._db._db.query(this.source);
45
+ const json = this._prepared.execute(this._db._db, params);
40
46
  return JSON.parse(json);
41
47
  }
42
48
 
@@ -60,12 +66,27 @@ class Statement {
60
66
  *iterate(...args) {
61
67
  yield* this.all(...args);
62
68
  }
69
+
70
+ close() {
71
+ if (!this._prepared) return false;
72
+ const prepared = this._prepared;
73
+ this._prepared = undefined;
74
+ this._db._statements.delete(this);
75
+ prepared.free();
76
+ return true;
77
+ }
78
+
79
+ finalize() {
80
+ return this.close();
81
+ }
63
82
  }
64
83
 
65
84
  class Database {
66
85
  constructor() {
67
86
  this._db = new Db();
68
87
  this._inTransaction = false;
88
+ this._closed = false;
89
+ this._statements = new Set();
69
90
  }
70
91
 
71
92
  prepare(sql) {
@@ -74,6 +95,7 @@ class Database {
74
95
 
75
96
  // Run one or more `;`-separated statements with no bound parameters.
76
97
  exec(sql) {
98
+ this._assertOpen();
77
99
  for (const stmt of sql.split(";").map((s) => s.trim()).filter(Boolean)) {
78
100
  this._db.query(stmt);
79
101
  }
@@ -82,11 +104,13 @@ class Database {
82
104
 
83
105
  // The most recent committed transaction id (upper bound for `... BEFORE tx`).
84
106
  get currentTx() {
107
+ this._assertOpen();
85
108
  return this._db.currentTx();
86
109
  }
87
110
 
88
111
  // Export the database as a `.pvdb` byte image (Uint8Array).
89
112
  serialize() {
113
+ this._assertOpen();
90
114
  return this._db.export();
91
115
  }
92
116
 
@@ -94,19 +118,23 @@ class Database {
94
118
  throw new Error("picovolt: pragma is not supported");
95
119
  }
96
120
 
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.
121
+ // Wrap a synchronous callback in an engine transaction, matching
122
+ // better-sqlite3's common pattern.
99
123
  transaction(fn) {
100
124
  if (typeof fn !== "function") throw new TypeError("transaction expects a function");
125
+ this._assertOpen();
101
126
  const db = this;
102
127
  function wrapped(...args) {
128
+ db._assertOpen();
103
129
  if (db._inTransaction) return fn(...args);
104
- const snapshot = db.serialize();
130
+ db._db.beginTransaction();
105
131
  db._inTransaction = true;
106
132
  try {
107
- return fn(...args);
133
+ const value = fn(...args);
134
+ db._db.commitTransaction();
135
+ return value;
108
136
  } catch (error) {
109
- db._db = Db.fromBytes(snapshot);
137
+ if (db._db.inTransaction()) db._db.rollbackTransaction();
110
138
  throw error;
111
139
  } finally {
112
140
  db._inTransaction = false;
@@ -119,7 +147,14 @@ class Database {
119
147
  }
120
148
 
121
149
  close() {
122
- /* the WebAssembly instance is reclaimed by the GC */
150
+ if (this._closed) return;
151
+ for (const statement of [...this._statements]) statement.close();
152
+ this._db.free();
153
+ this._closed = true;
154
+ }
155
+
156
+ _assertOpen() {
157
+ if (this._closed) throw new Error("PicoVolt database is closed");
123
158
  }
124
159
  }
125
160
 
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: