facetful 0.1.0 → 0.3.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
@@ -14,6 +14,9 @@ browser** — facet counts, pivots, top-k, filters — at interaction speed.
14
14
  did-you-mean hints.
15
15
  - **Larger-than-memory**: tables can live in OPFS and load column segments
16
16
  lazily through a byte-budgeted LRU cache.
17
+ - **Filter-mask cache**: WHERE conjuncts are cached as per-row-group bitmaps,
18
+ so facet bursts sharing a filter evaluate it once, and a `LIKE '%needle%'`
19
+ extending a cached needle verifies only the rows the shorter one matched.
17
20
  - Dates, medians, stddev, group_concat, `select *`, LIKE fast paths — the
18
21
  boring things work.
19
22
 
@@ -77,7 +80,9 @@ count(distinct), median, stddev, group_concat. Scalars: math (abs, round,
77
80
  floor, ceil, sqrt, pow, exp, ln, sign), text (lower, upper, length, substr,
78
81
  trim/ltrim/rtrim, replace, instr, concat), null handling (coalesce, ifnull,
79
82
  nullif), temporal (year, month, day, hour, minute, second, date, timestamp,
80
- strftime). Quote column names with spaces: `"Capacity (MW)"`.
83
+ strftime). `GROUP BY` and `ORDER BY` accept select aliases or 1-based
84
+ positions. Numbers may use exponents (`1e6`). Quote column names with spaces:
85
+ `"Capacity (MW)"`.
81
86
 
82
87
  ## Building the wasm from source
83
88
 
package/core.js CHANGED
@@ -3,6 +3,13 @@
3
3
 
4
4
  const KINDS = { 1: "int", 2: "float", 3: "bool", 4: "text", 5: "date", 6: "timestamp" };
5
5
 
6
+ // wasm32 pointers and lengths are u32, but exports returning them arrive in JS
7
+ // as signed i32. Once linear memory grows past 2 GB (large parquet compiles do
8
+ // this) every pointer above the halfway mark is negative and the typed-array
9
+ // constructor throws "Start offset ... is outside the bounds of the buffer".
10
+ // Re-interpret as unsigned before building any view over memory.
11
+ const u32 = (n) => n >>> 0;
12
+
6
13
  // The wasm module declares one import: env.opfs_read(fileId, offset, len, destPtr)
7
14
  // -> bytes read. The browser worker supplies a real implementation over OPFS
8
15
  // sync access handles; environments without OPFS (Node smoke test) get a stub
@@ -13,7 +20,7 @@ export async function instantiate(wasmBytes, opfsRead) {
13
20
  opfs_read: (fileId, offset, len, destPtr) => {
14
21
  if (!opfsRead || !engine) return -1;
15
22
  // the view must be built per call: memory.buffer detaches on growth
16
- return opfsRead(fileId, offset, new Uint8Array(engine.mem(), destPtr, len));
23
+ return opfsRead(fileId, offset, new Uint8Array(engine.mem(), u32(destPtr), u32(len)));
17
24
  },
18
25
  };
19
26
  const { instance } = await WebAssembly.instantiate(wasmBytes, { env });
