Skip to content

Stronger dialect support + 14 new readers - #569

Merged
thomasp85 merged 80 commits into
mainfrom
issue-341-readers
Oct 9, 2026
Merged

thomasp85 merged 80 commits into
mainfrom
issue-341-readers

Conversation

@thomasp85

@thomasp85 thomasp85 commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

This PR provides a large swath of readers build on top of the existing ADBC/ODBC support we have. It refactors the dialects into a specific module with dialect detection, expands the dialect to accomodate new wrinkles discovered by the new readers, provides a multitiered selectioon mechanism where ADBC is preferred, but ODBC is tried if no suitable ADBC driver exist, and lastly adds an autowrapping of a cache reader when needed. The latter is supported through the dialect system as some dbs forcefully requires a cache to work. For the others we try a temp table creation and if it fails we use a cache.

The dialects added here are

  • Snowflake
  • Databricks
  • BigQuery
  • Redshift
  • PostgreSQL
  • MySQL/MariaDB
  • Datafusion
  • Clickhouse
  • MonetDB
  • Exasol
  • Trino
  • Drill
  • Druid
  • Oracle
  • Microsoft SQL Server

Fix #341, fix #509, closes #386, closes #458

Changes with runtime-semantics impact riding along in this PR

These commits change behavior rather than just how SQL text is built. They are
called out here so they can be reviewed individually; see also the CHANGELOG
entries.

  • a687c9c4 — Vega-Lite timestamp double-rescaling fix
    (src/writer/vegalite/data.rs): unrelated correctness fix for microsecond
    timestamps being rescaled twice.
  • Temporal-unit mismatch detection/conversion + new error path in scale
    resolution (src/plot/scale/scale_type/mod.rs, temporal_unit_micros,
    convert_range_to_transform_unit, check_temporal_domain): new error path,
    surfaced by the broader multi-backend test matrix.
  • Extent merging/sorting (src/execute/schema.rs, merge_extent_rows):
    portability fix for MySQL "can't reopen table" / ClickHouse alias reuse;
    changes how min/max extents are derived.
  • Public API breaks: CacheBackend moved to test-support, CachingReader::new
    is now #[cfg(test)], Spec::layer_sql/Spec::stat_sql removed. Recorded in
    CHANGELOG under [Unreleased].

@thomasp85

Copy link
Copy Markdown
Collaborator Author

I've been hard at work setting up testing infrastructure for this so we can be somewhat sure that ggsql actually works with the stated backends. For non-cloud backends we have a live CI that runs a battery of queries against a db. For cloud based we have a nightly run backed by credentials in the repo secrets

The tests currently miss:

  • Drill (only commercial odbc driver + it requires a cache layer meaning that ggsql will not really talk to it)
  • Oracle (only commercial drivers available)
  • Datafusion (bug in the available ADBC driver - filed upstream)
  • Snowflake (awaiting an account from org to set up as nightly job)

All of this testing has payed off. So many portability issues have been uncovered and fixed at the detriment of the complexity of this PR. Some stat queries have been rewritten, the dialect system expanded, and quoting has become a part of the dialect

- Error instead of panicking when a scheme without registry ADBC info
  (monetdb, drill) reaches AdbcReader::from_connection_string, e.g. via
  the GGSQL_<SCHEME>_ADBC_DRIVER env override.
- reader=native no longer falls through to ODBC when no native reader
  exists or is compiled in; it errors explicitly like reader=adbc.
- odbc:// URIs are handed to the driver with ggsql-owned params stripped,
  so ?cache=off and friends no longer leak into the connection string.
CachingReader::materialize_table executes the body against the primary
but quoted aliases with the cache dialect, sending double-quoted
identifiers to backtick dialects like MySQL. Regression test included.
- case_greatest/case_least no longer index-panic on empty input.
- Date/datetime/time literal defaults clamp out-of-range input instead
  of panicking (and no longer wrap negative time-of-day via as u32).
- drop_stmt_handle skips its debug_assert while unwinding, avoiding an
  abort on a failed free during panic.
- ODBC diagnostic buffer growth is clamped to SqlSmallInt::MAX so the
  length argument cannot wrap negative.
- Dialect module docs for redshift, exasol, monetdb and bigquery claimed
  spatial support that supports_spatial() (false by default) blocks;
  the docs now match the code.
- Battery live_skip for the boxplot global-source case extended to
  mariadb, which shares MySQL's temp-table reopen limitation.
