1use super::super::statement::{GraphStmt, NodedbStatement};
6use super::{edge_predicate, tokenizer, variants};
7use crate::error::SqlError;
8
9pub fn try_parse(sql: &str) -> Option<Result<NodedbStatement, SqlError>> {
18 let trimmed = sql.trim();
19 let upper = trimmed.to_ascii_uppercase();
20
21 if upper.starts_with("MATCH ") || upper.starts_with("OPTIONAL MATCH ") {
22 return Some(Ok(NodedbStatement::Graph(GraphStmt::MatchQuery {
23 body: trimmed.to_string(),
24 })));
25 }
26
27 if !upper.starts_with("GRAPH ") {
28 return None;
29 }
30
31 Some(parse_graph(trimmed))
32}
33
34fn parse_graph(sql: &str) -> Result<NodedbStatement, SqlError> {
38 let blanked = tokenizer::blank_comments(sql);
39 let trimmed = blanked.trim();
40 let upper = trimmed.to_ascii_uppercase();
41 let upper = upper.as_str();
42 let (head, predicate_text) = edge_predicate::split_edge_where(trimmed)?;
43 let predicate = match predicate_text {
44 Some(text) => edge_predicate::parse_edge_predicate(text)?,
45 None => Vec::new(),
46 };
47 let toks = tokenizer::tokenize(head);
48
49 if upper.starts_with("GRAPH TRAVERSE ") {
50 return variants::parse_traverse(&toks, predicate);
51 }
52 if upper.starts_with("GRAPH PATH ") {
53 return variants::parse_path(&toks, predicate);
54 }
55 if predicate_text.is_some() {
56 return Err(SqlError::Parse {
57 detail: format!(
58 "{} does not accept EDGE WHERE: only GRAPH TRAVERSE and GRAPH PATH filter on \
59 edge properties",
60 graph_command(upper)
61 ),
62 });
63 }
64
65 if upper.starts_with("GRAPH INSERT EDGE ") {
66 variants::parse_insert_edge(&toks)
67 } else if upper.starts_with("GRAPH DELETE EDGE ") {
68 variants::parse_delete_edge(&toks)
69 } else if upper.starts_with("GRAPH LABEL ") {
70 variants::parse_set_labels(&toks, false)
71 } else if upper.starts_with("GRAPH UNLABEL ") {
72 variants::parse_set_labels(&toks, true)
73 } else if upper.starts_with("GRAPH NEIGHBORS ") {
74 variants::parse_neighbors(&toks)
75 } else if upper.starts_with("GRAPH ALGO ") {
76 variants::parse_algo(&toks)
77 } else if upper.starts_with("GRAPH RAG FUSION ") {
78 variants::parse_rag_fusion(&toks)
79 } else {
80 Err(SqlError::Parse {
83 detail: "unrecognised GRAPH command".to_owned(),
84 })
85 }
86}
87
88fn graph_command(upper: &str) -> String {
91 const MULTI_WORD: [&str; 3] = ["GRAPH INSERT EDGE", "GRAPH DELETE EDGE", "GRAPH RAG FUSION"];
92 if let Some(command) = MULTI_WORD.iter().find(|c| upper.starts_with(*c)) {
93 return (*command).to_owned();
94 }
95 upper
96 .split_whitespace()
97 .take(2)
98 .collect::<Vec<_>>()
99 .join(" ")
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::ddl_ast::statement::{GraphDirection, GraphProperties};
106
107 fn parsed(sql: &str) -> NodedbStatement {
109 try_parse(sql)
110 .expect("input is graph DSL")
111 .expect("statement is well-formed")
112 }
113
114 #[test]
115 fn parse_graph_insert_edge_keyword_shaped_ids() {
116 let stmt = parsed("GRAPH INSERT EDGE IN 'myedges' FROM 'TO' TO 'FROM' TYPE 'LABEL'");
117 match stmt {
118 NodedbStatement::Graph(GraphStmt::GraphInsertEdge {
119 collection,
120 src,
121 dst,
122 label,
123 properties,
124 }) => {
125 assert_eq!(collection, "myedges");
126 assert_eq!(src, "TO");
127 assert_eq!(dst, "FROM");
128 assert_eq!(label, "LABEL");
129 assert_eq!(properties, GraphProperties::None);
130 }
131 other => panic!("expected GraphInsertEdge, got {other:?}"),
132 }
133 }
134
135 #[test]
136 fn parse_graph_delete_edge_with_collection() {
137 let stmt = parsed("GRAPH DELETE EDGE IN 'myedges' FROM 'a' TO 'b' TYPE 'l'");
138 match stmt {
139 NodedbStatement::Graph(GraphStmt::GraphDeleteEdge {
140 collection,
141 src,
142 dst,
143 label,
144 }) => {
145 assert_eq!(collection, "myedges");
146 assert_eq!(src, "a");
147 assert_eq!(dst, "b");
148 assert_eq!(label, "l");
149 }
150 other => panic!("expected GraphDeleteEdge, got {other:?}"),
151 }
152 }
153
154 #[test]
158 fn parse_graph_insert_edge_missing_collection_names_the_clause() {
159 let error = try_parse("GRAPH INSERT EDGE FROM 'a' TO 'b' TYPE 'l'")
160 .expect("input is graph DSL")
161 .expect_err("missing IN <collection> must not produce a statement");
162 assert!(
163 error.to_string().contains("IN <collection>"),
164 "the error must name the missing clause: {error}"
165 );
166 }
167
168 #[test]
169 fn parse_graph_insert_edge_with_object_properties() {
170 let stmt = parsed(
171 "GRAPH INSERT EDGE IN 'edges' FROM 'a' TO 'b' TYPE 'l' PROPERTIES { note: '} DEPTH 999' }",
172 );
173 match stmt {
174 NodedbStatement::Graph(GraphStmt::GraphInsertEdge {
175 collection,
176 properties,
177 ..
178 }) => {
179 assert_eq!(collection, "edges");
180 match properties {
181 GraphProperties::Object(s) => assert!(s.contains("} DEPTH 999")),
182 other => panic!("expected Object properties, got {other:?}"),
183 }
184 }
185 other => panic!("expected GraphInsertEdge, got {other:?}"),
186 }
187 }
188
189 #[test]
190 fn parse_graph_traverse_keyword_substring_id() {
191 let stmt =
192 parsed("GRAPH TRAVERSE IN 'kw' FROM 'node_with_DEPTH_in_name' DEPTH 2 LABEL 'l'");
193 match stmt {
194 NodedbStatement::Graph(GraphStmt::GraphTraverse {
195 collection,
196 start,
197 depth,
198 ..
199 }) => {
200 assert_eq!(collection, "kw");
201 assert_eq!(start, "node_with_DEPTH_in_name");
202 assert_eq!(depth, 2);
203 }
204 other => panic!("expected GraphTraverse, got {other:?}"),
205 }
206 }
207
208 #[test]
209 fn parse_graph_path() {
210 let stmt = parsed("GRAPH PATH IN 'docs' FROM 'a' TO 'b' MAX_DEPTH 5 LABEL 'l'");
211 match stmt {
212 NodedbStatement::Graph(GraphStmt::GraphPath {
213 collection,
214 src,
215 dst,
216 max_depth,
217 edge_labels,
218 edge_predicate,
219 }) => {
220 assert!(edge_predicate.is_empty());
221 assert_eq!(collection, "docs");
222 assert_eq!(src, "a");
223 assert_eq!(dst, "b");
224 assert_eq!(max_depth, 5);
225 assert_eq!(edge_labels, vec!["l".to_string()]);
226 }
227 other => panic!("expected GraphPath, got {other:?}"),
228 }
229 }
230
231 fn labels_of(sql: &str) -> Vec<String> {
232 match parsed(sql) {
233 NodedbStatement::Graph(
234 GraphStmt::GraphTraverse { edge_labels, .. }
235 | GraphStmt::GraphNeighbors { edge_labels, .. }
236 | GraphStmt::GraphPath { edge_labels, .. },
237 ) => edge_labels,
238 other => panic!("expected a graph walk statement, got {other:?}"),
239 }
240 }
241
242 fn ab() -> Vec<String> {
243 vec!["a".to_string(), "b".to_string()]
244 }
245
246 #[test]
247 fn label_list_parses_on_every_walk_statement() {
248 assert_eq!(
249 labels_of("GRAPH TRAVERSE IN 'c' FROM 'x' DEPTH 2 LABEL 'a', 'b' DIRECTION out"),
250 ab()
251 );
252 assert_eq!(
253 labels_of("GRAPH NEIGHBORS IN 'c' OF 'x' LABEL 'a', 'b' DIRECTION both"),
254 ab()
255 );
256 assert_eq!(
257 labels_of("GRAPH PATH IN 'c' FROM 'x' TO 'y' MAX_DEPTH 3 LABEL 'a', 'b'"),
258 ab()
259 );
260 }
261
262 #[test]
263 fn parenthesised_label_list_parses() {
264 assert_eq!(
265 labels_of("GRAPH TRAVERSE IN 'c' FROM 'x' LABEL ('a','b')"),
266 ab()
267 );
268 assert_eq!(
269 labels_of("GRAPH NEIGHBORS IN 'c' OF 'x' LABEL ('a', 'b')"),
270 ab()
271 );
272 assert_eq!(
273 labels_of("GRAPH PATH IN 'c' FROM 'x' TO 'y' LABEL ('a','b')"),
274 ab()
275 );
276 }
277
278 #[test]
279 fn omitted_label_clause_is_an_empty_set() {
280 assert!(labels_of("GRAPH TRAVERSE IN 'c' FROM 'x' DEPTH 2").is_empty());
281 assert!(labels_of("GRAPH NEIGHBORS IN 'c' OF 'x'").is_empty());
282 assert!(labels_of("GRAPH PATH IN 'c' FROM 'x' TO 'y'").is_empty());
283 }
284
285 #[test]
286 fn label_clause_without_quoted_labels_is_refused() {
287 for sql in [
288 "GRAPH TRAVERSE IN 'c' FROM 'x' LABEL DIRECTION out",
289 "GRAPH TRAVERSE IN 'c' FROM 'x' LABEL knows",
290 "GRAPH NEIGHBORS IN 'c' OF 'x' LABEL",
291 "GRAPH PATH IN 'c' FROM 'x' TO 'y' LABEL knows",
292 ] {
293 let error = try_parse(sql)
294 .expect("graph DSL")
295 .expect_err("a LABEL clause with no quoted label must be refused");
296 assert!(
297 error.to_string().contains("LABEL"),
298 "the error must name the LABEL clause: {error}"
299 );
300 }
301 }
302
303 #[test]
304 fn quoted_keyword_label_does_not_shadow_the_keyword() {
305 let stmt = parsed("GRAPH TRAVERSE IN 'c' FROM 'x' LABEL 'DIRECTION', 'b' DIRECTION in");
306 match stmt {
307 NodedbStatement::Graph(GraphStmt::GraphTraverse {
308 edge_labels,
309 direction,
310 ..
311 }) => {
312 assert_eq!(edge_labels, vec!["DIRECTION".to_string(), "b".to_string()]);
313 assert_eq!(direction, GraphDirection::In);
314 }
315 other => panic!("expected GraphTraverse, got {other:?}"),
316 }
317 }
318
319 #[test]
320 fn parse_graph_labels_list() {
321 let stmt = parsed("GRAPH LABEL 'alice' AS 'Person', 'User'");
322 match stmt {
323 NodedbStatement::Graph(GraphStmt::GraphSetLabels {
324 node_id,
325 labels,
326 remove,
327 }) => {
328 assert_eq!(node_id, "alice");
329 assert_eq!(labels, vec!["Person".to_string(), "User".to_string()]);
330 assert!(!remove);
331 }
332 other => panic!("expected GraphSetLabels, got {other:?}"),
333 }
334 }
335
336 #[test]
337 fn parse_graph_algo_pagerank() {
338 let stmt = parsed("GRAPH ALGO PAGERANK ON users ITERATIONS 5 DAMPING 0.85");
339 match stmt {
340 NodedbStatement::Graph(GraphStmt::GraphAlgo {
341 algorithm,
342 collection,
343 damping,
344 max_iterations,
345 ..
346 }) => {
347 assert_eq!(algorithm, "PAGERANK");
348 assert_eq!(collection, "users");
349 assert_eq!(damping, Some(0.85));
350 assert_eq!(max_iterations, Some(5));
351 }
352 other => panic!("expected GraphAlgo, got {other:?}"),
353 }
354 }
355
356 #[test]
357 fn parse_graph_algo_personalization() {
358 let stmt = parsed(
359 r#"GRAPH ALGO PAGERANK ON 'users' DAMPING 0.9 PERSONALIZATION {"alice": 1.0, "bob": 0.5}"#,
360 );
361 match stmt {
362 NodedbStatement::Graph(GraphStmt::GraphAlgo {
363 algorithm,
364 collection,
365 damping,
366 personalization,
367 ..
368 }) => {
369 assert_eq!(algorithm, "PAGERANK");
370 assert_eq!(collection, "users");
371 assert_eq!(damping, Some(0.9));
372 let raw = personalization.expect("personalization object present");
373 assert!(raw.contains("alice"));
374 assert!(raw.contains("bob"));
375 let map: std::collections::HashMap<String, f64> = sonic_rs::from_str(&raw).unwrap();
377 assert_eq!(map.get("alice"), Some(&1.0));
378 assert_eq!(map.get("bob"), Some(&0.5));
379 }
380 other => panic!("expected GraphAlgo, got {other:?}"),
381 }
382 }
383
384 #[test]
385 fn parse_match_query_captures_raw() {
386 let stmt = parsed("MATCH (x)-[:l]->(y) RETURN x, y");
387 match stmt {
388 NodedbStatement::Graph(GraphStmt::MatchQuery { body }) => {
389 assert!(body.starts_with("MATCH"));
390 }
391 other => panic!("expected MatchQuery, got {other:?}"),
392 }
393 }
394
395 #[test]
396 fn non_graph_returns_none() {
397 assert!(try_parse("SELECT * FROM users").is_none());
398 assert!(try_parse("CREATE COLLECTION users").is_none());
399 }
400
401 #[test]
404 fn parse_rag_fusion_full_syntax() {
405 let stmt = parsed(
406 "GRAPH RAG FUSION ON entities \
407 QUERY ARRAY[0.1, 0.2, 0.3] \
408 VECTOR_TOP_K 50 \
409 EXPANSION_DEPTH 2 \
410 EDGE_LABEL 'related_to' \
411 FINAL_TOP_K 10 \
412 RRF_K (60.0, 35.0)",
413 );
414 match stmt {
415 NodedbStatement::Graph(GraphStmt::GraphRagFusion { collection, params }) => {
416 assert_eq!(collection, "entities");
417 let v = params.query_vector.expect("QUERY ARRAY parsed");
418 assert_eq!(v.len(), 3);
419 assert!((v[0] - 0.1f32).abs() < 1e-5);
420 assert_eq!(params.vector_top_k, Some(50));
421 assert_eq!(params.expansion_depth, Some(2));
422 assert_eq!(params.edge_label.as_deref(), Some("related_to"));
423 assert_eq!(params.final_top_k, Some(10));
424 let (k1, k2) = params.rrf_k.unwrap();
425 assert!((k1 - 60.0).abs() < 1e-10);
426 assert!((k2 - 35.0).abs() < 1e-10);
427 }
428 other => panic!("expected GraphRagFusion, got {other:?}"),
429 }
430 }
431
432 #[test]
433 fn parse_rag_fusion_minimal_defaults_to_none() {
434 let stmt = parsed("GRAPH RAG FUSION ON mycol QUERY ARRAY[1.0, 0.0]");
435 match stmt {
436 NodedbStatement::Graph(GraphStmt::GraphRagFusion { collection, params }) => {
437 assert_eq!(collection, "mycol");
438 assert!(params.query_vector.is_some());
439 assert_eq!(params.vector_top_k, None);
440 assert_eq!(params.expansion_depth, None);
441 assert_eq!(params.edge_label, None);
442 assert_eq!(params.final_top_k, None);
443 assert_eq!(params.rrf_k, None);
444 assert_eq!(params.vector_field, None);
445 assert_eq!(params.direction, None);
446 assert_eq!(params.max_visited, None);
447 }
448 other => panic!("expected GraphRagFusion, got {other:?}"),
449 }
450 }
451
452 #[test]
453 fn parse_rag_fusion_direction_and_max_visited() {
454 let stmt =
455 parsed("GRAPH RAG FUSION ON col QUERY ARRAY[0.5] DIRECTION both MAX_VISITED 500");
456 match stmt {
457 NodedbStatement::Graph(GraphStmt::GraphRagFusion { params, .. }) => {
458 assert_eq!(params.direction, Some(GraphDirection::Both));
459 assert_eq!(params.max_visited, Some(500));
460 }
461 other => panic!("expected GraphRagFusion, got {other:?}"),
462 }
463 }
464
465 #[test]
466 fn parse_rag_fusion_vector_field_is_captured() {
467 let stmt = parsed("GRAPH RAG FUSION ON col QUERY ARRAY[0.5] VECTOR_FIELD 'embedding'");
468 match stmt {
469 NodedbStatement::Graph(GraphStmt::GraphRagFusion { params, .. }) => {
470 assert_eq!(params.vector_field.as_deref(), Some("embedding"));
471 }
472 other => panic!("expected GraphRagFusion, got {other:?}"),
473 }
474 }
475
476 #[test]
477 fn parse_rag_fusion_rrf_k_both_values_captured() {
478 let stmt = parsed("GRAPH RAG FUSION ON col QUERY ARRAY[0.5] RRF_K (1.0, 99.5)");
479 match stmt {
480 NodedbStatement::Graph(GraphStmt::GraphRagFusion { params, .. }) => {
481 let (k1, k2) = params.rrf_k.expect("RRF_K must be parsed");
482 assert!((k1 - 1.0).abs() < 1e-10, "vector_k must be 1.0, got {k1}");
483 assert!((k2 - 99.5).abs() < 1e-10, "graph_k must be 99.5, got {k2}");
484 }
485 other => panic!("expected GraphRagFusion, got {other:?}"),
486 }
487 }
488
489 #[test]
490 fn parse_rag_fusion_missing_collection_names_the_clause() {
491 let error = try_parse("GRAPH RAG FUSION QUERY ARRAY[0.1] VECTOR_TOP_K 5")
492 .expect("input is graph DSL")
493 .expect_err("missing ON <collection> must not produce a statement");
494 assert!(
495 error.to_string().contains("ON <collection>"),
496 "the error must name the missing clause: {error}"
497 );
498 }
499
500 #[test]
502 fn malformed_graph_is_distinguishable_from_non_graph() {
503 assert!(
504 try_parse("SELECT * FROM users").is_none(),
505 "non-graph input must stay None so other families get a turn"
506 );
507 assert!(
508 matches!(try_parse("GRAPH TRAVERSE FROM 'b'"), Some(Err(_))),
509 "malformed graph input must report its own error"
510 );
511 }
512
513 #[test]
515 fn direction_vocabulary_is_closed() {
516 for word in ["IN", "OUT", "BOTH", "both"] {
517 let sql = format!("GRAPH TRAVERSE IN 'c' FROM 'a' DIRECTION {word}");
518 assert!(
519 try_parse(&sql).expect("graph DSL").is_ok(),
520 "'{word}' is documented vocabulary and must parse"
521 );
522 }
523 for word in ["INBOUND", "BANANA"] {
524 let sql = format!("GRAPH TRAVERSE IN 'c' FROM 'a' DIRECTION {word}");
525 let error = try_parse(&sql)
526 .expect("graph DSL")
527 .expect_err("'{word}' is outside the vocabulary");
528 assert!(
529 error.to_string().contains(word),
530 "the error must name the offending value: {error}"
531 );
532 }
533 assert!(
534 try_parse("GRAPH TRAVERSE IN 'c' FROM 'a'")
535 .expect("graph DSL")
536 .is_ok(),
537 "an omitted DIRECTION must keep defaulting"
538 );
539 }
540
541 fn parse_error(sql: &str) -> String {
544 try_parse(sql)
545 .expect("input is graph DSL")
546 .expect_err("statement must be refused")
547 .to_string()
548 }
549
550 #[test]
551 fn misspelled_clause_keyword_is_named() {
552 let error = parse_error("GRAPH TRAVERSE FROM 1 DEPTS 3 IN g");
553 assert!(
554 error.contains("unexpected token 'DEPTS' in GRAPH TRAVERSE"),
555 "{error}"
556 );
557 let error = parse_error("GRAPH NEIGHBORS OF 1 LABELS 'follows' IN g");
558 assert!(
559 error.contains("unexpected token 'LABELS' in GRAPH NEIGHBORS"),
560 "{error}"
561 );
562 }
563
564 #[test]
565 fn stray_tokens_are_refused() {
566 for (sql, token) in [
567 ("GRAPH TRAVERSE FROM 1 SIDEWAYS IN g", "SIDEWAYS"),
568 ("GRAPH TRAVERSE FROM 1 BANANA PHONE IN g", "BANANA"),
569 ("GRAPH INSERT EDGE IN 'e' FROM 'a' TO 'b' TYP 'x'", "TYP"),
570 (
571 "GRAPH DELETE EDGE IN 'e' FROM 'a' TO 'b' TYPE 'x' EXTRA",
572 "EXTRA",
573 ),
574 ("GRAPH PATH IN 'g' FROM 'a' TO 'b' MAXDEPTH 3", "MAXDEPTH"),
575 ("GRAPH LABEL 'a' AS 'P' EXTRA", "EXTRA"),
576 ("GRAPH ALGO PAGERANK ON g ITERATION 5", "ITERATION"),
577 (
578 "GRAPH RAG FUSION ON e QUERY ARRAY[0.1] VECTOR_TOPK 5",
579 "VECTOR_TOPK",
580 ),
581 ("GRAPH TRAVERSE IN 'g' FROM 'a' ]", "]"),
582 ] {
583 let error = parse_error(sql);
584 assert!(
585 error.contains(&format!("unexpected token '{token}'")),
586 "{sql}: {error}"
587 );
588 }
589 }
590
591 #[test]
592 fn stray_string_at_clause_position_is_refused() {
593 let error = parse_error("GRAPH TRAVERSE IN 'g' FROM 'a' 'b'");
594 assert!(error.contains("unexpected string 'b'"), "{error}");
595 }
596
597 #[test]
598 fn repeated_clause_is_refused() {
599 let error = parse_error("GRAPH TRAVERSE IN 'g' FROM 'a' DEPTH 1 DEPTH 3");
600 assert!(
601 error.contains("DEPTH given more than once in GRAPH TRAVERSE"),
602 "{error}"
603 );
604 }
605
606 #[test]
607 fn direction_value_in_does_not_shadow_in_clause() {
608 match parsed("GRAPH TRAVERSE FROM 'b' DIRECTION in IN 'g'") {
609 NodedbStatement::Graph(GraphStmt::GraphTraverse {
610 collection,
611 start,
612 direction,
613 ..
614 }) => {
615 assert_eq!(collection, "g");
616 assert_eq!(start, "b");
617 assert_eq!(direction, GraphDirection::In);
618 }
619 other => panic!("expected GraphTraverse, got {other:?}"),
620 }
621 }
622
623 #[test]
624 fn non_numeric_clause_values_are_refused() {
625 let error = parse_error("GRAPH ALGO PAGERANK ON g DAMPING abc");
626 assert!(error.contains("DAMPING must be a number"), "{error}");
627 let error = parse_error("GRAPH TRAVERSE IN 'g' FROM 'a' DEPTH '3'");
628 assert!(
629 error.contains("DEPTH must be a non-negative integer"),
630 "{error}"
631 );
632 let error = parse_error("GRAPH RAG FUSION ON e QUERY ARRAY[0.1, x]");
633 assert!(error.contains("'x'"), "{error}");
634 }
635
636 #[test]
637 fn comments_are_not_tokens() {
638 match parsed("GRAPH TRAVERSE IN 'g' /* note */ FROM 'a' -- trailing note") {
639 NodedbStatement::Graph(GraphStmt::GraphTraverse {
640 collection, start, ..
641 }) => {
642 assert_eq!(collection, "g");
643 assert_eq!(start, "a");
644 }
645 other => panic!("expected GraphTraverse, got {other:?}"),
646 }
647 }
648
649 #[test]
650 fn quoted_identifier_is_a_value() {
651 match parsed(r#"GRAPH NEIGHBORS IN "My Graph" OF 'a'"#) {
652 NodedbStatement::Graph(GraphStmt::GraphNeighbors { collection, .. }) => {
653 assert_eq!(collection, "My Graph");
654 }
655 other => panic!("expected GraphNeighbors, got {other:?}"),
656 }
657 }
658
659 #[test]
660 fn rag_fusion_bm25_field_is_the_on_after_bm25() {
661 let params_of = |sql: &str| match parsed(sql) {
662 NodedbStatement::Graph(GraphStmt::GraphRagFusion { collection, params }) => {
663 assert_eq!(collection, "entities");
664 params
665 }
666 other => panic!("expected GraphRagFusion, got {other:?}"),
667 };
668 let params = params_of(
669 "GRAPH RAG FUSION ON entities QUERY ARRAY[0.1] BM25 'q' ON 'body' \
670 RRF_K (1.0, 2.0, 3.0)",
671 );
672 assert_eq!(params.bm25_query.as_deref(), Some("q"));
673 assert_eq!(params.bm25_field.as_deref(), Some("body"));
674 assert_eq!(params.rrf_k_triple, Some((1.0, 2.0, 3.0)));
675 let params = params_of("GRAPH RAG FUSION ON entities QUERY ARRAY[0.1]");
676 assert_eq!(params.bm25_field, None);
677 }
678}