diff --git a/CHANGELOG.md b/CHANGELOG.md index ab860a7..9b9677e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Enabled the crate's `spatial` feature, so `DRAW spatial` map layers now render through the Vega-Lite writer instead of raising "Spatial visualization requires the 'spatial' feature to be enabled". Spatial layers require DuckDB's `spatial` extension at runtime: ggsql issues `LOAD spatial` automatically but never `INSTALL` it, so run `INSTALL spatial` once per machine (e.g. via `DuckDBReader.execute_sql()`; the download is cached under `~/.duckdb/extensions`). Works with WKB geometry columns and the built-in `ggsql:world` dataset, including named map projections such as `PROJECT TO equal_earth`. +- New `CachingReader.execute_sql_cached()` method that runs SQL on the reader's private in-memory cache connection — the compute surface where all ggsql-generated derived SQL executes — instead of the primary data source. This is the surface for per-connection setup that derived SQL depends on: e.g. `INSTALL`/`LOAD` of DuckDB extensions such as `spatial`, which would fail on a locked-down primary and take no effect on the cache even where it succeeds. ### Changed diff --git a/src/lib.rs b/src/lib.rs index e1a4bc9..306af66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -741,6 +741,33 @@ impl PyCachingReader { df_to_py(py, &df) } + /// Execute a SQL query against the cache backend (compute surface). + /// + /// All ggsql-generated derived SQL runs on the private in-memory cache + /// connection, so this is the surface to use for statements that must + /// take effect there — e.g. `INSTALL spatial; LOAD spatial`, which the + /// spatial geom requires on the cache connection. Routing such + /// statements through `execute_sql` would send them to the primary data + /// source instead, where they typically fail (locked-down connections) + /// and do nothing for the cache. + /// + /// Parameters + /// ---------- + /// sql : str + /// The SQL statement to execute on the cache connection. + /// + /// Returns + /// ------- + /// pyarrow.Table + /// The query result as a pyarrow Table. + fn execute_sql_cached(&self, py: Python<'_>, sql: &str) -> PyResult> { + let df = self + .inner + .execute_sql_cached(sql) + .map_err(|e| PyErr::new::(e.to_string()))?; + df_to_py(py, &df) + } + /// Execute a ggsql query and return the visualization specification. /// /// Base reads hit the (memoized) primary; derived tables are diff --git a/tests/test_ggsql.py b/tests/test_ggsql.py index 3d47051..ceb57c9 100644 --- a/tests/test_ggsql.py +++ b/tests/test_ggsql.py @@ -1062,6 +1062,24 @@ def test_execute_sql(self): table = reader.execute_sql("SELECT 1 AS x") assert table.num_rows == 1 + def test_execute_sql_cached(self): + """execute_sql_cached runs on the cache, never touching the primary. + + This is the surface for per-connection setup that ggsql's derived + SQL depends on (e.g. INSTALL/LOAD of DuckDB extensions), which + would fail or take no effect on a locked-down primary. + """ + primary = _CountingReader() + reader = ggsql.CachingReader(primary) + table = reader.execute_sql_cached("SELECT 42 AS x") + assert table.column("x").to_pylist() == [42] + assert primary.calls == 0 + + def test_execute_sql_cached_error(self): + reader = ggsql.CachingReader("duckdb://memory") + with pytest.raises(ValueError): + reader.execute_sql_cached("SELECT * FROM __ggsql_missing__") + def test_register_and_query(self): reader = ggsql.CachingReader("duckdb://memory") reader.register("mytab", pa.table({"x": [1, 2], "y": [3, 4]}))