- Rename leftovers (sql_percentile -> sql_quantile) and stale doc
  references cleaned up.
- CHANGELOG entries for the API breaks and behavior changes riding
  along in this PR.

@teunbrand teunbrand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling it a day, I'll continue reviewing tomorrow

Comment thread src/reader/mod.rs
/// SQL type name for numeric columns (e.g., "DOUBLE PRECISION").
/// Derived from [`type_names`]; override that, not this.
///
/// [`type_names`]: SqlDialect::type_names
fn number_type_name(&self) -> Option<&str> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit frivolous comment, but I get that these accessors were valuable before your PR, but can now mostly be replaced by self.type_names().{number, date, integer, ...} at call sites. Doesn't do any harm to keep them, but it'd clean up the SqlDialect trait a bit.

If we want to keep them, it might make sense to double-down on their accessor role by using a get_-prefix.

Comment thread src/reader/mod.rs
/// `CREATE TABLE AS`).
SelectInto,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From comment it is not clear what role this function serves

Suggested change
/// Emulate `GENERATE_SERIES(0, n - 1)` for DBs that lack it

Comment thread src/reader/mod.rs
@@ -46,39 +46,55 @@ use crate::{naming, DataFrame, GgsqlError, Result};
///
/// Default implementations produce portable ANSI SQL.
pub trait SqlDialect {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For our own sense-making efforts, I'd recommend to group the methods into categories. Some of these are pure SQL translations, others are SQL pattern generators, some are self-knowledge (i.e. does this dialect support spatial stuff), there are some spatial specific patterns/translations. The methods are currently somewhat mixed and reordering them gives a semblance of structure.

Comment thread src/plot/layer/geom/tile.rs Outdated
select_parts.join(", ")
);
let sql = crate::sql::Select::new(dialect)
.select(select_parts.join(", "))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.select(select_parts.join(", "))
.select_items(select_parts)

Comment thread src/plot/layer/geom/segment.rs Outdated
);
let __ggsql_vertices__ = dialect.quote_ident("__ggsql_vertices__");
let sql = crate::sql::Select::new(dialect)
.select(select_parts.join(", "))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.select(select_parts.join(", "))
.select_items(select_parts)

Comment thread src/reader/connection.rs
/// These are consumed during dispatch and must never reach a driver's own
/// URI parsing or option map — drivers reject unknown keys.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct GgsqlParams {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name of this structure is too general, mostly because it implies this'd be a parameter structure we use throughout (like GgsqlError for example). I'd be happy with something like ConnectParams or Own(ed)Params or something.

Comment thread src/reader/connection.rs Outdated
Comment on lines +49 to +57
/// `cache_ttl=<secs>`: raw value; parsed by
/// [`ConnUri::cache_config_override`].
pub cache_ttl: Option<String>,
/// `cache_max_bytes=<n|1MB|…>`: raw value.
pub cache_max_bytes: Option<String>,
/// `cache_disabled=1|true|yes`.
pub cache_disabled: Option<bool>,
/// `stmt.<key>=<value>` params (prefix stripped): driver *statement*
/// options, applied to every statement the reader creates.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments here don't immediately make sense to me, so maybe add 1 line of explanatory prose per field (e.g. 'Maximum bytes the cache is allowed to have', or whatever it actually means)

