Merge 'move our dbsp-based views to materialized views' from Glauber Costa

We will implement normal SQLite-style view-as-an-alias for
compatibility, and will call our incremental views materialized views.

Closes #2571
This commit is contained in:
Jussi Saurio
2025-08-13 08:53:30 +03:00
committed by GitHub
6 changed files with 65 additions and 23 deletions

View File

@@ -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();

View File

@@ -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,

View File

@@ -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()
)

View File

@@ -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,

View File

@@ -130,6 +130,17 @@ pub enum Stmt {
/// query
select: Box<Select>,
},
/// `CREATE MATERIALIZED VIEW`
CreateMaterializedView {
/// `IF NOT EXISTS`
if_not_exists: bool,
/// view name
view_name: QualifiedName,
/// columns
columns: Option<Vec<IndexedColumn>>,
/// query
select: Box<Select>,
},
/// `CREATE VIRTUAL TABLE`
CreateVirtualTable(Box<CreateVirtualTable>),
/// `DELETE`

View File

@@ -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 });
}