diff --git a/CHANGELOG.md b/CHANGELOG.md index 0544d49b..93826705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Fixed +- Side effects like `CREATE TEMP TABLE` before the `VISUALISE` statement are now + separated from directly feeding into the visualisation data (#415) - Fixed bug where panel axes were unintentionally anchored to zero when using `FACET ... SETTING free => 'x'/'y'` (#410). diff --git a/src/execute/cte.rs b/src/execute/cte.rs index 0ab34848..9b544988 100644 --- a/src/execute/cte.rs +++ b/src/execute/cte.rs @@ -210,35 +210,79 @@ pub fn split_with_query(source_tree: &SourceTree) -> Option<(String, String)> { Some((cte_prefix, trailing)) } -/// Transform global SQL for execution with temp tables +/// Transform global SQL for execution with temp tables. /// -/// If the SQL has a WITH clause followed by SELECT, extracts just the SELECT -/// portion and transforms CTE references to temp table names. -/// For SQL without WITH clause, just transforms any CTE references. +/// Returns statements to execute directly as side effects (CREATE, INSERT, …) +/// and an optional query whose result should be wrapped as the global temp +/// table. pub fn transform_global_sql( source_tree: &SourceTree, materialized_ctes: &HashSet, -) -> Option { +) -> (Vec, Option) { + // Collect side-effect statements (CREATE, INSERT, UPDATE, DELETE) that + // need to run before the main query. These appear alongside a trailing + // SELECT or VISUALISE FROM. + // + // Only structured DML is handled here — other_sql_statement nodes + // (INSTALL, LOAD, SET, …) are pre-executed in prepare_data_with_reader. + let root = source_tree.root(); + + let side_effect_stmts = r#" + (sql_statement + [(create_statement) + (insert_statement) + (update_statement) + (delete_statement)] @stmt) + "#; + let side_effects: Vec = source_tree + .find_texts(&root, side_effect_stmts) + .into_iter() + .map(|s| transform_cte_references(s.trim(), materialized_ctes)) + .filter(|s| !s.is_empty()) + .collect(); + // Try to extract trailing SELECT (WITH...SELECT or direct SELECT) let select_sql = split_with_query(source_tree) .map(|(_, select)| select) .or_else(|| { // Fallback: direct SELECT statement (no WITH clause) - let root = source_tree.root(); source_tree.find_text(&root, "(sql_statement (select_statement) @select)") }); if let Some(select_sql) = select_sql { - Some(transform_cte_references(&select_sql, materialized_ctes)) - } else if does_consume_cte(source_tree) { - // Non-SELECT executable SQL (CREATE, INSERT, UPDATE, DELETE) - // OR VISUALISE FROM (which injects SELECT * FROM ) - // Extract SQL (with injection if VISUALISE FROM) and transform CTE references - let sql = source_tree.extract_sql()?; - Some(transform_cte_references(&sql, materialized_ctes)) + return ( + side_effects, + Some(transform_cte_references(&select_sql, materialized_ctes)), + ); + } + + if !has_executable_sql(source_tree) { + return (vec![], None); + } + + // We have non-SELECT executable SQL and/or VISUALISE FROM. + // Side-effects run directly, VISUALISE FROM becomes the queryable part. + // A bare WITH clause without a trailing statement is not executable on + // its own (its CTEs are already materialized separately). + let viz_from_query = source_tree + .find_text( + &root, + r#"(visualise_statement (from_clause (table_ref) @table))"#, + ) + .map(|table| { + let q = format!("SELECT * FROM {}", table); + transform_cte_references(&q, materialized_ctes) + }); + + if !side_effects.is_empty() || viz_from_query.is_some() { + (side_effects, viz_from_query) } else { - // No executable SQL (just CTEs) - None + // has_executable_sql was true but we found no specific statements or + // VISUALISE FROM — fall back to extract_sql as the query. + let query = source_tree + .extract_sql() + .map(|s| transform_cte_references(&s, materialized_ctes)); + (vec![], query) } } @@ -248,7 +292,7 @@ pub fn transform_global_sql( /// This handles cases like `WITH a AS (...), b AS (...) VISUALISE` where the WITH /// clause has no trailing SELECT - these CTEs are still extracted for layer use /// but shouldn't be executed as global data. -pub fn does_consume_cte(source_tree: &SourceTree) -> bool { +pub fn has_executable_sql(source_tree: &SourceTree) -> bool { let root = source_tree.root(); // Check for direct executable statements (SELECT, CREATE, INSERT, UPDATE, diff --git a/src/execute/mod.rs b/src/execute/mod.rs index 8a1a3911..0ad813cc 100644 --- a/src/execute/mod.rs +++ b/src/execute/mod.rs @@ -1043,7 +1043,9 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result Result prec.right(seq( - caseInsensitive('INSERT'), + token(prec(1, caseInsensitive('INSERT'))), repeat1(choice( $.sql_keyword, $.identifier, @@ -149,7 +149,7 @@ module.exports = grammar({ // UPDATE statement update_statement: $ => prec.right(seq( - caseInsensitive('UPDATE'), + token(prec(1, caseInsensitive('UPDATE'))), repeat1(choice( $.sql_keyword, $.identifier, @@ -163,7 +163,7 @@ module.exports = grammar({ // DELETE statement delete_statement: $ => prec.right(seq( - caseInsensitive('DELETE'), + token(prec(1, caseInsensitive('DELETE'))), repeat1(choice( $.sql_keyword, $.identifier, @@ -177,7 +177,7 @@ module.exports = grammar({ other_sql_statement: $ => prec(-1, repeat1(choice( $.non_from_sql_keyword, - token(/[^\s;(),'"]+/), + /[^\s;(),'"]+/, $.string, $.number, $.subquery, diff --git a/tree-sitter-ggsql/test/corpus/basic.txt b/tree-sitter-ggsql/test/corpus/basic.txt index ec39fc83..43bd2895 100644 --- a/tree-sitter-ggsql/test/corpus/basic.txt +++ b/tree-sitter-ggsql/test/corpus/basic.txt @@ -2865,6 +2865,132 @@ VISUALISE DRAW point MAPPING x AS x, y AS y (bare_identifier)))) (aesthetic_name))))))))) +================================================================================ +CREATE statement before VISUALISE FROM +================================================================================ + +CREATE TEMP TABLE data AS SELECT 1; +VISUALISE FROM data DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (create_statement + (select_statement + (select_body + (number)))))) + (visualise_statement + (visualise_keyword) + (from_clause + (table_ref + (qualified_name + (identifier + (bare_identifier))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +INSERT statement before VISUALISE FROM +================================================================================ + +INSERT INTO data VALUES (1, 2); +VISUALISE FROM data DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (insert_statement))) + (visualise_statement + (visualise_keyword) + (from_clause + (table_ref + (qualified_name + (identifier + (bare_identifier))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +UPDATE statement before VISUALISE FROM +================================================================================ + +UPDATE data SET x = 1; +VISUALISE FROM data DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (update_statement))) + (visualise_statement + (visualise_keyword) + (from_clause + (table_ref + (qualified_name + (identifier + (bare_identifier))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +DELETE statement before VISUALISE FROM +================================================================================ + +DELETE FROM data WHERE x = 1; +VISUALISE FROM data DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (delete_statement))) + (visualise_statement + (visualise_keyword) + (from_clause + (table_ref + (qualified_name + (identifier + (bare_identifier))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +CREATE and INSERT before VISUALISE FROM +================================================================================ + +CREATE TABLE data (x INT); +INSERT INTO data VALUES (1); +VISUALISE FROM data DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (create_statement)) + (sql_statement + (insert_statement))) + (visualise_statement + (visualise_keyword) + (from_clause + (table_ref + (qualified_name + (identifier + (bare_identifier))))) + (viz_clause + (draw_clause + (geom_type))))) + ================================================================================ Arbitrary SQL setup statements ================================================================================