Comment thread src/reader/connection.rs
continue;
}
let (key, value) = match segment.split_once('=') {
Some((k, v)) => (k, Some(v)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nitpick with 0 priority.

We could do this

Suggested change
Some((k, v)) => (k, Some(v)),
Some((k, v)) => (k, Some(v.to_string())),

And then the match arms could simplify to just use value instead of value.map(|v| v.to_string()).

Comment thread src/reader/connection.rs Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that we can only use the datafusion cache when duckdb/sqlite are compiled in?

Comment thread src/reader/connection.rs Outdated
@@ -78,48 +191,37 @@ fn cache_uri(scheme: &str) -> Result<&'static str> {
match scheme {
"duckdb" => Ok("duckdb://memory"),
"sqlite" => Ok("sqlite://memory"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This differs from L549 (sqlite://:memory:). Should this be unified?

@teunbrand teunbrand left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM, few comments that deserve a glance but nothing that should stop a merge.

Caveat to my review is that I don't really know the DB connection stuff very well, so I'm mostly trusting you on this.

Few things that didn't map neatly onto changes:

  • Can you update any relevant CLAUDE.mds? I found the testing architecture with e.g. golden.rs living in the execute folder hard to follow, so having this written down somewhere would help me as well as future agents.

  • rewrite_namespaced_sql in src/parser/sql.rs should probably also use the dialect quoting instead of the naming module. It is now only called in DuckDB/SQLite so not immediately relevant.

  • Could have the assistant give another look at comments. I typically ask it to review the comments it made and pay attention to (1) generally terse style (2) delete commentary about decisions (3) delete commentary about history and (4) move implementation details to relevant lines of code. (2) and (3) are mostly because assistants are bad at following CLAUDE.md:121 (Comments describe the current state of the code) directive.

Comment thread src/reader/adbc.rs
"AdbcReader::register: empty DataFrame not supported".into(),
));
}
// Zero-row frames are fine: the CREATE below still runs, leaving an

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the comments here can generally be improved. Many are a full paragraph, contain details that are already abstracted and I've not found them helpful in grokking what the code is doing. This specific one here seems to be misplaced, as it is about logic that is at L625: ~40 lines down the road.

Comment thread src/reader/adbc.rs
)));
self.registered_tables.note_registered(name);

if batch.num_rows() > 0 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a personal bias against indentation depth, but if you early exit here if you have less than 1 row, you save 1 level.

Comment thread src/execute/schema.rs
Comment on lines +247 to +251
let grp_cols = group_by
.iter()
.map(|c| dialect.quote_ident(c))
.collect::<Vec<_>>()
.join(", ");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (quoting vector followed by joining) is a recurring pattern that might be worth a little helper, maybe on the dialect itself?

There are also some variants of this that follow the same pattern (like quoted_groups in boxplot.rs).

Comment on lines +273 to +275
{__bin_src__} AS (SELECT *, {bin_expr} AS {bin_key} FROM {__stat_src__}), \
{__binned__} AS (SELECT {binned} FROM {__bin_src__} GROUP BY {group}) \
SELECT *, {bin} + {width} AS {bin_end}, \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oughtn't this be Select-constructed to deal with the wildcard?

/// registered tables by name; anything unrecognized defaults to
/// `Float64`, which is the right shape for the derived columns
/// (bins, densities, quantiles) the pipeline produces.
pub(crate) struct StubReader {

@teunbrand teunbrand Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easily will get confused with stubs in TABULATE (read: I'd like Ctrl/Cmd+F global search on 'stub' to only give tabulate stuff 😁 ), so I'd argue for some different name, e.g. TestReader, DummyReader or SterileReader or something you see fit.

Comment on lines +97 to +107
matrix:
include:
- { backend: snowflake, driver: snowflake }
- { backend: bigquery, driver: bigquery }
- { backend: databricks, driver: databricks }

env:
GGSQL_TEST_URI_SNOWFLAKE: ${{ secrets.GGSQL_TEST_URI_SNOWFLAKE }}
GGSQL_TEST_URI_BIGQUERY: ${{ secrets.GGSQL_TEST_URI_BIGQUERY }}
GGSQL_TEST_URI_DATABRICKS: ${{ secrets.GGSQL_TEST_URI_DATABRICKS }}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagged by assistant: all 3 backends have access to all 3 secrets, whereas snowflake db doesn't need to know about bigquery secret. Probably not too severe, but worth mentioning.

@@ -0,0 +1,154 @@
name: Nightly Cloud Dialect Tests

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagged by assistant: workflow has default permissions, whereas it needs only read permissions. Probably not severe, but worth mentioning.

Comment thread .github/scripts/live/ggsql_live_test.csv
Comment on lines 753 to +773
@@ -718,6 +758,32 @@ fn build_bin_condition(
oob_squish: bool,
is_first: bool,
is_last: bool,
dialect: &dyn super::SqlDialect,
) -> String {
build_bin_condition_expr(
&dialect.quote_ident(column_name),
lower_expr,
upper_expr,
closed_left,
oob_squish,
is_first,
is_last,
dialect,
)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wrapper seems frivolous (i.e. 20 lines of wrapper + comments to save 1 inline dialect.quote_ident(). Do we really need it?

@thomasp85
thomasp85 merged commit afe60e6 into main Oct 9, 2026
23 checks passed
This was referenced Oct 9, 2026
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.

MonetDB Support Meta-issue: More readers

2 participants