mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-30 22:44:21 +01:00
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.
285 lines
8.7 KiB
Rust
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)
|
|
}
|