diff --git a/CHANGELOG.md b/CHANGELOG.md index a52635553..db602e5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ ### Added - New `HybridReader` that composes any primary `Reader` with an in-process `DuckDBReader` for staging. `register()` writes to staging; `execute_sql` routes queries that reference registered names to staging and everything else to the primary. Available behind the existing `duckdb` feature. +- `HybridReader` now caches query results in its staging DuckDB to keep + visualization iteration fast across `DRAW`/`SCALE`/`FACET` tweaks. Cache + hits are sub-millisecond; entries are evicted by TTL (default 300s) and + by an LRU byte-budget (default 512 MB). Tunable via + `HybridReader::with_cache_config(...)` and globally disabled with + `GGSQL_HYBRID_CACHE_DISABLED=1`. The Jupyter kernel adds a + `-- @uncache` meta-command that clears the cache without restarting the + session, and the kernel now emits both `application/vnd.vegalite.v5+json` + and `v6+json` mime payloads so JupyterLab 4.x (built-in v5 renderer) and + nteract / newer Lab extensions (v6 renderer) both display visualizations + natively without falling back to embedded HTML. +- `Reader::clear_cache()` trait method (default `Ok(())`) — readers without + a cache inherit it as a no-op; `HybridReader` overrides to drop its + cache tables. - New `AdbcReader` for connecting to data sources via [ADBC](https://arrow.apache.org/adbc/) (Arrow Database Connectivity), behind a new off-by-default `adbc` feature flag. Generic over any concrete diff --git a/Cargo.lock b/Cargo.lock index 2dbe4d44b..f005bd516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2079,6 +2079,7 @@ dependencies = [ "csscolorparser", "duckdb", "geozero", + "hex", "jsonschema", "libloading", "palette", @@ -2088,10 +2089,12 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "sha2", "sprintf", "tempfile", "thiserror 1.0.69", "toml_edit 0.22.27", + "tracing", "tree-sitter", "tree-sitter-ggsql", "ureq", diff --git a/ggsql-jupyter/src/display.rs b/ggsql-jupyter/src/display.rs index ebf03a7ef..9896995b2 100644 --- a/ggsql-jupyter/src/display.rs +++ b/ggsql-jupyter/src/display.rs @@ -100,15 +100,71 @@ fn format_connection_changed(display_name: &str) -> Value { /// Format Vega-Lite visualization as display_data fn format_vegalite(spec: String, hints: &RenderHints) -> Value { + // Parse the spec ONCE up front. If it is not valid JSON we cannot + // produce a meaningful Vega-Lite bundle and would only confuse the + // notebook frontend by emitting a v5/v6 mime payload that wraps an + // `{"error": "..."}` placeholder. Return a plain-text error output in + // that case so the failure surfaces cleanly to the user instead of + // rendering as a silently broken chart. + let spec_value: Value = match serde_json::from_str(&spec) { + Ok(v) => v, + Err(e) => { + tracing::error!("Failed to parse Vega-Lite JSON: {}", e); + return json!({ + "data": { + "text/plain": format!("ggsql: invalid Vega-Lite spec: {e}") + }, + "metadata": {}, + "transient": {} + }); + } + }; + let html = vegalite_html(&spec, hints); + + // Rewrite the spec's $schema to v5 for the v5 mime bundle so clients + // that validate the schema URL against the mime version (notably + // JupyterLab 4.x's built-in @jupyterlab/vega5-extension) accept it. + // The two mime payloads are otherwise identical; ggsql's generated + // specs use core Vega-Lite features that are stable across v5 and v6. + let mut spec_v5 = spec_value.clone(); + if let Some(obj) = spec_v5.as_object_mut() { + obj.insert( + "$schema".to_string(), + json!("https://vega.github.io/schema/vega-lite/v5.json"), + ); + } + json!({ "data": { + // Newer native mime bundle. nteract and newer Lab extensions + // render this directly. JupyterLab 4.x does NOT have a built-in + // v6 renderer and will fall through to the v5 bundle below. + "application/vnd.vegalite.v6+json": spec_value, + + // v5 native mime bundle — JupyterLab 4.x has a built-in + // renderer for this (no extra extensions, no script execution, + // no CDN round-trip). Chosen preferentially over text/html. + "application/vnd.vegalite.v5+json": spec_v5, + + // HTML with embedded vega-embed as a last-resort fallback for + // clients that lack any native Vega-Lite renderer. Requires + // notebook trust because it contains a