From 770f86e4906c0c91adfc0ae393cd71c68b255758 Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Tue, 12 Aug 2025 14:06:19 -0500 Subject: [PATCH] move our dbsp-based views to materialized views We will implement normal SQLite-style view-as-an-alias for compatibility, and will call our incremental views materialized views. --- core/incremental/view.rs | 31 +++++++++---------- core/translate/mod.rs | 8 +++-- core/translate/view.rs | 10 +++--- vendored/sqlite3-parser/src/parser/ast/fmt.rs | 23 ++++++++++++++ vendored/sqlite3-parser/src/parser/ast/mod.rs | 11 +++++++ vendored/sqlite3-parser/src/parser/parse.y | 5 +++ 6 files changed, 65 insertions(+), 23 deletions(-) diff --git a/core/incremental/view.rs b/core/incremental/view.rs index e163f55de..dde96500b 100644 --- a/core/incremental/view.rs +++ b/core/incremental/view.rs @@ -94,7 +94,7 @@ pub struct IncrementalView { } impl IncrementalView { - /// Validate that a CREATE VIEW statement can be handled by IncrementalView + /// Validate that a CREATE MATERIALIZED VIEW statement can be handled by IncrementalView /// This should be called early, before updating sqlite_master pub fn can_create_view( select: &turso_sqlite3_parser::ast::Select, @@ -149,7 +149,7 @@ impl IncrementalView { /// Check if this view has the same SQL definition as the provided SQL string pub fn has_same_sql(&self, sql: &str) -> bool { // Parse the SQL to extract just the SELECT statement - if let Ok(Some(Cmd::Stmt(Stmt::CreateView { select, .. }))) = + if let Ok(Some(Cmd::Stmt(Stmt::CreateMaterializedView { select, .. }))) = Parser::new(sql.as_bytes()).next() { // Compare the SELECT statements as SQL strings @@ -178,15 +178,14 @@ impl IncrementalView { let cmd = parser.next()?; let cmd = cmd.expect("View is an empty statement"); match cmd { - Cmd::Stmt(Stmt::CreateView { - temporary: _, + Cmd::Stmt(Stmt::CreateMaterializedView { if_not_exists: _, view_name, columns: _, select, }) => IncrementalView::from_stmt(view_name, select, schema), _ => Err(LimboError::ParseError(format!( - "View is not a CREATE VIEW statement: {sql}" + "View is not a CREATE MATERIALIZED VIEW statement: {sql}" ))), } } @@ -1018,7 +1017,7 @@ mod tests { #[test] fn test_projection_simple_columns() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a, b FROM t"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a, b FROM t"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1042,7 +1041,7 @@ mod tests { #[test] fn test_projection_arithmetic_expression() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a * 2 as doubled FROM t"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a * 2 as doubled FROM t"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1066,7 +1065,7 @@ mod tests { #[test] fn test_projection_multiple_expressions() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a + b as sum, a - b as diff, c FROM t"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a + b as sum, a - b as diff, c FROM t"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1093,7 +1092,7 @@ mod tests { #[test] fn test_projection_function_call() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT hex(a) as hex_a, b FROM t"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT hex(a) as hex_a, b FROM t"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1120,7 +1119,7 @@ mod tests { #[test] fn test_projection_mixed_columns_and_expressions() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a, b * 2 as doubled, c, a + b + c as total FROM t"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a, b * 2 as doubled, c, a + b + c as total FROM t"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1152,7 +1151,7 @@ mod tests { #[test] fn test_projection_complex_expression() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT (a * 2) + (b * 3) as weighted, c / 2 as half FROM t"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT (a * 2) + (b * 3) as weighted, c / 2 as half FROM t"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1176,7 +1175,7 @@ mod tests { #[test] fn test_projection_with_where_clause() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a, a * 2 as doubled FROM t WHERE b > 2"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a, a * 2 as doubled FROM t WHERE b > 2"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1202,7 +1201,7 @@ mod tests { #[test] fn test_projection_more_output_columns_than_input() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a, b, a * 2 as doubled_a, b * 3 as tripled_b, a + b as sum, hex(c) as hex_c FROM t"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a, b, a * 2 as doubled_a, b * 3 as tripled_b, a + b as sum, hex(c) as hex_c FROM t"; let view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1237,7 +1236,7 @@ mod tests { #[test] fn test_aggregation_count_with_group_by() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a, COUNT(*) FROM t GROUP BY a"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a, COUNT(*) FROM t GROUP BY a"; let mut view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1291,7 +1290,7 @@ mod tests { #[test] fn test_aggregation_sum_with_filter() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT SUM(b) FROM t WHERE a > 1"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT SUM(b) FROM t WHERE a > 1"; let mut view = IncrementalView::from_sql(sql, &schema).unwrap(); @@ -1329,7 +1328,7 @@ mod tests { #[test] fn test_aggregation_incremental_updates() { let schema = create_test_schema(); - let sql = "CREATE VIEW v AS SELECT a, COUNT(*), SUM(b) FROM t GROUP BY a"; + let sql = "CREATE MATERIALIZED VIEW v AS SELECT a, COUNT(*), SUM(b) FROM t GROUP BY a"; let mut view = IncrementalView::from_sql(sql, &schema).unwrap(); diff --git a/core/translate/mod.rs b/core/translate/mod.rs index 85c95f429..96b23cac8 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -125,6 +125,7 @@ pub fn translate_inner( | ast::Stmt::CreateTable { .. } | ast::Stmt::CreateTrigger { .. } | ast::Stmt::CreateView { .. } + | ast::Stmt::CreateMaterializedView { .. } | ast::Stmt::CreateVirtualTable(..) | ast::Stmt::Delete(..) | ast::Stmt::DropIndex { .. } @@ -189,9 +190,12 @@ pub fn translate_inner( program, )?, ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"), - ast::Stmt::CreateView { + ast::Stmt::CreateView { .. } => { + bail_parse_error!("CREATE VIEW not supported yet.") + } + ast::Stmt::CreateMaterializedView { view_name, select, .. - } => view::translate_create_view( + } => view::translate_create_materialized_view( schema, view_name.name.as_str(), &select, diff --git a/core/translate/view.rs b/core/translate/view.rs index f9ab2e989..d21950a59 100644 --- a/core/translate/view.rs +++ b/core/translate/view.rs @@ -8,7 +8,7 @@ use crate::{Connection, Result, SymbolTable}; use std::sync::Arc; use turso_sqlite3_parser::ast::{self, fmt::ToTokens}; -pub fn translate_create_view( +pub fn translate_create_materialized_view( schema: &Schema, view_name: &str, select_stmt: &ast::Select, @@ -19,7 +19,7 @@ pub fn translate_create_view( // Check if experimental views are enabled if !connection.experimental_views_enabled() { return Err(crate::LimboError::ParseError( - "CREATE VIEW is an experimental feature. Enable with --experimental-views flag" + "CREATE MATERIALIZED VIEW is an experimental feature. Enable with --experimental-views flag" .to_string(), )); } @@ -40,7 +40,7 @@ pub fn translate_create_view( IncrementalView::can_create_view(select_stmt, schema)?; // Reconstruct the SQL string - let sql = create_view_to_str(view_name, select_stmt); + let sql = create_materialized_view_to_str(view_name, select_stmt); // Open cursor to sqlite_schema table let table = schema.get_btree_table(SQLITE_TABLEID).unwrap(); @@ -78,9 +78,9 @@ pub fn translate_create_view( Ok(program) } -fn create_view_to_str(view_name: &str, select_stmt: &ast::Select) -> String { +fn create_materialized_view_to_str(view_name: &str, select_stmt: &ast::Select) -> String { format!( - "CREATE VIEW {} AS {}", + "CREATE MATERIALIZED VIEW {} AS {}", view_name, select_stmt.format().unwrap() ) diff --git a/vendored/sqlite3-parser/src/parser/ast/fmt.rs b/vendored/sqlite3-parser/src/parser/ast/fmt.rs index 1a2d1b5c3..64f722421 100644 --- a/vendored/sqlite3-parser/src/parser/ast/fmt.rs +++ b/vendored/sqlite3-parser/src/parser/ast/fmt.rs @@ -391,6 +391,29 @@ impl ToTokens for Stmt { s.append(TK_AS, None)?; select.to_tokens_with_context(s, context) } + Self::CreateMaterializedView { + if_not_exists, + view_name, + columns, + select, + } => { + s.append(TK_CREATE, None)?; + s.append(TK_MATERIALIZED, None)?; + s.append(TK_VIEW, None)?; + if *if_not_exists { + s.append(TK_IF, None)?; + s.append(TK_NOT, None)?; + s.append(TK_EXISTS, None)?; + } + view_name.to_tokens_with_context(s, context)?; + if let Some(columns) = columns { + s.append(TK_LP, None)?; + comma(columns, s, context)?; + s.append(TK_RP, None)?; + } + s.append(TK_AS, None)?; + select.to_tokens_with_context(s, context) + } Self::CreateVirtualTable(create_virtual_table) => { let CreateVirtualTable { if_not_exists, diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index 8bfb2ef4a..e8d755799 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -130,6 +130,17 @@ pub enum Stmt { /// query select: Box, + }, /// `CREATE VIRTUAL TABLE` CreateVirtualTable(Box), /// `DELETE` diff --git a/vendored/sqlite3-parser/src/parser/parse.y b/vendored/sqlite3-parser/src/parser/parse.y index 912c72bbb..d599318c5 100644 --- a/vendored/sqlite3-parser/src/parser/parse.y +++ b/vendored/sqlite3-parser/src/parser/parse.y @@ -478,6 +478,11 @@ cmd ::= createkw temp(T) VIEW ifnotexists(E) fullname(Y) eidlist_opt(C) self.ctx.stmt = Some(Stmt::CreateView{ temporary: T, if_not_exists: E, view_name: Y, columns: C, select: Box::new(S) }); } +cmd ::= createkw MATERIALIZED VIEW ifnotexists(E) fullname(Y) eidlist_opt(C) + AS select(S). { + self.ctx.stmt = Some(Stmt::CreateMaterializedView{ if_not_exists: E, view_name: Y, columns: C, + select: Box::new(S) }); +} cmd ::= DROP VIEW ifexists(E) fullname(X). { self.ctx.stmt = Some(Stmt::DropView{ if_exists: E, view_name: X }); }