mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-06 08:44:23 +01:00
triggers: translation functions for DDL
This commit is contained in:
@@ -35,6 +35,7 @@ pub(crate) mod schema;
|
||||
pub(crate) mod select;
|
||||
pub(crate) mod subquery;
|
||||
pub(crate) mod transaction;
|
||||
pub(crate) mod trigger;
|
||||
pub(crate) mod update;
|
||||
pub(crate) mod upsert;
|
||||
mod values;
|
||||
@@ -169,7 +170,40 @@ pub fn translate_inner(
|
||||
program,
|
||||
connection,
|
||||
)?,
|
||||
ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"),
|
||||
ast::Stmt::CreateTrigger {
|
||||
temporary,
|
||||
if_not_exists,
|
||||
trigger_name,
|
||||
time,
|
||||
event,
|
||||
tbl_name,
|
||||
for_each_row,
|
||||
when_clause,
|
||||
commands,
|
||||
} => {
|
||||
// Reconstruct SQL for storage
|
||||
let sql = trigger::create_trigger_to_sql(
|
||||
temporary,
|
||||
if_not_exists,
|
||||
&trigger_name,
|
||||
time,
|
||||
&event,
|
||||
&tbl_name,
|
||||
for_each_row,
|
||||
when_clause.as_deref(),
|
||||
&commands,
|
||||
);
|
||||
trigger::translate_create_trigger(
|
||||
trigger_name,
|
||||
resolver,
|
||||
temporary,
|
||||
if_not_exists,
|
||||
time,
|
||||
tbl_name,
|
||||
program,
|
||||
sql,
|
||||
)?
|
||||
}
|
||||
ast::Stmt::CreateView {
|
||||
view_name,
|
||||
select,
|
||||
@@ -232,7 +266,15 @@ pub fn translate_inner(
|
||||
if_exists,
|
||||
tbl_name,
|
||||
} => translate_drop_table(tbl_name, resolver, if_exists, program, connection)?,
|
||||
ast::Stmt::DropTrigger { .. } => bail_parse_error!("DROP TRIGGER not supported yet"),
|
||||
ast::Stmt::DropTrigger {
|
||||
if_exists,
|
||||
trigger_name,
|
||||
} => trigger::translate_drop_trigger(
|
||||
resolver.schema,
|
||||
trigger_name.name.as_str(),
|
||||
if_exists,
|
||||
program,
|
||||
)?,
|
||||
ast::Stmt::DropView {
|
||||
if_exists,
|
||||
view_name,
|
||||
|
||||
@@ -307,6 +307,7 @@ pub enum SchemaEntryType {
|
||||
Table,
|
||||
Index,
|
||||
View,
|
||||
Trigger,
|
||||
}
|
||||
|
||||
impl SchemaEntryType {
|
||||
@@ -315,6 +316,7 @@ impl SchemaEntryType {
|
||||
SchemaEntryType::Table => "table",
|
||||
SchemaEntryType::Index => "index",
|
||||
SchemaEntryType::View => "view",
|
||||
SchemaEntryType::Trigger => "trigger",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,7 +679,7 @@ pub fn translate_drop_table(
|
||||
let table_reg =
|
||||
program.emit_string8_new_reg(normalize_ident(tbl_name.name.as_str()).to_string()); // r3
|
||||
program.mark_last_insn_constant();
|
||||
let table_type = program.emit_string8_new_reg("trigger".to_string()); // r4
|
||||
let _table_type = program.emit_string8_new_reg("trigger".to_string()); // r4
|
||||
program.mark_last_insn_constant();
|
||||
let row_id_reg = program.alloc_register(); // r5
|
||||
|
||||
@@ -692,7 +694,7 @@ pub fn translate_drop_table(
|
||||
db: 0,
|
||||
});
|
||||
|
||||
// 1. Remove all entries from the schema table related to the table we are dropping, except for triggers
|
||||
// 1. Remove all entries from the schema table related to the table we are dropping (including triggers)
|
||||
// loop to beginning of schema table
|
||||
let end_metadata_label = program.allocate_label();
|
||||
let metadata_loop = program.allocate_label();
|
||||
@@ -716,18 +718,6 @@ pub fn translate_drop_table(
|
||||
flags: CmpInsFlags::default(),
|
||||
collation: program.curr_collation(),
|
||||
});
|
||||
program.emit_column_or_rowid(
|
||||
sqlite_schema_cursor_id_0,
|
||||
0,
|
||||
table_name_and_root_page_register,
|
||||
);
|
||||
program.emit_insn(Insn::Eq {
|
||||
lhs: table_name_and_root_page_register,
|
||||
rhs: table_type,
|
||||
target_pc: next_label,
|
||||
flags: CmpInsFlags::default(),
|
||||
collation: program.curr_collation(),
|
||||
});
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: sqlite_schema_cursor_id_0,
|
||||
dest: row_id_reg,
|
||||
|
||||
293
core/translate/trigger.rs
Normal file
293
core/translate/trigger.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use crate::translate::emitter::Resolver;
|
||||
use crate::translate::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID};
|
||||
use crate::translate::ProgramBuilder;
|
||||
use crate::translate::ProgramBuilderOpts;
|
||||
use crate::util::normalize_ident;
|
||||
use crate::vdbe::builder::CursorType;
|
||||
use crate::vdbe::insn::{Cookie, Insn};
|
||||
use crate::{bail_parse_error, Result};
|
||||
use turso_parser::ast::{self, QualifiedName};
|
||||
|
||||
/// Reconstruct SQL string from CREATE TRIGGER AST
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn create_trigger_to_sql(
|
||||
temporary: bool,
|
||||
if_not_exists: bool,
|
||||
trigger_name: &QualifiedName,
|
||||
time: Option<ast::TriggerTime>,
|
||||
event: &ast::TriggerEvent,
|
||||
tbl_name: &QualifiedName,
|
||||
for_each_row: bool,
|
||||
when_clause: Option<&ast::Expr>,
|
||||
commands: &[ast::TriggerCmd],
|
||||
) -> String {
|
||||
let mut sql = String::new();
|
||||
sql.push_str("CREATE");
|
||||
if temporary {
|
||||
sql.push_str(" TEMP");
|
||||
}
|
||||
sql.push_str(" TRIGGER");
|
||||
if if_not_exists {
|
||||
sql.push_str(" IF NOT EXISTS");
|
||||
}
|
||||
sql.push(' ');
|
||||
sql.push_str(trigger_name.name.as_str());
|
||||
sql.push(' ');
|
||||
|
||||
if let Some(t) = time {
|
||||
match t {
|
||||
ast::TriggerTime::Before => sql.push_str("BEFORE "),
|
||||
ast::TriggerTime::After => sql.push_str("AFTER "),
|
||||
ast::TriggerTime::InsteadOf => sql.push_str("INSTEAD OF "),
|
||||
}
|
||||
}
|
||||
|
||||
match event {
|
||||
ast::TriggerEvent::Delete => sql.push_str("DELETE"),
|
||||
ast::TriggerEvent::Insert => sql.push_str("INSERT"),
|
||||
ast::TriggerEvent::Update => sql.push_str("UPDATE"),
|
||||
ast::TriggerEvent::UpdateOf(cols) => {
|
||||
sql.push_str("UPDATE OF ");
|
||||
for (i, col) in cols.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push_str(col.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sql.push_str(" ON ");
|
||||
sql.push_str(tbl_name.name.as_str());
|
||||
if for_each_row {
|
||||
sql.push_str(" FOR EACH ROW");
|
||||
}
|
||||
|
||||
if let Some(when) = when_clause {
|
||||
sql.push_str(" WHEN ");
|
||||
sql.push_str(&when.to_string());
|
||||
}
|
||||
|
||||
sql.push_str(" BEGIN");
|
||||
for cmd in commands {
|
||||
sql.push(' ');
|
||||
sql.push_str(&cmd.to_string());
|
||||
sql.push(';');
|
||||
}
|
||||
sql.push_str(" END");
|
||||
|
||||
sql
|
||||
}
|
||||
|
||||
/// Translate CREATE TRIGGER statement
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn translate_create_trigger(
|
||||
trigger_name: QualifiedName,
|
||||
resolver: &Resolver,
|
||||
temporary: bool,
|
||||
if_not_exists: bool,
|
||||
time: Option<ast::TriggerTime>,
|
||||
tbl_name: QualifiedName,
|
||||
mut program: ProgramBuilder,
|
||||
sql: String,
|
||||
) -> Result<ProgramBuilder> {
|
||||
program.begin_write_operation();
|
||||
let normalized_trigger_name = normalize_ident(trigger_name.name.as_str());
|
||||
let normalized_table_name = normalize_ident(tbl_name.name.as_str());
|
||||
|
||||
// Check if trigger already exists
|
||||
if resolver
|
||||
.schema
|
||||
.get_trigger_for_table(&normalized_table_name, &normalized_trigger_name)
|
||||
.is_some()
|
||||
{
|
||||
if if_not_exists {
|
||||
return Ok(program);
|
||||
}
|
||||
bail_parse_error!("Trigger {} already exists", normalized_trigger_name);
|
||||
}
|
||||
|
||||
// Verify the table exists
|
||||
if resolver.schema.get_table(&normalized_table_name).is_none() {
|
||||
bail_parse_error!("no such table: {}", normalized_table_name);
|
||||
}
|
||||
|
||||
if time
|
||||
.as_ref()
|
||||
.is_some_and(|t| *t == ast::TriggerTime::InsteadOf)
|
||||
{
|
||||
bail_parse_error!("INSTEAD OF triggers are not supported yet");
|
||||
}
|
||||
|
||||
if temporary {
|
||||
bail_parse_error!("TEMPORARY triggers are not supported yet");
|
||||
}
|
||||
|
||||
let opts = ProgramBuilderOpts {
|
||||
num_cursors: 1,
|
||||
approx_num_insns: 30,
|
||||
approx_num_labels: 1,
|
||||
};
|
||||
program.extend(&opts);
|
||||
|
||||
// Open cursor to sqlite_schema table
|
||||
let table = resolver.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: 1i64.into(),
|
||||
db: 0,
|
||||
});
|
||||
|
||||
// Add the trigger entry to sqlite_schema
|
||||
emit_schema_entry(
|
||||
&mut program,
|
||||
resolver,
|
||||
sqlite_schema_cursor_id,
|
||||
None, // cdc_table_cursor_id, no cdc for triggers
|
||||
SchemaEntryType::Trigger,
|
||||
&normalized_trigger_name,
|
||||
&normalized_table_name,
|
||||
0, // triggers don't have a root page
|
||||
Some(sql.clone()),
|
||||
)?;
|
||||
|
||||
// Update schema version
|
||||
program.emit_insn(Insn::SetCookie {
|
||||
db: 0,
|
||||
cookie: Cookie::SchemaVersion,
|
||||
value: (resolver.schema.schema_version + 1) as i32,
|
||||
p5: 0,
|
||||
});
|
||||
|
||||
// Parse schema to load the new trigger
|
||||
program.emit_insn(Insn::ParseSchema {
|
||||
db: sqlite_schema_cursor_id,
|
||||
where_clause: Some(format!("name = '{normalized_trigger_name}'")),
|
||||
});
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
/// Translate DROP TRIGGER statement
|
||||
pub fn translate_drop_trigger(
|
||||
schema: &crate::schema::Schema,
|
||||
trigger_name: &str,
|
||||
if_exists: bool,
|
||||
mut program: ProgramBuilder,
|
||||
) -> Result<ProgramBuilder> {
|
||||
program.begin_write_operation();
|
||||
let normalized_trigger_name = normalize_ident(trigger_name);
|
||||
|
||||
// Check if trigger exists
|
||||
if schema.get_trigger(&normalized_trigger_name).is_none() {
|
||||
if if_exists {
|
||||
return Ok(program);
|
||||
}
|
||||
bail_parse_error!("no such trigger: {}", normalized_trigger_name);
|
||||
}
|
||||
|
||||
let opts = ProgramBuilderOpts {
|
||||
num_cursors: 1,
|
||||
approx_num_insns: 30,
|
||||
approx_num_labels: 1,
|
||||
};
|
||||
program.extend(&opts);
|
||||
|
||||
// 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: 1i64.into(),
|
||||
db: 0,
|
||||
});
|
||||
|
||||
let search_loop_label = program.allocate_label();
|
||||
let skip_non_trigger_label = program.allocate_label();
|
||||
let done_label = program.allocate_label();
|
||||
let rewind_done_label = program.allocate_label();
|
||||
|
||||
// Find and delete the trigger from sqlite_schema
|
||||
program.emit_insn(Insn::Rewind {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
pc_if_empty: rewind_done_label,
|
||||
});
|
||||
|
||||
program.preassign_label_to_next_insn(search_loop_label);
|
||||
|
||||
// Check if this is the trigger we're looking for
|
||||
// sqlite_schema columns: type, name, tbl_name, rootpage, sql
|
||||
// Column 0: type (should be "trigger")
|
||||
// Column 1: name (should match trigger_name)
|
||||
let type_reg = program.alloc_register();
|
||||
let name_reg = program.alloc_register();
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
column: 0,
|
||||
dest: type_reg,
|
||||
default: None,
|
||||
});
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
column: 1,
|
||||
dest: name_reg,
|
||||
default: None,
|
||||
});
|
||||
|
||||
// Check if type == "trigger"
|
||||
let type_str_reg = program.emit_string8_new_reg("trigger".to_string());
|
||||
program.emit_insn(Insn::Ne {
|
||||
lhs: type_reg,
|
||||
rhs: type_str_reg,
|
||||
target_pc: skip_non_trigger_label,
|
||||
flags: crate::vdbe::insn::CmpInsFlags::default(),
|
||||
collation: program.curr_collation(),
|
||||
});
|
||||
|
||||
// Check if name matches
|
||||
let trigger_name_str_reg = program.emit_string8_new_reg(normalized_trigger_name.clone());
|
||||
program.emit_insn(Insn::Ne {
|
||||
lhs: name_reg,
|
||||
rhs: trigger_name_str_reg,
|
||||
target_pc: skip_non_trigger_label,
|
||||
flags: crate::vdbe::insn::CmpInsFlags::default(),
|
||||
collation: program.curr_collation(),
|
||||
});
|
||||
|
||||
// Found it! Delete the row
|
||||
program.emit_insn(Insn::Delete {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
table_name: SQLITE_TABLEID.to_string(),
|
||||
is_part_of_update: false,
|
||||
});
|
||||
program.emit_insn(Insn::Goto {
|
||||
target_pc: done_label,
|
||||
});
|
||||
|
||||
program.preassign_label_to_next_insn(skip_non_trigger_label);
|
||||
// Continue to next row
|
||||
program.emit_insn(Insn::Next {
|
||||
cursor_id: sqlite_schema_cursor_id,
|
||||
pc_if_next: search_loop_label,
|
||||
});
|
||||
|
||||
program.preassign_label_to_next_insn(done_label);
|
||||
|
||||
program.preassign_label_to_next_insn(rewind_done_label);
|
||||
|
||||
// Update schema version
|
||||
program.emit_insn(Insn::SetCookie {
|
||||
db: 0,
|
||||
cookie: Cookie::SchemaVersion,
|
||||
value: (schema.schema_version + 1) as i32,
|
||||
p5: 0,
|
||||
});
|
||||
|
||||
program.emit_insn(Insn::DropTrigger {
|
||||
db: 0,
|
||||
trigger_name: normalized_trigger_name.clone(),
|
||||
});
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
@@ -7596,6 +7596,24 @@ pub fn op_drop_view(
|
||||
Ok(InsnFunctionStepResult::Step)
|
||||
}
|
||||
|
||||
pub fn op_drop_trigger(
|
||||
program: &Program,
|
||||
state: &mut ProgramState,
|
||||
insn: &Insn,
|
||||
pager: &Arc<Pager>,
|
||||
mv_store: Option<&Arc<MvStore>>,
|
||||
) -> Result<InsnFunctionStepResult> {
|
||||
load_insn!(DropTrigger { db, trigger_name }, insn);
|
||||
|
||||
let conn = program.connection.clone();
|
||||
conn.with_schema_mut(|schema| {
|
||||
schema.remove_trigger(trigger_name)?;
|
||||
Ok::<(), crate::LimboError>(())
|
||||
})?;
|
||||
state.pc += 1;
|
||||
Ok(InsnFunctionStepResult::Step)
|
||||
}
|
||||
|
||||
pub fn op_close(
|
||||
program: &Program,
|
||||
state: &mut ProgramState,
|
||||
|
||||
@@ -1407,6 +1407,15 @@ pub fn insn_to_row(
|
||||
0,
|
||||
format!("DROP TABLE {table_name}"),
|
||||
),
|
||||
Insn::DropTrigger { db, trigger_name } => (
|
||||
"DropTrigger",
|
||||
*db as i32,
|
||||
0,
|
||||
0,
|
||||
Value::build_text(trigger_name.clone()),
|
||||
0,
|
||||
format!("DROP TRIGGER {trigger_name}"),
|
||||
),
|
||||
Insn::DropView { db, view_name } => (
|
||||
"DropView",
|
||||
*db as i32,
|
||||
|
||||
@@ -980,6 +980,13 @@ pub enum Insn {
|
||||
// The name of the index being dropped
|
||||
index: Arc<Index>,
|
||||
},
|
||||
/// Drop a trigger
|
||||
DropTrigger {
|
||||
/// The database within which this trigger needs to be dropped (P1).
|
||||
db: usize,
|
||||
/// The name of the trigger being dropped
|
||||
trigger_name: String,
|
||||
},
|
||||
|
||||
/// Close a cursor.
|
||||
Close {
|
||||
@@ -1383,6 +1390,7 @@ impl InsnVariants {
|
||||
InsnVariants::Destroy => execute::op_destroy,
|
||||
InsnVariants::ResetSorter => execute::op_reset_sorter,
|
||||
InsnVariants::DropTable => execute::op_drop_table,
|
||||
InsnVariants::DropTrigger => execute::op_drop_trigger,
|
||||
InsnVariants::DropView => execute::op_drop_view,
|
||||
InsnVariants::Close => execute::op_close,
|
||||
InsnVariants::IsNull => execute::op_is_null,
|
||||
|
||||
Reference in New Issue
Block a user