linkrs_query/
dml_events.rs1use std::sync::Arc;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum DmlOp {
20 Insert,
21 Update,
22 Delete,
23}
24
25#[derive(Debug, Clone)]
30pub struct DmlStatementEvent {
31 pub op: DmlOp,
32 pub space_name: String,
33 pub rows: u64,
34}
35
36pub type DmlStatementCallback = Arc<dyn Fn(&DmlStatementEvent) + Send + Sync>;
38
39pub fn classify_dml(query_text: &str) -> Option<DmlOp> {
45 let keyword = leading_keyword(query_text);
46 if keyword.eq_ignore_ascii_case("MATCH")
47 || keyword.eq_ignore_ascii_case("WITH")
48 || keyword.eq_ignore_ascii_case("UNWIND")
49 || keyword.eq_ignore_ascii_case("OPTIONAL")
50 {
51 let upper = query_text.to_uppercase();
52 if upper.contains("DELETE") || upper.contains("DETACH DELETE") {
53 return Some(DmlOp::Delete);
54 }
55 if upper.contains("SET")
56 || upper.contains("REMOVE")
57 || upper.contains("MERGE")
58 || upper.contains("CREATE")
59 {
60 if upper.contains("MERGE") || upper.contains("CREATE") {
61 return Some(DmlOp::Insert);
62 }
63 return Some(DmlOp::Update);
64 }
65 return None;
66 }
67 if keyword.eq_ignore_ascii_case("INSERT")
68 || keyword.eq_ignore_ascii_case("CREATE")
69 || keyword.eq_ignore_ascii_case("MERGE")
70 {
71 return Some(DmlOp::Insert);
72 }
73 if keyword.eq_ignore_ascii_case("UPDATE")
74 || keyword.eq_ignore_ascii_case("SET")
75 || keyword.eq_ignore_ascii_case("REMOVE")
76 || keyword.eq_ignore_ascii_case("ALTER")
77 {
78 return Some(DmlOp::Update);
79 }
80 if keyword.eq_ignore_ascii_case("DELETE")
81 || keyword.eq_ignore_ascii_case("DETACH")
82 || keyword.eq_ignore_ascii_case("DROP")
83 {
84 return Some(DmlOp::Delete);
85 }
86 None
87}
88
89fn leading_keyword(query: &str) -> &str {
91 let bytes = query.as_bytes();
92 let mut pos = 0;
93 while pos < bytes.len() {
94 while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
95 pos += 1;
96 }
97 if pos >= bytes.len() {
98 break;
99 }
100 if query[pos..].starts_with("--")
101 || query[pos..].starts_with("//")
102 || query[pos..].starts_with('#')
103 {
104 while pos < bytes.len() && bytes[pos] != b'\n' {
105 pos += 1;
106 }
107 continue;
108 }
109 if query[pos..].starts_with("/*") {
110 if let Some(end) = query[pos..].find("*/") {
111 pos += end + 2;
112 continue;
113 }
114 return "";
115 }
116 break;
117 }
118 let rest = &query[pos..];
119 let end = rest
120 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != ':')
121 .unwrap_or(rest.len());
122 let mut token = &rest[..end];
123 if token.eq_ignore_ascii_case("OPTIONAL") || token.eq_ignore_ascii_case("DETACH") {
125 let after: &str = rest[end..].trim_start();
126 let after_end = after
127 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != ':')
128 .unwrap_or(after.len());
129 if !after[..after_end].is_empty() {
130 token = &after[..after_end];
131 }
132 }
133 token
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn leading_keywords_classify() {
142 assert_eq!(classify_dml("INSERT INTO t"), Some(DmlOp::Insert));
143 assert_eq!(classify_dml(" CREATE TAG u"), Some(DmlOp::Insert));
144 assert_eq!(classify_dml("-- comment\nMERGE (n)"), Some(DmlOp::Insert));
145 assert_eq!(classify_dml("UPDATE t SET a"), Some(DmlOp::Update));
146 assert_eq!(classify_dml("/* c */ SET n.a = 1"), Some(DmlOp::Update));
147 assert_eq!(classify_dml("DELETE FROM t"), Some(DmlOp::Delete));
148 assert_eq!(classify_dml("DETACH DELETE n"), Some(DmlOp::Delete));
149 assert_eq!(classify_dml("DROP TAG t"), Some(DmlOp::Delete));
150 }
151
152 #[test]
153 fn match_family_scans_trailing_clause() {
154 assert_eq!(classify_dml("MATCH (n) DELETE n"), Some(DmlOp::Delete));
155 assert_eq!(
156 classify_dml("MATCH (n) SET n.a = 1 RETURN n"),
157 Some(DmlOp::Update)
158 );
159 assert_eq!(
160 classify_dml("MATCH (n) RETURN n"),
161 None,
162 "pure reads stay silent"
163 );
164 }
165}