Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Py<PyAny>> {
let df = self
.inner
.execute_sql_cached(sql)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(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
Expand Down
18 changes: 18 additions & 0 deletions tests/test_ggsql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]}))
Expand Down
Loading