cypherlite_query/executor/operators/
delete.rs1use crate::executor::eval::eval;
4use crate::executor::{ExecutionError, Params, Record, ScalarFnLookup, TriggerLookup, Value};
5use crate::parser::ast::Expression;
6use cypherlite_core::LabelRegistry;
7use cypherlite_storage::StorageEngine;
8
9pub fn execute_delete(
13 source_records: Vec<Record>,
14 exprs: &[Expression],
15 detach: bool,
16 engine: &mut StorageEngine,
17 params: &Params,
18 scalar_fns: &dyn ScalarFnLookup,
19 trigger_fns: &dyn TriggerLookup,
20) -> Result<Vec<Record>, ExecutionError> {
21 let mut node_ids_to_delete = Vec::new();
24 let mut edge_ids_to_delete = Vec::new();
25
26 for record in &source_records {
27 for expr in exprs {
28 let val = eval(expr, record, &*engine, params, scalar_fns)?;
29 match val {
30 Value::Node(nid) => {
31 if !node_ids_to_delete.contains(&nid) {
32 node_ids_to_delete.push(nid);
33 }
34 }
35 Value::Edge(eid) => {
36 if !edge_ids_to_delete.contains(&eid) {
37 edge_ids_to_delete.push(eid);
38 }
39 }
40 Value::Null => {
41 }
43 _ => {
44 return Err(ExecutionError {
45 message: "DELETE requires a node or edge value".to_string(),
46 });
47 }
48 }
49 }
50 }
51
52 for eid in &edge_ids_to_delete {
54 let edge_props = engine
56 .get_edge(*eid)
57 .map(|e| e.properties.clone())
58 .unwrap_or_default();
59 let rel_type_name = engine.get_edge(*eid).and_then(|e| {
60 engine
61 .catalog()
62 .rel_type_name(e.rel_type_id)
63 .map(|s| s.to_string())
64 });
65 let ctx = cypherlite_core::TriggerContext {
66 entity_type: cypherlite_core::EntityType::Edge,
67 entity_id: eid.0,
68 label_or_type: rel_type_name,
69 properties: edge_props
70 .iter()
71 .map(|(k, v)| {
72 let name = engine
73 .catalog()
74 .prop_key_name(*k)
75 .unwrap_or("?")
76 .to_string();
77 (name, v.clone())
78 })
79 .collect(),
80 operation: cypherlite_core::TriggerOperation::Delete,
81 };
82 trigger_fns.fire_before_delete(&ctx)?;
83
84 engine.delete_edge(*eid).map_err(|e| ExecutionError {
85 message: format!("failed to delete edge: {}", e),
86 })?;
87
88 trigger_fns.fire_after_delete(&ctx)?;
89 }
90
91 for nid in &node_ids_to_delete {
93 if !detach {
94 let edges = engine.get_edges_for_node(*nid);
96 if !edges.is_empty() {
97 return Err(ExecutionError {
98 message: format!(
99 "cannot delete node {} because it still has {} relationship(s). Use DETACH DELETE",
100 nid.0,
101 edges.len()
102 ),
103 });
104 }
105 }
106
107 let node_props = engine
109 .get_node(*nid)
110 .map(|n| n.properties.clone())
111 .unwrap_or_default();
112 let label_name = engine
113 .get_node(*nid)
114 .and_then(|n| n.labels.first().copied())
115 .and_then(|lid| engine.catalog().label_name(lid).map(|s| s.to_string()));
116 let ctx = cypherlite_core::TriggerContext {
117 entity_type: cypherlite_core::EntityType::Node,
118 entity_id: nid.0,
119 label_or_type: label_name,
120 properties: node_props
121 .iter()
122 .map(|(k, v)| {
123 let name = engine
124 .catalog()
125 .prop_key_name(*k)
126 .unwrap_or("?")
127 .to_string();
128 (name, v.clone())
129 })
130 .collect(),
131 operation: cypherlite_core::TriggerOperation::Delete,
132 };
133 trigger_fns.fire_before_delete(&ctx)?;
134
135 engine.delete_node(*nid).map_err(|e| ExecutionError {
136 message: format!("failed to delete node: {}", e),
137 })?;
138
139 trigger_fns.fire_after_delete(&ctx)?;
140 }
141
142 Ok(source_records)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::executor::Record;
150 use cypherlite_core::{DatabaseConfig, LabelRegistry, SyncMode};
151 use tempfile::tempdir;
152
153 fn test_engine(dir: &std::path::Path) -> StorageEngine {
154 let config = DatabaseConfig {
155 path: dir.join("test.cyl"),
156 wal_sync_mode: SyncMode::Normal,
157 ..Default::default()
158 };
159 StorageEngine::open(config).expect("open")
160 }
161
162 #[test]
164 fn test_delete_node_with_edges_no_detach_fails() {
165 let dir = tempdir().expect("tempdir");
166 let mut engine = test_engine(dir.path());
167
168 let knows_type = engine.get_or_create_rel_type("KNOWS");
169 let n1 = engine.create_node(vec![], vec![]);
170 let n2 = engine.create_node(vec![], vec![]);
171 engine
172 .create_edge(n1, n2, knows_type, vec![])
173 .expect("edge");
174
175 let mut record = Record::new();
176 record.insert("n".to_string(), Value::Node(n1));
177
178 let exprs = vec![Expression::Variable("n".to_string())];
179 let params = Params::new();
180
181 let result = execute_delete(vec![record], &exprs, false, &mut engine, ¶ms, &(), &());
182 assert!(result.is_err());
183 let err = result.expect_err("should error");
184 assert!(err.message.contains("cannot delete node"));
185 assert!(err.message.contains("DETACH DELETE"));
186 }
187
188 #[test]
189 fn test_delete_node_with_detach_succeeds() {
190 let dir = tempdir().expect("tempdir");
191 let mut engine = test_engine(dir.path());
192
193 let knows_type = engine.get_or_create_rel_type("KNOWS");
194 let n1 = engine.create_node(vec![], vec![]);
195 let n2 = engine.create_node(vec![], vec![]);
196 engine
197 .create_edge(n1, n2, knows_type, vec![])
198 .expect("edge");
199
200 let mut record = Record::new();
201 record.insert("n".to_string(), Value::Node(n1));
202
203 let exprs = vec![Expression::Variable("n".to_string())];
204 let params = Params::new();
205
206 let result = execute_delete(vec![record], &exprs, true, &mut engine, ¶ms, &(), &());
207 assert!(result.is_ok());
208 assert!(engine.get_node(n1).is_none());
209 assert_eq!(engine.edge_count(), 0);
210 }
211
212 #[test]
213 fn test_delete_isolated_node() {
214 let dir = tempdir().expect("tempdir");
215 let mut engine = test_engine(dir.path());
216
217 let n1 = engine.create_node(vec![], vec![]);
218
219 let mut record = Record::new();
220 record.insert("n".to_string(), Value::Node(n1));
221
222 let exprs = vec![Expression::Variable("n".to_string())];
223 let params = Params::new();
224
225 let result = execute_delete(vec![record], &exprs, false, &mut engine, ¶ms, &(), &());
226 assert!(result.is_ok());
227 assert!(engine.get_node(n1).is_none());
228 }
229
230 #[test]
231 fn test_delete_null_is_noop() {
232 let dir = tempdir().expect("tempdir");
233 let mut engine = test_engine(dir.path());
234
235 let mut record = Record::new();
236 record.insert("n".to_string(), Value::Null);
237
238 let exprs = vec![Expression::Variable("n".to_string())];
239 let params = Params::new();
240
241 let result = execute_delete(vec![record], &exprs, false, &mut engine, ¶ms, &(), &());
242 assert!(result.is_ok());
243 }
244}