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