picovolt 1.6.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.6.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)
@@ -122,20 +122,18 @@ Install the first-class CLI with `cargo install picovolt`, then use `pv query`,
122
122
  Rust, Python, Go, Node, and browser projects are in [`starters/`](starters/README.md); supported adapters
123
123
  are catalogued in [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md).
124
124
 
125
- SQL supported: `CREATE TABLE [IF NOT EXISTS]` with `PRIMARY KEY`, `UNIQUE`, and `NOT NULL`,
126
- `CREATE [UNIQUE] INDEX ON t (col)`, single- and multi-row `INSERT`,
127
- `UPDATE ... SET ... WHERE`, `DELETE ... WHERE`, `DROP TABLE [IF EXISTS]`, and
128
- `SELECT [DISTINCT] {* | col [AS alias], ... | COUNT/SUM/MIN/MAX/AVG(...) [AS alias]}
129
- FROM t [WHERE <pred>] [GROUP BY cols] [HAVING <pred>] [BEFORE tx]
130
- [ORDER BY col [ASC|DESC], ...] [LIMIT n] [OFFSET n]`, where `<pred>` combines
131
- `col <op> value` (`=`, `!=`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`),
132
- `col [NOT] IN (...)`, `col [NOT] BETWEEN a AND b`, and `col IS [NOT] NULL` with
133
- `AND`, `OR`, and parentheses. Integer and decimal values compare by magnitude.
134
- Two-table equality `INNER`/`LEFT JOIN` queries support qualified references,
135
- projection, aliases, `DISTINCT`, filters, ordering, and pagination. Rust callers
136
- can cache `Database::prepare(...)` templates and use explicit transaction
137
- lifecycle methods or atomic `Database::transaction(...)` closures with both
138
- filesystem and 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.
139
137
  Durability is selectable via `Database::set_durability` (`Fast` OS-cache default,
140
138
  or crash-safe `Sync` with fsync and an atomic manifest).
141
139
 
@@ -147,8 +145,8 @@ lookups roughly 11,000 times faster than a scan, plus range predicates), MVCC
147
145
  time-travel, opt-in crash-safe durability (`Durability::Sync`), and a fast
148
146
  compile-and-publish path (CAS dedup, columnar compression, memory-mappable
149
147
  single-file artifacts). Current limits include full-workspace transaction
150
- backups rather than an incremental WAL, only basic two-table equality joins,
151
- and no concurrent writers.
148
+ backups rather than an incremental WAL, left-deep equality joins rather than a
149
+ general SQL planner, and no concurrent writers.
152
150
 
153
151
  ## Install and distribution
154
152
 
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.6.0",
5
+ "version": "1.7.0",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
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
@@ -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
@@ -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
@@ -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
 
@@ -98,8 +122,10 @@ class Database {
98
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
130
  db._db.beginTransaction();
105
131
  db._inTransaction = true;
@@ -121,7 +147,14 @@ class Database {
121
147
  }
122
148
 
123
149
  close() {
124
- /* 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");
125
158
  }
126
159
  }
127
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: