diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd15e31..17223518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ handled auto-resizing in the plot pane. We now have a per-output-location path in the Jupyter kernel (#360) +### Changed + +- Reverted an earlier decision to materialize CTEs and the global query in Rust +before registering them back to the backend. We now keep the data purely on the +backend until the layer query as was always intended (#363) + ### Removed - Removed polars from dependency list along with all its transient dependencies. Rewrote DataFrame struct on top of arrow (#350) diff --git a/src/execute/cte.rs b/src/execute/cte.rs index dc4637f4..6abe56ab 100644 --- a/src/execute/cte.rs +++ b/src/execute/cte.rs @@ -146,28 +146,17 @@ pub fn materialize_ctes(ctes: &[CteDefinition], reader: &dyn Reader) -> Result = df.get_column_names(); - for (old, new) in current_names.iter().zip(cte.column_aliases.iter()) { - df = df.rename(old, new).map_err(|e| { - GgsqlError::ReaderError(format!( - "Failed to apply column alias '{}' for CTE '{}': {}", - new, cte.name, e - )) - })?; - } + let statements = reader.dialect().create_or_replace_temp_table_sql( + &temp_table_name, + &cte.column_aliases, + &transformed_body, + ); + for stmt in &statements { + reader.execute_sql(stmt).map_err(|e| { + GgsqlError::ReaderError(format!("Failed to materialize CTE '{}': {}", cte.name, e)) + })?; } - reader.register(&temp_table_name, df, true).map_err(|e| { - GgsqlError::ReaderError(format!("Failed to register CTE '{}': {}", cte.name, e)) - })?; - materialized.insert(cte.name.clone()); } diff --git a/src/execute/mod.rs b/src/execute/mod.rs index d9215b41..82c9e1c0 100644 --- a/src/execute/mod.rs +++ b/src/execute/mod.rs @@ -968,9 +968,16 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result Vec { + let body = wrap_with_column_aliases(body_sql, column_aliases); + vec![format!( + "CREATE OR REPLACE TEMP TABLE {} AS {}", + naming::quote_ident(name), + body + )] + } +} + +/// Wrap a body SQL in a CTE with a column alias list when aliases are present. +/// This is a portable way to rename the body's output columns without relying +/// on `CREATE TABLE t(a, b) AS ...` (which SQLite does not support). +pub(crate) fn wrap_with_column_aliases(body_sql: &str, column_aliases: &[String]) -> String { + if column_aliases.is_empty() { + return body_sql.to_string(); + } + let cols = column_aliases + .iter() + .map(|c| naming::quote_ident(c)) + .collect::>() + .join(", "); + format!( + "WITH __ggsql_aliased__({}) AS ({}) SELECT * FROM __ggsql_aliased__", + cols, body_sql + ) } pub struct AnsiDialect; diff --git a/src/reader/sqlite.rs b/src/reader/sqlite.rs index c50ce6b6..7023d04f 100644 --- a/src/reader/sqlite.rs +++ b/src/reader/sqlite.rs @@ -92,6 +92,22 @@ impl super::SqlDialect for SqliteDialect { table.replace('\'', "''") ) } + + /// SQLite does not support `CREATE OR REPLACE`, so emit a drop-then-create + /// pair. Column aliases are preserved portably via the default CTE wrapper. + fn create_or_replace_temp_table_sql( + &self, + name: &str, + column_aliases: &[String], + body_sql: &str, + ) -> Vec { + let qname = naming::quote_ident(name); + let body = super::wrap_with_column_aliases(body_sql, column_aliases); + vec![ + format!("DROP TABLE IF EXISTS {}", qname), + format!("CREATE TEMP TABLE {} AS {}", qname, body), + ] + } } /// SQLite database reader