Skip to content
Draft
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## [Unreleased]

### Added

- New caching layer that wraps any `Reader` with an in-memory, writeable cache
backend (currently duckdb or sqlite), making write-constrained databases
usable and avoiding repeated remote reads during interactive iteration.
Memoized reads are bounded by a TTL and an LRU byte budget, configurable per
connection. The cache can be cleared mid-session with the `-- @uncache` meta-command.

## 0.4.1 - 2026-06-22

### Changed
Expand Down
2 changes: 2 additions & 0 deletions ggsql-cli/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The binary name is `ggsql` (not `ggsql-cli`) — that's what release artifacts a

Only public `ggsql::*` API is used (`reader`, `writer`, `validate`, `parser`, `VERSION`) — this crate has no awareness of internal modules.

`exec`/`run` build their reader via the library factory `ggsql::reader::connection::reader_from_uri`. They accept an in-memory caching layer (off by default) selected either by the composite connection scheme `<primary>+<cache>://…` (e.g. `odbc+duckdb://…`) or the `--cache <duckdb|sqlite>` flag; the two cannot be combined.

## Build & install

```sh
Expand Down
127 changes: 66 additions & 61 deletions ggsql-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ pub enum Commands {
/// The ggsql query to execute
query: String,

/// Data source connection string (duckdb://, sqlite://, odbc://)
/// Data source connection string (duckdb://, sqlite://, odbc://).
#[arg(long, default_value = "duckdb://memory")]
reader: String,

/// In-memory cache backend wrapping the reader (duckdb, sqlite). Off by default.
#[arg(long)]
cache: Option<String>,

/// Output format (vegalite)
#[arg(long, default_value = "vegalite")]
writer: String,
Expand All @@ -56,10 +60,14 @@ pub enum Commands {
/// Path to .sql file containing ggsql query
file: PathBuf,

/// Data source connection string (duckdb://, sqlite://, odbc://)
/// Data source connection string (duckdb://, sqlite://, odbc://).
#[arg(long, default_value = "duckdb://memory")]
reader: String,

/// In-memory cache backend wrapping the reader (duckdb, sqlite). Off by default.
#[arg(long)]
cache: Option<String>,

/// Output format (vegalite)
#[arg(long, default_value = "vegalite")]
writer: String,
Expand Down Expand Up @@ -147,27 +155,29 @@ fn main() -> anyhow::Result<()> {
Commands::Exec {
query,
reader,
cache,
writer,
output,
verbose,
} => {
if verbose {
eprintln!("Executing query: {}", query);
}
cmd_exec(query, reader, writer, output, verbose);
cmd_exec(query, reader, cache, writer, output, verbose);
}

Commands::Run {
file,
reader,
cache,
writer,
output,
verbose,
} => {
if verbose {
eprintln!("Running query from file: {}", file.display());
}
cmd_run(file, reader, writer, output, verbose);
cmd_run(file, reader, cache, writer, output, verbose);
}

Commands::Parse { query, format } => {
Expand All @@ -194,86 +204,81 @@ fn main() -> anyhow::Result<()> {
Ok(())
}

fn cmd_run(file: PathBuf, reader: String, writer: String, output: Option<PathBuf>, verbose: bool) {
fn cmd_run(
file: PathBuf,
reader: String,
cache: Option<String>,
writer: String,
output: Option<PathBuf>,
verbose: bool,
) {
match std::fs::read_to_string(&file) {
Ok(query) => cmd_exec(query, reader, writer, output, verbose),
Ok(query) => cmd_exec(query, reader, cache, writer, output, verbose),
Err(e) => {
eprintln!("Failed to read file {}: {}", file.display(), e);
std::process::exit(1);
}
}
}

fn cmd_exec(query: String, reader: String, writer: String, output: Option<PathBuf>, verbose: bool) {
fn cmd_exec(
query: String,
reader: String,
cache: Option<String>,
writer: String,
output: Option<PathBuf>,
verbose: bool,
) {
use ggsql::reader::connection;

if verbose {
eprintln!("Reader: {}", reader);
if let Some(ref cache) = cache {
eprintln!("Cache: {}", cache);
}
eprintln!("Writer: {}", writer);
if let Some(ref output_file) = output {
eprintln!("Output: {}", output_file.display());
}
}

if reader.starts_with("duckdb://") {
#[cfg(feature = "duckdb")]
{
let r = match ggsql::reader::DuckDBReader::from_connection_string(&reader) {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to create reader: {}", e);
std::process::exit(1);
}
};
exec_with_reader(&query, &r, &writer, output, verbose);
}
#[cfg(not(feature = "duckdb"))]
{
eprintln!("DuckDB reader not compiled in. Rebuild with --features duckdb");
std::process::exit(1);
}
} else if reader.starts_with("sqlite://") {
#[cfg(feature = "sqlite")]
{
let r = match ggsql::reader::SqliteReader::from_connection_string(&reader) {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to create reader: {}", e);
std::process::exit(1);
// Build the reader. A composite `<primary>+<cache>://` URI is handled by
// `reader_from_uri`; the `--cache` flag is an explicit alternative and may
// not be combined with a composite URI.
let built = match cache {
Some(cache_scheme) => {
if connection::split_cache_uri(&reader).is_some() {
eprintln!(
"Cannot combine --cache with a composite '<primary>+<cache>://' connection string"
);
std::process::exit(1);
}
// `--cache <scheme>` is sugar for the composite `<primary>+<scheme>://` URI.
match reader.split_once("://") {
Some((scheme, rest)) => {
connection::reader_from_uri(&format!("{scheme}+{cache_scheme}://{rest}"))
}
};
exec_with_reader(&query, &r, &writer, output, verbose);
}
#[cfg(not(feature = "sqlite"))]
{
eprintln!("SQLite reader not compiled in. Rebuild with --features sqlite");
std::process::exit(1);
}
} else if reader.starts_with("odbc://") {
#[cfg(feature = "odbc")]
{
let r = match ggsql::reader::OdbcReader::from_connection_string(&reader) {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to create reader: {}", e);
None => {
eprintln!("Invalid --reader connection string: {reader}");
std::process::exit(1);
}
};
exec_with_reader(&query, &r, &writer, output, verbose);
}
}
#[cfg(not(feature = "odbc"))]
{
eprintln!("ODBC reader not compiled in. Rebuild with --features odbc");
None => connection::reader_from_uri(&reader),
};

