Skip to content

feat(pkg-r): route ggsql viz execution through ggsql::caching_reader() - #327

Draft
cpsievert wants to merge 5 commits into
mainfrom
ggsql-caching-reader-r
Draft

cpsievert wants to merge 5 commits into
mainfrom
ggsql-caching-reader-r

Conversation

@cpsievert

@cpsievert cpsievert commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Note: this PR was drafted with AI assistance and has not had rigorous human review. It's intended to start a discussion on whether this is an acceptable direction — please treat it accordingly.

Summary

The pkg-r counterpart of #321. Replaces querychat-R's hand-rolled SQL/VISUALISE split (pkg-r/R/querychat_viz.R) with whole-query execution through ggsql::caching_reader() wrapping the querychat executor:

  • Base reads go to the executor (database pushdown preserved) and are memoized for the cache TTL — iterative chart refinement over one shared base read now costs 1 source read instead of N.
  • Derived tables live in the reader's private in-memory DuckDB cache, so layer-level sources (DRAW ... FROM <table>) now run end-to-end — previously rejected outright by has_layer_level_source(). Note 3 of the R ggsql-syntax.md prompt is updated accordingly: it now documents attaching FROM <source> to a layer's MAPPING, with an example combining raw points from the final SELECT and a summary line from a CTE.
  • execute_ggsql() takes a reader and the raw query string; callers wanting validation feedback run ggsql::ggsql_validate() first (a Validated splits layer-level sources into overlapping SQL/visual parts that can't be concatenated back).
  • One caching reader per chat client (created lazily on first viz call), so memoized reads are shared across the session and the cache is freed when the client is dropped. restore_viz_widgets() creates one reader per restore pass.
  • Lazy tbl_sql results are materialized with dplyr::collect() in the bridge.

Dependency

Requires ggsql::caching_reader(), available in ggsql-r >= 0.5.2.9000 — see posit-dev/ggsql-r#18 (still draft, which is why this PR is too). The floor is enforced at the tool gates: with an older ggsql, check_viz_deps() drops the "visualize" tool with a warning (the established UX for missing ggsql) rather than erroring at call time, the package remains installable against ggsql 0.5.2, and viz-related tests skip below the floor. A temporary Remotes: posit-dev/ggsql-r#18 in pkg-r/DESCRIPTION makes CI install the dev ggsql so the floor-gated tests actually run there; it must be removed before release (Remotes is not CRAN-compliant) once a ggsql release with caching_reader() is on CRAN/r-universe.

Docs

New "Caching" section in vignette("visualize"): memoization behavior, the staleness caveat (charts can lag data changes by up to the TTL when update queries are enabled or the database is written externally), and the GGSQL_CACHE_TTL / GGSQL_CACHE_MAX_BYTES / GGSQL_CACHE_DISABLED knobs. The "How it works" steps now describe whole-query execution through the caching reader.

Benchmark

Where caching pays off, and how we verified it. Querychat's viz loop is iterative refinement: the LLM reissues the same data query with a different chart encoding again and again. Before this change, every chart call re-executed that query against the data source from scratch — the dominant cost of the loop. With a caching reader, one reader lives for the chat session and reuses the first result whenever the SQL text repeats, so a refinement costs no source read at all. As evidence, we simulated a typical session end to end in R (counting executor around a DataFrameSource): nine chart refinements over one shared base read, one exact repeat, and one multi-source query of a kind the old code rejected outright. The data source was read once instead of ten times, the multi-source query ran, and bookmark restore of two widgets sharing a base read cost 1 source read. The benefit is smaller for cheap local sources and largest for remote databases, where every eliminated read is a network round trip.

Test plan

  • test-querychat-viz.R rewritten for the reader-first API: basic/uppercase-column queries, FROM before VISUALISE, layer-level FROM sources (previously rejected), a raw + summary overlay via CTE, plus a "viz caching behavior" block proving identical queries share memoized reads through one reader and independent readers don't share.
  • Viz-tool registration tests in test-QueryChat.R skip below the ggsql floor, matching the tool-gate behavior.

@cpsievert cpsievert changed the title Route pkg-r ggsql viz execution through ggsql::caching_reader() feat(pkg-r): route ggsql viz execution through ggsql::caching_reader() Oct 13, 2026
Port of the Python approach in #321 to pkg-r: replace the hand-rolled
SQL/VISUALISE split with whole-query execution through a caching reader
wrapping the querychat executor.

- Base reads go to the executor (database pushdown preserved) and are
  memoized for the cache TTL: a session of chart refinements over one
  shared base read costs 1 source read instead of N.
- Derived tables live in the reader's private in-memory cache, so
  layer-level sources (DRAW ... FROM <table>) now run end to end;
  previously rejected outright. The ggsql-syntax.md prompt update on the
  base branch now holds for R as well.
- execute_ggsql() takes a reader and the raw query string; validation
  feedback still runs through ggsql::ggsql_validate() first.
- One caching reader per chat client (created lazily on first viz call),
  and one per restore_viz_widgets() pass so bookmarked widgets share
  memoized base reads — the regression the Python side hit.
- tbl_sql results are materialized with dplyr::collect() in the bridge.

Requires ggsql >= 0.5.2.9000 (enforced lazily via check_installed()).
Docs: new "Caching" section in vignette("visualize") covering
memoization, the staleness caveat, and the GGSQL_CACHE_* knobs.
Port of the prompt update from #321's branch to main's revised
ggsql-syntax.md (which landed via #322 with the rejected-stance note 3).
Accurate once viz execution routes through the caching reader.
@cpsievert
cpsievert force-pushed the ggsql-caching-reader-r branch from 1749e64 to aa71e16 Compare October 13, 2026 16:42
@cpsievert
cpsievert changed the base branch from ggsql-caching-reader to main October 13, 2026 16:42
CI installs ggsql 0.5.2 (CRAN), which lacks caching_reader():

- check_viz_deps() and the bookmark-restore gate now require ggsql >=
  0.5.2.9000, so older ggsql drops the visualize tool / skips restores
  with a warning (the established UX) instead of erroring at call time.
- viz_caching_reader() looks up caching_reader via getExportedValue() so
  R CMD check doesn't warn about a missing export on older ggsql.
- Viz tool tests skip below the floor, mirroring test-querychat-viz.R.
@cpsievert
cpsievert force-pushed the ggsql-caching-reader-r branch 2 times, most recently from 01c5ba6 to aa17058 Compare October 13, 2026 17:21
Adds a Remotes entry so CI installs ggsql with caching_reader()
(0.5.2.9000), letting the floor-gated viz tests actually run instead
of skip. The ggsql= prefix is required: the repo (ggsql-r) and the
package (ggsql) have different names, and pak only applies Remotes
entries whose package name matches the dependency.

Temporary: remove once a ggsql release with caching_reader() is on
CRAN/r-universe (Remotes is not CRAN-compliant anyway).
@cpsievert
cpsievert force-pushed the ggsql-caching-reader-r branch from aa17058 to e014071 Compare October 13, 2026 17:26
Mirrors the pkg-py cleanup: execute_ggsql() was a one-line wrapper
around ggsql::ggsql_execute() whose roxygen block mostly restated
CachingReader behavior, and its version gate was redundant with the
gates already at the tool entry points (visualize_result(),
restore_viz_widgets()). Inline ggsql::ggsql_execute() at both call
sites and drop man/execute_ggsql.Rd.

viz_caching_reader() stays: unlike the deleted wrapper it carries real
logic (version gate, R CMD check-safe lookup of caching_reader, and
tbl_sql materialization).

Tests: test-querychat-viz.R calls ggsql::ggsql_execute() directly, and
the test-viz-tool.R mocks move from the querychat namespace to the
ggsql one (ggsql_execute), alongside the existing ggsql mocks.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant