diff --git a/examples/README.md b/examples/README.md index c98ea78..8a8ad6f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,8 @@ Beyond the per-SDK quick-start tours above, the [SQLR-38 umbrella](../docs/roadm |---|---|---|---| | LLM agent with persistent memory | Python | Vector + lexical recall, fact extraction, summaries — all in one `.sqlrite` file | [`python-agent/`](python-agent/) | | Chat with your notes (MCP) | Node.js | Markdown → SQLRite hybrid retrieval, served to Claude Desktop via `sqlrite-mcp --read-only` | [`nodejs-notes/`](nodejs-notes/) | +| Local-first journaling | Tauri 2 + Svelte 5 | Markdown daily notes in one `.sqlrite` file, BM25 full-text search, "ask my journal" natural-language SQL | [`desktop-journal/`](desktop-journal/) | +| Browser SQL playground | WASM | The full engine in WebAssembly — SQL editor, sample datasets, HNSW vector search, all in a browser tab. Live at [sqlritedb.com/playground](https://sqlritedb.com/playground) | [`wasm-playground/`](wasm-playground/) | ## Running the Rust quickstart diff --git a/examples/wasm-playground/README.md b/examples/wasm-playground/README.md new file mode 100644 index 0000000..0d4c3cd --- /dev/null +++ b/examples/wasm-playground/README.md @@ -0,0 +1,163 @@ +# Browser SQL playground (WebAssembly) + +> **Live:** **https://sqlritedb.com/playground** + +A zero-install, browser-only SQL playground that runs the **full SQLRite +engine entirely in WebAssembly** — no server, no account, nothing to +install. Type SQL, hit Run, see results. Load a sample dataset and poke at +JOINs, `GROUP BY`, and HNSW cosine vector search right on the page. Your +data never leaves the browser tab. + +This is example app #4 of the [SQLR-38 example-apps umbrella](../README.md). +It's the highest-marketing-leverage example — a "try it in your browser" +link is the fastest path from a curious dev to a running query. + +> [!NOTE] +> Unlike the other examples (which are standalone runnable directories), +> the playground's home is the marketing site itself. The implementation +> lives in [`web/src/app/playground/`](../../web/src/app/playground/) so it +> can ship as a route on https://sqlritedb.com and reuse the site's nav, +> footer, theme, and SEO plumbing. This directory is the canonical README + +> architecture write-up. For a bare, framework-free version of the same +> idea, see the minimal vanilla-HTML demo in [`examples/wasm/`](../wasm/). + +## What it shows off + +| Feature | How | +|---|---| +| **WASM SDK** (`@joaoh82/sqlrite-wasm`) | The same `sqlrite-engine` crate every SDK uses, built for `wasm32-unknown-unknown`. ~750 KB gzipped, fetched once. | +| **HNSW vector search** | The "Movies" dataset has 12 films with hand-made 4-dim embeddings + a `USING hnsw (embedding) WITH (metric = 'cosine')` index; the sample query ranks them with `vec_distance_cosine` in `ORDER BY`. | +| **JOINs** | The "Northwind" dataset joins 4 tables (order lines → orders → customers → products). | +| **Aggregates / filters** | The "Pokémon" dataset for `WHERE`, `ORDER BY`, `GROUP BY`, `COUNT` / `AVG`. | +| **Transactions** | `BEGIN` / `COMMIT` / `ROLLBACK` work like any other statement. | + +## Feature set + +- Multi-line SQL editor (CodeMirror 6) with SQL syntax highlighting. +- **Run** button + **Cmd/Ctrl+Enter** shortcut. Multi-statement scripts run + top to bottom; the last `SELECT`'s rows are shown. +- Results grid with **per-column types**, **NULL highlighting**, and **CSV + export**. +- **Sample datasets** dropdown — schema + data + an example query in one + click. +- **Reset DB** — back to a fresh in-memory database. +- **Share** — encodes the editor SQL into the URL hash (`#sql=…`) and copies + a shareable link. +- **Persistence** — your session survives reloads (see below). +- **Download / Upload `.sql`** — take your session script with you, or load + someone else's. + +## Architecture + +``` + Browser tab + ┌──────────────────────────────────────────────────────────────┐ + │ /playground (Next.js route, statically prerendered) │ + │ │ + │ PlaygroundLoader ── dynamic(ssr:false) ──▶ Playground │ + │ │ │ + │ ┌─────────────────────────────────────────┤ │ + │ ▼ ▼ ▼ │ + │ CodeMirror 6 ResultsPane lib/wasm.ts │ + │ (SQL editor) (table + CSV) │ │ + │ ▼ │ + │ runtime import("/playground/pkg/ │ + │ sqlrite_wasm.js") │ + │ │ │ + │ ▼ │ + │ sqlrite_wasm_bg.wasm (the engine) │ + │ new Database() · exec() · query() │ + └──────────────────────────────────────────────────────────────┘ + persistence: OPFS ─▶ fallback localStorage ─▶ none +``` + +**Key files** (under [`web/src/app/playground/`](../../web/src/app/playground/)): + +- `page.tsx` — server component: SEO metadata, JSON-LD, the static shell. +- `PlaygroundLoader.tsx` — `dynamic(..., { ssr: false })` so CodeMirror + + the WASM engine stay out of SSR and the initial bundle. +- `Playground.tsx` — the orchestrator (state, run loop, datasets, share, + persistence, download/upload). +- `components/Editor.tsx` — CodeMirror 6 wrapper (theme, SQL mode, + Cmd/Ctrl+Enter keymap). +- `components/ResultsPane.tsx` — results table, NULL/type rendering, CSV. +- `lib/wasm.ts` — loads + initialises the WASM module exactly once. +- `lib/sql.ts` — quote/comment-aware statement splitter, SELECT detection, + CSV. +- `lib/datasets.ts` — the three sample datasets. +- `lib/persist.ts` — OPFS / localStorage session storage + share-hash codec. + +The WASM artifact is a **pinned, vendored copy** of `sdk/wasm/pkg/` in +[`web/public/playground/pkg/`](../../web/public/playground/pkg/), served as a +static asset. It is loaded with a *runtime* `import()` (kept external via +`/* webpackIgnore */`) rather than bundled, because wasm-pack's glue fetches +its sibling `.wasm` via `import.meta.url` — a shape the Next bundler mangles. + +### How persistence works (script replay) + +The WASM build is **in-memory only** — there is no `.sqlrite` byte image to +save. So instead of persisting the *database*, the playground persists the +*script*: every mutating statement (`CREATE` / `INSERT` / `UPDATE` / +`DELETE` / tx control) you successfully run is appended to a session log. On +reload, that log is replayed into a fresh in-memory database, reconstructing +the same state. **Download .sql** hands you that exact replayable script. + +Storage backend, in priority order: + +1. **OPFS** (Origin Private File System) — `session.sql` + `editor.sql`. +2. **localStorage** — when OPFS write access isn't available. +3. **none** — locked-down / private contexts; the playground still works, + it just won't survive a reload. + +## Running locally + +The playground is part of the website app: + +```sh +cd web +npm install +npm run dev # http://localhost:3000/playground +``` + +To refresh the vendored WASM bundle after changing the engine or the WASM +SDK: + +```sh +cd sdk/wasm +wasm-pack build --target web --release --out-dir pkg +cp pkg/sqlrite_wasm.js pkg/sqlrite_wasm_bg.wasm pkg/sqlrite_wasm.d.ts \ + pkg/sqlrite_wasm_bg.wasm.d.ts ../../web/public/playground/pkg/ +``` + +CI already builds the WASM target (`wasm-build` job in +[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)) and reports its +gzipped size; the website deploys to Vercel. + +## Bundle size + +The WASM module is **~750 KB gzipped** (~2.1 MB raw) — comfortably under the +4 MB budget the umbrella set. Shrinking it further (feature-gating unused +engine pieces, `wasm-opt` passes) is a deliberate follow-up, not a blocker. + +## Known limitations + +- **No binary `.sqlrite` export.** The WASM build is in-memory only and the + SDK exposes no serialise/deserialise. Persistence is via SQL-script replay + (above); binary round-trip needs an engine change (an in-memory storage + backend + `Database.export()/import()` on the SDK) and is tracked as a + follow-up. +- **OPFS support varies.** Safari added OPFS write support relatively late, + and some private-browsing modes block it. The playground feature-detects + and falls back to `localStorage`, then to no persistence. +- **No `ask` (natural-language → SQL).** That needs a server-side API key, + which a static browser page can't safely hold. It ships in the REPL, MCP + server, and desktop app instead. +- **Engine gaps surface as errors.** Aggregates over JOIN results and a few + other shapes aren't implemented yet — they return a normal engine error in + the results pane. See [`docs/supported-sql.md`](../../docs/supported-sql.md). + +## Browser support + +Latest two majors of **Chrome, Firefox, and Safari**. Requires WebAssembly +and ES modules (both ~universally available since 2018). Persistence quality +degrades gracefully on browsers without OPFS write support. diff --git a/sdk/wasm/Cargo.lock b/sdk/wasm/Cargo.lock index 3ae0019..38cd7d9 100644 --- a/sdk/wasm/Cargo.lock +++ b/sdk/wasm/Cargo.lock @@ -365,11 +365,23 @@ checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" dependencies = [ "log", "recursive", + "sqlparser_derive", +] + +[[package]] +name = "sqlparser_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6dd45d8fc1c79299bfbb7190e42ccbbdf6a5f52e4a6ad98d92357ea965bd289" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "sqlrite-ask" -version = "0.1.23" +version = "0.10.2" dependencies = [ "serde", "serde_json", @@ -378,7 +390,7 @@ dependencies = [ [[package]] name = "sqlrite-engine" -version = "0.1.23" +version = "0.10.2" dependencies = [ "log", "prettytable-rs", @@ -389,7 +401,7 @@ dependencies = [ [[package]] name = "sqlrite-wasm" -version = "0.1.23" +version = "0.10.2" dependencies = [ "console_error_panic_hook", "js-sys", diff --git a/web/README.md b/web/README.md index ad2ae18..f603aed 100644 --- a/web/README.md +++ b/web/README.md @@ -16,6 +16,7 @@ extracted into its own repository later without rewrites. ## Pages - `/` — landing (hero with animated REPL, features, architecture, roadmap, SDK switcher, SQL surface, desktop showcase, blog series, footer) +- `/playground` — in-browser SQL playground: the full engine compiled to WebAssembly, with a CodeMirror editor, sample datasets, HNSW vector search, and OPFS session persistence. The WASM bundle is a pinned copy of `sdk/wasm/pkg/` vendored into `public/playground/pkg/`. See [`../examples/wasm-playground/README.md`](../examples/wasm-playground/README.md). - `/docs` — Getting Started page (sticky sidebar nav + on-page TOC) - `/blog` — index of long-form posts pulled from `content/blog/*.mdx` - `/blog/[slug]` — per-post detail page (MDX rendered server-side, `Article` JSON-LD, breadcrumb JSON-LD, dynamic OG image, prev/next navigation) diff --git a/web/package-lock.json b/web/package-lock.json index 8d76c5d..e723626 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,9 +8,14 @@ "name": "sqlrite-website", "version": "0.1.0", "dependencies": { + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.43.0", "@posthog/next": "^0.4.47", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "codemirror": "^6.0.2", "gray-matter": "^4.0.3", "lucide-react": "^0.469.0", "next": "^15.5.18", @@ -69,6 +74,101 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", + "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", + "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", + "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -828,6 +928,36 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@mdx-js/mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", @@ -3035,6 +3165,21 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", @@ -3093,6 +3238,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6655,6 +6806,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.18.tgz", "integrity": "sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.18", "@swc/helpers": "0.5.15", @@ -8136,6 +8288,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -8695,6 +8853,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/web/package.json b/web/package.json index 310ece7..46f35fb 100644 --- a/web/package.json +++ b/web/package.json @@ -10,9 +10,14 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.43.0", "@posthog/next": "^0.4.47", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "codemirror": "^6.0.2", "gray-matter": "^4.0.3", "lucide-react": "^0.469.0", "next": "^15.5.18", diff --git a/web/public/playground/pkg/sqlrite_wasm.d.ts b/web/public/playground/pkg/sqlrite_wasm.d.ts new file mode 100644 index 0000000..6fd6bd7 --- /dev/null +++ b/web/public/playground/pkg/sqlrite_wasm.d.ts @@ -0,0 +1,181 @@ +/* tslint:disable */ +/* eslint-disable */ + +export class AskPromptOptions { + free(): void; + [Symbol.dispose](): void; + /** + * Construct an empty options object. JS can mutate fields in + * place: `const opts = new AskPromptOptions(); opts.model = '...'`. + */ + constructor(); + /** + * Anthropic prompt-cache TTL on the schema block: `"5m"` + * (default), `"1h"`, or `"off"`. + */ + get cache_ttl(): string | undefined; + /** + * Anthropic prompt-cache TTL on the schema block: `"5m"` + * (default), `"1h"`, or `"off"`. + */ + set cache_ttl(value: string | null | undefined); + /** + * `max_tokens` for the LLM call (default: 1024). + */ + get max_tokens(): number | undefined; + /** + * `max_tokens` for the LLM call (default: 1024). + */ + set max_tokens(value: number | null | undefined); + /** + * Model ID (default: `"claude-sonnet-4-6"`). + */ + get model(): string | undefined; + /** + * Model ID (default: `"claude-sonnet-4-6"`). + */ + set model(value: string | null | undefined); +} + +/** + * A SQLRite database handle. Always in-memory in the WASM build. + * Drop the handle (set to `null` / let GC collect it) to free the + * underlying state. + */ +export class Database { + free(): void; + [Symbol.dispose](): void; + /** + * Parse an Anthropic API response (the full JSON the JS caller's + * fetch returned) back into `{ sql, explanation, usage }`. + * + * Pass the raw response body as a string. The parser: + * * Extracts the first text content block from `content[]`. + * * Reads token counts from `usage`. + * * Parses the model's text as JSON (tolerant to fenced / + * leading-prose shapes — see `sqlrite_ask::parse_response`). + * + * On parse failure (the model emitted unparseable text, or the + * API response was malformed), throws a JS Error with the + * underlying reason. + */ + askParse(raw_api_response: string): any; + /** + * Build the LLM-provider request payload for `question` against + * the current schema. Returns a JS object ready to POST to the + * caller's backend. + * + * ```js + * const payload = db.askPrompt('How many users?'); + * // → { model, max_tokens, system: [...], messages: [...] } + * const response = await fetch('/api/llm/complete', { + * method: 'POST', + * body: JSON.stringify(payload), + * }); + * const apiResponse = await response.json(); + * const result = db.askParse(JSON.stringify(apiResponse)); + * // → { sql, explanation, usage: {...} } + * ``` + * + * `options` (optional) accepts: + * * `model` — override the default `claude-sonnet-4-6`. + * * `maxTokens` — override the default `1024`. + * * `cacheTtl` — `"5m"` (default), `"1h"`, or `"off"`. + */ + askPrompt(question: string, options?: AskPromptOptions | null): any; + /** + * Number of columns in the projection of a SELECT. Useful when + * a caller wants to build their own UI column list without + * iterating rows. + */ + columns(sql: string): any; + /** + * Runs one SQL statement that doesn't produce rows (CREATE / + * INSERT / UPDATE / DELETE / BEGIN / COMMIT / ROLLBACK). For + * SELECT use [`query`]. + */ + exec(sql: string): void; + /** + * Creates an in-memory database. The only mode supported by + * the WASM build — file-backed mode isn't meaningful in a + * browser sandbox. + */ + constructor(); + /** + * Runs a SELECT and returns an array of row objects. Each + * object's keys are column names in projection order; values + * are typed JS primitives — `number` for Integer/Real, + * `string` for Text, `boolean` for Bool, `null` for NULL. + */ + query(sql: string): any; + /** + * Returns `true` while a `BEGIN … COMMIT/ROLLBACK` block is open. + */ + readonly inTransaction: boolean; + /** + * Always `false` in the WASM build (file-backed / read-only + * opens aren't exposed). Kept for API-shape parity with the + * Node.js SDK. + */ + readonly readonly: boolean; +} + +/** + * Runs once when the WASM module is first imported. Wires up + * `console.error`-backed panic reporting so a Rust panic shows a + * real stack trace in devtools instead of a generic "unreachable" + * trap. + */ +export function _init(): void; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_askpromptoptions_free: (a: number, b: number) => void; + readonly __wbg_database_free: (a: number, b: number) => void; + readonly __wbg_get_askpromptoptions_cache_ttl: (a: number) => [number, number]; + readonly __wbg_get_askpromptoptions_max_tokens: (a: number) => number; + readonly __wbg_get_askpromptoptions_model: (a: number) => [number, number]; + readonly __wbg_set_askpromptoptions_cache_ttl: (a: number, b: number, c: number) => void; + readonly __wbg_set_askpromptoptions_max_tokens: (a: number, b: number) => void; + readonly __wbg_set_askpromptoptions_model: (a: number, b: number, c: number) => void; + readonly _init: () => void; + readonly askpromptoptions_new: () => number; + readonly database_askParse: (a: number, b: number, c: number) => [number, number, number]; + readonly database_askPrompt: (a: number, b: number, c: number, d: number) => [number, number, number]; + readonly database_columns: (a: number, b: number, c: number) => [number, number, number]; + readonly database_exec: (a: number, b: number, c: number) => [number, number]; + readonly database_inTransaction: (a: number) => number; + readonly database_new: () => [number, number, number]; + readonly database_query: (a: number, b: number, c: number) => [number, number, number]; + readonly database_readonly: (a: number) => number; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/web/public/playground/pkg/sqlrite_wasm.js b/web/public/playground/pkg/sqlrite_wasm.js new file mode 100644 index 0000000..48f6997 --- /dev/null +++ b/web/public/playground/pkg/sqlrite_wasm.js @@ -0,0 +1,561 @@ +/* @ts-self-types="./sqlrite_wasm.d.ts" */ + +export class AskPromptOptions { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + AskPromptOptionsFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_askpromptoptions_free(ptr, 0); + } + /** + * Construct an empty options object. JS can mutate fields in + * place: `const opts = new AskPromptOptions(); opts.model = '...'`. + */ + constructor() { + const ret = wasm.askpromptoptions_new(); + this.__wbg_ptr = ret >>> 0; + AskPromptOptionsFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Anthropic prompt-cache TTL on the schema block: `"5m"` + * (default), `"1h"`, or `"off"`. + * @returns {string | undefined} + */ + get cache_ttl() { + const ret = wasm.__wbg_get_askpromptoptions_cache_ttl(this.__wbg_ptr); + let v1; + if (ret[0] !== 0) { + v1 = getStringFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + } + return v1; + } + /** + * `max_tokens` for the LLM call (default: 1024). + * @returns {number | undefined} + */ + get max_tokens() { + const ret = wasm.__wbg_get_askpromptoptions_max_tokens(this.__wbg_ptr); + return ret === 0x100000001 ? undefined : ret; + } + /** + * Model ID (default: `"claude-sonnet-4-6"`). + * @returns {string | undefined} + */ + get model() { + const ret = wasm.__wbg_get_askpromptoptions_model(this.__wbg_ptr); + let v1; + if (ret[0] !== 0) { + v1 = getStringFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + } + return v1; + } + /** + * Anthropic prompt-cache TTL on the schema block: `"5m"` + * (default), `"1h"`, or `"off"`. + * @param {string | null} [arg0] + */ + set cache_ttl(arg0) { + var ptr0 = isLikeNone(arg0) ? 0 : passStringToWasm0(arg0, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + wasm.__wbg_set_askpromptoptions_cache_ttl(this.__wbg_ptr, ptr0, len0); + } + /** + * `max_tokens` for the LLM call (default: 1024). + * @param {number | null} [arg0] + */ + set max_tokens(arg0) { + wasm.__wbg_set_askpromptoptions_max_tokens(this.__wbg_ptr, isLikeNone(arg0) ? 0x100000001 : (arg0) >>> 0); + } + /** + * Model ID (default: `"claude-sonnet-4-6"`). + * @param {string | null} [arg0] + */ + set model(arg0) { + var ptr0 = isLikeNone(arg0) ? 0 : passStringToWasm0(arg0, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + wasm.__wbg_set_askpromptoptions_model(this.__wbg_ptr, ptr0, len0); + } +} +if (Symbol.dispose) AskPromptOptions.prototype[Symbol.dispose] = AskPromptOptions.prototype.free; + +/** + * A SQLRite database handle. Always in-memory in the WASM build. + * Drop the handle (set to `null` / let GC collect it) to free the + * underlying state. + */ +export class Database { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + DatabaseFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_database_free(ptr, 0); + } + /** + * Parse an Anthropic API response (the full JSON the JS caller's + * fetch returned) back into `{ sql, explanation, usage }`. + * + * Pass the raw response body as a string. The parser: + * * Extracts the first text content block from `content[]`. + * * Reads token counts from `usage`. + * * Parses the model's text as JSON (tolerant to fenced / + * leading-prose shapes — see `sqlrite_ask::parse_response`). + * + * On parse failure (the model emitted unparseable text, or the + * API response was malformed), throws a JS Error with the + * underlying reason. + * @param {string} raw_api_response + * @returns {any} + */ + askParse(raw_api_response) { + const ptr0 = passStringToWasm0(raw_api_response, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.database_askParse(this.__wbg_ptr, ptr0, len0); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } + /** + * Build the LLM-provider request payload for `question` against + * the current schema. Returns a JS object ready to POST to the + * caller's backend. + * + * ```js + * const payload = db.askPrompt('How many users?'); + * // → { model, max_tokens, system: [...], messages: [...] } + * const response = await fetch('/api/llm/complete', { + * method: 'POST', + * body: JSON.stringify(payload), + * }); + * const apiResponse = await response.json(); + * const result = db.askParse(JSON.stringify(apiResponse)); + * // → { sql, explanation, usage: {...} } + * ``` + * + * `options` (optional) accepts: + * * `model` — override the default `claude-sonnet-4-6`. + * * `maxTokens` — override the default `1024`. + * * `cacheTtl` — `"5m"` (default), `"1h"`, or `"off"`. + * @param {string} question + * @param {AskPromptOptions | null} [options] + * @returns {any} + */ + askPrompt(question, options) { + const ptr0 = passStringToWasm0(question, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + let ptr1 = 0; + if (!isLikeNone(options)) { + _assertClass(options, AskPromptOptions); + ptr1 = options.__destroy_into_raw(); + } + const ret = wasm.database_askPrompt(this.__wbg_ptr, ptr0, len0, ptr1); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } + /** + * Number of columns in the projection of a SELECT. Useful when + * a caller wants to build their own UI column list without + * iterating rows. + * @param {string} sql + * @returns {any} + */ + columns(sql) { + const ptr0 = passStringToWasm0(sql, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.database_columns(this.__wbg_ptr, ptr0, len0); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } + /** + * Runs one SQL statement that doesn't produce rows (CREATE / + * INSERT / UPDATE / DELETE / BEGIN / COMMIT / ROLLBACK). For + * SELECT use [`query`]. + * @param {string} sql + */ + exec(sql) { + const ptr0 = passStringToWasm0(sql, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.database_exec(this.__wbg_ptr, ptr0, len0); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } + /** + * Returns `true` while a `BEGIN … COMMIT/ROLLBACK` block is open. + * @returns {boolean} + */ + get inTransaction() { + const ret = wasm.database_inTransaction(this.__wbg_ptr); + return ret !== 0; + } + /** + * Creates an in-memory database. The only mode supported by + * the WASM build — file-backed mode isn't meaningful in a + * browser sandbox. + */ + constructor() { + const ret = wasm.database_new(); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + this.__wbg_ptr = ret[0] >>> 0; + DatabaseFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Runs a SELECT and returns an array of row objects. Each + * object's keys are column names in projection order; values + * are typed JS primitives — `number` for Integer/Real, + * `string` for Text, `boolean` for Bool, `null` for NULL. + * @param {string} sql + * @returns {any} + */ + query(sql) { + const ptr0 = passStringToWasm0(sql, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.database_query(this.__wbg_ptr, ptr0, len0); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } + /** + * Always `false` in the WASM build (file-backed / read-only + * opens aren't exposed). Kept for API-shape parity with the + * Node.js SDK. + * @returns {boolean} + */ + get readonly() { + const ret = wasm.database_readonly(this.__wbg_ptr); + return ret !== 0; + } +} +if (Symbol.dispose) Database.prototype[Symbol.dispose] = Database.prototype.free; + +/** + * Runs once when the WASM module is first imported. Wires up + * `console.error`-backed panic reporting so a Rust panic shows a + * real stack trace in devtools instead of a generic "unreachable" + * trap. + */ +export function _init() { + wasm._init(); +} +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_Error_960c155d3d49e4c2: function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return ret; + }, + __wbg_String_8564e559799eccda: function(arg0, arg1) { + const ret = String(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_is_string_6df3bf7ef1164ed3: function(arg0) { + const ret = typeof(arg0) === 'string'; + return ret; + }, + __wbg___wbindgen_throw_6b64449b9b9ed33c: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }, + __wbg_new_227d7c05414eb861: function() { + const ret = new Error(); + return ret; + }, + __wbg_new_34d45cc8e36aaead: function() { + const ret = new Map(); + return ret; + }, + __wbg_new_682678e2f47e32bc: function() { + const ret = new Array(); + return ret; + }, + __wbg_new_aa8d0fa9762c29bd: function() { + const ret = new Object(); + return ret; + }, + __wbg_set_3bf1de9fab0cd644: function(arg0, arg1, arg2) { + arg0[arg1 >>> 0] = arg2; + }, + __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { + arg0[arg1] = arg2; + }, + __wbg_set_fde2cec06c23692b: function(arg0, arg1, arg2) { + const ret = arg0.set(arg1, arg2); + return ret; + }, + __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbindgen_cast_0000000000000001: function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0) { + // Cast intrinsic for `I64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_cast_0000000000000004: function(arg0) { + // Cast intrinsic for `U64 -> Externref`. + const ret = BigInt.asUintN(64, arg0); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./sqlrite_wasm_bg.js": import0, + }; +} + +const AskPromptOptionsFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_askpromptoptions_free(ptr >>> 0, 1)); +const DatabaseFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_database_free(ptr >>> 0, 1)); + +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasm; +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('sqlrite_wasm_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/web/public/playground/pkg/sqlrite_wasm_bg.wasm b/web/public/playground/pkg/sqlrite_wasm_bg.wasm new file mode 100644 index 0000000..b40de0a Binary files /dev/null and b/web/public/playground/pkg/sqlrite_wasm_bg.wasm differ diff --git a/web/public/playground/pkg/sqlrite_wasm_bg.wasm.d.ts b/web/public/playground/pkg/sqlrite_wasm_bg.wasm.d.ts new file mode 100644 index 0000000..cf14d4e --- /dev/null +++ b/web/public/playground/pkg/sqlrite_wasm_bg.wasm.d.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const __wbg_askpromptoptions_free: (a: number, b: number) => void; +export const __wbg_database_free: (a: number, b: number) => void; +export const __wbg_get_askpromptoptions_cache_ttl: (a: number) => [number, number]; +export const __wbg_get_askpromptoptions_max_tokens: (a: number) => number; +export const __wbg_get_askpromptoptions_model: (a: number) => [number, number]; +export const __wbg_set_askpromptoptions_cache_ttl: (a: number, b: number, c: number) => void; +export const __wbg_set_askpromptoptions_max_tokens: (a: number, b: number) => void; +export const __wbg_set_askpromptoptions_model: (a: number, b: number, c: number) => void; +export const _init: () => void; +export const askpromptoptions_new: () => number; +export const database_askParse: (a: number, b: number, c: number) => [number, number, number]; +export const database_askPrompt: (a: number, b: number, c: number, d: number) => [number, number, number]; +export const database_columns: (a: number, b: number, c: number) => [number, number, number]; +export const database_exec: (a: number, b: number, c: number) => [number, number]; +export const database_inTransaction: (a: number) => number; +export const database_new: () => [number, number, number]; +export const database_query: (a: number, b: number, c: number) => [number, number, number]; +export const database_readonly: (a: number) => number; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_start: () => void; diff --git a/web/src/app/examples/page.tsx b/web/src/app/examples/page.tsx index 951f8e0..9132dbf 100644 --- a/web/src/app/examples/page.tsx +++ b/web/src/app/examples/page.tsx @@ -72,6 +72,19 @@ const itemListJsonLd = { "A markdown daily-notes desktop app backed by a single .sqlrite file. Phase 8 BM25 full-text search with hit highlighting, click-to-filter tags, and an 'ask my journal' panel powered by the engine's natural-language SQL feature.", }, }, + { + "@type": "ListItem", + position: 4, + item: { + "@type": "WebApplication", + name: "Browser SQL playground — WASM", + url: `${SITE.url}/playground`, + applicationCategory: "DeveloperApplication", + operatingSystem: "Any (WebAssembly)", + description: + "The full SQLRite engine compiled to WebAssembly, running entirely in the browser. SQL editor, sample datasets, HNSW vector search, CSV export, and shareable links — no server, no install.", + }, + }, ], }; @@ -83,6 +96,8 @@ type Example = { language: string; repoPath: string; features: string[]; + /** Optional live URL (the playground is hosted on the site itself). */ + liveUrl?: string; /** Optional repo-relative path to a demo asset (GIF/MP4) rendered * on the card as a poster + link. Currently only the journal app * has one — see SQLR-41's Remotion composition. */ @@ -141,6 +156,23 @@ const EXAMPLES: Example[] = [ mp4Path: "examples/desktop-journal/docs/demo.mp4", }, }, + { + status: "shipped", + title: "Browser SQL playground — WebAssembly", + blurb: + "The full SQLRite engine compiled to WebAssembly, running entirely in a browser tab — no server, no install, no account. Type SQL, hit Run, see results. Load a Pokémon / Northwind / movies-with-embeddings dataset in one click, then poke at JOINs, GROUP BY, and HNSW cosine vector search right on the page.", + bullets: [ + "Same sqlrite-engine crate as every other SDK, built for wasm32 via @joaoh82/sqlrite-wasm (~750 KB gzipped, fetched once)", + "CodeMirror 6 editor with SQL highlighting + Cmd/Ctrl+Enter; results grid with column types, NULL highlighting, and CSV export", + "Vector-search demo dataset: 12 films with 4-dim embeddings + an HNSW index, ranked by vec_distance_cosine in the browser", + "Session persists to OPFS (localStorage fallback) via SQL-script replay; share a query by URL hash; download / upload .sql", + "Hosted on this site — open it live, no clone required", + ], + language: "WebAssembly", + repoPath: "examples/wasm-playground", + features: ["WASM SDK", "HNSW", "CodeMirror 6", "OPFS"], + liveUrl: "/playground", + }, ]; const pillStyle: React.CSSProperties = { @@ -291,6 +323,11 @@ export default function ExamplesIndexPage() { marginTop: 8, }} > + {ex.liveUrl ? ( + + ▸ Try it live + + ) : null} - More examples in flight: a browser SQL playground (WASM) - and a Go edge/IoT event collector. See{" "} - /docs for the engine reference. + One more example in flight: a Go edge/IoT event collector. + See /docs for the engine reference.

diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 5e6f667..a48505d 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -2375,3 +2375,370 @@ html { color: var(--color-fg); text-wrap: balance; } + +/* ============================================================ */ +/* SQLR-42 — WASM SQL playground (/playground) */ +/* ============================================================ */ + +.pg-shell { + display: flex; + flex-direction: column; + gap: 12px; +} + +.pg-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 10px 12px; + border: 1px solid var(--color-line); + border-radius: 10px; + background: var(--color-bg-card); +} + +.pg-toolbar-skeleton { + height: 56px; +} + +.pg-btn, +.pg-btn-primary, +.pg-btn-ghost { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 13px; + font-weight: 500; + border-radius: 7px; + padding: 7px 13px; + cursor: pointer; + border: 1px solid var(--color-line); + background: var(--color-bg-elev); + color: var(--color-fg); + transition: border-color 0.15s, background 0.15s, opacity 0.15s; +} +.pg-btn:hover, +.pg-btn-ghost:hover { + border-color: var(--color-accent-line); + background: var(--color-accent-soft); +} +.pg-btn-primary { + border-color: var(--color-accent-line); + background: var(--color-accent-soft); + color: var(--color-accent); + font-weight: 600; +} +.pg-btn-primary:hover { + background: color-mix(in oklab, var(--color-accent) 22%, transparent); +} +.pg-btn:disabled, +.pg-btn-primary:disabled, +.pg-select:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.pg-btn-ghost { + padding: 5px 10px; + font-size: 12px; +} + +.pg-kbd { + font-family: var(--font-mono); + font-size: 11px; + opacity: 0.75; +} + +.pg-field { + display: inline-flex; + align-items: center; + gap: 6px; +} +.pg-field-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-fg-dim); + font-family: var(--font-mono); +} +.pg-select { + font-size: 13px; + padding: 7px 10px; + border-radius: 7px; + border: 1px solid var(--color-line); + background: var(--color-bg-elev); + color: var(--color-fg); + cursor: pointer; +} + +.pg-storage-badge { + margin-left: auto; + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-fg-dim); + padding: 4px 9px; + border: 1px solid var(--color-line); + border-radius: 999px; + white-space: nowrap; +} + +.pg-status { + margin: 0; + min-height: 18px; + font-size: 13px; + color: var(--color-fg-mute); +} + +.pg-panes { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 12px; + min-height: 420px; +} +@media (max-width: 900px) { + .pg-panes { + grid-template-columns: minmax(0, 1fr); + } +} + +.pg-pane { + border: 1px solid var(--color-line); + border-radius: 10px; + overflow: hidden; + background: var(--color-bg-card); + min-height: 420px; + display: flex; + flex-direction: column; +} +.pg-pane-editor { + border-left: 2px solid var(--color-accent-line); +} +.pg-pane-skeleton { + opacity: 0.5; +} + +.pg-editor { + flex: 1; + min-height: 0; + overflow: auto; +} +.pg-editor .cm-editor { + height: 100%; +} + +/* Results pane */ +.pg-result-empty, +.pg-result-message { + padding: 20px; + color: var(--color-fg-mute); + font-size: 13.5px; +} +.pg-result-error { + padding: 16px 18px; + color: var(--color-fg); + font-size: 13px; + overflow: auto; +} +.pg-result-error strong { + color: var(--color-warn); + display: block; + margin-bottom: 6px; +} +.pg-result-error pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-mono); + font-size: 12.5px; + color: var(--color-fg-mute); +} + +.pg-result-rows { + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; +} +.pg-result-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 12px; + border-bottom: 1px solid var(--color-line); +} +.pg-result-meta { + font-family: var(--font-mono); + font-size: 12px; + color: var(--color-fg-dim); +} + +.pg-table-scroll { + overflow: auto; + flex: 1; + min-height: 0; +} +.pg-table { + border-collapse: collapse; + width: 100%; + font-size: 13px; +} +.pg-table thead th { + position: sticky; + top: 0; + background: var(--color-bg-elev); + text-align: left; + padding: 8px 12px; + border-bottom: 1px solid var(--color-line); + white-space: nowrap; + vertical-align: bottom; +} +.pg-col-name { + display: block; + font-weight: 600; + color: var(--color-fg); +} +.pg-col-type { + display: block; + font-size: 10.5px; + color: var(--color-fg-dim); + text-transform: lowercase; + margin-top: 1px; +} +.pg-table tbody td { + padding: 6px 12px; + border-bottom: 1px solid var(--color-line-soft); + font-family: var(--font-mono); + font-size: 12.5px; + vertical-align: top; +} +.pg-table tbody tr:hover td { + background: var(--color-accent-soft); +} +.pg-th-idx, +.pg-td-idx { + width: 1%; + color: var(--color-fg-dim); + font-family: var(--font-mono); + text-align: right; + user-select: none; +} +.pg-cell-num { + color: var(--color-num); + text-align: right; +} +.pg-cell-bool { + color: var(--color-kw); +} +.pg-cell-text { + color: var(--color-fg); +} +.pg-cell-null { + color: var(--color-fg-dim); + font-style: italic; + font-family: var(--font-mono); + font-size: 12.5px; + padding: 6px 12px; + border-bottom: 1px solid var(--color-line-soft); +} + +/* Static explainer below the app */ +.pg-notes { + margin-top: 48px; + max-width: 760px; +} +.pg-notes h2 { + font-size: 18px; + margin: 28px 0 10px; +} +.pg-notes p { + color: var(--color-fg-mute); + line-height: 1.65; + margin: 0 0 12px; +} +.pg-notes ul { + margin: 0 0 12px; + padding-left: 20px; + color: var(--color-fg-mute); + line-height: 1.65; +} +.pg-notes li { + margin-bottom: 8px; +} +.pg-notes code { + font-family: var(--font-mono); + font-size: 0.86em; + background: var(--color-bg-elev); + border: 1px solid var(--color-line); + border-radius: 4px; + padding: 1px 5px; +} +.pg-notes a { + color: var(--color-accent); +} + +/* in the hero subtitle */ +.sec-head .sub kbd { + font-family: var(--font-mono); + font-size: 11px; + background: var(--color-bg-elev); + border: 1px solid var(--color-line); + border-bottom-width: 2px; + border-radius: 4px; + padding: 1px 5px; + color: var(--color-fg); +} + +/* Homepage playground card (SQLR-42) */ +.pg-home { + padding-top: 8px; +} +.pg-home-card { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 28px; + align-items: center; + padding: 28px 32px; + border: 1px solid var(--color-line); + border-left: 2px solid var(--color-accent); + border-radius: 12px; + background: var(--color-bg-card); + transition: border-color 0.15s, background 0.15s; +} +.pg-home-card:hover { + border-color: var(--color-accent-line); + background: color-mix(in oklab, var(--color-bg-card) 80%, var(--color-accent-soft)); +} +.pg-home-copy h2 { + margin: 14px 0 10px; + font-size: 26px; +} +.pg-home-copy p { + margin: 0 0 14px; + color: var(--color-fg-mute); + line-height: 1.6; +} +.pg-home-link { + color: var(--color-accent); + font-weight: 600; + font-size: 14px; +} +.pg-home-snippet { + margin: 0; + padding: 16px 18px; + border-radius: 8px; + background: var(--color-bg); + border: 1px solid var(--color-line); + overflow-x: auto; + font-family: var(--font-mono); + font-size: 12.5px; + line-height: 1.55; + color: var(--color-fg-mute); +} +@media (max-width: 760px) { + .pg-home-card { + grid-template-columns: minmax(0, 1fr); + } + .pg-home-snippet { + display: none; + } +} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 4f1589d..f94a866 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -8,6 +8,7 @@ import { Features } from "@/components/features"; import { Footer } from "@/components/footer"; import { Hero } from "@/components/hero"; import { Nav } from "@/components/nav"; +import { PlaygroundCard } from "@/components/playground-card"; import { Roadmap } from "@/components/roadmap"; import { SDKShowcase } from "@/components/sdk-showcase"; import { SQLRef } from "@/components/sql-ref"; @@ -87,6 +88,7 @@ export default function Home() { />