let reader = match built {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to create reader: {}", e);
std::process::exit(1);
}
} else if reader.starts_with("postgres://") || reader.starts_with("postgresql://") {
eprintln!("PostgreSQL reader is not yet implemented");
std::process::exit(1);
} else {
eprintln!("Unsupported connection string: {}", reader);
std::process::exit(1);
}
};

exec_with_reader(&query, reader.as_ref(), &writer, output, verbose);
}

fn exec_with_reader<R: Reader>(
fn exec_with_reader<R: Reader + ?Sized>(
query: &str,
reader: &R,
writer: &str,
Expand Down Expand Up @@ -430,7 +435,7 @@ fn cmd_validate(query: String, _reader: Option<String>) {
}

// Prints a CSV-like output to stdout with aligned columns
fn print_table_fallback<R: Reader>(query: &str, reader: &R, max_rows: usize) {
fn print_table_fallback<R: Reader + ?Sized>(query: &str, reader: &R, max_rows: usize) {
let source_tree = match parser::SourceTree::new(query) {
Ok(st) => st,
Err(e) => {
Expand Down
2 changes: 1 addition & 1 deletion ggsql-jupyter/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ ggsql-jupyter/

1. `ggsql-jupyter --install` writes a kernelspec into the active Python environment (Jupyter, conda, uv, virtualenv — auto-detected).
2. `ggsql-jupyter <connection-file>` is the entry point Jupyter invokes; it reads the connection JSON, opens the five ZMQ sockets (shell, control, iopub, stdin, heartbeat), and runs `kernel.rs`'s message loop.
3. Each `execute_request` is dispatched through `executor.rs` → `ggsql::reader::DuckDBReader::execute(...)`. The kernel keeps a single persistent in-memory DuckDB session so cells share state.
3. Each `execute_request` is dispatched through `executor.rs` → `ggsql::reader::DuckDBReader::execute(...)`. The kernel keeps a single persistent in-memory DuckDB session so cells share state. Readers are built via the library factory `ggsql::reader::connection::reader_from_uri`, so a composite `<primary>+<cache>://` connection string (e.g. via `-- @connect:`) wraps the reader in an in-memory caching layer — the persistent kernel session means repeated cells reuse cached remote reads.
4. The result is wrapped by `display.rs` into a Jupyter `display_data` message — Vega-Lite specs go through vega-embed in an HTML payload (works in classic Jupyter, JupyterLab, and Positron); pure SQL goes out as an HTML table.

## Positron-specific bits
Expand Down
Loading
Loading