1use std::collections::HashMap;
10use std::sync::Arc;
11
12use grafeo_common::types::{EpochId, TxId, Value};
13use grafeo_common::utils::error::{Error, Result};
14use grafeo_core::graph::GraphStoreMut;
15use grafeo_core::graph::lpg::LpgStore;
16
17use crate::catalog::Catalog;
18use crate::database::QueryResult;
19use crate::query::binder::Binder;
20use crate::query::executor::Executor;
21use crate::query::optimizer::Optimizer;
22use crate::query::plan::{LogicalExpression, LogicalOperator, LogicalPlan};
23use crate::query::planner::Planner;
24use crate::transaction::TransactionManager;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum QueryLanguage {
29 #[cfg(feature = "gql")]
31 Gql,
32 #[cfg(feature = "cypher")]
34 Cypher,
35 #[cfg(feature = "gremlin")]
37 Gremlin,
38 #[cfg(feature = "graphql")]
40 GraphQL,
41 #[cfg(feature = "sql-pgq")]
43 SqlPgq,
44 #[cfg(feature = "sparql")]
46 Sparql,
47 #[cfg(all(feature = "graphql", feature = "rdf"))]
49 GraphQLRdf,
50}
51
52impl QueryLanguage {
53 #[must_use]
55 pub const fn is_lpg(&self) -> bool {
56 match self {
57 #[cfg(feature = "gql")]
58 Self::Gql => true,
59 #[cfg(feature = "cypher")]
60 Self::Cypher => true,
61 #[cfg(feature = "gremlin")]
62 Self::Gremlin => true,
63 #[cfg(feature = "graphql")]
64 Self::GraphQL => true,
65 #[cfg(feature = "sql-pgq")]
66 Self::SqlPgq => true,
67 #[cfg(feature = "sparql")]
68 Self::Sparql => false,
69 #[cfg(all(feature = "graphql", feature = "rdf"))]
70 Self::GraphQLRdf => false,
71 }
72 }
73}
74
75pub type QueryParams = HashMap<String, Value>;
77
78pub struct QueryProcessor {
98 lpg_store: Arc<LpgStore>,
100 graph_store: Arc<dyn GraphStoreMut>,
102 tx_manager: Arc<TransactionManager>,
104 catalog: Arc<Catalog>,
106 optimizer: Optimizer,
108 tx_context: Option<(EpochId, TxId)>,
110 #[cfg(feature = "rdf")]
112 rdf_store: Option<Arc<grafeo_core::graph::rdf::RdfStore>>,
113}
114
115impl QueryProcessor {
116 #[must_use]
118 pub fn for_lpg(store: Arc<LpgStore>) -> Self {
119 let optimizer = Optimizer::from_store(&store);
120 let graph_store = Arc::clone(&store) as Arc<dyn GraphStoreMut>;
121 Self {
122 lpg_store: store,
123 graph_store,
124 tx_manager: Arc::new(TransactionManager::new()),
125 catalog: Arc::new(Catalog::new()),
126 optimizer,
127 tx_context: None,
128 #[cfg(feature = "rdf")]
129 rdf_store: None,
130 }
131 }
132
133 #[must_use]
135 pub fn for_lpg_with_tx(store: Arc<LpgStore>, tx_manager: Arc<TransactionManager>) -> Self {
136 let optimizer = Optimizer::from_store(&store);
137 let graph_store = Arc::clone(&store) as Arc<dyn GraphStoreMut>;
138 Self {
139 lpg_store: store,
140 graph_store,
141 tx_manager,
142 catalog: Arc::new(Catalog::new()),
143 optimizer,
144 tx_context: None,
145 #[cfg(feature = "rdf")]
146 rdf_store: None,
147 }
148 }
149
150 #[must_use]
152 pub fn for_graph_store_with_tx(
153 store: Arc<dyn GraphStoreMut>,
154 tx_manager: Arc<TransactionManager>,
155 ) -> Self {
156 let optimizer = Optimizer::from_graph_store(&*store);
157 Self {
158 lpg_store: Arc::new(LpgStore::new()), graph_store: store,
160 tx_manager,
161 catalog: Arc::new(Catalog::new()),
162 optimizer,
163 tx_context: None,
164 #[cfg(feature = "rdf")]
165 rdf_store: None,
166 }
167 }
168
169 #[cfg(feature = "rdf")]
171 #[must_use]
172 pub fn with_rdf(
173 lpg_store: Arc<LpgStore>,
174 rdf_store: Arc<grafeo_core::graph::rdf::RdfStore>,
175 ) -> Self {
176 let optimizer = Optimizer::from_store(&lpg_store);
177 let graph_store = Arc::clone(&lpg_store) as Arc<dyn GraphStoreMut>;
178 Self {
179 lpg_store,
180 graph_store,
181 tx_manager: Arc::new(TransactionManager::new()),
182 catalog: Arc::new(Catalog::new()),
183 optimizer,
184 tx_context: None,
185 rdf_store: Some(rdf_store),
186 }
187 }
188
189 #[must_use]
193 pub fn with_tx_context(mut self, viewing_epoch: EpochId, tx_id: TxId) -> Self {
194 self.tx_context = Some((viewing_epoch, tx_id));
195 self
196 }
197
198 #[must_use]
200 pub fn with_catalog(mut self, catalog: Arc<Catalog>) -> Self {
201 self.catalog = catalog;
202 self
203 }
204
205 #[must_use]
207 pub fn with_optimizer(mut self, optimizer: Optimizer) -> Self {
208 self.optimizer = optimizer;
209 self
210 }
211
212 pub fn process(
232 &self,
233 query: &str,
234 language: QueryLanguage,
235 params: Option<&QueryParams>,
236 ) -> Result<QueryResult> {
237 if language.is_lpg() {
238 self.process_lpg(query, language, params)
239 } else {
240 #[cfg(feature = "rdf")]
241 {
242 self.process_rdf(query, language, params)
243 }
244 #[cfg(not(feature = "rdf"))]
245 {
246 Err(Error::Internal(
247 "RDF support not enabled. Compile with --features rdf".to_string(),
248 ))
249 }
250 }
251 }
252
253 fn process_lpg(
255 &self,
256 query: &str,
257 language: QueryLanguage,
258 params: Option<&QueryParams>,
259 ) -> Result<QueryResult> {
260 let start_time = std::time::Instant::now();
261
262 let mut logical_plan = self.translate_lpg(query, language)?;
264
265 if let Some(params) = params {
267 substitute_params(&mut logical_plan, params)?;
268 }
269
270 let mut binder = Binder::new();
272 let _binding_context = binder.bind(&logical_plan)?;
273
274 let optimized_plan = self.optimizer.optimize(logical_plan)?;
276
277 let planner = if let Some((epoch, tx_id)) = self.tx_context {
279 Planner::with_context(
280 Arc::clone(&self.graph_store),
281 Arc::clone(&self.tx_manager),
282 Some(tx_id),
283 epoch,
284 )
285 } else {
286 Planner::with_context(
287 Arc::clone(&self.graph_store),
288 Arc::clone(&self.tx_manager),
289 None,
290 self.tx_manager.current_epoch(),
291 )
292 };
293 let mut physical_plan = planner.plan(&optimized_plan)?;
294
295 let executor = Executor::with_columns(physical_plan.columns.clone());
297 let mut result = executor.execute(physical_plan.operator.as_mut())?;
298
299 let elapsed_ms = start_time.elapsed().as_secs_f64() * 1000.0;
301 let rows_scanned = result.rows.len() as u64; result.execution_time_ms = Some(elapsed_ms);
303 result.rows_scanned = Some(rows_scanned);
304
305 Ok(result)
306 }
307
308 fn translate_lpg(&self, query: &str, language: QueryLanguage) -> Result<LogicalPlan> {
310 match language {
311 #[cfg(feature = "gql")]
312 QueryLanguage::Gql => {
313 use crate::query::gql_translator;
314 gql_translator::translate(query)
315 }
316 #[cfg(feature = "cypher")]
317 QueryLanguage::Cypher => {
318 use crate::query::cypher_translator;
319 cypher_translator::translate(query)
320 }
321 #[cfg(feature = "gremlin")]
322 QueryLanguage::Gremlin => {
323 use crate::query::gremlin_translator;
324 gremlin_translator::translate(query)
325 }
326 #[cfg(feature = "graphql")]
327 QueryLanguage::GraphQL => {
328 use crate::query::graphql_translator;
329 graphql_translator::translate(query)
330 }
331 #[cfg(feature = "sql-pgq")]
332 QueryLanguage::SqlPgq => {
333 use crate::query::sql_pgq_translator;
334 sql_pgq_translator::translate(query)
335 }
336 #[allow(unreachable_patterns)]
337 _ => Err(Error::Internal(format!(
338 "Language {:?} is not an LPG language",
339 language
340 ))),
341 }
342 }
343
344 #[cfg(feature = "rdf")]
346 fn process_rdf(
347 &self,
348 query: &str,
349 language: QueryLanguage,
350 _params: Option<&QueryParams>,
351 ) -> Result<QueryResult> {
352 use crate::query::planner_rdf::RdfPlanner;
353
354 let rdf_store = self.rdf_store.as_ref().ok_or_else(|| {
355 Error::Internal("RDF store not configured for this processor".to_string())
356 })?;
357
358 let logical_plan = self.translate_rdf(query, language)?;
360
361 let mut binder = Binder::new();
363 let _binding_context = binder.bind(&logical_plan)?;
364
365 let optimized_plan = self.optimizer.optimize(logical_plan)?;
367
368 let planner = RdfPlanner::new(Arc::clone(rdf_store));
370 let mut physical_plan = planner.plan(&optimized_plan)?;
371
372 let executor = Executor::with_columns(physical_plan.columns.clone());
374 executor.execute(physical_plan.operator.as_mut())
375 }
376
377 #[cfg(feature = "rdf")]
379 fn translate_rdf(&self, query: &str, language: QueryLanguage) -> Result<LogicalPlan> {
380 match language {
381 #[cfg(feature = "sparql")]
382 QueryLanguage::Sparql => {
383 use crate::query::sparql_translator;
384 sparql_translator::translate(query)
385 }
386 #[cfg(all(feature = "graphql", feature = "rdf"))]
387 QueryLanguage::GraphQLRdf => {
388 use crate::query::graphql_rdf_translator;
389 graphql_rdf_translator::translate(query, "http://example.org/")
391 }
392 _ => Err(Error::Internal(format!(
393 "Language {:?} is not an RDF language",
394 language
395 ))),
396 }
397 }
398
399 #[must_use]
401 pub fn lpg_store(&self) -> &Arc<LpgStore> {
402 &self.lpg_store
403 }
404
405 #[must_use]
407 pub fn catalog(&self) -> &Arc<Catalog> {
408 &self.catalog
409 }
410
411 #[must_use]
413 pub fn optimizer(&self) -> &Optimizer {
414 &self.optimizer
415 }
416
417 #[cfg(feature = "rdf")]
419 #[must_use]
420 pub fn rdf_store(&self) -> Option<&Arc<grafeo_core::graph::rdf::RdfStore>> {
421 self.rdf_store.as_ref()
422 }
423}
424
425impl QueryProcessor {
426 #[must_use]
428 pub fn tx_manager(&self) -> &Arc<TransactionManager> {
429 &self.tx_manager
430 }
431}
432
433fn substitute_params(plan: &mut LogicalPlan, params: &QueryParams) -> Result<()> {
435 substitute_in_operator(&mut plan.root, params)
436}
437
438fn substitute_in_operator(op: &mut LogicalOperator, params: &QueryParams) -> Result<()> {
440 use crate::query::plan::*;
441
442 match op {
443 LogicalOperator::Filter(filter) => {
444 substitute_in_expression(&mut filter.predicate, params)?;
445 substitute_in_operator(&mut filter.input, params)?;
446 }
447 LogicalOperator::Return(ret) => {
448 for item in &mut ret.items {
449 substitute_in_expression(&mut item.expression, params)?;
450 }
451 substitute_in_operator(&mut ret.input, params)?;
452 }
453 LogicalOperator::Project(proj) => {
454 for p in &mut proj.projections {
455 substitute_in_expression(&mut p.expression, params)?;
456 }
457 substitute_in_operator(&mut proj.input, params)?;
458 }
459 LogicalOperator::NodeScan(scan) => {
460 if let Some(input) = &mut scan.input {
461 substitute_in_operator(input, params)?;
462 }
463 }
464 LogicalOperator::EdgeScan(scan) => {
465 if let Some(input) = &mut scan.input {
466 substitute_in_operator(input, params)?;
467 }
468 }
469 LogicalOperator::Expand(expand) => {
470 substitute_in_operator(&mut expand.input, params)?;
471 }
472 LogicalOperator::Join(join) => {
473 substitute_in_operator(&mut join.left, params)?;
474 substitute_in_operator(&mut join.right, params)?;
475 for cond in &mut join.conditions {
476 substitute_in_expression(&mut cond.left, params)?;
477 substitute_in_expression(&mut cond.right, params)?;
478 }
479 }
480 LogicalOperator::LeftJoin(join) => {
481 substitute_in_operator(&mut join.left, params)?;
482 substitute_in_operator(&mut join.right, params)?;
483 if let Some(cond) = &mut join.condition {
484 substitute_in_expression(cond, params)?;
485 }
486 }
487 LogicalOperator::Aggregate(agg) => {
488 for expr in &mut agg.group_by {
489 substitute_in_expression(expr, params)?;
490 }
491 for agg_expr in &mut agg.aggregates {
492 if let Some(expr) = &mut agg_expr.expression {
493 substitute_in_expression(expr, params)?;
494 }
495 }
496 substitute_in_operator(&mut agg.input, params)?;
497 }
498 LogicalOperator::Sort(sort) => {
499 for key in &mut sort.keys {
500 substitute_in_expression(&mut key.expression, params)?;
501 }
502 substitute_in_operator(&mut sort.input, params)?;
503 }
504 LogicalOperator::Limit(limit) => {
505 substitute_in_operator(&mut limit.input, params)?;
506 }
507 LogicalOperator::Skip(skip) => {
508 substitute_in_operator(&mut skip.input, params)?;
509 }
510 LogicalOperator::Distinct(distinct) => {
511 substitute_in_operator(&mut distinct.input, params)?;
512 }
513 LogicalOperator::CreateNode(create) => {
514 for (_, expr) in &mut create.properties {
515 substitute_in_expression(expr, params)?;
516 }
517 if let Some(input) = &mut create.input {
518 substitute_in_operator(input, params)?;
519 }
520 }
521 LogicalOperator::CreateEdge(create) => {
522 for (_, expr) in &mut create.properties {
523 substitute_in_expression(expr, params)?;
524 }
525 substitute_in_operator(&mut create.input, params)?;
526 }
527 LogicalOperator::DeleteNode(delete) => {
528 substitute_in_operator(&mut delete.input, params)?;
529 }
530 LogicalOperator::DeleteEdge(delete) => {
531 substitute_in_operator(&mut delete.input, params)?;
532 }
533 LogicalOperator::SetProperty(set) => {
534 for (_, expr) in &mut set.properties {
535 substitute_in_expression(expr, params)?;
536 }
537 substitute_in_operator(&mut set.input, params)?;
538 }
539 LogicalOperator::Union(union) => {
540 for input in &mut union.inputs {
541 substitute_in_operator(input, params)?;
542 }
543 }
544 LogicalOperator::AntiJoin(anti) => {
545 substitute_in_operator(&mut anti.left, params)?;
546 substitute_in_operator(&mut anti.right, params)?;
547 }
548 LogicalOperator::Bind(bind) => {
549 substitute_in_expression(&mut bind.expression, params)?;
550 substitute_in_operator(&mut bind.input, params)?;
551 }
552 LogicalOperator::TripleScan(scan) => {
553 if let Some(input) = &mut scan.input {
554 substitute_in_operator(input, params)?;
555 }
556 }
557 LogicalOperator::Unwind(unwind) => {
558 substitute_in_expression(&mut unwind.expression, params)?;
559 substitute_in_operator(&mut unwind.input, params)?;
560 }
561 LogicalOperator::MapCollect(mc) => {
562 substitute_in_operator(&mut mc.input, params)?;
563 }
564 LogicalOperator::Merge(merge) => {
565 for (_, expr) in &mut merge.match_properties {
566 substitute_in_expression(expr, params)?;
567 }
568 for (_, expr) in &mut merge.on_create {
569 substitute_in_expression(expr, params)?;
570 }
571 for (_, expr) in &mut merge.on_match {
572 substitute_in_expression(expr, params)?;
573 }
574 substitute_in_operator(&mut merge.input, params)?;
575 }
576 LogicalOperator::MergeRelationship(merge_rel) => {
577 for (_, expr) in &mut merge_rel.match_properties {
578 substitute_in_expression(expr, params)?;
579 }
580 for (_, expr) in &mut merge_rel.on_create {
581 substitute_in_expression(expr, params)?;
582 }
583 for (_, expr) in &mut merge_rel.on_match {
584 substitute_in_expression(expr, params)?;
585 }
586 substitute_in_operator(&mut merge_rel.input, params)?;
587 }
588 LogicalOperator::AddLabel(add_label) => {
589 substitute_in_operator(&mut add_label.input, params)?;
590 }
591 LogicalOperator::RemoveLabel(remove_label) => {
592 substitute_in_operator(&mut remove_label.input, params)?;
593 }
594 LogicalOperator::ShortestPath(sp) => {
595 substitute_in_operator(&mut sp.input, params)?;
596 }
597 LogicalOperator::InsertTriple(insert) => {
599 if let Some(ref mut input) = insert.input {
600 substitute_in_operator(input, params)?;
601 }
602 }
603 LogicalOperator::DeleteTriple(delete) => {
604 if let Some(ref mut input) = delete.input {
605 substitute_in_operator(input, params)?;
606 }
607 }
608 LogicalOperator::Modify(modify) => {
609 substitute_in_operator(&mut modify.where_clause, params)?;
610 }
611 LogicalOperator::ClearGraph(_)
612 | LogicalOperator::CreateGraph(_)
613 | LogicalOperator::DropGraph(_)
614 | LogicalOperator::LoadGraph(_)
615 | LogicalOperator::CopyGraph(_)
616 | LogicalOperator::MoveGraph(_)
617 | LogicalOperator::AddGraph(_) => {}
618 LogicalOperator::Empty => {}
619 LogicalOperator::VectorScan(scan) => {
620 substitute_in_expression(&mut scan.query_vector, params)?;
621 if let Some(ref mut input) = scan.input {
622 substitute_in_operator(input, params)?;
623 }
624 }
625 LogicalOperator::VectorJoin(join) => {
626 substitute_in_expression(&mut join.query_vector, params)?;
627 substitute_in_operator(&mut join.input, params)?;
628 }
629 LogicalOperator::Except(except) => {
630 substitute_in_operator(&mut except.left, params)?;
631 substitute_in_operator(&mut except.right, params)?;
632 }
633 LogicalOperator::Intersect(intersect) => {
634 substitute_in_operator(&mut intersect.left, params)?;
635 substitute_in_operator(&mut intersect.right, params)?;
636 }
637 LogicalOperator::Otherwise(otherwise) => {
638 substitute_in_operator(&mut otherwise.left, params)?;
639 substitute_in_operator(&mut otherwise.right, params)?;
640 }
641 LogicalOperator::Apply(apply) => {
642 substitute_in_operator(&mut apply.input, params)?;
643 substitute_in_operator(&mut apply.subplan, params)?;
644 }
645 LogicalOperator::CreatePropertyGraph(_) => {}
647 LogicalOperator::CallProcedure(_) => {}
649 }
650 Ok(())
651}
652
653fn substitute_in_expression(expr: &mut LogicalExpression, params: &QueryParams) -> Result<()> {
655 use crate::query::plan::LogicalExpression;
656
657 match expr {
658 LogicalExpression::Parameter(name) => {
659 if let Some(value) = params.get(name) {
660 *expr = LogicalExpression::Literal(value.clone());
661 } else {
662 return Err(Error::Internal(format!("Missing parameter: ${}", name)));
663 }
664 }
665 LogicalExpression::Binary { left, right, .. } => {
666 substitute_in_expression(left, params)?;
667 substitute_in_expression(right, params)?;
668 }
669 LogicalExpression::Unary { operand, .. } => {
670 substitute_in_expression(operand, params)?;
671 }
672 LogicalExpression::FunctionCall { args, .. } => {
673 for arg in args {
674 substitute_in_expression(arg, params)?;
675 }
676 }
677 LogicalExpression::List(items) => {
678 for item in items {
679 substitute_in_expression(item, params)?;
680 }
681 }
682 LogicalExpression::Map(pairs) => {
683 for (_, value) in pairs {
684 substitute_in_expression(value, params)?;
685 }
686 }
687 LogicalExpression::IndexAccess { base, index } => {
688 substitute_in_expression(base, params)?;
689 substitute_in_expression(index, params)?;
690 }
691 LogicalExpression::SliceAccess { base, start, end } => {
692 substitute_in_expression(base, params)?;
693 if let Some(s) = start {
694 substitute_in_expression(s, params)?;
695 }
696 if let Some(e) = end {
697 substitute_in_expression(e, params)?;
698 }
699 }
700 LogicalExpression::Case {
701 operand,
702 when_clauses,
703 else_clause,
704 } => {
705 if let Some(op) = operand {
706 substitute_in_expression(op, params)?;
707 }
708 for (cond, result) in when_clauses {
709 substitute_in_expression(cond, params)?;
710 substitute_in_expression(result, params)?;
711 }
712 if let Some(el) = else_clause {
713 substitute_in_expression(el, params)?;
714 }
715 }
716 LogicalExpression::Property { .. }
717 | LogicalExpression::Variable(_)
718 | LogicalExpression::Literal(_)
719 | LogicalExpression::Labels(_)
720 | LogicalExpression::Type(_)
721 | LogicalExpression::Id(_) => {}
722 LogicalExpression::ListComprehension {
723 list_expr,
724 filter_expr,
725 map_expr,
726 ..
727 } => {
728 substitute_in_expression(list_expr, params)?;
729 if let Some(filter) = filter_expr {
730 substitute_in_expression(filter, params)?;
731 }
732 substitute_in_expression(map_expr, params)?;
733 }
734 LogicalExpression::ListPredicate {
735 list_expr,
736 predicate,
737 ..
738 } => {
739 substitute_in_expression(list_expr, params)?;
740 substitute_in_expression(predicate, params)?;
741 }
742 LogicalExpression::ExistsSubquery(_) | LogicalExpression::CountSubquery(_) => {
743 }
745 LogicalExpression::PatternComprehension { projection, .. } => {
746 substitute_in_expression(projection, params)?;
747 }
748 LogicalExpression::MapProjection { entries, .. } => {
749 for entry in entries {
750 if let crate::query::plan::MapProjectionEntry::LiteralEntry(_, expr) = entry {
751 substitute_in_expression(expr, params)?;
752 }
753 }
754 }
755 LogicalExpression::Reduce {
756 initial,
757 list,
758 expression,
759 ..
760 } => {
761 substitute_in_expression(initial, params)?;
762 substitute_in_expression(list, params)?;
763 substitute_in_expression(expression, params)?;
764 }
765 }
766 Ok(())
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772
773 #[test]
774 fn test_query_language_is_lpg() {
775 #[cfg(feature = "gql")]
776 assert!(QueryLanguage::Gql.is_lpg());
777 #[cfg(feature = "cypher")]
778 assert!(QueryLanguage::Cypher.is_lpg());
779 #[cfg(feature = "sparql")]
780 assert!(!QueryLanguage::Sparql.is_lpg());
781 }
782
783 #[test]
784 fn test_processor_creation() {
785 let store = Arc::new(LpgStore::new());
786 let processor = QueryProcessor::for_lpg(store);
787 assert!(processor.lpg_store().node_count() == 0);
788 }
789
790 #[cfg(feature = "gql")]
791 #[test]
792 fn test_process_simple_gql() {
793 let store = Arc::new(LpgStore::new());
794 store.create_node(&["Person"]);
795 store.create_node(&["Person"]);
796
797 let processor = QueryProcessor::for_lpg(store);
798 let result = processor
799 .process("MATCH (n:Person) RETURN n", QueryLanguage::Gql, None)
800 .unwrap();
801
802 assert_eq!(result.row_count(), 2);
803 assert_eq!(result.columns[0], "n");
804 }
805
806 #[cfg(feature = "cypher")]
807 #[test]
808 fn test_process_simple_cypher() {
809 let store = Arc::new(LpgStore::new());
810 store.create_node(&["Person"]);
811
812 let processor = QueryProcessor::for_lpg(store);
813 let result = processor
814 .process("MATCH (n:Person) RETURN n", QueryLanguage::Cypher, None)
815 .unwrap();
816
817 assert_eq!(result.row_count(), 1);
818 }
819
820 #[cfg(feature = "gql")]
821 #[test]
822 fn test_process_with_params() {
823 let store = Arc::new(LpgStore::new());
824 store.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
825 store.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
826 store.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
827
828 let processor = QueryProcessor::for_lpg(store);
829
830 let mut params = HashMap::new();
832 params.insert("min_age".to_string(), Value::Int64(30));
833
834 let result = processor
835 .process(
836 "MATCH (n:Person) WHERE n.age > $min_age RETURN n",
837 QueryLanguage::Gql,
838 Some(¶ms),
839 )
840 .unwrap();
841
842 assert_eq!(result.row_count(), 2);
844 }
845
846 #[cfg(feature = "gql")]
847 #[test]
848 fn test_missing_param_error() {
849 let store = Arc::new(LpgStore::new());
850 store.create_node(&["Person"]);
851
852 let processor = QueryProcessor::for_lpg(store);
853
854 let params: HashMap<String, Value> = HashMap::new();
856 let result = processor.process(
857 "MATCH (n:Person) WHERE n.age > $min_age RETURN n",
858 QueryLanguage::Gql,
859 Some(¶ms),
860 );
861
862 assert!(result.is_err());
864 let err = result.unwrap_err();
865 assert!(
866 err.to_string().contains("Missing parameter"),
867 "Expected 'Missing parameter' error, got: {}",
868 err
869 );
870 }
871
872 #[cfg(feature = "gql")]
873 #[test]
874 fn test_params_in_filter_with_property() {
875 let store = Arc::new(LpgStore::new());
877 store.create_node_with_props(&["Num"], [("value", Value::Int64(10))]);
878 store.create_node_with_props(&["Num"], [("value", Value::Int64(20))]);
879
880 let processor = QueryProcessor::for_lpg(store);
881
882 let mut params = HashMap::new();
883 params.insert("threshold".to_string(), Value::Int64(15));
884
885 let result = processor
886 .process(
887 "MATCH (n:Num) WHERE n.value > $threshold RETURN n.value",
888 QueryLanguage::Gql,
889 Some(¶ms),
890 )
891 .unwrap();
892
893 assert_eq!(result.row_count(), 1);
895 let row = &result.rows[0];
896 assert_eq!(row[0], Value::Int64(20));
897 }
898
899 #[cfg(feature = "gql")]
900 #[test]
901 fn test_params_in_multiple_where_conditions() {
902 let store = Arc::new(LpgStore::new());
904 store.create_node_with_props(
905 &["Person"],
906 [("age", Value::Int64(25)), ("score", Value::Int64(80))],
907 );
908 store.create_node_with_props(
909 &["Person"],
910 [("age", Value::Int64(35)), ("score", Value::Int64(90))],
911 );
912 store.create_node_with_props(
913 &["Person"],
914 [("age", Value::Int64(45)), ("score", Value::Int64(70))],
915 );
916
917 let processor = QueryProcessor::for_lpg(store);
918
919 let mut params = HashMap::new();
920 params.insert("min_age".to_string(), Value::Int64(30));
921 params.insert("min_score".to_string(), Value::Int64(75));
922
923 let result = processor
924 .process(
925 "MATCH (n:Person) WHERE n.age > $min_age AND n.score > $min_score RETURN n",
926 QueryLanguage::Gql,
927 Some(¶ms),
928 )
929 .unwrap();
930
931 assert_eq!(result.row_count(), 1);
933 }
934
935 #[cfg(feature = "gql")]
936 #[test]
937 fn test_params_with_in_list() {
938 let store = Arc::new(LpgStore::new());
940 store.create_node_with_props(&["Item"], [("status", Value::String("active".into()))]);
941 store.create_node_with_props(&["Item"], [("status", Value::String("pending".into()))]);
942 store.create_node_with_props(&["Item"], [("status", Value::String("deleted".into()))]);
943
944 let processor = QueryProcessor::for_lpg(store);
945
946 let mut params = HashMap::new();
948 params.insert("target".to_string(), Value::String("active".into()));
949
950 let result = processor
951 .process(
952 "MATCH (n:Item) WHERE n.status = $target RETURN n",
953 QueryLanguage::Gql,
954 Some(¶ms),
955 )
956 .unwrap();
957
958 assert_eq!(result.row_count(), 1);
959 }
960
961 #[cfg(feature = "gql")]
962 #[test]
963 fn test_params_same_type_comparison() {
964 let store = Arc::new(LpgStore::new());
966 store.create_node_with_props(&["Data"], [("value", Value::Int64(100))]);
967 store.create_node_with_props(&["Data"], [("value", Value::Int64(50))]);
968
969 let processor = QueryProcessor::for_lpg(store);
970
971 let mut params = HashMap::new();
973 params.insert("threshold".to_string(), Value::Int64(75));
974
975 let result = processor
976 .process(
977 "MATCH (n:Data) WHERE n.value > $threshold RETURN n",
978 QueryLanguage::Gql,
979 Some(¶ms),
980 )
981 .unwrap();
982
983 assert_eq!(result.row_count(), 1);
985 }
986
987 #[cfg(feature = "gql")]
988 #[test]
989 fn test_process_empty_result_has_columns() {
990 let store = Arc::new(LpgStore::new());
992 let processor = QueryProcessor::for_lpg(store);
995 let result = processor
996 .process(
997 "MATCH (n:Person) RETURN n.name AS name, n.age AS age",
998 QueryLanguage::Gql,
999 None,
1000 )
1001 .unwrap();
1002
1003 assert_eq!(result.row_count(), 0);
1004 assert_eq!(result.columns.len(), 2);
1005 assert_eq!(result.columns[0], "name");
1006 assert_eq!(result.columns[1], "age");
1007 }
1008
1009 #[cfg(feature = "gql")]
1010 #[test]
1011 fn test_params_string_equality() {
1012 let store = Arc::new(LpgStore::new());
1014 store.create_node_with_props(&["Item"], [("name", Value::String("alpha".into()))]);
1015 store.create_node_with_props(&["Item"], [("name", Value::String("beta".into()))]);
1016 store.create_node_with_props(&["Item"], [("name", Value::String("gamma".into()))]);
1017
1018 let processor = QueryProcessor::for_lpg(store);
1019
1020 let mut params = HashMap::new();
1021 params.insert("target".to_string(), Value::String("beta".into()));
1022
1023 let result = processor
1024 .process(
1025 "MATCH (n:Item) WHERE n.name = $target RETURN n.name",
1026 QueryLanguage::Gql,
1027 Some(¶ms),
1028 )
1029 .unwrap();
1030
1031 assert_eq!(result.row_count(), 1);
1032 assert_eq!(result.rows[0][0], Value::String("beta".into()));
1033 }
1034}