From 45ddd100592560e4f21835f442ebc0985d7f22b8 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 6 Oct 2026 17:42:15 +0200 Subject: [PATCH 01/10] Consolidate dialect resolution into the registry Dialect selection (override -> scheme -> detect) was implemented three times, in connection.rs, adbc.rs and odbc/mod.rs, each with its own 'Unknown dialect' message. There is now a single entry point, registry::resolve_dialect, plus registry::detect_or_err and a shared unknown_dialect_error; the detect_dialect/dialect_for_scheme wrappers in dialects/mod.rs are gone. ADBC options for adbc:// URIs are now built from the parsed ConnUri params instead of re-splitting the query string. --- src/reader/adbc.rs | 64 ++++++++++++++++++-------------------- src/reader/connection.rs | 24 ++++++++------ src/reader/dialects/mod.rs | 62 +++++++----------------------------- src/reader/odbc/mod.rs | 9 ++---- src/reader/registry.rs | 60 +++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 102 deletions(-) diff --git a/src/reader/adbc.rs b/src/reader/adbc.rs index a875aa6b..4b94cf0d 100644 --- a/src/reader/adbc.rs +++ b/src/reader/adbc.rs @@ -183,9 +183,32 @@ pub fn driver_env_var(scheme: &str) -> String { crate::reader::registry::adbc_driver_env_var(scheme) } -/// Parse a `k=v&…` query string into ADBC database options. The keys `uri`, -/// `username`, and `password` map to their dedicated ADBC options; everything -/// else passes through as a driver-specific option. +/// Convert the parsed driver-bound params of a +/// [`ConnUri`](crate::reader::connection::ConnUri) into ADBC database +/// options, without re-splitting the query string. The keys `uri`, +/// `username`, and `password` map to their dedicated ADBC options; +/// everything else passes through as a driver-specific option. Bare flags +/// (no value) have no option equivalent and are skipped. +fn conn_params_to_opts(params: &[(String, Option)]) -> Vec<(OptionDatabase, OptionValue)> { + params + .iter() + .filter_map(|(key, value)| { + let value = value.as_ref()?; + let opt_key = match key.as_str() { + "uri" => OptionDatabase::Uri, + "username" => OptionDatabase::Username, + "password" => OptionDatabase::Password, + other => OptionDatabase::Other(other.to_string()), + }; + Some((opt_key, OptionValue::String(value.clone()))) + }) + .collect() +} + +/// Parse a `k=v&…` query string into ADBC database options. Used for +/// driver-rewritten query strings (BigQuery's Simba grammar) that no longer +/// correspond to the parsed [`ConnUri`](crate::reader::connection::ConnUri); +/// the parsed-params form is [`conn_params_to_opts`]. fn query_params_to_opts(query: &str) -> Vec<(OptionDatabase, OptionValue)> { let mut opts = Vec::new(); for segment in query.split('&') { @@ -268,8 +291,8 @@ impl AdbcReader { /// see [`driver_uri_for`] and [`bigquery_driver_uri`]. /// /// The dialect is chosen from the scheme through the registry (see - /// [`resolve_dialect`]); unknown backends are an error, never a silent - /// ANSI fallback. + /// [`crate::reader::registry::resolve_dialect`]); unknown backends are + /// an error, never a silent ANSI fallback. pub fn from_connection_string(uri: &str) -> Result { let conn = crate::reader::connection::ConnUri::parse(uri)?; let scheme = conn.scheme.as_str(); @@ -295,7 +318,7 @@ impl AdbcReader { GgsqlError::ReaderError(format!("ADBC driver '{}' failed to load: {}", body, e)) })? }; - (driver, query_params_to_opts(query)) + (driver, conn_params_to_opts(&conn.params)) } else { let entry = crate::reader::registry::by_scheme(scheme).ok_or_else(|| { GgsqlError::ReaderError(format!("No known ADBC driver for scheme '{}://'", scheme)) @@ -331,40 +354,13 @@ impl AdbcReader { (driver, opts) }; - let dialect = resolve_dialect(&conn)?; + let dialect = crate::reader::registry::resolve_dialect(&conn)?; Self::new_with_database_opts(driver, dialect, opts) .map(|reader| reader.with_statement_opts(conn.ggsql.stmt_options.clone())) } } -/// Pick the dialect for an ADBC connection: an explicit `dialect=` override -/// wins; otherwise the scheme (or, for `adbc://`, the driver name) -/// resolves through the registry. Unknown backends are an error pointing at -/// the override — never a silent ANSI fallback. -fn resolve_dialect( - conn: &crate::reader::connection::ConnUri, -) -> Result> { - if let Some(name) = &conn.ggsql.dialect { - return crate::reader::registry::dialect_override(name).ok_or_else(|| { - GgsqlError::ReaderError(format!( - "Unknown dialect '{name}' in connection URI. Use dialect=ansi or any \ - supported scheme (postgres, mysql, …)." - )) - }); - } - if conn.scheme != "adbc" { - return Ok(crate::reader::registry::by_scheme(&conn.scheme) - .expect("checked by caller") - .dialect()); - } - let body = conn.body.as_str(); - if let Some(entry) = crate::reader::registry::by_scheme(body) { - return Ok(entry.dialect()); - } - crate::reader::dialects::detect_dialect(None, Some(body)) -} - /// Compute the URI handed to the driver as the `uri` database option, /// applying the registry's rewrite rule for the backend. fn driver_uri_for(kind: crate::reader::registry::DriverUri, body: &str, full_uri: &str) -> String { diff --git a/src/reader/connection.rs b/src/reader/connection.rs index cc8a72f2..50627f0a 100644 --- a/src/reader/connection.rs +++ b/src/reader/connection.rs @@ -354,12 +354,7 @@ fn build_backend_reader( Some( crate::reader::registry::dialect_override(name) .map(|d| d as Box) - .ok_or_else(|| { - GgsqlError::ReaderError(format!( - "Unknown dialect '{name}' in connection URI. Use dialect=ansi \ - or any supported scheme (postgres, mysql, …)." - )) - })?, + .ok_or_else(|| crate::reader::registry::unknown_dialect_error(name))?, ) } else if entry.scheme == "flightsql" { None @@ -760,13 +755,22 @@ mod tests { #[test] fn test_requires_cache_dialects() { + let dialect_for = |scheme: &str| { + crate::reader::registry::by_scheme(scheme) + .map(|e| e.dialect()) + .unwrap() + }; for scheme in ["trino", "druid", "drill"] { - let d = crate::reader::dialects::dialect_for_scheme(scheme).unwrap(); - assert!(d.requires_cache(), "scheme {scheme} should require a cache"); + assert!( + dialect_for(scheme).requires_cache(), + "scheme {scheme} should require a cache" + ); } for scheme in ["postgres", "duckdb", "sqlite", "clickhouse", "datafusion"] { - let d = crate::reader::dialects::dialect_for_scheme(scheme).unwrap(); - assert!(!d.requires_cache(), "scheme {scheme} should be probed"); + assert!( + !dialect_for(scheme).requires_cache(), + "scheme {scheme} should be probed" + ); } } diff --git a/src/reader/dialects/mod.rs b/src/reader/dialects/mod.rs index e12bf978..7e94cbfb 100644 --- a/src/reader/dialects/mod.rs +++ b/src/reader/dialects/mod.rs @@ -1,13 +1,8 @@ -//! Backend-specific SQL dialects and dialect detection. +//! Backend-specific SQL dialects. //! -//! One unit struct per backend implementing [`SqlDialect`], plus: -//! -//! - [`detect_dialect`]: pick a dialect from an ODBC DBMS name or driver -//! string (substring matching, most specific patterns first). -//! - [`dialect_for_scheme`]: pick a dialect from a ggsql URI scheme -//! (`postgres://`, `mysql://`, …), used by ADBC/ODBC reader dispatch. -//! -//! Both return boxed trait objects so readers can store them uniformly. +//! One unit struct per backend implementing [`SqlDialect`. Dialect lookup +//! — by URI scheme, by `dialect=` override, or by ODBC DBMS name/driver +//! detection — lives in the [`registry`](crate::reader::registry). pub mod bigquery; pub mod clickhouse; @@ -45,49 +40,9 @@ pub use snowflake::SnowflakeDialect; pub use sqlite::SqliteDialect; pub use trino::TrinoDialect; +#[cfg(test)] use crate::reader::SqlDialect; -/// Detect the backend SQL dialect from a DBMS name and/or a driver hint -/// (ODBC driver name, ADBC driver name). -/// -/// The DBMS name is checked first; the driver hint is a fallback. Matching -/// is case-insensitive substring matching against the [`registry`], most -/// specific entries first (e.g. SQL Server before anything containing -/// "sql"). -/// -/// Unknown backends are an **error** naming what was seen — silently -/// falling back to ANSI produced broken SQL too often. The escape hatch is -/// a `dialect=ansi` (or `dialect=`) parameter on the connection -/// URI; see [`crate::reader::registry::dialect_override`]. -/// -/// [`registry`]: crate::reader::registry -pub fn detect_dialect( - dbms_name: Option<&str>, - driver_hint: Option<&str>, -) -> crate::Result> { - crate::reader::registry::detect(dbms_name, driver_hint) - .map(|e| e.dialect()) - .ok_or_else(|| { - crate::GgsqlError::ReaderError(format!( - "Unrecognized database backend (DBMS name: {}, driver: {}). \ - ggsql does not know which SQL dialect to use. If the backend \ - is close to a supported one, pin the dialect explicitly with \ - a `dialect=` parameter (e.g. dialect=postgres), or use \ - dialect=ansi for generic ANSI SQL.", - dbms_name.unwrap_or(""), - driver_hint.unwrap_or(""), - )) - }) -} - -/// Pick a dialect from a ggsql URI scheme (`postgres`, `mysql`, …). -/// -/// Returns `None` for unknown schemes so dispatch can report the scheme -/// itself as unsupported. -pub fn dialect_for_scheme(scheme: &str) -> Option> { - crate::reader::registry::by_scheme(scheme).map(|e| e.dialect()) -} - /// CASE-based scalar greatest/least for backends without `GREATEST`/`LEAST` /// (SQL Server, SQLite, Druid, Drill, MonetDB). Builds a left-folded chain of /// two-way comparisons. @@ -228,6 +183,11 @@ fn skip_balanced_parens(bytes: &[u8], i: &mut usize) -> Option<()> { #[cfg(test)] mod tests { use super::*; + use crate::reader::registry::{by_scheme, detect_or_err as detect_dialect}; + + fn dialect_for_scheme(scheme: &str) -> Option> { + by_scheme(scheme).map(|e| e.dialect()) + } fn assert_type_name(d: &dyn SqlDialect, expected: Option<&str>) { assert_eq!(d.number_type_name(), expected); @@ -355,7 +315,7 @@ mod tests { ); } - /// All schemes `dialect_for_scheme` knows, for conformance sweeps. + /// All schemes the registry knows, for conformance sweeps. const ALL_SCHEMES: &[&str] = &[ "postgres", "redshift", diff --git a/src/reader/odbc/mod.rs b/src/reader/odbc/mod.rs index 52319bc1..c5d0f37b 100644 --- a/src/reader/odbc/mod.rs +++ b/src/reader/odbc/mod.rs @@ -24,7 +24,7 @@ use wrapper::{Connection, Statement}; /// `Driver=` value from the ODBC connection string serves as the driver hint. fn detect_dialect(dbms_name: Option<&str>, conn_str: &str) -> Result> { let driver = super::connection::extract_odbc_value(conn_str, "driver"); - super::dialects::detect_dialect(dbms_name, driver.as_deref()) + super::registry::detect_or_err(dbms_name, driver.as_deref()) .map(|d| d as Box) } @@ -81,12 +81,7 @@ impl OdbcReader { (None, Some(name)) => Some( crate::reader::registry::dialect_override(&name) .map(|d| d as Box) - .ok_or_else(|| { - GgsqlError::ReaderError(format!( - "Unknown dialect '{name}' in ODBC connection string. Use \ - Dialect=ansi or any supported scheme (postgres, mysql, …)." - )) - })?, + .ok_or_else(|| crate::reader::registry::unknown_dialect_error(&name))?, ), (None, None) => None, }; diff --git a/src/reader/registry.rs b/src/reader/registry.rs index d959dbd8..053c64fc 100644 --- a/src/reader/registry.rs +++ b/src/reader/registry.rs @@ -397,6 +397,66 @@ pub fn dialect_override(name: &str) -> Option> { by_scheme(name).map(|e| e.dialect()) } +/// Error for an unrecognized `dialect=` override value, naming the escape +/// hatches. Shared so every dispatch path reports the same message. +pub fn unknown_dialect_error(name: &str) -> crate::GgsqlError { + crate::GgsqlError::ReaderError(format!( + "Unknown dialect '{name}'. Use dialect=ansi or any supported scheme \ + (postgres, mysql, …)." + )) +} + +/// Resolve the SQL dialect for a parsed connection URI: an explicit +/// `dialect=` override wins; otherwise the scheme (or, for +/// `adbc://`, the driver name) resolves through the registry. +/// Unknown backends are an error pointing at the override — never a silent +/// ANSI fallback. +pub fn resolve_dialect( + conn: &crate::reader::connection::ConnUri, +) -> crate::Result> { + if let Some(name) = &conn.ggsql.dialect { + return dialect_override(name).ok_or_else(|| unknown_dialect_error(name)); + } + if conn.scheme != "adbc" { + return by_scheme(&conn.scheme).map(|e| e.dialect()).ok_or_else(|| { + crate::GgsqlError::ReaderError(format!( + "Unsupported connection scheme '{}://'. Supported: {}", + conn.scheme, + supported_schemes() + )) + }); + } + let body = conn.body.as_str(); + if let Some(entry) = by_scheme(body) { + return Ok(entry.dialect()); + } + detect_or_err(None, Some(body)) +} + +/// Detect a dialect from an ODBC DBMS name and/or driver string, or error +/// naming what was seen. Unknown backends are an **error** — silently +/// falling back to ANSI produced broken SQL too often. The escape hatch is +/// a `dialect=ansi` (or `dialect=`) parameter; see +/// [`dialect_override`]. +pub fn detect_or_err( + dbms_name: Option<&str>, + driver_hint: Option<&str>, +) -> crate::Result> { + detect(dbms_name, driver_hint) + .map(|e| e.dialect()) + .ok_or_else(|| { + crate::GgsqlError::ReaderError(format!( + "Unrecognized database backend (DBMS name: {}, driver: {}). \ + ggsql does not know which SQL dialect to use. If the backend \ + is close to a supported one, pin the dialect explicitly with \ + a `dialect=` parameter (e.g. dialect=postgres), or use \ + dialect=ansi for generic ANSI SQL.", + dbms_name.unwrap_or(""), + driver_hint.unwrap_or(""), + )) + }) +} + #[cfg(test)] mod tests { use super::*; From d70786313ad9bb4ea31fd24871b19346c13374bf Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 6 Oct 2026 17:52:18 +0200 Subject: [PATCH 02/10] Explicit FromItem kinds in the SQL builder; derived tables via Select everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Select::from/from_aliased now take a FromItem (Table / Query / Raw / Fragment) instead of guessing from whitespace whether a string is a table or a query — a quoted name with a space was previously mis-parenthesized, and an already-aliased fragment got a second alias. The handful of trait-boundary arguments that are genuinely 'bare name or pre-composed relation' use FromItem::Fragment with a documented contract; sql_quantile's signature now carries the kind explicitly. Hand-built derived tables in the oracle, duckdb, clickhouse, snowflake and trino dialects now go through sql::Select, so they get CTE hoisting, the dialect's alias clause and a mandatory (MySQL-safe) derived-table alias like everything else. build_limited combined with order_by no longer nests ORDER BY inside a derived-table limit wrap (T-SQL error 1033): dialects whose sql_limit wraps (mssql, oracle) opt in via sql_limit_wraps_query and the ordering is emitted outside the wrap. --- src/execute/layer.rs | 6 +- src/execute/schema.rs | 4 +- src/golden/duckdb.sql | 2 +- src/golden/oracle.sql | 128 +++++++++++++------------- src/golden/snowflake.sql | 2 +- src/golden/trino.sql | 2 +- src/plot/layer/geom/boxplot.rs | 24 ++--- src/plot/layer/geom/density.rs | 4 +- src/plot/layer/geom/histogram.rs | 2 +- src/plot/layer/geom/mod.rs | 10 +- src/plot/layer/geom/ribbon.rs | 2 +- src/plot/layer/geom/rule.rs | 9 +- src/plot/layer/geom/segment.rs | 5 +- src/plot/layer/geom/spatial.rs | 2 +- src/plot/layer/geom/stat_aggregate.rs | 8 +- src/plot/layer/geom/tile.rs | 9 +- src/plot/layer/geom/types.rs | 4 +- src/plot/projection/coord/map.rs | 4 +- src/reader/dialects/bigquery.rs | 12 ++- src/reader/dialects/clickhouse.rs | 26 ++++-- src/reader/dialects/databricks.rs | 8 +- src/reader/dialects/datafusion.rs | 8 +- src/reader/dialects/duckdb.rs | 31 +++++-- src/reader/dialects/mod.rs | 2 +- src/reader/dialects/monetdb.rs | 10 +- src/reader/dialects/mssql.rs | 6 +- src/reader/dialects/oracle.rs | 20 +++- src/reader/dialects/postgres.rs | 10 +- src/reader/dialects/snowflake.rs | 24 +++-- src/reader/dialects/trino.rs | 24 +++-- src/reader/mod.rs | 32 +++++-- src/sql/mod.rs | 119 ++++++++++++++++++------ 32 files changed, 367 insertions(+), 192 deletions(-) diff --git a/src/execute/layer.rs b/src/execute/layer.rs index 305d3ccb..bd382b32 100644 --- a/src/execute/layer.rs +++ b/src/execute/layer.rs @@ -379,7 +379,7 @@ pub fn apply_pre_stat_transform( }) .collect(); - crate::sql::select_from(dialect, &select_exprs.join(", "), query, "__ggsql_pre__") + crate::sql::select_from(dialect, &select_exprs.join(", "), crate::sql::FromItem::Query(query), "__ggsql_pre__") } /// Part 1: Build the initial layer query with SELECT, casts, filters, and aesthetic renames. @@ -428,7 +428,7 @@ pub fn build_layer_base_query( // Build query with optional WHERE clause let mut query = crate::sql::Select::new(dialect) .select(select_clause) - .from_aliased(source_query, "__ggsql_src__"); + .from_aliased(crate::sql::FromItem::Query(source_query), "__ggsql_src__"); if let Some(ref f) = layer.filter { query = query.and_where(f.as_str()); } @@ -696,7 +696,7 @@ where } else { crate::sql::Select::new(dialect) .select_star_plus(&stat_rename_exprs, "__ggsql_stat__") - .from_aliased(&transformed_query, "__ggsql_stat__") + .from_aliased(crate::sql::FromItem::Query(&transformed_query), "__ggsql_stat__") .build() } } diff --git a/src/execute/schema.rs b/src/execute/schema.rs index 7abb9e43..88be31b5 100644 --- a/src/execute/schema.rs +++ b/src/execute/schema.rs @@ -57,7 +57,7 @@ pub fn build_minmax_query( crate::sql::Select::new(dialect) .with_cte(&__ggsql_source__, source_query) .select(exprs.join(", ")) - .from(&__ggsql_source__) + .from(crate::sql::FromItem::Table(&__ggsql_source__)) .build() } @@ -163,7 +163,7 @@ where { let schema_query = crate::sql::Select::new(dialect) .select_star() - .from_aliased(query, naming::SCHEMA_ALIAS) + .from_aliased(crate::sql::FromItem::Query(query), naming::SCHEMA_ALIAS) .build_limited(1); let schema_df = execute_query(&schema_query)?; diff --git a/src/golden/duckdb.sql b/src/golden/duckdb.sql index 72d2a7f6..d92914a3 100644 --- a/src/golden/duckdb.sql +++ b/src/golden/duckdb.sql @@ -136,7 +136,7 @@ SELECT ST_Transform(ST_Point(0, 0), '+proj=longlat +datum=WGS84 +no_defs +type=c LOAD spatial; CREATE OR REPLACE TEMP TABLE "__ggsql_clip_boundary___" AS SELECT ST_Transform(ST_GeomFromText('POLYGON((-180.000000 85.000000, 180.000000 85.000000, 180.000000 -85.000000, -180.000000 -85.000000, -180.000000 85.000000))'), 'EPSG:4326', '+proj=longlat +datum=WGS84 +no_defs +type=crs', always_xy := true) AS geom; SELECT ST_AsText(ST_Transform(ST_GeomFromText('POLYGON((-180.000000 85.000000, 180.000000 85.000000, 180.000000 -85.000000, -180.000000 -85.000000, -180.000000 85.000000))'), 'EPSG:4326', '+proj=merc +lon_0=0', always_xy := true)) AS wkt; -SELECT ST_XMin(ext) AS xmin, ST_YMin(ext) AS ymin, ST_XMax(ext) AS xmax, ST_YMax(ext) AS ymax FROM (SELECT ST_Extent_Agg(ST_Transform(geom, '+proj=longlat +datum=WGS84 +no_defs +type=crs', '+proj=merc +lon_0=0', always_xy := true)) AS ext FROM __ggsql_clip_boundary___); +SELECT ST_XMin(ext) AS xmin, ST_YMin(ext) AS ymin, ST_XMax(ext) AS xmax, ST_YMax(ext) AS ymax FROM (SELECT ST_Extent_Agg(ST_Transform(geom, '+proj=longlat +datum=WGS84 +no_defs +type=crs', '+proj=merc +lon_0=0', always_xy := true)) AS ext FROM __ggsql_clip_boundary___) AS "__ggsql_ext__"; CREATE OR REPLACE TEMP TABLE "__ggsql_layer_0___proj" AS SELECT ST_X("__ggsql_proj_pt__") AS "__ggsql_aes_pos1__", ST_Y("__ggsql_proj_pt__") AS "__ggsql_aes_pos2__" FROM (SELECT *, ST_Transform(ST_Point("__ggsql_aes_pos1__", "__ggsql_aes_pos2__"), '+proj=longlat +datum=WGS84 +no_defs +type=crs', '+proj=merc +lon_0=0', always_xy := true) AS "__ggsql_proj_pt__" FROM (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") AS "__ggsql_proj__") AS "__ggsql_pp__"; SELECT MIN("__ggsql_aes_pos1__"), MIN("__ggsql_aes_pos2__"), MAX("__ggsql_aes_pos1__"), MAX("__ggsql_aes_pos2__") FROM "__ggsql_layer_0___proj"; SELECT * FROM "__ggsql_layer_0___proj"; diff --git a/src/golden/oracle.sql b/src/golden/oracle.sql index 8fd8ca07..671ed035 100644 --- a/src/golden/oracle.sql +++ b/src/golden/oracle.sql @@ -1,145 +1,145 @@ -- case: scatter_color -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT *, "grp" AS "__ggsql_aes_fill__", "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__"; -- case: histogram -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT MIN("__ggsql_aes_pos1__") as min_val, MAX("__ggsql_aes_pos1__") as max_val FROM (SELECT *, "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_stats__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_bin" AS "__ggsql_aes_pos1__", "__ggsql_stat_bin_end" AS "__ggsql_aes_pos1end__", "__ggsql_stat_count" AS "__ggsql_aes_pos2__" FROM (WITH "__stat_src__" AS (SELECT *, "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__"), "__bin_src__" AS (SELECT *, (GREATEST(0, CEIL(("__ggsql_aes_pos1__" - 1 + 1 * 0.5) / 1) - 1)) * 1 + 1 - 1 * 0.5 AS "__ggsql_bin_key__" FROM "__stat_src__"), "__binned__" AS (SELECT "__ggsql_bin_key__" AS "__ggsql_stat_bin", COUNT(*) AS "__ggsql_stat_count" FROM "__bin_src__" GROUP BY "__ggsql_bin_key__") SELECT *, "__ggsql_stat_bin" + 1 AS "__ggsql_stat_bin_end", "__ggsql_stat_count" * 1.0 / SUM("__ggsql_stat_count") OVER () AS "__ggsql_stat_density" FROM "__binned__") "__ggsql_stat__"; -- case: boxplot_grouped -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS "__ggsql_aes_pos2__", "__ggsql_stat_value2" AS "__ggsql_aes_pos2end__" FROM (WITH summary AS ( SELECT "__ggsql_fn__".*, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT "__ggsql_aes_pos1__", MIN("__ggsql_aes_pos2__") AS min, MAX("__ggsql_aes_pos2__") AS max, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS q1, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS median, PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS q3 FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__") "__ggsql_fn__" ), outliers AS ( SELECT raw."__ggsql_aes_pos2__" AS value, 'outlier' AS type, raw."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__" FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "raw" JOIN summary ON raw."__ggsql_aes_pos1__" = summary."__ggsql_aes_pos1__" WHERE raw."__ggsql_aes_pos2__" NOT BETWEEN summary.lower AND summary.upper ) SELECT "__ggsql_aes_pos1__", 'lower_whisker' AS "__ggsql_stat_type", q1 AS "__ggsql_stat_value", lower AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'upper_whisker' AS "__ggsql_stat_type", q3 AS "__ggsql_stat_value", upper AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'box' AS "__ggsql_stat_type", q1 AS "__ggsql_stat_value", q3 AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'median' AS "__ggsql_stat_type", median AS "__ggsql_stat_value", NULL AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", type AS "__ggsql_stat_type", value AS "__ggsql_stat_value", NULL AS "__ggsql_stat_value2" FROM outliers) "__ggsql_stat__"; -- case: line_aggregate -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: density_grouped -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos1" AS "__ggsql_aes_pos1__", "__ggsql_stat_density" AS "__ggsql_aes_pos2__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos1__"*"__ggsql_aes_pos1__") - AVG("__ggsql_aes_pos1__")*AVG("__ggsql_aes_pos1__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_fill__", "__ggsql_aes_stroke__", MIN("__ggsql_aes_pos1__") AS x_min, MAX("__ggsql_aes_pos1__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_qt__" WHERE "__ggsql_aes_pos1__" IS NOT NULL GROUP BY "__ggsql_aes_fill__", "__ggsql_aes_stroke__" ), data AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_aes_pos1__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_data__" WHERE "__ggsql_aes_pos1__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), grid AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + ("seq".n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range "__ggsql_global__" CROSS JOIN "__ggsql_seq__" "seq" CROSS JOIN (SELECT DISTINCT "__ggsql_aes_fill__", "__ggsql_aes_stroke__" FROM bandwidth) "__ggsql_groups__" ) SELECT "__ggsql_stat_pos1", "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos1", grid."__ggsql_aes_fill__" AS "__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" AS "__ggsql_aes_stroke__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_fill__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_fill__") AND (data."__ggsql_aes_stroke__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_stroke__") INNER JOIN grid ON (grid."__ggsql_aes_fill__" IS NOT DISTINCT FROM data."__ggsql_aes_fill__") AND (grid."__ggsql_aes_stroke__" IS NOT DISTINCT FROM data."__ggsql_aes_stroke__") GROUP BY grid.x, grid."__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" ) "__ggsql_kde__") "__ggsql_stat__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos1" AS "__ggsql_aes_pos1__", "__ggsql_stat_density" AS "__ggsql_aes_pos2__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos1__"*"__ggsql_aes_pos1__") - AVG("__ggsql_aes_pos1__")*AVG("__ggsql_aes_pos1__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_fill__", "__ggsql_aes_stroke__", MIN("__ggsql_aes_pos1__") AS x_min, MAX("__ggsql_aes_pos1__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_qt__" WHERE "__ggsql_aes_pos1__" IS NOT NULL GROUP BY "__ggsql_aes_fill__", "__ggsql_aes_stroke__" ), data AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_aes_pos1__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_data__" WHERE "__ggsql_aes_pos1__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), grid AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + (seq.n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range AS "__ggsql_global__" CROSS JOIN "__ggsql_seq__" AS seq CROSS JOIN (SELECT DISTINCT "__ggsql_aes_fill__", "__ggsql_aes_stroke__" FROM bandwidth) AS "__ggsql_groups__" ) SELECT "__ggsql_stat_pos1", "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos1", grid."__ggsql_aes_fill__" AS "__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" AS "__ggsql_aes_stroke__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_fill__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_fill__") AND (data."__ggsql_aes_stroke__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_stroke__") INNER JOIN grid ON (grid."__ggsql_aes_fill__" IS NOT DISTINCT FROM data."__ggsql_aes_fill__") AND (grid."__ggsql_aes_stroke__" IS NOT DISTINCT FROM data."__ggsql_aes_stroke__") GROUP BY grid.x, grid."__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" ) AS "__ggsql_kde__") "__ggsql_stat__"; -- case: line_temporal BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; CREATE TABLE "__ggsql_global___" AS SELECT * FROM ggsql_live_test; -SELECT * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__" ORDER BY "__ggsql_aes_pos1__"; -- case: boxplot_global_source BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; CREATE TABLE "__ggsql_global___" AS SELECT * FROM ggsql_live_test; -SELECT * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS "__ggsql_aes_pos2__", "__ggsql_stat_value2" AS "__ggsql_aes_pos2end__" FROM (WITH summary AS ( SELECT "__ggsql_fn__".*, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT "__ggsql_aes_pos1__", MIN("__ggsql_aes_pos2__") AS min, MAX("__ggsql_aes_pos2__") AS max, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS q1, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS median, PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS q3 FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__") "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__") "__ggsql_fn__" ), outliers AS ( SELECT raw."__ggsql_aes_pos2__" AS value, 'outlier' AS type, raw."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__" FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__") "raw" JOIN summary ON raw."__ggsql_aes_pos1__" = summary."__ggsql_aes_pos1__" WHERE raw."__ggsql_aes_pos2__" NOT BETWEEN summary.lower AND summary.upper ) SELECT "__ggsql_aes_pos1__", 'lower_whisker' AS "__ggsql_stat_type", q1 AS "__ggsql_stat_value", lower AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'upper_whisker' AS "__ggsql_stat_type", q3 AS "__ggsql_stat_value", upper AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'box' AS "__ggsql_stat_type", q1 AS "__ggsql_stat_value", q3 AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'median' AS "__ggsql_stat_type", median AS "__ggsql_stat_value", NULL AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", type AS "__ggsql_stat_type", value AS "__ggsql_stat_value", NULL AS "__ggsql_stat_value2" FROM outliers) "__ggsql_stat__"; -- case: bar_count -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_count" AS "__ggsql_aes_pos2__" FROM (WITH "__stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__"), "__grouped__" AS (SELECT "__ggsql_aes_pos1__", COUNT(*) AS "__ggsql_stat_count" FROM "__stat_src__" GROUP BY "__ggsql_aes_pos1__") SELECT *, "__ggsql_stat_count" * 1.0 / SUM("__ggsql_stat_count") OVER () AS "__ggsql_stat_proportion" FROM "__grouped__") "__ggsql_stat__"; -- case: smooth_lm -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_pos1" AS "__ggsql_aes_pos1__", "__ggsql_stat_intensity" AS "__ggsql_aes_pos2__" FROM (WITH coefficients AS ( SELECT AVG("__ggsql_aes_pos1__") AS x_mean, AVG("__ggsql_aes_pos2__") AS y_mean, AVG("__ggsql_aes_pos1__" * "__ggsql_aes_pos2__") AS xy_mean, AVG("__ggsql_aes_pos1__" * "__ggsql_aes_pos1__") AS xx_mean, MIN("__ggsql_aes_pos1__") AS x_min, MAX("__ggsql_aes_pos1__") AS x_max FROM (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_smooth__" WHERE "__ggsql_aes_pos1__" IS NOT NULL AND "__ggsql_aes_pos2__" IS NOT NULL ) SELECT x_min AS "__ggsql_stat_pos1", (y_mean + ((xy_mean - x_mean * y_mean) / (xx_mean - x_mean * x_mean)) * (x_min - x_mean)) AS "__ggsql_stat_intensity" FROM coefficients UNION ALL SELECT x_max AS "__ggsql_stat_pos1", (y_mean + ((xy_mean - x_mean * y_mean) / (xx_mean - x_mean * x_mean)) * (x_max - x_mean)) AS "__ggsql_stat_intensity" FROM coefficients) "__ggsql_stat__"; -- case: ribbon -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2max__", "id" AS "__ggsql_aes_pos2min__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2max__", "id" AS "__ggsql_aes_pos2min__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2max__", "id" AS "__ggsql_aes_pos2min__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" ORDER BY "__ggsql_aes_pos1__"; -- case: segment -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "id" AS "__ggsql_aes_pos1end__", "val" AS "__ggsql_aes_pos2__", "val" AS "__ggsql_aes_pos2end__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "id" AS "__ggsql_aes_pos1end__", "val" AS "__ggsql_aes_pos2__", "val" AS "__ggsql_aes_pos2end__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT *, "id" AS "__ggsql_aes_pos1__", "id" AS "__ggsql_aes_pos1end__", "val" AS "__ggsql_aes_pos2__", "val" AS "__ggsql_aes_pos2end__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__"; -- case: tile -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "val" AS "__ggsql_aes_pos1__", "id" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "val" AS "__ggsql_aes_pos1__", "id" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_pos1min" AS "__ggsql_aes_pos1min__", "__ggsql_stat_pos1max" AS "__ggsql_aes_pos1max__", "__ggsql_stat_pos2min" AS "__ggsql_aes_pos2min__", "__ggsql_stat_pos2max" AS "__ggsql_aes_pos2max__" FROM (SELECT ("__ggsql_aes_pos1__" - 0.5) AS "__ggsql_stat_pos1min", ("__ggsql_aes_pos1__" + 0.5) AS "__ggsql_stat_pos1max", ("__ggsql_aes_pos2__" - 0.5) AS "__ggsql_stat_pos2min", ("__ggsql_aes_pos2__" + 0.5) AS "__ggsql_stat_pos2max" FROM (SELECT *, "val" AS "__ggsql_aes_pos1__", "id" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_tile_stat__") "__ggsql_stat__"; -- case: area -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" ORDER BY "__ggsql_aes_pos1__"; -- case: violin -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__", "__ggsql_stat_density" AS "__ggsql_aes_offset__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos2__"*"__ggsql_aes_pos2__") - AVG("__ggsql_aes_pos2__")*AVG("__ggsql_aes_pos2__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_pos1__", MIN("__ggsql_aes_pos2__") AS x_min, MAX("__ggsql_aes_pos2__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__" ), data AS ( SELECT "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_data__" WHERE "__ggsql_aes_pos2__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), full_grid AS ( SELECT "__ggsql_aes_pos1__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + ("seq".n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range "__ggsql_global__" CROSS JOIN "__ggsql_seq__" "seq" CROSS JOIN (SELECT DISTINCT "__ggsql_aes_pos1__" FROM bandwidth) "__ggsql_groups__" ), grid AS ( SELECT full_grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", full_grid.x FROM full_grid INNER JOIN bandwidth ON (full_grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") WHERE full_grid.x >= bandwidth.x_min - 3 * bandwidth.bw AND full_grid.x <= bandwidth.x_max + 3 * bandwidth.bw ) SELECT "__ggsql_stat_pos2", "__ggsql_aes_pos1__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos2", grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") INNER JOIN grid ON (grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM data."__ggsql_aes_pos1__") GROUP BY grid.x, grid."__ggsql_aes_pos1__" ) "__ggsql_kde__") "__ggsql_stat__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__", "__ggsql_stat_density" AS "__ggsql_aes_offset__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos2__"*"__ggsql_aes_pos2__") - AVG("__ggsql_aes_pos2__")*AVG("__ggsql_aes_pos2__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_pos1__", MIN("__ggsql_aes_pos2__") AS x_min, MAX("__ggsql_aes_pos2__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__" ), data AS ( SELECT "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_data__" WHERE "__ggsql_aes_pos2__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), full_grid AS ( SELECT "__ggsql_aes_pos1__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + (seq.n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range AS "__ggsql_global__" CROSS JOIN "__ggsql_seq__" AS seq CROSS JOIN (SELECT DISTINCT "__ggsql_aes_pos1__" FROM bandwidth) AS "__ggsql_groups__" ), grid AS ( SELECT full_grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", full_grid.x FROM full_grid INNER JOIN bandwidth ON (full_grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") WHERE full_grid.x >= bandwidth.x_min - 3 * bandwidth.bw AND full_grid.x <= bandwidth.x_max + 3 * bandwidth.bw ) SELECT "__ggsql_stat_pos2", "__ggsql_aes_pos1__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos2", grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") INNER JOIN grid ON (grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM data."__ggsql_aes_pos1__") GROUP BY grid.x, grid."__ggsql_aes_pos1__" ) AS "__ggsql_kde__") "__ggsql_stat__"; -- case: filter_layer BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; CREATE TABLE "__ggsql_global___" AS SELECT * FROM ggsql_live_test; -SELECT * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") "__ggsql_src__" WHERE grp = 'a'; -- case: binned_temporal -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "id", "val", "grp", "day", "mixed Case", (CASE WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.2 THEN 19000.1 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.2 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.4 THEN 19000.300000000003 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.4 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.6 THEN 19000.5 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.6 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.8 THEN 19000.699999999997 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.8 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19001 THEN 19000.9 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19001 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) <= 19001.2 THEN 19001.1 ELSE NULL END) AS "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" FROM (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_pre__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "id", "val", "grp", "day", "mixed Case", "__ggsql_lim__", (CASE WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.2 THEN 19000.1 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.2 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.4 THEN 19000.300000000003 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.4 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.6 THEN 19000.5 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.6 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19000.8 THEN 19000.699999999997 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19000.8 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) < 19001 THEN 19000.9 WHEN CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) >= 19001 AND CAST("__ggsql_aes_pos1__" AS BINARY_DOUBLE) <= 19001.2 THEN 19001.1 ELSE NULL END) AS "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" FROM (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_pre__"; -- case: binned_temporal_date -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "id", "val", "grp", "day", "mixed Case", (CASE WHEN "__ggsql_aes_pos1__" >= CAST('2022-01-08' AS DATE) AND "__ggsql_aes_pos1__" <= CAST('2022-01-09' AS DATE) THEN CAST('2022-01-08' AS DATE) ELSE NULL END) AS "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" FROM (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_pre__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "id", "val", "grp", "day", "mixed Case", "__ggsql_lim__", (CASE WHEN "__ggsql_aes_pos1__" >= CAST('2022-01-08' AS DATE) AND "__ggsql_aes_pos1__" <= CAST('2022-01-09' AS DATE) THEN CAST('2022-01-08' AS DATE) ELSE NULL END) AS "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" FROM (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_pre__"; -- case: sum_aggregate -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", SUM("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", SUM("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: percentile_p10 -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.1) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.1) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: percentile_p90 -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: quoted_column -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS "__ggsql_aes_pos2__", "__ggsql_stat_value2" AS "__ggsql_aes_pos2end__" FROM (WITH summary AS ( SELECT "__ggsql_fn__".*, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT "__ggsql_aes_pos1__", MIN("__ggsql_aes_pos2__") AS min, MAX("__ggsql_aes_pos2__") AS max, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS q1, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS median, PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS q3 FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__") "__ggsql_fn__" ), outliers AS ( SELECT raw."__ggsql_aes_pos2__" AS value, 'outlier' AS type, raw."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__" FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "raw" JOIN summary ON raw."__ggsql_aes_pos1__" = summary."__ggsql_aes_pos1__" WHERE raw."__ggsql_aes_pos2__" NOT BETWEEN summary.lower AND summary.upper ) SELECT "__ggsql_aes_pos1__", 'lower_whisker' AS "__ggsql_stat_type", q1 AS "__ggsql_stat_value", lower AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'upper_whisker' AS "__ggsql_stat_type", q3 AS "__ggsql_stat_value", upper AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'box' AS "__ggsql_stat_type", q1 AS "__ggsql_stat_value", q3 AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", 'median' AS "__ggsql_stat_type", median AS "__ggsql_stat_value", NULL AS "__ggsql_stat_value2" FROM summary UNION ALL SELECT "__ggsql_aes_pos1__", type AS "__ggsql_stat_type", value AS "__ggsql_stat_value", NULL AS "__ggsql_stat_value2" FROM outliers) "__ggsql_stat__"; -- case: partition_by_quoted -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "mixed Case", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "mixed Case") "__ggsql_stat__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "mixed Case", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "mixed Case") "__ggsql_stat__"; -- case: grouped_bar -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "grp" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "grp" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_count" AS "__ggsql_aes_pos2__" FROM (WITH "__stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "grp" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__"), "__grouped__" AS (SELECT "__ggsql_aes_fill__", "__ggsql_aes_pos1__", COUNT(*) AS "__ggsql_stat_count" FROM "__stat_src__" GROUP BY __ggsql_aes_fill__, "__ggsql_aes_pos1__") SELECT *, "__ggsql_stat_count" * 1.0 / SUM("__ggsql_stat_count") OVER (PARTITION BY "__ggsql_aes_fill__") AS "__ggsql_stat_proportion" FROM "__grouped__") "__ggsql_stat__"; -- case: grouped_histogram -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT MIN("__ggsql_aes_pos1__") as min_val, MAX("__ggsql_aes_pos1__") as max_val FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_stats__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_bin" AS "__ggsql_aes_pos1__", "__ggsql_stat_bin_end" AS "__ggsql_aes_pos1end__", "__ggsql_stat_count" AS "__ggsql_aes_pos2__" FROM (WITH "__stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__"), "__bin_src__" AS (SELECT *, (GREATEST(0, CEIL(("__ggsql_aes_pos1__" - 1 + 1 * 0.5) / 1) - 1)) * 1 + 1 - 1 * 0.5 AS "__ggsql_bin_key__" FROM "__stat_src__"), "__binned__" AS (SELECT "__ggsql_aes_fill__", "__ggsql_bin_key__" AS "__ggsql_stat_bin", COUNT(*) AS "__ggsql_stat_count" FROM "__bin_src__" GROUP BY __ggsql_aes_fill__, "__ggsql_bin_key__") SELECT *, "__ggsql_stat_bin" + 1 AS "__ggsql_stat_bin_end", "__ggsql_stat_count" * 1.0 / SUM("__ggsql_stat_count") OVER (PARTITION BY "__ggsql_aes_fill__") AS "__ggsql_stat_density" FROM "__binned__") "__ggsql_stat__"; -- case: grouped_smooth -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT "__ggsql_stat__".*, "__ggsql_stat_pos1" AS "__ggsql_aes_pos1__", "__ggsql_stat_intensity" AS "__ggsql_aes_pos2__" FROM (WITH coefficients AS ( SELECT "__ggsql_aes_stroke__", AVG("__ggsql_aes_pos1__") AS x_mean, AVG("__ggsql_aes_pos2__") AS y_mean, AVG("__ggsql_aes_pos1__" * "__ggsql_aes_pos2__") AS xy_mean, AVG("__ggsql_aes_pos1__" * "__ggsql_aes_pos1__") AS xx_mean, MIN("__ggsql_aes_pos1__") AS x_min, MAX("__ggsql_aes_pos1__") AS x_max FROM (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_smooth__" WHERE "__ggsql_aes_pos1__" IS NOT NULL AND "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_stroke__" ) SELECT "__ggsql_aes_stroke__", x_min AS "__ggsql_stat_pos1", (y_mean + ((xy_mean - x_mean * y_mean) / (xx_mean - x_mean * x_mean)) * (x_min - x_mean)) AS "__ggsql_stat_intensity" FROM coefficients UNION ALL SELECT "__ggsql_aes_stroke__", x_max AS "__ggsql_stat_pos1", (y_mean + ((xy_mean - x_mean * y_mean) / (xx_mean - x_mean * x_mean)) * (x_max - x_mean)) AS "__ggsql_stat_intensity" FROM coefficients) "__ggsql_stat__"; -- case: spatial_mercator -- ERROR: Validation error: Spatial operations are not supported by this database backend -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT * FROM (SELECT proj4text FROM spatial_ref_sys WHERE srid = 4326) WHERE ROWNUM <= 1; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; +SELECT * FROM (SELECT proj4text FROM spatial_ref_sys WHERE srid = 4326) "__ggsql_lim__" WHERE ROWNUM <= 1; SELECT ST_Transform(ST_SetSRID(ST_Point(0, 0), 4326), '+proj=merc +lon_0=0') AS g; -- case: text_date_cast -SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") WHERE ROWNUM <= 1; -WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; +SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; +WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__"; diff --git a/src/golden/snowflake.sql b/src/golden/snowflake.sql index 656ca268..7935eedc 100644 --- a/src/golden/snowflake.sql +++ b/src/golden/snowflake.sql @@ -139,7 +139,7 @@ SELECT ST_Transform(ST_SetSRID(ST_Point(0, 0), 4326), '+proj=merc +lon_0=0') AS DROP TABLE IF EXISTS "__ggsql_clip_boundary___"; CREATE TEMP TABLE "__ggsql_clip_boundary___" AS SELECT ST_Transform(ST_SetSRID(ST_GeomFromText('POLYGON((-180.000000 85.000000, 180.000000 85.000000, 180.000000 -85.000000, -180.000000 -85.000000, -180.000000 85.000000))'), 4326), '+proj=longlat +datum=WGS84 +no_defs +type=crs') AS geom; SELECT ST_AsText(ST_Transform(ST_SetSRID(ST_GeomFromText('POLYGON((-180.000000 85.000000, 180.000000 85.000000, 180.000000 -85.000000, -180.000000 -85.000000, -180.000000 85.000000))'), 4326), '+proj=merc +lon_0=0')) AS wkt; -SELECT MIN(ST_XMIN(g)) AS xmin, MIN(ST_YMIN(g)) AS ymin, MAX(ST_XMAX(g)) AS xmax, MAX(ST_YMAX(g)) AS ymax FROM (SELECT ST_ENVELOPE(ST_Transform(ST_SetSRID(geom, 4326), '+proj=merc +lon_0=0')) AS g FROM __ggsql_clip_boundary___); +SELECT MIN(ST_XMIN(g)) AS xmin, MIN(ST_YMIN(g)) AS ymin, MAX(ST_XMAX(g)) AS xmax, MAX(ST_YMAX(g)) AS ymax FROM (SELECT ST_ENVELOPE(ST_Transform(ST_SetSRID(geom, 4326), '+proj=merc +lon_0=0')) AS g FROM __ggsql_clip_boundary___) AS "__ggsql_env__"; DROP TABLE IF EXISTS "__ggsql_layer_0___proj"; CREATE TEMP TABLE "__ggsql_layer_0___proj" AS SELECT ST_X("__ggsql_proj_pt__") AS "__ggsql_aes_pos1__", ST_Y("__ggsql_proj_pt__") AS "__ggsql_aes_pos2__" FROM (SELECT *, ST_Transform(ST_SetSRID(ST_Point("__ggsql_aes_pos1__", "__ggsql_aes_pos2__"), 4326), '+proj=merc +lon_0=0') AS "__ggsql_proj_pt__" FROM (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") AS "__ggsql_proj__") AS "__ggsql_pp__"; SELECT MIN("__ggsql_aes_pos1__"), MIN("__ggsql_aes_pos2__"), MAX("__ggsql_aes_pos1__"), MAX("__ggsql_aes_pos2__") FROM "__ggsql_layer_0___proj"; diff --git a/src/golden/trino.sql b/src/golden/trino.sql index 804dc05c..a710e254 100644 --- a/src/golden/trino.sql +++ b/src/golden/trino.sql @@ -139,7 +139,7 @@ SELECT ST_Transform(ST_SetSRID(ST_Point(0, 0), 4326), '+proj=merc +lon_0=0') AS DROP TABLE IF EXISTS "__ggsql_clip_boundary___"; CREATE TEMP TABLE "__ggsql_clip_boundary___" AS SELECT ST_Transform(ST_SetSRID(ST_GeomFromText('POLYGON((-180.000000 85.000000, 180.000000 85.000000, 180.000000 -85.000000, -180.000000 -85.000000, -180.000000 85.000000))'), 4326), '+proj=longlat +datum=WGS84 +no_defs +type=crs') AS geom; SELECT ST_AsText(ST_Transform(ST_SetSRID(ST_GeomFromText('POLYGON((-180.000000 85.000000, 180.000000 85.000000, 180.000000 -85.000000, -180.000000 -85.000000, -180.000000 85.000000))'), 4326), '+proj=merc +lon_0=0')) AS wkt; -SELECT MIN(ST_XMin(g)) AS xmin, MIN(ST_YMin(g)) AS ymin, MAX(ST_XMax(g)) AS xmax, MAX(ST_YMax(g)) AS ymax FROM (SELECT ST_Envelope(ST_Transform(ST_SetSRID(geom, 4326), '+proj=merc +lon_0=0')) AS g FROM __ggsql_clip_boundary___); +SELECT MIN(ST_XMin(g)) AS xmin, MIN(ST_YMin(g)) AS ymin, MAX(ST_XMax(g)) AS xmax, MAX(ST_YMax(g)) AS ymax FROM (SELECT ST_Envelope(ST_Transform(ST_SetSRID(geom, 4326), '+proj=merc +lon_0=0')) AS g FROM __ggsql_clip_boundary___) AS "__ggsql_env__"; DROP TABLE IF EXISTS "__ggsql_layer_0___proj"; CREATE TEMP TABLE "__ggsql_layer_0___proj" AS SELECT ST_X("__ggsql_proj_pt__") AS "__ggsql_aes_pos1__", ST_Y("__ggsql_proj_pt__") AS "__ggsql_aes_pos2__" FROM (SELECT *, ST_Transform(ST_SetSRID(ST_Point("__ggsql_aes_pos1__", "__ggsql_aes_pos2__"), 4326), '+proj=merc +lon_0=0') AS "__ggsql_proj_pt__" FROM (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") AS "__ggsql_proj__") AS "__ggsql_pp__"; SELECT MIN("__ggsql_aes_pos1__"), MIN("__ggsql_aes_pos2__"), MAX("__ggsql_aes_pos1__"), MAX("__ggsql_aes_pos2__") FROM "__ggsql_layer_0___proj"; diff --git a/src/plot/layer/geom/boxplot.rs b/src/plot/layer/geom/boxplot.rs index 22fb38b2..82ef779f 100644 --- a/src/plot/layer/geom/boxplot.rs +++ b/src/plot/layer/geom/boxplot.rs @@ -216,9 +216,9 @@ fn boxplot_sql_compute_summary( let groups_str = quoted_groups.join(", "); let lower_expr = dialect.sql_greatest(&[&format!("q1 - {coef} * (q3 - q1)"), "min"]); let upper_expr = dialect.sql_least(&[&format!("q3 + {coef} * (q3 - q1)"), "max"]); - let q1 = dialect.sql_quantile(value, 0.25, from, groups); - let median = dialect.sql_quantile(value, 0.50, from, groups); - let q3 = dialect.sql_quantile(value, 0.75, from, groups); + let q1 = dialect.sql_quantile(value, 0.25, crate::sql::FromItem::Query(from), groups); + let median = dialect.sql_quantile(value, 0.50, crate::sql::FromItem::Query(from), groups); + let q3 = dialect.sql_quantile(value, 0.75, crate::sql::FromItem::Query(from), groups); let quoted_value = dialect.quote_ident(value); let mut items: Vec = Vec::new(); if !groups_str.is_empty() { @@ -233,7 +233,7 @@ fn boxplot_sql_compute_summary( ]); let mut inner = crate::sql::Select::new(dialect) .select_items(&items) - .from_aliased(from, "__ggsql_qt__") + .from_aliased(crate::sql::FromItem::Query(from), "__ggsql_qt__") .and_where(format!("{quoted_value} IS NOT NULL")); if !groups_str.is_empty() { inner = inner.group_by(&groups_str); @@ -247,7 +247,7 @@ fn boxplot_sql_compute_summary( ], "__ggsql_fn__", ) - .from_aliased(&inner, "__ggsql_fn__") + .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_fn__") .build() } @@ -277,7 +277,7 @@ fn boxplot_sql_filter_outliers( items.extend(keep_columns); crate::sql::Select::new(dialect) .select_items(&items) - .from_aliased(from, "raw") + .from_aliased(crate::sql::FromItem::Query(from), "raw") .join_raw(&format!("JOIN summary ON {}", join_pairs.join(" AND "))) .and_where(format!( "raw.{quoted_value} NOT BETWEEN summary.lower AND summary.upper" @@ -432,9 +432,9 @@ mod tests { &AnsiDialect, ); - let q1 = AnsiDialect.sql_quantile("price", 0.25, "SELECT * FROM sales", &groups); - let median = AnsiDialect.sql_quantile("price", 0.50, "SELECT * FROM sales", &groups); - let q3 = AnsiDialect.sql_quantile("price", 0.75, "SELECT * FROM sales", &groups); + let q1 = AnsiDialect.sql_quantile("price", 0.25, crate::sql::FromItem::Query("SELECT * FROM sales"), &groups); + let median = AnsiDialect.sql_quantile("price", 0.50, crate::sql::FromItem::Query("SELECT * FROM sales"), &groups); + let q3 = AnsiDialect.sql_quantile("price", 0.75, crate::sql::FromItem::Query("SELECT * FROM sales"), &groups); let expected = format!( r#"SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, @@ -466,9 +466,9 @@ mod tests { &AnsiDialect, ); - let q1 = AnsiDialect.sql_quantile("revenue", 0.25, "SELECT * FROM data", &groups); - let median = AnsiDialect.sql_quantile("revenue", 0.50, "SELECT * FROM data", &groups); - let q3 = AnsiDialect.sql_quantile("revenue", 0.75, "SELECT * FROM data", &groups); + let q1 = AnsiDialect.sql_quantile("revenue", 0.25, crate::sql::FromItem::Query("SELECT * FROM data"), &groups); + let median = AnsiDialect.sql_quantile("revenue", 0.50, crate::sql::FromItem::Query("SELECT * FROM data"), &groups); + let q3 = AnsiDialect.sql_quantile("revenue", 0.75, crate::sql::FromItem::Query("SELECT * FROM data"), &groups); let expected = format!( r#"SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, diff --git a/src/plot/layer/geom/density.rs b/src/plot/layer/geom/density.rs index aa03e5de..8ad666b6 100644 --- a/src/plot/layer/geom/density.rs +++ b/src/plot/layer/geom/density.rs @@ -274,8 +274,8 @@ fn silverman_rule( let adjust = 0.9 * adjust; let v = dialect.quote_ident(value_column); let stddev = format!("SQRT(AVG({v}*{v}) - AVG({v})*AVG({v}))", v = v); - let q75 = dialect.sql_quantile(value_column, 0.75, from, groups); - let q25 = dialect.sql_quantile(value_column, 0.25, from, groups); + let q75 = dialect.sql_quantile(value_column, 0.75, crate::sql::FromItem::Query(from), groups); + let q25 = dialect.sql_quantile(value_column, 0.25, crate::sql::FromItem::Query(from), groups); let iqr = format!("({q75} - {q25}) / 1.34"); let min_expr = dialect.sql_least(&[&stddev, &iqr]); // POWER, not POW: T-SQL has no POW function. And the base must not be an diff --git a/src/plot/layer/geom/histogram.rs b/src/plot/layer/geom/histogram.rs index 58f4e6a9..b2767c9e 100644 --- a/src/plot/layer/geom/histogram.rs +++ b/src/plot/layer/geom/histogram.rs @@ -149,7 +149,7 @@ fn stat_histogram( let stats_query = crate::sql::select_from( dialect, &format!("MIN({x_col}) as min_val, MAX({x_col}) as max_val"), - query, + crate::sql::FromItem::Query(query), "__ggsql_stats__", ); let stats_df = execute_query(&stats_query)?; diff --git a/src/plot/layer/geom/mod.rs b/src/plot/layer/geom/mod.rs index a5427f6e..1609116d 100644 --- a/src/plot/layer/geom/mod.rs +++ b/src/plot/layer/geom/mod.rs @@ -381,7 +381,7 @@ pub(crate) fn project_position_columns( let inner = crate::sql::Select::new(dialect) .select_star_plus(&[format!("{transformed} AS {proj_col}")], "__ggsql_proj__") - .from_aliased(query, "__ggsql_proj__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_proj__") .build(); let x_expr = format!("ST_X({proj_col})"); let y_expr = format!("ST_Y({proj_col})"); @@ -394,7 +394,7 @@ pub(crate) fn project_position_columns( &[format!("{x_expr} AS {pos1}"), format!("{y_expr} AS {pos2}")], "__ggsql_pp__", ) - .from_aliased(&inner, "__ggsql_pp__") + .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_pp__") .build()); } let select_list: Vec = columns @@ -413,7 +413,7 @@ pub(crate) fn project_position_columns( Ok(crate::sql::select_from( dialect, &select_list.join(", "), - &inner, + crate::sql::FromItem::Query(&inner), "__ggsql_pp__", )) } @@ -518,7 +518,7 @@ pub(crate) fn densify_edges( )], "__ggsql_indexed__", ) - .from_aliased(query, "__ggsql_indexed__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_indexed__") .build() } else { query.to_string() @@ -568,7 +568,7 @@ pub(crate) fn densify_edges( edge_extras.push(format!("{seg_len} AS {__ggsql_seg_len__}")); let edges_query = crate::sql::Select::new(dialect) .select_star_plus(&edge_extras, "__ggsql_src__") - .from_aliased(&indexed_query, "__ggsql_src__") + .from_aliased(crate::sql::FromItem::Query(&indexed_query), "__ggsql_src__") .window(format!("w AS ({window_def})")) .build(); diff --git a/src/plot/layer/geom/ribbon.rs b/src/plot/layer/geom/ribbon.rs index 3a05bc47..ff9edffa 100644 --- a/src/plot/layer/geom/ribbon.rs +++ b/src/plot/layer/geom/ribbon.rs @@ -196,7 +196,7 @@ fn expand_ribbon_to_polygon( ], "__ggsql_ribbon__", ) - .from_aliased(query, "__ggsql_ribbon__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_ribbon__") .build(); // Build select list for each half diff --git a/src/plot/layer/geom/rule.rs b/src/plot/layer/geom/rule.rs index adc5d7d5..5deadcaa 100644 --- a/src/plot/layer/geom/rule.rs +++ b/src/plot/layer/geom/rule.rs @@ -99,7 +99,7 @@ impl GeomTrait for Rule { let clip_table = clip_boundary_table(); crate::sql::Select::new(dialect) .select_star() - .from_aliased(&densified, "__ggsql_dens__") + .from_aliased(crate::sql::FromItem::Query(&densified), "__ggsql_dens__") .and_where(dialect.sql_st_contains( &format!("(SELECT geom FROM {clip_table})"), &dialect.sql_st_point(&pos1_q, &pos2_q), @@ -250,7 +250,7 @@ fn expand_rule_to_segment( let scalar_bbox = |col: &str| { format!( "({})", - crate::sql::select_from(dialect, col, bbox_expr, "__ggsql_bbox__") + crate::sql::select_from(dialect, col, crate::sql::FromItem::Query(bbox_expr), "__ggsql_bbox__") ) }; let (fixed_expr, span_expr) = if has_pos1 { @@ -291,7 +291,7 @@ fn expand_rule_to_segment( )], "__ggsql_rule_src__", ) - .from_aliased(query, "__ggsql_rule_src__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_rule_src__") .build(); let vertices_table = format!("(SELECT 0 AS {__ggsql_vertex__} UNION ALL SELECT 1)"); @@ -305,8 +305,7 @@ fn expand_rule_to_segment( let __ggsql_vertices__ = dialect.quote_ident("__ggsql_vertices__"); let sql = crate::sql::Select::new(dialect) .select_items(&select_parts) - .from_aliased(&numbered, "__ggsql_rule__") - .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rule__") .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) .build(); let mut out_columns: Vec = passthrough_cols.into_iter().cloned().collect(); diff --git a/src/plot/layer/geom/segment.rs b/src/plot/layer/geom/segment.rs index e1c49874..68033fec 100644 --- a/src/plot/layer/geom/segment.rs +++ b/src/plot/layer/geom/segment.rs @@ -124,7 +124,7 @@ fn expand_segment_to_vertices( )], "__ggsql_numbered__", ) - .from_aliased(query, "__ggsql_numbered__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_numbered__") .build(); let __ggsql_vertex__ = dialect.quote_ident("__ggsql_vertex__"); @@ -148,8 +148,7 @@ fn expand_segment_to_vertices( let __ggsql_vertices__ = dialect.quote_ident("__ggsql_vertices__"); let sql = crate::sql::Select::new(dialect) .select_items(&select_parts) - .from_aliased(&numbered, "__ggsql_seg__") - .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_seg__") .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) .build(); let mut out_columns: Vec = passthrough_cols.into_iter().cloned().collect(); diff --git a/src/plot/layer/geom/spatial.rs b/src/plot/layer/geom/spatial.rs index fd8036bb..ad092d7e 100644 --- a/src/plot/layer/geom/spatial.rs +++ b/src/plot/layer/geom/spatial.rs @@ -25,7 +25,7 @@ fn apply_clip_boundary( let filtered = crate::sql::Select::new(dialect) .select_star() - .from_aliased(query, "__ggsql_clip__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_clip__") .and_where(dialect.sql_st_intersects(col, &clip_geom)) .build(); dialect.sql_select_replace(&geom_expr, col, &filtered, columns) diff --git a/src/plot/layer/geom/stat_aggregate.rs b/src/plot/layer/geom/stat_aggregate.rs index 99237a0b..c8190d99 100644 --- a/src/plot/layer/geom/stat_aggregate.rs +++ b/src/plot/layer/geom/stat_aggregate.rs @@ -458,10 +458,10 @@ fn simple_stat_sql_quantile( group_cols: &[String], ) -> String { if let Some(frac) = percentile_fraction(name) { - return dialect.sql_quantile(raw_col, frac, src_alias, group_cols); + return dialect.sql_quantile(raw_col, frac, crate::sql::FromItem::Table(src_alias), group_cols); } - let p75 = dialect.sql_quantile(raw_col, 0.75, src_alias, group_cols); - let p25 = dialect.sql_quantile(raw_col, 0.25, src_alias, group_cols); + let p75 = dialect.sql_quantile(raw_col, 0.75, crate::sql::FromItem::Table(src_alias), group_cols); + let p25 = dialect.sql_quantile(raw_col, 0.25, crate::sql::FromItem::Table(src_alias), group_cols); format!("({} - {})", p75, p25) } @@ -1057,7 +1057,7 @@ mod tests { &self, column: &str, fraction: f64, - _from: &str, + _from: crate::sql::FromItem<'_>, _groups: &[String], ) -> String { format!("QUANTILE_CONT({}, {})", self.quote_ident(column), fraction) diff --git a/src/plot/layer/geom/tile.rs b/src/plot/layer/geom/tile.rs index f40b1f03..52017eab 100644 --- a/src/plot/layer/geom/tile.rs +++ b/src/plot/layer/geom/tile.rs @@ -310,7 +310,7 @@ fn expand_rect_to_polygon( )], "__ggsql_numbered__", ) - .from_aliased(query, "__ggsql_numbered__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_numbered__") .build(); // Step 2: Expand to 4 corners via CROSS JOIN with UNION ALL literal table. @@ -349,8 +349,7 @@ fn expand_rect_to_polygon( let sql = crate::sql::Select::new(dialect) .select_items(&select_parts) - .from_aliased(&numbered, "__ggsql_rect__") - .join_raw(&format!("CROSS JOIN {corners_table} {__ggsql_corners__}")) + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rect__") .join_raw(&format!("CROSS JOIN {corners_table} {__ggsql_corners__}")) .build(); // Output columns: passthrough + poly_id + pos1 + pos2 @@ -389,7 +388,7 @@ fn rename_agg_stats_to_aes( .collect(); crate::sql::Select::new(dialect) .select_star_plus(&aliases, "__ggsql_post_agg__") - .from_aliased(&agg_query, "__ggsql_post_agg__") + .from_aliased(crate::sql::FromItem::Query(&agg_query), "__ggsql_post_agg__") .build() } @@ -529,7 +528,7 @@ fn stat_tile( // Build transformed query let transformed_query = - crate::sql::select_from(dialect, &select_list, query, "__ggsql_tile_stat__"); + crate::sql::select_from(dialect, &select_list, crate::sql::FromItem::Query(query), "__ggsql_tile_stat__"); // Use the same consumed aesthetic names for StatResult Ok(StatResult::Transformed { diff --git a/src/plot/layer/geom/types.rs b/src/plot/layer/geom/types.rs index af55c655..6dad13fd 100644 --- a/src/plot/layer/geom/types.rs +++ b/src/plot/layer/geom/types.rs @@ -242,7 +242,7 @@ pub fn wrap_with_order_by( } => StatResult::Transformed { query: crate::sql::Select::new(dialect) .select_star() - .from_aliased(&query, "__ggsql_ord__") + .from_aliased(crate::sql::FromItem::Query(&query), "__ggsql_ord__") .order_by(order_quoted) .build(), stat_columns, @@ -275,7 +275,7 @@ pub fn wrap_with_dummy_axis(query: &str, axis: &str, dialect: &dyn SqlDialect) - &[format!("'{dummy_v}' AS {}", dialect.quote_ident(&stat_col))], "__ggsql_dummy_src__", ) - .from_aliased(query, "__ggsql_dummy_src__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_dummy_src__") .build() } diff --git a/src/plot/projection/coord/map.rs b/src/plot/projection/coord/map.rs index a14e2ee6..7bae70ff 100644 --- a/src/plot/projection/coord/map.rs +++ b/src/plot/projection/coord/map.rs @@ -758,7 +758,7 @@ fn detect_source_srid( } let sql = crate::sql::Select::new(dialect) .select(format!("{} AS srid", dialect.sql_st_srid(&ensure_geom))) - .from_aliased(&layer_queries[idx], "__ggsql_srid__") + .from_aliased(crate::sql::FromItem::Query(&layer_queries[idx]), "__ggsql_srid__") .and_where(format!("{geom_col} IS NOT NULL")) .build_limited(1); if let Ok(df) = execute_query(&sql) { @@ -929,7 +929,7 @@ fn query_spatial_ref_sys( ) -> Option { let sql = crate::sql::Select::new(dialect) .select("proj4text") - .from("spatial_ref_sys") + .from(crate::sql::FromItem::Table("spatial_ref_sys")) .and_where(format!("srid = {code}")) .build_limited(1); let df = execute_query(&sql).ok()?; diff --git a/src/reader/dialects/bigquery.rs b/src/reader/dialects/bigquery.rs index 0ec5422c..6b0d7f68 100644 --- a/src/reader/dialects/bigquery.rs +++ b/src/reader/dialects/bigquery.rs @@ -43,7 +43,13 @@ impl SqlDialect for BigQueryDialect { ) } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { // APPROX_QUANTILES(x, 100) returns an array of 101 boundaries. // BigQuery rejects the correlated scalar subquery of the default; the // native aggregate computes within the caller's GROUP BY, so `from` @@ -100,7 +106,7 @@ mod tests { #[test] fn quantile_uses_approx_quantiles() { - let sql = BigQueryDialect.sql_quantile("v", 0.25, "t", &[]); + let sql = BigQueryDialect.sql_quantile("v", 0.25, crate::sql::FromItem::Table("t"), &[]); assert_eq!(sql, "APPROX_QUANTILES(`v`, 100)[SAFE_OFFSET(25)]"); } @@ -109,7 +115,7 @@ mod tests { // sql_quantile must not emit a correlated subquery — BigQuery // rejects those. It uses the APPROX_QUANTILES aggregate, valid // inside the GROUP BY queries that boxplot and density build. - let sql = BigQueryDialect.sql_quantile("v", 0.75, "SELECT * FROM t", &["g".to_string()]); + let sql = BigQueryDialect.sql_quantile("v", 0.75, crate::sql::FromItem::Query("SELECT * FROM t"), &["g".to_string()]); assert_eq!(sql, "APPROX_QUANTILES(`v`, 100)[SAFE_OFFSET(75)]"); assert!(!sql.contains("SELECT"), "must not be a subquery: {sql}"); } diff --git a/src/reader/dialects/clickhouse.rs b/src/reader/dialects/clickhouse.rs index 5d0c41c5..2349496a 100644 --- a/src/reader/dialects/clickhouse.rs +++ b/src/reader/dialects/clickhouse.rs @@ -67,11 +67,15 @@ impl SqlDialect for ClickHouseDialect { from: &str, _all_columns: &[String], ) -> String { - let __ggsql_sr__ = self.quote_ident("__ggsql_sr__"); - if expr == col { - return format!("SELECT * FROM ({from}) {__ggsql_sr__}"); - } - format!("SELECT * REPLACE ({expr} AS {col}) FROM ({from}) {__ggsql_sr__}") + let list = if expr == col { + "*".to_string() + } else { + format!("* REPLACE ({expr} AS {col})") + }; + crate::sql::Select::new(self) + .select(list) + .from_aliased(crate::sql::FromItem::Query(from), "__ggsql_sr__") + .build() } fn sql_generate_series(&self, n: usize) -> String { @@ -100,7 +104,13 @@ impl SqlDialect for ClickHouseDialect { /// Every caller embeds this in a `GROUP BY {groups}` query over `from`, so /// the native aggregate is equivalent to the correlated scalar subquery /// other dialects produce, and far cheaper. - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!( "quantileExactInclusive({fraction})({column})", column = self.quote_ident(column) @@ -174,11 +184,11 @@ mod tests { #[test] fn quantile_uses_exact_inclusive() { assert_eq!( - ClickHouseDialect.sql_quantile("v", 0.9, "t", &[]), + ClickHouseDialect.sql_quantile("v", 0.9, crate::sql::FromItem::Table("t"), &[]), "quantileExactInclusive(0.9)(`v`)" ); assert_eq!( - ClickHouseDialect.sql_quantile("v", 0.5, "SELECT * FROM t", &[]), + ClickHouseDialect.sql_quantile("v", 0.5, crate::sql::FromItem::Query("SELECT * FROM t"), &[]), "quantileExactInclusive(0.5)(`v`)" ); } diff --git a/src/reader/dialects/databricks.rs b/src/reader/dialects/databricks.rs index 8e8bc961..1e0c4574 100644 --- a/src/reader/dialects/databricks.rs +++ b/src/reader/dialects/databricks.rs @@ -43,7 +43,13 @@ impl SqlDialect for DatabricksDialect { ) } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { // Spark forbids correlated scalar subqueries in the SELECT list of a // GROUP BY query ("is neither present in GROUP BY, nor in an // aggregate function"), so the correlated-subquery default fails diff --git a/src/reader/dialects/datafusion.rs b/src/reader/dialects/datafusion.rs index 28f6cde0..10e1f921 100644 --- a/src/reader/dialects/datafusion.rs +++ b/src/reader/dialects/datafusion.rs @@ -48,7 +48,13 @@ impl SqlDialect for DataFusionDialect { /// ScalarSubquery"). Every caller embeds this in a `GROUP BY {groups}` /// query over `from`, where the native approximate aggregate is /// equivalent. - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!( "approx_percentile_cont({}, {fraction})", self.quote_ident(column) diff --git a/src/reader/dialects/duckdb.rs b/src/reader/dialects/duckdb.rs index 05fd9a3b..a60ada58 100644 --- a/src/reader/dialects/duckdb.rs +++ b/src/reader/dialects/duckdb.rs @@ -38,7 +38,10 @@ impl SqlDialect for DuckDbDialect { from: &str, _all_columns: &[String], ) -> String { - format!("SELECT * REPLACE ({expr} AS {col}) FROM ({from})") + crate::sql::Select::new(self) + .select(format!("* REPLACE ({expr} AS {col})")) + .from_aliased(crate::sql::FromItem::Query(from), "__ggsql_sr__") + .build() } fn sql_geometry_to_wkb(&self, column: &str) -> String { @@ -46,11 +49,17 @@ impl SqlDialect for DuckDbDialect { } fn sql_geometry_bbox(&self, column: &str, from: &str) -> String { - format!( - "SELECT ST_XMin(ext) AS xmin, ST_YMin(ext) AS ymin, \ - ST_XMax(ext) AS xmax, ST_YMax(ext) AS ymax \ - FROM (SELECT ST_Extent_Agg({column}) AS ext FROM {from})" - ) + let inner = crate::sql::Select::new(self) + .select(format!("ST_Extent_Agg({column}) AS ext")) + .from(crate::sql::FromItem::Fragment(from)) + .build(); + crate::sql::Select::new(self) + .select( + "ST_XMin(ext) AS xmin, ST_YMin(ext) AS ymin, \ + ST_XMax(ext) AS xmax, ST_YMax(ext) AS ymax", + ) + .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_ext__") + .build() } fn sql_spatial_setup(&self) -> Vec { @@ -69,7 +78,13 @@ impl SqlDialect for DuckDbDialect { format!("{__ggsql_seq__}(n) AS (SELECT generate_series FROM GENERATE_SERIES(0, {n} - 1))") } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!("QUANTILE_CONT({}, {})", self.quote_ident(column), fraction) } @@ -124,7 +139,7 @@ mod tests { fn quantile_is_native_and_quotes_raw_names() { // stat_aggregate passes the raw column name; the dialect must quote. assert_eq!( - DuckDbDialect.sql_quantile("mixed Case", 0.25, "src", &[]), + DuckDbDialect.sql_quantile("mixed Case", 0.25, crate::sql::FromItem::Table("src"), &[]), "QUANTILE_CONT(\"mixed Case\", 0.25)" ); } diff --git a/src/reader/dialects/mod.rs b/src/reader/dialects/mod.rs index 7e94cbfb..6c0746bd 100644 --- a/src/reader/dialects/mod.rs +++ b/src/reader/dialects/mod.rs @@ -346,7 +346,7 @@ mod tests { for scheme in ALL_SCHEMES { let d = dialect_for_scheme(scheme).unwrap(); let quoted = d.quote_ident("mixed Case"); - let sql = d.sql_quantile("mixed Case", 0.5, "t", &[]); + let sql = d.sql_quantile("mixed Case", 0.5, crate::sql::FromItem::Table("t"), &[]); assert!( sql.contains("ed), "{scheme}: sql_quantile does not quote its column: {sql}" diff --git a/src/reader/dialects/monetdb.rs b/src/reader/dialects/monetdb.rs index f845da6b..01c181f8 100644 --- a/src/reader/dialects/monetdb.rs +++ b/src/reader/dialects/monetdb.rs @@ -28,7 +28,13 @@ impl SqlDialect for MonetDbDialect { super::case_least(exprs) } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!( "QUANTILE({column}, {fraction})", column = self.quote_ident(column) @@ -113,7 +119,7 @@ mod tests { #[test] fn quantile_is_native() { assert_eq!( - MonetDbDialect.sql_quantile("v", 0.75, "t", &[]), + MonetDbDialect.sql_quantile("v", 0.75, crate::sql::FromItem::Table("t"), &[]), "QUANTILE(\"v\", 0.75)" ); } diff --git a/src/reader/dialects/mssql.rs b/src/reader/dialects/mssql.rs index 12ce0a2b..d35853a6 100644 --- a/src/reader/dialects/mssql.rs +++ b/src/reader/dialects/mssql.rs @@ -73,10 +73,14 @@ impl SqlDialect for MssqlDialect { fn sql_limit(&self, query: &str, n: usize) -> String { crate::sql::Select::new(self) .select(format!("TOP {n} *")) - .from_aliased(query, "__ggsql_lim__") + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_lim__") .build() } + fn sql_limit_wraps_query(&self) -> bool { + true + } + fn allows_cte_in_derived_table(&self) -> bool { // T-SQL forbids CTEs inside a derived table ("Incorrect syntax near // the keyword 'WITH'"); the query builder hoists them out. diff --git a/src/reader/dialects/oracle.rs b/src/reader/dialects/oracle.rs index d0cfd7bd..74c8ad18 100644 --- a/src/reader/dialects/oracle.rs +++ b/src/reader/dialects/oracle.rs @@ -35,7 +35,15 @@ impl SqlDialect for OracleDialect { } fn sql_limit(&self, query: &str, n: usize) -> String { - format!("SELECT * FROM ({query}) WHERE ROWNUM <= {n}") + crate::sql::Select::new(self) + .select_star() + .from_aliased(crate::sql::FromItem::Query(query), "__ggsql_lim__") + .and_where(format!("ROWNUM <= {n}")) + .build() + } + + fn sql_limit_wraps_query(&self) -> bool { + true } fn sql_generate_series(&self, n: usize) -> String { @@ -48,7 +56,13 @@ impl SqlDialect for OracleDialect { ) } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!( "PERCENTILE_CONT({fraction}) WITHIN GROUP (ORDER BY {column})", column = self.quote_ident(column) @@ -104,7 +118,7 @@ mod tests { fn limit_uses_rownum() { assert_eq!( OracleDialect.sql_limit("SELECT a FROM t", 5), - "SELECT * FROM (SELECT a FROM t) WHERE ROWNUM <= 5" + "SELECT * FROM (SELECT a FROM t) \"__ggsql_lim__\" WHERE ROWNUM <= 5" ); } diff --git a/src/reader/dialects/postgres.rs b/src/reader/dialects/postgres.rs index 27241b8c..f974010d 100644 --- a/src/reader/dialects/postgres.rs +++ b/src/reader/dialects/postgres.rs @@ -40,7 +40,13 @@ impl SqlDialect for PostgresDialect { } } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!( "PERCENTILE_CONT({fraction}) WITHIN GROUP (ORDER BY {column})", column = self.quote_ident(column) @@ -67,7 +73,7 @@ mod tests { #[test] fn quantile_uses_ordered_set() { - let sql = PostgresDialect.sql_quantile("v", 0.5, "t", &[]); + let sql = PostgresDialect.sql_quantile("v", 0.5, crate::sql::FromItem::Table("t"), &[]); assert_eq!(sql, "PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY \"v\")"); } diff --git a/src/reader/dialects/snowflake.rs b/src/reader/dialects/snowflake.rs index 8d2af33d..da5d4bc1 100644 --- a/src/reader/dialects/snowflake.rs +++ b/src/reader/dialects/snowflake.rs @@ -37,7 +37,13 @@ impl SqlDialect for SnowflakeDialect { ) } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!( "APPROX_PERCENTILE({column}, {fraction})", column = self.quote_ident(column) @@ -62,11 +68,17 @@ impl SqlDialect for SnowflakeDialect { fn sql_geometry_bbox(&self, column: &str, from: &str) -> String { // Snowflake has no ST_Extent aggregate; aggregate per-row envelopes. - format!( - "SELECT MIN(ST_XMIN(g)) AS xmin, MIN(ST_YMIN(g)) AS ymin, \ - MAX(ST_XMAX(g)) AS xmax, MAX(ST_YMAX(g)) AS ymax \ - FROM (SELECT ST_ENVELOPE({column}) AS g FROM {from})" - ) + let inner = crate::sql::Select::new(self) + .select(format!("ST_ENVELOPE({column}) AS g")) + .from(crate::sql::FromItem::Fragment(from)) + .build(); + crate::sql::Select::new(self) + .select( + "MIN(ST_XMIN(g)) AS xmin, MIN(ST_YMIN(g)) AS ymin, \ + MAX(ST_XMAX(g)) AS xmax, MAX(ST_YMAX(g)) AS ymax", + ) + .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_env__") + .build() } } diff --git a/src/reader/dialects/trino.rs b/src/reader/dialects/trino.rs index dd033081..464d602f 100644 --- a/src/reader/dialects/trino.rs +++ b/src/reader/dialects/trino.rs @@ -33,7 +33,13 @@ impl SqlDialect for TrinoDialect { ) } - fn sql_quantile(&self, column: &str, fraction: f64, _from: &str, _groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> String { format!( "approx_percentile({column}, {fraction})", column = self.quote_ident(column) @@ -58,11 +64,17 @@ impl SqlDialect for TrinoDialect { } fn sql_geometry_bbox(&self, column: &str, from: &str) -> String { - format!( - "SELECT MIN(ST_XMin(g)) AS xmin, MIN(ST_YMin(g)) AS ymin, \ - MAX(ST_XMax(g)) AS xmax, MAX(ST_YMax(g)) AS ymax \ - FROM (SELECT ST_Envelope({column}) AS g FROM {from})" - ) + let inner = crate::sql::Select::new(self) + .select(format!("ST_Envelope({column}) AS g")) + .from(crate::sql::FromItem::Fragment(from)) + .build(); + crate::sql::Select::new(self) + .select( + "MIN(ST_XMin(g)) AS xmin, MIN(ST_YMin(g)) AS ymin, \ + MAX(ST_XMax(g)) AS xmax, MAX(ST_YMax(g)) AS ymax", + ) + .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_env__") + .build() } } diff --git a/src/reader/mod.rs b/src/reader/mod.rs index fa631523..1e3d45ff 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -126,6 +126,15 @@ pub trait SqlDialect { format!("{} LIMIT {}", query, n) } + /// Whether [`sql_limit`](SqlDialect::sql_limit) wraps the query in a + /// derived table (T-SQL's `SELECT TOP n * FROM (…)`, Oracle's ROWNUM + /// wrap) rather than appending a clause (`LIMIT n`). Callers use this + /// to keep ORDER BY out of the derived table, where T-SQL rejects it + /// (error 1033). Default `false`; wrapper-style dialects must override. + fn sql_limit_wraps_query(&self) -> bool { + false + } + /// Cast an expression to a SQL type name. /// /// Default uses `CAST(expr AS type)`. Override for backends that prefer a @@ -228,7 +237,7 @@ pub trait SqlDialect { // duplicated column wins over the star's. return crate::sql::Select::new(self) .select_plus_star(&[format!("{expr} AS {col}")], "__ggsql_sr__") - .from_aliased(from, "__ggsql_sr__") + .from_aliased(crate::sql::FromItem::Query(from), "__ggsql_sr__") .build(); } let select_list: Vec = all_columns @@ -242,7 +251,12 @@ pub trait SqlDialect { } }) .collect(); - crate::sql::select_from(self, &select_list.join(", "), from, "__ggsql_sr__") + crate::sql::select_from( + self, + &select_list.join(", "), + crate::sql::FromItem::Query(from), + "__ggsql_sr__", + ) } /// SQL expression constructing a point geometry from x/y expressions. @@ -323,13 +337,13 @@ pub trait SqlDialect { fn sql_geometry_bbox(&self, column: &str, from: &str) -> String { let extent = crate::sql::Select::new(self) .select(format!("ST_Extent({column}) AS ext")) - .from(from) + .from(crate::sql::FromItem::Fragment(from)) .build(); crate::sql::select_from( self, "ST_XMin(ext) AS xmin, ST_YMin(ext) AS ymin, \ ST_XMax(ext) AS xmax, ST_YMax(ext) AS ymax", - &extent, + crate::sql::FromItem::Query(&extent), "__ggsql_ext__", ) } @@ -425,7 +439,13 @@ pub trait SqlDialect { /// `x = fraction * (cnt - 1)`, the result interpolates linearly between /// the rows ranked `floor(x) + 1` and `ceil(x) + 1`, which is exact for /// every fraction. - fn sql_quantile(&self, column: &str, fraction: f64, from: &str, groups: &[String]) -> String { + fn sql_quantile( + &self, + column: &str, + fraction: f64, + from: crate::sql::FromItem<'_>, + groups: &[String], + ) -> String { // The correlation predicate references the enclosing query's alias // and this scalar subquery's own alias, so both are needed quoted. let __ggsql_qt__ = self.quote_ident("__ggsql_qt__"); @@ -480,7 +500,7 @@ pub trait SqlDialect { (MAX(CASE WHEN rn = {hi} THEN __val END) - \ MAX(CASE WHEN rn = {lo} THEN __val END)) * MAX({frac})" )) - .from_aliased(&inner, "__ggsql_tile__"); + .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_tile__"); if let Some(filter) = group_filter { outer = outer.and_where(filter); } diff --git a/src/sql/mod.rs b/src/sql/mod.rs index 26059c36..c9d9d278 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -25,19 +25,41 @@ pub fn ident(dialect: &D, name: &str) -> String { dialect.quote_ident(name) } +/// What a FROM item is, decided by the caller — never guessed from text. +/// +/// The one judged case is [`FromItem::Fragment`], the contract for `from` +/// arguments passed across the [`SqlDialect`] trait: generated SQL only ever +/// passes a bare (possibly quoted) table/CTE name or an already-parenthesized +/// relation, so a leading `(` unambiguously means "already a relation". +#[derive(Debug, Clone, Copy)] +pub enum FromItem<'a> { + /// A bare table or CTE name; emitted verbatim, never parenthesized + /// (MySQL/MariaDB/T-SQL reject `FROM (name)`). + Table(&'a str), + /// A full query; parenthesized as a derived table, with a leading `WITH` + /// hoisted out when the dialect forbids CTEs in that position. + Query(&'a str), + /// An already-composed relation (e.g. [`Select::build_derived`] output); + /// emitted verbatim. + Raw(&'a str), + /// A trait-level FROM fragment: [`FromItem::Raw`] when it starts with + /// `(`, otherwise [`FromItem::Table`]. + Fragment(&'a str), +} + /// `SELECT * FROM () AS "alias"` — the canonical derived-table wrap. pub fn wrap_all(dialect: &D, query: &str, alias: &str) -> String { Select::new(dialect) .select_star() - .from_aliased(query, alias) + .from_aliased(FromItem::Query(query), alias) .build() } -/// `SELECT FROM () AS "alias"`. +/// `SELECT FROM AS "alias"`. pub fn select_from( dialect: &D, list: &str, - from: &str, + from: FromItem<'_>, alias: &str, ) -> String { Select::new(dialect) @@ -141,17 +163,16 @@ impl<'d, D: SqlDialect + ?Sized> Select<'d, D> { self } - /// FROM a table name or subquery, without an alias. Parenthesized only - /// when `from` is a query (bare table/CTE names must not be - /// parenthesized: MySQL/MariaDB/T-SQL reject `FROM (name)`). - pub fn from(mut self, from: &str) -> Self { + /// FROM a table, query, or composed relation (see [`FromItem`]), without + /// an alias. + pub fn from(mut self, from: FromItem<'_>) -> Self { self.from = self.prepare_from(from); self } /// FROM with a table alias. The alias clause is emitted by the dialect /// (`AS "alias"` on most backends, bare `"alias"` on Oracle). - pub fn from_aliased(mut self, from: &str, alias: &str) -> Self { + pub fn from_aliased(mut self, from: FromItem<'_>, alias: &str) -> Self { let fragment = self.prepare_from(from); self.from = format!("{} {}", fragment, self.dialect.sql_table_alias(alias)); self @@ -242,8 +263,24 @@ impl<'d, D: SqlDialect + ?Sized> Select<'d, D> { /// Build and apply the dialect's row-limit wrapper (`LIMIT n`, /// `SELECT TOP n * FROM (...)`, `WHERE ROWNUM <= n`, ...). + /// + /// When the dialect wraps the query in a derived table + /// ([`SqlDialect::sql_limit_wraps_query`], e.g. T-SQL's TOP), an ORDER BY + /// is moved outside the wrap — ordering inside a derived table is T-SQL + /// error 1033. Clause-style limits (`LIMIT n`) keep the ordering inside, + /// where `ORDER BY … LIMIT n` is valid. pub fn build_limited(self, n: usize) -> String { - self.dialect.sql_limit(&self.build(), n) + if self.ordering.is_some() && self.dialect.sql_limit_wraps_query() { + let ordering = self.ordering.clone().expect("checked above"); + let body = Select { + ordering: None, + ..self + } + .build(); + format!("{} ORDER BY {}", self.dialect.sql_limit(&body, n), ordering) + } else { + self.dialect.sql_limit(&self.build(), n) + } } /// Build as a parenthesized, aliased derived-table fragment: @@ -256,17 +293,19 @@ impl<'d, D: SqlDialect + ?Sized> Select<'d, D> { format!("({}) {}", self.build(), alias_clause) } - /// The FROM fragment for `from`: parenthesized when it is a query, with - /// a leading `WITH` hoisted into `self.ctes` when the dialect forbids - /// CTEs inside derived tables. - fn prepare_from(&mut self, from: &str) -> String { - let trimmed = from.trim(); - if trimmed.starts_with('(') { - return trimmed.to_string(); - } - if !trimmed.contains(char::is_whitespace) { - return trimmed.to_string(); - } + /// The FROM fragment for `from` (see [`FromItem`]): queries are + /// parenthesized, with a leading `WITH` hoisted into `self.ctes` when + /// the dialect forbids CTEs inside derived tables. + fn prepare_from(&mut self, from: FromItem<'_>) -> String { + let trimmed = match from { + FromItem::Table(t) => return t.trim().to_string(), + FromItem::Raw(r) => return r.trim().to_string(), + FromItem::Fragment(f) => { + let f = f.trim(); + return f.to_string(); + } + FromItem::Query(q) => q.trim(), + }; if !self.dialect.allows_cte_in_derived_table() { if let Some((cte, body)) = split_cte_prefix(trimmed) { self.push_hoisted_cte(cte); @@ -306,16 +345,38 @@ mod tests { #[test] fn bare_table_is_not_parenthesized() { assert_eq!( - select_from(&AnsiDialect, "a, b", "mytable", "s"), + select_from(&AnsiDialect, "a, b", FromItem::Table("mytable"), "s"), "SELECT a, b FROM mytable AS \"s\"" ); } #[test] - fn already_parenthesized_from_is_kept() { + fn raw_relation_is_emitted_verbatim() { + // A pre-composed relation keeps its own alias; the FROM alias is not + // stacked on top (that would be invalid SQL). + let rel = Select::new(&AnsiDialect) + .select("1 AS a") + .build_derived("u"); + assert_eq!( + select_from(&AnsiDialect, "a", FromItem::Raw(&rel), "s"), + "SELECT a FROM (SELECT 1 AS a) AS \"u\" AS \"s\"" + ); + } + + #[test] + fn fragment_distinguishes_relation_from_table() { + assert_eq!( + select_from(&AnsiDialect, "a", FromItem::Fragment("\"my table\""), "s"), + "SELECT a FROM \"my table\" AS \"s\"" + ); assert_eq!( - select_from(&AnsiDialect, "a", "(SELECT 1 AS a) AS u", "s"), - "SELECT a FROM (SELECT 1 AS a) AS u AS \"s\"" + select_from( + &AnsiDialect, + "a", + FromItem::Fragment("(SELECT 1 AS a) AS \"u\""), + "s" + ), + "SELECT a FROM (SELECT 1 AS a) AS \"u\" AS \"s\"" ); } @@ -360,14 +421,14 @@ mod tests { assert_eq!( Select::new(&OracleDialect) .select_star_plus(&["1 AS one".to_string()], "s") - .from_aliased("SELECT a FROM t", "s") + .from_aliased(FromItem::Query("SELECT a FROM t"), "s") .build(), "SELECT \"s\".*, 1 AS one FROM (SELECT a FROM t) \"s\"" ); assert_eq!( Select::new(&AnsiDialect) .select_star_plus(&["1 AS one".to_string()], "s") - .from_aliased("SELECT a FROM t", "s") + .from_aliased(FromItem::Query("SELECT a FROM t"), "s") .build(), "SELECT *, 1 AS one FROM (SELECT a FROM t) AS \"s\"" ); @@ -378,7 +439,7 @@ mod tests { assert_eq!( Select::new(&AnsiDialect) .select("a") - .from("t") + .from(FromItem::Table("t")) .and_where("a > 1") .and_where("b < 2") .order_by("a") @@ -408,14 +469,14 @@ mod tests { assert_eq!( Select::new(&AnsiDialect) .select("a") - .from("t") + .from(FromItem::Table("t")) .build_limited(3), "SELECT a FROM t LIMIT 3" ); assert_eq!( Select::new(&MssqlDialect) .select("a") - .from("t") + .from(FromItem::Table("t")) .build_limited(3), "SELECT TOP 3 * FROM (SELECT a FROM t) AS \"__ggsql_lim__\"" ); From 49327a1f6f48caf59f7e85620efef1f23a997a4b Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 6 Oct 2026 17:54:27 +0200 Subject: [PATCH 03/10] Route table drops through a dialect hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DROP TABLE IF EXISTS was hard-coded in the ADBC reader, ODBC reader and the temp-table probe, ignoring Oracle's lack of IF EXISTS — the probe could leave a permanent __ggsql_probe table behind on pre-23c Oracle. There is now SqlDialect::drop_table_sql with a guarded PL/SQL override on Oracle, shared with the GuardedDropThenCreate temp-table style. The probe's behavior (DDL against the user's database on every connect) is now documented. --- src/reader/adbc.rs | 4 ++-- src/reader/connection.rs | 11 +++++++---- src/reader/dialects/oracle.rs | 12 ++++++++++++ src/reader/mod.rs | 22 +++++++++++++--------- src/reader/odbc/mod.rs | 4 ++-- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/reader/adbc.rs b/src/reader/adbc.rs index 4b94cf0d..a24baa94 100644 --- a/src/reader/adbc.rs +++ b/src/reader/adbc.rs @@ -598,7 +598,7 @@ where // silently tolerated below. let schema = batch.schema(); if replace { - let drop_sql = format!("DROP TABLE IF EXISTS {}", self.dialect.quote_ident(name)); + let drop_sql = self.dialect.drop_table_sql(name); self.new_query_statement(&mut conn, &drop_sql)? .execute_update() .map_err(|e| GgsqlError::ReaderError(format!("ADBC execute_update DROP: {}", e)))?; @@ -699,7 +699,7 @@ where name ))); } - let sql = format!("DROP TABLE IF EXISTS {}", self.dialect.quote_ident(name)); + let sql = self.dialect.drop_table_sql(name); // Ignore the returned DataFrame — DROP TABLE has no result rows. self.execute_sql(&sql)?; self.registered_tables.note_unregistered(name); diff --git a/src/reader/connection.rs b/src/reader/connection.rs index 50627f0a..fa3c98b2 100644 --- a/src/reader/connection.rs +++ b/src/reader/connection.rs @@ -533,15 +533,18 @@ pub fn reader_from_uri(uri: &str) -> Result> { /// ggsql stages internal results in, using the dialect's own temp-table DDL /// (the same mechanism the executor relies on). Best effort: the probe table /// is dropped afterwards, and any failure means "cannot". +/// +/// Note this runs DDL (CREATE + DROP of a `__ggsql_probe___` table) +/// against the user's database on every connection; on backends without +/// temp tables (e.g. Oracle) that is a permanent table for the duration of +/// the probe. The drop goes through the dialect so backends without +/// `DROP TABLE IF EXISTS` (Oracle) get their guarded form. fn probe_temp_tables(reader: &dyn Reader) -> bool { let probe = format!("__ggsql_probe_{}__", crate::naming::session_id()); let dialect = reader.dialect(); let stmts = dialect.create_or_replace_temp_table_sql(&probe, &[], "SELECT 1 AS x"); let ok = stmts.iter().all(|s| reader.execute_sql(s).is_ok()); - let _ = reader.execute_sql(&format!( - "DROP TABLE IF EXISTS {}", - dialect.quote_ident(&probe) - )); + let _ = reader.execute_sql(&dialect.drop_table_sql(&probe)); ok } diff --git a/src/reader/dialects/oracle.rs b/src/reader/dialects/oracle.rs index 74c8ad18..625f2b94 100644 --- a/src/reader/dialects/oracle.rs +++ b/src/reader/dialects/oracle.rs @@ -79,6 +79,18 @@ impl SqlDialect for OracleDialect { format!("TIMESTAMP '1970-01-01 00:00:00' + NUMTODSINTERVAL({secs}, 'SECOND')") } + fn drop_table_sql(&self, name: &str) -> String { + // Oracle has no DROP TABLE IF EXISTS. The drop must spell the name + // exactly like the CREATE: an unquoted name would be folded to + // uppercase and never match the quoted (case-preserved) table, + // silently leaving stale tables behind under the WHEN OTHERS guard. + let qname = self.quote_ident(name); + format!( + "BEGIN EXECUTE IMMEDIATE 'DROP TABLE {}'; EXCEPTION WHEN OTHERS THEN NULL; END;", + qname.replace('\'', "''") + ) + } + fn temp_table_style(&self) -> crate::reader::TempTableStyle { // Oracle has no DROP TABLE IF EXISTS; guard the drop with PL/SQL. crate::reader::TempTableStyle::GuardedDropThenCreate diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 1e3d45ff..95dec3c9 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -697,6 +697,14 @@ pub trait SqlDialect { /// Build the DDL statement(s) needed to (re)create a temporary table /// that holds the result of `body_sql`. /// + /// Drop a table ggsql created (materialized internal table or registered + /// data), best effort: the statement must not fail when the table is + /// absent. Default is `DROP TABLE IF EXISTS`; Oracle overrides with a + /// PL/SQL-guarded drop (it has no `IF EXISTS`). + fn drop_table_sql(&self, name: &str) -> String { + format!("DROP TABLE IF EXISTS {}", self.quote_ident(name)) + } + /// Column aliases from `WITH t(a, b) AS (...)` are preserved portably by /// wrapping the body in a named CTE with a column alias list, so the /// backend never needs to support `CREATE TABLE t(a, b) AS ...` syntax. @@ -723,7 +731,10 @@ pub trait SqlDialect { format!("CREATE TEMP TABLE {} AS {}", qname, body), ], TempTableStyle::CreateOrReplaceTemp => { - vec![format!("CREATE OR REPLACE TEMP TABLE {} AS {}", qname, body)] + vec![format!( + "CREATE OR REPLACE TEMP TABLE {} AS {}", + qname, body + )] } TempTableStyle::DropTemporaryThenCreateTemp => vec![ format!("DROP TEMPORARY TABLE IF EXISTS {}", qname), @@ -737,14 +748,7 @@ pub trait SqlDialect { format!("CREATE TABLE {} AS {}", qname, body), ], TempTableStyle::GuardedDropThenCreate => vec![ - // The drop must spell the name exactly like the CREATE below: - // an unquoted name would be folded to uppercase by Oracle and - // never match the quoted (case-preserved) table, silently - // leaving stale tables behind under the WHEN OTHERS guard. - format!( - "BEGIN EXECUTE IMMEDIATE 'DROP TABLE {}'; EXCEPTION WHEN OTHERS THEN NULL; END;", - qname.replace('\'', "''") - ), + self.drop_table_sql(name), format!("CREATE TABLE {} AS {}", qname, body), ], TempTableStyle::SelectInto => { diff --git a/src/reader/odbc/mod.rs b/src/reader/odbc/mod.rs index c5d0f37b..5de6e3f3 100644 --- a/src/reader/odbc/mod.rs +++ b/src/reader/odbc/mod.rs @@ -155,7 +155,7 @@ impl Reader for OdbcReader { super::validate_table_name(name)?; if replace { - let drop_sql = format!("DROP TABLE IF EXISTS {}", self.dialect.quote_ident(name)); + let drop_sql = self.dialect.drop_table_sql(name); let _ = self.connection.execute(&drop_sql); } @@ -257,7 +257,7 @@ impl Reader for OdbcReader { ))); } - let sql = format!("DROP TABLE IF EXISTS {}", self.dialect.quote_ident(name)); + let sql = self.dialect.drop_table_sql(name); self.connection.execute(&sql).map_err(|e| { GgsqlError::ReaderError(format!("Failed to unregister table '{}': {}", name, e)) })?; From 2ffd820ae66721f388d40fe4b0832b211daa40a6 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 6 Oct 2026 17:57:23 +0200 Subject: [PATCH 04/10] Move driver quirks into registry capabilities Error-text and DBMS-name matches hard-coded in the generic readers are now declared per backend in the registry, next to DriverUri: - AdbcInfo gains ddl_empty_result_error (BigQuery), ddl_retry_error (Databricks) and ingest_schema_align_error (DataFusion); AdbcReader carries the entry's AdbcInfo instead of matching substrings inline. - DatabaseEntry gains odbc_row_array_size (Oracle: 1), replacing the dbms-name 'oracle' substring check for the ODBC fetch batch size. Registry detection 'All' patterns now match at word boundaries, so the 'ora' in 'Teradata Corporation ODBC Driver' no longer detects Oracle; Exasol's weak 'exa' pattern is replaced by the exaodbc driver name. --- src/reader/adbc.rs | 63 +++++++++++++++++++++++--------- src/reader/odbc/mod.rs | 17 +++++---- src/reader/registry.rs | 83 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 134 insertions(+), 29 deletions(-) diff --git a/src/reader/adbc.rs b/src/reader/adbc.rs index a24baa94..68c4aef7 100644 --- a/src/reader/adbc.rs +++ b/src/reader/adbc.rs @@ -27,6 +27,9 @@ pub struct AdbcReader { // takes &self. connection: RefCell<::ConnectionType>, dialect: Box, + /// Registry-declared driver quirks (error-text conventions), when the + /// reader was built for a registered backend. `None` for ad-hoc drivers. + adbc_info: Option, registered_tables: crate::reader::RegisteredTables, // Driver-specific statement options (from `stmt.`-prefixed URI params) // applied to every statement created in execute_sql. @@ -59,6 +62,7 @@ impl AdbcReader { _database: database, connection: RefCell::new(connection), dialect, + adbc_info: None, registered_tables: crate::reader::RegisteredTables::new(), statement_opts: Vec::new(), }) @@ -89,6 +93,7 @@ impl AdbcReader { _database: database, connection: RefCell::new(connection), dialect, + adbc_info: None, registered_tables: crate::reader::RegisteredTables::new(), statement_opts: Vec::new(), }) @@ -107,6 +112,14 @@ impl AdbcReader { self } + /// Attach the registry's driver quirks for this backend, so error-text + /// conventions (DDL-without-result-set signaling, ingest schema + /// mismatches) are honored where the driver requires them. + fn with_adbc_info(mut self, info: crate::reader::registry::AdbcInfo) -> Self { + self.adbc_info = Some(info); + self + } + /// Create a statement for `sql` with [`Self::statement_opts`] applied — /// the shared constructor behind every `set_sql_query` call site. fn new_query_statement( @@ -356,8 +369,15 @@ impl AdbcReader { let dialect = crate::reader::registry::resolve_dialect(&conn)?; - Self::new_with_database_opts(driver, dialect, opts) - .map(|reader| reader.with_statement_opts(conn.ggsql.stmt_options.clone())) + let reader = Self::new_with_database_opts(driver, dialect, opts)? + .with_statement_opts(conn.ggsql.stmt_options.clone()); + // adbc:// with a recognized short name also gets quirks. + Ok( + match adbc_info_for_scheme(if scheme == "adbc" { body } else { scheme }) { + Some(info) => reader.with_adbc_info(info), + None => reader, + }, + ) } } @@ -510,24 +530,29 @@ where Ok(reader) => reader, Err(e) => { let msg = e.to_string(); - // BigQuery DDL/DML jobs without a result set execute to - // completion but fail at read time with "job has no - // destination table to read". The statement has already - // run — report an empty frame. Retrying via - // execute_update would re-execute the statement, which - // breaks non-idempotent DDL (a second CREATE TABLE fails - // with "already exists"). - if msg.contains("no destination table to read") { + // Driver-declared quirks (registry `AdbcInfo`): some + // drivers signal "DDL without a result set" only through + // error text. + // - `ddl_empty_result_error` (BigQuery): the statement + // already ran — report an empty frame. Retrying via + // execute_update would re-execute it, which breaks + // non-idempotent DDL ("already exists"). + // - `ddl_retry_error` (Databricks): the query path cannot + // run resultless statements at all; retry via + // execute_update, the path meant for them. + let info = self.adbc_info; + if info + .and_then(|i| i.ddl_empty_result_error) + .is_some_and(|needle| msg.contains(needle)) + { return Ok(DataFrame::from_record_batch(RecordBatch::new_empty( std::sync::Arc::new(arrow::datatypes::Schema::empty()), ))); } - // The Databricks driver's query path cannot build a result - // reader for statements without a result set (DDL), failing - // with "schema bytes are empty" before executing. Retry via - // execute_update — the ADBC path meant for exactly those - // statements — and report an empty frame. - if msg.contains("schema bytes are empty") { + if info + .and_then(|i| i.ddl_retry_error) + .is_some_and(|needle| msg.contains(needle)) + { drop(stmt); let mut update_stmt = self.new_query_statement(&mut conn, sql)?; update_stmt.execute_update().map_err(|e| { @@ -663,7 +688,11 @@ where match stmt.execute_update() { Ok(_) => break, Err(e) => { - if attempts == 0 && e.to_string().contains("different schema") { + let schema_mismatch = self + .adbc_info + .and_then(|i| i.ingest_schema_align_error) + .is_some_and(|needle| e.to_string().contains(needle)); + if attempts == 0 && schema_mismatch { attempts += 1; match conn.get_table_schema(None, None, name) { Ok(target) => { diff --git a/src/reader/odbc/mod.rs b/src/reader/odbc/mod.rs index 5de6e3f3..5933744a 100644 --- a/src/reader/odbc/mod.rs +++ b/src/reader/odbc/mod.rs @@ -120,13 +120,16 @@ impl OdbcReader { None => detect_dialect(dbms_name.as_deref(), &conn_str)?, }; - // Oracle ODBC rejects block cursors (SQL_ATTR_ROW_ARRAY_SIZE > 1) - // with HY090 at SQLFetch time, and the failed fetch leaves the - // cursor unusable — fetch row-by-row instead. - let batch_size = match &dbms_name { - Some(name) if name.to_lowercase().contains("oracle") => 1, - _ => BATCH_SIZE, - }; + // Fetch batch size is a per-backend capability (registry + // `odbc_row_array_size`): Oracle ODBC rejects block cursors + // (SQL_ATTR_ROW_ARRAY_SIZE > 1) with HY090 at SQLFetch time, and the + // failed fetch leaves the cursor unusable. + let batch_size = super::registry::detect( + dbms_name.as_deref(), + super::connection::extract_odbc_value(&conn_str, "driver").as_deref(), + ) + .and_then(|e| e.odbc_row_array_size) + .unwrap_or(BATCH_SIZE); Ok(Self { connection, diff --git a/src/reader/registry.rs b/src/reader/registry.rs index 053c64fc..996df0f4 100644 --- a/src/reader/registry.rs +++ b/src/reader/registry.rs @@ -55,6 +55,33 @@ pub struct AdbcInfo { /// and individual connection options", and Druid with "Unsupported /// option: Other(\"tls\")" — their params stay in the URI only. pub params_as_options: bool, + /// Error substring meaning "a DDL/DML statement without a result set + /// ran fine but the read failed" — report an empty frame instead of an + /// error (BigQuery: "job has no destination table to read"). Matching + /// on driver error text is fragile, so it is declared here per driver + /// rather than hard-coded in the reader. + pub ddl_empty_result_error: Option<&'static str>, + /// Error substring meaning the driver's query path cannot execute + /// statements without a result set; retry those via `execute_update` + /// (Databricks: "schema bytes are empty"). + pub ddl_retry_error: Option<&'static str>, + /// Error substring on ingest meaning the target table's schema differs + /// from the batch; retry once with the batch aligned to the target + /// schema (DataFusion: "different schema"). + pub ingest_schema_align_error: Option<&'static str>, +} + +impl AdbcInfo { + /// No driver-specific error-text quirks; the `adbc!` macro spreads this. + const NO_QUIRKS: Self = Self { + lib_name: "", + dbc_id: "", + driver_uri: DriverUri::Passthrough, + params_as_options: false, + ddl_empty_result_error: None, + ddl_retry_error: None, + ingest_schema_align_error: None, + }; } /// A DBMS-name/driver-string detection pattern. @@ -69,11 +96,22 @@ impl DetectPattern { fn matches(&self, lower: &str) -> bool { match self { DetectPattern::Contains(s) => lower.contains(s), - DetectPattern::All(ss) => ss.iter().all(|s| lower.contains(s)), + // Each needle must appear at a word boundary (start of the + // string or right after a non-alphanumeric character), so + // "ora" matches "Oracle ODBC Driver" but the "ora" in + // "Teradata Corporation ODBC Driver" does not. + DetectPattern::All(ss) => ss.iter().all(|s| contains_word_prefix(lower, s)), } } } +/// `haystack` contains `needle` starting at a word boundary. +fn contains_word_prefix(haystack: &str, needle: &str) -> bool { + haystack + .match_indices(needle) + .any(|(i, _)| i == 0 || !haystack.as_bytes()[i - 1].is_ascii_alphanumeric()) +} + /// An in-process reader ggsql ships for a backend, preferred over external /// drivers when its cargo feature is compiled in. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -104,6 +142,10 @@ pub struct DatabaseEntry { /// Server/Port/Database synthesis — Oracle ODBC rejects a connection /// string that mixes the two vocabularies. pub odbc_dbq_style: bool, + /// ODBC fetch batch size override. Oracle ODBC rejects block cursors + /// (SQL_ATTR_ROW_ARRAY_SIZE > 1) with HY090 at SQLFetch time, and the + /// failed fetch leaves the cursor unusable — fetch row-by-row (1). + pub odbc_row_array_size: Option, /// In-process reader to prefer when its cargo feature is compiled in /// (duckdb, sqlite). `None` for backends reached only through external /// ADBC/ODBC drivers. @@ -132,6 +174,7 @@ macro_rules! entry { dialect: $dialect, adbc: $adbc, odbc_dbq_style: false, + odbc_row_array_size: None, native_reader: None, } }; @@ -144,6 +187,17 @@ macro_rules! adbc { dbc_id: $id, driver_uri: $uri, params_as_options: $opts, + ..AdbcInfo::NO_QUIRKS + }) + }; + ($lib:literal, $id:literal, $uri:expr, $opts:expr, $($k:ident: $v:expr),+ $(,)?) => { + Some(AdbcInfo { + lib_name: $lib, + dbc_id: $id, + driver_uri: $uri, + params_as_options: $opts, + $($k: $v),+, + ..AdbcInfo::NO_QUIRKS }) }; } @@ -213,7 +267,13 @@ pub static REGISTRY: &[DatabaseEntry] = &[ "BigQuery", &[Has("bigquery")], || Box::new(BigQueryDialect), - adbc!("adbc_driver_bigquery", "bigquery", BigQuerySimba, true) + adbc!( + "adbc_driver_bigquery", + "bigquery", + BigQuerySimba, + true, + ddl_empty_result_error: Some("no destination table to read") + ) ), entry!( "databricks", @@ -221,7 +281,13 @@ pub static REGISTRY: &[DatabaseEntry] = &[ "Databricks", &[Has("databricks"), Has("spark")], || Box::new(DatabricksDialect), - adbc!("adbc_driver_databricks", "databricks", Passthrough, false) + adbc!( + "adbc_driver_databricks", + "databricks", + Passthrough, + false, + ddl_retry_error: Some("schema bytes are empty") + ) ), entry!( "clickhouse", @@ -233,6 +299,7 @@ pub static REGISTRY: &[DatabaseEntry] = &[ ), DatabaseEntry { odbc_dbq_style: true, + odbc_row_array_size: Some(1), ..entry!( "oracle", &[], @@ -254,7 +321,7 @@ pub static REGISTRY: &[DatabaseEntry] = &[ "exasol", &[], "Exasol", - &[Has("exasol"), DetectPattern::All(&["exa", "odbc"])], + &[Has("exasol"), Has("exaodbc")], || Box::new(ExasolDialect), adbc!("adbc_driver_exasol", "exasol", Passthrough, true) ), @@ -288,7 +355,13 @@ pub static REGISTRY: &[DatabaseEntry] = &[ "DataFusion", &[Has("datafusion")], || Box::new(DataFusionDialect), - adbc!("adbc_driver_datafusion", "datafusion", Passthrough, true) + adbc!( + "adbc_driver_datafusion", + "datafusion", + Passthrough, + true, + ingest_schema_align_error: Some("different schema") + ) ), DatabaseEntry { native_reader: Some(NativeReader::DuckDb), From 0aec82606db8767c8f265b25aa3ab9ba5fe1141f Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 6 Oct 2026 17:58:21 +0200 Subject: [PATCH 05/10] Document the trait's quoting convention; consistent series-CTE naming The SqlDialect docs now state the raw-vs-quoted identifier convention and the FromItem::Fragment contract for 'from' arguments. BigQuery, Databricks and ClickHouse build their series CTE name through quote_ident like every other dialect instead of hard-coding backticks. --- src/reader/dialects/bigquery.rs | 10 ++++++++-- src/reader/dialects/clickhouse.rs | 10 ++++++++-- src/reader/dialects/databricks.rs | 3 ++- src/reader/mod.rs | 11 +++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/reader/dialects/bigquery.rs b/src/reader/dialects/bigquery.rs index 6b0d7f68..518470f2 100644 --- a/src/reader/dialects/bigquery.rs +++ b/src/reader/dialects/bigquery.rs @@ -35,8 +35,9 @@ impl SqlDialect for BigQueryDialect { } fn sql_generate_series(&self, n: usize) -> String { + let seq = self.quote_ident("__ggsql_seq__"); format!( - "`__ggsql_seq__`(n) AS (\ + "{seq}(n) AS (\ SELECT CAST(g AS FLOAT64) AS n \ FROM UNNEST(GENERATE_ARRAY(0, {n} - 1)) AS g\ )" @@ -115,7 +116,12 @@ mod tests { // sql_quantile must not emit a correlated subquery — BigQuery // rejects those. It uses the APPROX_QUANTILES aggregate, valid // inside the GROUP BY queries that boxplot and density build. - let sql = BigQueryDialect.sql_quantile("v", 0.75, crate::sql::FromItem::Query("SELECT * FROM t"), &["g".to_string()]); + let sql = BigQueryDialect.sql_quantile( + "v", + 0.75, + crate::sql::FromItem::Query("SELECT * FROM t"), + &["g".to_string()], + ); assert_eq!(sql, "APPROX_QUANTILES(`v`, 100)[SAFE_OFFSET(75)]"); assert!(!sql.contains("SELECT"), "must not be a subquery: {sql}"); } diff --git a/src/reader/dialects/clickhouse.rs b/src/reader/dialects/clickhouse.rs index 2349496a..c1f7e540 100644 --- a/src/reader/dialects/clickhouse.rs +++ b/src/reader/dialects/clickhouse.rs @@ -79,8 +79,9 @@ impl SqlDialect for ClickHouseDialect { } fn sql_generate_series(&self, n: usize) -> String { + let seq = self.quote_ident("__ggsql_seq__"); format!( - "`__ggsql_seq__`(n) AS (\ + "{seq}(n) AS (\ SELECT toFloat64(number) AS n FROM numbers({n})\ )" ) @@ -188,7 +189,12 @@ mod tests { "quantileExactInclusive(0.9)(`v`)" ); assert_eq!( - ClickHouseDialect.sql_quantile("v", 0.5, crate::sql::FromItem::Query("SELECT * FROM t"), &[]), + ClickHouseDialect.sql_quantile( + "v", + 0.5, + crate::sql::FromItem::Query("SELECT * FROM t"), + &[] + ), "quantileExactInclusive(0.5)(`v`)" ); } diff --git a/src/reader/dialects/databricks.rs b/src/reader/dialects/databricks.rs index 1e0c4574..ab4a4d7e 100644 --- a/src/reader/dialects/databricks.rs +++ b/src/reader/dialects/databricks.rs @@ -34,8 +34,9 @@ impl SqlDialect for DatabricksDialect { // ("generator is not supported: nested in expressions"), so the // explode must stand alone in an inner SELECT and the CAST moves // outside it. + let seq = self.quote_ident("__ggsql_seq__"); format!( - "`__ggsql_seq__`(n) AS (\ + "{seq}(n) AS (\ SELECT CAST(n AS DOUBLE) AS n FROM (\ SELECT explode(sequence(0, {n} - 1)) AS n\ )\ diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 95dec3c9..7e484434 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -45,6 +45,17 @@ use crate::{naming, DataFrame, GgsqlError, Result}; /// SQL type names and functionality in the syntax supported by that backend. /// /// Default implementations produce portable ANSI SQL. +/// +/// # Quoting convention +/// +/// Identifier parameters are **raw** (unquoted) unless the parameter name +/// says otherwise: parameters called `qcol`/`qname` or documented as +/// "already-quoted" arrive quoted by the caller. Methods receiving a raw +/// identifier must quote it with [`SqlDialect::quote_ident`] before +/// interpolating. SQL *fragments* (expressions, `from` arguments) arrive +/// fully composed; `from` arguments follow the +/// [`crate::sql::FromItem::Fragment`] contract — a bare (possibly quoted) +/// table/CTE name or an already-parenthesized relation, never a raw query. pub trait SqlDialect { /// SQL type names for table creation and casts. Dialects override this /// one method with a [`TypeNames`] literal rather than the individual From b7ce35b821d32ed082b5feba50edd565bf5e5104 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 6 Oct 2026 18:00:59 +0200 Subject: [PATCH 06/10] Module placement cleanup - The WITH-clause scanner (split_cte_prefix and helpers) moves from dialects/mod.rs to sql/mod.rs, its only consumer. - AnsiDialect moves into dialects/ with the other dialect impls (re-exported from reader:: for compatibility). - Dead code removed: sql::ident had no callers. - Feature gates corrected: create_table_sql and sniff_temporal_strings_in_batch are ADBC-only; the former was gated any(adbc, odbc) and the latter ungated. --- src/reader/dialects/mod.rs | 148 ++----------------------------------- src/reader/mod.rs | 6 +- src/sql/mod.rs | 142 +++++++++++++++++++++++++++++++++-- 3 files changed, 148 insertions(+), 148 deletions(-) diff --git a/src/reader/dialects/mod.rs b/src/reader/dialects/mod.rs index 6c0746bd..f90aaa14 100644 --- a/src/reader/dialects/mod.rs +++ b/src/reader/dialects/mod.rs @@ -40,9 +40,16 @@ pub use snowflake::SnowflakeDialect; pub use sqlite::SqliteDialect; pub use trino::TrinoDialect; -#[cfg(test)] use crate::reader::SqlDialect; +/// Generic ANSI SQL dialect: the fallback for backends ggsql doesn't +/// recognise, selected explicitly with `dialect=ansi`. Every trait method +/// keeps its portable default. +#[derive(Debug, Default, Clone, Copy)] +pub struct AnsiDialect; + +impl SqlDialect for AnsiDialect {} + /// CASE-based scalar greatest/least for backends without `GREATEST`/`LEAST` /// (SQL Server, SQLite, Druid, Drill, MonetDB). Builds a left-folded chain of /// two-way comparisons. @@ -71,115 +78,6 @@ pub(crate) fn case_least(exprs: &[&str]) -> String { result } -/// Split a query into its leading `WITH` clause and the remaining main -/// query. Returns `None` when the query does not start with `WITH`. -/// -/// Used by dialects that forbid CTEs inside derived tables (SQL Server) to -/// hoist the CTE definitions out of a subquery wrap. The scanner is aware -/// of string literals and quoted identifiers, and handles optional CTE -/// column lists (`cte(c1, c2) AS (...)`) and `WITH RECURSIVE`. -pub fn split_cte_prefix(query: &str) -> Option<(&str, &str)> { - let s = query.trim_start(); - let bytes = s.as_bytes(); - if bytes.len() < 5 || !s[..4].eq_ignore_ascii_case("with") || !bytes[4].is_ascii_whitespace() { - return None; - } - let mut i = 4; - skip_ws(bytes, &mut i); - if s.len() - i >= 9 && s[i..i + 9].eq_ignore_ascii_case("recursive") { - i += 9; - } - loop { - skip_ws(bytes, &mut i); - if i >= bytes.len() { - return None; - } - let name_start = i; - match bytes[i] { - q @ (b'"' | b'`') => skip_quoted(bytes, &mut i, q), - _ => { - while i < bytes.len() - && (bytes[i].is_ascii_alphanumeric() || matches!(bytes[i], b'_' | b'$')) - { - i += 1; - } - } - } - if i == name_start { - return None; - } - skip_ws(bytes, &mut i); - if i < bytes.len() && bytes[i] == b'(' { - skip_balanced_parens(bytes, &mut i)?; - skip_ws(bytes, &mut i); - } - if s.len() - i < 2 || !s[i..i + 2].eq_ignore_ascii_case("as") { - return None; - } - i += 2; - skip_ws(bytes, &mut i); - if i >= bytes.len() || bytes[i] != b'(' { - return None; - } - skip_balanced_parens(bytes, &mut i)?; - let cte_end = i; - let mut j = i; - skip_ws(bytes, &mut j); - if j < bytes.len() && bytes[j] == b',' { - i = j + 1; - continue; - } - return Some((&s[..cte_end], s[j..].trim_start())); - } -} - -fn skip_ws(bytes: &[u8], i: &mut usize) { - while *i < bytes.len() && bytes[*i].is_ascii_whitespace() { - *i += 1; - } -} - -/// Skip past a quoted region; `*i` is at the opening quote. A doubled quote -/// is treated as an escape (SQL string/identifier convention). -fn skip_quoted(bytes: &[u8], i: &mut usize, quote: u8) { - *i += 1; - while *i < bytes.len() { - if bytes[*i] == quote { - if *i + 1 < bytes.len() && bytes[*i + 1] == quote { - *i += 2; - continue; - } - *i += 1; - return; - } - *i += 1; - } -} - -/// Skip a balanced parenthesised region; `*i` is at the opening `(`. -/// Returns `None` when the parens never balance. -fn skip_balanced_parens(bytes: &[u8], i: &mut usize) -> Option<()> { - let mut depth = 0usize; - while *i < bytes.len() { - match bytes[*i] { - b'(' => { - depth += 1; - *i += 1; - } - b')' => { - depth -= 1; - *i += 1; - if depth == 0 { - return Some(()); - } - } - q @ (b'\'' | b'"' | b'`') => skip_quoted(bytes, i, q), - _ => *i += 1, - } - } - None -} - #[cfg(test)] mod tests { use super::*; @@ -272,36 +170,6 @@ mod tests { assert!(dialect_for_scheme("nosuchdb").is_none()); } - #[test] - fn splits_cte_prefix() { - let (cte, body) = split_cte_prefix( - "WITH a AS (SELECT 1 AS x), b(n) AS (SELECT 2) SELECT * FROM a JOIN b ON a.x = b.n", - ) - .unwrap(); - assert_eq!(cte, "WITH a AS (SELECT 1 AS x), b(n) AS (SELECT 2)"); - assert_eq!(body, "SELECT * FROM a JOIN b ON a.x = b.n"); - } - - #[test] - fn splits_cte_with_parens_and_strings() { - let (cte, body) = split_cte_prefix( - "WITH RECURSIVE \"__ggsql_t__\" AS (SELECT '(' AS s, f(1, (2)) AS v) SELECT v FROM \"__ggsql_t__\"", - ) - .unwrap(); - assert_eq!( - cte, - "WITH RECURSIVE \"__ggsql_t__\" AS (SELECT '(' AS s, f(1, (2)) AS v)" - ); - assert_eq!(body, "SELECT v FROM \"__ggsql_t__\""); - } - - #[test] - fn no_cte_returns_none() { - assert!(split_cte_prefix("SELECT 1").is_none()); - assert!(split_cte_prefix("WITHHELD AS x").is_none()); - assert!(split_cte_prefix("WITH a AS (SELECT 1").is_none()); - } - #[test] fn per_dialect_quoting() { assert_eq!(dialect_for_scheme("mysql").unwrap().quote_ident("c"), "`c`"); diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 7e484434..470d1bd0 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -931,8 +931,7 @@ pub fn default_sql_aggregate( Some(s) } -pub struct AnsiDialect; -impl SqlDialect for AnsiDialect {} +pub use dialects::AnsiDialect; /// Fail fast when a spatial feature is used on a backend whose dialect /// reports `supports_spatial() == false`, rather than emitting spatial SQL @@ -1076,7 +1075,7 @@ pub(crate) fn register_column_type( /// Build a `CREATE TABLE (col TYPE, …)` statement from an Arrow /// schema, with column types from [`register_column_type`]. -#[cfg(any(feature = "adbc", feature = "odbc"))] +#[cfg(feature = "adbc")] pub(crate) fn create_table_sql( name: &str, schema: &arrow::datatypes::Schema, @@ -1169,6 +1168,7 @@ pub(crate) fn normalize_result_batch( /// datetimes (`YYYY-MM-DD` + `T`/space + time) it becomes Timestamp(µs). /// Anything else is left untouched. Columns with no non-null values are /// left alone — there is nothing to infer from. +#[cfg(feature = "adbc")] pub(crate) fn sniff_temporal_strings_in_batch( batch: arrow::record_batch::RecordBatch, ) -> Result { diff --git a/src/sql/mod.rs b/src/sql/mod.rs index c9d9d278..bc2acf4e 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -16,13 +16,115 @@ //! //! Dialects supply only those primitives; the composition logic is shared. -use crate::reader::dialects::split_cte_prefix; use crate::reader::SqlDialect; -/// Quote an identifier for this dialect. Thin wrapper over -/// [`SqlDialect::quote_ident`] for call-site uniformity with the builders. -pub fn ident(dialect: &D, name: &str) -> String { - dialect.quote_ident(name) +/// Split a query into its leading `WITH` clause and the remaining main +/// query. Returns `None` when the query does not start with `WITH`. +/// +/// Used by dialects that forbid CTEs inside derived tables (SQL Server) to +/// hoist the CTE definitions out of a subquery wrap. The scanner is aware +/// of string literals and quoted identifiers, and handles optional CTE +/// column lists (`cte(c1, c2) AS (...)`) and `WITH RECURSIVE`. +pub fn split_cte_prefix(query: &str) -> Option<(&str, &str)> { + let s = query.trim_start(); + let bytes = s.as_bytes(); + if bytes.len() < 5 || !s[..4].eq_ignore_ascii_case("with") || !bytes[4].is_ascii_whitespace() { + return None; + } + let mut i = 4; + skip_ws(bytes, &mut i); + if s.len() - i >= 9 && s[i..i + 9].eq_ignore_ascii_case("recursive") { + i += 9; + } + loop { + skip_ws(bytes, &mut i); + if i >= bytes.len() { + return None; + } + let name_start = i; + match bytes[i] { + q @ (b'"' | b'`') => skip_quoted(bytes, &mut i, q), + _ => { + while i < bytes.len() + && (bytes[i].is_ascii_alphanumeric() || matches!(bytes[i], b'_' | b'$')) + { + i += 1; + } + } + } + if i == name_start { + return None; + } + skip_ws(bytes, &mut i); + if i < bytes.len() && bytes[i] == b'(' { + skip_balanced_parens(bytes, &mut i)?; + skip_ws(bytes, &mut i); + } + if s.len() - i < 2 || !s[i..i + 2].eq_ignore_ascii_case("as") { + return None; + } + i += 2; + skip_ws(bytes, &mut i); + if i >= bytes.len() || bytes[i] != b'(' { + return None; + } + skip_balanced_parens(bytes, &mut i)?; + let cte_end = i; + let mut j = i; + skip_ws(bytes, &mut j); + if j < bytes.len() && bytes[j] == b',' { + i = j + 1; + continue; + } + return Some((&s[..cte_end], s[j..].trim_start())); + } +} + +fn skip_ws(bytes: &[u8], i: &mut usize) { + while *i < bytes.len() && bytes[*i].is_ascii_whitespace() { + *i += 1; + } +} + +/// Skip past a quoted region; `*i` is at the opening quote. A doubled quote +/// is treated as an escape (SQL string/identifier convention). +fn skip_quoted(bytes: &[u8], i: &mut usize, quote: u8) { + *i += 1; + while *i < bytes.len() { + if bytes[*i] == quote { + if *i + 1 < bytes.len() && bytes[*i + 1] == quote { + *i += 2; + continue; + } + *i += 1; + return; + } + *i += 1; + } +} + +/// Skip a balanced parenthesised region; `*i` is at the opening `(`. +/// Returns `None` when the parens never balance. +fn skip_balanced_parens(bytes: &[u8], i: &mut usize) -> Option<()> { + let mut depth = 0usize; + while *i < bytes.len() { + match bytes[*i] { + b'(' => { + depth += 1; + *i += 1; + } + b')' => { + depth -= 1; + *i += 1; + if depth == 0 { + return Some(()); + } + } + q @ (b'\'' | b'"' | b'`') => skip_quoted(bytes, i, q), + _ => *i += 1, + } + } + None } /// What a FROM item is, decided by the caller — never guessed from text. @@ -334,6 +436,36 @@ mod tests { use crate::reader::dialects::oracle::OracleDialect; use crate::reader::AnsiDialect; + #[test] + fn splits_cte_prefix() { + let (cte, body) = split_cte_prefix( + "WITH a AS (SELECT 1 AS x), b(n) AS (SELECT 2) SELECT * FROM a JOIN b ON a.x = b.n", + ) + .unwrap(); + assert_eq!(cte, "WITH a AS (SELECT 1 AS x), b(n) AS (SELECT 2)"); + assert_eq!(body, "SELECT * FROM a JOIN b ON a.x = b.n"); + } + + #[test] + fn splits_cte_with_parens_and_strings() { + let (cte, body) = split_cte_prefix( + "WITH RECURSIVE \"__ggsql_t__\" AS (SELECT '(' AS s, f(1, (2)) AS v) SELECT v FROM \"__ggsql_t__\"", + ) + .unwrap(); + assert_eq!( + cte, + "WITH RECURSIVE \"__ggsql_t__\" AS (SELECT '(' AS s, f(1, (2)) AS v)" + ); + assert_eq!(body, "SELECT v FROM \"__ggsql_t__\""); + } + + #[test] + fn no_cte_returns_none() { + assert!(split_cte_prefix("SELECT 1").is_none()); + assert!(split_cte_prefix("WITHHELD AS x").is_none()); + assert!(split_cte_prefix("WITH a AS (SELECT 1").is_none()); + } + #[test] fn basic_wrap() { assert_eq!( From 9368cfeb6d2274557da46e40335f370d55b86684 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Tue, 6 Oct 2026 18:01:51 +0200 Subject: [PATCH 07/10] Express MonetDB's temp-table creation as a TempTableStyle MonetDB overrode create_or_replace_temp_table_sql against the trait doc's guidance; its ON COMMIT PRESERVE ROWS CTAS is now a proper TempTableStyle variant (DropThenCreateTempPreserveRows) handled by the shared default implementation. --- src/reader/dialects/monetdb.rs | 25 ++----------------------- src/reader/mod.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/reader/dialects/monetdb.rs b/src/reader/dialects/monetdb.rs index 01c181f8..df7efe99 100644 --- a/src/reader/dialects/monetdb.rs +++ b/src/reader/dialects/monetdb.rs @@ -51,29 +51,8 @@ impl SqlDialect for MonetDbDialect { )] } - fn create_or_replace_temp_table_sql( - &self, - name: &str, - column_aliases: &[String], - body_sql: &str, - ) -> Vec { - // MonetDB temp tables default to ON COMMIT DELETE ROWS: with ODBC - // autocommit the CTAS statement's own commit wipes the rows it just - // staged, leaving every materialized table empty. The ON COMMIT - // clause goes at the end of the CTAS form. - let body = crate::reader::wrap_with_column_aliases( - &|c: &str| self.quote_ident(c), - body_sql, - column_aliases, - ); - vec![ - format!("DROP TABLE IF EXISTS {}", self.quote_ident(name)), - format!( - "CREATE TEMP TABLE {} AS {} ON COMMIT PRESERVE ROWS", - self.quote_ident(name), - body - ), - ] + fn temp_table_style(&self) -> crate::reader::TempTableStyle { + crate::reader::TempTableStyle::DropThenCreateTempPreserveRows } fn sql_temporal_as_number( diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 470d1bd0..f4fb21f3 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -751,6 +751,13 @@ pub trait SqlDialect { format!("DROP TEMPORARY TABLE IF EXISTS {}", qname), format!("CREATE TEMPORARY TABLE {} AS {}", qname, body), ], + TempTableStyle::DropThenCreateTempPreserveRows => vec![ + format!("DROP TABLE IF EXISTS {}", qname), + format!( + "CREATE TEMP TABLE {} AS {} ON COMMIT PRESERVE ROWS", + qname, body + ), + ], TempTableStyle::CreateOrReplaceTempView => { vec![format!("CREATE OR REPLACE TEMP VIEW {} AS {}", qname, body)] } @@ -822,6 +829,11 @@ pub enum TempTableStyle { /// `DROP TEMPORARY TABLE IF EXISTS` then `CREATE TEMPORARY TABLE AS` /// (MySQL/MariaDB, ClickHouse). DropTemporaryThenCreateTemp, + /// `DROP TABLE IF EXISTS` then `CREATE TEMP TABLE AS … ON COMMIT + /// PRESERVE ROWS` (MonetDB temp tables default to ON COMMIT DELETE + /// ROWS: with ODBC autocommit the CTAS statement's own commit would + /// wipe the rows it just staged). + DropThenCreateTempPreserveRows, /// Single `CREATE OR REPLACE TEMP VIEW AS` (Databricks/Spark). CreateOrReplaceTempView, /// `DROP TABLE IF EXISTS` then plain `CREATE TABLE AS` — no temp-table From 89bee05e5463422114ddcf34232667467803c21b Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Thu, 8 Oct 2026 14:43:05 +0200 Subject: [PATCH 08/10] Re-bless Oracle golden after rebase onto issue-341-readers --- src/golden/oracle.sql | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/golden/oracle.sql b/src/golden/oracle.sql index 671ed035..652fa0f2 100644 --- a/src/golden/oracle.sql +++ b/src/golden/oracle.sql @@ -17,12 +17,12 @@ SELECT "__ggsql_stat__".*, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql -- case: line_aggregate SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: density_grouped SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos1" AS "__ggsql_aes_pos1__", "__ggsql_stat_density" AS "__ggsql_aes_pos2__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos1__"*"__ggsql_aes_pos1__") - AVG("__ggsql_aes_pos1__")*AVG("__ggsql_aes_pos1__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_fill__", "__ggsql_aes_stroke__", MIN("__ggsql_aes_pos1__") AS x_min, MAX("__ggsql_aes_pos1__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_qt__" WHERE "__ggsql_aes_pos1__" IS NOT NULL GROUP BY "__ggsql_aes_fill__", "__ggsql_aes_stroke__" ), data AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_aes_pos1__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_data__" WHERE "__ggsql_aes_pos1__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), grid AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + (seq.n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range AS "__ggsql_global__" CROSS JOIN "__ggsql_seq__" AS seq CROSS JOIN (SELECT DISTINCT "__ggsql_aes_fill__", "__ggsql_aes_stroke__" FROM bandwidth) AS "__ggsql_groups__" ) SELECT "__ggsql_stat_pos1", "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos1", grid."__ggsql_aes_fill__" AS "__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" AS "__ggsql_aes_stroke__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_fill__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_fill__") AND (data."__ggsql_aes_stroke__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_stroke__") INNER JOIN grid ON (grid."__ggsql_aes_fill__" IS NOT DISTINCT FROM data."__ggsql_aes_fill__") AND (grid."__ggsql_aes_stroke__" IS NOT DISTINCT FROM data."__ggsql_aes_stroke__") GROUP BY grid.x, grid."__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" ) AS "__ggsql_kde__") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos1" AS "__ggsql_aes_pos1__", "__ggsql_stat_density" AS "__ggsql_aes_pos2__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos1__"*"__ggsql_aes_pos1__") - AVG("__ggsql_aes_pos1__")*AVG("__ggsql_aes_pos1__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos1__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_fill__", "__ggsql_aes_stroke__", MIN("__ggsql_aes_pos1__") AS x_min, MAX("__ggsql_aes_pos1__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_qt__" WHERE "__ggsql_aes_pos1__" IS NOT NULL GROUP BY "__ggsql_aes_fill__", "__ggsql_aes_stroke__" ), data AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_aes_pos1__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_fill__", "val" AS "__ggsql_aes_pos1__", "grp" AS "__ggsql_aes_stroke__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_data__" WHERE "__ggsql_aes_pos1__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), grid AS ( SELECT "__ggsql_aes_fill__", "__ggsql_aes_stroke__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + ("seq".n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range "__ggsql_global__" CROSS JOIN "__ggsql_seq__" "seq" CROSS JOIN (SELECT DISTINCT "__ggsql_aes_fill__", "__ggsql_aes_stroke__" FROM bandwidth) "__ggsql_groups__" ) SELECT "__ggsql_stat_pos1", "__ggsql_aes_fill__", "__ggsql_aes_stroke__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos1", grid."__ggsql_aes_fill__" AS "__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" AS "__ggsql_aes_stroke__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_fill__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_fill__") AND (data."__ggsql_aes_stroke__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_stroke__") INNER JOIN grid ON (grid."__ggsql_aes_fill__" IS NOT DISTINCT FROM data."__ggsql_aes_fill__") AND (grid."__ggsql_aes_stroke__" IS NOT DISTINCT FROM data."__ggsql_aes_stroke__") GROUP BY grid.x, grid."__ggsql_aes_fill__", grid."__ggsql_aes_stroke__" ) "__ggsql_kde__") "__ggsql_stat__"; -- case: line_temporal BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; @@ -71,7 +71,7 @@ SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELE -- case: violin SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__", "__ggsql_stat_density" AS "__ggsql_aes_offset__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos2__"*"__ggsql_aes_pos2__") - AVG("__ggsql_aes_pos2__")*AVG("__ggsql_aes_pos2__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_pos1__", MIN("__ggsql_aes_pos2__") AS x_min, MAX("__ggsql_aes_pos2__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__" ), data AS ( SELECT "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") AS "__ggsql_data__" WHERE "__ggsql_aes_pos2__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), full_grid AS ( SELECT "__ggsql_aes_pos1__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + (seq.n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range AS "__ggsql_global__" CROSS JOIN "__ggsql_seq__" AS seq CROSS JOIN (SELECT DISTINCT "__ggsql_aes_pos1__" FROM bandwidth) AS "__ggsql_groups__" ), grid AS ( SELECT full_grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", full_grid.x FROM full_grid INNER JOIN bandwidth ON (full_grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") WHERE full_grid.x >= bandwidth.x_min - 3 * bandwidth.bw AND full_grid.x <= bandwidth.x_max + 3 * bandwidth.bw ) SELECT "__ggsql_stat_pos2", "__ggsql_aes_pos1__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos2", grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") INNER JOIN grid ON (grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM data."__ggsql_aes_pos1__") GROUP BY grid.x, grid."__ggsql_aes_pos1__" ) AS "__ggsql_kde__") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__", "__ggsql_stat_density" AS "__ggsql_aes_offset__" FROM (WITH bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG("__ggsql_aes_pos2__"*"__ggsql_aes_pos2__") - AVG("__ggsql_aes_pos2__")*AVG("__ggsql_aes_pos2__")), (PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") - PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__")) / 1.34) * POWER(1.0 * COUNT(*), -0.2) AS bw, "__ggsql_aes_pos1__", MIN("__ggsql_aes_pos2__") AS x_min, MAX("__ggsql_aes_pos2__") AS x_max FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__" ), data AS ( SELECT "__ggsql_aes_pos1__", "__ggsql_aes_pos2__" AS val, 1.0 AS weight FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") "__ggsql_data__" WHERE "__ggsql_aes_pos2__" IS NOT NULL ), "__ggsql_seq__"(n) AS (SELECT CAST(LEVEL - 1 AS BINARY_DOUBLE) AS n FROM DUAL CONNECT BY LEVEL <= 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), full_grid AS ( SELECT "__ggsql_aes_pos1__", ("__ggsql_global__".min - "__ggsql_global__".expansion) + ("seq".n * (("__ggsql_global__".max - "__ggsql_global__".min) + 2 * "__ggsql_global__".expansion) / 511) AS x FROM global_range "__ggsql_global__" CROSS JOIN "__ggsql_seq__" "seq" CROSS JOIN (SELECT DISTINCT "__ggsql_aes_pos1__" FROM bandwidth) "__ggsql_groups__" ), grid AS ( SELECT full_grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", full_grid.x FROM full_grid INNER JOIN bandwidth ON (full_grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") WHERE full_grid.x >= bandwidth.x_min - 3 * bandwidth.bw AND full_grid.x <= bandwidth.x_max + 3 * bandwidth.bw ) SELECT "__ggsql_stat_pos2", "__ggsql_aes_pos1__", "__ggsql_stat_intensity", "__ggsql_stat_intensity" / "__norm" AS "__ggsql_stat_density" FROM ( SELECT grid.x AS "__ggsql_stat_pos2", grid."__ggsql_aes_pos1__" AS "__ggsql_aes_pos1__", SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS "__ggsql_stat_intensity", SUM(data.weight) AS "__norm" FROM data INNER JOIN bandwidth ON (data."__ggsql_aes_pos1__" IS NOT DISTINCT FROM bandwidth."__ggsql_aes_pos1__") INNER JOIN grid ON (grid."__ggsql_aes_pos1__" IS NOT DISTINCT FROM data."__ggsql_aes_pos1__") GROUP BY grid.x, grid."__ggsql_aes_pos1__" ) "__ggsql_kde__") "__ggsql_stat__"; -- case: filter_layer BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; @@ -93,17 +93,17 @@ SELECT "id", "val", "grp", "day", "mixed Case", "__ggsql_lim__", (CASE WHEN "__g -- case: sum_aggregate SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", SUM("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", SUM("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: percentile_p10 SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.1) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.1) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: percentile_p90 SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__" WHERE grp = 'a') SELECT "__ggsql_aes_pos1__", PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY "__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_stat__"; -- case: quoted_column SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; @@ -113,7 +113,7 @@ SELECT "__ggsql_stat__".*, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql -- case: partition_by_quoted SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "mixed Case", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "mixed Case") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "mixed Case" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "mixed Case", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "mixed Case") "__ggsql_stat__"; -- case: grouped_bar SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; From 95f8e6483b30a03553b8a006e5c990a71a83d5c0 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Thu, 8 Oct 2026 15:07:19 +0200 Subject: [PATCH 09/10] Fix fmt and ggsql-jupyter sql_quantile call sites --- ggsql-jupyter/src/data_explorer.rs | 7 +++-- src/execute/layer.rs | 12 ++++++-- src/plot/layer/geom/boxplot.rs | 42 +++++++++++++++++++++++---- src/plot/layer/geom/density.rs | 14 +++++++-- src/plot/layer/geom/rule.rs | 10 +++++-- src/plot/layer/geom/segment.rs | 3 +- src/plot/layer/geom/stat_aggregate.rs | 21 ++++++++++++-- src/plot/layer/geom/tile.rs | 16 +++++++--- src/plot/projection/coord/map.rs | 5 +++- 9 files changed, 107 insertions(+), 23 deletions(-) diff --git a/ggsql-jupyter/src/data_explorer.rs b/ggsql-jupyter/src/data_explorer.rs index 48f144bf..f51dc2f2 100644 --- a/ggsql-jupyter/src/data_explorer.rs +++ b/ggsql-jupyter/src/data_explorer.rs @@ -4,6 +4,7 @@ //! paginated data access. use ggsql::reader::Reader; +use ggsql::sql::FromItem; use serde_json::{json, Value}; /// Result of handling an RPC call. @@ -539,7 +540,8 @@ impl DataExplorerState { // a bare aggregate, which without a FROM would compute // over an empty input and yield NULL). let from_query = format!("SELECT * FROM {}", self.table_path); - let median_expr = dialect.sql_quantile(&col.name, 0.5, &from_query, &[]); + let median_expr = + dialect.sql_quantile(&col.name, 0.5, FromItem::Query(&from_query), &[]); let median_sql = format!( "SELECT {} AS \"median_val\" FROM ({}) AS \"__ggsql_qt__\"", median_expr, from_query @@ -776,7 +778,8 @@ impl DataExplorerState { let from_query = format!("SELECT * FROM {}", self.table_path); for q in &quantiles_param { if let Some(q_val) = q.as_f64() { - let expr = dialect.sql_quantile(&col.name, q_val, &from_query, &[]); + let expr = + dialect.sql_quantile(&col.name, q_val, FromItem::Query(&from_query), &[]); // See the median path above: the expression needs the FROM. let q_sql = format!( "SELECT {} AS \"q_val\" FROM ({}) AS \"__ggsql_qt__\"", diff --git a/src/execute/layer.rs b/src/execute/layer.rs index bd382b32..7e612df7 100644 --- a/src/execute/layer.rs +++ b/src/execute/layer.rs @@ -379,7 +379,12 @@ pub fn apply_pre_stat_transform( }) .collect(); - crate::sql::select_from(dialect, &select_exprs.join(", "), crate::sql::FromItem::Query(query), "__ggsql_pre__") + crate::sql::select_from( + dialect, + &select_exprs.join(", "), + crate::sql::FromItem::Query(query), + "__ggsql_pre__", + ) } /// Part 1: Build the initial layer query with SELECT, casts, filters, and aesthetic renames. @@ -696,7 +701,10 @@ where } else { crate::sql::Select::new(dialect) .select_star_plus(&stat_rename_exprs, "__ggsql_stat__") - .from_aliased(crate::sql::FromItem::Query(&transformed_query), "__ggsql_stat__") + .from_aliased( + crate::sql::FromItem::Query(&transformed_query), + "__ggsql_stat__", + ) .build() } } diff --git a/src/plot/layer/geom/boxplot.rs b/src/plot/layer/geom/boxplot.rs index 82ef779f..cd0545ba 100644 --- a/src/plot/layer/geom/boxplot.rs +++ b/src/plot/layer/geom/boxplot.rs @@ -432,9 +432,24 @@ mod tests { &AnsiDialect, ); - let q1 = AnsiDialect.sql_quantile("price", 0.25, crate::sql::FromItem::Query("SELECT * FROM sales"), &groups); - let median = AnsiDialect.sql_quantile("price", 0.50, crate::sql::FromItem::Query("SELECT * FROM sales"), &groups); - let q3 = AnsiDialect.sql_quantile("price", 0.75, crate::sql::FromItem::Query("SELECT * FROM sales"), &groups); + let q1 = AnsiDialect.sql_quantile( + "price", + 0.25, + crate::sql::FromItem::Query("SELECT * FROM sales"), + &groups, + ); + let median = AnsiDialect.sql_quantile( + "price", + 0.50, + crate::sql::FromItem::Query("SELECT * FROM sales"), + &groups, + ); + let q3 = AnsiDialect.sql_quantile( + "price", + 0.75, + crate::sql::FromItem::Query("SELECT * FROM sales"), + &groups, + ); let expected = format!( r#"SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, @@ -466,9 +481,24 @@ mod tests { &AnsiDialect, ); - let q1 = AnsiDialect.sql_quantile("revenue", 0.25, crate::sql::FromItem::Query("SELECT * FROM data"), &groups); - let median = AnsiDialect.sql_quantile("revenue", 0.50, crate::sql::FromItem::Query("SELECT * FROM data"), &groups); - let q3 = AnsiDialect.sql_quantile("revenue", 0.75, crate::sql::FromItem::Query("SELECT * FROM data"), &groups); + let q1 = AnsiDialect.sql_quantile( + "revenue", + 0.25, + crate::sql::FromItem::Query("SELECT * FROM data"), + &groups, + ); + let median = AnsiDialect.sql_quantile( + "revenue", + 0.50, + crate::sql::FromItem::Query("SELECT * FROM data"), + &groups, + ); + let q3 = AnsiDialect.sql_quantile( + "revenue", + 0.75, + crate::sql::FromItem::Query("SELECT * FROM data"), + &groups, + ); let expected = format!( r#"SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, diff --git a/src/plot/layer/geom/density.rs b/src/plot/layer/geom/density.rs index 8ad666b6..1b215aac 100644 --- a/src/plot/layer/geom/density.rs +++ b/src/plot/layer/geom/density.rs @@ -274,8 +274,18 @@ fn silverman_rule( let adjust = 0.9 * adjust; let v = dialect.quote_ident(value_column); let stddev = format!("SQRT(AVG({v}*{v}) - AVG({v})*AVG({v}))", v = v); - let q75 = dialect.sql_quantile(value_column, 0.75, crate::sql::FromItem::Query(from), groups); - let q25 = dialect.sql_quantile(value_column, 0.25, crate::sql::FromItem::Query(from), groups); + let q75 = dialect.sql_quantile( + value_column, + 0.75, + crate::sql::FromItem::Query(from), + groups, + ); + let q25 = dialect.sql_quantile( + value_column, + 0.25, + crate::sql::FromItem::Query(from), + groups, + ); let iqr = format!("({q75} - {q25}) / 1.34"); let min_expr = dialect.sql_least(&[&stddev, &iqr]); // POWER, not POW: T-SQL has no POW function. And the base must not be an diff --git a/src/plot/layer/geom/rule.rs b/src/plot/layer/geom/rule.rs index 5deadcaa..6f24e5c0 100644 --- a/src/plot/layer/geom/rule.rs +++ b/src/plot/layer/geom/rule.rs @@ -250,7 +250,12 @@ fn expand_rule_to_segment( let scalar_bbox = |col: &str| { format!( "({})", - crate::sql::select_from(dialect, col, crate::sql::FromItem::Query(bbox_expr), "__ggsql_bbox__") + crate::sql::select_from( + dialect, + col, + crate::sql::FromItem::Query(bbox_expr), + "__ggsql_bbox__" + ) ) }; let (fixed_expr, span_expr) = if has_pos1 { @@ -305,7 +310,8 @@ fn expand_rule_to_segment( let __ggsql_vertices__ = dialect.quote_ident("__ggsql_vertices__"); let sql = crate::sql::Select::new(dialect) .select_items(&select_parts) - .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rule__") .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rule__") + .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) .build(); let mut out_columns: Vec = passthrough_cols.into_iter().cloned().collect(); diff --git a/src/plot/layer/geom/segment.rs b/src/plot/layer/geom/segment.rs index 68033fec..5a49e25c 100644 --- a/src/plot/layer/geom/segment.rs +++ b/src/plot/layer/geom/segment.rs @@ -148,7 +148,8 @@ fn expand_segment_to_vertices( let __ggsql_vertices__ = dialect.quote_ident("__ggsql_vertices__"); let sql = crate::sql::Select::new(dialect) .select_items(&select_parts) - .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_seg__") .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_seg__") + .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) .build(); let mut out_columns: Vec = passthrough_cols.into_iter().cloned().collect(); diff --git a/src/plot/layer/geom/stat_aggregate.rs b/src/plot/layer/geom/stat_aggregate.rs index c8190d99..ad2c25fc 100644 --- a/src/plot/layer/geom/stat_aggregate.rs +++ b/src/plot/layer/geom/stat_aggregate.rs @@ -458,10 +458,25 @@ fn simple_stat_sql_quantile( group_cols: &[String], ) -> String { if let Some(frac) = percentile_fraction(name) { - return dialect.sql_quantile(raw_col, frac, crate::sql::FromItem::Table(src_alias), group_cols); + return dialect.sql_quantile( + raw_col, + frac, + crate::sql::FromItem::Table(src_alias), + group_cols, + ); } - let p75 = dialect.sql_quantile(raw_col, 0.75, crate::sql::FromItem::Table(src_alias), group_cols); - let p25 = dialect.sql_quantile(raw_col, 0.25, crate::sql::FromItem::Table(src_alias), group_cols); + let p75 = dialect.sql_quantile( + raw_col, + 0.75, + crate::sql::FromItem::Table(src_alias), + group_cols, + ); + let p25 = dialect.sql_quantile( + raw_col, + 0.25, + crate::sql::FromItem::Table(src_alias), + group_cols, + ); format!("({} - {})", p75, p25) } diff --git a/src/plot/layer/geom/tile.rs b/src/plot/layer/geom/tile.rs index 52017eab..2aee5add 100644 --- a/src/plot/layer/geom/tile.rs +++ b/src/plot/layer/geom/tile.rs @@ -349,7 +349,8 @@ fn expand_rect_to_polygon( let sql = crate::sql::Select::new(dialect) .select_items(&select_parts) - .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rect__") .join_raw(&format!("CROSS JOIN {corners_table} {__ggsql_corners__}")) + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rect__") + .join_raw(&format!("CROSS JOIN {corners_table} {__ggsql_corners__}")) .build(); // Output columns: passthrough + poly_id + pos1 + pos2 @@ -388,7 +389,10 @@ fn rename_agg_stats_to_aes( .collect(); crate::sql::Select::new(dialect) .select_star_plus(&aliases, "__ggsql_post_agg__") - .from_aliased(crate::sql::FromItem::Query(&agg_query), "__ggsql_post_agg__") + .from_aliased( + crate::sql::FromItem::Query(&agg_query), + "__ggsql_post_agg__", + ) .build() } @@ -527,8 +531,12 @@ fn stat_tile( let select_list = select_parts.join(", "); // Build transformed query - let transformed_query = - crate::sql::select_from(dialect, &select_list, crate::sql::FromItem::Query(query), "__ggsql_tile_stat__"); + let transformed_query = crate::sql::select_from( + dialect, + &select_list, + crate::sql::FromItem::Query(query), + "__ggsql_tile_stat__", + ); // Use the same consumed aesthetic names for StatResult Ok(StatResult::Transformed { diff --git a/src/plot/projection/coord/map.rs b/src/plot/projection/coord/map.rs index 7bae70ff..3c23797b 100644 --- a/src/plot/projection/coord/map.rs +++ b/src/plot/projection/coord/map.rs @@ -758,7 +758,10 @@ fn detect_source_srid( } let sql = crate::sql::Select::new(dialect) .select(format!("{} AS srid", dialect.sql_st_srid(&ensure_geom))) - .from_aliased(crate::sql::FromItem::Query(&layer_queries[idx]), "__ggsql_srid__") + .from_aliased( + crate::sql::FromItem::Query(&layer_queries[idx]), + "__ggsql_srid__", + ) .and_where(format!("{geom_col} IS NOT NULL")) .build_limited(1); if let Ok(df) = execute_query(&sql) { From 4f13537a8b70d21287348b53d6addbfdea599c9f Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Thu, 8 Oct 2026 20:16:14 +0200 Subject: [PATCH 10/10] Update src/sql/mod.rs Co-authored-by: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> --- src/sql/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sql/mod.rs b/src/sql/mod.rs index bc2acf4e..ce615888 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -402,10 +402,7 @@ impl<'d, D: SqlDialect + ?Sized> Select<'d, D> { let trimmed = match from { FromItem::Table(t) => return t.trim().to_string(), FromItem::Raw(r) => return r.trim().to_string(), - FromItem::Fragment(f) => { - let f = f.trim(); - return f.to_string(); - } + FromItem::Fragment(f) => return f.trim().to_string(), FromItem::Query(q) => q.trim(), }; if !self.dialect.allows_cte_in_derived_table() {