From d4b487eebc22359e57f31ef10bfd1cea93698280 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Tue, 18 Nov 2025 12:14:27 +0200 Subject: [PATCH] triggers: add in-memory schema entries --- core/schema.rs | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/core/schema.rs b/core/schema.rs index affb19440..c87a04381 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -74,6 +74,47 @@ impl Clone for View { /// Type alias for regular views collection pub type ViewsMap = HashMap>; +/// Trigger structure +#[derive(Debug, Clone)] +pub struct Trigger { + pub name: String, + pub sql: String, + pub table_name: String, + pub time: turso_parser::ast::TriggerTime, + pub event: turso_parser::ast::TriggerEvent, + pub for_each_row: bool, + pub when_clause: Option, + pub commands: Vec, + pub temporary: bool, +} + +impl Trigger { + #[allow(clippy::too_many_arguments)] + pub fn new( + name: String, + sql: String, + table_name: String, + time: Option, + event: turso_parser::ast::TriggerEvent, + for_each_row: bool, + when_clause: Option, + commands: Vec, + temporary: bool, + ) -> Self { + Self { + name, + sql, + table_name, + time: time.unwrap_or(turso_parser::ast::TriggerTime::Before), + event, + for_each_row, + when_clause, + commands, + temporary, + } + } +} + use crate::storage::btree::{BTreeCursor, CursorTrait}; use crate::translate::collate::CollationSeq; use crate::translate::plan::{SelectPlan, TableReferences}; @@ -130,6 +171,9 @@ pub struct Schema { pub views: ViewsMap, + /// table_name to list of triggers + pub triggers: HashMap>>, + /// table_name to list of indexes for the table pub indexes: HashMap>>, pub has_indexes: std::collections::HashSet, @@ -163,6 +207,7 @@ impl Schema { let materialized_view_sql = HashMap::new(); let incremental_views = HashMap::new(); let views: ViewsMap = HashMap::new(); + let triggers = HashMap::new(); let table_to_materialized_views: HashMap> = HashMap::new(); let incompatible_views = HashSet::new(); Self { @@ -171,6 +216,7 @@ impl Schema { materialized_view_sql, incremental_views, views, + triggers, indexes, has_indexes, indexes_enabled, @@ -310,6 +356,71 @@ impl Schema { self.views.get(&name).cloned() } + pub fn add_trigger(&mut self, trigger: Trigger, table_name: &str) -> Result<()> { + self.check_object_name_conflict(&trigger.name)?; + let table_name = normalize_ident(table_name); + + self.triggers + .entry(table_name) + .or_default() + .push(Arc::new(trigger)); + + Ok(()) + } + + pub fn remove_trigger(&mut self, name: &str) -> Result<()> { + let name = normalize_ident(name); + + let mut removed = false; + for triggers_list in self.triggers.values_mut() { + for i in 0..triggers_list.len() { + let trigger = &triggers_list[i]; + if normalize_ident(&trigger.name) == name { + removed = true; + triggers_list.remove(i); + break; + } + } + if removed { + break; + } + } + if !removed { + return Err(crate::LimboError::ParseError(format!( + "no such trigger: {name}" + ))); + } + Ok(()) + } + + pub fn get_trigger_for_table(&self, table_name: &str, name: &str) -> Option> { + let table_name = normalize_ident(table_name); + let name = normalize_ident(name); + self.triggers + .get(&table_name) + .and_then(|triggers| triggers.iter().find(|t| t.name == name).cloned()) + } + + pub fn get_triggers_for_table( + &self, + table_name: &str, + ) -> impl Iterator> + Clone { + let table_name = normalize_ident(table_name); + self.triggers + .get(&table_name) + .map(|triggers| triggers.iter()) + .unwrap_or_default() + } + + pub fn get_trigger(&self, name: &str) -> Option> { + let name = normalize_ident(name); + self.triggers + .values() + .flatten() + .find(|t| t.name == name) + .cloned() + } + pub fn add_btree_table(&mut self, table: Arc) -> Result<()> { self.check_object_name_conflict(&table.name)?; let name = normalize_ident(&table.name); @@ -856,6 +967,45 @@ impl Schema { } } } + "trigger" => { + use turso_parser::ast::{Cmd, Stmt}; + use turso_parser::parser::Parser; + + let sql = maybe_sql.expect("sql should be present for trigger"); + let trigger_name = name.to_string(); + + let mut parser = Parser::new(sql.as_bytes()); + let Ok(Some(Cmd::Stmt(Stmt::CreateTrigger { + temporary, + if_not_exists: _, + trigger_name: _, + time, + event, + tbl_name, + for_each_row, + when_clause, + commands, + }))) = parser.next_cmd() + else { + return Err(crate::LimboError::ParseError(format!( + "invalid trigger sql: {sql}" + ))); + }; + self.add_trigger( + Trigger::new( + trigger_name.clone(), + sql.to_string(), + tbl_name.name.to_string(), + time, + event, + for_each_row, + when_clause.map(|e| *e), + commands, + temporary, + ), + tbl_name.name.as_str(), + )?; + } _ => {} }; @@ -1198,6 +1348,16 @@ impl Clone for Schema { .iter() .map(|(name, view)| (name.clone(), Arc::new((**view).clone()))) .collect(); + let triggers = self + .triggers + .iter() + .map(|(table_name, triggers)| { + ( + table_name.clone(), + triggers.iter().map(|t| Arc::new((**t).clone())).collect(), + ) + }) + .collect(); let incompatible_views = self.incompatible_views.clone(); Self { tables, @@ -1205,6 +1365,7 @@ impl Clone for Schema { materialized_view_sql, incremental_views, views, + triggers, indexes, has_indexes: self.has_indexes.clone(), indexes_enabled: self.indexes_enabled,