diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index fa8ef206..e9fd5990 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -8,7 +8,11 @@ on: workflow_dispatch: jobs: + build-wasm: + uses: ./.github/workflows/wasm.yaml + publish-website: + needs: build-wasm runs-on: ubuntu-latest permissions: contents: write @@ -22,6 +26,12 @@ jobs: sudo docker image prune --all --force sudo docker builder prune -a + - name: Download WASM demo artifact + uses: actions/download-artifact@v4 + with: + name: wasm-demo + path: doc/wasm + - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/wasm.yaml b/.github/workflows/wasm.yaml index 2059020d..d204dc60 100644 --- a/.github/workflows/wasm.yaml +++ b/.github/workflows/wasm.yaml @@ -6,6 +6,7 @@ on: pull_request: branches: [main] workflow_dispatch: + workflow_call: jobs: build: @@ -47,6 +48,10 @@ jobs: - name: Install wasm-pack run: cargo install wasm-pack + - name: Build WASM library + working-directory: ggsql-wasm/library + run: npm install && npm run build + - name: Build WASM package working-directory: ggsql-wasm run: wasm-pack build --target web --profile wasm --no-opt @@ -54,3 +59,13 @@ jobs: - name: Optimise WASM binary working-directory: ggsql-wasm run: wasm-opt pkg/ggsql_wasm_bg.wasm -o pkg/ggsql_wasm_bg.wasm -Oz --all-features + + - name: Build WASM demo + working-directory: ggsql-wasm/demo + run: npm install && npm run build + + - name: Upload demo artifact + uses: actions/upload-artifact@v4 + with: + name: wasm-demo + path: ggsql-wasm/demo/dist/ diff --git a/doc/.gitignore b/doc/.gitignore index 9cd8b596..06fb2886 100644 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -1,3 +1,4 @@ /.quarto/ **/*.quarto_ipynb _site +wasm diff --git a/doc/_quarto.yml b/doc/_quarto.yml index dd94735d..273e0577 100644 --- a/doc/_quarto.yml +++ b/doc/_quarto.yml @@ -1,5 +1,7 @@ project: type: website + resources: + - wasm/** website: title: "ggsql" @@ -50,6 +52,9 @@ website: - examples.qmd - href: faq.qmd text: FAQ + right: + - text: Playground + href: wasm/index.html tools: - icon: github menu: @@ -122,3 +127,14 @@ format: window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}}; plausible.init() + include-after-body: + - text: | + diff --git a/ggsql-wasm/Cargo.toml b/ggsql-wasm/Cargo.toml index cc7bcf5b..6805a915 100644 --- a/ggsql-wasm/Cargo.toml +++ b/ggsql-wasm/Cargo.toml @@ -13,14 +13,17 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" js-sys = "0.3" csv = "1" -polars = { version = "0.52", default-features = false, features = ["sql", "dtype-full"] } -ggsql = { path = "../src", default-features = false, features = ["polars-sql", "vegalite"] } +polars = { version = "0.52", default-features = false, features = ["dtype-full"] } +ggsql = { path = "../src", default-features = false, features = ["vegalite", "sqlite", "builtin-data"] } +serde_json = "1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.35", features = ["full"] } [target.'cfg(target_arch = "wasm32")'.dependencies] tokio = { version = "1.35", default-features = false } +sqlite-wasm-rs = "0.5.2" diff --git a/ggsql-wasm/build-wasm.sh b/ggsql-wasm/build-wasm.sh new file mode 100755 index 00000000..afd5c3c0 --- /dev/null +++ b/ggsql-wasm/build-wasm.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +SKIP_BINARY=false +SKIP_OPT=false +for arg in "$@"; do + case "$arg" in + --skip-binary) SKIP_BINARY=true ;; + --skip-opt) SKIP_OPT=true ;; + *) echo "Unknown argument: $arg" >&2; exit 1 ;; + esac +done + +check_wasm32_support() { + local cc="${CC:-clang}" + if ! echo "int main(){return 0;}" | \ + "$cc" -target wasm32-unknown-unknown -c -o /dev/null -x c - 2>/dev/null; then + echo "Error: '$cc' does not support the wasm32-unknown-unknown target." >&2 + echo "Install an LLVM/clang toolchain with wasm backend support (e.g. 'sudo apt-get install llvm' on Debian/Ubuntu)." >&2 + exit 1 + fi +} + +echo "Building WASM library..." +(cd "$SCRIPT_DIR/library" && npm install && npm run build) + +if [ "$SKIP_BINARY" = false ]; then + echo "Checking wasm32 compiler support..." + check_wasm32_support + + echo "Building WASM binary..." + (cd "$SCRIPT_DIR" && wasm-pack build --target web --profile wasm --no-opt) + + if [ "$SKIP_OPT" = false ]; then + echo "Optimising WASM binary..." + (cd "$SCRIPT_DIR" && wasm-opt pkg/ggsql_wasm_bg.wasm -o pkg/ggsql_wasm_bg.wasm -Oz --all-features) + else + echo "Skipping wasm-opt (--skip-opt)." + fi +else + echo "Skipping WASM binary build (--skip-binary)." +fi + +echo "Building WASM demo and Quarto integration..." +(cd "$SCRIPT_DIR/demo" && npm install && npm run build) + +echo "Copying output to doc/wasm..." +rm -rf "$REPO_ROOT/doc/wasm" +cp -r "$SCRIPT_DIR/demo/dist" "$REPO_ROOT/doc/wasm" + +echo "Done! Output is in: $REPO_ROOT/doc/wasm" diff --git a/ggsql-wasm/demo/build.mjs b/ggsql-wasm/demo/build.mjs new file mode 100644 index 00000000..3fada56f --- /dev/null +++ b/ggsql-wasm/demo/build.mjs @@ -0,0 +1,87 @@ +import * as esbuild from "esbuild"; +import { copyFileSync, mkdirSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const isWatch = process.argv.includes("--watch"); +const distDir = join(__dirname, "dist"); + +// Ensure dist/ directory exists +mkdirSync(distDir, { recursive: true }); + +// Copy static files +console.log("Copying static files..."); +copyFileSync(join(__dirname, "src/index.html"), join(distDir, "index.html")); +copyFileSync( + join(__dirname, "../pkg/ggsql_wasm_bg.wasm"), + join(distDir, "ggsql_wasm_bg.wasm"), +); +copyFileSync( + join(__dirname, "node_modules/vscode-oniguruma/release/onig.wasm"), + join(distDir, "onig.wasm"), +); +copyFileSync( + join(__dirname, "../../ggsql-vscode/syntaxes/ggsql.tmLanguage.json"), + join(distDir, "ggsql.tmLanguage.json"), +); + +// Build Monaco editor web worker +console.log("Building Monaco editor worker..."); +await esbuild.build({ + entryPoints: [ + join( + __dirname, + "node_modules/monaco-editor/esm/vs/editor/editor.worker.js", + ), + ], + bundle: true, + outfile: join(distDir, "editor.worker.js"), + format: "iife", +}); + +// Shared build options +const sharedOptions = { + bundle: true, + format: "esm", + platform: "browser", + target: "es2020", + sourcemap: true, + nodePaths: [join(__dirname, "node_modules")], + loader: { + ".ttf": "file", + }, +}; + +// Build playground bundle +const playgroundOptions = { + ...sharedOptions, + entryPoints: [join(__dirname, "src/main.ts")], + outfile: join(distDir, "bundle.js"), +}; + +// Build quarto integration bundle +const quartoOptions = { + ...sharedOptions, + entryPoints: [join(__dirname, "src/quarto/main.ts")], + outfile: join(distDir, "quarto.js"), + loader: { + ...sharedOptions.loader, + ".css": "css", + }, +}; + +if (isWatch) { + console.log("Starting watch mode..."); + const playgroundCtx = await esbuild.context(playgroundOptions); + const quartoCtx = await esbuild.context(quartoOptions); + await Promise.all([playgroundCtx.watch(), quartoCtx.watch()]); + console.log("Watching for changes..."); +} else { + console.log("Building bundles..."); + await Promise.all([ + esbuild.build(playgroundOptions), + esbuild.build(quartoOptions), + ]); + console.log("Build complete!"); +} diff --git a/ggsql-wasm/demo/package-lock.json b/ggsql-wasm/demo/package-lock.json new file mode 100644 index 00000000..7d7920ea --- /dev/null +++ b/ggsql-wasm/demo/package-lock.json @@ -0,0 +1,1659 @@ +{ + "name": "ggsql-wasm-demo", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ggsql-wasm-demo", + "version": "0.1.0", + "dependencies": { + "ggsql-wasm": "file:../pkg", + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "monaco-editor": "^0.55.0", + "typescript": "^5.9.0", + "vega": "^6.2.0", + "vega-embed": "^7.1.0", + "vega-lite": "6.4.1", + "vscode-oniguruma": "^2.0.1", + "vscode-textmate": "^9.3.0" + } + }, + "../pkg": { + "name": "ggsql-wasm", + "version": "0.1.0", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" + }, + "bin": { + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ggsql-wasm": { + "resolved": "../pkg", + "link": true + }, + "node_modules/hyparquet": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/hyparquet/-/hyparquet-1.25.0.tgz", + "integrity": "sha512-isJx+RplYT3aJc5yhaG5CeOZSBJecHZgYsUi7NE6P/nAbxxA0hZcyul0tUsWCQLc9QXYQ2uFyYBrk61JbJO0cg==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vega": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vega/-/vega-6.2.0.tgz", + "integrity": "sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-crossfilter": "~5.1.0", + "vega-dataflow": "~6.1.0", + "vega-encode": "~5.1.0", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-force": "~5.1.0", + "vega-format": "~2.1.0", + "vega-functions": "~6.1.0", + "vega-geo": "~5.1.0", + "vega-hierarchy": "~5.1.0", + "vega-label": "~2.1.0", + "vega-loader": "~5.1.0", + "vega-parser": "~7.1.0", + "vega-projection": "~2.1.0", + "vega-regression": "~2.1.0", + "vega-runtime": "~7.1.0", + "vega-scale": "~8.1.0", + "vega-scenegraph": "~5.1.0", + "vega-statistics": "~2.0.0", + "vega-time": "~3.1.0", + "vega-transforms": "~5.1.0", + "vega-typings": "~2.1.0", + "vega-util": "~2.1.0", + "vega-view": "~6.1.0", + "vega-view-transforms": "~5.1.0", + "vega-voronoi": "~5.1.0", + "vega-wordcloud": "~5.1.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-canvas": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-2.0.0.tgz", + "integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-crossfilter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz", + "integrity": "sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-dataflow": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-6.1.0.tgz", + "integrity": "sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-format": "^2.1.0", + "vega-loader": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-embed": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-7.1.0.tgz", + "integrity": "sha512-ZmEIn5XJrQt7fSh2lwtSdXG/9uf3yIqZnvXFEwBJRppiBgrEWZcZbj6VK3xn8sNTFQ+sQDXW5sl/6kmbAW3s5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "fast-json-patch": "^3.1.1", + "json-stringify-pretty-compact": "^4.0.0", + "semver": "^7.7.2", + "tslib": "^2.8.1", + "vega-interpreter": "^2.0.0", + "vega-schema-url-parser": "^3.0.2", + "vega-themes": "3.0.0", + "vega-tooltip": "1.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-encode": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-5.1.0.tgz", + "integrity": "sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-event-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-4.0.0.tgz", + "integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-expression": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.1.0.tgz", + "integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/estree": "^1.0.8", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-force": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-5.1.0.tgz", + "integrity": "sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-force": "^3.0.0", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-2.1.0.tgz", + "integrity": "sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-format": "^3.1.0", + "d3-time-format": "^4.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-functions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-6.1.1.tgz", + "integrity": "sha512-Due6jP0y0FfsGMTrHnzUGnEwXPu7VwE+9relfo+LjL/tRPYnnKqwWvzt7n9JkeBuZqjkgYjMzm/WucNn6Hkw5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-dataflow": "^6.1.0", + "vega-expression": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-scenegraph": "^5.1.0", + "vega-selections": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-geo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-5.1.0.tgz", + "integrity": "sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-projection": "^2.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-hierarchy": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz", + "integrity": "sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-hierarchy": "^3.1.2", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-interpreter": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vega-interpreter/-/vega-interpreter-2.2.1.tgz", + "integrity": "sha512-o+4ZEme2mdFLewlpF76dwPWW2VkZ3TAF3DMcq75/NzA5KPvnN4wnlCM8At2FVawbaHRyGdVkJSS5ROF5KwpHPQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-2.1.0.tgz", + "integrity": "sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-lite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.1.tgz", + "integrity": "sha512-KO3ybHNouRK4A0al/+2fN9UqgTEfxrd/ntGLY933Hg5UOYotDVQdshR3zn7OfXwQ7uj0W96Vfa5R+QxO8am3IQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "json-stringify-pretty-compact": "~4.0.0", + "tslib": "~2.8.1", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-util": "~2.1.0", + "yargs": "~18.0.0" + }, + "bin": { + "vl2pdf": "bin/vl2pdf", + "vl2png": "bin/vl2png", + "vl2svg": "bin/vl2svg", + "vl2vg": "bin/vl2vg" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "^6.0.0" + } + }, + "node_modules/vega-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-5.1.0.tgz", + "integrity": "sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-dsv": "^3.0.1", + "topojson-client": "^3.1.0", + "vega-format": "^2.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-7.1.0.tgz", + "integrity": "sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-event-selector": "^4.0.0", + "vega-functions": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-projection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-2.1.0.tgz", + "integrity": "sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "vega-scale": "^8.1.0" + } + }, + "node_modules/vega-regression": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-2.1.0.tgz", + "integrity": "sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-runtime": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-7.1.0.tgz", + "integrity": "sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scale": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-8.1.0.tgz", + "integrity": "sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scenegraph": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz", + "integrity": "sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "^3.1.0", + "d3-shape": "^3.2.0", + "vega-canvas": "^2.0.0", + "vega-loader": "^5.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-schema-url-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz", + "integrity": "sha512-xAnR7KAvNPYewI3O0l5QGdT8Tv0+GCZQjqfP39cW/hbe/b3aYMAQ39vm8O2wfXUHzm04xTe7nolcsx8WQNVLRQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-selections": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-6.1.2.tgz", + "integrity": "sha512-xJ+V4qdd46nk2RBdwIRrQm2iSTMHdlu/omhLz1pqRL3jZDrkqNBXimrisci2kIKpH2WBpA1YVagwuZEKBmF2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "3.2.4", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-statistics": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-2.0.0.tgz", + "integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4" + } + }, + "node_modules/vega-themes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-3.0.0.tgz", + "integrity": "sha512-1iFiI3BNmW9FrsLnDLx0ZKEddsCitRY3XmUAwp6qmp+p+IXyJYc9pfjlVj9E6KXBPfm4cQyU++s0smKNiWzO4g==", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-3.1.0.tgz", + "integrity": "sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-tooltip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-1.0.0.tgz", + "integrity": "sha512-P1R0JP29v0qnTuwzCQ0SPJlkjAzr6qeyj+H4VgUFSykHmHc1OBxda//XBaFDl/bZgIscEMvjKSjZpXd84x3aZQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-5.1.0.tgz", + "integrity": "sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-typings": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-2.1.0.tgz", + "integrity": "sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/geojson": "7946.0.16", + "vega-event-selector": "^4.0.0", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-util": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-2.1.0.tgz", + "integrity": "sha512-PGfp0m0QCufDmcxKJCWQy4Ov23FoF8DSXmoJwSezi3itQaa2hbxK0+xwsTMP2vy4PR16Pu25HMzgMwXVW1+33w==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/vega-view": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-6.1.0.tgz", + "integrity": "sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-timer": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-format": "^2.1.0", + "vega-functions": "^6.1.0", + "vega-runtime": "^7.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-view-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz", + "integrity": "sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-voronoi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-5.1.0.tgz", + "integrity": "sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-delaunay": "^6.0.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-wordcloud": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz", + "integrity": "sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vscode-oniguruma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz", + "integrity": "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-textmate": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + } + } +} diff --git a/ggsql-wasm/demo/package.json b/ggsql-wasm/demo/package.json new file mode 100644 index 00000000..a145b247 --- /dev/null +++ b/ggsql-wasm/demo/package.json @@ -0,0 +1,25 @@ +{ + "name": "ggsql-wasm-demo", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs", + "dev": "node build.mjs --watch", + "serve": "npx http-server dist -p 8080 -c-1" + }, + "dependencies": { + "ggsql-wasm": "file:../pkg", + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "monaco-editor": "^0.55.0", + "typescript": "^5.9.0", + "vega": "^6.2.0", + "vega-embed": "^7.1.0", + "vega-lite": "6.4.1", + "vscode-oniguruma": "^2.0.1", + "vscode-textmate": "^9.3.0" + } +} diff --git a/ggsql-wasm/demo/src/context.ts b/ggsql-wasm/demo/src/context.ts new file mode 100644 index 00000000..e895418f --- /dev/null +++ b/ggsql-wasm/demo/src/context.ts @@ -0,0 +1,58 @@ +import init, { GgsqlContext } from "ggsql-wasm"; +import { WASM_BASE } from "./wasmBase"; + +export class WasmContextManager { + private context: GgsqlContext | null = null; + private initialized = false; + + async initialize(): Promise { + if (this.initialized) return; + + await init(WASM_BASE + "ggsql_wasm_bg.wasm"); + this.context = new GgsqlContext(); + this.initialized = true; + } + + private getContext(): GgsqlContext { + if (!this.context) { + throw new Error("Context not initialized. Call initialize() first."); + } + return this.context; + } + + execute(query: string): string { + return this.getContext().execute(query); + } + + hasVisual(query: string): boolean { + return this.getContext().has_visual(query); + } + + executeSql(query: string): string { + return this.getContext().execute_sql(query); + } + + registerCSV(name: string, data: Uint8Array): void { + this.getContext().register_csv(name, data); + } + + async registerParquet(name: string, data: Uint8Array): Promise { + await this.getContext().register_parquet(name, data); + } + + async registerBuiltinDatasets(): Promise { + await this.getContext().register_builtin_datasets(); + } + + unregister(name: string): void { + this.getContext().unregister(name); + } + + listTables(): string[] { + return Array.from(this.getContext().list_tables() as Iterable); + } + + isInitialized(): boolean { + return this.initialized; + } +} diff --git a/ggsql-wasm/demo/src/editor.ts b/ggsql-wasm/demo/src/editor.ts new file mode 100644 index 00000000..ab6e27b0 --- /dev/null +++ b/ggsql-wasm/demo/src/editor.ts @@ -0,0 +1,180 @@ +import * as monaco from "monaco-editor"; +import { createOnigScanner, createOnigString, loadWASM } from "vscode-oniguruma"; +import { Registry, parseRawGrammar, type IGrammar } from "vscode-textmate"; +import { WASM_BASE } from "./wasmBase"; + +// Must be set before any Monaco editor is created +(self as any).MonacoEnvironment = { + getWorkerUrl: (_moduleId: string, _label: string) => WASM_BASE + "editor.worker.js", +}; + +// Map TextMate scope names to Monaco theme token colors +const SCOPE_TO_TOKEN: [string, string][] = [ + ["comment", "comment"], + ["string", "string"], + ["constant.numeric", "number"], + ["constant.language", "keyword"], + ["keyword", "keyword"], + ["support.function", "type"], + ["support.type.geom", "type"], + ["support.type.aesthetic", "variable"], + ["support.type.coord", "type"], + ["support.type.theme", "type"], + ["support.type.property", "variable"], + ["constant.language.scale-type", "type"], + ["keyword.operator", "operator"], + ["punctuation", "delimiter"], +]; + +function scopeToMonacoToken(scopes: string[]): string { + // Walk scopes from most specific to least + for (let i = scopes.length - 1; i >= 0; i--) { + const scope = scopes[i]; + for (const [pattern, token] of SCOPE_TO_TOKEN) { + if (scope.startsWith(pattern)) { + return token; + } + } + } + return ""; +} + +async function initTextMateGrammar(): Promise { + // Load oniguruma WASM + const onigWasm = await fetch(WASM_BASE + "onig.wasm"); + const onigBuffer = await onigWasm.arrayBuffer(); + await loadWASM(onigBuffer); + + // Create the TextMate registry + const registry = new Registry({ + onigLib: Promise.resolve({ + createOnigScanner, + createOnigString, + }), + loadGrammar: async (scopeName: string) => { + if (scopeName === "source.ggsql") { + const response = await fetch(WASM_BASE + "ggsql.tmLanguage.json"); + const grammarText = await response.text(); + return parseRawGrammar(grammarText, "ggsql.tmLanguage.json"); + } + return null; + }, + }); + + return registry.loadGrammar("source.ggsql"); +} + +export class EditorManager { + private editor: monaco.editor.IStandaloneCodeEditor | null = null; + private onChangeCallback: ((query: string) => void) | null = null; + private changeTimeoutId: number | null = null; + + async initialize( + container: HTMLElement, + initialValue: string, + ): Promise { + // Register ggsql language + monaco.languages.register({ id: "ggsql" }); + + // Apply language configuration from ggsql-vscode/language-configuration.json + monaco.languages.setLanguageConfiguration("ggsql", { + comments: { + lineComment: "--", + blockComment: ["/*", "*/"], + }, + brackets: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ], + autoClosingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "'", close: "'", notIn: ["string", "comment"] }, + { open: '"', close: '"', notIn: ["string", "comment"] }, + ], + surroundingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "'", close: "'" }, + { open: '"', close: '"' }, + ], + }); + + // Load TextMate grammar for tokenization + const grammar = await initTextMateGrammar(); + if (grammar) { + // Create a custom tokens provider using the TextMate grammar + monaco.languages.setTokensProvider("ggsql", { + getInitialState: () => new TMState(null), + tokenize: (line: string, state: TMState): monaco.languages.ILineTokens => { + const result = grammar.tokenizeLine(line, state.ruleStack); + const tokens: monaco.languages.IToken[] = result.tokens.map((t) => ({ + startIndex: t.startIndex, + scopes: scopeToMonacoToken(t.scopes), + })); + return { + tokens, + endState: new TMState(result.ruleStack), + }; + }, + }); + } + + // Create editor + this.editor = monaco.editor.create(container, { + value: initialValue, + language: "ggsql", + theme: "vs", + automaticLayout: true, + minimap: { enabled: false }, + fontSize: 14, + lineNumbers: "on", + scrollBeyondLastLine: false, + wordWrap: "on", + padding: { top: 10 }, + }); + + // Set up change listener with debounce + this.editor.onDidChangeModelContent(() => { + if (this.changeTimeoutId !== null) { + clearTimeout(this.changeTimeoutId); + } + this.changeTimeoutId = window.setTimeout(() => { + if (this.onChangeCallback && this.editor) { + this.onChangeCallback(this.editor.getValue()); + } + }, 100); + }); + } + + getValue(): string { + return this.editor?.getValue() || ""; + } + + setValue(value: string): void { + this.editor?.setValue(value); + } + + onChange(callback: (query: string) => void): void { + this.onChangeCallback = callback; + } +} + +// TextMate state wrapper for Monaco +class TMState implements monaco.languages.IState { + constructor(public ruleStack: any) {} + + clone(): TMState { + return new TMState(this.ruleStack); + } + + equals(other: monaco.languages.IState): boolean { + if (!(other instanceof TMState)) return false; + if (!this.ruleStack && !other.ruleStack) return true; + if (!this.ruleStack || !other.ruleStack) return false; + return this.ruleStack.equals(other.ruleStack); + } +} diff --git a/ggsql-wasm/demo/src/examples.ts b/ggsql-wasm/demo/src/examples.ts new file mode 100644 index 00000000..61b9acba --- /dev/null +++ b/ggsql-wasm/demo/src/examples.ts @@ -0,0 +1,214 @@ +export interface Example { + name: string; + query: string; + section: string; +} + +export const examples: Example[] = [ + // === Layers === + { + section: "Layers", + name: "Area", + query: `VISUALISE FROM ggsql:airquality +DRAW area + MAPPING Date AS x, Wind AS y`, + }, + { + section: "Layers", + name: "Bar", + query: `VISUALISE FROM ggsql:penguins +DRAW bar + MAPPING species AS x`, + }, + { + section: "Layers", + name: "Boxplot", + query: `VISUALISE FROM ggsql:penguins +DRAW boxplot + MAPPING species AS x, bill_len AS y, island AS fill`, + }, + { + section: "Layers", + name: "Density", + query: `VISUALISE bill_dep AS x, species AS colour FROM ggsql:penguins + DRAW density MAPPING body_mass AS weight`, + }, + { + section: "Layers", + name: "Histogram", + query: `VISUALISE FROM ggsql:penguins +DRAW histogram + MAPPING body_mass AS x`, + }, + { + section: "Layers", + name: "Line", + query: `VISUALISE FROM ggsql:airquality +DRAW line + MAPPING Day AS x, Temp AS y, Month AS color`, + }, + { + section: "Layers", + name: "Path", + query: `WITH df(x, y, id) AS (VALUES + (1.0, 1.0, 'A'), + (2.0, 1.0, 'A'), + (1.0, 3.0, 'A'), + (3.0, 1.0, 'B'), + (2.0, 3.0, 'B'), + (3.0, 3.0, 'B') +) +VISUALIZE x, y FROM df +DRAW line + MAPPING id AS colour`, + }, + { + section: "Layers", + name: "Point", + query: `SELECT * FROM ggsql:penguins +VISUALISE +DRAW point MAPPING bill_len AS x, bill_dep AS y, body_mass AS size, species AS color +LABEL title => 'Penguin Measurements', x => 'Bill Length (mm)', y => 'Bill Depth (mm)'`, + }, + { + section: "Layers", + name: "Polygon", + query: `WITH df(x, y, id) AS (VALUES + (1.0, 1.0, 'A'), + (2.0, 1.0, 'A'), + (1.0, 3.0, 'A'), + (3.0, 1.0, 'B'), + (2.0, 3.0, 'B'), + (3.0, 3.0, 'B') +) +VISUALIZE x, y FROM df +DRAW polygon + MAPPING id AS colour`, + }, + { + section: "Layers", + name: "Ribbon", + query: ` VISUALISE FROM ggsql:airquality + DRAW ribbon + MAPPING Date AS x, Wind AS ymin, Temp AS ymax`, + }, + { + section: "Layers", + name: "Violin", + query: `VISUALISE species AS x, bill_dep AS y FROM ggsql:penguins + DRAW violin`, + }, + // === Scales === + { + section: "Scales", + name: "Binned", + query: `VISUALISE bill_len AS x, bill_dep AS y, body_mass AS color FROM ggsql:penguins +DRAW point +SCALE BINNED color TO viridis`, + }, + { + section: "Scales", + name: "Continuous", + query: `VISUALISE bill_len AS x, bill_dep AS y FROM ggsql:penguins +DRAW point +SCALE x FROM [0, null]`, + }, + { + section: "Scales", + name: "Discrete", + query: `VISUALISE bill_len AS x, bill_dep AS y, island AS shape, island AS color FROM ggsql:penguins +DRAW point + SETTING size => 6 +SCALE shape TO ['star', 'circle', 'diamond'] +SCALE color`, + }, + { + section: "Scales", + name: "Identity", + query: `WITH t(category, value, style) AS (VALUES + ('A', 45, 'forestgreen'), + ('B', 72, '#3401e3'), + ('C', 38, 'hsl(150deg 30% 60%)') +) +VISUALISE category AS x, value AS y, style AS fill FROM t +DRAW bar +SCALE IDENTITY fill`, + }, + { + section: "Scales", + name: "Ordinal", + query: `VISUALISE Ozone AS x, Temp AS y FROM ggsql:airquality +DRAW point + MAPPING Month AS color +SCALE ORDINAL color + RENAMING * => '{}th month'`, + }, + { + section: "Scales", + name: "Faceting", + query: `VISUALISE sex AS x FROM ggsql:penguins +DRAW bar +FACET species +SCALE panel FROM ['Adelie', null] + RENAMING null => 'The rest'`, + }, + + // === Aesthetics === + { + section: "Aesthetics", + name: "Position", + query: `SELECT * FROM ggsql:penguins +VISUALISE +DRAW point MAPPING bill_len AS x, bill_dep AS y`, + }, + { + section: "Aesthetics", + name: "Fill", + query: `VISUALISE FROM ggsql:penguins +DRAW point + MAPPING bill_dep AS x, body_mass AS y, species AS fill + SETTING stroke => null +SCALE color TO category10`, + }, + { + section: "Aesthetics", + name: "Opacity", + query: `VISUALISE FROM ggsql:airquality +DRAW area + MAPPING Date AS x, Wind AS y + SETTING opacity => 0.2`, + }, + { + section: "Aesthetics", + name: "Linetype", + query: `VISUALISE FROM ggsql:airquality +DRAW line + MAPPING Day AS x, Temp AS y, Month AS linetype +SCALE ORDINAL linetype`, + }, + { + section: "Aesthetics", + name: "Linewidth", + query: `VISUALISE FROM ggsql:airquality +DRAW line + MAPPING Day AS x, Temp AS y, Month AS colour + SETTING linewidth => 5`, + }, + { + section: "Aesthetics", + name: "Shape", + query: `VISUALISE FROM ggsql:penguins +DRAW point + MAPPING bill_dep AS x, body_mass AS y, species AS shape + SETTING linewidth => 1, size => 5 +SCALE shape TO ['star', 'bowtie', 'square-plus']`, + }, + { + section: "Aesthetics", + name: "Size", + query: `SELECT * FROM ggsql:penguins +VISUALISE +DRAW point MAPPING bill_len AS x, bill_dep AS y, body_mass AS size +LABEL title => 'Penguin Measurements', x => 'Bill Length (mm)', y => 'Bill Depth (mm)'`, + }, +]; diff --git a/ggsql-wasm/demo/src/index.html b/ggsql-wasm/demo/src/index.html new file mode 100644 index 00000000..2aa62f85 --- /dev/null +++ b/ggsql-wasm/demo/src/index.html @@ -0,0 +1,63 @@ + + + + + + ggsql playground + + + + + + + +
+ + +
+ +
+ + + +
+
+
+
+
+

