Files
turso/core/translate/view.rs
Glauber Costa 097510216e implement the projector operator for DBSP
My goal with this patch is to be able to implement the ProjectOperator
for DBSP circuits using VDBE for expression evaluation.

*not* doing so is dangerous for the following reason: we will end up
with different, subtle, and incompatible behavior between SQLite
expressions if they are used in views versus outside of views.

In fact, even in our prototype had them: our projection tests, which
used to pass, were actually wrong =) (sqlite would return something
different if those functions were executed outside the view context)

For optimization reasons, we single out trivial expressions: they don't
have go through VDBE. Trivial expressions are expressions that only
involve Columns, Literals, and simple operators on elements of the same
type. Even type coercion takes this out of the realm of trivial.

Everything that is not trivial, is then translated with translate_expr -
in the same way SQLite will, and then compiled with VDBE.

We can, over time, make this process much better. There are essentially
infinite opportunities for optimization here. But for now, the main
warts are:
* VDBE execution needs a connection
* There is no good way in VDBE to pass parameters to a program.
* It is almost trivial to pollute the original connection. For example,
  we need to issue HALT for the program to stop, but seeing that halt
  will usually cause the program to try and halt the original program.

Subprograms, like the ones we use in triggers are a possible solution,
but they are much more expensive to execute, especially given that our
execution would essentially have to have a program with no other role
than to wrap the subprogram.

Therefore, what I am doing is:
* There is an in-memory database inside the projection operator (an
  obvious optimization is to share it with *all* projection operators).
* We obtain a connection to that database when the operator is created
* We use that connection to execute our VDBE, which offers a clean, safe
  and isolated way to execute the expression.
* We feed the values to the program manually by editing the registers
  directly.
2025-08-25 17:48:17 +03:00

285 lines
8.7 KiB
Rust

