1use crate::parser::ast::Stmt;
28use linkrs_core::types::expr::{ContextualExpression, Expression};
29use linkrs_core::Value;
30
31mod delete;
32mod insert;
33mod merge;
34mod set_remove;
35mod update;
36
37pub const DML_PARAM_PREFIX: &str = "__dml_";
39
40#[derive(Debug, Clone)]
42pub struct DmlShape {
43 pub normalized_text: String,
45 pub values: Vec<Value>,
47}
48
49pub fn normalize_shape(stmt: &Stmt) -> Option<DmlShape> {
55 let mut values = Vec::new();
56 let normalized_text = match stmt {
57 Stmt::Insert(s) => insert::render_insert(s, &mut values)?,
58 Stmt::Delete(s) => delete::render_delete(s, &mut values)?,
59 Stmt::Update(s) => update::render_update(s, &mut values)?,
60 Stmt::Merge(s) => merge::render_merge(s, &mut values)?,
61 Stmt::Set(s) => set_remove::render_set(s, &mut values)?,
62 Stmt::Remove(s) => set_remove::render_remove(s, &mut values)?,
63 _ => return None,
64 };
65 if values.is_empty() {
66 return None;
67 }
68 Some(DmlShape {
69 normalized_text,
70 values,
71 })
72}
73
74pub(crate) fn render_contextual(
81 out: &mut String,
82 values: &mut Vec<Value>,
83 expr: &ContextualExpression,
84) -> Option<()> {
85 render_expr(out, values, expr.expression()?.inner())
86}
87
88fn render_expr(out: &mut String, values: &mut Vec<Value>, expr: &Expression) -> Option<()> {
94 match expr {
95 Expression::Literal(value) => {
96 push_param(out, values, value.clone());
97 Some(())
98 }
99 Expression::Variable(name) => {
100 out.push_str(name);
101 Some(())
102 }
103 Expression::Parameter(name) => {
104 out.push('@');
105 out.push_str(name);
106 Some(())
107 }
108 Expression::SessionVariable(name) => {
109 out.push('$');
110 out.push_str(name);
111 Some(())
112 }
113 Expression::Property { object, property } => {
114 render_expr(out, values, object)?;
115 out.push('.');
116 out.push_str(property);
117 Some(())
118 }
119 Expression::Binary { left, op, right } => {
120 if matches!(
124 op,
125 linkrs_core::types::operators::BinaryOperator::In
126 | linkrs_core::types::operators::BinaryOperator::NotIn
127 | linkrs_core::types::operators::BinaryOperator::Subscript
128 | linkrs_core::types::operators::BinaryOperator::Attribute
129 | linkrs_core::types::operators::BinaryOperator::JsonGet
130 | linkrs_core::types::operators::BinaryOperator::JsonGetText
131 | linkrs_core::types::operators::BinaryOperator::JsonPathGet
132 | linkrs_core::types::operators::BinaryOperator::JsonPathGetText
133 ) {
134 return None;
135 }
136 out.push('(');
137 render_expr(out, values, left)?;
138 out.push(' ');
139 out.push_str(op.name());
140 out.push(' ');
141 render_expr(out, values, right)?;
142 out.push(')');
143 Some(())
144 }
145 Expression::Unary { op, operand } => {
146 out.push('(');
147 out.push_str(op.name());
148 out.push(' ');
149 render_expr(out, values, operand)?;
150 out.push(')');
151 Some(())
152 }
153 Expression::Function { name, args } => {
154 out.push_str(name);
155 out.push('(');
156 for (index, arg) in args.iter().enumerate() {
157 if index > 0 {
158 out.push_str(", ");
159 }
160 render_expr(out, values, arg.as_expr())?;
161 }
162 out.push(')');
163 Some(())
164 }
165 Expression::List(items) => {
166 out.push('[');
167 for (index, item) in items.iter().enumerate() {
168 if index > 0 {
169 out.push_str(", ");
170 }
171 render_expr(out, values, item)?;
172 }
173 out.push(']');
174 Some(())
175 }
176 Expression::Map(pairs) => {
177 out.push('{');
178 for (index, (key, value)) in pairs.iter().enumerate() {
179 if index > 0 {
180 out.push_str(", ");
181 }
182 out.push_str(key);
183 out.push_str(": ");
184 render_expr(out, values, value)?;
185 }
186 out.push('}');
187 Some(())
188 }
189 Expression::Label(name) => {
190 out.push(':');
191 out.push_str(name);
192 Some(())
193 }
194 Expression::TagProperty { tag_name, property } => {
195 out.push_str(tag_name);
196 out.push('.');
197 out.push_str(property);
198 Some(())
199 }
200 Expression::EdgeProperty {
201 edge_name,
202 property,
203 } => {
204 out.push_str(edge_name);
205 out.push('.');
206 out.push_str(property);
207 Some(())
208 }
209 Expression::LabelTagProperty { tag, property } => {
210 match tag.as_ref() {
211 Expression::Variable(name) => out.push_str(name),
212 Expression::Label(name) => out.push_str(name),
213 _ => return None,
214 }
215 out.push('.');
216 out.push_str(property);
217 Some(())
218 }
219 _ => None,
220 }
221}
222
223fn push_param(out: &mut String, values: &mut Vec<Value>, value: Value) {
225 let index = values.len();
226 out.push('@');
227 out.push_str(DML_PARAM_PREFIX);
228 out.push_str(&index.to_string());
229 values.push(value);
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::parser::ast::{InsertTarget, UpdateTarget};
236 use crate::parser::Parser;
237
238 fn parse(query: &str) -> Stmt {
239 let mut parser = Parser::new(query);
240 let result = parser
241 .parse()
242 .unwrap_or_else(|e| panic!("parse failed for {query:?}: {e}"));
243 assert!(!parser.has_errors(), "parse errors for {query:?}");
244 result.ast.stmt().clone()
245 }
246
247 #[test]
248 fn normalizes_insert_vertex_shape() {
249 let query = "INSERT VERTEX person(name, age) VALUES \"p00001\": (\"Alice\", 30)";
250 let stmt = parse(query);
251 let shape = normalize_shape(&stmt).expect("shape should normalize");
252 assert_eq!(
253 shape.normalized_text,
254 "INSERT VERTEX person(name, age) VALUES @__dml_0: (@__dml_1, @__dml_2)"
255 );
256 assert_eq!(shape.values.len(), 3);
257 assert_eq!(shape.values[0], Value::from("p00001"));
258 assert_eq!(shape.values[1], Value::from("Alice"));
259 assert_eq!(shape.values[2], Value::from(30i64));
260 }
261
262 #[test]
263 fn normalizes_insert_edge_shape() {
264 let query = "INSERT EDGE works_at(position, salary) VALUES \"p00001\" -> \"c0001\" @0: (\"Engineer\", 45000)";
265 let stmt = parse(query);
266 let shape = normalize_shape(&stmt).expect("shape should normalize");
267 assert_eq!(
268 shape.normalized_text,
269 "INSERT EDGE works_at(position, salary) VALUES @__dml_0 -> @__dml_1 @@__dml_2: (@__dml_3, @__dml_4)"
270 );
271 assert_eq!(shape.values.len(), 5);
272 }
273
274 #[test]
275 fn same_shape_produces_same_normalized_text() {
276 let a = parse("INSERT VERTEX user(id, name) VALUES \"u1\": (\"Bob\", 1)");
277 let b = parse("INSERT VERTEX user(id, name) VALUES \"u2\": (\"Alice\", 2)");
278 let shape_a = normalize_shape(&a).expect("shape a");
279 let shape_b = normalize_shape(&b).expect("shape b");
280 assert_eq!(shape_a.normalized_text, shape_b.normalized_text);
281 }
282
283 #[test]
284 fn normalized_text_reparses_with_parameters() {
285 let query = "INSERT VERTEX person(name, age) VALUES \"p00001\": (\"Alice\", 30)";
286 let stmt = parse(query);
287 let shape = normalize_shape(&stmt).expect("shape should normalize");
288 let reparsed = parse(&shape.normalized_text);
289 match reparsed {
290 Stmt::Insert(insert) => {
291 assert!(matches!(insert.target, InsertTarget::Vertices { .. }));
292 if let InsertTarget::Vertices { values, .. } = &insert.target {
293 assert!(values[0].vid.is_parameter());
294 }
295 }
296 _ => panic!("reparse should produce INSERT"),
297 }
298 }
299
300 #[test]
301 fn normalizes_if_not_exists_and_multi_row() {
302 let query =
303 "INSERT VERTEX IF NOT EXISTS person(name) VALUES \"p1\": (\"A\"), \"p2\": (\"B\")";
304 let stmt = parse(query);
305 let shape = normalize_shape(&stmt).expect("shape should normalize");
306 assert_eq!(
307 shape.normalized_text,
308 "INSERT VERTEX IF NOT EXISTS person(name) VALUES @__dml_0: (@__dml_1), @__dml_2: (@__dml_3)"
309 );
310 assert_eq!(shape.values.len(), 4);
311 }
312
313 #[test]
314 fn parameterizes_nested_literal_in_insert() {
315 let query = "INSERT VERTEX user(id, name) VALUES \"u1\": (upper(\"bob\"), 1)";
316 let stmt = parse(query);
317 let shape = normalize_shape(&stmt).expect("shape should normalize");
318 assert_eq!(
319 shape.normalized_text,
320 "INSERT VERTEX user(id, name) VALUES @__dml_0: (upper(@__dml_1), @__dml_2)"
321 );
322 }
323
324 #[test]
325 fn skips_unrenderable_expression() {
326 let query = "UPDATE VERTEX \"v1\" SET name = \"x\" WHERE arr[0] > 1";
327 let stmt = parse(query);
328 assert!(normalize_shape(&stmt).is_none());
329 }
330
331 #[test]
332 fn skip_non_dml() {
333 let stmt = parse("MATCH (n:person) RETURN n");
334 assert!(normalize_shape(&stmt).is_none());
335 }
336
337 #[test]
338 fn normalizes_update_vertex() {
339 let query = "UPDATE VERTEX \"v1\" SET name = \"x\"";
340 let stmt = parse(query);
341 let shape = normalize_shape(&stmt).expect("shape should normalize");
342 assert_eq!(
343 shape.normalized_text,
344 "UPDATE VERTEX @__dml_0 SET name = @__dml_1"
345 );
346 assert_eq!(shape.values.len(), 2);
347 }
348
349 #[test]
350 fn normalizes_update_edge_long_form() {
351 let query = "UPDATE EDGE OF works_at FROM \"p1\" TO \"p2\" @1 SET salary = 100";
352 let stmt = parse(query);
353 let shape = normalize_shape(&stmt).expect("shape should normalize");
354 assert_eq!(
355 shape.normalized_text,
356 "UPDATE EDGE OF works_at FROM @__dml_0 TO @__dml_1 @@__dml_2 SET salary = @__dml_3"
357 );
358 }
359
360 #[test]
361 fn normalizes_upsert_edge_short_form() {
362 let query = "UPSERT EDGE \"p1\" -> \"p2\" @1 OF works_at SET salary = 100";
363 let stmt = parse(query);
364 let shape = normalize_shape(&stmt).expect("shape should normalize");
365 assert_eq!(
366 shape.normalized_text,
367 "UPSERT EDGE @__dml_0 -> @__dml_1 @@__dml_2 OF works_at SET salary = @__dml_3"
368 );
369 }
370
371 #[test]
372 fn normalizes_update_with_where() {
373 let query = "UPDATE VERTEX \"v1\" SET name = \"x\" WHERE age > 30";
374 let stmt = parse(query);
375 let shape = normalize_shape(&stmt).expect("shape should normalize");
376 assert_eq!(
377 shape.normalized_text,
378 "UPDATE VERTEX @__dml_0 SET name = @__dml_1 WHERE (age > @__dml_2)"
379 );
380 }
381
382 #[test]
383 fn update_reparse_matches_target() {
384 let query = "UPDATE VERTEX \"v1\" SET name = \"x\" WHERE age > 30";
385 let stmt = parse(query);
386 let shape = normalize_shape(&stmt).expect("shape should normalize");
387 let reparsed = parse(&shape.normalized_text);
388 match reparsed {
389 Stmt::Update(update) => {
390 assert!(matches!(update.target, UpdateTarget::Vertex(_)));
391 assert!(update.where_clause.is_some());
392 }
393 _ => panic!("reparse should produce UPDATE"),
394 }
395 }
396
397 #[test]
398 fn update_with_yield_is_skipped() {
399 let query = "UPSERT VERTEX ON person SET name = \"x\" WHERE id(vid) == \"v1\" YIELD name";
400 let stmt = parse(query);
401 assert!(normalize_shape(&stmt).is_none());
402 }
403
404 #[test]
405 fn normalizes_delete_vertex_list() {
406 let query = "DELETE VERTEX person FROM \"v1\", \"v2\"";
407 let stmt = parse(query);
408 let shape = normalize_shape(&stmt).expect("shape should normalize");
409 assert_eq!(
410 shape.normalized_text,
411 "DELETE VERTEX person FROM @__dml_0, @__dml_1"
412 );
413 }
414
415 #[test]
416 fn normalizes_delete_edge() {
417 let query = "DELETE EDGE works_at \"s1\" -> \"d1\" @0";
418 let stmt = parse(query);
419 let shape = normalize_shape(&stmt).expect("shape should normalize");
420 assert_eq!(
421 shape.normalized_text,
422 "DELETE EDGE works_at @__dml_0 -> @__dml_1 @@__dml_2"
423 );
424 }
425
426 #[test]
427 fn normalizes_delete_tag_with_edge() {
428 let query = "DELETE VERTEX person FROM \"v1\" WITH EDGE";
429 let stmt = parse(query);
430 let shape = normalize_shape(&stmt).expect("shape should normalize");
431 assert_eq!(
432 shape.normalized_text,
433 "DELETE VERTEX person FROM @__dml_0 WITH EDGE"
434 );
435 }
436
437 #[test]
438 fn normalizes_merge_node_with_properties() {
439 let query = "MERGE (n:person {name: \"a\"}) ON CREATE SET age = 1";
440 let stmt = parse(query);
441 let shape = normalize_shape(&stmt).expect("shape should normalize");
442 assert_eq!(
443 shape.normalized_text,
444 "MERGE (n:person {name: @__dml_0}) ON CREATE SET age = @__dml_1"
445 );
446 }
447
448 #[test]
449 fn normalizes_merge_path() {
450 let query = "MERGE (a:person)-[:knows]->(b:person) ON MATCH SET score = 5";
451 let stmt = parse(query);
452 let shape = normalize_shape(&stmt).expect("shape should normalize");
453 assert_eq!(
454 shape.normalized_text,
455 "MERGE (a:person)-[:knows]->(b:person) ON MATCH SET score = @__dml_0"
456 );
457 }
458
459 #[test]
460 fn normalizes_set_statement() {
461 let query = "SET name = \"x\", age = 30";
462 let stmt = parse(query);
463 let shape = normalize_shape(&stmt).expect("shape should normalize");
464 assert_eq!(shape.normalized_text, "SET name = @__dml_0, age = @__dml_1");
465 }
466
467 #[test]
468 fn remove_without_literals_is_skipped() {
469 let query = "REMOVE v.name, v.age";
470 let stmt = parse(query);
471 assert!(normalize_shape(&stmt).is_none());
472 }
473
474 #[test]
475 fn skips_non_roundtrippable_binary_operators() {
476 use linkrs_core::types::operators::BinaryOperator;
477 let cases = [
478 BinaryOperator::In,
479 BinaryOperator::NotIn,
480 BinaryOperator::Subscript,
481 BinaryOperator::Attribute,
482 BinaryOperator::JsonGet,
483 BinaryOperator::JsonGetText,
484 BinaryOperator::JsonPathGet,
485 BinaryOperator::JsonPathGetText,
486 ];
487 for op in cases {
488 let expr = Expression::binary(Expression::variable("a"), op, Expression::variable("b"));
489 let mut out = String::new();
490 let mut values = Vec::new();
491 assert!(
492 render_expr(&mut out, &mut values, &expr).is_none(),
493 "op {op:?} should be excluded"
494 );
495 }
496 }
497}