Repository navigation
Conversation
Expose the ggsql crate's CachingReader to R. Source reads hit the primary reader and are memoized (TTL + LRU byte budget); ggsql-generated derived tables run on a private in-memory DuckDB cache, so queries with layer-level sources (DRAW ... FROM <table>) work end to end. The primary can be a connection string (duckdb/odbc/snowflake, same dispatch as Reader$new()) or the execute_sql/register/unregister callbacks custom_reader() takes. Cache behavior follows the GGSQL_CACHE_TTL / GGSQL_CACHE_MAX_BYTES / GGSQL_CACHE_DISABLED env vars, overridable per reader via ttl_secs, max_bytes, and enabled. Also: - Track names registered via ggsql_register() on the Reader and merge them into ggsql_table_names(), since registrations on a caching reader land in the private cache, which the primary's SHOW TABLES never sees. - Surface the R condition message from failing reader hooks (via geterrmessage()) instead of extendr's opaque call dump; affects custom_reader() too.
actions/checkout v4 now refuses to check out fork PR code under pull_request_target without allow-unsafe-pr-checkout. Safe here: the checked-out code is treated as passive data (air format + reviewdog suggestions), never executed.
cpsievert
added a commit
to posit-dev/querychat
that referenced
this pull request
Oct 13, 2026
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. Temporary: remove once a ggsql release with caching_reader() is on CRAN/r-universe (Remotes is not CRAN-compliant anyway).
cpsievert
added a commit
to posit-dev/querychat
that referenced
this pull request
Oct 13, 2026
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
added a commit
to posit-dev/querychat
that referenced
this pull request
Oct 13, 2026
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).
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two things are impossible or wasteful with a callback-backed reader (
custom_reader()) today. Every read re-executes the source SQL, even when identical to the read before it. And queries with layer-level sources (DRAW ... FROM <table>) fail outright, because a callback reader gives ggsql nowhere to stage derived tables.The Rust crate has handled both since 0.5.2 with
CachingReader: source reads go to the primary and are memoized (TTL plus an LRU byte budget), and all ggsql-generated derived SQL runs on a private in-memory DuckDB cache. This PR exposes it to R ascaching_reader(), mirroring the Python binding in posit-dev/ggsql-python#10.primarycan be a connection string (duckdb://,odbc://,snowflake://— the same dispatch asReader$new(), one scheme more than the Python binding accepts) or theexecute_sql/register/unregistercallbackscustom_reader()takes. Passing an existingReadererrors with guidance. Cache behavior follows the sameGGSQL_CACHE_TTL/GGSQL_CACHE_MAX_BYTES/GGSQL_CACHE_DISABLEDenv vars as the Python binding, overridable per reader.Design decisions worth a look
SHOW TABLESroutes to the primary, soggsql_table_names()/ggsql_table()couldn't see them. Fixed R-side by tracking registrations on theReaderobject and merging them into the listing. An upstream fix was considered and set aside: the crate documents introspection as deliberately describing only the primary, and it wouldn't help R anyway (ggsql_table_names()issuesSHOW TABLESas raw SQL).custom_reader(): hook errors now surface the R condition message (e.g. DuckDB's "Catalog Error: ...") instead of extendr's opaque call dump. Small and self-contained; happy to split into its own PR.unsafe impl Send for RCallbackReader— required byCachingReader'sBox<dyn Reader + Send>; safe because the R FFI only runs on R's main thread. Reasoning is in a SAFETY comment; worth a second pair of eyes.Known upstream issues (ggsql crate, not yet filed)
Both cross-binding; neither affects the querychat path. Issue drafts exist and can be filed once this direction is agreed.
SHOW TABLESresults are memoized, so tables created on the primary take up tottl_secsto appear in listings. Documented on?caching_reader.CREATE TABLE ... FROM <registered_table>routes to the cache, but the new name never enters the resident set, so subsequent reads route to the primary and fail permanently. No binding-side workaround is possible.Downstream
Motivating use case is querychat's R viz tool — see posit-dev/querychat#327 for the integration (the pkg-r port of posit-dev/querychat#321).
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: 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, and the multi-source query ran. In the Python benchmark (3M-row data frame) this also cut session wall time by ~35%; the benefit is smaller for cheap local sources and largest for remote databases, where every eliminated read is a network round trip.
Test plan
16 new tests covering dispatch, memoization, TTL, env-var config, layer-level sources, and the table-listing fix. Full suite: 191 expectations, 0 failures;
air/cargo fmtclean. (Theformat-suggestfailure on this PR is a fork-PR infrastructure issue —actions/checkoutnow refuses fork checkouts underpull_request_target; a fix is included here, though it only takes effect for future PRs once merged, since that workflow runs from the base branch.)