Skip to content

Add CachingReader binding (stacked on #9) - #10

Merged
cpsievert merged 8 commits into
ggsql-0.5.2-upgradefrom
caching-reader
Oct 12, 2026
Merged

cpsievert merged 8 commits into
ggsql-0.5.2-upgradefrom
caching-reader

Conversation

@cpsievert

@cpsievert cpsievert commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Exposes upstream ggsql 0.5.2's CachingReader to Python as ggsql.CachingReader. Source reads hit the primary reader (memoized with TTL + LRU byte budget); all ggsql-generated derived SQL (__ggsql_* tables) runs on a private in-memory DuckDB cache.

reader = ggsql.CachingReader("duckdb://memory", ttl_secs=60)
# or wrap any Python object with the bridge protocol
# (execute_sql / register / unregister) — the querychat path
reader = ggsql.CachingReader(my_reader)
spec = reader.execute("SELECT ... VISUALISE ...")
  • Primary accepts a duckdb:// connection string (built fresh internally) or a bridged Python reader; passing an existing DuckDBReader instance errors with guidance.
  • Config from GGSQL_CACHE_DISABLED / GGSQL_CACHE_TTL / GGSQL_CACHE_MAX_BYTES, keyword args take precedence.
  • No GIL release in execute (bridged primaries call back into Python — same stance as DuckDBReader).

Why: querychat currently rejects ggsql queries with layer-level sources and re-runs identical base reads on every LLM tool call. test_layer_level_source_through_bridge demonstrates VISUALISE ... FROM <table> running end-to-end through a bridged primary — the capability querychat's _viz_ggsql.py names as its long-term fix.

Benchmark evidence (merge gate)

Benchmark: simulated LLM-style viz session — 9 tool calls sharing one identical base read over a 3M-row DataFrameSource (iterative chart refinement), 1 exact repeat, 1 multi-relation query. Old path = querychat's hand-rolled split (_viz_ggsql.py @ querychat 40f328e4); new path = whole-query execution through CachingReader wrapping the querychat executor — see draft PR posit-dev/querychat#321 for the integration.

call old (s) new (s)
1 4.839 5.107
2 5.909 3.576
3 7.512 5.322
4 5.335 2.979
5 4.459 2.195
6 5.947 3.684
7 5.256 2.981
8 5.922 3.623
9 5.896 3.647
multi-relation unsupported 3.582s
total (calls 1–9) 51.075 33.113

Source reads during calls 1–9 (one shared base read): old = 9, new = 1. The multi-relation call adds 2 more on the new path (base + layer source); the old path rejects such queries outright.

Takeaways:

  • 9× fewer source reads for iterative chart refinement — the LLM loop's dominant pattern.
  • ~35% wall-time reduction overall, with no cold-path regression (call 1: 5.1s vs 4.8s — small cache-staging overhead).
  • Remaining per-call cost is data movement + Vega-Lite rendering of the ~1.5M-row result, not the source read.
  • Capability unlock: DRAW ... FROM <table> (multi-relation) queries now work end-to-end through the bridged executor.

Environment: ggsql 0.5.2 (this branch), M-series arm64, Python 3.12. Two consecutive runs produced near-identical numbers. Repro: .venv/bin/python _dev/ggsql-caching-benchmark/bench.py on querychat branch ggsql-caching-reader (posit-dev/querychat#321).

Stacking

Stacked on #9 (ggsql-0.5.2-upgrade); base retargets to main after #9 merges. Design spec and implementation plan live locally in docs/superpowers/ (gitignored).

Test plan

  • 15 new tests: constructor dispatch, memoization hit/miss, TTL expiry, enabled=False + env-var disable, layer-level source through bridged primary, cache-resident routing, top-level execute fast path, package export
  • Full suite: 121 passed, 5 skipped; cargo fmt --check and cargo clippy -- -D warnings clean

@cpsievert
cpsievert marked this pull request as ready for review October 12, 2026 19:54
cpsievert added a commit to posit-dev/querychat that referenced this pull request Oct 12, 2026
_viz_ggsql.py lazily imports ggsql.CachingReader at first viz call;
older releases would fail with ImportError at runtime. CachingReader
first ships in ggsql 0.5.2 (posit-dev/ggsql-python#10).
@cpsievert
cpsievert added this pull request to stack #11 October 12, 2026 21:40
@cpsievert
cpsievert requested a balanced review from Copilot October 12, 2026 21:40

This comment was marked as resolved.

cpsievert added a commit that referenced this pull request Oct 12, 2026
Registered tables still live in the private cache, reads referencing
them still route there, and derived SQL still runs on the cache backend
— the flag only stops the memo being consulted or written. Addresses
Copilot review on #10.
Wraps upstream ggsql::reader::CachingReader: an internal in-memory
DuckDB cache backend, primary given as a duckdb:// connection string
(built fresh internally) or any Python object with the reader bridge
protocol. Config from GGSQL_CACHE_* env vars with keyword overrides.
Package-level export included here rather than as a later task because
the constructor tests reference ggsql.CachingReader directly.
Registered tables still live in the private cache, reads referencing
them still route there, and derived SQL still runs on the cache backend
— the flag only stops the memo being consulted or written. Addresses
Copilot review on #10.
@cpsievert
cpsievert merged commit e6bf1ef into main Oct 12, 2026
14 checks passed
cpsievert added a commit to posit-dev/querychat that referenced this pull request Oct 13, 2026
_viz_ggsql.py lazily imports ggsql.CachingReader at first viz call;
older releases would fail with ImportError at runtime. CachingReader
first ships in ggsql 0.5.2 (posit-dev/ggsql-python#10).
cpsievert added a commit to posit-dev/querychat that referenced this pull request Oct 13, 2026
_viz_ggsql.py lazily imports ggsql.CachingReader at first viz call;
older releases would fail with ImportError at runtime. CachingReader
first ships in ggsql 0.5.2 (posit-dev/ggsql-python#10).
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.

2 participants