picovolt 1.3.0 → 1.4.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.1.0-blue.svg)](CHANGELOG.md)
4
+ [![Version](https://img.shields.io/badge/version-1.4.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)
@@ -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 103 unit and
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,7 +52,7 @@ 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 mmap monolith, `bake` |
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) |
@@ -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 caps WASM resource counts. The decoders are fuzzed (a cross-platform
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 advisories. Both run in CI. See [SECURITY.md](SECURITY.md).
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,7 +112,13 @@ 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
- SQL supported: `CREATE TABLE`, `CREATE INDEX ON t (col)`, `INSERT`,
115
+ Install the first-class CLI with `cargo install picovolt`, then use `pv query`,
116
+ `pv inspect`, `pv import`, `pv export`, and `pv bake`. Copyable Rust, Python, Go,
117
+ 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` with `PRIMARY KEY`, `UNIQUE`, and `NOT NULL`,
121
+ `CREATE [UNIQUE] INDEX ON t (col)`, `INSERT`,
113
122
  `UPDATE ... SET ... WHERE`, `DELETE ... WHERE`, `DROP TABLE`, 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]
@@ -117,6 +126,9 @@ FROM t [WHERE <pred>] [GROUP BY cols] [HAVING <pred>] [BEFORE tx]
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
+ Basic `SELECT * FROM a [INNER|LEFT] JOIN b ON a_key = b_key` equality joins are
130
+ also supported. Rust callers can cache `Database::prepare(...)` templates and use
131
+ atomic `Database::transaction(...)` closures with in-memory databases.
120
132
  Durability is selectable via `Database::set_durability` (`Fast` OS-cache default,
121
133
  or crash-safe `Sync` with fsync and an atomic manifest).
122
134
 
@@ -126,9 +138,9 @@ around 33k rows/s, linear), larger-than-RAM reads through a bounded buffer pool
126
138
  667-page dataset serves from a 16-page pool), ordered secondary indexes (point
127
139
  lookups roughly 11,000 times faster than a scan, plus range predicates), MVCC
128
140
  time-travel, opt-in crash-safe durability (`Durability::Sync`), and a fast
129
- compile-and-publish path (CAS dedup, columnar compression, single-file mmap
130
- artifacts). Current limits: indexes are in-memory (rebuilt on open) and there is
131
- no concurrency.
141
+ compile-and-publish path (CAS dedup, columnar compression, memory-mappable
142
+ single-file artifacts). Current limits include no filesystem `BEGIN`/`COMMIT`
143
+ transactions, only basic two-table equality joins, and no concurrent writers.
132
144
 
133
145
  ## Install and distribution
134
146
 
@@ -136,10 +148,12 @@ no concurrency.
136
148
  |--------|-----|
137
149
  | **Rust** (crates.io) | `cargo add picovolt` |
138
150
  | **JavaScript / npm** (WebAssembly, browser and Node) | `npm install picovolt` |
139
- | **C / Go / Python** (native, via the C ABI) | `cargo build --release --features capi`, then see [`bindings/`](bindings) |
151
+ | **Python** (native wheels) | `python -m pip install picovolt` (after the first PyPI release) |
152
+ | **C / Go** (native, via the C ABI) | `cargo build --release --features capi`, then see [`bindings/`](bindings) |
140
153
  | **In-memory** (native, no filesystem) | `Database::open_memory()`, export with `bake_to_bytes()` |
141
154
 
142
- PicoVolt runs in the browser through its in-memory backend. Build the WebAssembly
155
+ PicoVolt runs in the browser through its in-memory backend plus an OPFS persistence
156
+ wrapper and Web Worker endpoint. Build the WebAssembly
143
157
  package with `wasm-pack build --target bundler --release -- --features wasm`, then
144
158
  `import { Db } from "picovolt"` and run SQL with `db.query(...)`. See
145
159
  [src/wasm_api.rs](src/wasm_api.rs) for the JavaScript surface.
@@ -155,9 +169,9 @@ All bindings accept positional `?` parameters
155
169
  a familiar surface, drop-in adapters are provided: a `better-sqlite3`-style
156
170
  JavaScript API (`import Database from "picovolt/sqlite"`), a Python DB-API 2.0
157
171
  module (`import picovolt.dbapi2 as sqlite`), and the Go `database/sql` driver
158
- ([`bindings/go/pvsql`](bindings/go/pvsql)). Shared limits across all of them:
159
- positional `?` only, no SQL transactions, no JOINs, and `CREATE TABLE` takes
160
- column names only.
172
+ ([`bindings/go/pvsql`](bindings/go/pvsql)). Shared limits include positional `?`
173
+ only and the intentionally compact SQL grammar; JavaScript and in-memory Rust
174
+ also expose rollback-capable transaction wrappers.
161
175
 
162
176
  ## Server mode
163
177
 
@@ -169,13 +183,22 @@ thread over a channel, so the single-threaded core is unchanged.
169
183
  ```sh
170
184
  cargo build --release --features server
171
185
  ./target/release/picovolt-server --memory --addr 127.0.0.1:8080
172
- curl -s localhost:8080/v1/query -d '{"sql":"SELECT 1 + 1","params":[]}'
186
+ curl -s localhost:8080/v1/query \
187
+ -H 'Content-Type: application/json' \
188
+ -d '{"sql":"CREATE TABLE demo (value)","params":[]}'
173
189
  ```
174
190
 
175
- Endpoints are `POST /v1/query`, `GET /v1/tx`, and `GET /v1/health`. There is no
176
- authentication or TLS, so run it behind a reverse proxy. See
191
+ Endpoints are `POST /v1/query`, `GET /v1/tx`, and `GET /v1/health`. Loopback use
192
+ may omit authentication. A non-loopback bind is refused unless a bearer token is
193
+ provided with `--token-file` or `PICOVOLT_SERVER_TOKEN`; send it as
194
+ `Authorization: Bearer ...`. Query bodies, queues, execution time, rows scanned,
195
+ result rows, and response size are bounded. TLS is not built in, so network
196
+ deployments still belong behind a TLS-terminating reverse proxy. See
177
197
  [src/bin/server.rs](src/bin/server.rs).
178
198
 
199
+ Applications accepting SQL from users can also call `Database::query_with_limits`
200
+ directly and choose their own scan, result, memory, and deadline budgets.
201
+
179
202
  ## Extending PicoVolt
180
203
 
181
204
  There are two extension paths: sandboxed WebAssembly user-defined functions, and
@@ -187,6 +210,7 @@ native modules built on the public API. Both are documented in
187
210
  | | |
188
211
  |--|--|
189
212
  | Roadmap | [ROADMAP.md](ROADMAP.md) |
213
+ | One-million-download plan | [docs/ROADMAP_1M_DOWNLOADS.md](docs/ROADMAP_1M_DOWNLOADS.md) |
190
214
  | Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
191
215
  | Code of conduct | [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) |
192
216
  | 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.3.0",
5
+ "version": "1.4.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 __wbg___wbindgen_boolean_get_fa956cfa2d1bd751(arg0) {
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 __wbg___wbindgen_debug_string_c25d447a39f5578f(arg0, arg1) {
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 __wbg___wbindgen_is_null_ea9085d691f535d3(arg0) {
150
+ export function __wbg___wbindgen_is_null_7d13f41e1a2d5140(arg0) {
151
151
  const ret = arg0 === null;
152
152
  return ret;
153
153
  }
154
- export function __wbg___wbindgen_is_undefined_c05833b95a3cf397(arg0) {
154
+ export function __wbg___wbindgen_is_undefined_6cff064c44e0d823(arg0) {
155
155
  const ret = arg0 === undefined;
156
156
  return ret;
157
157
  }
158
- export function __wbg___wbindgen_number_get_394265ed1e1b84ee(arg0, arg1) {
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 __wbg___wbindgen_string_get_b0ca35b86a603356(arg0, arg1) {
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 __wbg___wbindgen_throw_344f42d3211c4765(arg0, arg1) {
172
+ export function __wbg___wbindgen_throw_bb96b2010945f0bc(arg0, arg1) {
173
173
  throw new Error(getStringFromWasm0(arg0, arg1));
174
174
  }
175
- export function __wbg_call_e3b662382210db98() { return handleError(function (arg0, arg1, arg2, arg3) {
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 __wbg_error_a6fa202b58aa1cd3(arg0, arg1) {
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 __wbg_from_13e323c65fc8f464(arg0) {
190
+ export function __wbg_from_74f3d90e0ff11240(arg0) {
191
191
  const ret = Array.from(arg0);
192
192
  return ret;
193
193
  }
194
- export function __wbg_get_unchecked_6e0ad6d2a41b06f6(arg0, arg1) {
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 __wbg_instanceof_Uint8Array_309b927aaf7a3fc7(arg0) {
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 __wbg_length_1f0964f4a5e2c6d8(arg0) {
208
+ export function __wbg_length_36bd29c6848c2144(arg0) {
209
209
  const ret = arg0.length;
210
210
  return ret;
211
211
  }
212
- export function __wbg_length_370319915dc99107(arg0) {
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 __wbg_prototypesetcall_4770620bbe4688a0(arg0, arg1, arg2) {
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); there are no transactions; blob parameters are unsupported.
12
+ // supported); blob parameters are unsupported.
13
13
 
14
14
  import { Db } from "./picovolt.js";
15
15
 
16
16
  function rowToObject(columns, row) {
17
- const obj = {};
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
- transaction() {
95
- throw new Error("picovolt: transactions are not supported");
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
+ });