use crate::schema::Schema;
use crate::translate::emitter::Resolver;
use crate::translate::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID};
use crate::util::normalize_ident;
use crate::vdbe::builder::{CursorType, ProgramBuilder};
use crate::vdbe::insn::{CmpInsFlags, Cookie, Insn};
use crate::{Connection, Result, SymbolTable};
use std::sync::Arc;
use turso_parser::ast::{self, fmt::ToTokens};
/// Common logic for creating views (both regular and materialized)
fn emit_create_view_program(
schema: &Schema,
view_name: &str,
sql: String,
syms: &SymbolTable,
program: &mut ProgramBuilder,
populate_materialized: bool,
) -> Result<()> {
let normalized_view_name = normalize_ident(view_name);
// Open cursor to sqlite_schema table
let table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table.clone()));
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: 1usize.into(),
db: 0,
});
// Add the view entry to sqlite_schema
let resolver = Resolver::new(schema, syms);
emit_schema_entry(
program,
&resolver,
sqlite_schema_cursor_id,
None, // cdc_table_cursor_id, no cdc for views
SchemaEntryType::View,
&normalized_view_name,
&normalized_view_name, // for views, tbl_name is same as name
0, // views don't have a root page
Some(sql),
)?;
// Parse schema to load the new view
program.emit_insn(Insn::ParseSchema {
db: sqlite_schema_cursor_id,
where_clause: Some(format!("name = '{normalized_view_name}'")),
});
program.emit_insn(Insn::SetCookie {
db: 0,
cookie: Cookie::SchemaVersion,
value: (schema.schema_version + 1) as i32,
p5: 0,
});
// Populate materialized views if needed
// Note: This must come after SetCookie since it may do I/O operations
if populate_materialized {
program.emit_insn(Insn::PopulateMaterializedViews);
}
Ok(())
}
pub fn translate_create_materialized_view(
schema: &Schema,
view_name: &str,
select_stmt: &ast::Select,
connection: Arc<Connection>,
syms: &SymbolTable,
mut program: ProgramBuilder,
) -> Result<ProgramBuilder> {
// Check if experimental views are enabled
if !connection.experimental_views_enabled() {
return Err(crate::LimboError::ParseError(
"CREATE MATERIALIZED VIEW is an experimental feature. Enable with --experimental-views flag"
.to_string(),
));
}
let normalized_view_name = normalize_ident(view_name);
// Check if view already exists
if schema
.get_materialized_view(&normalized_view_name)
.is_some()
{
return Err(crate::LimboError::ParseError(format!(
"View {normalized_view_name} already exists"
)));
}
// Validate that this view can be created as an IncrementalView
// This validation happens before updating sqlite_master to prevent
// storing invalid view definitions
use crate::incremental::view::IncrementalView;
IncrementalView::can_create_view(select_stmt)?;
// Reconstruct the SQL string
let sql = create_materialized_view_to_str(view_name, select_stmt);
// Use common logic to emit the view creation program
emit_create_view_program(schema, view_name, sql, syms, &mut program, true)?;
program.epilogue(schema);
Ok(program)
}
fn create_materialized_view_to_str(view_name: &str, select_stmt: &ast::Select) -> String {
format!(
"CREATE MATERIALIZED VIEW {} AS {}",
view_name,
select_stmt.format().unwrap()
)
}
pub fn translate_create_view(
schema: &Schema,
view_name: &str,
select_stmt: &ast::Select,
_columns: &[ast::IndexedColumn],
_connection: Arc<Connection>,
syms: &SymbolTable,
mut program: ProgramBuilder,
) -> Result<ProgramBuilder> {
let normalized_view_name = normalize_ident(view_name);
// Check if view already exists
if schema.get_view(&normalized_view_name).is_some()
|| schema
.get_materialized_view(&normalized_view_name)
.is_some()
{
return Err(crate::LimboError::ParseError(format!(
"View {normalized_view_name} already exists"
)));
}
// Reconstruct the SQL string
let sql = create_view_to_str(view_name, select_stmt);
// Use common logic to emit the view creation program
emit_create_view_program(schema, view_name, sql, syms, &mut program, false)?;
Ok(program)
}
fn create_view_to_str(view_name: &str, select_stmt: &ast::Select) -> String {
format!(
"CREATE VIEW {} AS {}",
view_name,
select_stmt.format().unwrap()
)
}
pub fn translate_drop_view(
schema: &Schema,
view_name: &str,
if_exists: bool,
mut program: ProgramBuilder,
) -> Result<ProgramBuilder> {
let normalized_view_name = normalize_ident(view_name);
// Check if view exists (either regular or materialized)
let view_exists = schema.get_view(&normalized_view_name).is_some()
|| schema
.get_materialized_view(&normalized_view_name)
.is_some();
if !view_exists && !if_exists {
return Err(crate::LimboError::ParseError(format!(
"no such view: {normalized_view_name}"
)));
}
if !view_exists && if_exists {
// View doesn't exist but IF EXISTS was specified, nothing to do
return Ok(program);
}
// Open cursor to sqlite_schema table
let schema_table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
let sqlite_schema_cursor_id =
program.alloc_cursor_id(CursorType::BTreeTable(schema_table.clone()));
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: 1usize.into(),
db: 0,
});
// Allocate registers for searching
let view_name_reg = program.alloc_register();
let type_reg = program.alloc_register();
let rowid_reg = program.alloc_register();
// Set the view name and type we're looking for
program.emit_insn(Insn::String8 {
dest: view_name_reg,
value: normalized_view_name.clone(),
});
program.emit_insn(Insn::String8 {
dest: type_reg,
value: "view".to_string(),
});
// Start scanning from the beginning
let end_loop_label = program.allocate_label();
let loop_start_label = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: sqlite_schema_cursor_id,
pc_if_empty: end_loop_label,
});
program.preassign_label_to_next_insn(loop_start_label);
// Check if this row is the view we're looking for
// Column 0 is type, Column 1 is name, Column 2 is tbl_name
let col0_reg = program.alloc_register();
let col1_reg = program.alloc_register();
program.emit_column_or_rowid(sqlite_schema_cursor_id, 0, col0_reg);
program.emit_column_or_rowid(sqlite_schema_cursor_id, 1, col1_reg);
// Check if type == 'view' and name == view_name
let skip_delete_label = program.allocate_label();
program.emit_insn(Insn::Ne {
lhs: col0_reg,
rhs: type_reg,
target_pc: skip_delete_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
program.emit_insn(Insn::Ne {
lhs: col1_reg,
rhs: view_name_reg,
target_pc: skip_delete_label,
flags: CmpInsFlags::default(),
collation: program.curr_collation(),
});
// Get the rowid and delete this row
program.emit_insn(Insn::RowId {
cursor_id: sqlite_schema_cursor_id,
dest: rowid_reg,
});
program.emit_insn(Insn::Delete {
cursor_id: sqlite_schema_cursor_id,
table_name: "sqlite_schema".to_string(),
});
program.resolve_label(skip_delete_label, program.offset());
// Move to next row
program.emit_insn(Insn::Next {
cursor_id: sqlite_schema_cursor_id,
pc_if_next: loop_start_label,
});
program.preassign_label_to_next_insn(end_loop_label);
// Remove the view from the in-memory schema
program.emit_insn(Insn::DropView {
db: 0,
view_name: normalized_view_name.clone(),
});
// Update schema version (increment schema cookie)
let schema_version_reg = program.alloc_register();
program.emit_insn(Insn::Integer {
dest: schema_version_reg,
value: (schema.schema_version + 1) as i64,
});
program.emit_insn(Insn::SetCookie {
db: 0,
cookie: Cookie::SchemaVersion,
value: (schema.schema_version + 1) as i32,
p5: 1, // update version
});
program.epilogue(schema);
Ok(program)
}