Repository: FalkorDB/falkordb-rs-next-gen
Pull request #60: clean runtime
Author: login: AviAvni
Created: 2025-05-05T20:21:48Z | Merged: 2025-05-06T09:32:35Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Streamlined query planning and execution by separating planning from runtime execution.
  - Introduced a runtime abstraction for query execution with interior mutability support.
  - Simplified query interfaces by removing the debug flag and updating planner usage.
  - Enhanced variable and parameter handling during query evaluation.
  - Updated internal graph handling to use mutable references safely.
  - Unified runtime function signatures and encapsulated graph access within runtime methods.

- **New Features**
  - Added method to retrieve the count of node labels.
  - Extended arithmetic operations for values to support subtraction, multiplication, division, and modulo with error handling.
  - Introduced a custom number wrapper in tests to improve expression evaluation coverage.
  - Added tests for node labels and range function behavior.

- **Bug Fixes**
  - Improved error handling and type checking during query execution.
  - Ensured read-only queries reject write operations with proper error messages.

- **Tests**
  - Added validation to ensure read-only queries cannot perform write operations.
  - Enhanced test comparisons between write and read-only query results with optional strictness.
  - Refactored operator tests to cover logical and arithmetic combinations more comprehensively.
  - Extended tests to cover nested maps, lists, and empty collections in query expressions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/graph/src/graph.rs b/graph/src/graph.rs
index c2551654..2251101b 100644
--- a/graph/src/graph.rs
+++ b/graph/src/graph.rs
@@ -8,22 +8,14 @@ use orx_tree::DynTree;
 use roaring::RoaringTreemap;
 
 use crate::{
+    ast::ExprIR,
     cypher::Parser,
     matrix::{self, Matrix, Remove, Set, Size},
     planner::{IR, Planner},
-    runtime::{Runtime, evaluate_param, ro_run, run},
     tensor::{self, Tensor},
     value::Value,
 };
 
-pub trait ReturnCallback {
-    fn return_value(
-        &self,
-        graph: &Graph,
-        value: Value,
-    );
-}
-
 pub struct Graph {
     node_cap: u64,
     relationship_cap: u64,
@@ -75,6 +67,10 @@ impl Graph {
         }
     }
 
+    pub fn get_labels_count(&self) -> usize {
+        self.node_labels.len()
+    }
+
     pub fn get_labels(&self) -> impl Iterator<Item = &String> {
         self.node_labels.iter()
     }
@@ -116,141 +112,44 @@ impl Graph {
             .map(|p| p as u64)
     }
 