@@ -24,7 +31,7 @@ export async function instantiate(wasmBytes, opfsRead) {
24
31
  export class Engine {
25
32
  constructor(exports) {
26
33
  this.w = exports;
27
- this.scratch = this.w.alloc(4096);
34
+ this.scratch = u32(this.w.alloc(4096));
28
35
  this.enc = new TextEncoder();
29
36
  this.dec = new TextDecoder();
30
37
  }
@@ -35,7 +42,7 @@ export class Engine {
35
42
  /** Open a .facetful image from bytes; returns a table handle. */
36
43
  openTable(bytes) {
37
44
  const src = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
38
- const ptr = this.w.alloc(src.byteLength);
45
+ const ptr = u32(this.w.alloc(src.byteLength));
39
46
  new Uint8Array(this.mem(), ptr, src.byteLength).set(src);
40
47
  const handle = this.w.table_open(ptr, src.byteLength);
41
48
  if (!handle) throw new Error("not a valid .facetful image");
@@ -60,6 +67,11 @@ export class Engine {
60
67
  return this.w.table_warm(tableHandle, colIdx);
61
68
  }
62
69
 
70
+ /** Filter-mask cache byte budget for a table; 0 disables it. */
71
+ setMaskBudget(tableHandle, bytes) {
72
+ this.w.table_set_mask_budget(tableHandle, bytes);
73
+ }
74
+
63
75
  /** { segments, bytes } currently cached. */
64
76
  cacheStats(tableHandle) {
65
77
  const packed = this.w.table_cache_stats(tableHandle);
@@ -77,7 +89,7 @@ export class Engine {
77
89
  */
78
90
  compileTable(rows, columns, { groupTarget = 65536 } = {}) {
79
91
  const put = (src) => {
80
- const p = this.w.alloc(src.byteLength);
92
+ const p = u32(this.w.alloc(src.byteLength));
81
93
  new Uint8Array(this.mem(), p, src.byteLength).set(
82
94
  new Uint8Array(src.buffer, src.byteOffset, src.byteLength),
83
95
  );
@@ -111,7 +123,7 @@ export class Engine {
111
123
 
112
124
  /** Copy a compiled image's bytes out (for OPFS persistence). */
113
125
  imageBytes(img) {
114
- return new Uint8Array(this.mem(), this.w.image_ptr(img), this.w.image_len(img)).slice();
126
+ return new Uint8Array(this.mem(), u32(this.w.image_ptr(img)), u32(this.w.image_len(img))).slice();
115
127
  }
116
128
 
117
129
  /** Open a table over a compiled image; consumes the image handle (no copy). */
@@ -124,7 +136,7 @@ export class Engine {
124
136
  /** Run SQL; returns { columns, rowCount, stats } with copied-out buffers. */
125
137
  query(tableHandle, sql) {
126
138
  const sqlBytes = this.enc.encode(sql);
127
- const sqlPtr = this.w.alloc(sqlBytes.byteLength);
139
+ const sqlPtr = u32(this.w.alloc(sqlBytes.byteLength));
128
140
  new Uint8Array(this.mem(), sqlPtr, sqlBytes.byteLength).set(sqlBytes);
129
141
  const h = this.w.query_run(tableHandle, sqlPtr, sqlBytes.byteLength);
130
142
  try {
@@ -141,17 +153,17 @@ export class Engine {
141
153
  const nameLen = this.w.col_name(h, i, this.scratch, 4096);
142
154
  const name = this.dec.decode(new Uint8Array(this.mem(), this.scratch, nameLen));
143
155
  const validity = new Uint8Array(
144
- this.mem(), this.w.col_validity_ptr(h, i), Math.ceil(rowCount / 8),
156
+ this.mem(), u32(this.w.col_validity_ptr(h, i)), Math.ceil(rowCount / 8),
145
157
  ).slice();
146
158
  const col = { name, kind, validity };
147
159
  if (kind === "int" || kind === "float" || kind === "date" || kind === "timestamp") {
148
- col.values = new Float64Array(this.mem(), this.w.col_f64_ptr(h, i), rowCount).slice();
160
+ col.values = new Float64Array(this.mem(), u32(this.w.col_f64_ptr(h, i)), rowCount).slice();
149
161
  } else if (kind === "bool") {
150
- col.values = new Uint8Array(this.mem(), this.w.col_bools_ptr(h, i), rowCount).slice();
162
+ col.values = new Uint8Array(this.mem(), u32(this.w.col_bools_ptr(h, i)), rowCount).slice();
151
163
  } else {
152
- col.offsets = new Uint32Array(this.mem(), this.w.col_offsets_ptr(h, i), rowCount + 1).slice();
164
+ col.offsets = new Uint32Array(this.mem(), u32(this.w.col_offsets_ptr(h, i)), rowCount + 1).slice();
153
165
  col.bytes = new Uint8Array(
154
- this.mem(), this.w.col_bytes_ptr(h, i), this.w.col_bytes_len(h, i),
166
+ this.mem(), u32(this.w.col_bytes_ptr(h, i)), u32(this.w.col_bytes_len(h, i)),
155
167
  ).slice();
156
168
  }
157
169
  columns.push(col);
Binary file
package/index.d.ts CHANGED
@@ -113,6 +113,9 @@ export declare class Facetful {
113
113
  /** Current segment-cache occupancy for a table. */
114
114
  cacheStats(options?: { table?: string }): Promise<{ segments: number; bytes: number }>;
115
115
 
116
+ /** Filter-mask cache byte budget for a table (default 16 MB); 0 disables it. */
117
+ setMaskBudget(bytes: number, options?: { table?: string }): Promise<void>;
118
+
116
119
  /**
117
120
  * Run SQL (SELECT-only; the table is always `t`). `table` picks a loaded
118
121
  * table by name, defaulting to the most recently loaded. Rejects with an
package/index.js CHANGED
@@ -103,6 +103,11 @@ export class Facetful {
103
103
  return { segments, bytes };
104
104
  }
105
105
 
106
+ /** Filter-mask cache byte budget for a table (default 16 MB); 0 disables it. */
107
+ async setMaskBudget(bytes, { table } = {}) {
108
+ await this._call({ cmd: "setMaskBudget", bytes, table });
109
+ }
110
+
106
111
  /** Run SQL. `table` selects a loaded table (defaults to the last loaded). */
107
112
  async query(sql, { table } = {}) {
108
113
  const { result } = await this._call({ cmd: "query", sql, table });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facetful",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Tiny columnar SQL engine for browser faceting — Rust/wasm, ~170KB gz, zero dependencies. Open Parquet instantly, facet 1M rows in milliseconds, persist to OPFS.",
5
5
  "license": "MIT",
6
6
  "author": "David Raznick",
package/worker.js CHANGED
@@ -191,6 +191,11 @@ self.onmessage = async (e) => {
191
191
  await new Promise((r) => setTimeout(r, 0));
192
192
  }
193
193
  reply({ ok: true, bytes });
194
+ } else if (cmd === "setMaskBudget") {
195
+ const handle = e.data.table ? tables.get(e.data.table) : lastTable;
196
+ if (!handle) throw new Error("no table loaded");
197
+ engine.setMaskBudget(handle, e.data.bytes);
198
+ reply({ ok: true });
194
199
  } else if (cmd === "cacheStats") {
195
200
  const handle = e.data.table ? tables.get(e.data.table) : lastTable;
196
201
  if (!handle) throw new Error("no table loaded");