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 305d3ccb..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(", "), 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 +433,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 +701,10 @@ 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..652fa0f2 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 * 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__" "__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 * 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__") "__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;; 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 * 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__") "__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;; 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 * 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__" "__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 * 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__" "__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 * 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__" "__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 * 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__" "__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..cd0545ba 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,24 @@ 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 +481,24 @@ 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..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, 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..6f24e5c0 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,12 @@ 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 +296,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,7 +310,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__") + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rule__") .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) .build(); diff --git a/src/plot/layer/geom/segment.rs b/src/plot/layer/geom/segment.rs index e1c49874..5a49e25c 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,7 +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__") + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_seg__") .join_raw(&format!("CROSS JOIN {vertices_table} {__ggsql_vertices__}")) .build(); 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..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, 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 +1072,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..2aee5add 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,7 +349,7 @@ fn expand_rect_to_polygon( let sql = crate::sql::Select::new(dialect) .select_items(&select_parts) - .from_aliased(&numbered, "__ggsql_rect__") + .from_aliased(crate::sql::FromItem::Query(&numbered), "__ggsql_rect__") .join_raw(&format!("CROSS JOIN {corners_table} {__ggsql_corners__}")) .build(); @@ -389,7 +389,10 @@ 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() } @@ -528,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, 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/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..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(&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 +932,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/adbc.rs b/src/reader/adbc.rs index a875aa6b..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( @@ -183,9 +196,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 +304,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 +331,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,38 +367,18 @@ impl AdbcReader { (driver, opts) }; - let dialect = resolve_dialect(&conn)?; - - Self::new_with_database_opts(driver, dialect, opts) - .map(|reader| reader.with_statement_opts(conn.ggsql.stmt_options.clone())) - } -} + let dialect = crate::reader::registry::resolve_dialect(&conn)?; -/// 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()); + 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, + }, + ) } - crate::reader::dialects::detect_dialect(None, Some(body)) } /// Compute the URI handed to the driver as the `uri` database option, @@ -514,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| { @@ -602,7 +623,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)))?; @@ -667,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) => { @@ -703,7 +728,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 cc8a72f2..fa3c98b2 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 @@ -538,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 } @@ -760,13 +758,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/bigquery.rs b/src/reader/dialects/bigquery.rs index 0ec5422c..518470f2 100644 --- a/src/reader/dialects/bigquery.rs +++ b/src/reader/dialects/bigquery.rs @@ -35,15 +35,22 @@ 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\ )" ) } - 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 +107,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 +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, "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..c1f7e540 100644 --- a/src/reader/dialects/clickhouse.rs +++ b/src/reader/dialects/clickhouse.rs @@ -67,16 +67,21 @@ 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 { + let seq = self.quote_ident("__ggsql_seq__"); format!( - "`__ggsql_seq__`(n) AS (\ + "{seq}(n) AS (\ SELECT toFloat64(number) AS n FROM numbers({n})\ )" ) @@ -100,7 +105,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 +185,16 @@ 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..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\ )\ @@ -43,7 +44,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 e12bf978..f90aaa14 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; @@ -47,46 +42,13 @@ pub use trino::TrinoDialect; 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(""), - )) - }) -} +/// 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; -/// 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()) -} +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 @@ -116,118 +78,14 @@ 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::*; + 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); @@ -312,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`"); @@ -355,7 +183,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", @@ -386,7 +214,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..df7efe99 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) @@ -45,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( @@ -113,7 +98,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..625f2b94 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) @@ -65,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 @@ -104,7 +130,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..f4fb21f3 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 @@ -126,6 +137,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 +248,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 +262,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 +348,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 +450,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 +511,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); } @@ -677,6 +708,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. @@ -703,12 +742,22 @@ 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), 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)] } @@ -717,14 +766,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 => { @@ -787,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 @@ -896,8 +943,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 @@ -1041,7 +1087,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, @@ -1134,6 +1180,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/reader/odbc/mod.rs b/src/reader/odbc/mod.rs index 52319bc1..5933744a 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, }; @@ -125,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, @@ -160,7 +158,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); } @@ -262,7 +260,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)) })?; diff --git a/src/reader/registry.rs b/src/reader/registry.rs index d959dbd8..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), @@ -397,6 +470,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::*; diff --git a/src/sql/mod.rs b/src/sql/mod.rs index 26059c36..ce615888 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -16,28 +16,152 @@ //! //! 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. +/// +/// 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 +265,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 +365,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 +395,16 @@ 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) => return f.trim().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); @@ -295,6 +433,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!( @@ -306,16 +474,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 +550,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 +568,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 +598,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__\"" );