-    pub fn query<CB: ReturnCallback>(
-        &mut self,
-        query: &str,
-        callback: &CB,
-        debug: bool,
-    ) -> Result<ResultSummary, String> {
-        let mut parse_duration = Duration::ZERO;
-        let mut plan_duration = Duration::ZERO;
-
-        let mut parser = Parser::new(query);
-        let (parameters, query) = parser.parse_parameters()?;
-
-        let evaluate = {
-            match self.cache.lock() {
-                Ok(mut cache) => {
-                    if let Some(f) = cache.get(query) {
-                        f.to_owned()
-                    } else {
-                        let start = Instant::now();
-                        let ir = parser.parse()?;
-                        parse_duration = start.elapsed();
-
-                        let mut planner = Planner::new();
-                        let start = Instant::now();
-                        let value = planner.plan(ir, debug);
-                        plan_duration = start.elapsed();
-
-                        cache.insert(query.to_string(), value.clone());
-                        value
-                    }
-                }
-                Err(_) => {
-                    return Err("Failed to acquire read lock on cache".to_string());
-                }
-            }
-        };
-
-        let labels_count = self.node_labels.len();
-        let mut runtime = Runtime::new(
-            parameters
-                .into_iter()
-                .map(|(k, v)| (k, evaluate_param(v.root())))
-                .collect(),
-        );
-        let start = Instant::now();
-        run(
-            &mut BTreeMap::new(),
-            self,
-            &mut runtime,
-            callback,
-            &evaluate.root(),
-        )?;
-        let run_duration = start.elapsed();
-
-        Ok(ResultSummary {
-            parse_duration,
-            plan_duration,
-            run_duration,
-            labels_added: self.node_labels.len() - labels_count,
-            labels_removed: 0,
-            nodes_created: runtime.nodes_created,
-            relationships_created: runtime.relationships_created,
-            nodes_deleted: runtime.nodes_deleted,
-            relationships_deleted: runtime.relationships_deleted,
-            properties_set: runtime.properties_set,
-            properties_removed: runtime.properties_removed,
-        })
-    }
-
-    pub fn ro_query<CB: ReturnCallback>(
+    pub fn get_plan(
         &self,
         query: &str,
-        callback: &CB,
-        debug: bool,
-    ) -> Result<ResultSummary, String> {
+    ) -> Result<
+        (
+            DynTree<IR>,
+            BTreeMap<String, DynTree<ExprIR>>,
+            Duration,
+            Duration,
+        ),
+        String,
+    > {
         let mut parse_duration = Duration::ZERO;
         let mut plan_duration = Duration::ZERO;
 
         let mut parser = Parser::new(query);
         let (parameters, query) = parser.parse_parameters()?;
 
-        let evaluate = {
-            match self.cache.lock() {
-                Ok(mut cache) => {
-                    if let Some(f) = cache.get(query) {
-                        f.to_owned()
-                    } else {
-                        let start = Instant::now();
-                        let ir = parser.parse()?;
-                        parse_duration = start.elapsed();
-
-                        let mut planner = Planner::new();
-                        let start = Instant::now();
-                        let value = planner.plan(ir, debug);
-                        plan_duration = start.elapsed();
-
-                        cache.insert(query.to_string(), value.clone());
-                        value
-                    }
-                }
-                Err(_) => {
-                    return Err("Failed to acquire read lock on cache".to_string());
+        match self.cache.lock() {
+            Ok(mut cache) => {
+                if let Some(f) = cache.get(query) {
+                    Ok((f.to_owned(), parameters, parse_duration, plan_duration))
+                } else {
+                    let start = Instant::now();
+                    let ir = parser.parse()?;
+                    parse_duration = start.elapsed();
+
+                    let mut planner = Planner::new();
+                    let start = Instant::now();
+                    let value = planner.plan(ir);
+                    plan_duration = start.elapsed();
+
+                    cache.insert(query.to_string(), value.clone());
+                    Ok((value, parameters, parse_duration, plan_duration))
                 }
             }
-        };
-
-        let mut runtime = Runtime::new(
-            parameters
-                .into_iter()
-                .map(|(k, v)| (k, evaluate_param(v.root())))
-                .collect(),
-        );
-        let start = Instant::now();
-        ro_run(
-            &mut BTreeMap::new(),
-            self,
-            &mut runtime,
-            callback,
-            &evaluate.root(),
-        )?;
-        let run_duration = start.elapsed();
-
-        Ok(ResultSummary {
-            parse_duration,
-            plan_duration,
-            run_duration,
-            labels_added: 0,
-            labels_removed: 0,
-            nodes_created: 0,
-            relationships_created: 0,
-            nodes_deleted: 0,
-            relationships_deleted: 0,
-            properties_set: 0,
-            properties_removed: 0,
-        })
+            Err(_) => Err("Failed to acquire read lock on cache".to_string()),
+        }
     }
 
     fn get_label_matrix(
@@ -586,17 +485,3 @@ impl Graph {
             .unwrap_or_else(|| panic!("Relationship with id {id} not found"))
     }
 }
-
-pub struct ResultSummary {
-    pub parse_duration: Duration,
-    pub plan_duration: Duration,
-    pub run_duration: Duration,
-    pub labels_added: usize,
-    pub labels_removed: i32,
-    pub nodes_created: i32,
-    pub relationships_created: i32,
-    pub nodes_deleted: i32,
-    pub relationships_deleted: i32,
-    pub properties_set: i32,
-    pub properties_removed: i32,
-}
diff --git a/graph/src/planner.rs b/graph/src/planner.rs
index faaeebc0..17bbafc2 100644
--- a/graph/src/planner.rs
+++ b/graph/src/planner.rs
@@ -476,7 +476,6 @@ impl Planner {
     pub fn plan(
         &mut self,
         ir: QueryIR,
-        _debug: bool,
     ) -> DynTree<IR> {
         self.plan_query(ir, &mut Vec::new().into_iter())
     }
diff --git a/graph/src/runtime.rs b/graph/src/runtime.rs
index 6f74c425..e5e8c0dc 100644
--- a/graph/src/runtime.rs
+++ b/graph/src/runtime.rs
@@ -1,22 +1,45 @@
-use crate::graph::ReturnCallback;
 use crate::{ast::ExprIR, graph::Graph, planner::IR, value::Contains, value::Value};
 use crate::{matrix, tensor};
-use orx_tree::{DynNode, NodeRef};
+use orx_tree::{DynNode, DynTree, NodeRef};
 use rand::Rng;
+use std::cell::RefCell;
 use std::cmp::Ordering;
 use std::collections::BTreeMap;
 use std::hash::{DefaultHasher, Hash, Hasher};
+use std::time::{Duration, Instant};
 
-type ReadFn = fn(&Graph, &mut Runtime, Vec<Value>) -> Result<Value, String>;
-type WriteFn = fn(&mut Graph, &mut Runtime, Vec<Value>) -> Result<Value, String>;
+type RuntimeFn<'a> = fn(&mut Runtime<'a>, Vec<Value>) -> Result<Value, String>;
 
-pub struct Runtime {
-    read_functions: BTreeMap<String, ReadFn>,
-    write_functions: BTreeMap<String, WriteFn>,
+pub trait ReturnCallback {
+    fn return_value(
+        &self,
+        graph: &RefCell<Graph>,
+        value: Value,
+    );
+}
+
+pub struct ResultSummary {
+    pub run_duration: Duration,
+    pub labels_added: usize,
+    pub labels_removed: i32,
+    pub nodes_created: i32,
+    pub relationships_created: i32,
+    pub nodes_deleted: i32,
+    pub relationships_deleted: i32,
+    pub properties_set: i32,
+    pub properties_removed: i32,
+}
+
+pub struct Runtime<'a> {
+    read_functions: BTreeMap<String, RuntimeFn<'a>>,
+    write_functions: BTreeMap<String, RuntimeFn<'a>>,
     agg_ctxs: BTreeMap<u64, (Value, Value)>,
     node_iters: Vec<matrix::Iter<bool>>,
     relationship_iters: Vec<tensor::Iter>,
     parameters: BTreeMap<String, Value>,
+    vars: BTreeMap<String, Value>,
+    g: &'a RefCell<Graph>,
+    write: bool,
     pub nodes_created: i32,
     pub relationships_created: i32,
     pub nodes_deleted: i32,
@@ -25,11 +48,15 @@ pub struct Runtime {
     pub properties_removed: i32,
 }
 
-impl Runtime {
+impl<'a> Runtime<'a> {
     #[must_use]
-    pub fn new(parameters: BTreeMap<String, Value>) -> Self {
-        let mut write_functions: BTreeMap<String, WriteFn> = BTreeMap::new();
-        let mut read_functions: BTreeMap<String, ReadFn> = BTreeMap::new();
+    pub fn new(
+        g: &'a RefCell<Graph>,
+        parameters: BTreeMap<String, DynTree<ExprIR>>,
+        write: bool,
+    ) -> Self {
+        let mut write_functions: BTreeMap<String, RuntimeFn> = BTreeMap::new();
+        let mut read_functions: BTreeMap<String, RuntimeFn> = BTreeMap::new();
 
         // write functions
         write_functions.insert("create_node".to_string(), Self::create_node);
@@ -110,7 +137,13 @@ impl Runtime {
             agg_ctxs: BTreeMap::new(),
             node_iters: Vec::new(),
             relationship_iters: Vec::new(),
-            parameters,
+            parameters: parameters
+                .into_iter()
+                .map(|(k, v)| (k, evaluate_param(v.root())))
+                .collect(),
+            vars: BTreeMap::new(),
+            g,
+            write,
             nodes_created: 0,
             relationships_created: 0,
             nodes_deleted: 0,
@@ -120,9 +153,299 @@ impl Runtime {
         }
     }
 
+    pub fn query<CB: ReturnCallback>(
+        &mut self,
+        plan: DynTree<IR>,
+        callback: &CB,
+    ) -> Result<ResultSummary, String> {
+        let labels_count = self.g.borrow().get_labels_count();
+        let start = Instant::now();
+        self.run(callback, &plan.root())?;
+        let run_duration = start.elapsed();
+
+        Ok(ResultSummary {
+            run_duration,
+            labels_added: self.g.borrow().get_labels_count() - labels_count,
+            labels_removed: 0,
+            nodes_created: self.nodes_created,
+            relationships_created: self.relationships_created,
+            nodes_deleted: self.nodes_deleted,
+            relationships_deleted: self.relationships_deleted,
+            properties_set: self.properties_set,
+            properties_removed: self.properties_removed,
+        })
+    }
+
+    #[allow(clippy::too_many_lines)]
+    fn run_expr(
+        &mut self,
+        ir: &DynNode<ExprIR>,
+    ) -> Result<Value, String> {
+        match ir.data() {
+            ExprIR::Null => Ok(Value::Null),
+            ExprIR::Bool(x) => Ok(Value::Bool(*x)),
+            ExprIR::Integer(x) => Ok(Value::Int(*x)),
+            ExprIR::Float(x) => Ok(Value::Float(*x)),
+            ExprIR::String(x) => Ok(Value::String(x.to_string())),
+            ExprIR::Var(x) => self.vars.get(x).map_or_else(
+                || Err(format!("Variable {x} not found")),
+                |v| Ok(v.to_owned()),
+            ),
+            ExprIR::Parameter(x) => self.parameters.get(x).map_or_else(
+                || Err(format!("Parameter {x} not found")),
+                |v| Ok(v.to_owned()),
+            ),
+            ExprIR::List => Ok(Value::List(
+                ir.children()
+                    .map(|ir| self.run_expr(&ir))
+                    .collect::<Result<Vec<_>, _>>()?,
+            )),
+            ExprIR::Length => match self.run_expr(&ir.child(0))? {
+                Value::List(arr) => Ok(Value::Int(arr.len() as _)),
+                _ => Err("Length operator requires a list".to_string()),
+            },
+            ExprIR::GetElement => {
+                let arr = self.run_expr(&ir.child(0))?;
+                let i = self.run_expr(&ir.child(1))?;
+                match (arr, i) {
+                    (Value::List(values), Value::Int(i)) => {
+                        if i >= 0 && i < values.len() as _ {
+                            Ok(values[i as usize].clone())
+                        } else {
+                            Ok(Value::Null)
+                        }
+                    }
+                    (Value::List(_), v) => {
+                        Err(format!("Type mismatch: expected Bool but was {v:?}"))
+                    }
+                    v => Err(format!("Type mismatch: expected List but was {v:?}")),
+                }
+            }
+            ExprIR::GetElements => {
+                let arr = self.run_expr(&ir.child(0))?;
+                let a = self.run_expr(&ir.child(1))?;
+                let b = self.run_expr(&ir.child(2))?;
+                get_elements(arr, a, b)
+            }
+            ExprIR::IsNull => match self.run_expr(&ir.child(0))? {
+                Value::Null => Ok(Value::Bool(true)),
+                _ => Ok(Value::Bool(false)),
+            },
+            ExprIR::IsNode => match self.run_expr(&ir.child(0))? {
+                Value::Node(_) => Ok(Value::Bool(true)),
+                _ => Ok(Value::Bool(false)),
+            },
+            ExprIR::IsRelationship => match self.run_expr(&ir.child(0))? {
+                Value::Relationship(_, _, _) => Ok(Value::Bool(true)),
+                _ => Ok(Value::Bool(false)),
+            },
+            ExprIR::Or => {
+                let mut is_null = false;
+                for ir in ir.children() {
+                    match self.run_expr(&ir)? {
+                        Value::Bool(true) => return Ok(Value::Bool(true)),
+                        Value::Bool(false) => {}
+                        Value::Null => is_null = true,
+                        _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
+                    }
+                }
+                if is_null {
+                    return Ok(Value::Null);
+                }
+
+                Ok(Value::Bool(false))
+            }
+            ExprIR::Xor => {
+                let mut last = None;
+                for ir in ir.children() {
+                    match self.run_expr(&ir)? {
+                        Value::Bool(b) => last = Some(last.map_or(b, |l| logical_xor(l, b))),
+                        Value::Null => return Ok(Value::Null),
+                        _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
+                    }
+                }
+                Ok(Value::Bool(last.unwrap_or(false)))
+            }
+            ExprIR::And => {
+                let mut is_null = false;
+                for ir in ir.children() {
+                    match self.run_expr(&ir)? {
+                        Value::Bool(false) => return Ok(Value::Bool(false)),
+                        Value::Bool(true) => {}
+                        Value::Null => is_null = true,
+                        _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
+                    }
+                }
+                if is_null {
+                    return Ok(Value::Null);
+                }
+
+                Ok(Value::Bool(true))
+            }
+            ExprIR::Not => match self.run_expr(&ir.child(0))? {
+                Value::Bool(b) => Ok(Value::Bool(!b)),
+                Value::Null => Ok(Value::Null),
+                _ => {
+                    Err("InvalidArgumentType: Not operator requires a boolean or null".to_string())
+                }
+            },
+            ExprIR::Negate => match self.run_expr(&ir.child(0))? {
+                Value::Int(i) => Ok(Value::Int(-i)),
+                Value::Float(f) => Ok(Value::Float(-f)),
+                Value::Null => Ok(Value::Null),
+                _ => Err(
+                    "InvalidArgumentType: Negate operator requires an Integer or Float".to_string(),
+                ),
+            },
+            ExprIR::Eq => all_equals(ir.children().map(|ir| self.run_expr(&ir))),
+            ExprIR::Neq => ir
+                .children()
+                .flat_map(|ir| self.run_expr(&ir))
+                .reduce(|a, b| Value::Bool(a != b))
+                .ok_or_else(|| "Neq operator requires at least one argument".to_string()),
+            ExprIR::Lt => match (self.run_expr(&ir.child(0))?, self.run_expr(&ir.child(1))?) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
+                _ => Err("Lt operator requires two integers".to_string()),
+            },
+            ExprIR::Gt => match (self.run_expr(&ir.child(0))?, self.run_expr(&ir.child(1))?) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
+                _ => Err("Gt operator requires two integers".to_string()),
+            },
+            ExprIR::Le => match (self.run_expr(&ir.child(0))?, self.run_expr(&ir.child(1))?) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
+                _ => Err("Le operator requires two integers".to_string()),
+            },
+            ExprIR::Ge => match (self.run_expr(&ir.child(0))?, self.run_expr(&ir.child(1))?) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
+                _ => Err("Ge operator requires two integers".to_string()),
+            },
+            ExprIR::In => {
+                let value = self.run_expr(&ir.child(0))?;
+                let list = self.run_expr(&ir.child(1))?;
+                list_contains(&list, &value)
+            }
+            ExprIR::Add => ir
+                .children()
+                .map(|ir| self.run_expr(&ir))
+                .reduce(|acc, value| acc? + value?)
+                .ok_or_else(|| "Add operator requires at least one operand".to_string())?,
+            ExprIR::Sub => ir
+                .children()
+                .map(|ir| self.run_expr(&ir))
+                .reduce(|acc, value| acc? - value?)
+                .ok_or_else(|| "Sub operator requires at least one argument".to_string())?,
+            ExprIR::Mul => ir
+                .children()
+                .map(|ir| self.run_expr(&ir))
+                .reduce(|acc, value| acc? * value?)
+                .ok_or_else(|| "Mul operator requires at least one argument".to_string())?,
+            ExprIR::Div => ir
+                .children()
+                .map(|ir| self.run_expr(&ir))
+                .reduce(|acc, value| acc? / value?)
+                .ok_or_else(|| "Div operator requires at least one argument".to_string())?,
+            ExprIR::Modulo => ir
+                .children()
+                .map(|ir| self.run_expr(&ir))
+                .reduce(|acc, value| acc? % value?)
+                .ok_or_else(|| "Modulo operator requires at least one argument".to_string())?,
+            ExprIR::Pow => ir
+                .children()
+                .flat_map(|ir| self.run_expr(&ir))
+                .reduce(|a, b| match (a, b) {
+                    (Value::Int(a), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
+                    _ => Value::Null,
+                })
+                .ok_or_else(|| "Pow operator requires at least one argument".to_string()),
+            ExprIR::FuncInvocation(name) => {
+                let args = ir
+                    .children()
+                    .map(|ir| self.run_expr(&ir))
+                    .collect::<Result<Vec<_>, _>>()?;
+                if let Some(func) = self.write_functions.get(name) {
+                    if !self.write {
+                        return Err("graph.RO_QUERY is to be executed only on read-only queries"
+                            .to_string());
+                    }
+                    func(self, args)
+                } else if let Some(func) = self.read_functions.get(name) {
+                    func(self, args)
+                } else {
+                    Err(format!("Function {name} not found"))
+                }
+            }
+            ExprIR::Map => Ok(Value::Map(
+                ir.children()
+                    .map(|child| {
+                        (
+                            if let ExprIR::Var(key) = child.data() {
+                                key.to_string()
+                            } else {
+                                todo!();
+                            },
+                            self.run_expr(&child.child(0)).unwrap_or(Value::Null),
+                        )
+                    })
+                    .collect(),
+            )),
+            ExprIR::Set(x) => {
+                let v = self.run_expr(&ir.child(0))?;
+                self.vars.insert(x.to_string(), v.clone());
+                Ok(v)
+            }
+        }
+    }
+
+    #[allow(clippy::too_many_lines)]
+    fn run<CB: ReturnCallback>(
+        &mut self,
+        callback: &CB,
+        ir: &DynNode<IR>,
+    ) -> Result<Value, String> {
+        match ir.data() {
+            IR::Expr(expr) => self.run_expr(&expr.root()),
+            IR::If => match self.run(callback, &ir.child(0))? {
+                Value::Bool(true) => self.run(callback, &ir.child(1)),
+                _ => Ok(Value::Null),
+            },
+            IR::For => {
+                self.run(callback, &ir.child(0))?;
+                while self.run(callback, &ir.child(1))? == Value::Bool(true) {
+                    self.run(callback, &ir.child(3))?;
+                    self.run(callback, &ir.child(2))?;
+                }
+                Ok(Value::Null)
+            }
+            IR::Return => {
+                let v = self.run(callback, &ir.child(0))?;
+                callback.return_value(&self.g, v);
+                Ok(Value::Null)
+            }
+            IR::ReturnAggregation => {
+                self.run(callback, &ir.child(0))?;
+                for (keys, r) in self.agg_ctxs.values_mut() {
+                    if let Value::List(keys) = keys {
+                        keys.push(r.clone());
+                        callback.return_value(&self.g, Value::List(keys.clone()));
+                    } else {
+                        callback.return_value(&self.g, Value::List(vec![r.clone()]));
+                    }
+                }
+                self.agg_ctxs.clear();
+                Ok(Value::Null)
+            }
+            IR::Block => {
+                let mut v = Value::Null;
+                for ir in ir.children() {
+                    v = self.run(callback, &ir)?;
+                }
+                Ok(v)
+            }
+        }
+    }
+
     #[allow(clippy::unnecessary_wraps)]
     fn create_node(
-        g: &mut Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -147,7 +470,7 @@ impl Runtime {
                         _ => 1,
                     })
                     .sum::<i32>();
-                Ok(g.create_node(&labels, attrs))
+                Ok(runtime.g.borrow_mut().create_node(&labels, attrs))
             }
             _ => Ok(Value::Null),
         }
@@ -155,13 +478,13 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn delete_entity(
-        g: &mut Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
         for n in args {
             if let Value::Node(id) = n {
                 runtime.nodes_deleted += 1;
+                let mut g = runtime.g.borrow_mut();
                 for (src, dest, id) in g.get_node_relationships(id).collect::<Vec<_>>() {
                     runtime.relationships_deleted += 1;
                     g.delete_relationship(id, src, dest);
@@ -174,7 +497,6 @@ impl Runtime {
     }
 
     fn create_relationship(
-        g: &mut Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -201,7 +523,10 @@ impl Runtime {
                         _ => 1,
                     })
                     .sum::<i32>();
-                Ok(g.create_relationship(&relationship_type, from, to, attrs))
+                Ok(runtime
+                    .g
+                    .borrow_mut()
+                    .create_relationship(&relationship_type, from, to, attrs))
             }
             _ => todo!(),
         }
@@ -209,7 +534,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn create_aggregate_ctx(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -224,7 +548,6 @@ impl Runtime {
     }
 
     fn create_node_iter(
-        g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -232,20 +555,23 @@ impl Runtime {
         match (iter.next(), iter.next()) {
             (Some(Value::List(raw_labels)), None) => {
                 runtime.node_iters.push(
-                    g.get_nodes(
-                        raw_labels
-                            .into_iter()
-                            .filter_map(|label| {
-                                if let Value::String(label) = label {
-                                    Some(label)
-                                } else {
-                                    None
-                                }
-                            })
-                            .collect::<Vec<_>>()
-                            .as_slice(),
-                    )
-                    .unwrap(),
+                    runtime
+                        .g
+                        .borrow()
+                        .get_nodes(
+                            raw_labels
+                                .into_iter()
+                                .filter_map(|label| {
+                                    if let Value::String(label) = label {
+                                        Some(label)
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .collect::<Vec<_>>()
+                                .as_slice(),
+                        )
+                        .unwrap(),
                 );
 
                 Ok(Value::Int(runtime.node_iters.len() as i64 - 1))
@@ -255,7 +581,6 @@ impl Runtime {
     }
 
     fn next_node(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -269,7 +594,6 @@ impl Runtime {
     }
 
     fn create_relationship_iter(
-        g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -278,7 +602,7 @@ impl Runtime {
             (Some(Value::String(raw_type)), None) => {
                 runtime
                     .relationship_iters
-                    .push(g.get_relationships(&[raw_type]).unwrap());
+                    .push(runtime.g.borrow().get_relationships(&[raw_type]).unwrap());
                 Ok(Value::Int(runtime.relationship_iters.len() as i64 - 1))
             }
             _ => todo!(),
@@ -286,7 +610,6 @@ impl Runtime {
     }
 
     fn next_relationship(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -302,16 +625,20 @@ impl Runtime {
     }
 
     fn property(
-        g: &Graph,
-        _runtime: &mut Self,
+        runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
         let mut iter = args.into_iter();
         match (iter.next(), iter.next(), iter.next()) {
-            (Some(Value::Node(node_id)), Some(Value::String(property)), None) => g
+            (Some(Value::Node(node_id)), Some(Value::String(property)), None) => runtime
+                .g
+                .borrow()
                 .get_node_property_id(&property)
                 .map_or(Ok(Value::Null), |property_id| {
-                    g.get_node_property(node_id, property_id)
+                    runtime
+                        .g
+                        .borrow()
+                        .get_node_property(node_id, property_id)
                         .map_or(Ok(Value::Null), Ok)
                 }),
             (Some(Value::Map(map)), Some(Value::String(property)), None) => {
@@ -323,15 +650,19 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn labels(
-        g: &Graph,
-        _runtime: &mut Self,
+        runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
         let mut iter = args.into_iter();
         match (iter.next(), iter.next()) {
             (Some(Value::Node(node_id)), None) => Ok(Value::List(
-                g.get_node_label_ids(node_id)
-                    .map(|label_id| Value::String(g.get_label_by_id(label_id).to_string()))
+                runtime
+                    .g
+                    .borrow()
+                    .get_node_label_ids(node_id)
+                    .map(|label_id| {
+                        Value::String(runtime.g.borrow().get_label_by_id(label_id).to_string())
+                    })
                     .collect(),
             )),
             _ => Ok(Value::Null),
@@ -363,7 +694,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn start_node(
-        _g: &Graph,
         _runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -376,7 +706,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn end_node(
-        _g: &Graph,
         _runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -389,7 +718,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn collect(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -408,7 +736,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn count(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -430,7 +757,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn sum(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -448,7 +774,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn max(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -468,7 +793,6 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn min(
-        _g: &Graph,
         runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -487,7 +811,6 @@ impl Runtime {
     }
 
     fn value_to_integer(
-        _g: &Graph,
         _runtime: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -513,7 +836,6 @@ impl Runtime {
     }
 
     fn size(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -530,7 +852,6 @@ impl Runtime {
     }
 
     fn head(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -552,7 +873,6 @@ impl Runtime {
     }
 
     fn last(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -568,7 +888,6 @@ impl Runtime {
     }
 
     fn tail(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -590,7 +909,6 @@ impl Runtime {
     }
 
     fn reverse(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -611,7 +929,6 @@ impl Runtime {
     }
 
     fn substring(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -667,7 +984,6 @@ impl Runtime {
     }
 
     fn split(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -703,7 +1019,6 @@ impl Runtime {
     }
 
     fn string_to_lower(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -719,7 +1034,6 @@ impl Runtime {
     }
 
     fn string_to_upper(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -735,7 +1049,6 @@ impl Runtime {
     }
 
     fn string_replace(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -757,7 +1070,6 @@ impl Runtime {
     }
 
     fn string_left(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -781,7 +1093,6 @@ impl Runtime {
     }
 
     fn string_ltrim(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -797,7 +1108,6 @@ impl Runtime {
     }
 
     fn string_right(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -821,7 +1131,6 @@ impl Runtime {
         }
     }
     fn string_join(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -859,7 +1168,6 @@ impl Runtime {
     }
 
     fn string_match_reg_ex(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -892,7 +1200,6 @@ impl Runtime {
     }
 
     fn string_replace_reg_ex(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -926,7 +1233,6 @@ impl Runtime {
     }
 
     fn abs(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -943,7 +1249,6 @@ impl Runtime {
     }
 
     fn ceil(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -960,7 +1265,6 @@ impl Runtime {
     }
 
     fn e(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -971,7 +1275,6 @@ impl Runtime {
     }
 
     fn exp(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -988,7 +1291,6 @@ impl Runtime {
     }
 
     fn floor(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1005,7 +1307,6 @@ impl Runtime {
     }
 
     fn log(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1022,7 +1323,6 @@ impl Runtime {
     }
 
     fn log10(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1038,7 +1338,6 @@ impl Runtime {
         }
     }
     fn pow(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1059,7 +1358,6 @@ impl Runtime {
     }
 
     fn rand(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1073,7 +1371,6 @@ impl Runtime {
     }
 
     fn round(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1090,7 +1387,6 @@ impl Runtime {
     }
 
     fn sign(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1111,7 +1407,6 @@ impl Runtime {
     }
 
     fn sqrt(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1140,7 +1435,6 @@ impl Runtime {
     }
 
     fn range(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1149,17 +1443,24 @@ impl Runtime {
         let step = args.get(2).unwrap_or(&Value::Int(1));
         match (start, end, step) {
             (Value::Int(start), Value::Int(end), Value::Int(step)) => {
-                Ok(Value::List(if step < &0 {
-                    (*end..=*start)
-                        .step_by((-step) as usize)
-                        .map(Value::Int)
-                        .collect()
+                if start >= end && step < &0 {
+                    Ok(Value::List(
+                        (*end..=*start)
+                            .rev()
+                            .step_by(step.abs() as usize)
+                            .map(Value::Int)
+                            .collect(),
+                    ))
+                } else if step < &0 {
+                    Ok(Value::List(vec![]))
                 } else {
-                    (*start..=*end)
-                        .step_by(*step as usize)
-                        .map(Value::Int)
-                        .collect()
-                }))
+                    Ok(Value::List(
+                        (*start..=*end)
+                            .step_by(*step as usize)
+                            .map(Value::Int)
+                            .collect(),
+                    ))
+                }
             }
             _ => Err("Range operator requires two integers".to_string()),
         }
@@ -1170,7 +1471,6 @@ impl Runtime {
     //
 
     fn internal_starts_with(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1188,7 +1488,6 @@ impl Runtime {
     }
 
     fn internal_ends_with(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1205,7 +1504,6 @@ impl Runtime {
     }
 
     fn internal_contains(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1222,7 +1520,6 @@ impl Runtime {
     }
 
     fn internal_regex_matches(
-        _: &Graph,
         _: &mut Self,
         args: Vec<Value>,
     ) -> Result<Value, String> {
@@ -1246,12 +1543,14 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn db_labels(
-        g: &Graph,
-        _runtime: &mut Self,
+        runtime: &mut Self,
         _args: Vec<Value>,
     ) -> Result<Value, String> {
         Ok(Value::List(
-            g.get_labels()
+            runtime
+                .g
+                .borrow()
+                .get_labels()
                 .map(|n| Value::String(n.to_string()))
                 .collect(),
         ))
@@ -1259,12 +1558,14 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn db_types(
-        g: &Graph,
-        _runtime: &mut Self,
+        runtime: &mut Self,
         _args: Vec<Value>,
     ) -> Result<Value, String> {
         Ok(Value::List(
-            g.get_types()
+            runtime
+                .g
+                .borrow()
+                .get_types()
                 .map(|n| Value::String(n.to_string()))
                 .collect(),
         ))
@@ -1272,601 +1573,22 @@ impl Runtime {
 
     #[allow(clippy::unnecessary_wraps)]
     fn db_properties(
-        g: &Graph,
-        _runtime: &mut Self,
+        runtime: &mut Self,
         _args: Vec<Value>,
     ) -> Result<Value, String> {
         Ok(Value::List(
-            g.get_properties()
+            runtime
+                .g
+                .borrow()
+                .get_properties()
                 .map(|n| Value::String(n.to_string()))
                 .collect(),
         ))
     }
 }
 
-#[allow(clippy::too_many_lines)]
-fn ro_run_expr(
-    vars: &mut BTreeMap<String, Value>,
-    g: &Graph,
-    runtime: &mut Runtime,
-    ir: &DynNode<ExprIR>,
-) -> Result<Value, String> {
-    match ir.data() {
-        ExprIR::Null => Ok(Value::Null),
-        ExprIR::Bool(x) => Ok(Value::Bool(*x)),
-        ExprIR::Integer(x) => Ok(Value::Int(*x)),
-        ExprIR::Float(x) => Ok(Value::Float(*x)),
-        ExprIR::String(x) => Ok(Value::String(x.to_string())),
-        ExprIR::Var(x) => vars.get(x).map_or_else(
-            || Err(format!("Variable {x} not found")),
-            |v| Ok(v.to_owned()),
-        ),
-        ExprIR::Parameter(x) => runtime.parameters.get(x).map_or_else(
-            || Err(format!("Parameter {x} not found")),
-            |v| Ok(v.to_owned()),
-        ),
-        ExprIR::List => Ok(Value::List(
-            ir.children()
-                .map(|ir| ro_run_expr(vars, g, runtime, &ir))
-                .collect::<Result<Vec<_>, _>>()?,
-        )),
-        ExprIR::Length => match ro_run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::List(arr) => Ok(Value::Int(arr.len() as _)),
-            _ => Err("Length operator requires a list".to_string()),
-        },
-        ExprIR::GetElement => {
-            let arr = ro_run_expr(vars, g, runtime, &ir.child(0))?;
-            let i = ro_run_expr(vars, g, runtime, &ir.child(1))?;
-            match (arr, i) {
-                (Value::List(values), Value::Int(i)) => {
-                    if i >= 0 && i < values.len() as _ {
-                        Ok(values[i as usize].clone())
-                    } else {
-                        Ok(Value::Null)
-                    }
-                }
-                (Value::List(_), v) => Err(format!("Type mismatch: expected Bool but was {v:?}")),
-                v => Err(format!("Type mismatch: expected List but was {v:?}")),
-            }
-        }
-        ExprIR::GetElements => {
-            let arr = ro_run_expr(vars, g, runtime, &ir.child(0))?;
-            let a = ro_run_expr(vars, g, runtime, &ir.child(1))?;
-            let b = ro_run_expr(vars, g, runtime, &ir.child(2))?;
-            get_elements(arr, a, b)
-        }
-        ExprIR::IsNull => match ro_run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Null => Ok(Value::Bool(true)),
-            _ => Ok(Value::Bool(false)),
-        },
-        ExprIR::IsNode => match ro_run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Node(_) => Ok(Value::Bool(true)),
-            _ => Ok(Value::Bool(false)),
-        },
-        ExprIR::IsRelationship => match ro_run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Relationship(_, _, _) => Ok(Value::Bool(true)),
-            _ => Ok(Value::Bool(false)),
-        },
-        ExprIR::Or => {
-            let mut is_null = false;
-            for ir in ir.children() {
-                match ro_run_expr(vars, g, runtime, &ir)? {
-                    Value::Bool(true) => return Ok(Value::Bool(true)),
-                    Value::Bool(false) => {}
-                    Value::Null => is_null = true,
-                    _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
-                }
-            }
-            if is_null {
-                return Ok(Value::Null);
-            }
-
-            Ok(Value::Bool(false))
-        }
-        ExprIR::Xor => {
-            let mut last = None;
-            for ir in ir.children() {
-                match ro_run_expr(vars, g, runtime, &ir)? {
-                    Value::Bool(b) => last = Some(last.map_or(b, |l| logical_xor(l, b))),
-                    Value::Null => return Ok(Value::Null),
-                    _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
-                }
-            }
-            Ok(Value::Bool(last.unwrap_or(false)))
-        }
-        ExprIR::And => {
-            let mut is_null = false;
-            for ir in ir.children() {
-                match ro_run_expr(vars, g, runtime, &ir)? {
-                    Value::Bool(false) => return Ok(Value::Bool(false)),
-                    Value::Bool(true) => {}
-                    Value::Null => is_null = true,
-                    _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
-                }
-            }
-            if is_null {
-                return Ok(Value::Null);
-            }
-
-            Ok(Value::Bool(true))
-        }
-        ExprIR::Not => match ro_run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Bool(b) => Ok(Value::Bool(!b)),
-            Value::Null => Ok(Value::Null),
-            _ => Err("InvalidArgumentType: Not operator requires a boolean or null".to_string()),
-        },
-        ExprIR::Negate => match ro_run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Int(i) => Ok(Value::Int(-i)),
-            Value::Float(f) => Ok(Value::Float(-f)),
-            Value::Null => Ok(Value::Null),
-            _ => {
-                Err("InvalidArgumentType: Negate operator requires an Integer or Float".to_string())
-            }
-        },
-        ExprIR::Eq => all_equals(ir.children().map(|ir| ro_run_expr(vars, g, runtime, &ir))),
-        ExprIR::Neq => ir
-            .children()
-            .flat_map(|ir| ro_run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| Value::Bool(a != b))
-            .ok_or_else(|| "Neq operator requires at least one argument".to_string()),
-        ExprIR::Lt => match (
-            ro_run_expr(vars, g, runtime, &ir.child(0))?,
-            ro_run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
-            _ => Err("Lt operator requires two integers".to_string()),
-        },
-        ExprIR::Gt => match (
-            ro_run_expr(vars, g, runtime, &ir.child(0))?,
-            ro_run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
-            _ => Err("Gt operator requires two integers".to_string()),
-        },
-        ExprIR::Le => match (
-            ro_run_expr(vars, g, runtime, &ir.child(0))?,
-            ro_run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
-            _ => Err("Le operator requires two integers".to_string()),
-        },
-        ExprIR::Ge => match (
-            ro_run_expr(vars, g, runtime, &ir.child(0))?,
-            ro_run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
-            _ => Err("Ge operator requires two integers".to_string()),
-        },
-        ExprIR::In => {
-            let value = ro_run_expr(vars, g, runtime, &ir.child(0))?;
-            let list = ro_run_expr(vars, g, runtime, &ir.child(1))?;
-            list_contains(&list, &value)
-        }
-        ExprIR::Add => ir
-            .children()
-            .map(|ir| ro_run_expr(vars, g, runtime, &ir))
-            .reduce(|acc, value| acc? + value?)
-            .ok_or_else(|| "Add operator requires at least one operand".to_string())?,
-        ExprIR::Sub => ir
-            .children()
-            .flat_map(|ir| ro_run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a - b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Sub operator requires at least one argument".to_string()),
-        ExprIR::Mul => ir
-            .children()
-            .flat_map(|ir| ro_run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a * b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Mul operator requires at least one argument".to_string()),
-        ExprIR::Div => ir
-            .children()
-            .flat_map(|ir| ro_run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a / b),
-                (Value::Int(a), Value::Float(b)) => Value::Float(a as f64 / b),
-                (Value::Float(a), Value::Int(b)) => Value::Float(a / b as f64),
-                (Value::Float(a), Value::Float(b)) => Value::Float(a / b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Div operator requires at least one argument".to_string()),
-        ExprIR::Pow => ir
-            .children()
-            .flat_map(|ir| ro_run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Pow operator requires at least one argument".to_string()),
-        ExprIR::Modulo => ir
-            .children()
-            .flat_map(|ir| ro_run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a % b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Modulo operator requires at least one argument".to_string()),
-        ExprIR::FuncInvocation(name) => {
-            let args = ir
-                .children()
-                .map(|ir| ro_run_expr(vars, g, runtime, &ir))
-                .collect::<Result<Vec<_>, _>>()?;
-            #[allow(clippy::option_if_let_else)]
-            if let Some(func) = runtime.read_functions.get(name) {
-                func(g, runtime, args)
-            } else {
-                Err(format!("Function {name} not found"))
-            }
-        }
-        ExprIR::Map => Ok(Value::Map(
-            ir.children()
-                .map(|child| {
-                    (
-                        if let ExprIR::Var(key) = child.data() {
-                            key.to_string()
-                        } else {
-                            todo!();
-                        },
-                        ro_run_expr(vars, g, runtime, &child.child(0)).unwrap_or(Value::Null),
-                    )
-                })
-                .collect(),
-        )),
-        ExprIR::Set(x) => {
-            let v = ro_run_expr(vars, g, runtime, &ir.child(0))?;
-            vars.insert(x.to_string(), v.clone());
-            Ok(v)
-        }
-    }
-}
-
-#[allow(clippy::too_many_lines)]
-pub fn ro_run<CB: ReturnCallback>(
-    vars: &mut BTreeMap<String, Value>,
-    g: &Graph,
-    runtime: &mut Runtime,
-    callback: &CB,
-    ir: &DynNode<IR>,
-) -> Result<Value, String> {
-    match ir.data() {
-        IR::Expr(expr) => ro_run_expr(vars, g, runtime, &expr.root()),
-        IR::If => match ro_run(vars, g, runtime, callback, &ir.child(0))? {
-            Value::Bool(true) => ro_run(vars, g, runtime, callback, &ir.child(1)),
-            _ => Ok(Value::Null),
-        },
-        IR::For => {
-            ro_run(vars, g, runtime, callback, &ir.child(0))?;
-            while ro_run(vars, g, runtime, callback, &ir.child(1))? == Value::Bool(true) {
-                ro_run(vars, g, runtime, callback, &ir.child(3))?;
-                ro_run(vars, g, runtime, callback, &ir.child(2))?;
-            }
-            Ok(Value::Null)
-        }
-        IR::Return => {
-            let v = ro_run(vars, g, runtime, callback, &ir.child(0))?;
-            callback.return_value(g, v);
-            Ok(Value::Null)
-        }
-        IR::ReturnAggregation => {
-            ro_run(vars, g, runtime, callback, &ir.child(0))?;
-            for (keys, r) in runtime.agg_ctxs.values_mut() {
-                if let Value::List(keys) = keys {
-                    keys.push(r.clone());
-                    callback.return_value(g, Value::List(keys.clone()));
-                } else {
-                    callback.return_value(g, Value::List(vec![r.clone()]));
-                }
-            }
-            runtime.agg_ctxs.clear();
-            Ok(Value::Null)
-        }
-        IR::Block => {
-            let mut v = Value::Null;
-            for ir in ir.children() {
-                v = ro_run(vars, g, runtime, callback, &ir)?;
-            }
-            Ok(v)
-        }
-    }
-}
-
-#[allow(clippy::too_many_lines)]
-fn run_expr(
-    vars: &mut BTreeMap<String, Value>,
-    g: &mut Graph,
-    runtime: &mut Runtime,
-    ir: &DynNode<ExprIR>,
-) -> Result<Value, String> {
-    match ir.data() {
-        ExprIR::Null => Ok(Value::Null),
-        ExprIR::Bool(x) => Ok(Value::Bool(*x)),
-        ExprIR::Integer(x) => Ok(Value::Int(*x)),
-        ExprIR::Float(x) => Ok(Value::Float(*x)),
-        ExprIR::String(x) => Ok(Value::String(x.to_string())),
-        ExprIR::Var(x) => vars.get(x).map_or_else(
-            || Err(format!("Variable {x} not found")),
-            |v| Ok(v.to_owned()),
-        ),
-        ExprIR::Parameter(x) => runtime.parameters.get(x).map_or_else(
-            || Err(format!("Parameter {x} not found")),
-            |v| Ok(v.to_owned()),
-        ),
-        ExprIR::List => Ok(Value::List(
-            ir.children()
-                .map(|ir| run_expr(vars, g, runtime, &ir))
-                .collect::<Result<Vec<_>, _>>()?,
-        )),
-        ExprIR::Length => match run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::List(arr) => Ok(Value::Int(arr.len() as _)),
-            _ => Err("Length operator requires a list".to_string()),
-        },
-        ExprIR::GetElement => {
-            let arr = run_expr(vars, g, runtime, &ir.child(0))?;
-            let i = run_expr(vars, g, runtime, &ir.child(1))?;
-            match (arr, i) {
-                (Value::List(values), Value::Int(i)) => {
-                    if i >= 0 && i < values.len() as _ {
-                        Ok(values[i as usize].clone())
-                    } else {
-                        Ok(Value::Null)
-                    }
-                }
-                (Value::List(_), v) => Err(format!("Type mismatch: expected Bool but was {v:?}")),
-                v => Err(format!("Type mismatch: expected List but was {v:?}")),
-            }
-        }
-        ExprIR::GetElements => {
-            let arr = run_expr(vars, g, runtime, &ir.child(0))?;
-            let a = run_expr(vars, g, runtime, &ir.child(1))?;
-            let b = run_expr(vars, g, runtime, &ir.child(2))?;
-            get_elements(arr, a, b)
-        }
-        ExprIR::IsNull => match run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Null => Ok(Value::Bool(true)),
-            _ => Ok(Value::Bool(false)),
-        },
-        ExprIR::IsNode => match run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Node(_) => Ok(Value::Bool(true)),
-            _ => Ok(Value::Bool(false)),
-        },
-        ExprIR::IsRelationship => match run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Relationship(_, _, _) => Ok(Value::Bool(true)),
-            _ => Ok(Value::Bool(false)),
-        },
-        ExprIR::Or => {
-            let mut is_null = false;
-            for ir in ir.children() {
-                match run_expr(vars, g, runtime, &ir)? {
-                    Value::Bool(true) => return Ok(Value::Bool(true)),
-                    Value::Bool(false) => {}
-                    Value::Null => is_null = true,
-                    _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
-                }
-            }
-            if is_null {
-                return Ok(Value::Null);
-            }
-
-            Ok(Value::Bool(false))
-        }
-        ExprIR::Xor => {
-            let mut last = None;
-            for ir in ir.children() {
-                match run_expr(vars, g, runtime, &ir)? {
-                    Value::Bool(b) => last = Some(last.map_or(b, |l| logical_xor(l, b))),
-                    Value::Null => return Ok(Value::Null),
-                    _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
-                }
-            }
-            Ok(Value::Bool(last.unwrap_or(false)))
-        }
-        ExprIR::And => {
-            let mut is_null = false;
-            for ir in ir.children() {
-                match run_expr(vars, g, runtime, &ir)? {
-                    Value::Bool(false) => return Ok(Value::Bool(false)),
-                    Value::Bool(true) => {}
-                    Value::Null => is_null = true,
-                    _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
-                }
-            }
-            if is_null {
-                return Ok(Value::Null);
-            }
-
-            Ok(Value::Bool(true))
-        }
-        ExprIR::Not => match run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Bool(b) => Ok(Value::Bool(!b)),
-            Value::Null => Ok(Value::Null),
-            _ => Err("InvalidArgumentType: Not operator requires a boolean or null".to_string()),
-        },
-        ExprIR::Negate => match run_expr(vars, g, runtime, &ir.child(0))? {
-            Value::Int(i) => Ok(Value::Int(-i)),
-            Value::Float(f) => Ok(Value::Float(-f)),
-            Value::Null => Ok(Value::Null),
-            _ => {
-                Err("InvalidArgumentType: Negate operator requires an Integer or Float".to_string())
-            }
-        },
-        ExprIR::Eq => all_equals(ir.children().map(|ir| run_expr(vars, g, runtime, &ir))),
-        ExprIR::Neq => ir
-            .children()
-            .flat_map(|ir| run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| Value::Bool(a != b))
-            .ok_or_else(|| "Neq operator requires at least one argument".to_string()),
-        ExprIR::Lt => match (
-            run_expr(vars, g, runtime, &ir.child(0))?,
-            run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
-            _ => Err("Lt operator requires two integers".to_string()),
-        },
-        ExprIR::Gt => match (
-            run_expr(vars, g, runtime, &ir.child(0))?,
-            run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
-            _ => Err("Gt operator requires two integers".to_string()),
-        },
-        ExprIR::Le => match (
-            run_expr(vars, g, runtime, &ir.child(0))?,
-            run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
-            _ => Err("Le operator requires two integers".to_string()),
-        },
-        ExprIR::Ge => match (
-            run_expr(vars, g, runtime, &ir.child(0))?,
-            run_expr(vars, g, runtime, &ir.child(1))?,
-        ) {
-            (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
-            _ => Err("Ge operator requires two integers".to_string()),
-        },
-        ExprIR::In => {
-            let value = run_expr(vars, g, runtime, &ir.child(0))?;
-            let list = run_expr(vars, g, runtime, &ir.child(1))?;
-            list_contains(&list, &value)
-        }
-        ExprIR::Add => ir
-            .children()
-            .map(|ir| run_expr(vars, g, runtime, &ir))
-            .reduce(|acc, value| acc? + value?)
-            .ok_or_else(|| "Add operator requires at least one operand".to_string())?,
-        ExprIR::Sub => ir
-            .children()
-            .flat_map(|ir| run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a - b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Sub operator requires at least one argument".to_string()),
-        ExprIR::Mul => ir
-            .children()
-            .flat_map(|ir| run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a * b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Mul operator requires at least one argument".to_string()),
-        ExprIR::Div => ir
-            .children()
-            .flat_map(|ir| run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a / b),
-                (Value::Int(a), Value::Float(b)) => Value::Float(a as f64 / b),
-                (Value::Float(a), Value::Int(b)) => Value::Float(a / b as f64),
-                (Value::Float(a), Value::Float(b)) => Value::Float(a / b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Div operator requires at least one argument".to_string()),
-        ExprIR::Pow => ir
-            .children()
-            .flat_map(|ir| run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Pow operator requires at least one argument".to_string()),
-        ExprIR::Modulo => ir
-            .children()
-            .flat_map(|ir| run_expr(vars, g, runtime, &ir))
-            .reduce(|a, b| match (a, b) {
-                (Value::Int(a), Value::Int(b)) => Value::Int(a % b),
-                _ => Value::Null,
-            })
-            .ok_or_else(|| "Modulo operator requires at least one argument".to_string()),
-        ExprIR::FuncInvocation(name) => {
-            let args = ir
-                .children()
-                .map(|ir| run_expr(vars, g, runtime, &ir))
-                .collect::<Result<Vec<_>, _>>()?;
-            if let Some(func) = runtime.write_functions.get(name) {
-                func(g, runtime, args)
-            } else if let Some(func) = runtime.read_functions.get(name) {
-                func(g, runtime, args)
-            } else {
-                Err(format!("Function {name} not found"))
-            }
-        }
-        ExprIR::Map => Ok(Value::Map(
-            ir.children()
-                .map(|child| {
-                    (
-                        if let ExprIR::Var(key) = child.data() {
-                            key.to_string()
-                        } else {
-                            todo!();
-                        },
-                        run_expr(vars, g, runtime, &child.child(0)).unwrap_or(Value::Null),
-                    )
-                })
-                .collect(),
-        )),
-        ExprIR::Set(x) => {
-            let v = run_expr(vars, g, runtime, &ir.child(0))?;
-            vars.insert(x.to_string(), v.clone());
-            Ok(v)
-        }
-    }
-}
-
-#[allow(clippy::too_many_lines)]
-pub fn run<CB: ReturnCallback>(
-    vars: &mut BTreeMap<String, Value>,
-    g: &mut Graph,
-    runtime: &mut Runtime,
-    callback: &CB,
-    ir: &DynNode<IR>,
-) -> Result<Value, String> {
-    match ir.data() {
-        IR::Expr(expr) => run_expr(vars, g, runtime, &expr.root()),
-        IR::If => match run(vars, g, runtime, callback, &ir.child(0))? {
-            Value::Bool(true) => run(vars, g, runtime, callback, &ir.child(1)),
-            _ => Ok(Value::Null),
-        },
-        IR::For => {
-            run(vars, g, runtime, callback, &ir.child(0))?;
-            while run(vars, g, runtime, callback, &ir.child(1))? == Value::Bool(true) {
-                run(vars, g, runtime, callback, &ir.child(3))?;
-                run(vars, g, runtime, callback, &ir.child(2))?;
-            }
-            Ok(Value::Null)
-        }
-        IR::Return => {
-            let v = run(vars, g, runtime, callback, &ir.child(0))?;
-            callback.return_value(g, v);
-            Ok(Value::Null)
-        }
-        IR::ReturnAggregation => {
-            run(vars, g, runtime, callback, &ir.child(0))?;
-            for (keys, r) in runtime.agg_ctxs.values_mut() {
-                if let Value::List(keys) = keys {
-                    keys.push(r.clone());
-                    callback.return_value(g, Value::List(keys.clone()));
-                } else {
-                    callback.return_value(g, Value::List(vec![r.clone()]));
-                }
-            }
-            runtime.agg_ctxs.clear();
-            Ok(Value::Null)
-        }
-        IR::Block => {
-            let mut v = Value::Null;
-            for ir in ir.children() {
-                v = run(vars, g, runtime, callback, &ir)?;
-            }
-            Ok(v)
-        }
-    }
-}
-
 #[must_use]
-pub fn evaluate_param(expr: DynNode<ExprIR>) -> Value {
+fn evaluate_param(expr: DynNode<ExprIR>) -> Value {
     match expr.data() {
         ExprIR::Null => Value::Null,
         ExprIR::Bool(x) => Value::Bool(*x),
diff --git a/graph/src/value.rs b/graph/src/value.rs
index ab6b1d6f..c72f9c2d 100644
--- a/graph/src/value.rs
+++ b/graph/src/value.rs
@@ -1,7 +1,7 @@
 use std::cmp::Ordering;
 use std::collections::BTreeMap;
 use std::hash::Hash;
-use std::ops::Add;
+use std::ops::{Add, Div, Mul, Rem, Sub};
 
 #[derive(Clone, Debug, PartialEq)]
 pub enum Value {
@@ -75,6 +75,142 @@ impl Add for Value {
     }
 }
 
+impl Sub for Value {
+    type Output = Result<Self, String>;
+
+    fn sub(
+        self,
+        rhs: Self,
+    ) -> Self::Output {
+        match (self, rhs) {
+            (Self::Null, _) | (_, Self::Null) => Ok(Self::Null),
+            (Self::Int(a), Self::Int(b)) => Ok(Self::Int(a.wrapping_sub(b))),
+            (Self::Float(a), Self::Float(b)) => Ok(Self::Float(a - b)),
+            (Self::Float(a), Self::Int(b)) => Ok(Self::Float(a - b as f64)),
+            (Self::Int(a), Self::Float(b)) => Ok(Self::Float(a as f64 - b)),
+            (a, b) => Err(format!(
+                "Unexpected types for sub operator ({}, {})",
+                a.name(),
+                b.name()
+            )),
+        }
+    }
+}
+
+impl Mul for Value {
+    type Output = Result<Self, String>;
+
+    fn mul(
+        self,
+        rhs: Self,
+    ) -> Self::Output {
+        match (self, rhs) {
+            (Self::Null, _) | (_, Self::Null) => Ok(Self::Null),
+            (Self::Int(a), Self::Int(b)) => Ok(Self::Int(a.wrapping_mul(b))),
+            (Self::Float(a), Self::Float(b)) => Ok(Self::Float(a * b)),
+            (Self::Float(a), Self::Int(b)) => Ok(Self::Float(a * b as f64)),
+            (Self::Int(a), Self::Float(b)) => Ok(Self::Float(a as f64 * b)),
+            (a, b) => Err(format!(
+                "Unexpected types for mul operator ({}, {})",
+                a.name(),
+                b.name()
+            )),
+        }
+    }
+}
+
+impl Div for Value {
+    type Output = Result<Self, String>;
+
+    fn div(
+        self,
+        rhs: Self,
+    ) -> Self::Output {
+        match (self, rhs) {
+            (Self::Null, _) | (_, Self::Null) => Ok(Self::Null),
+            (Self::Int(a), Self::Int(b)) => {
+                if b == 0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Int(a.wrapping_div(b)))
+                }
+            }
+            (Self::Float(a), Self::Float(b)) => {
+                if b == 0.0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Float(a / b))
+                }
+            }
+            (Self::Float(a), Self::Int(b)) => {
+                if b == 0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Float(a / b as f64))
+                }
+            }
+            (Self::Int(a), Self::Float(b)) => {
+                if b == 0.0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Float(a as f64 / b))
+                }
+            }
+            (a, b) => Err(format!(
+                "Unexpected types for div operator ({}, {})",
+                a.name(),
+                b.name()
+            )),
+        }
+    }
+}
+
+impl Rem for Value {
+    type Output = Result<Self, String>;
+
+    fn rem(
+        self,
+        rhs: Self,
+    ) -> Self::Output {
+        match (self, rhs) {
+            (Self::Null, _) | (_, Self::Null) => Ok(Self::Null),
+            (Self::Int(a), Self::Int(b)) => {
+                if b == 0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Int(a.wrapping_rem(b)))
+                }
+            }
+            (Self::Float(a), Self::Float(b)) => {
+                if b == 0.0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Float(a % b))
+                }
+            }
+            (Self::Float(a), Self::Int(b)) => {
+                if b == 0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Float(a % b as f64))
+                }
+            }
+            (Self::Int(a), Self::Float(b)) => {
+                if b == 0.0 {
+                    Err("Division by zero".to_string())
+                } else {
+                    Ok(Self::Float(a as f64 % b))
+                }
+            }
+            (a, b) => Err(format!(
+                "Unexpected types for rem operator ({}, {})",
+                a.name(),
+                b.name()
+            )),
+        }
+    }
+}
+
 trait OrderedEnum {
     fn order(&self) -> u32;
 }
diff --git a/src/lib.rs b/src/lib.rs
index b7937aa6..8e16242e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,7 +1,5 @@
-use graph::{
-    cypher::Parser, graph::Graph, graph::ReturnCallback, matrix::init, planner::Planner,
-    value::Value,
-};
+use graph::runtime::{ReturnCallback, Runtime};
+use graph::{cypher::Parser, graph::Graph, matrix::init, planner::Planner, value::Value};
 use redis_module::{
     Context, NextArg, REDISMODULE_TYPE_METHOD_VERSION, RedisError, RedisModule_Alloc,
     RedisModule_Calloc, RedisModule_Free, RedisModule_Realloc, RedisModuleTypeMethods, RedisResult,
@@ -48,12 +46,12 @@ static GRAPH_TYPE: RedisType = RedisType::new(
 #[unsafe(no_mangle)]
 unsafe extern "C" fn my_free(value: *mut c_void) {
     unsafe {
-        drop(Box::from_raw(value.cast::<Graph>()));
+        drop(Box::from_raw(value.cast::<RefCell<Graph>>()));
     }
 }
 
 fn raw_value_to_redis_value(
-    g: &Graph,
+    g: &RefCell<Graph>,
     r: &Value,
 ) -> RedisValue {
     match r {
@@ -68,7 +66,7 @@ fn raw_value_to_redis_value(
 }
 
 fn inner_raw_value_to_redis_value(
-    g: &Graph,
+    g: &RefCell<Graph>,
     r: &Value,
 ) -> RedisValue {
     match r {
@@ -110,7 +108,7 @@ fn inner_raw_value_to_redis_value(
         ]),
         Value::Node(id) => {
             let mut props = Vec::new();
-            for (key, value) in g.get_node_properties(*id) {
+            for (key, value) in g.borrow().get_node_properties(*id) {
                 let mut prop = Vec::new();
                 prop.push(RedisValue::Integer(*key as _));
                 if let RedisValue::Array(mut v) = inner_raw_value_to_redis_value(g, value) {
@@ -123,7 +121,8 @@ fn inner_raw_value_to_redis_value(
                 RedisValue::Array(vec![
                     RedisValue::Integer(*id as _),
                     RedisValue::Array(
-                        g.get_node_label_ids(*id)
+                        g.borrow()
+                            .get_node_label_ids(*id)
                             .map(|l| RedisValue::Integer(l as _))
                             .collect(),
                     ),
@@ -133,7 +132,7 @@ fn inner_raw_value_to_redis_value(
         }
         Value::Relationship(id, from, to) => {
             let mut props = Vec::new();
-            for (key, value) in g.get_relationship_properties(*id) {
+            for (key, value) in g.borrow().get_relationship_properties(*id) {
                 let mut prop = Vec::new();
                 prop.push(RedisValue::Integer(*key as _));
                 if let RedisValue::Array(mut v) = inner_raw_value_to_redis_value(g, value) {
@@ -145,7 +144,7 @@ fn inner_raw_value_to_redis_value(
                 RedisValue::Integer(7),
                 RedisValue::Array(vec![
                     RedisValue::Integer(*id as _),
-                    RedisValue::Integer(g.get_relationship_type_id(*id) as _),
+                    RedisValue::Integer(g.borrow().get_relationship_type_id(*id) as _),
                     RedisValue::Integer(*from as _),
                     RedisValue::Integer(*to as _),
                     RedisValue::Array(props),
@@ -174,7 +173,7 @@ impl RedisValuesCollector {
 impl ReturnCallback for RedisValuesCollector {
     fn return_value(
         &self,
-        graph: &Graph,
+        graph: &RefCell<Graph>,
         value: Value,
     ) {
         self.res
@@ -204,7 +203,7 @@ fn graph_delete(
     let mut args = args.into_iter().skip(1);
     let key = args.next_arg()?;
     let key = ctx.open_key_writable(&key);
-    if key.get_value::<Graph>(&GRAPH_TYPE)?.is_some() {
+    if key.get_value::<RefCell<Graph>>(&GRAPH_TYPE)?.is_some() {
         key.delete()
     } else {
         EMPTY_KEY_ERR
@@ -213,13 +212,15 @@ fn graph_delete(
 
 #[inline]
 fn query_mut(
-    graph: &mut Graph,
-    debug: u64,
+    graph: &RefCell<Graph>,
     query: &str,
 ) -> Result<RedisValue, RedisError> {
     let collector = RedisValuesCollector::new();
-    graph
-        .query(query, &collector, debug > 0)
+    let (plan, parameters, parse_duration, plan_duration) =
+        graph.borrow().get_plan(query).map_err(RedisError::String)?;
+    let mut runtime = Runtime::new(graph, parameters, true);
+    runtime
+        .query(plan, &collector)
         .map(|summary| {
             vec![
                 vec![
@@ -257,15 +258,14 @@ fn graph_query(
     let mut args = args.into_iter().skip(1);
     let key = args.next_arg()?;
     let query = args.next_str()?;
-    let debug = args.next_u64().unwrap_or(0);
 
     let key = ctx.open_key_writable(&key);
 
-    if let Some(graph) = key.get_value::<Graph>(&GRAPH_TYPE)? {
-        query_mut(graph, debug, query)
+    if let Some(graph) = key.get_value::<RefCell<Graph>>(&GRAPH_TYPE)? {
+        query_mut(graph, query)
     } else {
-        let mut value = Graph::new(16384, 16384);
-        let res = query_mut(&mut value, debug, query);
+        let mut value = RefCell::new(Graph::new(16384, 16384));
+        let res = query_mut(&mut value, query);
         key.set_value(&GRAPH_TYPE, value)?;
         res
     }
@@ -287,17 +287,19 @@ fn graph_ro_query(
     let mut args = args.into_iter().skip(1);
     let key = args.next_arg()?;
     let query = args.next_str()?;
-    let debug = args.next_u64().unwrap_or(0);
 
     let key = ctx.open_key(&key);
 
     // We check if the key exists and is of type Graph if wrong type `get_value` return an error
-    (key.get_value::<Graph>(&GRAPH_TYPE)?).map_or(
+    (key.get_value::<RefCell<Graph>>(&GRAPH_TYPE)?).map_or(
         // If the key does not exist, we return an error
         EMPTY_KEY_ERR,
         |graph| {
             let collector = RedisValuesCollector::new();
-            match graph.ro_query(query, &collector, debug > 0) {
+            let (plan, parameters, parse_duration, plan_duration) =
+                graph.borrow().get_plan(query).map_err(RedisError::String)?;
+            let mut runtime = Runtime::new(graph, parameters, false);
+            match runtime.query(plan, &collector) {
                 Ok(_) => Ok(vec![
                     vec![
                         vec![
@@ -394,7 +396,7 @@ fn graph_plan(
     match parser.parse() {
         Ok(ir) => {
             let mut planner = Planner::new();
-            let ir = planner.plan(ir, false);
+            let ir = planner.plan(ir);
             Ok(RedisValue::BulkString(format!("{ir}")))
         }
         Err(err) => Err(RedisError::String(err)),
diff --git a/tests/test_e2e.py b/tests/test_e2e.py
index d4f43ec4..d9d03afa 100644
--- a/tests/test_e2e.py
+++ b/tests/test_e2e.py
@@ -53,19 +53,26 @@ def setup_function(function):
         g.delete()
 
 
-def query(query: str, params=None, write: bool = False):
+def query(query: str, params=None, write: bool = False, compare_results: bool = True):
     if write:
+        try:
+            g.query("RETURN 1")
+            read_res = g.ro_query(query, params)
+            assert False
+        except ResponseError as e:
+            assert "graph.RO_QUERY is to be executed only on read-only queries" == str(e)
         return g.query(query, params)
     else:
         write_res = g.query(query, params)
         read_res = g.ro_query(query, params)
-        assert len(write_res.result_set) == len(read_res.result_set)
-        for i in range(len(write_res.result_set)):
-            assert len(write_res.result_set[i]) == len(read_res.result_set[i])
-            for j in range(len(write_res.result_set[i])):
-                assert (write_res.result_set[i][j] == read_res.result_set[i][j]
-                        or (math.isnan(write_res.result_set[i][j])
-                            and math.isnan(read_res.result_set[i][j])))
+        if compare_results:
+            assert len(write_res.result_set) == len(read_res.result_set)
+            for i in range(len(write_res.result_set)):
+                assert len(write_res.result_set[i]) == len(read_res.result_set[i])
+                for j in range(len(write_res.result_set[i])):
+                    assert (write_res.result_set[i][j] == read_res.result_set[i][j]
+                            or (math.isnan(write_res.result_set[i][j])
+                                and math.isnan(read_res.result_set[i][j])))
         return write_res
 
 
@@ -151,29 +158,44 @@ def test_parameters():
         res = query("RETURN $p", params={"p": value})
         assert res.result_set == [[value]]
 
+class CustomNumber:
+    def __init__(self, value):
+        # if isinstance(value, float) and len(f"{value}") >= 15:
+        #     self.value = float(f"{value:.15g}")
+        # else:
+        self.value = value
+    
+    def __add__(self, other):
+        return CustomNumber(self.value + other.value)
+    
+    def __sub__(self, other):
+        return CustomNumber(self.value - other.value)
+    
+    def __mul__(self, other):
+        return CustomNumber(self.value * other.value)
+
+    def __truediv__(self, other):
+        if isinstance(self.value, int) and isinstance(other.value, int):
+            return CustomNumber(self.value // other.value)
+        return CustomNumber(self.value / other.value)
+    
+    def __mod__(self, other):
+        return CustomNumber(self.value % other.value)
 
 def test_operators():
-    for a in [True, False]:
-        for b in [True, False]:
-            res = query(f"RETURN {a} AND {b}")
-            assert res.result_set == [[1 if a and b else 0]]
-
-    for a in [True, False]:
-        for b in [True, False]:
-            for c in [True, False]:
-                res = query(f"RETURN {a} AND {b} AND {c}")
-                assert res.result_set == [[1 if a and b and c else 0]]
-
-    for a in [True, False]:
-        for b in [True, False]:
-            res = query(f"RETURN {a} OR {b}")
-            assert res.result_set == [[1 if a or b else 0]]
-
-    for a in [True, False]:
-        for b in [True, False]:
-            for c in [True, False]:
-                res = query(f"RETURN {a} OR {b} OR {c}")
-                assert res.result_set == [[1 if a or b or c else 0]]
+    for op in ["and", "or"]:
+        for a in [True, False]:
+            for b in [True, False]:
+                res = query(f"RETURN {a} {op} {b}")
+                assert res.result_set == [[1 if eval(f"{a} {op} {b}") else 0]]
+
+    for op1 in ["and", "or"]:
+        for op2 in ["and", "or"]:
+            for a in [True, False]:
+                for b in [True, False]:
+                    for c in [True, False]:
+                        res = query(f"RETURN {a} {op1} {b} {op2} {c}")
+                        assert res.result_set == [[1 if eval(f"{a} {op1} {b} {op2} {c}") else 0]]
 
     for a in [True, False]:
         for b in [True, False]:
@@ -204,22 +226,22 @@ def test_operators():
             res = query(f"RETURN {a} + {b} * ({a} + {b})")
             assert res.result_set == [[a + b * (a + b)]]
 
-    for op1 in ['+', '-', '*', '/']:
-        for op2 in ['+', '-', '*', '/']:
-            for op3 in ['+', '-', '*', '/']:
-                for op4 in ['+', '-', '*', '/']:
-                    res = query(f"RETURN 1 {op1} 2 {op2} 3 {op3} 4 {op4} 5")
-                    pyop1 = op1.replace("/", "//")
-                    pyop2 = op2.replace("/", "//")
-                    pyop3 = op3.replace("/", "//")
-                    pyop4 = op4.replace("/", "//")
-                    assert res.result_set == [[eval(f"1 {pyop1} 2 {pyop2} 3 {pyop3} 4 {pyop4} 5")]]
+    for op1 in ['+', '-', '*', '/', '%']:
+        for op2 in ['+', '-', '*', '/', '%']:
+            for op3 in ['+', '-', '*', '/', '%']:
+                for op4 in ['+', '-', '*', '/', '%']:
+                    for n1 in [2, 2.0]:
+                        for n2 in [4, 4.0]:
+                            for n3 in [8, 8.0]:
+                                for n4 in [16, 16.0]:
+                                    for n5 in [32, 32.0]:
+                                        res = query(f"RETURN {n1} {op1} {n2} {op2} {n3} {op3} {n4} {op4} {n5}")
+                                        assert res.result_set == [[eval(f"CustomNumber({n1}) {op1} CustomNumber({n2}) {op2} CustomNumber({n3}) {op3} CustomNumber({n4}) {op4} CustomNumber({n5})").value]]
 
     for i, a in enumerate([True, 1, 'Avi', [1]]):
         res = query(f"RETURN {{a0: true, a1: 1, a2: 'Avi', a3: [1]}}.a{i}")
         assert res.result_set == [[a]]
 
-    for i, a in enumerate([True, 1, 'Avi', [1]]):
         res = query(f"RETURN {{a: {{a0: true, a1: 1, a2: 'Avi', a3: [1]}}}}.a.a{i}")
         assert res.result_set == [[a]]
 
@@ -227,16 +249,14 @@ def test_operators():
         res = query(f"RETURN [][{a}]")
         assert res.result_set == [[None]]
 
-    for a in range(5):
         res = query(f"RETURN [0, 1, 2, 3, 4][{a}]")
         assert res.result_set == [[[0, 1, 2, 3, 4][a]]]
 
-    for a in range(5):
         res = query(f"RETURN [[0, 1, 2, 3, 4]][0][{a}]")
         assert res.result_set == [[[0, 1, 2, 3, 4][a]]]
 
-    res = query(f"UNWIND [NULL, true, false, 1, 'Avi'] AS x RETURN x IS NULL")
-    assert res.result_set == [[True], [False], [False], [False], [False]]
+    res = query(f"UNWIND [NULL, true, false, 1, 'Avi', [], {{}}] AS x RETURN x IS NULL")
+    assert res.result_set == [[True], [False], [False], [False], [False], [False], [False]]
 
 
 def test_unwind():
@@ -324,6 +344,24 @@ def test_graph_crud():
     # assert res.relationships_deleted == 3
 
 
+def test_node_labels():
+    res = query("CREATE ()", write=True)
+    assert res.result_set == []
+    assert res.nodes_created == 1
+
+    res = query("MATCH (n) RETURN labels(n)")
+    assert res.result_set == [[[]]]
+
+    res = query("MATCH (n) DELETE n", write=True)
+    assert res.nodes_deleted == 1
+
+    res = query("CREATE (:N:M)", write=True)
+    assert res.result_set == []
+    assert res.nodes_created == 1
+
+    res = query("MATCH (n) RETURN labels(n)")
+    assert res.result_set == [[["N", "M"]]]
+
 def test_large_graph():
     query("UNWIND range(0, 100000) AS x CREATE (n:N {v: x})-[r:R {v: x}]->(m:M {v: x})", write=True)
 
@@ -1594,7 +1632,7 @@ def test_pow():
 
 
 def test_rand():
-    res = query("RETURN rand() AS name", write=True)
+    res = query("RETURN rand() AS name", compare_results=False)
     assert res.result_set[0][0] >= 0.0
     assert res.result_set[0][0] < 1.0
 
@@ -1715,6 +1753,19 @@ def test_sqrt():
         assert "Received 2 arguments to function 'sqrt', expected at most 1" in str(e)
 
 
+def test_range():
+    res = query("RETURN range(1, 10) AS name")
+    assert res.result_set == [[list(range(1, 11))]]
+
+    res = query("RETURN range(10, 1) AS name")
+    assert res.result_set == [[[]]]
+
+    res = query("RETURN range(1, 10, 2) AS name")
+    assert res.result_set == [[list(range(1, 11, 2))]]
+
+    res = query("RETURN range(10, 1, -2) AS name")
+    assert res.result_set == [[list(range(10, 0, -2))]]
+
 def test_aggregation():
     res = query("UNWIND range(1, 10) AS x RETURN collect(x)")
     assert_result_set_equal_no_order(res, [[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]])


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🔭 Outside diff range comments (2)</summary><blockquote>

<details>
<summary>graph/src/runtime.rs (2)</summary><blockquote>

`1652-1660`: _⚠️ Potential issue_

**Incorrect random API usage – `rand::rng()` does not exist**

`rand`’s current public API exposes `thread_rng()` with the `gen_range` method:

```diff
-let mut rng = rand::rng();
-Ok(Value::Float(rng.random_range(0.0..1.0)))
+let mut rng = rand::thread_rng();
+Ok(Value::Float(rng.gen_range(0.0..1.0)))
```

Import `rand::Rng` (already present) to make `gen_range` available.  
This fixes a compile-time error.

---

`1877-1904`: _🛠️ Refactor suggestion_

**`evaluate_param` panics on unsupported expressions**

The fallback arm `todo!()` will panic at runtime if a user supplies parameters such as arithmetic expressions or nested function calls.  
Either:

1. Restrict parameters syntactically during parsing, or  
2. Return `Result<Value, String>` from `evaluate_param` to surface an informative error instead of crashing.

```diff
-fn evaluate_param(expr: DynNode<ExprIR>) -> Value {
+fn evaluate_param(expr: DynNode<ExprIR>) -> Result<Value, String> {
     match expr.data() {
         ExprIR::Null => Ok(Value::Null),
         ...
-        _ => todo!(),
+        _ => Err(format!("Unsupported parameter expression: {:?}", expr.data())),
     }
 }
```

Callers will need a small `map(|(k,v)| Ok((k, evaluate_param(v.root())?)))` adjustment.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>graph/src/graph.rs (1)</summary><blockquote>

`165-172`: **Runtime re-evaluation of parameters on every cached hit costs O(query_size)**

Even when the plan is found in the cache we still call `Parser::parse_parameters` and later `Runtime::new(parameters)`, re-evaluating the parameter expression trees on every invocation.  
If the query is executed frequently with different parameter values it’s better to cache **only the AST/plan while re-parsing the parameter slice**, but if parameters are **constant** (Cypher’s `$name` syntax), consider extending the cache entry to also include the compiled parameter trees to avoid the extra parse step.

No immediate bug, just a performance note.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b3447f824c740a7958a0ba4c77a058d923661504 and b402730061fc02952da0605ac5b4c0e34bb5cc70.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `graph/src/graph.rs` (3 hunks)
* `graph/src/planner.rs` (0 hunks)
* `graph/src/runtime.rs` (6 hunks)
* `src/lib.rs` (4 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* graph/src/planner.rs

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>graph/src/graph.rs (1)</summary>

`138-157`: **Potential dead-lock & missing error-handling in `get_plan` cache access**

`Mutex::lock` may return a poisoned error if another thread panicked while holding the lock.  
The current implementation collapses every such case into the generic `"Failed to acquire read lock on cache"` string, losing the original cause.  In addition, any panic that happens between acquiring the lock and returning the cloned plan will leave the mutex poisoned, degrading the whole server.

```diff
-            Err(_) => Err("Failed to acquire read lock on cache".to_string()),
+            Err(poison) => {
+                // Preserve the original panic information for post-mortem analysis
+                Err(format!("Cache mutex poisoned – {}", poison))
+            }
```

Consider switching to an `RwLock` so read-only paths don’t block each other, and wrap the lock in a small helper that logs / converts the poisoning information.

</details>
<details>
<summary>src/lib.rs (1)</summary>

`221-222`: **Interface update looks good**

The removal of the obsolete `debug` flag simplifies the call.  
No functional issues spotted in the modified lines.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:24:42Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>graph/src/runtime.rs (1)</summary><blockquote>

`1684-1688`: _⚠️ Potential issue_

**Incorrect usage of `rand` API**

`rand::rng()` and `random_range` do not exist in the `rand` crate.

```diff
-let mut rng = rand::rng();
-Ok(Value::Float(rng.random_range(0.0..1.0)))
+let mut rng = rand::thread_rng();
+Ok(Value::Float(rng.gen_range(0.0..1.0)))
```

Also ensure the `Rng` trait is in scope (`use rand::Rng;` is already present).

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>graph/src/runtime.rs (1)</summary><blockquote>

`358-378`: **`flat_map` still causes a compile-time error – same issue as previously reported**

`Iterator::flat_map` expects the closure to return an *iterator*, but `self.ro_run_expr(&ir)` / `self.run_expr(&ir)` return `Result<Value, String>`.  
These blocks will not compile:

```rust
.children()
.flat_map(|ir| self.ro_run_expr(&ir))
```

Replace the pattern with `map` + `try_fold` (or `map` + `collect::<Result<Vec<_>, _>>()?`) as suggested in the earlier review.  
This applies to `Sub`, `Mul`, `Div`, `Pow`, `Modulo` (both read-only and read-write versions).



Also applies to: 642-656

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>graph/src/graph.rs (1)</summary><blockquote>

`115-126`: **`parse_duration` / `plan_duration` are always 0 for cached plans**

When a cached plan is returned the two duration fields are still `Duration::ZERO`, which may be misleading for observability.  
Consider documenting this behaviour or returning `None`/`Option<Duration>` so callers can distinguish “cached” from “measured”.

</blockquote></details>
<details>
<summary>graph/src/runtime.rs (1)</summary><blockquote>

`947-963`: **Multiple immutable borrows – consider consolidating**

`property()` calls `self.g.borrow()` three times, creating three independent immutable borrows.  
While valid, it is cheaper and clearer to borrow once and reuse the reference.

```diff
-            (Some(Value::Node(node_id)), Some(Value::String(property)), None) => runtime
-                .g
-                .borrow()
-                .get_node_property_id(&property)
+            (Some(Value::Node(node_id)), Some(Value::String(property)), None) => {
+                let g = runtime.g.borrow();
+                g.get_node_property_id(&property)
                 .map_or(Ok(Value::Null), |property_id| {
-                    runtime
-                        .g
-                        .borrow()
-                        .get_node_property(node_id, property_id)
+                    g.get_node_property(node_id, property_id)
                         .map_or(Ok(Value::Null), Ok)
                 })
+            }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b402730061fc02952da0605ac5b4c0e34bb5cc70 and 05db34e41e4f0230056bdaccd26ded0349f9032a.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `graph/src/graph.rs` (3 hunks)
* `graph/src/runtime.rs` (11 hunks)
* `src/lib.rs` (13 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/lib.rs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test
* GitHub Check: coverage

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>graph/src/runtime.rs (1)</summary>

`904-915`: **Bounds-check iterator index to avoid panic**

`next_node` / `next_relationship` index directly into `self.node_iters[...]` / `self.relationship_iters[...]` without checking that the index is valid.  
User input can crash the runtime with `index out of bounds`. Add a range check and return an error (`Err(String)`) when the index is invalid.



Also applies to: 938-945

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T21:18:57Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (6)</summary><blockquote>

<details>
<summary>graph/src/runtime.rs (6)</summary><blockquote>

`341-348`: _⚠️ Potential issue_

**Another invalid use of flat_map in Mul operator**

Similar to the Sub operator, the Mul operator incorrectly uses `flat_map` with a non-iterable Result return.


Apply the same fix pattern:

```diff
-ExprIR::Mul => ir
-    .children()
-    .flat_map(|ir| self.run_expr(&ir))
-    .reduce(|a, b| match (a, b) {
-        (Value::Int(a), Value::Int(b)) => Value::Int(a * b),
-        _ => Value::Null,
-    })
-    .ok_or_else(|| "Mul operator requires at least one argument".to_string()),
+ExprIR::Mul => ir
+    .children()
+    .map(|node| self.run_expr(&node))
+    .try_fold(None, |acc, v| {
+        let v = v?;
+        Ok::<_, String>(Some(match (acc, v) {
+            (None, cur)                     => cur,
+            (Some(Value::Int(a)), Value::Int(b)) => Value::Int(a * b),
+            _ => return Err("Mul operator requires integers".into()),
+        }))
+    })?
+    .ok_or_else(|| "Mul operator requires at least one argument".into()),
```

---

`332-340`: _⚠️ Potential issue_

**Invalid use of flat_map with Result return value**

The `flat_map` operator expects the closure to return an iterator, but `self.run_expr()` returns a `Result<Value, String>` which isn't iterable. This will cause a compilation error.


Replace `flat_map` with `map` and use `try_fold` to handle the `Result` values:

```diff
-ExprIR::Sub => ir
-    .children()
-    .flat_map(|ir| self.run_expr(&ir))
-    .reduce(|a, b| match (a, b) {
-        (Value::Int(a), Value::Int(b)) => Value::Int(a - b),
-        _ => Value::Null,
-    })
-    .ok_or_else(|| "Sub operator requires at least one argument".to_string()),
+ExprIR::Sub => ir
+    .children()
+    .map(|node| self.run_expr(&node))
+    .try_fold(None, |acc, v| {
+        let v = v?;
+        Ok::<_, String>(Some(match (acc, v) {
+            (None, cur)                     => cur,
+            (Some(Value::Int(a)), Value::Int(b)) => Value::Int(a - b),
+            _ => return Err("Sub operator requires integers".into()),
+        }))
+    })?
+    .ok_or_else(|| "Sub operator requires at least one argument".into()),
```

---

`359-366`: _⚠️ Potential issue_

**Pow operator needs flat_map fix**

The Pow operator has the same issue with `flat_map`.


Apply the same fix pattern:

```diff
-ExprIR::Pow => ir
-    .children()
-    .flat_map(|ir| self.run_expr(&ir))
-    .reduce(|a, b| match (a, b) {
-        (Value::Int(a), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
-        _ => Value::Null,
-    })
-    .ok_or_else(|| "Pow operator requires at least one argument".to_string()),
+ExprIR::Pow => ir
+    .children()
+    .map(|node| self.run_expr(&node))
+    .try_fold(None, |acc, v| {
+        let v = v?;
+        Ok::<_, String>(Some(match (acc, v) {
+            (None, cur)                     => cur,
+            (Some(Value::Int(a)), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
+            _ => return Err("Pow operator requires integer values".into()),
+        }))
+    })?
+    .ok_or_else(|| "Pow operator requires at least one argument".into()),
```

---

`166-169`: _🛠️ Refactor suggestion_

**Risk of integer underflow in labels_added calculation**

The current implementation uses unsigned subtraction to calculate `labels_added`, which could cause overflow if labels were removed during the query.


Use `saturating_sub` to prevent potential overflow:

```diff
-let labels_added = self.g.borrow().get_labels_count() - labels_count;
+let labels_added = self
+    .g
+    .borrow()
+    .get_labels_count()
+    .saturating_sub(labels_count);
```

---

`349-358`: _⚠️ Potential issue_

**Div operator also has flat_map issue**

The Div operator has the same issue with `flat_map`.


Apply the same fix pattern:

```diff
-ExprIR::Div => ir
-    .children()
-    .flat_map(|ir| self.run_expr(&ir))
-    .reduce(|a, b| match (a, b) {
-        (Value::Int(a), Value::Int(b)) => Value::Int(a / b),
-        (Value::Int(a), Value::Float(b)) => Value::Float(a as f64 / b),
-        (Value::Float(a), Value::Int(b)) => Value::Float(a / b as f64),
-        (Value::Float(a), Value::Float(b)) => Value::Float(a / b),
-        _ => Value::Null,
-    })
-    .ok_or_else(|| "Div operator requires at least one argument".to_string()),
+ExprIR::Div => ir
+    .children()
+    .map(|node| self.run_expr(&node))
+    .try_fold(None, |acc, v| {
+        let v = v?;
+        Ok::<_, String>(Some(match (acc, v) {
+            (None, cur)                     => cur,
+            (Some(Value::Int(a)), Value::Int(b)) => Value::Int(a / b),
+            (Some(Value::Int(a)), Value::Float(b)) => Value::Float(a as f64 / b),
+            (Some(Value::Float(a)), Value::Int(b)) => Value::Float(a / b as f64),
+            (Some(Value::Float(a)), Value::Float(b)) => Value::Float(a / b),
+            _ => return Err("Div operator requires numeric values".into()),
+        }))
+    })?
+    .ok_or_else(|| "Div operator requires at least one argument".into()),
```

---

`367-374`: _⚠️ Potential issue_

**Final flat_map issue in Modulo operator**

The Modulo operator has the same issue with `flat_map`.


Apply the same fix pattern:

```diff
-ExprIR::Modulo => ir
-    .children()
-    .flat_map(|ir| self.run_expr(&ir))
-    .reduce(|a, b| match (a, b) {
-        (Value::Int(a), Value::Int(b)) => Value::Int(a % b),
-        _ => Value::Null,
-    })
-    .ok_or_else(|| "Modulo operator requires at least one argument".to_string()),
+ExprIR::Modulo => ir
+    .children()
+    .map(|node| self.run_expr(&node))
+    .try_fold(None, |acc, v| {
+        let v = v?;
+        Ok::<_, String>(Some(match (acc, v) {
+            (None, cur)                     => cur,
+            (Some(Value::Int(a)), Value::Int(b)) => Value::Int(a % b),
+            _ => return Err("Modulo operator requires integer values".into()),
+        }))
+    })?
+    .ok_or_else(|| "Modulo operator requires at least one argument".into()),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>tests/test_e2e.py (1)</summary><blockquote>

`68-75`: **Improved NaN comparison for result validation**

The code correctly handles the case where both values might be NaN (which would normally compare as not equal), but the implementation is a bit verbose.


Consider using a dedicated helper function for comparing values that handles NaN cases:

```diff
+def values_equal(val1, val2):
+    """Compare two values, properly handling NaN cases"""
+    return (val1 == val2) or (math.isnan(val1) and math.isnan(val2))
+
 def query(query: str, params=None, write: bool = False, compare_results: bool = True):
     # ... existing code ...
     if compare_results:
         assert len(write_res.result_set) == len(read_res.result_set)
         for i in range(len(write_res.result_set)):
             assert len(write_res.result_set[i]) == len(read_res.result_set[i])
             for j in range(len(write_res.result_set[i])):
-                assert (write_res.result_set[i][j] == read_res.result_set[i][j]
-                        or (math.isnan(write_res.result_set[i][j])
-                            and math.isnan(read_res.result_set[i][j])))
+                assert values_equal(write_res.result_set[i][j], read_res.result_set[i][j])
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 05db34e41e4f0230056bdaccd26ded0349f9032a and d799fceafb5932a4f54a54a3aec6027c3d096a27.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `graph/src/runtime.rs` (11 hunks)
* `src/lib.rs` (13 hunks)
* `tests/test_e2e.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/test_e2e.py</summary>

61-61: Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`

Replace `assert False`

(B011)

---

63-63: Yoda condition detected

(SIM300)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test
* GitHub Check: coverage

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>tests/test_e2e.py (1)</summary>

`1604-1604`: **LGTM: Test correctly handles non-deterministic rand() function**

The rand() function produces different results each time it's called, so it makes sense to skip result comparison between write and read-only queries by setting `compare_results=False`.

</details>
<details>
<summary>src/lib.rs (9)</summary>

`1-2`: **Well-structured imports for new runtime functionality**

The imports are properly organized to include the new runtime module and its components.

---

`49-50`: **Memory management updates for RefCell**

The free function has been updated to properly handle the new `RefCell<Graph>` type.

---

`54-55`: **Signature update for RefCell-based graph access**

The functions have been properly updated to accept `&RefCell<Graph>` instead of direct `&Graph` references.




Also applies to: 69-70

---

`111-111`: **Accessing graph properties through RefCell**

The code correctly uses `borrow()` to access graph properties, maintaining the new borrowing pattern.




Also applies to: 124-126, 135-135, 147-148

---

`173-182`: **ReturnCallback trait updated for RefCell**

The trait implementation has been updated to work with the new `&RefCell<Graph>` pattern.

---

`215-224`: **Refactored query execution to use Runtime**

The query execution has been redesigned to use the new `Runtime` struct for handling query execution. This separates query planning from execution, which is a good separation of concerns.

---

`264-270`: **Updated graph creation and query with RefCell**

The code now wraps the graph in a RefCell and correctly passes it to query_mut.

---

`293-318`: **Read-only query execution updated**

The read-only query handler has been updated to use the new Runtime-based execution model with RefCell. It now checks if a query is read-only and handles execution accordingly.

---

`399-400`: **Simplified planner interface**

The debug flag has been removed from the planner interface, simplifying the API.

</details>
<details>
<summary>graph/src/runtime.rs (4)</summary>

`11-31`: **Well-designed types for runtime execution**

The `RuntimeFn` type and `ReturnCallback` trait create a clear interface for executing functions and handling return values. The `ResultSummary` struct provides a comprehensive way to track execution statistics.

---

`33-154`: **Well-structured Runtime implementation**

The Runtime struct maintains state for query execution and properly initializes all available functions. The parameter evaluation during initialization is a nice optimization.

---

`375-391`: **Well-implemented function invocation handling**

The function invocation handling correctly distinguishes between read and write operations, and enforces the constraint that read-only queries cannot execute write functions.

---

`1627-1650`: **Robust array range handling**

The `get_elements` function correctly handles array ranges, including negative indices.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:01:52Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>tests/test_e2e.py (1)</summary><blockquote>

`58-63`: **Refactor assertion logic to be more readable and robust.**

The current code attempts to verify that read-only queries reject write operations, but has two issues:

1. It uses an unrelated query `"RETURN 1"` before testing the actual query
2. The assertion pattern `assert False` followed by `except` is flagged by static analysis tools as problematic


```diff
-        try:
-            g.query("RETURN 1")
-            read_res = g.ro_query(query, params)
-            assert False
-        except ResponseError as e:
-            assert "graph.RO_QUERY is to be executed only on read-only queries" == str(e)
+        try:
+            read_res = g.ro_query(query, params)
+            raise AssertionError("Expected ro_query to fail for write operations")
+        except ResponseError as e:
+            assert str(e) == "graph.RO_QUERY is to be executed only on read-only queries"
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

61-61: Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`

Replace `assert False`

(B011)

---

63-63: Yoda condition detected

(SIM300)

</details>

</details>

</blockquote></details>
<details>
<summary>graph/src/runtime.rs (1)</summary><blockquote>

`156-177`: **Under-/overflow risk when computing `labels_added`**

`labels_added` is calculated with unsigned subtraction:

```rust
let labels_added = self.g.borrow().get_labels_count() - labels_count;
```

If labels were *removed* during the query this will wrap around and panic in debug builds.  
Use `saturating_sub` (or switch to a signed type) to make the computation safe:


```diff
-let labels_added = self.g.borrow().get_labels_count() - labels_count;
+let labels_added = self
+    .g
+    .borrow()
+    .get_labels_count()
+    .saturating_sub(labels_count);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>tests/test_e2e.py (1)</summary><blockquote>

`258-258`: **Remove unnecessary f-string prefix.**

The string doesn't contain any placeholders, so the f-string prefix isn't needed.


```diff
-    res = query(f"UNWIND [NULL, true, false, 1, 'Avi', [], {{}}] AS x RETURN x IS NULL")
+    res = query("UNWIND [NULL, true, false, 1, 'Avi', [], {}] AS x RETURN x IS NULL")
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

258-258: f-string without any placeholders

Remove extraneous `f` prefix

(F541)

</details>

</details>

</blockquote></details>
<details>
<summary>graph/src/runtime.rs (1)</summary><blockquote>

`179-397`: **Comprehensive implementation of run_expr for expression evaluation.**

The run_expr method handles all expression types thoroughly with appropriate error handling. While quite long, it's organized logically by expression type.


Consider breaking this long method into smaller, more focused methods for each major expression type category to improve maintainability. For example, you could have separate methods for handling logical operations, arithmetic operations, etc.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d799fceafb5932a4f54a54a3aec6027c3d096a27 and 76721553215965b679424ad8a54ec229c8a8d366.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `graph/src/runtime.rs` (12 hunks)
* `graph/src/value.rs` (2 hunks)
* `tests/test_e2e.py` (6 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/test_e2e.py</summary>

61-61: Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`

Replace `assert False`

(B011)

---

63-63: Yoda condition detected

(SIM300)

---

258-258: f-string without any placeholders

Remove extraneous `f` prefix

(F541)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: coverage
* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (19)</summary><blockquote>

<details>
<summary>graph/src/value.rs (5)</summary>

`4-4`: **Enhancement: Added arithmetic operation traits for Value.**

Good addition of the `Sub`, `Mul`, `Div`, and `Rem` traits to the existing `Add` import, which completes the set of basic arithmetic operations for the `Value` enum.

---

`78-98`: **Implementation of subtraction looks good.**

The `Sub` trait implementation follows the same pattern as the existing `Add` implementation, correctly handling null propagation and type coercions between Int and Float.

---

`100-120`: **Implementation of multiplication looks good.**

The `Mul` trait implementation follows the established pattern, handling null propagation, type coercions, and error cases consistently.

---

`122-166`: **Division implementation with zero checks looks good.**

The `Div` trait implementation properly handles division by zero checks for all numeric combinations, returning appropriate error messages while maintaining the established pattern for type coercions and null propagation.

---

`168-212`: **Remainder implementation with zero checks looks good.**

The `Rem` trait implementation properly handles division by zero checks for modulo operations, following the same pattern as the division implementation.

</details>
<details>
<summary>tests/test_e2e.py (7)</summary>

`56-57`: **Added compare_results parameter to support non-deterministic functions.**

Good enhancement to the query function to make it more flexible by allowing callers to specify whether results from write and read operations should be compared.

---

`68-75`: **Good enhancement to result comparison logic.**

The added code handles NaN values correctly during result comparison, which is important for numerical operations that might produce NaN results.

---

`161-183`: **Well-designed CustomNumber class for testing arithmetic operations.**

The CustomNumber class effectively enables testing of the arithmetic operations in the Value enum by providing Python equivalents of the operations. Good implementation of division that handles integer and float divisions appropriately.

---

`229-239`: **Comprehensive test for arithmetic operations.**

This nested loop provides thorough testing of combinations of arithmetic operators with different numeric types, which is especially important given the new arithmetic trait implementations in Value.

---

`347-364`: **Good test for node labels functionality.**

This new test function verifies the behavior of node creation with and without labels, and the retrieval of labels, which is an important part of the graph functionality.

---

`1635-1635`: **Appropriate use of compare_results=False for random values.**

Setting compare_results=False for the rand() function test is correct since random numbers should not be expected to be equal across different calls.

---

`1756-1767`: **Good test coverage for range function.**

The new test_range function provides comprehensive testing of the range function with various parameters, including positive and negative ranges and step values.

</details>
<details>
<summary>graph/src/runtime.rs (7)</summary>

`13-19`: **Good trait design for the callback mechanism.**

The ReturnCallback trait provides a clean way to handle query results, with a clear separation between the execution logic and result handling.

---

`21-31`: **Well-structured ResultSummary for query statistics.**

The ResultSummary struct captures all the relevant statistics about query execution, providing valuable information to callers.

---

`33-49`: **Good redesign of Runtime struct with proper lifetime annotations.**

The Runtime struct now holds a reference to a RefCell<Graph> with an appropriate lifetime annotation, enabling interior mutability while maintaining safe access patterns.

---

`140-147`: **Effective parameter evaluation in constructor.**

Parameters are evaluated eagerly in the constructor, converting them from IR to Value, which simplifies later execution logic.

---

`327-352`: **Good implementation of arithmetic operations using Value traits.**

The arithmetic operations now use the new traits implemented in Value, providing a clean and consistent way to handle these operations with appropriate error handling.

---

`399-445`: **Well-structured run method for IR execution.**

The run method provides a clean way to execute IR trees, with appropriate handling of control flow constructs like If and For.

---

`1446-1463`: **Good implementation of range function with step handling.**

The range function now correctly handles both positive and negative step values, with appropriate handling of edge cases.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:32:05Z

Review comments:
diff_hunk: @@ -120,6 +125,591 @@ impl Runtime {
         }
     }
 
+    #[allow(clippy::too_many_lines)]
+    fn ro_run_expr(
+        &mut self,
+        g: &Graph,
+        ir: &DynNode<ExprIR>,
+    ) -> Result<Value, String> {
+        match ir.data() {
+            ExprIR::Null => Ok(Value::Null),
+            ExprIR::Bool(x) => Ok(Value::Bool(*x)),
+            ExprIR::Integer(x) => Ok(Value::Int(*x)),
+            ExprIR::Float(x) => Ok(Value::Float(*x)),
+            ExprIR::String(x) => Ok(Value::String(x.to_string())),
+            ExprIR::Var(x) => self.vars.get(x).map_or_else(
+                || Err(format!("Variable {x} not found")),
+                |v| Ok(v.to_owned()),
+            ),
+            ExprIR::Parameter(x) => self.parameters.get(x).map_or_else(
+                || Err(format!("Parameter {x} not found")),
+                |v| Ok(v.to_owned()),
+            ),
+            ExprIR::List => Ok(Value::List(
+                ir.children()
+                    .map(|ir| self.ro_run_expr(g, &ir))
+                    .collect::<Result<Vec<_>, _>>()?,
+            )),
+            ExprIR::Length => match self.ro_run_expr(g, &ir.child(0))? {
+                Value::List(arr) => Ok(Value::Int(arr.len() as _)),
+                _ => Err("Length operator requires a list".to_string()),
+            },
+            ExprIR::GetElement => {
+                let arr = self.ro_run_expr(g, &ir.child(0))?;
+                let i = self.ro_run_expr(g, &ir.child(1))?;
+                match (arr, i) {
+                    (Value::List(values), Value::Int(i)) => {
+                        if i >= 0 && i < values.len() as _ {
+                            Ok(values[i as usize].clone())
+                        } else {
+                            Ok(Value::Null)
+                        }
+                    }
+                    (Value::List(_), v) => {
+                        Err(format!("Type mismatch: expected Bool but was {v:?}"))
+                    }
+                    v => Err(format!("Type mismatch: expected List but was {v:?}")),
+                }
+            }
+            ExprIR::GetElements => {
+                let arr = self.ro_run_expr(g, &ir.child(0))?;
+                let a = self.ro_run_expr(g, &ir.child(1))?;
+                let b = self.ro_run_expr(g, &ir.child(2))?;
+                get_elements(arr, a, b)
+            }
+            ExprIR::IsNull => match self.ro_run_expr(g, &ir.child(0))? {
+                Value::Null => Ok(Value::Bool(true)),
+                _ => Ok(Value::Bool(false)),
+            },
+            ExprIR::IsNode => match self.ro_run_expr(g, &ir.child(0))? {
+                Value::Node(_) => Ok(Value::Bool(true)),
+                _ => Ok(Value::Bool(false)),
+            },
+            ExprIR::IsRelationship => match self.ro_run_expr(g, &ir.child(0))? {
+                Value::Relationship(_, _, _) => Ok(Value::Bool(true)),
+                _ => Ok(Value::Bool(false)),
+            },
+            ExprIR::Or => {
+                let mut is_null = false;
+                for ir in ir.children() {
+                    match self.ro_run_expr(g, &ir)? {
+                        Value::Bool(true) => return Ok(Value::Bool(true)),
+                        Value::Bool(false) => {}
+                        Value::Null => is_null = true,
+                        _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
+                    }
+                }
+                if is_null {
+                    return Ok(Value::Null);
+                }
+
+                Ok(Value::Bool(false))
+            }
+            ExprIR::Xor => {
+                let mut last = None;
+                for ir in ir.children() {
+                    match self.ro_run_expr(g, &ir)? {
+                        Value::Bool(b) => last = Some(last.map_or(b, |l| logical_xor(l, b))),
+                        Value::Null => return Ok(Value::Null),
+                        _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
+                    }
+                }
+                Ok(Value::Bool(last.unwrap_or(false)))
+            }
+            ExprIR::And => {
+                let mut is_null = false;
+                for ir in ir.children() {
+                    match self.ro_run_expr(g, &ir)? {
+                        Value::Bool(false) => return Ok(Value::Bool(false)),
+                        Value::Bool(true) => {}
+                        Value::Null => is_null = true,
+                        _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
+                    }
+                }
+                if is_null {
+                    return Ok(Value::Null);
+                }
+
+                Ok(Value::Bool(true))
+            }
+            ExprIR::Not => match self.ro_run_expr(g, &ir.child(0))? {
+                Value::Bool(b) => Ok(Value::Bool(!b)),
+                Value::Null => Ok(Value::Null),
+                _ => {
+                    Err("InvalidArgumentType: Not operator requires a boolean or null".to_string())
+                }
+            },
+            ExprIR::Negate => match self.ro_run_expr(g, &ir.child(0))? {
+                Value::Int(i) => Ok(Value::Int(-i)),
+                Value::Float(f) => Ok(Value::Float(-f)),
+                Value::Null => Ok(Value::Null),
+                _ => Err(
+                    "InvalidArgumentType: Negate operator requires an Integer or Float".to_string(),
+                ),
+            },
+            ExprIR::Eq => all_equals(ir.children().map(|ir| self.ro_run_expr(g, &ir))),
+            ExprIR::Neq => ir
+                .children()
+                .flat_map(|ir| self.ro_run_expr(g, &ir))
+                .reduce(|a, b| Value::Bool(a != b))
+                .ok_or_else(|| "Neq operator requires at least one argument".to_string()),
+            ExprIR::Lt => match (
+                self.ro_run_expr(g, &ir.child(0))?,
+                self.ro_run_expr(g, &ir.child(1))?,
+            ) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
+                _ => Err("Lt operator requires two integers".to_string()),
+            },
+            ExprIR::Gt => match (
+                self.ro_run_expr(g, &ir.child(0))?,
+                self.ro_run_expr(g, &ir.child(1))?,
+            ) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
+                _ => Err("Gt operator requires two integers".to_string()),
+            },
+            ExprIR::Le => match (
+                self.ro_run_expr(g, &ir.child(0))?,
+                self.ro_run_expr(g, &ir.child(1))?,
+            ) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
+                _ => Err("Le operator requires two integers".to_string()),
+            },
+            ExprIR::Ge => match (
+                self.ro_run_expr(g, &ir.child(0))?,
+                self.ro_run_expr(g, &ir.child(1))?,
+            ) {
+                (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
+                _ => Err("Ge operator requires two integers".to_string()),
+            },
+            ExprIR::In => {
+                let value = self.ro_run_expr(g, &ir.child(0))?;
+                let list = self.ro_run_expr(g, &ir.child(1))?;
+                list_contains(&list, &value)
+            }
+            ExprIR::Add => ir
+                .children()
+                .map(|ir| self.ro_run_expr(g, &ir))
+                .reduce(|acc, value| acc? + value?)
+                .ok_or_else(|| "Add operator requires at least one operand".to_string())?,
+            ExprIR::Sub => ir
+                .children()
+                .flat_map(|ir| self.ro_run_expr(g, &ir))
+                .reduce(|a, b| match (a, b) {
+                    (Value::Int(a), Value::Int(b)) => Value::Int(a - b),
+                    _ => Value::Null,
+                })
+                .ok_or_else(|| "Sub operator requires at least one argument".to_string()),
+            ExprIR::Mul => ir
+                .children()
+                .flat_map(|ir| self.ro_run_expr(g, &ir))
+                .reduce(|a, b| match (a, b) {
+                    (Value::Int(a), Value::Int(b)) => Value::Int(a * b),
+                    _ => Value::Null,
+                })
+                .ok_or_else(|| "Mul operator requires at least one argument".to_string()),
+            ExprIR::Div => ir
+                .children()
+                .flat_map(|ir| self.ro_run_expr(g, &ir))
+                .reduce(|a, b| match (a, b) {
+                    (Value::Int(a), Value::Int(b)) => Value::Int(a / b),
+                    (Value::Int(a), Value::Float(b)) => Value::Float(a as f64 / b),
+                    (Value::Float(a), Value::Int(b)) => Value::Float(a / b as f64),
+                    (Value::Float(a), Value::Float(b)) => Value::Float(a / b),
+                    _ => Value::Null,
+                })
+                .ok_or_else(|| "Div operator requires at least one argument".to_string()),
+            ExprIR::Pow => ir
+                .children()
+                .flat_map(|ir| self.ro_run_expr(g, &ir))
+                .reduce(|a, b| match (a, b) {
+                    (Value::Int(a), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
+                    _ => Value::Null,
+                })
+                .ok_or_else(|| "Pow operator requires at least one argument".to_string()),
+            ExprIR::Modulo => ir
+                .children()
+                .flat_map(|ir| self.ro_run_expr(g, &ir))
+                .reduce(|a, b| match (a, b) {
+                    (Value::Int(a), Value::Int(b)) => Value::Int(a % b),
+                    _ => Value::Null,
+                })
+                .ok_or_else(|| "Modulo operator requires at least one argument".to_string()),
path: graph/src/runtime.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`flat_map` causes a compile-time error – switch to `map` / `try_fold`**

`Iterator::flat_map` expects the closure to return something that can itself be iterated.  
`self.ro_run_expr(..)` returns a `Result<Value, String>` which is **not** an iterator, so these lines will not compile:

```rust
.children()
.flat_map(|ir| self.ro_run_expr(g, &ir))
.reduce(|a, b| { ... })
```

Replace `flat_map` with `map`, collect the intermediate `Result`s and fold, or use `try_fold` which short-circuits on the first error:

```diff
-ExprIR::Sub => ir
-    .children()
-    .flat_map(|ir| self.ro_run_expr(g, &ir))
-    .reduce(|a, b| match (a, b) {
-        (Value::Int(a), Value::Int(b)) => Value::Int(a - b),
-        _ => Value::Null,
-    })
-    .ok_or_else(|| "Sub operator requires at least one argument".to_string()),
+ExprIR::Sub => ir
+    .children()
+    .map(|node| self.ro_run_expr(g, &node))
+    .try_fold(None, |acc, v| {
+        let v = v?;
+        Ok::<_, String>(Some(match (acc, v) {
+            (None, cur)                     => cur,
+            (Some(Value::Int(a)), Value::Int(b)) => Value::Int(a - b),
+            _ => return Err("Sub operator requires integers".into()),
+        }))
+    })?
+    .ok_or_else(|| "Sub operator requires at least one argument".into()),
```

The same pattern (`flat_map(..).reduce(..)`) appears in `Mul`, `Div`, `Pow`, `Modulo`, and their read-write counterparts – fix them as well to restore compilation.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T20:24:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,141 +112,44 @@
             .map(|p| p as u64)
     }
 
-    pub fn query<CB: ReturnCallback>(
-        &mut self,
-        query: &str,
-        callback: &CB,
-        debug: bool,
-    ) -> Result<ResultSummary, String> {
-        let mut parse_duration = Duration::ZERO;
-        let mut plan_duration = Duration::ZERO;
-
-        let mut parser = Parser::new(query);
-        let (parameters, query) = parser.parse_parameters()?;
-
-        let evaluate = {
-            match self.cache.lock() {
-                Ok(mut cache) => {
-                    if let Some(f) = cache.get(query) {
-                        f.to_owned()
-                    } else {
-                        let start = Instant::now();
-                        let ir = parser.parse()?;
-                        parse_duration = start.elapsed();
-
-                        let mut planner = Planner::new();
-                        let start = Instant::now();
-                        let value = planner.plan(ir, debug);
-                        plan_duration = start.elapsed();
-
-                        cache.insert(query.to_string(), value.clone());
-                        value
-                    }
-                }
-                Err(_) => {
-                    return Err("Failed to acquire read lock on cache".to_string());
-                }
-            }
-        };
-
-        let labels_count = self.node_labels.len();
-        let mut runtime = Runtime::new(
-            parameters
-                .into_iter()
-                .map(|(k, v)| (k, evaluate_param(v.root())))
-                .collect(),
-        );
-        let start = Instant::now();
-        run(
-            &mut BTreeMap::new(),
-            self,
-            &mut runtime,
-            callback,
-            &evaluate.root(),
-        )?;
-        let run_duration = start.elapsed();
-
-        Ok(ResultSummary {
-            parse_duration,
-            plan_duration,
-            run_duration,
-            labels_added: self.node_labels.len() - labels_count,
-            labels_removed: 0,
-            nodes_created: runtime.nodes_created,
-            relationships_created: runtime.relationships_created,
-            nodes_deleted: runtime.nodes_deleted,
-            relationships_deleted: runtime.relationships_deleted,
-            properties_set: runtime.properties_set,
-            properties_removed: runtime.properties_removed,
-        })
-    }
-
-    pub fn ro_query<CB: ReturnCallback>(
+    pub fn get_plan(
         &self,
         query: &str,
-        callback: &CB,
-        debug: bool,
-    ) -> Result<ResultSummary, String> {
+    ) -> Result<
+        (
+            DynTree<IR>,
+            BTreeMap<String, DynTree<ExprIR>>,
+            Duration,
+            Duration,
+        ),
+        String,
+    > {
         let mut parse_duration = Duration::ZERO;
         let mut plan_duration = Duration::ZERO;
 
         let mut parser = Parser::new(query);
         let (parameters, query) = parser.parse_parameters()?;
 
-        let evaluate = {
-            match self.cache.lock() {
-                Ok(mut cache) => {
-                    if let Some(f) = cache.get(query) {
-                        f.to_owned()
-                    } else {
-                        let start = Instant::now();
-                        let ir = parser.parse()?;
-                        parse_duration = start.elapsed();
-
-                        let mut planner = Planner::new();
-                        let start = Instant::now();
-                        let value = planner.plan(ir, debug);
-                        plan_duration = start.elapsed();
-
-                        cache.insert(query.to_string(), value.clone());
-                        value
-                    }
-                }
-                Err(_) => {
-                    return Err("Failed to acquire read lock on cache".to_string());
+        match self.cache.lock() {
+            Ok(mut cache) => {
+                if let Some(f) = cache.get(query) {
+                    Ok((f.to_owned(), parameters, parse_duration, plan_duration))
+                } else {
+                    let start = Instant::now();
+                    let ir = parser.parse()?;
+                    parse_duration = start.elapsed();
+
+                    let mut planner = Planner::new();
+                    let start = Instant::now();
+                    let value = planner.plan(ir);
+                    plan_duration = start.elapsed();
+
+                    cache.insert(query.to_string(), value.clone());
+                    Ok((value, parameters, parse_duration, plan_duration))
path: graph/src/graph.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid holding the cache mutex during expensive parse / plan work**

`parser.parse()` and `planner.plan()` are potentially heavy, yet they are executed while the cache mutex is held.  
Under concurrent load this can seriously throttle throughput because every thread blocks on the same lock until the whole planning pipeline finishes.

A common pattern is:

1. Take a short lock, **look-up** in the map, clone the value if it exists → return.  
2. *Release* the lock.
3. If the entry is missing, do the heavy work (parse / plan).  
4. Re-acquire the lock only to **insert** the computed value.

```diff
-        match self.cache.lock() {
-            Ok(mut cache) => {
-                if let Some(f) = cache.get(query) {
-                    Ok((f.to_owned(), parameters, parse_duration, plan_duration))
-                } else {
-                    /* heavy work here while holding the lock */
-                }
-            }
-            Err(_) => Err("Failed to acquire read lock on cache".to_string()),
-        }
+        // 1) optimistic read
+        if let Ok(cache) = self.cache.lock() {
+            if let Some(f) = cache.get(query) {
+                return Ok((f.clone(), parameters, parse_duration, plan_duration));
+            }
+        }
+
+        // 2-4) heavy work outside the lock
+        let ir = {
+            let start = Instant::now();
+            let ir = parser.parse()?;
+            parse_duration = start.elapsed();
+            ir
+        };
+        let value = {
+            let start = Instant::now();
+            let v = Planner::new().plan(ir);
+            plan_duration = start.elapsed();
+            v
+        };
+
+        // insert
+        self.cache
+            .lock()
+            .map_err(|_| "Failed to acquire lock on cache")?
+            .insert(query.to_owned(), value.clone());
+
+        Ok((value, parameters, parse_duration, plan_duration))
```

This keeps the critical section minimal and improves concurrency.



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T21:18:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,9 +150,622 @@
         }
     }
 
+    pub fn query<CB: ReturnCallback>(
+        &mut self,
+        plan: DynTree<IR>,
+        callback: &CB,
+    ) -> Result<ResultSummary, String> {
+        let labels_count = self.g.borrow().get_labels_count();
+        let start = Instant::now();
+        self.run(callback, &plan.root())?;
+        let run_duration = start.elapsed();
+
+        Ok(ResultSummary {
+            run_duration,
+            labels_added: self.g.borrow().get_labels_count() - labels_count,
+            labels_removed: 0,
+            nodes_created: self.nodes_created,
+            relationships_created: self.relationships_created,
+            nodes_deleted: self.nodes_deleted,
+            relationships_deleted: self.relationships_deleted,
+            properties_set: self.properties_set,
+            properties_removed: self.properties_removed,
+        })
path: graph/src/runtime.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Under-/overflow risk when computing `labels_added`**

`labels_added` is calculated with unsigned subtraction:

```rust
let labels_added = self.g.borrow().get_labels_count() - labels_count;
```

If labels were *removed* during the query this will wrap around and panic in debug builds.  
Use `saturating_sub` (or switch to a signed type) to make the computation safe:

```diff
-let labels_added = self.g.borrow().get_labels_count() - labels_count;
+let labels_added = self
+    .g
+    .borrow()
+    .get_labels_count()
+    .saturating_sub(labels_count);
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T21:18:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,19 +53,26 @@ def setup_function(function):
         g.delete()
 
 
-def query(query: str, params=None, write: bool = False):
+def query(query: str, params=None, write: bool = False, compare_results: bool = True):
     if write:
+        try:
+            g.query("RETURN 1")
+            read_res = g.ro_query(query, params)
+            assert False
+        except ResponseError as e:
+            assert "graph.RO_QUERY is to be executed only on read-only queries" == str(e)
path: tests/test_e2e.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Refactor assertion logic to be more readable and robust**

The current code attempts to verify that read-only queries reject write operations, but has two issues:

1. It uses an unrelated query `"RETURN 1"` before testing the actual query
2. The assertion pattern `assert False` followed by `except` is flagged by static analysis tools as problematic


```diff
-        try:
-            g.query("RETURN 1")
-            read_res = g.ro_query(query, params)
-            assert False
-        except ResponseError as e:
-            assert "graph.RO_QUERY is to be executed only on read-only queries" == str(e)
+        try:
+            read_res = g.ro_query(query, params)
+            raise AssertionError("Expected ro_query to fail for write operations")
+        except ResponseError as e:
+            assert str(e) == "graph.RO_QUERY is to be executed only on read-only queries"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        try:
            read_res = g.ro_query(query, params)
            raise AssertionError("Expected ro_query to fail for write operations")
        except ResponseError as e:
            assert str(e) == "graph.RO_QUERY is to be executed only on read-only queries"
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

61-61: Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`

Replace `assert False`

(B011)

---

63-63: Yoda condition detected

(SIM300)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T08:01:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-05T20:21:56Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update refactors the query handling in the `Graph` struct by removing the `query` and `ro_query` methods and introducing a new `get_plan` method that returns a planned IR tree with parameters and timing information. Runtime query execution and callback handling are moved out of `Graph` into a redesigned `Runtime` struct that manages variable state and evaluation. The planner interface is simplified by removing an unused debug flag. The internal graph is wrapped in `RefCell` to support interior mutability, and query execution in the Redis module is updated accordingly.

## Changes

| File(s)                      | Change Summary                                                                                                                      |
|------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `graph/src/graph.rs`          | Removed `query` and `ro_query` methods and `ReturnCallback` trait; added `get_plan` method returning planned IR, parameters, and timings; added `get_labels_count`; removed runtime execution and `ResultSummary` struct; refactored query handling to separate planning from execution. |
| `graph/src/planner.rs`        | Removed unused `_debug` parameter from `Planner::plan` method signature; method body unchanged.                                     |
| `graph/src/runtime.rs`        | Redesigned `Runtime` struct to hold a `RefCell<Graph>` reference and variable bindings; converted runtime evaluation functions into methods; unified runtime function signatures; integrated parameter evaluation at construction; added `query` method on `Runtime` with callback support; removed free functions and made `evaluate_param` private; added `ReturnCallback` trait and `ResultSummary` struct. |
| `src/lib.rs`                  | Changed graph storage to `RefCell<Graph>` for interior mutability; updated query functions to use `get_plan` and `Runtime` for execution; removed `debug` parameter from query interfaces; simplified planner call by removing debug flag; updated `ReturnCallback` trait implementation to use `RefCell<Graph>`. |
| `tests/test_e2e.py`           | Modified `query` test helper to optionally compare results of write and read-only queries; added assertion that read-only queries reject write operations; updated test calls accordingly. |
| `graph/src/value.rs`          | Added implementations for `Sub`, `Mul`, `Div`, and `Rem` traits on `Value` enum with error handling and type coercion, extending arithmetic operations. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Graph
    participant Planner
    participant Runtime

    Client->>Graph: query(query_str)
    Graph->>Graph: get_plan(query_str)
    Graph->>Planner: parse & plan query_str
    Planner-->>Graph: (plan, params, timings) or error
    Graph->>Runtime: new(g_refcell, params, write_flag)
    Runtime->>Runtime: execute plan with callback
    Runtime-->>Graph: ResultSummary
    Graph-->>Client: ResultSummary or error
```

## Possibly related PRs

- [#54](https://github.com/FalkorDB/falkordb-rs-next-gen/pull/54): Refactored IR representation to use `DynTree<IR>`, directly affecting planner and runtime logic now used in the new `get_plan` method.
- [#59](https://github.com/FalkorDB/falkordb-rs-next-gen/pull/59): Refactored expression AST representation from `QueryExprIR` to tree-based `ExprIR`, related to expression and IR planning layers used in this PR.
- [#58](https://github.com/FalkorDB/falkordb-rs-next-gen/pull/58): Introduced the `ReturnCallback` trait and callback-based query execution in `Graph::query` and `ro_query`, which this PR replaces by removing callback handling from `Graph`.

## Suggested labels

`enhancement`

## Suggested reviewers

- barakb

## Poem

> 🐇 In code’s green fields where queries play,  
> Plans are drawn in a clearer way.  
> No debug flags to cloud the light,  
> Runtime runs with pure delight.  
> Cache is kept, and calls are neat,  
> Rabbits hop to a tidy beat!  
> 🥕✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=FalkorDB/falkordb-rs-next-gen&utm_content=60):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-05T20:31:03Z
body: ## [Codecov](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) Report
Attention: Patch coverage is `85.05495%` with `68 lines` in your changes missing coverage. Please review.
> Project coverage is 62.45%. Comparing base [(`b3447f8`)](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/commit/b3447f824c740a7958a0ba4c77a058d923661504?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) to head [(`7672155`)](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/commit/76721553215965b679424ad8a54ec229c8a8d366?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB).
> Report is 5 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) | Patch % | Lines |
|---|---|---|
| [graph/src/runtime.rs](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&filepath=graph%2Fsrc%2Fruntime.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB#diff-Z3JhcGgvc3JjL3J1bnRpbWUucnM=) | 89.10% | [34 Missing :warning: ](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) |
| [graph/src/value.rs](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&filepath=graph%2Fsrc%2Fvalue.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB#diff-Z3JhcGgvc3JjL3ZhbHVlLnJz) | 63.63% | [32 Missing :warning: ](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) |
| [graph/src/graph.rs](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&filepath=graph%2Fsrc%2Fgraph.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB#diff-Z3JhcGgvc3JjL2dyYXBoLnJz) | 96.55% | [1 Missing :warning: ](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) |
| [src/lib.rs](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&filepath=src%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB#diff-c3JjL2xpYi5ycw==) | 96.15% | [1 Missing :warning: ](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #60      +/-   ##
==========================================
- Coverage   62.90%   62.45%   -0.45%     
==========================================
  Files          11       11              
  Lines        6213     5996     -217     
==========================================
- Hits         3908     3745     -163     
+ Misses       2305     2251      -54     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/60?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE