Files
turso/core/translate/delete.rs
Glauber Costa 1b5e74060a make sure that we are able to prevent views from being corrupted
as we make changes to the way materialized views are generated (think
adding new operators, changing the id of existing operators, etc), we
will need to persist the topology of the circuit itself. This is a
change that I believe to be premature. For now, it is enough to reserve
the first operator id for it, and add a version number to the table
name. We can just detect that something changed, and ask the user to
drop the view. We can get away with it due to the fact that the views
are experimental.
2025-09-25 22:52:08 -03:00

162 lines
5.7 KiB
Rust

use crate::schema::Table;
use crate::translate::emitter::emit_program;
use crate::translate::expr::ParamState;
use crate::translate::optimizer::optimize_plan;
use crate::translate::plan::{DeletePlan, Operation, Plan};
use crate::translate::planner::{parse_limit, parse_where};
use crate::util::normalize_ident;
use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, TableRefIdCounter};
use crate::{schema::Schema, Result, SymbolTable};
use std::sync::Arc;
use turso_parser::ast::{Expr, Limit, QualifiedName, ResultColumn};
use super::plan::{ColumnUsedMask, JoinedTable, TableReferences};
#[allow(clippy::too_many_arguments)]
pub fn translate_delete(
schema: &Schema,
tbl_name: &QualifiedName,
where_clause: Option<Box<Expr>>,
limit: Option<Limit>,
returning: Vec<ResultColumn>,
syms: &SymbolTable,
mut program: ProgramBuilder,
connection: &Arc<crate::Connection>,
) -> Result<ProgramBuilder> {
let tbl_name = normalize_ident(tbl_name.name.as_str());
// Check if this is a system table that should be protected from direct writes
if crate::schema::is_system_table(&tbl_name) {
crate::bail_parse_error!("table {} may not be modified", tbl_name);
}
if schema.table_has_indexes(&tbl_name) && !schema.indexes_enabled() {
// Let's disable altering a table with indices altogether instead of checking column by
// column to be extra safe.
crate::bail_parse_error!(
"DELETE for table with indexes is disabled. Omit the `--experimental-indexes=false` flag to enable this feature."
);
}
// FIXME: SQLite's delete using Returning is complex. It scans the table in read mode first, building
// the result set, and only after that it opens the table for writing and deletes the rows. It
// also uses a couple of instructions that we don't implement yet (i.e.: RowSetAdd, RowSetRead,
// RowSetTest). So for now I'll just defer it altogether.
if !returning.is_empty() {
crate::bail_parse_error!("RETURNING currently not implemented for DELETE statements.");
}
let result_columns = vec![];
let mut delete_plan = prepare_delete_plan(
schema,
tbl_name,
where_clause,
limit,
result_columns,
&mut program.table_reference_counter,
connection,
)?;
optimize_plan(&mut delete_plan, schema)?;
let Plan::Delete(ref delete) = delete_plan else {
panic!("delete_plan is not a DeletePlan");
};
let opts = ProgramBuilderOpts {
num_cursors: 1,
approx_num_insns: estimate_num_instructions(delete),
approx_num_labels: 0,
};
program.extend(&opts);
emit_program(connection, &mut program, delete_plan, schema, syms, |_| {})?;
Ok(program)
}
pub fn prepare_delete_plan(
schema: &Schema,
tbl_name: String,
where_clause: Option<Box<Expr>>,
limit: Option<Limit>,
result_columns: Vec<super::plan::ResultSetColumn>,
table_ref_counter: &mut TableRefIdCounter,
connection: &Arc<crate::Connection>,
) -> Result<Plan> {
let table = match schema.get_table(&tbl_name) {
Some(table) => table,
None => crate::bail_parse_error!("no such table: {}", tbl_name),
};
// Check if this is a materialized view
if schema.is_materialized_view(&tbl_name) {
crate::bail_parse_error!("cannot modify materialized view {}", tbl_name);
}
// Check if this table has any incompatible dependent views
let incompatible_views = schema.has_incompatible_dependent_views(&tbl_name);
if !incompatible_views.is_empty() {
use crate::incremental::compiler::DBSP_CIRCUIT_VERSION;
crate::bail_parse_error!(
"Cannot DELETE from table '{}' because it has incompatible dependent materialized view(s): {}. \n\
These views were created with a different DBSP version than the current version ({}). \n\
Please DROP and recreate the view(s) before modifying this table.",
tbl_name,
incompatible_views.join(", "),
DBSP_CIRCUIT_VERSION
);
}
let table = if let Some(table) = table.virtual_table() {
Table::Virtual(table.clone())
} else if let Some(table) = table.btree() {
Table::BTree(table.clone())
} else {
crate::bail_parse_error!("Table is neither a virtual table nor a btree table");
};
let indexes = schema.get_indices(table.get_name()).cloned().collect();
let joined_tables = vec![JoinedTable {
op: Operation::default_scan_for(&table),
table,
identifier: tbl_name,
internal_id: table_ref_counter.next(),
join_info: None,
col_used_mask: ColumnUsedMask::default(),
database_id: 0,
}];
let mut table_references = TableReferences::new(joined_tables, vec![]);
let mut where_predicates = vec![];
let mut param_ctx = ParamState::default();
// Parse the WHERE clause
parse_where(
where_clause.as_deref(),
&mut table_references,
None,
&mut where_predicates,
connection,
&mut param_ctx,
)?;
// Parse the LIMIT/OFFSET clause
let (resolved_limit, resolved_offset) = limit.map_or(Ok((None, None)), |mut l| {
parse_limit(&mut l, connection, &mut param_ctx)
})?;
let plan = DeletePlan {
table_references,
result_columns,
where_clause: where_predicates,
order_by: vec![],
limit: resolved_limit,
offset: resolved_offset,
contains_constant_false_condition: false,
indexes,
};
Ok(Plan::Delete(plan))
}
fn estimate_num_instructions(plan: &DeletePlan) -> usize {
let base = 20;
base + plan.table_references.joined_tables().len() * 10
}