Skip to main content

dbx_core/automation/
trigger_parser.rs

1//! SQL Trigger Parser
2//!
3//! CREATE TRIGGER, DROP TRIGGER SQL 파싱
4
5use crate::automation::{ForEachType, Trigger, TriggerOperation, TriggerTiming};
6use crate::error::{DbxError, DbxResult};
7use crate::sql::StringCaseExt;
8
9/// CREATE TRIGGER 파싱
10///
11/// # 문법
12/// ```sql
13/// CREATE TRIGGER trigger_name
14/// { BEFORE | AFTER } { INSERT | UPDATE | DELETE }
15/// ON table_name
16/// [ FOR EACH { ROW | STATEMENT } ]
17/// [ WHEN ( condition ) ]
18/// BEGIN
19///   sql_statement;
20///   ...
21/// END;
22/// ```
23pub fn parse_create_trigger(sql: &str) -> DbxResult<Trigger> {
24    // 간단한 수동 파싱 (sqlparser는 CREATE TRIGGER를 완전히 지원하지 않음)
25    let sql = sql.trim();
26
27    // CREATE TRIGGER 확인
28    if !sql.starts_with_ignore_ascii_case("CREATE TRIGGER") {
29        return Err(DbxError::InvalidOperation {
30            message: format!("Expected CREATE TRIGGER, got: {}", sql),
31            context: "SQL parsing".to_string(),
32        });
33    }
34
35    // 토큰 분리
36    let tokens: Vec<&str> = sql.split_whitespace().collect();
37    if tokens.len() < 7 {
38        return Err(DbxError::InvalidOperation {
39            message: "Invalid CREATE TRIGGER syntax".to_string(),
40            context: "SQL parsing".to_string(),
41        });
42    }
43
44    // Trigger 이름 추출 (CREATE TRIGGER name)
45    let name = tokens[2].to_string();
46
47    // Timing 추출 (BEFORE/AFTER)
48    let timing = match tokens[3].to_uppercase().as_str() {
49        "BEFORE" => TriggerTiming::Before,
50        "AFTER" => TriggerTiming::After,
51        _ => {
52            return Err(DbxError::InvalidOperation {
53                message: format!("Invalid timing: {}", tokens[3]),
54                context: "CREATE TRIGGER parsing".to_string(),
55            });
56        }
57    };
58
59    // Operation 추출 (INSERT/UPDATE/DELETE)
60    let operation = match tokens[4].to_uppercase().as_str() {
61        "INSERT" => TriggerOperation::Insert,
62        "UPDATE" => TriggerOperation::Update,
63        "DELETE" => TriggerOperation::Delete,
64        _ => {
65            return Err(DbxError::InvalidOperation {
66                message: format!("Invalid operation: {}", tokens[4]),
67                context: "CREATE TRIGGER parsing".to_string(),
68            });
69        }
70    };
71
72    // ON 확인
73    if tokens[5].to_uppercase() != "ON" {
74        return Err(DbxError::InvalidOperation {
75            message: format!("Expected ON, got: {}", tokens[5]),
76            context: "CREATE TRIGGER parsing".to_string(),
77        });
78    }
79
80    // 테이블 이름 추출
81    let table = tokens[6].to_string();
82
83    // FOR EACH 추출 (선택적)
84    let mut for_each = ForEachType::Row; // 기본값
85    let mut body_start_idx = 7;
86
87    if tokens.len() > 9 && tokens[7].to_uppercase() == "FOR" && tokens[8].to_uppercase() == "EACH" {
88        for_each = match tokens[9].to_uppercase().as_str() {
89            "ROW" => ForEachType::Row,
90            "STATEMENT" => ForEachType::Statement,
91            _ => {
92                return Err(DbxError::InvalidOperation {
93                    message: format!("Invalid FOR EACH type: {}", tokens[9]),
94                    context: "CREATE TRIGGER parsing".to_string(),
95                });
96            }
97        };
98        body_start_idx = 10;
99    }
100
101    // WHEN 조건 추출 (선택적)
102    let mut condition = None;
103    if tokens.len() > body_start_idx && tokens[body_start_idx].to_uppercase() == "WHEN" {
104        // WHEN (condition) 형태 파싱
105        let when_start = sql.find("WHEN").unwrap();
106        let begin_pos = sql
107            .find("BEGIN")
108            .ok_or_else(|| DbxError::InvalidOperation {
109                message: "Missing BEGIN".to_string(),
110                context: "CREATE TRIGGER parsing".to_string(),
111            })?;
112        let condition_str = sql[when_start + 4..begin_pos].trim();
113
114        // 괄호 제거
115        let condition_str = condition_str
116            .trim_start_matches('(')
117            .trim_end_matches(')')
118            .trim();
119        condition = Some(condition_str.to_string());
120    }
121
122    // BEGIN ... END 사이의 SQL 문장들 추출
123    let begin_pos = sql
124        .find("BEGIN")
125        .ok_or_else(|| DbxError::InvalidOperation {
126            message: "Missing BEGIN".to_string(),
127            context: "CREATE TRIGGER parsing".to_string(),
128        })?;
129    let end_pos = sql.rfind("END").ok_or_else(|| DbxError::InvalidOperation {
130        message: "Missing END".to_string(),
131        context: "CREATE TRIGGER parsing".to_string(),
132    })?;
133
134    let body_sql = sql[begin_pos + 5..end_pos].trim();
135
136    // 세미콜론으로 분리
137    let body: Vec<String> = body_sql
138        .split(';')
139        .map(|s| s.trim().to_string())
140        .filter(|s| !s.is_empty())
141        .collect();
142
143    if body.is_empty() {
144        return Err(DbxError::InvalidOperation {
145            message: "Trigger body is empty".to_string(),
146            context: "CREATE TRIGGER parsing".to_string(),
147        });
148    }
149
150    Ok(Trigger::new(
151        name, timing, operation, table, for_each, condition, body,
152    ))
153}
154
155/// DROP TRIGGER 파싱
156///
157/// # 문법
158/// ```sql
159/// DROP TRIGGER trigger_name;
160/// ```
161pub fn parse_drop_trigger(sql: &str) -> DbxResult<String> {
162    let sql = sql.trim();
163
164    if !sql.starts_with_ignore_ascii_case("DROP TRIGGER") {
165        return Err(DbxError::InvalidOperation {
166            message: format!("Expected DROP TRIGGER, got: {}", sql),
167            context: "SQL parsing".to_string(),
168        });
169    }
170
171    let tokens: Vec<&str> = sql.split_whitespace().collect();
172    if tokens.len() < 3 {
173        return Err(DbxError::InvalidOperation {
174            message: "Invalid DROP TRIGGER syntax".to_string(),
175            context: "SQL parsing".to_string(),
176        });
177    }
178
179    let name = tokens[2].trim_end_matches(';').to_string();
180    Ok(name)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_parse_create_trigger_basic() {
189        let sql = r#"
190            CREATE TRIGGER audit_trigger
191            AFTER INSERT ON users
192            BEGIN
193                INSERT INTO audit_logs VALUES (NEW.id, 'INSERT');
194            END;
195        "#;
196
197        let trigger = parse_create_trigger(sql).unwrap();
198        assert_eq!(trigger.name, "audit_trigger");
199        assert_eq!(trigger.timing, TriggerTiming::After);
200        assert_eq!(trigger.operation, TriggerOperation::Insert);
201        assert_eq!(trigger.table, "users");
202        assert_eq!(trigger.for_each, ForEachType::Row);
203        assert!(trigger.condition.is_none());
204        assert_eq!(trigger.body.len(), 1);
205    }
206
207    #[test]
208    fn test_parse_create_trigger_with_condition() {
209        let sql = r#"
210            CREATE TRIGGER check_age
211            BEFORE INSERT ON users
212            FOR EACH ROW
213            WHEN (NEW.age > 18)
214            BEGIN
215                INSERT INTO adult_users VALUES (NEW.id);
216            END;
217        "#;
218
219        let trigger = parse_create_trigger(sql).unwrap();
220        assert_eq!(trigger.name, "check_age");
221        assert_eq!(trigger.timing, TriggerTiming::Before);
222        assert_eq!(trigger.for_each, ForEachType::Row);
223        assert!(trigger.condition.is_some());
224        assert_eq!(trigger.condition.unwrap(), "NEW.age > 18");
225    }
226
227    #[test]
228    fn test_parse_create_trigger_multiple_statements() {
229        let sql = r#"
230            CREATE TRIGGER multi_action
231            AFTER UPDATE ON products
232            BEGIN
233                UPDATE logs SET count = count + 1;
234                INSERT INTO history VALUES (OLD.id, NEW.id);
235            END;
236        "#;
237
238        let trigger = parse_create_trigger(sql).unwrap();
239        assert_eq!(trigger.body.len(), 2);
240    }
241
242    #[test]
243    fn test_parse_drop_trigger() {
244        let sql = "DROP TRIGGER audit_trigger;";
245        let name = parse_drop_trigger(sql).unwrap();
246        assert_eq!(name, "audit_trigger");
247    }
248
249    #[test]
250    fn test_parse_create_trigger_invalid() {
251        let sql = "CREATE TABLE users (id INT);";
252        assert!(parse_create_trigger(sql).is_err());
253    }
254}