facetful 0.1.0 → 0.2.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 +6 -1
- package/core.js +23 -11
- package/facetful_wasm.wasm +0 -0
- package/index.d.ts +3 -0
- package/index.js +5 -0
- package/package.json +1 -1
- package/worker.js +5 -0
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).
|
|
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);
|
package/facetful_wasm.wasm
CHANGED
|
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.
|
|
3
|
+
"version": "0.2.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");
|