Problems

+
+
+
+ + + + diff --git a/ggsql-wasm/demo/src/main.ts b/ggsql-wasm/demo/src/main.ts new file mode 100644 index 00000000..30cffda4 --- /dev/null +++ b/ggsql-wasm/demo/src/main.ts @@ -0,0 +1,229 @@ +import "./styles.css"; +import vegaEmbed from "vega-embed"; +import { Warn } from "vega"; +import { WasmContextManager } from "./context"; +import { EditorManager } from "./editor"; +import { TableManager } from "./tableManager"; +import { examples } from "./examples"; + +// State +const contextManager = new WasmContextManager(); +const editorManager = new EditorManager(); +let tableManager: TableManager; + +// DOM elements +const statusEl = document.getElementById("status")!; +const editorContainer = document.getElementById("editor-container")!; +const vizOutput = document.getElementById("viz-output")!; +const errorMessages = document.getElementById("error-messages")!; +const tableList = document.getElementById("table-list")!; +const csvUpload = document.getElementById("csv-upload") as HTMLInputElement; +const examplesList = document.getElementById("examples-list")!; + +function setStatus(message: string, type: "loading" | "success" | "error") { + statusEl.textContent = message; + statusEl.className = type; +} + +function showProblems( + errors: string[], + warnings: string[], +) { + errorMessages.innerHTML = ""; + for (const msg of errors) { + const div = document.createElement("div"); + div.className = "error-message"; + div.textContent = msg; + errorMessages.appendChild(div); + } + for (const msg of warnings) { + const div = document.createElement("div"); + div.className = "warning-message"; + div.textContent = msg; + errorMessages.appendChild(div); + } +} + +interface SqlResult { + columns: string[]; + rows: string[][]; + total_rows: number; + truncated: boolean; +} + +function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderTable(data: SqlResult): string { + const ths = data.columns.map((c) => `${escapeHtml(c)}`).join(""); + const bodyRows = data.rows + .map( + (row) => + `${row.map((v) => `${escapeHtml(v)}`).join("")}`, + ) + .join(""); + const truncationRow = data.truncated + ? `Showing ${data.rows.length} of ${data.total_rows} rows` + : ""; + return `${ths}${bodyRows}${truncationRow}
`; +} + +async function executeQuery(query: string) { + if (!query.trim()) { + showProblems([], []); + vizOutput.innerHTML = + '

Enter a query to visualize

'; + return; + } + + try { + setStatus("Executing query...", "loading"); + + if (contextManager.hasVisual(query)) { + const result = contextManager.execute(query); + const spec = JSON.parse(result); + + vizOutput.innerHTML = ""; + + const warnings: string[] = []; + let _level = Warn; + const logger = { + level(_: number) { if (arguments.length) { _level = _; return this; } return _level; }, + error: (...args: any[]) => { console.error(...args); return logger; }, + warn: (...args: any[]) => { warnings.push(args.map(String).join(" ")); return logger; }, + info: () => logger, + debug: () => logger, + }; + + await vegaEmbed(vizOutput, spec, { + actions: { + export: true, + source: false, + compiled: false, + editor: false, + }, + renderer: "svg", + logger: logger as any, + }); + + showProblems([], warnings); + } else { + const result = JSON.parse(contextManager.executeSql(query)); + vizOutput.innerHTML = renderTable(result); + showProblems([], []); + } + + setStatus("Query executed successfully", "success"); + } catch (error: any) { + console.error("Query execution error:", error); + showProblems([error.toString()], []); + setStatus("Query error", "error"); + } +} + +// File upload handlers +csvUpload.addEventListener("change", async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + + try { + setStatus("Uploading data...", "loading"); + await tableManager.uploadFile(file); + setStatus("Uploaded: " + file.name, "success"); + csvUpload.value = ""; + } catch (error: any) { + showProblems(["Upload failed: " + error], []); + setStatus("Upload error", "error"); + } +}); + +function initializeExamples() { + let currentSection = ""; + examples.forEach((example) => { + if (example.section !== currentSection) { + currentSection = example.section; + const header = document.createElement("div"); + header.className = "example-section-header"; + header.textContent = currentSection; + examplesList.appendChild(header); + } + const button = document.createElement("button"); + button.className = "example-button"; + button.textContent = example.name; + button.onclick = () => { + editorManager.setValue(example.query); + //executeQuery(example.query); + }; + examplesList.appendChild(button); + }); +} + +function initializeMobileExamples() { + const select = document.getElementById( + "mobile-example-select", + ) as HTMLSelectElement; + + let currentSection = ""; + let optgroup: HTMLOptGroupElement | null = null; + examples.forEach((example, index) => { + if (example.section !== currentSection) { + currentSection = example.section; + optgroup = document.createElement("optgroup"); + optgroup.label = currentSection; + select.appendChild(optgroup); + } + const option = document.createElement("option"); + option.value = String(index); + option.textContent = example.name; + optgroup!.appendChild(option); + }); + + select.addEventListener("change", () => { + const idx = parseInt(select.value, 10); + if (!isNaN(idx) && examples[idx]) { + editorManager.setValue(examples[idx].query); + } + }); +} + +async function main() { + try { + setStatus("Loading WASM module...", "loading"); + await contextManager.initialize(); + + // Load builtin datasets + setStatus("Loading builtin datasets...", "loading"); + await contextManager.registerBuiltinDatasets(); + + setStatus("Initializing editor...", "loading"); + await editorManager.initialize(editorContainer, examples[0].query); + + tableManager = new TableManager(tableList, contextManager); + tableManager.onClickTable((name) => { + editorManager.setValue(`SELECT * FROM ${name}`); + }); + tableManager.refresh(); + + initializeExamples(); + initializeMobileExamples(); + + editorManager.onChange((query) => { + executeQuery(query); + }); + + setStatus("Ready", "success"); + + executeQuery(examples[0].query); + } catch (error: any) { + console.error("Initialization error:", error); + setStatus("Initialization failed", "error"); + showProblems(["Failed to initialize: " + error], []); + } +} + +main(); diff --git a/ggsql-wasm/demo/src/quarto/editor.ts b/ggsql-wasm/demo/src/quarto/editor.ts new file mode 100644 index 00000000..61f89386 --- /dev/null +++ b/ggsql-wasm/demo/src/quarto/editor.ts @@ -0,0 +1,213 @@ +import * as monaco from "monaco-editor"; +import { + createOnigScanner, + createOnigString, + loadWASM, +} from "vscode-oniguruma"; +import { Registry, parseRawGrammar, type IGrammar } from "vscode-textmate"; +import { WASM_BASE } from "../wasmBase"; + +// Must be set before any Monaco editor is created +(self as any).MonacoEnvironment = { + getWorkerUrl: (_moduleId: string, _label: string) => + WASM_BASE + "editor.worker.js", +}; + +// Map TextMate scope names to Monaco theme token colors +const SCOPE_TO_TOKEN: [string, string][] = [ + ["comment", "comment"], + ["string", "string"], + ["constant.numeric", "number"], + ["constant.language", "keyword"], + ["keyword", "keyword"], + ["support.function", "type"], + ["support.type.geom", "type"], + ["support.type.aesthetic", "variable"], + ["support.type.coord", "type"], + ["support.type.theme", "type"], + ["support.type.property", "variable"], + ["constant.language.scale-type", "type"], + ["keyword.operator", "operator"], + ["punctuation", "delimiter"], +]; + +function scopeToMonacoToken(scopes: string[]): string { + for (let i = scopes.length - 1; i >= 0; i--) { + const scope = scopes[i]; + for (const [pattern, token] of SCOPE_TO_TOKEN) { + if (scope.startsWith(pattern)) { + return token; + } + } + } + return ""; +} + +// Singleton grammar initialization +let grammarPromise: Promise | null = null; + +async function initTextMateGrammar(): Promise { + const onigWasm = await fetch(WASM_BASE + "onig.wasm"); + const onigBuffer = await onigWasm.arrayBuffer(); + await loadWASM(onigBuffer); + + const registry = new Registry({ + onigLib: Promise.resolve({ + createOnigScanner, + createOnigString, + }), + loadGrammar: async (scopeName: string) => { + if (scopeName === "source.ggsql") { + const response = await fetch(WASM_BASE + "ggsql.tmLanguage.json"); + const grammarText = await response.text(); + return parseRawGrammar(grammarText, "ggsql.tmLanguage.json"); + } + return null; + }, + }); + + return registry.loadGrammar("source.ggsql"); +} + +function getGrammar(): Promise { + if (!grammarPromise) { + grammarPromise = initTextMateGrammar(); + } + return grammarPromise; +} + +let languageRegistered = false; + +async function ensureLanguageRegistered(): Promise { + if (languageRegistered) return; + languageRegistered = true; + + monaco.languages.register({ id: "ggsql" }); + + monaco.languages.setLanguageConfiguration("ggsql", { + comments: { + lineComment: "--", + blockComment: ["/*", "*/"], + }, + brackets: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ], + autoClosingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "'", close: "'", notIn: ["string", "comment"] }, + { open: '"', close: '"', notIn: ["string", "comment"] }, + ], + surroundingPairs: [ + { open: "{", close: "}" }, + { open: "[", close: "]" }, + { open: "(", close: ")" }, + { open: "'", close: "'" }, + { open: '"', close: '"' }, + ], + }); + + const grammar = await getGrammar(); + if (grammar) { + monaco.languages.setTokensProvider("ggsql", { + getInitialState: () => new TMState(null), + tokenize: ( + line: string, + state: TMState + ): monaco.languages.ILineTokens => { + const result = grammar.tokenizeLine(line, state.ruleStack); + const tokens: monaco.languages.IToken[] = result.tokens.map((t) => ({ + startIndex: t.startIndex, + scopes: scopeToMonacoToken(t.scopes), + })); + return { + tokens, + endState: new TMState(result.ruleStack), + }; + }, + }); + } +} + +// TextMate state wrapper for Monaco +class TMState implements monaco.languages.IState { + constructor(public ruleStack: any) {} + + clone(): TMState { + return new TMState(this.ruleStack); + } + + equals(other: monaco.languages.IState): boolean { + if (!(other instanceof TMState)) return false; + if (!this.ruleStack && !other.ruleStack) return true; + if (!this.ruleStack || !other.ruleStack) return false; + return this.ruleStack.equals(other.ruleStack); + } +} + +export interface EditorInstance { + getValue(): string; + setValue(value: string): void; + editor: monaco.editor.IStandaloneCodeEditor; +} + +const LINE_HEIGHT = 19; +const PADDING_TOP = 8; +const PADDING_BOTTOM = 8; +const MAX_EDITOR_HEIGHT = 400; + +function editorHeight(lineCount: number): number { + const contentHeight = lineCount * LINE_HEIGHT + PADDING_TOP + PADDING_BOTTOM; + return Math.min(contentHeight, MAX_EDITOR_HEIGHT); +} + +export async function createEditor( + container: HTMLElement, + initialValue: string +): Promise { + await ensureLanguageRegistered(); + + const lineCount = initialValue.split("\n").length; + container.style.height = editorHeight(lineCount) + "px"; + + const editor = monaco.editor.create(container, { + value: initialValue, + language: "ggsql", + theme: "vs", + automaticLayout: true, + minimap: { enabled: false }, + fontSize: 13, + lineNumbers: "on", + glyphMargin: false, + folding: false, + lineNumbersMinChars: 2, + scrollBeyondLastLine: false, + wordWrap: "on", + padding: { top: PADDING_TOP, bottom: PADDING_BOTTOM }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + scrollbar: { + vertical: "auto", + horizontal: "hidden", + verticalScrollbarSize: 8, + }, + }); + + // Auto-resize editor height to content + editor.onDidContentSizeChange(() => { + const newLineCount = editor.getModel()?.getLineCount() || lineCount; + container.style.height = editorHeight(newLineCount) + "px"; + editor.layout(); + }); + + return { + getValue: () => editor.getValue(), + setValue: (value: string) => editor.setValue(value), + editor, + }; +} diff --git a/ggsql-wasm/demo/src/quarto/main.ts b/ggsql-wasm/demo/src/quarto/main.ts new file mode 100644 index 00000000..b8df29ee --- /dev/null +++ b/ggsql-wasm/demo/src/quarto/main.ts @@ -0,0 +1,300 @@ +import "./styles.css"; +import vegaEmbed from "vega-embed"; +import { WasmContextManager } from "../context"; +import { createEditor, type EditorInstance } from "./editor"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface CellInfo { + query: string; + rewrittenQuery: string; + cellDiv: HTMLElement; + codeScaffold: HTMLElement; + visId: string | null; + visContainer: HTMLElement | null; + result: string | null; + succeeded: boolean; + error: string | null; + editor: EditorInstance | null; + errorDisplay: HTMLElement | null; +} + +// --------------------------------------------------------------------------- +// Site root +// --------------------------------------------------------------------------- + +const SITE_ROOT = (() => { + const meta = document.querySelector('meta[name="quarto:offset"]'); + return meta?.getAttribute("content") || "./"; +})(); + +// --------------------------------------------------------------------------- +// CSV rewriting +// --------------------------------------------------------------------------- + +function findCsvReferences(queries: string[]): string[] { + const csvFiles = new Set(); + const re = /(?:FROM|JOIN)\s+'([^']+\.csv)'/gi; + for (const q of queries) { + let m: RegExpExecArray | null; + while ((m = re.exec(q)) !== null) { + csvFiles.add(m[1]); + } + } + return Array.from(csvFiles); +} + +function csvTableName(filename: string): string { + return filename.replace(/\.csv$/i, ""); +} + +function rewriteCsvRefs(query: string): string { + return query.replace( + /(?<=FROM|JOIN)\s+'([^']+)\.csv'/gi, + (_match, name) => ` ${name}` + ); +} + +// --------------------------------------------------------------------------- +// Vega embed options +// --------------------------------------------------------------------------- + +const VEGA_EMBED_OPTS = { + actions: { export: true, source: false, compiled: false, editor: false }, + renderer: "svg" as const, +}; + +// --------------------------------------------------------------------------- +// Phase 1: Gather cell metadata from the DOM (no mutations) +// --------------------------------------------------------------------------- + +function gatherCells(): CellInfo[] { + const cells: CellInfo[] = []; + + const codeEls = document.querySelectorAll( + "div.sourceCode.cell-code code.sourceCode.ggsql" + ); + + for (const codeEl of codeEls) { + const query = codeEl.textContent?.trim() || ""; + if (!query) continue; + + const cellDiv = codeEl.closest(".cell"); + if (!cellDiv) continue; + + const codeScaffold = + cellDiv.querySelector(".code-copy-outer-scaffold") || + cellDiv.querySelector(".sourceCode.cell-code"); + if (!codeScaffold) continue; + + const outputDiv = cellDiv.querySelector( + ".cell-output.cell-output-display" + ); + let visId: string | null = null; + let visContainer: HTMLElement | null = null; + + if (outputDiv) { + const visCandidates = outputDiv.querySelectorAll( + 'div[id^="vis-"]' + ); + if (visCandidates.length > 0) { + visContainer = visCandidates[0]; + visId = visContainer.id; + } + } + + cells.push({ + query, + rewrittenQuery: rewriteCsvRefs(query), + cellDiv, + codeScaffold, + visId, + visContainer, + result: null, + succeeded: false, + error: null, + editor: null, + errorDisplay: null, + }); + } + + return cells; +} + +// --------------------------------------------------------------------------- +// Phase 2: Initialize WASM context and execute all cells +// --------------------------------------------------------------------------- + +async function initAndExecute( + cells: CellInfo[] +): Promise { + const ctx = new WasmContextManager(); + + console.log("[ggsql-quarto] Loading WebAssembly…"); + try { + await ctx.initialize(); + } catch (e) { + console.error("[ggsql-quarto] WASM init failed:", e); + return null; + } + + console.log("[ggsql-quarto] Registering datasets…"); + try { + await ctx.registerBuiltinDatasets(); + } catch (e) { + console.error("[ggsql-quarto] Builtin dataset registration failed:", e); + return null; + } + + const csvFiles = findCsvReferences(cells.map((c) => c.query)); + if (csvFiles.length > 0) { + console.log("[ggsql-quarto] Loading data files:", csvFiles.join(", ")); + for (const file of csvFiles) { + try { + const resp = await fetch(SITE_ROOT + file); + if (!resp.ok) throw new Error(`HTTP ${resp.status} for ${file}`); + const bytes = new Uint8Array(await resp.arrayBuffer()); + ctx.registerCSV(csvTableName(file), bytes); + } catch (e) { + console.error(`[ggsql-quarto] Failed to load CSV '${file}':`, e); + return null; + } + } + } + + const total = cells.length; + console.log(`[ggsql-quarto] Executing ${total} cells…`); + for (let i = 0; i < total; i++) { + const cell = cells[i]; + try { + if (ctx.hasVisual(cell.rewrittenQuery)) { + cell.result = ctx.execute(cell.rewrittenQuery); + } else { + ctx.executeSql(cell.rewrittenQuery); + cell.result = null; + } + cell.succeeded = true; + } catch (e: any) { + cell.succeeded = false; + cell.error = String(e); + console.warn( + `[ggsql-quarto] Cell ${i + 1}/${total} failed:`, + cell.query.slice(0, 80), + e + ); + } + } + + const succeeded = cells.filter((c) => c.succeeded).length; + console.log(`[ggsql-quarto] ${succeeded}/${total} cells succeeded`); + + return ctx; +} + +// --------------------------------------------------------------------------- +// Phase 3: Mutate DOM — replace succeeded cells with editors, render results +// --------------------------------------------------------------------------- + +const DEBOUNCE_MS = 100; + +async function applyEditors( + cells: CellInfo[], + ctx: WasmContextManager +): Promise { + for (const cell of cells) { + if (!cell.succeeded) continue; + + const wrapper = document.createElement("div"); + wrapper.className = "ggsql-editor-wrapper"; + + const editorContainer = document.createElement("div"); + editorContainer.className = "ggsql-editor-container"; + wrapper.appendChild(editorContainer); + + const errorDisplay = document.createElement("div"); + errorDisplay.className = "ggsql-error-display"; + wrapper.appendChild(errorDisplay); + cell.errorDisplay = errorDisplay; + + wrapper.appendChild(errorDisplay); + + cell.codeScaffold.replaceWith(wrapper); + + const editorInst = await createEditor(editorContainer, cell.query); + cell.editor = editorInst; + + if (cell.result && cell.visId && cell.visContainer) { + try { + const spec = JSON.parse(cell.result); + cell.visContainer.innerHTML = ""; + await vegaEmbed("#" + cell.visId, spec, VEGA_EMBED_OPTS); + } catch (e) { + console.warn("[ggsql-quarto] vegaEmbed failed for", cell.visId, e); + } + } + + // Re-execute on every edit, debounced + let debounceTimer: number | undefined; + editorInst.editor.onDidChangeModelContent(() => { + clearTimeout(debounceTimer); + debounceTimer = window.setTimeout(() => { + executeCell(cell, editorInst, ctx); + }, DEBOUNCE_MS); + }); + } +} + +async function executeCell( + cell: CellInfo, + editorInst: EditorInstance, + ctx: WasmContextManager +): Promise { + const errorDisplay = cell.errorDisplay!; + errorDisplay.textContent = ""; + errorDisplay.classList.remove("visible"); + + const currentQuery = rewriteCsvRefs(editorInst.getValue()); + + try { + if (ctx.hasVisual(currentQuery)) { + const result = ctx.execute(currentQuery); + const spec = JSON.parse(result); + + if (cell.visContainer && cell.visId) { + cell.visContainer.innerHTML = ""; + await vegaEmbed("#" + cell.visId, spec, VEGA_EMBED_OPTS); + } + } else { + ctx.executeSql(currentQuery); + } + } catch (e: any) { + errorDisplay.textContent = String(e); + errorDisplay.classList.add("visible"); + } +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +async function main() { + const cells = gatherCells(); + if (cells.length === 0) return; + + console.log(`[ggsql-quarto] Found ${cells.length} ggsql cells`); + + const ctx = await initAndExecute(cells); + if (!ctx) return; + + const anySucceeded = cells.some((c) => c.succeeded); + if (!anySucceeded) return; + + await applyEditors(cells, ctx); + console.log("[ggsql-quarto] Done"); +} + +main().catch((e) => { + console.error("[ggsql-quarto] Unexpected error:", e); +}); diff --git a/ggsql-wasm/demo/src/quarto/styles.css b/ggsql-wasm/demo/src/quarto/styles.css new file mode 100644 index 00000000..a692cd54 --- /dev/null +++ b/ggsql-wasm/demo/src/quarto/styles.css @@ -0,0 +1,34 @@ +/* ggsql Quarto interactive editor styles */ + +.ggsql-editor-wrapper { + position: relative; + border: 1px solid #94D2BD; + border-radius: 4px; + overflow: hidden; + margin: 1em 0; +} + +.ggsql-editor-wrapper:focus-within { + border-color: #005F73; + box-shadow: 0 0 0 2px rgba(0, 95, 115, 0.15); +} + +.ggsql-editor-container { + width: 100%; +} + +.ggsql-error-display { + padding: 6px 10px; + font-size: 12px; + font-family: monospace; + color: #AE2012; + background: #FFF0ED; + border-top: 1px solid #E5A4A0; + white-space: pre-wrap; + word-break: break-word; + display: none; +} + +.ggsql-error-display.visible { + display: block; +} diff --git a/ggsql-wasm/demo/src/styles.css b/ggsql-wasm/demo/src/styles.css new file mode 100644 index 00000000..7ba5de28 --- /dev/null +++ b/ggsql-wasm/demo/src/styles.css @@ -0,0 +1,333 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Nunito Sans", -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + height: 100vh; + overflow: hidden; + background: #F9F9F9; + color: #001219; +} + +#app { + display: grid; + grid-template-columns: 200px 1fr 1fr; + grid-template-rows: 40px 1fr 150px; + height: 100vh; + gap: 1px; + background: #94D2BD; +} + +#header { + grid-column: 1 / -1; + background: #DEF1EB; + padding: 0 16px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #94D2BD; +} + +#header h1 { + font-size: 14px; + font-weight: 700; + color: #005F73; +} + +#status { + font-size: 12px; + padding: 3px 10px; + border-radius: 3px; + background: #F9F9F9; + color: #005F73; +} + +#status.loading { + background: #E9D8A6; + color: #001219; +} +#status.success { + background: #94D2BD; + color: #001219; +} +#status.error { + background: #f8d7da; + color: #721c24; +} + +#sidebar { + grid-row: 2 / 4; + background: #DEF1EB; + border-right: 1px solid #94D2BD; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.sidebar-section { + padding: 10px; +} + +.sidebar-section h3 { + font-size: 11px; + text-transform: uppercase; + color: #005F73; + margin-bottom: 8px; + font-weight: 700; + letter-spacing: 0.5px; +} + +.table-list { + list-style: none; +} + +.table-item { + padding: 4px 8px; + margin: 2px 0; + border-radius: 3px; + cursor: default; + font-size: 13px; + font-family: "Fira Code", "Consolas", "Monaco", monospace; + display: flex; + justify-content: space-between; + align-items: center; + color: #001219; +} + +.table-item:hover { + background: rgba(10, 147, 150, 0.1); +} + +.table-item-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.table-item-remove { + opacity: 0; + cursor: pointer; + color: #721c24; + font-size: 16px; + padding: 0 4px; + line-height: 1; +} + +.table-item:hover .table-item-remove { + opacity: 1; +} + +.file-input-wrapper input[type="file"] { + position: absolute; + left: -9999px; +} + +.file-input-label { + display: block; + padding: 6px 8px; + background: #005F73; + color: #F9F9F9; + text-align: center; + border-radius: 3px; + cursor: pointer; + font-size: 12px; + margin-bottom: 6px; +} + +.file-input-label:hover { + background: #0A9396; +} + +.example-section-header { + font-size: 11px; + text-transform: uppercase; + color: #005F73; + font-weight: 700; + letter-spacing: 0.5px; + margin: 10px 0 4px; + padding: 0 2px; +} + +.example-section-header:first-child { + margin-top: 0; +} + +.example-button { + display: block; + width: 100%; + padding: 5px 8px; + margin: 3px 0; + background: #F9F9F9; + border: 1px solid #94D2BD; + color: #001219; + border-radius: 3px; + cursor: pointer; + font-size: 12px; + text-align: left; +} + +.example-button:hover { + background: #94D2BD; + color: #001219; +} + +#editor-container { + background: #F9F9F9; + position: relative; + overflow: hidden; +} + +#viz-container { + display: flex; + flex-direction: column; + overflow: scroll; +} + +#viz-output { + flex: 1; + padding: 16px; + display: flex; + align-items: flex-start; + justify-content: center; +} + +#viz-output .vega-embed { + width: 100%; +} + +#viz-output .chart-wrapper { + min-width: 400px; + min-height: 400px; +} + +#viz-output table.ggsql-table { + border-collapse: separate; + border-spacing: 0; + font-family: "Fira Code", monospace; + font-size: 12px; + width: auto; + background: #FFFFFF; +} + +#viz-output table.ggsql-table th { + background: #DEF1EB; + color: #005F73; + font-weight: 600; + text-align: left; + padding: 6px 12px; + border-bottom: 2px solid #94D2BD; + position: sticky; + top: 0; + z-index: 1; +} + +#viz-output table.ggsql-table td { + padding: 4px 12px; + border-bottom: 1px solid #E0E0E0; + white-space: nowrap; +} + +#viz-output table.ggsql-table tr:hover td { + background: rgba(10, 147, 150, 0.05); +} + +#viz-output table.ggsql-table .truncation-row td { + text-align: center; + color: #005F73; + font-style: italic; + border-bottom: none; +} + +#error-panel { + grid-column: 2 / -1; + background: #F9F9F9; + border-top: 1px solid #94D2BD; + overflow-y: auto; + padding: 8px 16px; + font-family: "Fira Code", "Consolas", "Monaco", monospace; + font-size: 12px; +} + +.panel-label { + font-size: 11px; + text-transform: uppercase; + color: #005F73; + font-weight: 700; + letter-spacing: 0.5px; + margin-bottom: 6px; + font-family: "Nunito Sans", sans-serif; +} + +.error-message { + color: #721c24; + padding: 2px 0; + white-space: pre-wrap; + word-break: break-word; +} + +.warning-message { + color: #856404; + padding: 2px 0; + white-space: pre-wrap; + word-break: break-word; +} + +#mobile-toolbar { + display: none; +} + +@media (max-width: 1024px) { + #app { + grid-template-columns: 1fr; + grid-template-rows: 40px auto 1fr 1fr 100px; + } + + #header { + grid-row: 1; + } + + #mobile-toolbar { + display: flex; + align-items: center; + grid-row: 2; + grid-column: 1; + background: #DEF1EB; + padding: 6px 16px; + border-bottom: 1px solid #94D2BD; + } + + #mobile-example-select { + flex: 1; + padding: 4px 8px; + font-size: 13px; + font-family: "Nunito Sans", sans-serif; + border: 1px solid #94D2BD; + border-radius: 3px; + background: #F9F9F9; + color: #001219; + } + + #sidebar { + display: none; + } + + #viz-container { + grid-row: 3; + grid-column: 1; + } + + #editor-container { + grid-row: 4; + grid-column: 1; + } + + #error-panel { + grid-row: 5; + grid-column: 1; + } +} diff --git a/ggsql-wasm/demo/src/tableManager.ts b/ggsql-wasm/demo/src/tableManager.ts new file mode 100644 index 00000000..4cf70c90 --- /dev/null +++ b/ggsql-wasm/demo/src/tableManager.ts @@ -0,0 +1,94 @@ +import { WasmContextManager } from "./context"; + +export class TableManager { + private listElement: HTMLElement; + private contextManager: WasmContextManager; + private onChangeCallback: (() => void) | null = null; + private onClickCallback: ((tableName: string) => void) | null = null; + + constructor(listElement: HTMLElement, contextManager: WasmContextManager) { + this.listElement = listElement; + this.contextManager = contextManager; + } + + refresh(): void { + const tables = this.contextManager.listTables(); + + this.listElement.innerHTML = ""; + + if (tables.length === 0) { + const emptyItem = document.createElement("li"); + emptyItem.textContent = "No tables registered"; + emptyItem.style.color = "#005F73"; + emptyItem.style.fontSize = "12px"; + emptyItem.style.padding = "6px 8px"; + this.listElement.appendChild(emptyItem); + return; + } + + tables.forEach((tableName) => { + const item = document.createElement("li"); + item.className = "table-item"; + + const nameSpan = document.createElement("span"); + nameSpan.className = "table-item-name"; + nameSpan.textContent = tableName; + + item.appendChild(nameSpan); + item.style.cursor = "pointer"; + item.onclick = () => { + if (this.onClickCallback) this.onClickCallback(tableName); + }; + + if (!tableName.startsWith("ggsql:")) { + const removeBtn = document.createElement("span"); + removeBtn.className = "table-item-remove"; + removeBtn.textContent = "\u00d7"; + removeBtn.title = "Remove table"; + removeBtn.onclick = (e) => { + e.stopPropagation(); + this.removeTable(tableName); + }; + item.appendChild(removeBtn); + } + + this.listElement.appendChild(item); + }); + } + + async uploadFile(file: File): Promise { + const tableName = this.sanitiseTableName(file.name); + const buffer = await file.arrayBuffer(); + const data = new Uint8Array(buffer); + + if (file.name.endsWith(".parquet")) { + await this.contextManager.registerParquet(tableName, data); + } else { + this.contextManager.registerCSV(tableName, data); + } + + this.refresh(); + if (this.onChangeCallback) this.onChangeCallback(); + } + + removeTable(name: string): void { + this.contextManager.unregister(name); + this.refresh(); + if (this.onChangeCallback) this.onChangeCallback(); + } + + onChange(callback: () => void): void { + this.onChangeCallback = callback; + } + + onClickTable(callback: (tableName: string) => void): void { + this.onClickCallback = callback; + } + + private sanitiseTableName(filename: string): string { + return filename + .replace(/\.(csv|parquet|pq)$/i, "") + .replace(/[^a-zA-Z0-9_]/g, "_") + .toLowerCase(); + } +} diff --git a/ggsql-wasm/demo/src/wasmBase.ts b/ggsql-wasm/demo/src/wasmBase.ts new file mode 100644 index 00000000..aa77f92b --- /dev/null +++ b/ggsql-wasm/demo/src/wasmBase.ts @@ -0,0 +1,5 @@ +// Base URL for shared WASM assets (ggsql_wasm_bg.wasm, onig.wasm, etc.) +// Derived from import.meta.url so it resolves relative to the bundle, +// not the page that loads it. Works for both the playground (co-located) +// and quarto pages (loaded cross-directory via dynamic import). +export const WASM_BASE = new URL(".", import.meta.url).href; diff --git a/ggsql-wasm/demo/tsconfig.json b/ggsql-wasm/demo/tsconfig.json new file mode 100644 index 00000000..e4b660da --- /dev/null +++ b/ggsql-wasm/demo/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/ggsql-wasm/library/build.mjs b/ggsql-wasm/library/build.mjs new file mode 100644 index 00000000..4a51888c --- /dev/null +++ b/ggsql-wasm/library/build.mjs @@ -0,0 +1,27 @@ +import * as esbuild from "esbuild"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const isWatch = process.argv.includes("--watch"); + +const buildOptions = { + entryPoints: [join(__dirname, "src/index.ts")], + bundle: true, + outfile: join(__dirname, "dist/lib.js"), + format: "esm", + platform: "browser", + target: "es2020", + sourcemap: true, +}; + +if (isWatch) { + console.log("Starting watch mode..."); + const ctx = await esbuild.context(buildOptions); + await ctx.watch(); + console.log("Watching for changes..."); +} else { + console.log("Building library..."); + await esbuild.build(buildOptions); + console.log("Build complete!"); +} diff --git a/ggsql-wasm/library/package-lock.json b/ggsql-wasm/library/package-lock.json new file mode 100644 index 00000000..3e6fc917 --- /dev/null +++ b/ggsql-wasm/library/package-lock.json @@ -0,0 +1,523 @@ +{ + "name": "ggsql-wasm-lib", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ggsql-wasm-lib", + "version": "0.1.0", + "dependencies": { + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "typescript": "^5.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/hyparquet": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/hyparquet/-/hyparquet-1.25.1.tgz", + "integrity": "sha512-CXcN/u6RdQqsK8IphUptpAEqY8IzgwzHY+MuXX+2wpoWTumfxPVr6JYbbywsNsiAl9aEbM5sRtxkwRBa22b49w==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/ggsql-wasm/library/package.json b/ggsql-wasm/library/package.json new file mode 100644 index 00000000..b29653c1 --- /dev/null +++ b/ggsql-wasm/library/package.json @@ -0,0 +1,18 @@ +{ + "name": "ggsql-wasm-lib", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.mjs", + "dev": "node build.mjs --watch", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "hyparquet": "^1.25.0" + }, + "devDependencies": { + "esbuild": "^0.27.0", + "typescript": "^5.9.0" + } +} diff --git a/ggsql-wasm/library/src/csv.ts b/ggsql-wasm/library/src/csv.ts new file mode 100644 index 00000000..68162075 --- /dev/null +++ b/ggsql-wasm/library/src/csv.ts @@ -0,0 +1,181 @@ +import type { ColumnDescriptor, ColumnType } from "./index"; + +/** + * Convert CSV bytes to column descriptors. + * Synchronous — no external dependencies. + */ +export function convert_csv(bytes: Uint8Array): ColumnDescriptor[] { + const text = new TextDecoder().decode(bytes); + const lines = parseCSVLines(text); + + if (lines.length < 2) return []; + + const headers = lines[0]; + const nCols = headers.length; + const nRows = lines.length - 1; + + // Collect raw string values per column + const rawCols: (string | null)[][] = []; + for (let c = 0; c < nCols; c++) { + rawCols.push(new Array(nRows)); + } + + for (let r = 0; r < nRows; r++) { + const row = lines[r + 1]; + for (let c = 0; c < nCols; c++) { + const val = c < row.length ? row[c] : ""; + rawCols[c][r] = val === "" ? null : val; + } + } + + // Per-column type inference and conversion + const columns: ColumnDescriptor[] = []; + for (let c = 0; c < nCols; c++) { + const raw = rawCols[c]; + const colType = inferCSVColumnType(raw); + columns.push(buildCSVColumn(headers[c], raw, colType)); + } + + return columns; +} + +function inferCSVColumnType(values: (string | null)[]): ColumnType { + let allInt = true; + let allNum = true; + let allBool = true; + + for (let i = 0; i < values.length; i++) { + const v = values[i]; + if (v === null) continue; + + const lower = v.toLowerCase(); + if (lower !== "true" && lower !== "false") allBool = false; + + const num = Number(v); + if (v === "" || isNaN(num)) { + allNum = false; + allInt = false; + } else { + if (!Number.isSafeInteger(num)) allInt = false; + } + } + + if (allBool) return "bool"; + if (allInt) return "i64"; + if (allNum) return "f64"; + return "string"; +} + +function buildCSVColumn( + name: string, + rawStrings: (string | null)[], + colType: ColumnType, +): ColumnDescriptor { + const len = rawStrings.length; + const nulls = new Uint8Array(len); + + if (colType === "f64" || colType === "i64") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + if (rawStrings[i] === null) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = Number(rawStrings[i]); + nulls[i] = 1; + } + } + return { name, type: colType, values, nulls }; + } + + if (colType === "bool") { + const values = new Uint8Array(len); + for (let i = 0; i < len; i++) { + if (rawStrings[i] === null) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = rawStrings[i]!.toLowerCase() === "true" ? 1 : 0; + nulls[i] = 1; + } + } + return { name, type: colType, values, nulls }; + } + + // string + const values: string[] = []; + for (let i = 0; i < len; i++) { + if (rawStrings[i] === null) { + values.push(""); + nulls[i] = 0; + } else { + values.push(rawStrings[i]!); + nulls[i] = 1; + } + } + return { name, type: "string", values, nulls }; +} + +/** + * Parse CSV text into an array of rows (each row is an array of fields). + * Handles quoted fields, embedded commas, and embedded newlines. + */ +function parseCSVLines(text: string): string[][] { + const rows: string[][] = []; + let row: string[] = []; + let field = ""; + let inQuotes = false; + let i = 0; + + while (i < text.length) { + const ch = text[i]; + + if (inQuotes) { + if (ch === '"') { + if (i + 1 < text.length && text[i + 1] === '"') { + field += '"'; + i += 2; + } else { + inQuotes = false; + i++; + } + } else { + field += ch; + i++; + } + } else { + if (ch === '"') { + inQuotes = true; + i++; + } else if (ch === ",") { + row.push(field); + field = ""; + i++; + } else if (ch === "\r") { + row.push(field); + field = ""; + rows.push(row); + row = []; + i++; + if (i < text.length && text[i] === "\n") i++; + } else if (ch === "\n") { + row.push(field); + field = ""; + rows.push(row); + row = []; + i++; + } else { + field += ch; + i++; + } + } + } + + // Handle last field/row + if (field.length > 0 || row.length > 0) { + row.push(field); + rows.push(row); + } + + return rows; +} diff --git a/ggsql-wasm/library/src/index.ts b/ggsql-wasm/library/src/index.ts new file mode 100644 index 00000000..3b130354 --- /dev/null +++ b/ggsql-wasm/library/src/index.ts @@ -0,0 +1,22 @@ +// Converters +export { convert_csv } from "./csv"; +export { convert_parquet } from "./parquet"; + +// Types +export interface ColumnDescriptor { + name: string; + type: ColumnType; + values: Float64Array | Uint8Array | string[]; + nulls: Uint8Array; +} + +export type ColumnType = + | "f64" + | "i64" + | "bool" + | "date" + | "datetime" + | "string"; + +export const EPOCH = Date.UTC(1970, 0, 1); +export const MS_PER_DAY = 86400000; diff --git a/ggsql-wasm/library/src/parquet.ts b/ggsql-wasm/library/src/parquet.ts new file mode 100644 index 00000000..7789161b --- /dev/null +++ b/ggsql-wasm/library/src/parquet.ts @@ -0,0 +1,158 @@ +import type { ColumnDescriptor, ColumnType } from "./index"; +import { EPOCH, MS_PER_DAY } from "./index"; +import { parquetReadObjects } from "hyparquet"; + +/** + * Convert Parquet bytes to column descriptors. + * Dynamically imports hyparquet. + */ +export async function convert_parquet( + bytes: Uint8Array, +): Promise { + const buffer = bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength, + ); + + const asyncBuffer = { + byteLength: buffer.byteLength, + slice: (start: number, end: number) => + Promise.resolve(buffer.slice(start, end) as ArrayBuffer), + }; + + const rows: Record[] = await parquetReadObjects({ + file: asyncBuffer, + }); + + if (rows.length === 0) return []; + + const colNames = Object.keys(rows[0]); + const columns: ColumnDescriptor[] = []; + + for (const colName of colNames) { + const rawValues = rows.map((row) => row[colName]); + columns.push(buildColumn(colName, rawValues)); + } + + return columns; +} + +function inferColumnType(values: unknown[]): ColumnType { + let hasNumber = false; + let hasBool = false; + let hasDate = false; + let allSafeInt = true; + let allMidnight = true; + + for (let i = 0; i < values.length; i++) { + const v = values[i]; + if (v === null || v === undefined) continue; + + if (v instanceof Date) { + hasDate = true; + if ( + v.getUTCHours() !== 0 || + v.getUTCMinutes() !== 0 || + v.getUTCSeconds() !== 0 || + v.getUTCMilliseconds() !== 0 + ) { + allMidnight = false; + } + } else if (typeof v === "boolean") { + hasBool = true; + } else if (typeof v === "number") { + hasNumber = true; + if (!Number.isSafeInteger(v)) allSafeInt = false; + } else if (typeof v === "bigint") { + hasNumber = true; + // bigint values will be converted to Number, keep as i64 + } else { + return "string"; + } + } + + if (hasDate) return allMidnight ? "date" : "datetime"; + if (hasBool && !hasNumber) return "bool"; + if (hasNumber) return allSafeInt ? "i64" : "f64"; + return "string"; +} + +function buildColumn(name: string, rawValues: unknown[]): ColumnDescriptor { + const len = rawValues.length; + const nulls = new Uint8Array(len); + const type = inferColumnType(rawValues); + + if (type === "f64" || type === "i64") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i]; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = Number(v); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + if (type === "bool") { + const values = new Uint8Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i]; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = v ? 1 : 0; + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + if (type === "date") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i] as Date | null | undefined; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = Math.floor((v.getTime() - EPOCH) / MS_PER_DAY); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + if (type === "datetime") { + const values = new Float64Array(len); + for (let i = 0; i < len; i++) { + const v = rawValues[i] as Date | null | undefined; + if (v === null || v === undefined) { + values[i] = 0; + nulls[i] = 0; + } else { + values[i] = v.getTime(); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; + } + + // string + const values: string[] = []; + for (let i = 0; i < len; i++) { + const v = rawValues[i]; + if (v === null || v === undefined) { + values.push(""); + nulls[i] = 0; + } else { + values.push(String(v)); + nulls[i] = 1; + } + } + return { name, type, values, nulls }; +} diff --git a/ggsql-wasm/library/tsconfig.json b/ggsql-wasm/library/tsconfig.json new file mode 100644 index 00000000..b2699a94 --- /dev/null +++ b/ggsql-wasm/library/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/ggsql-wasm/src/lib.rs b/ggsql-wasm/src/lib.rs index 3cb1d1bc..d007a6e3 100644 --- a/ggsql-wasm/src/lib.rs +++ b/ggsql-wasm/src/lib.rs @@ -1,16 +1,166 @@ -use ggsql::reader::{PolarsReader, Reader}; +use ggsql::naming::DATA_PREFIX; +use ggsql::reader::sqlite::SqliteReader; +use ggsql::reader::Reader; +use ggsql::validate::validate; use ggsql::writer::{VegaLiteWriter, Writer}; +use ggsql::DataFrame; +use polars::prelude::IntoColumn; +use polars::prelude::*; +use serde_json::json; use std::cell::RefCell; use wasm_bindgen::prelude::*; +// ============================================================================ +// JS bridge declarations — CSV and Parquet parsing only +// ============================================================================ + +#[wasm_bindgen(module = "/library/dist/lib.js")] +extern "C" { + #[wasm_bindgen(catch)] + async fn convert_parquet(data: &[u8]) -> Result; + + #[wasm_bindgen(catch)] + fn convert_csv(data: &[u8]) -> Result; +} + +// ============================================================================ +// SQLite VFS initialization (wasm32 only) +// ============================================================================ + +#[cfg(target_arch = "wasm32")] +fn ensure_vfs_initialized() { + use std::sync::Once; + static INIT: Once = Once::new(); + INIT.call_once(|| { + let _ = sqlite_wasm_rs::MemVfsUtil::::new(); + }); +} + +#[cfg(not(target_arch = "wasm32"))] +fn ensure_vfs_initialized() { + // No VFS initialization needed on native targets +} + +// ============================================================================ +// Column descriptor → DataFrame conversion (for JS CSV/Parquet parsing) +// ============================================================================ + +/// Convert JS column descriptors to a Polars DataFrame. +fn columns_js_to_dataframe(columns_js: JsValue) -> Result { + let columns = js_sys::Array::from(&columns_js); + let len = columns.length(); + + if len == 0 { + return Ok(DataFrame::empty()); + } + + let mut series_vec: Vec = Vec::with_capacity(len as usize); + + for i in 0..len { + let col = columns.get(i); + let col_name = js_sys::Reflect::get(&col, &"name".into()) + .map_err(|_| JsValue::from_str("Missing column name"))? + .as_string() + .ok_or_else(|| JsValue::from_str("Column name is not a string"))?; + let col_type = js_sys::Reflect::get(&col, &"type".into()) + .map_err(|_| JsValue::from_str("Missing column type"))? + .as_string() + .ok_or_else(|| JsValue::from_str("Column type is not a string"))?; + let values_js = js_sys::Reflect::get(&col, &"values".into()) + .map_err(|_| JsValue::from_str("Missing column values"))?; + let nulls_js = js_sys::Reflect::get(&col, &"nulls".into()) + .map_err(|_| JsValue::from_str("Missing column nulls"))?; + + let nulls = js_sys::Uint8Array::new(&nulls_js).to_vec(); + + let series = match col_type.as_str() { + "f64" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v) } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "i64" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v as i64) } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "bool" => { + let raw = js_sys::Uint8Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v != 0) } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "string" => { + let arr = js_sys::Array::from(&values_js); + let values: Vec> = (0..arr.length()) + .zip(nulls.iter()) + .map(|(j, &n)| if n != 0 { arr.get(j).as_string() } else { None }) + .collect(); + Series::new(col_name.as_str().into(), values) + } + "date" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v as i32) } else { None }) + .collect(); + let s = Series::new(col_name.as_str().into(), values); + s.cast(&DataType::Date).map_err(|e| { + JsValue::from_str(&format!("Date cast error for '{}': {}", col_name, e)) + })? + } + "datetime" => { + let raw = js_sys::Float64Array::new(&values_js).to_vec(); + let values: Vec> = raw + .into_iter() + .zip(nulls.iter()) + .map(|(v, &n)| if n != 0 { Some(v as i64) } else { None }) + .collect(); + let s = Series::new(col_name.as_str().into(), values); + s.cast(&DataType::Datetime(TimeUnit::Milliseconds, None)) + .map_err(|e| { + JsValue::from_str(&format!("Datetime cast error for '{}': {}", col_name, e)) + })? + } + other => { + return Err(JsValue::from_str(&format!( + "Unknown column type: '{}'", + other + ))); + } + }; + + series_vec.push(series.into_column()); + } + + DataFrame::new(series_vec) + .map_err(|e| JsValue::from_str(&format!("DataFrame creation error: {}", e))) +} + +// ============================================================================ +// GgsqlContext - public WASM API +// ============================================================================ + /// Persistent ggsql context for WASM /// /// Create once and reuse for multiple queries to avoid memory issues. /// Uses interior mutability to avoid wasm_bindgen's &mut self aliasing issues. #[wasm_bindgen] pub struct GgsqlContext { - reader: RefCell, + reader: RefCell, writer: VegaLiteWriter, } @@ -19,8 +169,10 @@ impl GgsqlContext { /// Create a new ggsql context #[wasm_bindgen(constructor)] pub fn new() -> Result { - let reader = PolarsReader::from_connection_string("polars://memory") - .map_err(|e| JsValue::from_str(&format!("Reader error: {:?}", e)))?; + ensure_vfs_initialized(); + + let reader = SqliteReader::new() + .map_err(|e| JsValue::from_str(&format!("Failed to create SQLite reader: {:?}", e)))?; let writer = VegaLiteWriter::new(); Ok(GgsqlContext { reader: RefCell::new(reader), @@ -30,9 +182,8 @@ impl GgsqlContext { /// Execute a ggsql query and return Vega-Lite JSON pub fn execute(&self, query: &str) -> Result { - // Scope the mutable borrow to avoid aliasing issues let spec = { - let reader = self.reader.borrow_mut(); + let reader = self.reader.borrow(); reader .execute(query) .map_err(|e| JsValue::from_str(&format!("Execute error: {:?}", e)))? @@ -46,9 +197,99 @@ impl GgsqlContext { Ok(result) } - // TODO: Register a table from binary data (e.g. CSV, Parquet) - pub fn register(&self, _name: &str) -> Result<(), JsValue> { - Err(JsValue::from_str("Registration not yet implemented.")) + /// Check whether a query contains a VISUALISE clause + pub fn has_visual(&self, query: &str) -> bool { + match validate(query) { + Ok(v) => v.has_visual(), + Err(_) => false, + } + } + + /// Execute SQL-only query and return JSON with columns/rows + pub fn execute_sql(&self, query: &str) -> Result { + let df = { + let reader = self.reader.borrow(); + reader + .execute_sql(query) + .map_err(|e| JsValue::from_str(&format!("SQL error: {:?}", e)))? + }; + + let max_rows = 100usize; + let total_rows = df.height(); + let truncated = total_rows > max_rows; + let df = if truncated { + df.head(Some(max_rows)) + } else { + df + }; + + let columns: Vec = df + .get_column_names() + .into_iter() + .map(|s| s.to_string()) + .collect(); + let mut rows: Vec> = Vec::with_capacity(df.height()); + + for i in 0..df.height() { + let mut row = Vec::with_capacity(columns.len()); + for col in df.get_columns() { + let val = col + .get(i) + .map_err(|e| JsValue::from_str(&format!("Error reading row {}: {}", i, e)))?; + row.push(format!("{}", val)); + } + rows.push(row); + } + + let result = json!({ + "columns": columns, + "rows": rows, + "total_rows": total_rows, + "truncated": truncated, + }); + + serde_json::to_string(&result).map_err(|e| JsValue::from_str(&format!("JSON error: {}", e))) + } + + /// Register a CSV file as a table from raw bytes + pub fn register_csv(&self, name: &str, data: &[u8]) -> Result<(), JsValue> { + let columns_js = convert_csv(data) + .map_err(|e| JsValue::from_str(&format!("CSV parse error: {:?}", e)))?; + let df = columns_js_to_dataframe(columns_js)?; + let reader = self.reader.borrow(); + reader + .register(name, df, true) + .map_err(|e| JsValue::from_str(&format!("Registration error: {:?}", e))) + } + + /// Register a Parquet file as a table from raw bytes + pub async fn register_parquet(&self, name: &str, data: &[u8]) -> Result<(), JsValue> { + let columns_js = convert_parquet(data) + .await + .map_err(|e| JsValue::from_str(&format!("Parquet parse error: {:?}", e)))?; + let df = columns_js_to_dataframe(columns_js)?; + let reader = self.reader.borrow(); + reader + .register(name, df, true) + .map_err(|e| JsValue::from_str(&format!("Registration error: {:?}", e))) + } + + /// Register all known builtin datasets (e.g. ggsql:penguins) + pub async fn register_builtin_datasets(&self) -> Result<(), JsValue> { + for &name in ggsql::reader::data::KNOWN_DATASETS { + if let Some(bytes) = ggsql::reader::data::builtin_parquet_bytes(name) { + let table_name = ggsql::naming::builtin_data_table(name); + let columns_js = convert_parquet(bytes).await.map_err(|e| { + JsValue::from_str(&format!("Parquet error for '{}': {:?}", name, e)) + })?; + let df = columns_js_to_dataframe(columns_js)?; + let reader = self.reader.borrow(); + reader.register(&table_name, df, true).map_err(|e| { + JsValue::from_str(&format!("Registration error for '{}': {:?}", name, e)) + })?; + } + } + Ok(()) } /// Unregister a table @@ -56,9 +297,7 @@ impl GgsqlContext { let reader = self.reader.borrow(); reader .unregister(name) - .map_err(|e| JsValue::from_str(&format!("Unregister error: {:?}", e)))?; - - Ok(()) + .map_err(|e| JsValue::from_str(&format!("Unregister error: {:?}", e))) } /// List all registered tables @@ -70,6 +309,17 @@ impl GgsqlContext { for table in tables { array.push(&JsValue::from_str(&table)); } + + // Builtin datasets (translate internal name → ggsql:name) + for table in reader.list_tables(true) { + if let Some(name) = table + .strip_prefix(DATA_PREFIX) + .and_then(|s| s.strip_suffix("__")) + { + array.push(&JsValue::from_str(&format!("ggsql:{}", name))); + } + } + array.into() } } diff --git a/src/naming.rs b/src/naming.rs index b80d5942..882f40dc 100644 --- a/src/naming.rs +++ b/src/naming.rs @@ -70,7 +70,7 @@ const LAYER_PREFIX: &str = concatcp!(GGSQL_PREFIX, "layer_"); const AES_PREFIX: &str = concatcp!(GGSQL_PREFIX, "aes_"); /// Full prefix for builtin data tables: `__ggsql_data_` -const DATA_PREFIX: &str = concatcp!(GGSQL_PREFIX, "data_"); +pub const DATA_PREFIX: &str = concatcp!(GGSQL_PREFIX, "data_"); /// Key for global data in the layer data HashMap. /// Used as the key in PreparedData.data to store global data that applies to all layers. diff --git a/src/reader/data.rs b/src/reader/data.rs index fac5639a..99b528e9 100644 --- a/src/reader/data.rs +++ b/src/reader/data.rs @@ -28,7 +28,7 @@ static AIRQUALITY: &[u8] = include_bytes!(concat!( /// Get the embedded parquet bytes for a known builtin dataset. #[cfg(feature = "builtin-data")] -fn builtin_parquet_bytes(name: &str) -> Option<&'static [u8]> { +pub fn builtin_parquet_bytes(name: &str) -> Option<&'static [u8]> { match name { "penguins" => Some(PENGUINS), "airquality" => Some(AIRQUALITY), @@ -86,7 +86,7 @@ pub fn register_builtin_datasets_duckdb( // Polars-based builtin data loading // ============================================================================= -#[cfg(feature = "builtin-data")] +#[cfg(feature = "parquet")] pub fn load_builtin_dataframe(name: &str) -> Result { use polars::prelude::*; use std::io::Cursor; @@ -109,7 +109,7 @@ pub fn load_builtin_dataframe(name: &str) -> Result bool { diff --git a/src/reader/sqlite.rs b/src/reader/sqlite.rs index 96981bea..09d8b015 100644 --- a/src/reader/sqlite.rs +++ b/src/reader/sqlite.rs @@ -97,6 +97,18 @@ impl SqliteReader { &self.conn } + /// List table names known to this reader. + /// + /// When `internal` is false, filters out internal tables (prefixed with `__ggsql_`). + pub fn list_tables(&self, internal: bool) -> Vec { + self.registered_tables + .borrow() + .iter() + .filter(|name| internal || !name.starts_with("__ggsql_")) + .cloned() + .collect() + } + /// Check if a table is registered fn table_exists(&self, name: &str) -> bool { let sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1";