From c7bfa116fc00294421cabcbe966b11dea4d0a4c7 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Fri, 9 Oct 2026 12:38:35 +0200 Subject: [PATCH 1/5] Move ORDER BY out to top level --- src/execute/layer.rs | 13 ++++- src/golden/ansi.sql | 2 +- src/golden/bigquery.sql | 2 +- src/golden/clickhouse.sql | 2 +- src/golden/databricks.sql | 2 +- src/golden/datafusion.sql | 2 +- src/golden/drill.sql | 2 +- src/golden/druid.sql | 2 +- src/golden/duckdb.sql | 2 +- src/golden/exasol.sql | 2 +- src/golden/monetdb.sql | 2 +- src/golden/mssql.sql | 2 +- src/golden/mysql.sql | 2 +- src/golden/oracle.sql | 2 +- src/golden/postgres.sql | 2 +- src/golden/redshift.sql | 2 +- src/golden/snowflake.sql | 2 +- src/golden/sqlite.sql | 2 +- src/golden/trino.sql | 2 +- src/plot/layer/geom/bar.rs | 1 + src/plot/layer/geom/boxplot.rs | 2 + src/plot/layer/geom/density.rs | 1 + src/plot/layer/geom/histogram.rs | 1 + src/plot/layer/geom/smooth.rs | 2 + src/plot/layer/geom/spatial.rs | 1 + src/plot/layer/geom/stat_aggregate.rs | 1 + src/plot/layer/geom/tile.rs | 3 ++ src/plot/layer/geom/types.rs | 69 ++++++++++++++++++--------- src/plot/layer/geom/violin.rs | 2 + src/tests/battery/mod.rs | 25 ++++------ 30 files changed, 100 insertions(+), 57 deletions(-) diff --git a/src/execute/layer.rs b/src/execute/layer.rs index 7e612df7..3923ed97 100644 --- a/src/execute/layer.rs +++ b/src/execute/layer.rs @@ -566,13 +566,19 @@ where } } + // Geom-required ordering carried by the stat transform (e.g. line + // ordering along the domain axis). Applied on the final query below. + let mut stat_order_by: Vec = Vec::new(); + let final_query = match stat_result { StatResult::Transformed { query: transformed_query, stat_columns, dummy_columns, consumed_aesthetics, + order_by: stat_order, } => { + stat_order_by = stat_order; // Build stat column -> aesthetic mappings from geom defaults for renaming let mut final_remappings: HashMap = HashMap::new(); @@ -726,9 +732,14 @@ where normalize_mapping_column_names(layer); } - // Apply explicit ORDER BY if provided + // Apply ORDER BY. An explicit clause ORDER BY takes precedence over + // geom-required ordering carried by the stat transform. Ordering is only + // ever applied here, on the final outermost query — nested ORDER BY is + // rejected by some backends (T-SQL error 1033). let final_query = if let Some(ref o) = order_by { format!("{} ORDER BY {}", final_query, o.as_str()) + } else if !stat_order_by.is_empty() { + format!("{} ORDER BY {}", final_query, stat_order_by.join(", ")) } else { final_query }; diff --git a/src/golden/ansi.sql b/src/golden/ansi.sql index 1863aa77..71e5a0a1 100644 --- a/src/golden/ansi.sql +++ b/src/golden/ansi.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/bigquery.sql b/src/golden/bigquery.sql index 5dded256..d6dad5c3 100644 --- a/src/golden/bigquery.sql +++ b/src/golden/bigquery.sql @@ -17,7 +17,7 @@ SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS ` -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_ord__` ORDER BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__`; +SELECT *, `__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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__` ORDER BY `__ggsql_aes_pos1__`; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/golden/clickhouse.sql b/src/golden/clickhouse.sql index ed6975b5..44dba9a9 100644 --- a/src/golden/clickhouse.sql +++ b/src/golden/clickhouse.sql @@ -17,7 +17,7 @@ SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS ` -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_ord__` ORDER BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__`; +SELECT *, `__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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__` ORDER BY `__ggsql_aes_pos1__`; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/golden/databricks.sql b/src/golden/databricks.sql index 73315424..59348683 100644 --- a/src/golden/databricks.sql +++ b/src/golden/databricks.sql @@ -17,7 +17,7 @@ SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS ` -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_ord__` ORDER BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__`; +SELECT *, `__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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__` ORDER BY `__ggsql_aes_pos1__`; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/golden/datafusion.sql b/src/golden/datafusion.sql index 9301ccd4..950f46b3 100644 --- a/src/golden/datafusion.sql +++ b/src/golden/datafusion.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/drill.sql b/src/golden/drill.sql index 34f6cc9c..35ca7b5d 100644 --- a/src/golden/drill.sql +++ b/src/golden/drill.sql @@ -17,7 +17,7 @@ SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS ` -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_ord__` ORDER BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__`; +SELECT *, `__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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__` ORDER BY `__ggsql_aes_pos1__`; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/golden/druid.sql b/src/golden/druid.sql index 5d1532b8..23030d76 100644 --- a/src/golden/druid.sql +++ b/src/golden/druid.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/duckdb.sql b/src/golden/duckdb.sql index d92914a3..bea7608c 100644 --- a/src/golden/duckdb.sql +++ b/src/golden/duckdb.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/exasol.sql b/src/golden/exasol.sql index 1863aa77..71e5a0a1 100644 --- a/src/golden/exasol.sql +++ b/src/golden/exasol.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/monetdb.sql b/src/golden/monetdb.sql index 66f65895..e5c965ee 100644 --- a/src/golden/monetdb.sql +++ b/src/golden/monetdb.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/mssql.sql b/src/golden/mssql.sql index a5585303..01e1c136 100644 --- a/src/golden/mssql.sql +++ b/src/golden/mssql.sql @@ -17,7 +17,7 @@ WITH summary AS ( SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST( -- case: line_aggregate SELECT TOP 1 * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__") AS "__ggsql_lim__"; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__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__"; -WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT *, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM (SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT *, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT TOP 1 * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__") AS "__ggsql_lim__"; diff --git a/src/golden/mysql.sql b/src/golden/mysql.sql index 7052cb96..9b2aad0a 100644 --- a/src/golden/mysql.sql +++ b/src/golden/mysql.sql @@ -17,7 +17,7 @@ SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS ` -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_ord__` ORDER BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__`; +SELECT *, `__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) AS `__ggsql_src__`) SELECT `__ggsql_aes_pos1__`, AVG(`__ggsql_aes_pos2__`) AS `__ggsql_stat_pos2` FROM `__ggsql_stat_src__` AS `__ggsql_qt__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_stat__` ORDER BY `__ggsql_aes_pos1__`; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/golden/oracle.sql b/src/golden/oracle.sql index 652fa0f2..6a50dd8b 100644 --- a/src/golden/oracle.sql +++ b/src/golden/oracle.sql @@ -17,7 +17,7 @@ SELECT "__ggsql_stat__".*, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql -- case: line_aggregate SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case", MIN("__ggsql_lim__") AS "__ggsql_min___ggsql_lim__", MAX("__ggsql_lim__") AS "__ggsql_max___ggsql_lim__" FROM "__ggsql_source__"; -SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM (WITH "__ggsql_stat_src__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") "__ggsql_stat__"; +SELECT "__ggsql_stat__".*, "__ggsql_stat_pos2" AS "__ggsql_aes_pos2__" FROM (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_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; diff --git a/src/golden/postgres.sql b/src/golden/postgres.sql index 1ed7a366..6401ab74 100644 --- a/src/golden/postgres.sql +++ b/src/golden/postgres.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/redshift.sql b/src/golden/redshift.sql index 8a447c44..aad77525 100644 --- a/src/golden/redshift.sql +++ b/src/golden/redshift.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/snowflake.sql b/src/golden/snowflake.sql index 7935eedc..f4b9b337 100644 --- a/src/golden/snowflake.sql +++ b/src/golden/snowflake.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/sqlite.sql b/src/golden/sqlite.sql index 5b54c070..3659286a 100644 --- a/src/golden/sqlite.sql +++ b/src/golden/sqlite.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/golden/trino.sql b/src/golden/trino.sql index a710e254..e6984f32 100644 --- a/src/golden/trino.sql +++ b/src/golden/trino.sql @@ -17,7 +17,7 @@ SELECT *, "__ggsql_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS " -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM ggsql_live_test) AS "__ggsql_src__") SELECT MIN("id") AS "__ggsql_min_id", MAX("id") AS "__ggsql_max_id", MIN("val") AS "__ggsql_min_val", MAX("val") AS "__ggsql_max_val", MIN("grp") AS "__ggsql_min_grp", MAX("grp") AS "__ggsql_max_grp", MIN("day") AS "__ggsql_min_day", MAX("day") AS "__ggsql_max_day", MIN("mixed Case") AS "__ggsql_min_mixed Case", MAX("mixed Case") AS "__ggsql_max_mixed Case" FROM "__ggsql_source__"; -SELECT *, "__ggsql_stat_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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_ord__" ORDER BY "__ggsql_aes_pos1__") AS "__ggsql_stat__"; +SELECT *, "__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) AS "__ggsql_src__") SELECT "__ggsql_aes_pos1__", AVG("__ggsql_aes_pos2__") AS "__ggsql_stat_pos2" FROM "__ggsql_stat_src__" AS "__ggsql_qt__" GROUP BY "__ggsql_aes_pos1__") AS "__ggsql_stat__" ORDER BY "__ggsql_aes_pos1__"; -- case: density_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__" LIMIT 1; diff --git a/src/plot/layer/geom/bar.rs b/src/plot/layer/geom/bar.rs index 7cd76ddc..842db2fa 100644 --- a/src/plot/layer/geom/bar.rs +++ b/src/plot/layer/geom/bar.rs @@ -372,5 +372,6 @@ fn stat_bar_count( stat_columns, dummy_columns, consumed_aesthetics, + order_by: vec![], }) } diff --git a/src/plot/layer/geom/boxplot.rs b/src/plot/layer/geom/boxplot.rs index cd0545ba..1801404d 100644 --- a/src/plot/layer/geom/boxplot.rs +++ b/src/plot/layer/geom/boxplot.rs @@ -202,6 +202,7 @@ fn stat_boxplot( stat_columns, dummy_columns, consumed_aesthetics: vec!["pos2".to_string()], + order_by: vec![], }) } @@ -734,6 +735,7 @@ mod tests { stat_columns, dummy_columns, consumed_aesthetics, + .. } => { // The wrapped input introduces a synthetic pos1 column that the // GROUP BY then collapses to a single boxplot. diff --git a/src/plot/layer/geom/density.rs b/src/plot/layer/geom/density.rs index 1b215aac..a04eaa36 100644 --- a/src/plot/layer/geom/density.rs +++ b/src/plot/layer/geom/density.rs @@ -207,6 +207,7 @@ pub(crate) fn stat_density( stat_columns: stats, dummy_columns: vec![], consumed_aesthetics: consumed, + order_by: vec![], }) } diff --git a/src/plot/layer/geom/histogram.rs b/src/plot/layer/geom/histogram.rs index b2767c9e..31a20876 100644 --- a/src/plot/layer/geom/histogram.rs +++ b/src/plot/layer/geom/histogram.rs @@ -300,6 +300,7 @@ fn stat_histogram( ], dummy_columns: vec![], consumed_aesthetics: vec!["pos1".to_string(), "weight".to_string()], + order_by: vec![], }) } diff --git a/src/plot/layer/geom/smooth.rs b/src/plot/layer/geom/smooth.rs index 146f6f7a..b029ceba 100644 --- a/src/plot/layer/geom/smooth.rs +++ b/src/plot/layer/geom/smooth.rs @@ -215,6 +215,7 @@ fn stat_ols( stat_columns: vec!["pos1".to_string(), "intensity".to_string()], dummy_columns: vec![], consumed_aesthetics: vec!["pos1".to_string(), "pos2".to_string()], + order_by: vec![], }) } @@ -311,6 +312,7 @@ fn stat_tls( stat_columns: vec!["pos1".to_string(), "intensity".to_string()], dummy_columns: vec![], consumed_aesthetics: vec!["pos1".to_string(), "pos2".to_string()], + order_by: vec![], }) } diff --git a/src/plot/layer/geom/spatial.rs b/src/plot/layer/geom/spatial.rs index ad092d7e..607e4799 100644 --- a/src/plot/layer/geom/spatial.rs +++ b/src/plot/layer/geom/spatial.rs @@ -73,6 +73,7 @@ impl GeomTrait for Spatial { stat_columns: vec![], dummy_columns: vec![], consumed_aesthetics: vec![], + order_by: vec![], }) } diff --git a/src/plot/layer/geom/stat_aggregate.rs b/src/plot/layer/geom/stat_aggregate.rs index ad2c25fc..797bc89b 100644 --- a/src/plot/layer/geom/stat_aggregate.rs +++ b/src/plot/layer/geom/stat_aggregate.rs @@ -882,6 +882,7 @@ pub fn apply( stat_columns, dummy_columns: vec![], consumed_aesthetics, + order_by: vec![], }) } diff --git a/src/plot/layer/geom/tile.rs b/src/plot/layer/geom/tile.rs index 2aee5add..e4b34540 100644 --- a/src/plot/layer/geom/tile.rs +++ b/src/plot/layer/geom/tile.rs @@ -192,6 +192,7 @@ impl GeomTrait for Tile { mut stat_columns, dummy_columns, consumed_aesthetics, + order_by, } = tile_result { if !stat_columns.iter().any(|s| s == "aggregate") { @@ -202,6 +203,7 @@ impl GeomTrait for Tile { stat_columns, dummy_columns, consumed_aesthetics, + order_by, }); } } @@ -547,6 +549,7 @@ fn stat_tile( .iter() .map(|s| s.to_string()) .collect(), + order_by: vec![], }) } diff --git a/src/plot/layer/geom/types.rs b/src/plot/layer/geom/types.rs index 6dad13fd..c3fe4f55 100644 --- a/src/plot/layer/geom/types.rs +++ b/src/plot/layer/geom/types.rs @@ -202,6 +202,13 @@ pub enum StatResult { /// These aesthetics were used as input to the stat and should be removed /// from the layer mappings after the transform completes consumed_aesthetics: Vec, + /// ORDER BY expressions (already quoted) required for correct + /// rendering, e.g. line ordering along the domain axis. Carried as + /// data rather than embedded in `query` so the pipeline can apply + /// ordering on the final, outermost query only — nested ORDER BY is + /// rejected by some backends (T-SQL error 1033) and is semantically + /// meaningless in derived tables elsewhere. + order_by: Vec, }, } @@ -209,16 +216,19 @@ pub use crate::plot::types::ColumnInfo; /// Schema of a data source - list of columns with type info pub use crate::plot::types::Schema; -/// Wrap a stat result with `ORDER BY `. +/// Attach `ORDER BY ` to a stat result. /// /// Used by line/area/ribbon to ensure the rendered output is sorted along the /// domain axis whether or not the layer also goes through the Aggregate stat. +/// The ordering is recorded in [`StatResult::Transformed::order_by`] and left +/// for the pipeline to apply on the final, outermost query — embedding it in +/// the query text here would nest it inside derived tables, which T-SQL +/// rejects (error 1033). /// -/// - `Identity` → becomes `Transformed` with ` ORDER BY `, +/// - `Identity` → becomes `Transformed` over the unchanged input query with /// empty `stat_columns`/`dummy_columns`/`consumed_aesthetics`. -/// - `Transformed` → wraps the existing query in -/// `SELECT * FROM () AS "__ggsql_ord__" ORDER BY ` and preserves -/// the stat metadata. +/// - `Transformed` → appends to the existing `order_by` and preserves the +/// query and stat metadata. pub fn wrap_with_order_by( input_query: &str, result: StatResult, @@ -229,26 +239,28 @@ pub fn wrap_with_order_by( let order_quoted = dialect.quote_ident(&order_col); match result { StatResult::Identity => StatResult::Transformed { - query: format!("{} ORDER BY {}", input_query, order_quoted), + query: input_query.to_string(), stat_columns: vec![], dummy_columns: vec![], consumed_aesthetics: vec![], + order_by: vec![order_quoted], }, StatResult::Transformed { query, stat_columns, dummy_columns, consumed_aesthetics, - } => StatResult::Transformed { - query: crate::sql::Select::new(dialect) - .select_star() - .from_aliased(crate::sql::FromItem::Query(&query), "__ggsql_ord__") - .order_by(order_quoted) - .build(), - stat_columns, - dummy_columns, - consumed_aesthetics, - }, + mut order_by, + } => { + order_by.push(order_quoted); + StatResult::Transformed { + query, + stat_columns, + dummy_columns, + consumed_aesthetics, + order_by, + } + } } } @@ -298,12 +310,14 @@ pub fn wrap_stat_with_dummy_axis( stat_columns: vec![axis.to_string()], dummy_columns: vec![axis.to_string()], consumed_aesthetics: vec![], + order_by: vec![], }, StatResult::Transformed { query, mut stat_columns, mut dummy_columns, consumed_aesthetics, + order_by, } => { // Idempotent: a stat that already produced a dummy for this axis // must not be re-wrapped — the SQL would gain a duplicate column. @@ -324,6 +338,7 @@ pub fn wrap_stat_with_dummy_axis( stat_columns, dummy_columns, consumed_aesthetics, + order_by, } } } @@ -448,7 +463,7 @@ mod tests { } #[test] - fn wrap_with_order_by_identity_appends_order() { + fn wrap_with_order_by_identity_records_ordering() { let result = wrap_with_order_by( "SELECT * FROM t", StatResult::Identity, @@ -461,8 +476,12 @@ mod tests { stat_columns, dummy_columns, consumed_aesthetics, + order_by, } => { - assert_eq!(query, "SELECT * FROM t ORDER BY \"__ggsql_aes_pos1__\""); + // Query text is left alone; ordering is carried as data and + // applied on the final outermost query by execute/layer.rs. + assert_eq!(query, "SELECT * FROM t"); + assert_eq!(order_by, vec!["\"__ggsql_aes_pos1__\"".to_string()]); assert!(stat_columns.is_empty()); assert!(dummy_columns.is_empty()); assert!(consumed_aesthetics.is_empty()); @@ -472,12 +491,13 @@ mod tests { } #[test] - fn wrap_with_order_by_transformed_wraps_query_and_preserves_metadata() { + fn wrap_with_order_by_transformed_records_ordering_and_preserves_metadata() { let inner = StatResult::Transformed { query: "SELECT * FROM grouped".to_string(), stat_columns: vec!["pos2".to_string(), "aggregate".to_string()], dummy_columns: vec!["pos1".to_string()], consumed_aesthetics: vec!["pos2".to_string()], + order_by: vec![], }; let result = wrap_with_order_by( "SELECT * FROM raw", @@ -491,11 +511,10 @@ mod tests { stat_columns, dummy_columns, consumed_aesthetics, + order_by, } => { - assert_eq!( - query, - "SELECT * FROM (SELECT * FROM grouped) AS \"__ggsql_ord__\" ORDER BY \"__ggsql_aes_pos1__\"" - ); + assert_eq!(query, "SELECT * FROM grouped"); + assert_eq!(order_by, vec!["\"__ggsql_aes_pos1__\"".to_string()]); assert_eq!( stat_columns, vec!["pos2".to_string(), "aggregate".to_string()] @@ -529,6 +548,7 @@ mod tests { stat_columns, dummy_columns, consumed_aesthetics, + .. } => { assert!(query.contains("__ggsql_stat_dummy")); assert!(query.contains("__ggsql_stat_pos1")); @@ -548,6 +568,7 @@ mod tests { stat_columns: vec!["count".to_string()], dummy_columns: vec![], consumed_aesthetics: vec!["weight".to_string()], + order_by: vec![], }; let result = wrap_stat_with_dummy_pos1("SELECT * FROM raw", inner, &crate::reader::AnsiDialect); @@ -557,6 +578,7 @@ mod tests { stat_columns, dummy_columns, consumed_aesthetics, + .. } => { assert!(query.contains("__ggsql_stat_dummy")); assert!(query.contains("__ggsql_stat_pos1")); @@ -578,6 +600,7 @@ mod tests { stat_columns: vec!["pos1".to_string()], dummy_columns: vec!["pos1".to_string()], consumed_aesthetics: vec![], + order_by: vec![], }; let result = wrap_stat_with_dummy_pos1("SELECT *", inner, &crate::reader::AnsiDialect); match result { diff --git a/src/plot/layer/geom/violin.rs b/src/plot/layer/geom/violin.rs index 41601d0e..23607ca4 100644 --- a/src/plot/layer/geom/violin.rs +++ b/src/plot/layer/geom/violin.rs @@ -265,6 +265,7 @@ fn stat_violin( mut stat_columns, mut dummy_columns, consumed_aesthetics, + order_by, } => { if !stat_columns.iter().any(|s| s == "pos1") { stat_columns.push("pos1".to_string()); @@ -277,6 +278,7 @@ fn stat_violin( stat_columns, dummy_columns, consumed_aesthetics, + order_by, }) } } diff --git a/src/tests/battery/mod.rs b/src/tests/battery/mod.rs index d44b0eeb..44238b50 100644 --- a/src/tests/battery/mod.rs +++ b/src/tests/battery/mod.rs @@ -241,21 +241,16 @@ pub fn cases() -> Vec { "VISUALISE DRAW boxplot MAPPING grp AS x, val AS y FROM {table}", Expect::MinRows(1), ), - // Line with an aggregating stat: the geom appends ORDER BY after - // the stat transform, and the layer's stat-rename wrap then nests - // that ORDER BY inside a derived table. T-SQL rejects ORDER BY in - // derived tables without TOP/OFFSET (error 1033), so MSSQL is - // skipped until the pipeline applies ordering to final queries - // only; the broken shape is pinned in golden/mssql.sql. - Case { - live_skip: &[("mssql", "T-SQL 1033: ORDER BY nested in derived table")], - ..shared( - "line_aggregate", - "VISUALISE DRAW line MAPPING grp AS x, val AS y FROM {table} \ - SETTING aggregate => 'y:mean'", - Expect::ExactRows(2), - ) - }, + // Line with an aggregating stat: the geom's required ordering is + // carried as data on the stat result and applied on the final, + // outermost query only, so no ORDER BY is nested inside a derived + // table (which T-SQL rejects with error 1033). + shared( + "line_aggregate", + "VISUALISE DRAW line MAPPING grp AS x, val AS y FROM {table} \ + SETTING aggregate => 'y:mean'", + Expect::ExactRows(2), + ), // Grouped density: cross-group grid join with qualified // projections. A degenerate (NULL) bandwidth would silently // produce an empty result. From c76678e263d8b12bc33365ac105c4ad4e4c6c5c9 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Fri, 9 Oct 2026 12:44:34 +0200 Subject: [PATCH 2/5] Only reference temp tables once for boxplots --- src/golden/mysql.sql | 6 +- src/plot/layer/geom/boxplot.rs | 199 ++++++++++++++++++++++++++++----- src/reader/dialects/mysql.rs | 43 +++++++ src/reader/mod.rs | 114 +++++++++++++++++++ src/tests/battery/mod.rs | 34 ++---- 5 files changed, 340 insertions(+), 56 deletions(-) diff --git a/src/golden/mysql.sql b/src/golden/mysql.sql index 9b2aad0a..1fb8c164 100644 --- a/src/golden/mysql.sql +++ b/src/golden/mysql.sql @@ -12,7 +12,7 @@ SELECT *, `__ggsql_stat_bin` AS `__ggsql_aes_pos1__`, `__ggsql_stat_bin_end` AS -- case: boxplot_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH summary AS ( SELECT *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q3 FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__`) AS `__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) AS `__ggsql_src__`) AS `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) AS `__ggsql_stat__`; +SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH `__ggsql_bp_w__` AS ( SELECT `__ggsql_aes_pos1__`, `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL ), summary AS ( SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT `__ggsql_aes_pos1__`, MIN(__val) AS min, MAX(__val) AS max, MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) AS q1, MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) AS median, MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) AS q3 FROM `__ggsql_bp_w__` AS `__ggsql_w__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_fn__` ), outliers AS ( SELECT raw.__val AS value, 'outlier' AS type, raw.`__ggsql_aes_pos1__` AS `__ggsql_aes_pos1__` FROM `__ggsql_bp_w__` AS `raw` JOIN summary ON raw.`__ggsql_aes_pos1__` = summary.`__ggsql_aes_pos1__` WHERE raw.__val 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) AS `__ggsql_stat__`; -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; @@ -36,7 +36,7 @@ DROP TEMPORARY TABLE IF EXISTS `__ggsql_global___`; CREATE TEMPORARY TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; SELECT * FROM (SELECT * FROM `__ggsql_global___`) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH summary AS ( SELECT *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q3 FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__`) AS `__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___`) AS `__ggsql_src__`) AS `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) AS `__ggsql_stat__`; +SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH `__ggsql_bp_w__` AS ( SELECT `__ggsql_aes_pos1__`, `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL ), summary AS ( SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT `__ggsql_aes_pos1__`, MIN(__val) AS min, MAX(__val) AS max, MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) AS q1, MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) AS median, MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) AS q3 FROM `__ggsql_bp_w__` AS `__ggsql_w__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_fn__` ), outliers AS ( SELECT raw.__val AS value, 'outlier' AS type, raw.`__ggsql_aes_pos1__` AS `__ggsql_aes_pos1__` FROM `__ggsql_bp_w__` AS `raw` JOIN summary ON raw.`__ggsql_aes_pos1__` = summary.`__ggsql_aes_pos1__` WHERE raw.__val 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) AS `__ggsql_stat__`; -- case: bar_count SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; @@ -108,7 +108,7 @@ SELECT *, `__ggsql_stat_pos2` AS `__ggsql_aes_pos2__` FROM (WITH `__ggsql_stat_s -- case: quoted_column SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH summary AS ( SELECT *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q3 FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__`) AS `__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) AS `__ggsql_src__`) AS `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) AS `__ggsql_stat__`; +SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH `__ggsql_bp_w__` AS ( SELECT `__ggsql_aes_pos1__`, `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL ), summary AS ( SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT `__ggsql_aes_pos1__`, MIN(__val) AS min, MAX(__val) AS max, MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) AS q1, MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) AS median, MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) AS q3 FROM `__ggsql_bp_w__` AS `__ggsql_w__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_fn__` ), outliers AS ( SELECT raw.__val AS value, 'outlier' AS type, raw.`__ggsql_aes_pos1__` AS `__ggsql_aes_pos1__` FROM `__ggsql_bp_w__` AS `raw` JOIN summary ON raw.`__ggsql_aes_pos1__` = summary.`__ggsql_aes_pos1__` WHERE raw.__val 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) AS `__ggsql_stat__`; -- case: partition_by_quoted SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/plot/layer/geom/boxplot.rs b/src/plot/layer/geom/boxplot.rs index 1801404d..4790959f 100644 --- a/src/plot/layer/geom/boxplot.rs +++ b/src/plot/layer/geom/boxplot.rs @@ -175,16 +175,31 @@ fn stat_boxplot( )); } - // Query for boxplot summary statistics - let summary = boxplot_sql_compute_summary(&working_query, &groups, &value_col, coef, dialect); - let stats_query = boxplot_sql_append_outliers( - &summary, - &groups, + // Query for boxplot summary statistics. Dialects that forbid + // re-opening a temporary table in one statement (MySQL/MariaDB) provide + // a single-scan summary composed around a shared windowed pass; the + // fallback scans the source once per quantile. + let fractions = [(0.25, "q1"), (0.5, "median"), (0.75, "q3")]; + let stats_query = match dialect.sql_quantiles_single_scan( &value_col, - &working_query, - outliers, - dialect, - ); + &fractions, + crate::sql::FromItem::Query(&working_query), + &groups, + ) { + Some(parts) => boxplot_sql_single_scan(&parts, &groups, coef, outliers, dialect), + None => { + let summary = + boxplot_sql_compute_summary(&working_query, &groups, &value_col, coef, dialect); + boxplot_sql_append_outliers( + &summary, + &groups, + &value_col, + &working_query, + outliers, + dialect, + ) + } + }; let mut stat_columns = vec![ "type".to_string(), @@ -286,6 +301,102 @@ fn boxplot_sql_filter_outliers( .build() } +/// Build the visual-element rows (`lower_whisker`/`upper_whisker`/`box`/ +/// `median`) from a summary relation exposing `q1`, `q3`, `median`, +/// `lower`, `upper`, and the group columns. +fn boxplot_summary_rows_select(table: &str, groups: &[String], dialect: &dyn SqlDialect) -> String { + let value_name = dialect.quote_ident(&naming::stat_column("value")); + let value2_name = dialect.quote_ident(&naming::stat_column("value2")); + let type_name = dialect.quote_ident(&naming::stat_column("type")); + let quoted_groups: Vec = groups.iter().map(|g| dialect.quote_ident(g)).collect(); + let groups_str = quoted_groups.join(", "); + format!( + "SELECT {groups}, 'lower_whisker' AS {type_name}, q1 AS {value_name}, lower AS {value2_name} FROM {table} + UNION ALL + SELECT {groups}, 'upper_whisker' AS {type_name}, q3 AS {value_name}, upper AS {value2_name} FROM {table} + UNION ALL + SELECT {groups}, 'box' AS {type_name}, q1 AS {value_name}, q3 AS {value2_name} FROM {table} + UNION ALL + SELECT {groups}, 'median' AS {type_name}, median AS {value_name}, NULL AS {value2_name} FROM {table}", + groups = groups_str, + type_name = type_name, + value_name = value_name, + value2_name = value2_name, + table = table + ) +} + +/// Compose the full boxplot stats statement around a single-scan quantile +/// summary (see `SqlDialect::sql_quantiles_single_scan`). The source is +/// referenced exactly once — inside the windowed-pass CTE — which backends +/// that forbid re-opening a temporary table per statement (MySQL/MariaDB +/// error 1137) require. The outlier filter joins against the same pass, so +/// it adds no further scans of the source. +fn boxplot_sql_single_scan( + parts: &crate::reader::SingleScanQuantiles, + groups: &[String], + coef: &f64, + draw_outliers: &bool, + dialect: &dyn SqlDialect, +) -> String { + let quoted_groups: Vec = groups.iter().map(|g| dialect.quote_ident(g)).collect(); + 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 summary_with_bounds = crate::sql::Select::new(dialect) + .select_star_plus( + &[ + format!("{lower_expr} AS lower"), + format!("{upper_expr} AS upper"), + ], + "__ggsql_fn__", + ) + .from_aliased(crate::sql::FromItem::Query(&parts.summary), "__ggsql_fn__") + .build(); + + let summary_select = boxplot_summary_rows_select("summary", groups, dialect); + let quoted_pass = dialect.quote_ident(&parts.pass_name); + let pass_cte = format!("{quoted_pass} AS (\n {}\n)", parts.pass); + let summary_cte = format!("summary AS (\n {summary_with_bounds}\n)"); + + if !*draw_outliers { + return format!("WITH {pass_cte},\n{summary_cte}\n{summary_select}"); + } + + // Outlier rows come from the same windowed pass, joined to the summary + // for the whisker bounds, so the source is still scanned only once. + let mut join_pairs = Vec::new(); + let mut keep_columns = Vec::new(); + for column in groups { + let quoted = dialect.quote_ident(column); + join_pairs.push(format!("raw.{quoted} = summary.{quoted}")); + keep_columns.push(format!("raw.{quoted} AS {quoted}")); + } + let mut items = vec![ + "raw.__val AS value".to_string(), + "'outlier' AS type".to_string(), + ]; + items.extend(keep_columns); + let outliers = crate::sql::Select::new(dialect) + .select_items(&items) + .from_aliased(crate::sql::FromItem::Table("ed_pass), "raw") + .join_raw(&format!("JOIN summary ON {}", join_pairs.join(" AND "))) + .and_where("raw.__val NOT BETWEEN summary.lower AND summary.upper") + .build(); + + let value_name = dialect.quote_ident(&naming::stat_column("value")); + let value2_name = dialect.quote_ident(&naming::stat_column("value2")); + let type_name = dialect.quote_ident(&naming::stat_column("type")); + format!( + "WITH {pass_cte},\n{summary_cte},\noutliers AS (\n {outliers}\n) + {summary_select} + UNION ALL + SELECT {groups_str}, type AS {type_name}, value AS {value_name}, NULL AS {value2_name} + FROM outliers" + ) +} + fn boxplot_sql_append_outliers( from: &str, groups: &[String], @@ -301,35 +412,16 @@ fn boxplot_sql_append_outliers( let quoted_groups: Vec = groups.iter().map(|g| dialect.quote_ident(g)).collect(); let groups_str = quoted_groups.join(", "); - // Helper to build visual-element rows from summary table - // Each row type maps to one visual element with y and yend where needed - let build_summary_select = |table: &str| { - format!( - "SELECT {groups}, 'lower_whisker' AS {type_name}, q1 AS {value_name}, lower AS {value2_name} FROM {table} - UNION ALL - SELECT {groups}, 'upper_whisker' AS {type_name}, q3 AS {value_name}, upper AS {value2_name} FROM {table} - UNION ALL - SELECT {groups}, 'box' AS {type_name}, q1 AS {value_name}, q3 AS {value2_name} FROM {table} - UNION ALL - SELECT {groups}, 'median' AS {type_name}, median AS {value_name}, NULL AS {value2_name} FROM {table}", - groups = groups_str, - type_name = type_name, - value_name = value_name, - value2_name = value2_name, - table = table - ) - }; - if !*draw_outliers { // Build from subquery when no CTEs needed - return build_summary_select(&format!("({})", from)); + return boxplot_summary_rows_select(&format!("({})", from), groups, dialect); } // Grab query for outliers let outliers = boxplot_sql_filter_outliers(groups, value, raw_query, dialect); // Build summary select using CTE reference - let summary_select = build_summary_select("summary"); + let summary_select = boxplot_summary_rows_select("summary", groups, dialect); // Combine summary visual-elements with outliers format!( @@ -414,6 +506,53 @@ mod tests { /// Whitespace-insensitive SQL comparison: collapses whitespace runs and /// drops spaces adjacent to parens, so expectations can wrap and indent /// freely without depending on where line breaks fall. + #[test] + fn test_single_scan_statement_scans_source_once_with_outliers() { + // The whole stats statement (summary + outlier filter) must name the + // source query exactly once so MySQL/MariaDB error 1137 is avoided + // when the source is a temporary table. + use crate::reader::SqlDialect; + let dialect = crate::reader::dialects::MySqlDialect; + let groups = vec!["grp".to_string()]; + let fractions = [(0.25, "q1"), (0.5, "median"), (0.75, "q3")]; + let parts = dialect + .sql_quantiles_single_scan( + "val", + &fractions, + crate::sql::FromItem::Query("SELECT * FROM tmp_src"), + &groups, + ) + .unwrap(); + let sql = boxplot_sql_single_scan(&parts, &groups, &1.5, &true, &dialect); + assert_eq!(sql.matches("tmp_src").count(), 1); + assert!(sql.contains("WITH `__ggsql_bp_w__` AS (")); + assert!(sql.contains("outliers AS (")); + assert!(sql.contains("JOIN summary ON")); + assert!(sql.contains("NOT BETWEEN summary.lower AND summary.upper")); + assert!(sql.contains("'lower_whisker'")); + assert!(sql.contains("'outlier' AS type")); + } + + #[test] + fn test_single_scan_statement_without_outliers() { + use crate::reader::SqlDialect; + let dialect = crate::reader::dialects::MySqlDialect; + let groups = vec!["grp".to_string()]; + let fractions = [(0.25, "q1"), (0.5, "median"), (0.75, "q3")]; + let parts = dialect + .sql_quantiles_single_scan( + "val", + &fractions, + crate::sql::FromItem::Query("SELECT * FROM tmp_src"), + &groups, + ) + .unwrap(); + let sql = boxplot_sql_single_scan(&parts, &groups, &1.5, &false, &dialect); + assert_eq!(sql.matches("tmp_src").count(), 1); + assert!(!sql.contains("outliers")); + assert!(sql.contains("'median'")); + } + fn normalize_sql(s: &str) -> String { s.split_whitespace() .collect::>() diff --git a/src/reader/dialects/mysql.rs b/src/reader/dialects/mysql.rs index 15003dfa..542f3f8f 100644 --- a/src/reader/dialects/mysql.rs +++ b/src/reader/dialects/mysql.rs @@ -68,6 +68,21 @@ impl SqlDialect for MySqlDialect { format!("CAST('00:00:00' + INTERVAL {micros} MICROSECOND AS TIME)") } + fn sql_quantiles_single_scan( + &self, + column: &str, + fractions: &[(f64, &str)], + from: crate::sql::FromItem<'_>, + groups: &[String], + ) -> Option { + // MySQL/MariaDB refuse to open a temporary table twice in one + // statement (error 1137), so stats queries must scan the source + // exactly once. The single-scan shape does that: the source appears + // only inside the windowed-pass CTE, which both backends materialize + // when it is referenced more than once. + Some(self.build_single_scan_quantiles("__ggsql_bp_w__", column, fractions, from, groups)) + } + fn temp_table_style(&self) -> crate::reader::TempTableStyle { crate::reader::TempTableStyle::DropTemporaryThenCreateTemp } @@ -77,6 +92,34 @@ impl SqlDialect for MySqlDialect { mod tests { use super::*; + #[test] + fn single_scan_quantiles_reference_source_once() { + // Error 1137 ("Can't reopen table") forbids naming a temporary table + // twice per statement, so the source query must appear exactly once + // across both returned parts. + let parts = MySqlDialect + .sql_quantiles_single_scan( + "val", + &[(0.25, "q1"), (0.5, "median"), (0.75, "q3")], + crate::sql::FromItem::Table("tmp_src"), + &["grp".to_string()], + ) + .expect("MySQL provides a single-scan summary"); + let combined = format!("{}{}", parts.pass, parts.summary); + assert_eq!(combined.matches("tmp_src").count(), 1); + assert!(parts + .pass + .contains("ROW_NUMBER() OVER (PARTITION BY `grp` ORDER BY `val`)")); + assert!(parts.pass.contains("COUNT(*) OVER (PARTITION BY `grp` )")); + assert!(parts.summary.contains("FROM `__ggsql_bp_w__`")); + assert!(parts.summary.contains("MIN(__val) AS min")); + assert!(parts.summary.contains("MAX(__val) AS max")); + assert!(parts.summary.contains("AS q1")); + assert!(parts.summary.contains("AS median")); + assert!(parts.summary.contains("AS q3")); + assert!(parts.summary.contains("GROUP BY `grp`")); + } + #[test] fn null_safe_eq_uses_spaceship() { assert_eq!( diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 463c5103..ae5dc78b 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -332,6 +332,103 @@ pub trait SqlDialect { format!("({})", outer.build()) } + /// Single-scan alternative to embedding [`sql_quantile`] per fraction. + /// + /// Returns the parts of a grouped summary that computes `min`, `max`, + /// and all requested quantiles while referencing `from` exactly once (in + /// the windowed pass). Callers expose [`SingleScanQuantiles::pass`] as a + /// CTE named [`SingleScanQuantiles::pass_name`] and compose the rest of + /// their statement around it, so backends that forbid referencing a + /// temporary table more than once per statement (MySQL/MariaDB error + /// 1137, "Can't reopen table") still work when the source is a temp + /// table. Both MySQL and MariaDB materialize a multiply-referenced CTE, + /// so the summary and any additional scans reading the CTE do not + /// re-open the underlying temp table. + /// + /// The default returns `None`; callers then fall back to one scalar + /// subquery per fraction via [`sql_quantile`], which is fine on backends + /// without the temp-table restriction. + /// + /// [`sql_quantile`]: SqlDialect::sql_quantile + fn sql_quantiles_single_scan( + &self, + _column: &str, + _fractions: &[(f64, &str)], + _from: crate::sql::FromItem<'_>, + _groups: &[String], + ) -> Option { + None + } + + /// Shared builder for [`sql_quantiles_single_scan`] implementations. + /// Uses the same window-function interpolation math as the default + /// [`sql_quantile`], restructured so a grouped aggregate over one + /// windowed pass yields every fraction at once. + /// + /// [`sql_quantiles_single_scan`]: SqlDialect::sql_quantiles_single_scan + /// [`sql_quantile`]: SqlDialect::sql_quantile + fn build_single_scan_quantiles( + &self, + pass_name: &str, + column: &str, + fractions: &[(f64, &str)], + from: crate::sql::FromItem<'_>, + groups: &[String], + ) -> SingleScanQuantiles { + let quoted_groups: Vec = groups.iter().map(|g| self.quote_ident(g)).collect(); + let groups_str = quoted_groups.join(", "); + let quoted_column = self.quote_ident(column); + let partition_by = if quoted_groups.is_empty() { + String::new() + } else { + format!("PARTITION BY {} ", groups_str) + }; + + let mut pass_items = quoted_groups.clone(); + pass_items.extend([ + format!("{quoted_column} AS __val"), + format!("ROW_NUMBER() OVER ({partition_by}ORDER BY {quoted_column}) AS rn"), + format!("COUNT(*) OVER ({partition_by}) AS cnt"), + ]); + let pass = crate::sql::Select::new(self) + .select_items(&pass_items) + .from_aliased(from, "__ggsql_pct__") + .and_where(format!("{quoted_column} IS NOT NULL")) + .build(); + + let quantile_expr = |fraction: f64| { + let x = format!("{fraction} * (cnt - 1)"); + let lo = format!("1 + FLOOR({x})"); + let hi = format!("1 + {}", self.sql_ceil(&x)); + let frac = format!("{x} - FLOOR({x})"); + format!( + "MAX(CASE WHEN rn = {lo} THEN __val END) + \ + (MAX(CASE WHEN rn = {hi} THEN __val END) - \ + MAX(CASE WHEN rn = {lo} THEN __val END)) * MAX({frac})" + ) + }; + + let mut summary_items = quoted_groups.clone(); + summary_items.push("MIN(__val) AS min".to_string()); + summary_items.push("MAX(__val) AS max".to_string()); + for (fraction, alias) in fractions { + summary_items.push(format!("{} AS {alias}", quantile_expr(*fraction))); + } + let quoted_pass = self.quote_ident(pass_name); + let mut summary = crate::sql::Select::new(self) + .select_items(&summary_items) + .from_aliased(crate::sql::FromItem::Table("ed_pass), "__ggsql_w__"); + if !groups_str.is_empty() { + summary = summary.group_by(&groups_str); + } + + SingleScanQuantiles { + pass_name: pass_name.to_string(), + pass, + summary: summary.build(), + } + } + /// SQL fragment for a simple aggregate function applied to an /// already-quoted column expression. /// @@ -795,6 +892,23 @@ impl TypeNames { }; } +/// Components of a single-scan grouped quantile summary; see +/// [`SqlDialect::sql_quantiles_single_scan`]. +#[derive(Debug, Clone)] +pub struct SingleScanQuantiles { + /// Name under which the caller should expose `pass` as a CTE; `summary` + /// reads from it, and callers may join further scans (e.g. outlier + /// filters) against it without touching the original source again. + pub pass_name: String, + /// Windowed pass over the source, computing per-row group columns, the + /// value (`__val`), its rank (`rn`), and the group size (`cnt`). The + /// source query is referenced exactly once, here. + pub pass: String, + /// Grouped aggregate over `pass_name` producing the group columns, + /// `min`, `max`, and one aliased quantile column per requested fraction. + pub summary: String, +} + /// How a backend (re)creates a temporary table holding a query result; see /// [`SqlDialect::temp_table_style`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/tests/battery/mod.rs b/src/tests/battery/mod.rs index 44238b50..fb4766d7 100644 --- a/src/tests/battery/mod.rs +++ b/src/tests/battery/mod.rs @@ -265,29 +265,17 @@ pub fn cases() -> Vec { Expect::ExactRows(8), ), // Global-SQL source + multi-reference stat: the source is - // materialized as a temp table and the boxplot stats query - // references it many times in one statement. MySQL refuses to - // open a temporary table twice in one query (error 1137, "Can't - // reopen table") — the single-scan minmax query avoids it, but a - // stats query cannot. Skipped on MySQL until the pipeline stops - // materializing global sources into temporary tables there. - Case { - live_skip: &[ - ( - "mysql", - "error 1137: stats query references a temporary table multiple times", - ), - ( - "mariadb", - "error 1137: stats query references a temporary table multiple times", - ), - ], - ..shared( - "boxplot_global_source", - "SELECT * FROM {table} VISUALISE DRAW boxplot MAPPING grp AS x, val AS y", - Expect::MinRows(1), - ) - }, + // materialized as a temp table and the boxplot stats query needs it + // for the summary and the outlier filter. MySQL/MariaDB refuse to + // open a temporary table twice in one statement (error 1137, "Can't + // reopen table"), so their dialects compose the stats query around a + // single-scan windowed pass (see SqlDialect::sql_quantiles_single_scan) + // that names the source exactly once. + shared( + "boxplot_global_source", + "SELECT * FROM {table} VISUALISE DRAW boxplot MAPPING grp AS x, val AS y", + Expect::MinRows(1), + ), shared( "bar_count", "VISUALISE DRAW bar MAPPING grp AS x FROM {table}", From 07d7eaf8ab18dcb6de7a3a14504b923b7f15fe54 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Fri, 9 Oct 2026 12:50:58 +0200 Subject: [PATCH 3/5] extract reused logic --- src/reader/mod.rs | 60 +++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/reader/mod.rs b/src/reader/mod.rs index ae5dc78b..3e0c13d3 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -277,10 +277,7 @@ pub trait SqlDialect { let __ggsql_tile__ = self.quote_ident("__ggsql_tile__"); let quoted_column = self.quote_ident(column); - let x = format!("{fraction} * (cnt - 1)"); - let lo = format!("1 + FLOOR({x})"); - let hi = format!("1 + {}", self.sql_ceil(&x)); - let frac = format!("{x} - FLOOR({x})"); + let interpolation = self.sql_percentile_cont_expr(fraction); // Group correlation belongs in the scalar subquery's own WHERE, not // the windowed derived table's: MariaDB cannot resolve outer-query @@ -320,11 +317,7 @@ pub trait SqlDialect { .build(); let mut outer = crate::sql::Select::new(self) - .select(format!( - "MAX(CASE WHEN rn = {lo} THEN __val END) + \ - (MAX(CASE WHEN rn = {hi} THEN __val END) - \ - MAX(CASE WHEN rn = {lo} THEN __val END)) * MAX({frac})" - )) + .select(interpolation) .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_tile__"); if let Some(filter) = group_filter { outer = outer.and_where(filter); @@ -332,6 +325,30 @@ pub trait SqlDialect { format!("({})", outer.build()) } + /// The `percentile_cont` interpolation expression over a row set that + /// carries `__val` (value), `rn` (1-based rank), and `cnt` (group size) + /// columns, as produced by the windowed passes in [`sql_quantile`] and + /// [`build_single_scan_quantiles`]. Usable both as a scalar SELECT item + /// over the whole pass and as an aggregate in a `GROUP BY` over it. + /// + /// With `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. + /// + /// [`sql_quantile`]: SqlDialect::sql_quantile + /// [`build_single_scan_quantiles`]: SqlDialect::build_single_scan_quantiles + fn sql_percentile_cont_expr(&self, fraction: f64) -> String { + let x = format!("{fraction} * (cnt - 1)"); + let lo = format!("1 + FLOOR({x})"); + let hi = format!("1 + {}", self.sql_ceil(&x)); + let frac = format!("{x} - FLOOR({x})"); + format!( + "MAX(CASE WHEN rn = {lo} THEN __val END) + \ + (MAX(CASE WHEN rn = {hi} THEN __val END) - \ + MAX(CASE WHEN rn = {lo} THEN __val END)) * MAX({frac})" + ) + } + /// Single-scan alternative to embedding [`sql_quantile`] per fraction. /// /// Returns the parts of a grouped summary that computes `min`, `max`, @@ -361,9 +378,11 @@ pub trait SqlDialect { } /// Shared builder for [`sql_quantiles_single_scan`] implementations. - /// Uses the same window-function interpolation math as the default - /// [`sql_quantile`], restructured so a grouped aggregate over one - /// windowed pass yields every fraction at once. + /// Shares the interpolation math with the default [`sql_quantile`] via + /// [`sql_percentile_cont_expr`], restructured so a grouped aggregate + /// over one windowed pass yields every fraction at once. + /// + /// [`sql_percentile_cont_expr`]: SqlDialect::sql_percentile_cont_expr /// /// [`sql_quantiles_single_scan`]: SqlDialect::sql_quantiles_single_scan /// [`sql_quantile`]: SqlDialect::sql_quantile @@ -396,23 +415,14 @@ pub trait SqlDialect { .and_where(format!("{quoted_column} IS NOT NULL")) .build(); - let quantile_expr = |fraction: f64| { - let x = format!("{fraction} * (cnt - 1)"); - let lo = format!("1 + FLOOR({x})"); - let hi = format!("1 + {}", self.sql_ceil(&x)); - let frac = format!("{x} - FLOOR({x})"); - format!( - "MAX(CASE WHEN rn = {lo} THEN __val END) + \ - (MAX(CASE WHEN rn = {hi} THEN __val END) - \ - MAX(CASE WHEN rn = {lo} THEN __val END)) * MAX({frac})" - ) - }; - let mut summary_items = quoted_groups.clone(); summary_items.push("MIN(__val) AS min".to_string()); summary_items.push("MAX(__val) AS max".to_string()); for (fraction, alias) in fractions { - summary_items.push(format!("{} AS {alias}", quantile_expr(*fraction))); + summary_items.push(format!( + "{} AS {alias}", + self.sql_percentile_cont_expr(*fraction) + )); } let quoted_pass = self.quote_ident(pass_name); let mut summary = crate::sql::Select::new(self) From 0541dd2342a0efcecafe7f637bef553b59a5009d Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Fri, 9 Oct 2026 13:32:53 +0200 Subject: [PATCH 4/5] REvert MySQL fix and rely on non-temp tables instead --- src/golden/mysql.sql | 18 ++-- src/plot/layer/geom/boxplot.rs | 151 ++------------------------------- src/reader/connection.rs | 86 ++++++++++++++++++- src/reader/dialects/mysql.rs | 62 ++++---------- src/reader/mod.rs | 142 ++----------------------------- src/tests/battery/mod.rs | 12 +-- 6 files changed, 133 insertions(+), 338 deletions(-) diff --git a/src/golden/mysql.sql b/src/golden/mysql.sql index 1fb8c164..c303eff6 100644 --- a/src/golden/mysql.sql +++ b/src/golden/mysql.sql @@ -12,7 +12,7 @@ SELECT *, `__ggsql_stat_bin` AS `__ggsql_aes_pos1__`, `__ggsql_stat_bin_end` AS -- case: boxplot_grouped SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH `__ggsql_bp_w__` AS ( SELECT `__ggsql_aes_pos1__`, `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL ), summary AS ( SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT `__ggsql_aes_pos1__`, MIN(__val) AS min, MAX(__val) AS max, MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) AS q1, MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) AS median, MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) AS q3 FROM `__ggsql_bp_w__` AS `__ggsql_w__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_fn__` ), outliers AS ( SELECT raw.__val AS value, 'outlier' AS type, raw.`__ggsql_aes_pos1__` AS `__ggsql_aes_pos1__` FROM `__ggsql_bp_w__` AS `raw` JOIN summary ON raw.`__ggsql_aes_pos1__` = summary.`__ggsql_aes_pos1__` WHERE raw.__val 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) AS `__ggsql_stat__`; +SELECT *, `__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 *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q3 FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__`) AS `__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) AS `__ggsql_src__`) AS `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) AS `__ggsql_stat__`; -- case: line_aggregate SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; @@ -25,18 +25,18 @@ WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_fill__`, `val` AS `_ SELECT *, `__ggsql_stat_pos1` AS `__ggsql_aes_pos1__`, `__ggsql_stat_density` AS `__ggsql_aes_pos2__` FROM (WITH RECURSIVE bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG(`__ggsql_aes_pos1__`*`__ggsql_aes_pos1__`) - AVG(`__ggsql_aes_pos1__`)*AVG(`__ggsql_aes_pos1__`)), ((SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos1__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` ORDER BY `__ggsql_aes_pos1__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` ) AS cnt, `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` FROM (SELECT *, `grp` AS `__ggsql_aes_fill__`, `val` AS `__ggsql_aes_pos1__`, `grp` AS `__ggsql_aes_stroke__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos1__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_fill__` <=> `__ggsql_qt__`.`__ggsql_aes_fill__` AND `__ggsql_tile__`.`__ggsql_aes_stroke__` <=> `__ggsql_qt__`.`__ggsql_aes_stroke__`) - (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos1__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` ORDER BY `__ggsql_aes_pos1__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` ) AS cnt, `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` FROM (SELECT *, `grp` AS `__ggsql_aes_fill__`, `val` AS `__ggsql_aes_pos1__`, `grp` AS `__ggsql_aes_stroke__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos1__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_fill__` <=> `__ggsql_qt__`.`__ggsql_aes_fill__` AND `__ggsql_tile__`.`__ggsql_aes_stroke__` <=> `__ggsql_qt__`.`__ggsql_aes_stroke__`)) / 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) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos1__` IS NOT NULL GROUP BY `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` ), data AS ( SELECT `__ggsql_aes_fill__`, `__ggsql_aes_stroke__`, `__ggsql_aes_pos1__` AS val, 1.0 AS weight FROM (SELECT *, `grp` AS `__ggsql_aes_fill__`, `val` AS `__ggsql_aes_pos1__`, `grp` AS `__ggsql_aes_stroke__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_data__` WHERE `__ggsql_aes_pos1__` IS NOT NULL ), `__ggsql_base__`(n) AS (SELECT 0 UNION ALL SELECT n + 1 FROM `__ggsql_base__` WHERE n < 7),`__ggsql_seq__`(n) AS (SELECT CAST(a.n * 64 + b.n * 8 + c.n AS DOUBLE) AS n FROM `__ggsql_base__` a, `__ggsql_base__` b, `__ggsql_base__` c WHERE a.n * 64 + b.n * 8 + c.n < 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), grid AS ( SELECT `__ggsql_aes_fill__`, `__ggsql_aes_stroke__`, (`__ggsql_global__`.min - `__ggsql_global__`.expansion) + (`seq`.n * ((`__ggsql_global__`.max - `__ggsql_global__`.min) + 2 * `__ggsql_global__`.expansion) / 511) AS x FROM global_range AS `__ggsql_global__` CROSS JOIN `__ggsql_seq__` AS `seq` CROSS JOIN (SELECT DISTINCT `__ggsql_aes_fill__`, `__ggsql_aes_stroke__` FROM bandwidth) AS `__ggsql_groups__` ) SELECT `__ggsql_stat_pos1`, `__ggsql_aes_fill__`, `__ggsql_aes_stroke__`, `__ggsql_stat_intensity`, `__ggsql_stat_intensity` / `__norm` AS `__ggsql_stat_density` FROM ( SELECT grid.x AS `__ggsql_stat_pos1`, grid.`__ggsql_aes_fill__` AS `__ggsql_aes_fill__`, grid.`__ggsql_aes_stroke__` AS `__ggsql_aes_stroke__`, SUM(data.weight * ((EXP(-0.5 * (grid.x - data.val) * (grid.x - data.val) / (bandwidth.bw * bandwidth.bw))) * 0.3989422804014327)) / MIN(bandwidth.bw) AS `__ggsql_stat_intensity`, SUM(data.weight) AS `__norm` FROM data INNER JOIN bandwidth ON data.`__ggsql_aes_fill__` <=> bandwidth.`__ggsql_aes_fill__` AND data.`__ggsql_aes_stroke__` <=> bandwidth.`__ggsql_aes_stroke__` INNER JOIN grid ON grid.`__ggsql_aes_fill__` <=> data.`__ggsql_aes_fill__` AND grid.`__ggsql_aes_stroke__` <=> data.`__ggsql_aes_stroke__` GROUP BY grid.x, grid.`__ggsql_aes_fill__`, grid.`__ggsql_aes_stroke__` ) AS `__ggsql_kde__`) AS `__ggsql_stat__`; -- case: line_temporal -DROP TEMPORARY TABLE IF EXISTS `__ggsql_global___`; -CREATE TEMPORARY TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; +DROP TABLE IF EXISTS `__ggsql_global___`; +CREATE TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; SELECT * FROM (SELECT * FROM `__ggsql_global___`) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `day` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__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 *, `day` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__` ORDER BY `__ggsql_aes_pos1__`; -- case: boxplot_global_source -DROP TEMPORARY TABLE IF EXISTS `__ggsql_global___`; -CREATE TEMPORARY TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; +DROP TABLE IF EXISTS `__ggsql_global___`; +CREATE TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; SELECT * FROM (SELECT * FROM `__ggsql_global___`) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH `__ggsql_bp_w__` AS ( SELECT `__ggsql_aes_pos1__`, `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL ), summary AS ( SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT `__ggsql_aes_pos1__`, MIN(__val) AS min, MAX(__val) AS max, MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) AS q1, MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) AS median, MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) AS q3 FROM `__ggsql_bp_w__` AS `__ggsql_w__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_fn__` ), outliers AS ( SELECT raw.__val AS value, 'outlier' AS type, raw.`__ggsql_aes_pos1__` AS `__ggsql_aes_pos1__` FROM `__ggsql_bp_w__` AS `raw` JOIN summary ON raw.`__ggsql_aes_pos1__` = summary.`__ggsql_aes_pos1__` WHERE raw.__val 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) AS `__ggsql_stat__`; +SELECT *, `__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 *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q3 FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__`) AS `__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___`) AS `__ggsql_src__`) AS `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) AS `__ggsql_stat__`; -- case: bar_count SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; @@ -74,8 +74,8 @@ WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `_ SELECT *, `__ggsql_stat_pos2` AS `__ggsql_aes_pos2__`, `__ggsql_stat_density` AS `__ggsql_aes_offset__` FROM (WITH RECURSIVE bandwidth AS ( SELECT 0.9 * LEAST(SQRT(AVG(`__ggsql_aes_pos2__`*`__ggsql_aes_pos2__`) - AVG(`__ggsql_aes_pos2__`)*AVG(`__ggsql_aes_pos2__`)), ((SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) - (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`)) / 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) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__` ), data AS ( SELECT `__ggsql_aes_pos1__`, `__ggsql_aes_pos2__` AS val, 1.0 AS weight FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_data__` WHERE `__ggsql_aes_pos2__` IS NOT NULL ), `__ggsql_base__`(n) AS (SELECT 0 UNION ALL SELECT n + 1 FROM `__ggsql_base__` WHERE n < 7),`__ggsql_seq__`(n) AS (SELECT CAST(a.n * 64 + b.n * 8 + c.n AS DOUBLE) AS n FROM `__ggsql_base__` a, `__ggsql_base__` b, `__ggsql_base__` c WHERE a.n * 64 + b.n * 8 + c.n < 512), global_range AS ( SELECT MIN(x_min) AS min, MAX(x_max) AS max, 3 * MAX(bw) AS expansion FROM bandwidth ), full_grid AS ( SELECT `__ggsql_aes_pos1__`, (`__ggsql_global__`.min - `__ggsql_global__`.expansion) + (`seq`.n * ((`__ggsql_global__`.max - `__ggsql_global__`.min) + 2 * `__ggsql_global__`.expansion) / 511) AS x FROM global_range AS `__ggsql_global__` CROSS JOIN `__ggsql_seq__` AS `seq` CROSS JOIN (SELECT DISTINCT `__ggsql_aes_pos1__` FROM bandwidth) AS `__ggsql_groups__` ), grid AS ( SELECT full_grid.`__ggsql_aes_pos1__` AS `__ggsql_aes_pos1__`, full_grid.x FROM full_grid INNER JOIN bandwidth ON full_grid.`__ggsql_aes_pos1__` <=> 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__` <=> bandwidth.`__ggsql_aes_pos1__` INNER JOIN grid ON grid.`__ggsql_aes_pos1__` <=> data.`__ggsql_aes_pos1__` GROUP BY grid.x, grid.`__ggsql_aes_pos1__` ) AS `__ggsql_kde__`) AS `__ggsql_stat__`; -- case: filter_layer -DROP TEMPORARY TABLE IF EXISTS `__ggsql_global___`; -CREATE TEMPORARY TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; +DROP TABLE IF EXISTS `__ggsql_global___`; +CREATE TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; SELECT * FROM (SELECT * FROM `__ggsql_global___`) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `id` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__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 *, `id` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__` WHERE grp = 'a'; @@ -108,7 +108,7 @@ SELECT *, `__ggsql_stat_pos2` AS `__ggsql_aes_pos2__` FROM (WITH `__ggsql_stat_s -- case: quoted_column SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; -SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH `__ggsql_bp_w__` AS ( SELECT `__ggsql_aes_pos1__`, `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL ), summary AS ( SELECT *, GREATEST(q1 - 1.5 * (q3 - q1), min) AS lower, LEAST(q3 + 1.5 * (q3 - q1), max) AS upper FROM (SELECT `__ggsql_aes_pos1__`, MIN(__val) AS min, MAX(__val) AS max, MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) AS q1, MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) AS median, MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) AS q3 FROM `__ggsql_bp_w__` AS `__ggsql_w__` GROUP BY `__ggsql_aes_pos1__`) AS `__ggsql_fn__` ), outliers AS ( SELECT raw.__val AS value, 'outlier' AS type, raw.`__ggsql_aes_pos1__` AS `__ggsql_aes_pos1__` FROM `__ggsql_bp_w__` AS `raw` JOIN summary ON raw.`__ggsql_aes_pos1__` = summary.`__ggsql_aes_pos1__` WHERE raw.__val 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) AS `__ggsql_stat__`; +SELECT *, `__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 *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q3 FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `mixed Case` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM ggsql_live_test) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__`) AS `__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) AS `__ggsql_src__`) AS `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) AS `__ggsql_stat__`; -- case: partition_by_quoted SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/plot/layer/geom/boxplot.rs b/src/plot/layer/geom/boxplot.rs index 4790959f..6576dd17 100644 --- a/src/plot/layer/geom/boxplot.rs +++ b/src/plot/layer/geom/boxplot.rs @@ -175,31 +175,16 @@ fn stat_boxplot( )); } - // Query for boxplot summary statistics. Dialects that forbid - // re-opening a temporary table in one statement (MySQL/MariaDB) provide - // a single-scan summary composed around a shared windowed pass; the - // fallback scans the source once per quantile. - let fractions = [(0.25, "q1"), (0.5, "median"), (0.75, "q3")]; - let stats_query = match dialect.sql_quantiles_single_scan( - &value_col, - &fractions, - crate::sql::FromItem::Query(&working_query), + // Query for boxplot summary statistics + let summary = boxplot_sql_compute_summary(&working_query, &groups, &value_col, coef, dialect); + let stats_query = boxplot_sql_append_outliers( + &summary, &groups, - ) { - Some(parts) => boxplot_sql_single_scan(&parts, &groups, coef, outliers, dialect), - None => { - let summary = - boxplot_sql_compute_summary(&working_query, &groups, &value_col, coef, dialect); - boxplot_sql_append_outliers( - &summary, - &groups, - &value_col, - &working_query, - outliers, - dialect, - ) - } - }; + &value_col, + &working_query, + outliers, + dialect, + ); let mut stat_columns = vec![ "type".to_string(), @@ -326,77 +311,6 @@ fn boxplot_summary_rows_select(table: &str, groups: &[String], dialect: &dyn Sql ) } -/// Compose the full boxplot stats statement around a single-scan quantile -/// summary (see `SqlDialect::sql_quantiles_single_scan`). The source is -/// referenced exactly once — inside the windowed-pass CTE — which backends -/// that forbid re-opening a temporary table per statement (MySQL/MariaDB -/// error 1137) require. The outlier filter joins against the same pass, so -/// it adds no further scans of the source. -fn boxplot_sql_single_scan( - parts: &crate::reader::SingleScanQuantiles, - groups: &[String], - coef: &f64, - draw_outliers: &bool, - dialect: &dyn SqlDialect, -) -> String { - let quoted_groups: Vec = groups.iter().map(|g| dialect.quote_ident(g)).collect(); - 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 summary_with_bounds = crate::sql::Select::new(dialect) - .select_star_plus( - &[ - format!("{lower_expr} AS lower"), - format!("{upper_expr} AS upper"), - ], - "__ggsql_fn__", - ) - .from_aliased(crate::sql::FromItem::Query(&parts.summary), "__ggsql_fn__") - .build(); - - let summary_select = boxplot_summary_rows_select("summary", groups, dialect); - let quoted_pass = dialect.quote_ident(&parts.pass_name); - let pass_cte = format!("{quoted_pass} AS (\n {}\n)", parts.pass); - let summary_cte = format!("summary AS (\n {summary_with_bounds}\n)"); - - if !*draw_outliers { - return format!("WITH {pass_cte},\n{summary_cte}\n{summary_select}"); - } - - // Outlier rows come from the same windowed pass, joined to the summary - // for the whisker bounds, so the source is still scanned only once. - let mut join_pairs = Vec::new(); - let mut keep_columns = Vec::new(); - for column in groups { - let quoted = dialect.quote_ident(column); - join_pairs.push(format!("raw.{quoted} = summary.{quoted}")); - keep_columns.push(format!("raw.{quoted} AS {quoted}")); - } - let mut items = vec![ - "raw.__val AS value".to_string(), - "'outlier' AS type".to_string(), - ]; - items.extend(keep_columns); - let outliers = crate::sql::Select::new(dialect) - .select_items(&items) - .from_aliased(crate::sql::FromItem::Table("ed_pass), "raw") - .join_raw(&format!("JOIN summary ON {}", join_pairs.join(" AND "))) - .and_where("raw.__val NOT BETWEEN summary.lower AND summary.upper") - .build(); - - let value_name = dialect.quote_ident(&naming::stat_column("value")); - let value2_name = dialect.quote_ident(&naming::stat_column("value2")); - let type_name = dialect.quote_ident(&naming::stat_column("type")); - format!( - "WITH {pass_cte},\n{summary_cte},\noutliers AS (\n {outliers}\n) - {summary_select} - UNION ALL - SELECT {groups_str}, type AS {type_name}, value AS {value_name}, NULL AS {value2_name} - FROM outliers" - ) -} - fn boxplot_sql_append_outliers( from: &str, groups: &[String], @@ -506,53 +420,6 @@ mod tests { /// Whitespace-insensitive SQL comparison: collapses whitespace runs and /// drops spaces adjacent to parens, so expectations can wrap and indent /// freely without depending on where line breaks fall. - #[test] - fn test_single_scan_statement_scans_source_once_with_outliers() { - // The whole stats statement (summary + outlier filter) must name the - // source query exactly once so MySQL/MariaDB error 1137 is avoided - // when the source is a temporary table. - use crate::reader::SqlDialect; - let dialect = crate::reader::dialects::MySqlDialect; - let groups = vec!["grp".to_string()]; - let fractions = [(0.25, "q1"), (0.5, "median"), (0.75, "q3")]; - let parts = dialect - .sql_quantiles_single_scan( - "val", - &fractions, - crate::sql::FromItem::Query("SELECT * FROM tmp_src"), - &groups, - ) - .unwrap(); - let sql = boxplot_sql_single_scan(&parts, &groups, &1.5, &true, &dialect); - assert_eq!(sql.matches("tmp_src").count(), 1); - assert!(sql.contains("WITH `__ggsql_bp_w__` AS (")); - assert!(sql.contains("outliers AS (")); - assert!(sql.contains("JOIN summary ON")); - assert!(sql.contains("NOT BETWEEN summary.lower AND summary.upper")); - assert!(sql.contains("'lower_whisker'")); - assert!(sql.contains("'outlier' AS type")); - } - - #[test] - fn test_single_scan_statement_without_outliers() { - use crate::reader::SqlDialect; - let dialect = crate::reader::dialects::MySqlDialect; - let groups = vec!["grp".to_string()]; - let fractions = [(0.25, "q1"), (0.5, "median"), (0.75, "q3")]; - let parts = dialect - .sql_quantiles_single_scan( - "val", - &fractions, - crate::sql::FromItem::Query("SELECT * FROM tmp_src"), - &groups, - ) - .unwrap(); - let sql = boxplot_sql_single_scan(&parts, &groups, &1.5, &false, &dialect); - assert_eq!(sql.matches("tmp_src").count(), 1); - assert!(!sql.contains("outliers")); - assert!(sql.contains("'median'")); - } - fn normalize_sql(s: &str) -> String { s.split_whitespace() .collect::>() diff --git a/src/reader/connection.rs b/src/reader/connection.rs index 319dffb8..faf0fe5d 100644 --- a/src/reader/connection.rs +++ b/src/reader/connection.rs @@ -545,7 +545,19 @@ 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 created = stmts.iter().all(|s| reader.execute_sql(s).is_ok()); + // Creating a temp table is not enough: ggsql's stats queries reference + // internal tables more than once per statement, which some backends + // forbid for temporary tables (MySQL/MariaDB error 1137, "Can't reopen + // table"). Probe a double reference as well so those backends fall back + // to the in-memory cache. + let quoted = dialect.quote_ident(&probe); + let ok = created + && reader + .execute_sql(&format!( + "SELECT a.x FROM {quoted} AS a JOIN {quoted} AS b ON a.x = b.x" + )) + .is_ok(); let _ = reader.execute_sql(&dialect.drop_table_sql(&probe)); ok } @@ -735,6 +747,78 @@ mod tests { assert_eq!(spec.metadata().rows, 1); } + #[cfg(feature = "duckdb")] + use crate::reader::{execute_with_reader, ColumnInfo, Spec, SqlDialect, TableInfo}; + #[cfg(feature = "duckdb")] + use crate::DataFrame; + + /// Simulates backends (MySQL/MariaDB, error 1137) that create temporary + /// tables fine but refuse to open one twice in a single statement. Any + /// SQL naming the probe table more than once fails. + #[cfg(feature = "duckdb")] + struct SingleOpenReader { + inner: Box, + } + + #[cfg(feature = "duckdb")] + impl SingleOpenReader { + fn new(inner: Box) -> Self { + Self { inner } + } + } + + #[cfg(feature = "duckdb")] + impl Reader for SingleOpenReader { + fn execute_sql(&self, sql: &str) -> Result { + if sql.matches("__ggsql_probe_").count() > 1 { + return Err(GgsqlError::ReaderError( + "error 1137: Can't reopen table".to_string(), + )); + } + self.inner.execute_sql(sql) + } + fn register(&self, name: &str, df: DataFrame, replace: bool) -> Result<()> { + self.inner.register(name, df, replace) + } + fn unregister(&self, name: &str) -> Result<()> { + self.inner.unregister(name) + } + fn execute(&self, query: &str) -> Result { + execute_with_reader(self, query) + } + fn dialect(&self) -> &dyn SqlDialect { + self.inner.dialect() + } + fn list_catalogs(&self) -> Result> { + self.inner.list_catalogs() + } + fn list_schemas(&self, c: &str) -> Result> { + self.inner.list_schemas(c) + } + fn list_tables(&self, c: &str, s: &str) -> Result> { + self.inner.list_tables(c, s) + } + fn list_columns(&self, c: &str, s: &str, t: &str) -> Result> { + self.inner.list_columns(c, s, t) + } + } + + #[cfg(feature = "duckdb")] + #[test] + fn test_auto_cache_wraps_when_temp_table_cannot_reopen() { + use crate::reader::duckdb::DuckDBReader; + + let primary = DuckDBReader::from_connection_string("duckdb://memory").unwrap(); + let conn = ConnUri::parse("mysql://u@h/db").unwrap(); + let reader = + auto_cache_if_needed(Box::new(SingleOpenReader::new(Box::new(primary))), &conn) + .unwrap(); + assert!( + reader.caches_sources(), + "expected a caching reader when temp tables cannot be re-opened" + ); + } + #[cfg(feature = "duckdb")] #[test] fn test_auto_cache_skips_writable_and_opted_out() { diff --git a/src/reader/dialects/mysql.rs b/src/reader/dialects/mysql.rs index 542f3f8f..95fb4ab4 100644 --- a/src/reader/dialects/mysql.rs +++ b/src/reader/dialects/mysql.rs @@ -68,23 +68,16 @@ impl SqlDialect for MySqlDialect { format!("CAST('00:00:00' + INTERVAL {micros} MICROSECOND AS TIME)") } - fn sql_quantiles_single_scan( - &self, - column: &str, - fractions: &[(f64, &str)], - from: crate::sql::FromItem<'_>, - groups: &[String], - ) -> Option { - // MySQL/MariaDB refuse to open a temporary table twice in one - // statement (error 1137), so stats queries must scan the source - // exactly once. The single-scan shape does that: the source appears - // only inside the windowed-pass CTE, which both backends materialize - // when it is referenced more than once. - Some(self.build_single_scan_quantiles("__ggsql_bp_w__", column, fractions, from, groups)) - } - fn temp_table_style(&self) -> crate::reader::TempTableStyle { - crate::reader::TempTableStyle::DropTemporaryThenCreateTemp + // MySQL/MariaDB refuse to open a *temporary* table twice in one + // statement (error 1137, "Can't reopen table"), which ggsql's + // multi-reference stats queries (boxplot, density, …) require. + // Regular tables have no such restriction, so internal + // materialization uses plain CREATE TABLE — the same approach as + // MSSQL/Oracle — relying on per-session names and drop-before-create + // for hygiene. Accounts without CREATE privilege fail the temp-table + // probe and fall back to the in-memory cache instead. + crate::reader::TempTableStyle::DropThenCreate } } @@ -92,34 +85,6 @@ impl SqlDialect for MySqlDialect { mod tests { use super::*; - #[test] - fn single_scan_quantiles_reference_source_once() { - // Error 1137 ("Can't reopen table") forbids naming a temporary table - // twice per statement, so the source query must appear exactly once - // across both returned parts. - let parts = MySqlDialect - .sql_quantiles_single_scan( - "val", - &[(0.25, "q1"), (0.5, "median"), (0.75, "q3")], - crate::sql::FromItem::Table("tmp_src"), - &["grp".to_string()], - ) - .expect("MySQL provides a single-scan summary"); - let combined = format!("{}{}", parts.pass, parts.summary); - assert_eq!(combined.matches("tmp_src").count(), 1); - assert!(parts - .pass - .contains("ROW_NUMBER() OVER (PARTITION BY `grp` ORDER BY `val`)")); - assert!(parts.pass.contains("COUNT(*) OVER (PARTITION BY `grp` )")); - assert!(parts.summary.contains("FROM `__ggsql_bp_w__`")); - assert!(parts.summary.contains("MIN(__val) AS min")); - assert!(parts.summary.contains("MAX(__val) AS max")); - assert!(parts.summary.contains("AS q1")); - assert!(parts.summary.contains("AS median")); - assert!(parts.summary.contains("AS q3")); - assert!(parts.summary.contains("GROUP BY `grp`")); - } - #[test] fn null_safe_eq_uses_spaceship() { assert_eq!( @@ -141,10 +106,13 @@ mod tests { } #[test] - fn temp_table_uses_temporary() { + fn internal_tables_use_regular_create_table() { + // Regular tables, not TEMPORARY: a temp table cannot be opened twice + // in one statement (error 1137), which multi-reference stats queries + // need. let stmts = MySqlDialect.create_or_replace_temp_table_sql("t", &[], "SELECT 1"); assert_eq!(stmts.len(), 2); - assert_eq!(stmts[0], "DROP TEMPORARY TABLE IF EXISTS `t`"); - assert_eq!(stmts[1], "CREATE TEMPORARY TABLE `t` AS SELECT 1"); + assert_eq!(stmts[0], "DROP TABLE IF EXISTS `t`"); + assert_eq!(stmts[1], "CREATE TABLE `t` AS SELECT 1"); } } diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 3e0c13d3..463c5103 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -277,7 +277,10 @@ pub trait SqlDialect { let __ggsql_tile__ = self.quote_ident("__ggsql_tile__"); let quoted_column = self.quote_ident(column); - let interpolation = self.sql_percentile_cont_expr(fraction); + let x = format!("{fraction} * (cnt - 1)"); + let lo = format!("1 + FLOOR({x})"); + let hi = format!("1 + {}", self.sql_ceil(&x)); + let frac = format!("{x} - FLOOR({x})"); // Group correlation belongs in the scalar subquery's own WHERE, not // the windowed derived table's: MariaDB cannot resolve outer-query @@ -317,7 +320,11 @@ pub trait SqlDialect { .build(); let mut outer = crate::sql::Select::new(self) - .select(interpolation) + .select(format!( + "MAX(CASE WHEN rn = {lo} THEN __val END) + \ + (MAX(CASE WHEN rn = {hi} THEN __val END) - \ + MAX(CASE WHEN rn = {lo} THEN __val END)) * MAX({frac})" + )) .from_aliased(crate::sql::FromItem::Query(&inner), "__ggsql_tile__"); if let Some(filter) = group_filter { outer = outer.and_where(filter); @@ -325,120 +332,6 @@ pub trait SqlDialect { format!("({})", outer.build()) } - /// The `percentile_cont` interpolation expression over a row set that - /// carries `__val` (value), `rn` (1-based rank), and `cnt` (group size) - /// columns, as produced by the windowed passes in [`sql_quantile`] and - /// [`build_single_scan_quantiles`]. Usable both as a scalar SELECT item - /// over the whole pass and as an aggregate in a `GROUP BY` over it. - /// - /// With `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. - /// - /// [`sql_quantile`]: SqlDialect::sql_quantile - /// [`build_single_scan_quantiles`]: SqlDialect::build_single_scan_quantiles - fn sql_percentile_cont_expr(&self, fraction: f64) -> String { - let x = format!("{fraction} * (cnt - 1)"); - let lo = format!("1 + FLOOR({x})"); - let hi = format!("1 + {}", self.sql_ceil(&x)); - let frac = format!("{x} - FLOOR({x})"); - format!( - "MAX(CASE WHEN rn = {lo} THEN __val END) + \ - (MAX(CASE WHEN rn = {hi} THEN __val END) - \ - MAX(CASE WHEN rn = {lo} THEN __val END)) * MAX({frac})" - ) - } - - /// Single-scan alternative to embedding [`sql_quantile`] per fraction. - /// - /// Returns the parts of a grouped summary that computes `min`, `max`, - /// and all requested quantiles while referencing `from` exactly once (in - /// the windowed pass). Callers expose [`SingleScanQuantiles::pass`] as a - /// CTE named [`SingleScanQuantiles::pass_name`] and compose the rest of - /// their statement around it, so backends that forbid referencing a - /// temporary table more than once per statement (MySQL/MariaDB error - /// 1137, "Can't reopen table") still work when the source is a temp - /// table. Both MySQL and MariaDB materialize a multiply-referenced CTE, - /// so the summary and any additional scans reading the CTE do not - /// re-open the underlying temp table. - /// - /// The default returns `None`; callers then fall back to one scalar - /// subquery per fraction via [`sql_quantile`], which is fine on backends - /// without the temp-table restriction. - /// - /// [`sql_quantile`]: SqlDialect::sql_quantile - fn sql_quantiles_single_scan( - &self, - _column: &str, - _fractions: &[(f64, &str)], - _from: crate::sql::FromItem<'_>, - _groups: &[String], - ) -> Option { - None - } - - /// Shared builder for [`sql_quantiles_single_scan`] implementations. - /// Shares the interpolation math with the default [`sql_quantile`] via - /// [`sql_percentile_cont_expr`], restructured so a grouped aggregate - /// over one windowed pass yields every fraction at once. - /// - /// [`sql_percentile_cont_expr`]: SqlDialect::sql_percentile_cont_expr - /// - /// [`sql_quantiles_single_scan`]: SqlDialect::sql_quantiles_single_scan - /// [`sql_quantile`]: SqlDialect::sql_quantile - fn build_single_scan_quantiles( - &self, - pass_name: &str, - column: &str, - fractions: &[(f64, &str)], - from: crate::sql::FromItem<'_>, - groups: &[String], - ) -> SingleScanQuantiles { - let quoted_groups: Vec = groups.iter().map(|g| self.quote_ident(g)).collect(); - let groups_str = quoted_groups.join(", "); - let quoted_column = self.quote_ident(column); - let partition_by = if quoted_groups.is_empty() { - String::new() - } else { - format!("PARTITION BY {} ", groups_str) - }; - - let mut pass_items = quoted_groups.clone(); - pass_items.extend([ - format!("{quoted_column} AS __val"), - format!("ROW_NUMBER() OVER ({partition_by}ORDER BY {quoted_column}) AS rn"), - format!("COUNT(*) OVER ({partition_by}) AS cnt"), - ]); - let pass = crate::sql::Select::new(self) - .select_items(&pass_items) - .from_aliased(from, "__ggsql_pct__") - .and_where(format!("{quoted_column} IS NOT NULL")) - .build(); - - let mut summary_items = quoted_groups.clone(); - summary_items.push("MIN(__val) AS min".to_string()); - summary_items.push("MAX(__val) AS max".to_string()); - for (fraction, alias) in fractions { - summary_items.push(format!( - "{} AS {alias}", - self.sql_percentile_cont_expr(*fraction) - )); - } - let quoted_pass = self.quote_ident(pass_name); - let mut summary = crate::sql::Select::new(self) - .select_items(&summary_items) - .from_aliased(crate::sql::FromItem::Table("ed_pass), "__ggsql_w__"); - if !groups_str.is_empty() { - summary = summary.group_by(&groups_str); - } - - SingleScanQuantiles { - pass_name: pass_name.to_string(), - pass, - summary: summary.build(), - } - } - /// SQL fragment for a simple aggregate function applied to an /// already-quoted column expression. /// @@ -902,23 +795,6 @@ impl TypeNames { }; } -/// Components of a single-scan grouped quantile summary; see -/// [`SqlDialect::sql_quantiles_single_scan`]. -#[derive(Debug, Clone)] -pub struct SingleScanQuantiles { - /// Name under which the caller should expose `pass` as a CTE; `summary` - /// reads from it, and callers may join further scans (e.g. outlier - /// filters) against it without touching the original source again. - pub pass_name: String, - /// Windowed pass over the source, computing per-row group columns, the - /// value (`__val`), its rank (`rn`), and the group size (`cnt`). The - /// source query is referenced exactly once, here. - pub pass: String, - /// Grouped aggregate over `pass_name` producing the group columns, - /// `min`, `max`, and one aliased quantile column per requested fraction. - pub summary: String, -} - /// How a backend (re)creates a temporary table holding a query result; see /// [`SqlDialect::temp_table_style`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/tests/battery/mod.rs b/src/tests/battery/mod.rs index fb4766d7..d793222b 100644 --- a/src/tests/battery/mod.rs +++ b/src/tests/battery/mod.rs @@ -265,12 +265,12 @@ pub fn cases() -> Vec { Expect::ExactRows(8), ), // Global-SQL source + multi-reference stat: the source is - // materialized as a temp table and the boxplot stats query needs it - // for the summary and the outlier filter. MySQL/MariaDB refuse to - // open a temporary table twice in one statement (error 1137, "Can't - // reopen table"), so their dialects compose the stats query around a - // single-scan windowed pass (see SqlDialect::sql_quantiles_single_scan) - // that names the source exactly once. + // materialized as an internal table and the boxplot stats query + // references it several times in one statement. MySQL/MariaDB refuse + // to open a *temporary* table twice in one statement (error 1137, + // "Can't reopen table"), so their dialect materializes internal + // tables as regular tables (TempTableStyle::DropThenCreate), which + // have no such restriction. shared( "boxplot_global_source", "SELECT * FROM {table} VISUALISE DRAW boxplot MAPPING grp AS x, val AS y", From e06a4608944aed490501be14402c62fff1d6fb28 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Fri, 9 Oct 2026 14:21:04 +0200 Subject: [PATCH 5/5] Cleanup after non-temp-tables --- src/execute/mod.rs | 19 +++++++++++++++++++ src/golden/mssql.sql | 3 +++ src/golden/mysql.sql | 3 +++ src/golden/oracle.sql | 3 +++ src/reader/dialects/datafusion.rs | 5 +++++ src/reader/dialects/mod.rs | 30 ++++++++++++++++++++++++++++++ src/reader/mod.rs | 19 +++++++++++++++++++ 7 files changed, 82 insertions(+) diff --git a/src/execute/mod.rs b/src/execute/mod.rs index 0a9d3bee..499d7f06 100644 --- a/src/execute/mod.rs +++ b/src/execute/mod.rs @@ -1628,6 +1628,25 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result = materialized_ctes + .iter() + .map(|name| naming::cte_table(name)) + .collect(); + if has_global_table { + tables.push(naming::global_table()); + } + for table in tables { + let _ = reader.execute_sql(&dialect.drop_table_sql(&table)); + } + } + Ok(PreparedData { data: data_map, specs, diff --git a/src/golden/mssql.sql b/src/golden/mssql.sql index 01e1c136..48dc02d6 100644 --- a/src/golden/mssql.sql +++ b/src/golden/mssql.sql @@ -30,6 +30,7 @@ SELECT * INTO "__ggsql_global___" FROM (SELECT * FROM ggsql_live_test) SELECT TOP 1 * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") AS "__schema__") AS "__ggsql_lim__"; WITH "__ggsql_source__" AS (SELECT *, "day" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") AS "__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___") AS "__ggsql_src__" ORDER BY "__ggsql_aes_pos1__"; +DROP TABLE IF EXISTS "__ggsql_global___"; -- case: boxplot_global_source DROP TABLE IF EXISTS "__ggsql_global___"; @@ -37,6 +38,7 @@ SELECT * INTO "__ggsql_global___" FROM (SELECT * FROM ggsql_live_test) SELECT TOP 1 * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") AS "__schema__") AS "__ggsql_lim__"; WITH "__ggsql_source__" AS (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") AS "__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__"; WITH summary AS ( SELECT *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEILING(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT "__ggsql_aes_pos2__" AS __val, ROW_NUMBER() OVER (PARTITION BY "__ggsql_aes_pos1__" ORDER BY "__ggsql_aes_pos2__") AS rn, COUNT(*) OVER (PARTITION BY "__ggsql_aes_pos1__" ) AS cnt, "__ggsql_aes_pos1__" FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") AS "__ggsql_src__") AS "__ggsql_pct__" WHERE "__ggsql_aes_pos2__" IS NOT NULL) AS "__ggsql_tile__" WHERE ("__ggsql_tile__"."__ggsql_aes_pos1__" IS NOT DISTINCT FROM "__ggsql_qt__"."__ggsql_aes_pos1__")) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEILING(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT "__ggsql_aes_pos2__" AS __val, ROW_NUMBER() OVER (PARTITION BY "__ggsql_aes_pos1__" ORDER BY "__ggsql_aes_pos2__") AS rn, COUNT(*) OVER (PARTITION BY "__ggsql_aes_pos1__" ) AS cnt, "__ggsql_aes_pos1__" FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") AS "__ggsql_src__") AS "__ggsql_pct__" WHERE "__ggsql_aes_pos2__" IS NOT NULL) AS "__ggsql_tile__" WHERE ("__ggsql_tile__"."__ggsql_aes_pos1__" IS NOT DISTINCT FROM "__ggsql_qt__"."__ggsql_aes_pos1__")) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEILING(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT "__ggsql_aes_pos2__" AS __val, ROW_NUMBER() OVER (PARTITION BY "__ggsql_aes_pos1__" ORDER BY "__ggsql_aes_pos2__") AS rn, COUNT(*) OVER (PARTITION BY "__ggsql_aes_pos1__" ) AS cnt, "__ggsql_aes_pos1__" FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") AS "__ggsql_src__") AS "__ggsql_pct__" WHERE "__ggsql_aes_pos2__" IS NOT NULL) AS "__ggsql_tile__" WHERE ("__ggsql_tile__"."__ggsql_aes_pos1__" IS NOT DISTINCT FROM "__ggsql_qt__"."__ggsql_aes_pos1__")) AS q3 FROM (SELECT *, "grp" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") AS "__ggsql_src__") AS "__ggsql_qt__" WHERE "__ggsql_aes_pos2__" IS NOT NULL GROUP BY "__ggsql_aes_pos1__") AS "__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___") AS "__ggsql_src__") AS "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_stat_type" AS "__ggsql_aes_type__", "__ggsql_stat_value" AS "__ggsql_aes_pos2__", "__ggsql_stat_value2" AS "__ggsql_aes_pos2end__" FROM (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) AS "__ggsql_stat__"; +DROP TABLE IF EXISTS "__ggsql_global___"; -- case: bar_count SELECT TOP 1 * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__") AS "__ggsql_lim__"; @@ -79,6 +81,7 @@ SELECT * INTO "__ggsql_global___" FROM (SELECT * FROM ggsql_live_test) SELECT TOP 1 * FROM (SELECT * FROM (SELECT * FROM "__ggsql_global___") AS "__schema__") AS "__ggsql_lim__"; WITH "__ggsql_source__" AS (SELECT *, "id" AS "__ggsql_aes_pos1__", "val" AS "__ggsql_aes_pos2__" FROM (SELECT * FROM "__ggsql_global___") AS "__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___") AS "__ggsql_src__" WHERE grp = 'a'; +DROP TABLE IF EXISTS "__ggsql_global___"; -- case: binned_temporal SELECT TOP 1 * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) AS "__schema__") AS "__ggsql_lim__"; diff --git a/src/golden/mysql.sql b/src/golden/mysql.sql index c303eff6..4e263132 100644 --- a/src/golden/mysql.sql +++ b/src/golden/mysql.sql @@ -30,6 +30,7 @@ CREATE TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; SELECT * FROM (SELECT * FROM `__ggsql_global___`) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `day` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__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 *, `day` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__` ORDER BY `__ggsql_aes_pos1__`; +DROP TABLE IF EXISTS `__ggsql_global___`; -- case: boxplot_global_source DROP TABLE IF EXISTS `__ggsql_global___`; @@ -37,6 +38,7 @@ CREATE TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; SELECT * FROM (SELECT * FROM `__ggsql_global___`) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) SELECT MIN(`id`) AS `__ggsql_min_id`, MAX(`id`) AS `__ggsql_max_id`, MIN(`val`) AS `__ggsql_min_val`, MAX(`val`) AS `__ggsql_max_val`, MIN(`grp`) AS `__ggsql_min_grp`, MAX(`grp`) AS `__ggsql_max_grp`, MIN(`day`) AS `__ggsql_min_day`, MAX(`day`) AS `__ggsql_max_day`, MIN(`mixed Case`) AS `__ggsql_min_mixed Case`, MAX(`mixed Case`) AS `__ggsql_max_mixed Case` FROM `__ggsql_source__`; SELECT *, `__ggsql_stat_type` AS `__ggsql_aes_type__`, `__ggsql_stat_value` AS `__ggsql_aes_pos2__`, `__ggsql_stat_value2` AS `__ggsql_aes_pos2end__` FROM (WITH summary AS ( SELECT *, 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, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.25 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.25 * (cnt - 1)) THEN __val END)) * MAX(0.25 * (cnt - 1) - FLOOR(0.25 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q1, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.5 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.5 * (cnt - 1)) THEN __val END)) * MAX(0.5 * (cnt - 1) - FLOOR(0.5 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS median, (SELECT MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END) + (MAX(CASE WHEN rn = 1 + CEIL(0.75 * (cnt - 1)) THEN __val END) - MAX(CASE WHEN rn = 1 + FLOOR(0.75 * (cnt - 1)) THEN __val END)) * MAX(0.75 * (cnt - 1) - FLOOR(0.75 * (cnt - 1))) FROM (SELECT `__ggsql_aes_pos2__` AS __val, ROW_NUMBER() OVER (PARTITION BY `__ggsql_aes_pos1__` ORDER BY `__ggsql_aes_pos2__`) AS rn, COUNT(*) OVER (PARTITION BY `__ggsql_aes_pos1__` ) AS cnt, `__ggsql_aes_pos1__` FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_pct__` WHERE `__ggsql_aes_pos2__` IS NOT NULL) AS `__ggsql_tile__` WHERE `__ggsql_tile__`.`__ggsql_aes_pos1__` <=> `__ggsql_qt__`.`__ggsql_aes_pos1__`) AS q3 FROM (SELECT *, `grp` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__`) AS `__ggsql_qt__` WHERE `__ggsql_aes_pos2__` IS NOT NULL GROUP BY `__ggsql_aes_pos1__`) AS `__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___`) AS `__ggsql_src__`) AS `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) AS `__ggsql_stat__`; +DROP TABLE IF EXISTS `__ggsql_global___`; -- case: bar_count SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; @@ -79,6 +81,7 @@ CREATE TABLE `__ggsql_global___` AS SELECT * FROM ggsql_live_test; SELECT * FROM (SELECT * FROM `__ggsql_global___`) AS `__schema__` LIMIT 1; WITH `__ggsql_source__` AS (SELECT *, `id` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__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 *, `id` AS `__ggsql_aes_pos1__`, `val` AS `__ggsql_aes_pos2__` FROM (SELECT * FROM `__ggsql_global___`) AS `__ggsql_src__` WHERE grp = 'a'; +DROP TABLE IF EXISTS `__ggsql_global___`; -- case: binned_temporal SELECT * FROM (SELECT * FROM ggsql_live_test) AS `__schema__` LIMIT 1; diff --git a/src/golden/oracle.sql b/src/golden/oracle.sql index 6a50dd8b..7362a07d 100644 --- a/src/golden/oracle.sql +++ b/src/golden/oracle.sql @@ -30,6 +30,7 @@ CREATE TABLE "__ggsql_global___" AS SELECT * FROM ggsql_live_test; 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__"; +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; -- case: boxplot_global_source BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; @@ -37,6 +38,7 @@ CREATE TABLE "__ggsql_global___" AS SELECT * FROM ggsql_live_test; 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__"; +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; -- case: bar_count SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; @@ -79,6 +81,7 @@ CREATE TABLE "__ggsql_global___" AS SELECT * FROM ggsql_live_test; 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'; +BEGIN EXECUTE IMMEDIATE 'DROP TABLE "__ggsql_global___"'; EXCEPTION WHEN OTHERS THEN NULL; END;; -- case: binned_temporal SELECT * FROM (SELECT * FROM (SELECT * FROM ggsql_live_test) "__schema__") "__ggsql_lim__" WHERE ROWNUM <= 1; diff --git a/src/reader/dialects/datafusion.rs b/src/reader/dialects/datafusion.rs index 5bd45f1d..cd843cff 100644 --- a/src/reader/dialects/datafusion.rs +++ b/src/reader/dialects/datafusion.rs @@ -31,6 +31,11 @@ impl SqlDialect for DataFusionDialect { crate::reader::TempTableStyle::DropThenCreate } + fn internal_tables_need_cleanup(&self) -> bool { + // DataFusion runs in-process; its tables vanish with the reader. + false + } + fn sql_generate_series(&self, n: usize) -> String { // DataFusion's generate_series is a table function whose single // output column is named `value`. diff --git a/src/reader/dialects/mod.rs b/src/reader/dialects/mod.rs index 3ae8a619..b8da6cda 100644 --- a/src/reader/dialects/mod.rs +++ b/src/reader/dialects/mod.rs @@ -253,6 +253,36 @@ mod tests { assert!(dialect_for_scheme("nosuchdb").is_none()); } + /// Persistent internal tables (regular CREATE TABLE materialization) + /// need explicit cleanup; genuine temp tables and in-process engines do + /// not. + #[test] + fn internal_tables_need_cleanup_only_for_persistent_dialects() { + for scheme in ["mysql", "mariadb", "mssql", "oracle"] { + assert!( + dialect_for_scheme(scheme) + .unwrap() + .internal_tables_need_cleanup(), + "scheme {scheme} should need cleanup" + ); + } + for scheme in [ + "postgres", + "duckdb", + "sqlite", + "datafusion", + "clickhouse", + "bigquery", + ] { + assert!( + !dialect_for_scheme(scheme) + .unwrap() + .internal_tables_need_cleanup(), + "scheme {scheme} should not need cleanup" + ); + } + } + #[test] fn per_dialect_quoting() { assert_eq!(dialect_for_scheme("mysql").unwrap().quote_ident("c"), "`c`"); diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 463c5103..aadc32b1 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -137,6 +137,25 @@ pub trait SqlDialect { TempTableStyle::DropThenCreateTemp } + /// Whether internal materialized tables (global source, CTEs) persist + /// beyond the session and must be dropped explicitly once execution + /// finishes with them. Defaults from [`temp_table_style`]: genuine + /// temporary tables are reclaimed by the backend at disconnect, while + /// regular tables linger. In-process engines (DataFusion) override to + /// `false` — their tables die with the reader. Caching readers report + /// the cache dialect here, so staged tables in the in-memory cache never + /// require cleanup either. + /// + /// [`temp_table_style`]: SqlDialect::temp_table_style + fn internal_tables_need_cleanup(&self) -> bool { + matches!( + self.temp_table_style(), + TempTableStyle::DropThenCreate + | TempTableStyle::GuardedDropThenCreate + | TempTableStyle::SelectInto + ) + } + // ===================================================================== // Types and identifiers // =====================================================================