Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
operators.go23.5 kB
// Operator evaluation for NornicDB Cypher. // // This file contains functions for evaluating logical, comparison, and arithmetic // operators in Cypher expressions. These operators follow Neo4j semantics for // compatibility. // // # Operator Categories // // Logical Operators: // - AND: Logical conjunction (short-circuit evaluation) // - OR: Logical disjunction (short-circuit evaluation) // - XOR: Logical exclusive or // - NOT: Logical negation // // Comparison Operators: // - = : Equality // - <> : Inequality (also !=) // - < : Less than // - > : Greater than // - <= : Less than or equal // - >= : Greater than or equal // - =~ : Regular expression match // // Arithmetic Operators: // - + : Addition (also date + duration) // - - : Subtraction (also date - duration, date - date) // - * : Multiplication // - / : Division // - % : Modulo // // # Operator Precedence // // From highest to lowest: // 1. Unary: NOT, - // 2. Multiplicative: *, /, % // 3. Additive: +, - // 4. Comparison: =, <>, <, >, <=, >=, =~ // 5. Logical AND // 6. Logical XOR // 7. Logical OR // // # ELI12 // // Operators are like action words in math: // - AND means "both must be true" (like needing both a ticket AND ID to enter) // - OR means "at least one" (like having either cash OR a card to pay) // - + means "add together" (like combining apples from two baskets) // - = means "are these the same?" (like checking if two puzzle pieces match) // // When you have multiple operators like "2 + 3 * 4", we follow PEMDAS rules: // multiplication (*) happens before addition (+), so it's 2 + (3 * 4) = 14. // // # Neo4j Compatibility // // These operators match Neo4j behavior exactly: // - NULL propagation (NULL AND true = NULL) // - Type coercion rules (comparing integers to floats) // - Date arithmetic (date + duration) // - Regex matching (=~ operator) package cypher import ( "strings" "github.com/orneryd/nornicdb/pkg/storage" ) // ======================================== // Logical Operators // ======================================== // hasLogicalOperator checks if the expression has a logical operator outside of quotes/parentheses. // // This function scans the expression for the given operator, ensuring it's not inside: // - String literals (single or double quotes) // - Nested parentheses // // # Parameters // // - expr: The expression to check // - op: The logical operator to find (case-insensitive) // // # Returns // // - true if the operator is found at the top level // - false if not found or only inside quotes/parentheses // // # Example // // hasLogicalOperator("a = 1 AND b = 2", " AND ") // true // hasLogicalOperator("name CONTAINS 'AND'", " AND ") // false (inside quotes) // hasLogicalOperator("(a AND b) OR c", " AND ") // false (inside parens) func (e *StorageExecutor) hasLogicalOperator(expr, op string) bool { upperExpr := strings.ToUpper(expr) upperOp := strings.ToUpper(op) inQuote := false quoteChar := rune(0) parenDepth := 0 for i := 0; i <= len(upperExpr)-len(upperOp); i++ { c := rune(expr[i]) switch { case c == '\'' || c == '"': if !inQuote { inQuote = true quoteChar = c } else if c == quoteChar { inQuote = false } case c == '(' && !inQuote: parenDepth++ case c == ')' && !inQuote: parenDepth-- case !inQuote && parenDepth == 0: if upperExpr[i:i+len(upperOp)] == upperOp { return true } } } return false } // evaluateLogicalAnd evaluates expr1 AND expr2. // // This implements short-circuit evaluation: if any part is false, // the entire expression is false without evaluating remaining parts. // // # Parameters // // - expr: The expression containing AND operators // - nodes: Map of variable names to nodes for property access // - rels: Map of variable names to relationships for property access // // # Returns // // - true if all parts evaluate to true // - false if any part evaluates to false // - nil if the expression cannot be split // // # Example // // evaluateLogicalAnd("a = 1 AND b = 2", nodes, rels) // true if both conditions match // evaluateLogicalAnd("true AND false", nodes, rels) // false func (e *StorageExecutor) evaluateLogicalAnd(expr string, nodes map[string]*storage.Node, rels map[string]*storage.Edge) interface{} { parts := e.splitByOperator(expr, " AND ") if len(parts) < 2 { return nil } for _, part := range parts { result := e.evaluateExpressionWithContext(part, nodes, rels) if result != true { return false } } return true } // evaluateLogicalOr evaluates expr1 OR expr2. // // This implements short-circuit evaluation: if any part is true, // the entire expression is true without evaluating remaining parts. // // # Parameters // // - expr: The expression containing OR operators // - nodes: Map of variable names to nodes for property access // - rels: Map of variable names to relationships for property access // // # Returns // // - true if any part evaluates to true // - false if all parts evaluate to false // - nil if the expression cannot be split // // # Example // // evaluateLogicalOr("a = 1 OR b = 2", nodes, rels) // true if either condition matches // evaluateLogicalOr("false OR true", nodes, rels) // true func (e *StorageExecutor) evaluateLogicalOr(expr string, nodes map[string]*storage.Node, rels map[string]*storage.Edge) interface{} { parts := e.splitByOperator(expr, " OR ") if len(parts) < 2 { return nil } for _, part := range parts { result := e.evaluateExpressionWithContext(part, nodes, rels) if result == true { return true } } return false } // evaluateLogicalXor evaluates expr1 XOR expr2. // // XOR (exclusive or) returns true when exactly one operand is true. // // # Parameters // // - expr: The expression containing XOR operator // - nodes: Map of variable names to nodes for property access // - rels: Map of variable names to relationships for property access // // # Returns // // - true if exactly one side is true // - false if both are true or both are false // - nil if the expression cannot be split into exactly 2 parts // // # Example // // evaluateLogicalXor("true XOR false", nodes, rels) // true // evaluateLogicalXor("true XOR true", nodes, rels) // false // evaluateLogicalXor("false XOR false", nodes, rels) // false func (e *StorageExecutor) evaluateLogicalXor(expr string, nodes map[string]*storage.Node, rels map[string]*storage.Edge) interface{} { parts := e.splitByOperator(expr, " XOR ") if len(parts) != 2 { return nil } left := e.evaluateExpressionWithContext(parts[0], nodes, rels) == true right := e.evaluateExpressionWithContext(parts[1], nodes, rels) == true return left != right } // ======================================== // Comparison Operators // ======================================== // hasComparisonOperator checks if the expression has a comparison operator. // // Operators checked (in order of specificity): // - <> (not equal) // - <= (less than or equal) // - >= (greater than or equal) // - =~ (regex match) // - != (not equal, alternative) // - = (equal) // - < (less than) // - > (greater than) // // # Parameters // // - expr: The expression to check // // # Returns // // - true if any comparison operator is found at the top level // // # Example // // hasComparisonOperator("a = 1") // true // hasComparisonOperator("a <= 5") // true // hasComparisonOperator("name =~ '.*'") // true // hasComparisonOperator("a + b") // false func (e *StorageExecutor) hasComparisonOperator(expr string) bool { ops := []string{"<>", "<=", ">=", "=~", "!=", "=", "<", ">"} for _, op := range ops { if e.hasOperatorOutsideQuotes(expr, op) { return true } } return false } // hasOperatorOutsideQuotes checks if operator exists outside quotes and parentheses. // // This function handles special cases like ensuring "=" is not confused with // "<=", ">=", "!=" or "=~". // // # Parameters // // - expr: The expression to check // - op: The operator to find // // # Returns // // - true if the operator is found at the top level // // # Example // // hasOperatorOutsideQuotes("a = 1", "=") // true // hasOperatorOutsideQuotes("a <= 1", "=") // false (part of <=) // hasOperatorOutsideQuotes("name = 'test=1'", "=") // true (first = only) func (e *StorageExecutor) hasOperatorOutsideQuotes(expr, op string) bool { inQuote := false quoteChar := rune(0) parenDepth := 0 for i := 0; i <= len(expr)-len(op); i++ { c := rune(expr[i]) switch { case c == '\'' || c == '"': if !inQuote { inQuote = true quoteChar = c } else if c == quoteChar { inQuote = false } case c == '(' && !inQuote: parenDepth++ case c == ')' && !inQuote: parenDepth-- case !inQuote && parenDepth == 0: if expr[i:i+len(op)] == op { // Make sure = is not part of <= or >= if op == "=" { if i > 0 && (expr[i-1] == '<' || expr[i-1] == '>' || expr[i-1] == '!') { continue } if i < len(expr)-1 && expr[i+1] == '~' { continue } } return true } } } return false } // evaluateComparisonExpr evaluates comparison expressions. // // Operators are evaluated in order of specificity to handle multi-character // operators correctly (e.g., "<>" before "<" and ">"). // // # Parameters // // - expr: The comparison expression // - nodes: Map of variable names to nodes for property access // - rels: Map of variable names to relationships for property access // // # Returns // // - true or false based on the comparison result // - nil if no valid comparison operator found // // # Example // // evaluateComparisonExpr("5 > 3", nodes, rels) // true // evaluateComparisonExpr("'abc' = 'abc'", nodes, rels) // true // evaluateComparisonExpr("n.age >= 18", nodes, rels) // depends on n.age func (e *StorageExecutor) evaluateComparisonExpr(expr string, nodes map[string]*storage.Node, rels map[string]*storage.Edge) interface{} { // Try operators in order of specificity ops := []struct { op string eval func(left, right interface{}) bool }{ {"<>", func(l, r interface{}) bool { return !e.compareEqual(l, r) }}, {"!=", func(l, r interface{}) bool { return !e.compareEqual(l, r) }}, {"<=", func(l, r interface{}) bool { return e.compareLess(l, r) || e.compareEqual(l, r) }}, {">=", func(l, r interface{}) bool { return e.compareGreater(l, r) || e.compareEqual(l, r) }}, {"=~", e.compareRegex}, {"=", e.compareEqual}, {"<", e.compareLess}, {">", e.compareGreater}, } for _, op := range ops { parts := e.splitByOperator(expr, op.op) if len(parts) == 2 { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) return op.eval(left, right) } } return nil } // ======================================== // Arithmetic Operators // ======================================== // hasArithmeticOperator checks if the expression has arithmetic operators. // // Checks for: +, -, *, /, % // Note: + and - are checked with surrounding spaces to distinguish // from unary operators and signs in numbers. // // # Parameters // // - expr: The expression to check // // # Returns // // - true if any arithmetic operator is found at the top level // // # Example // // hasArithmeticOperator("a + b") // true // hasArithmeticOperator("a * b") // true // hasArithmeticOperator("-5") // false (unary minus) // hasArithmeticOperator("a - b") // true func (e *StorageExecutor) hasArithmeticOperator(expr string) bool { // Include + for date arithmetic (date + duration) // Check with and without spaces for + and - operators ops := []string{" + ", "+", "*", "/", "%", " - ", "-"} for _, op := range ops { if e.hasOperatorOutsideQuotes(expr, op) { return true } } return false } // evaluateArithmeticExpr evaluates arithmetic expressions. // // Supports: // - Numeric operations: +, -, *, /, % // - Date arithmetic: date + duration, date - duration, date - date // // # Parameters // // - expr: The arithmetic expression // - nodes: Map of variable names to nodes for property access // - rels: Map of variable names to relationships for property access // // # Returns // // - The numeric result (int64 or float64) // - For date arithmetic: string (formatted date) or *CypherDuration // - nil if no valid arithmetic operator found // // # Example // // evaluateArithmeticExpr("5 + 3", nodes, rels) // int64(8) // evaluateArithmeticExpr("10 / 3", nodes, rels) // float64(3.333...) // evaluateArithmeticExpr("date('2025-01-01') + duration('P5D')", ...) // "2025-01-06..." func (e *StorageExecutor) evaluateArithmeticExpr(expr string, nodes map[string]*storage.Node, rels map[string]*storage.Edge) interface{} { // Handle + operator (date + duration, or numeric addition) // Try with spaces first, then without if parts := e.splitByOperator(expr, " + "); len(parts) == 2 { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) return e.add(left, right) } if parts := e.splitByOperator(expr, "+"); len(parts) == 2 { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) return e.add(left, right) } // Handle * operator if parts := e.splitByOperator(expr, "*"); len(parts) == 2 { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) return e.multiply(left, right) } // Handle / operator if parts := e.splitByOperator(expr, "/"); len(parts) == 2 { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) return e.divide(left, right) } // Handle % operator if parts := e.splitByOperator(expr, "%"); len(parts) == 2 { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) return e.modulo(left, right) } // Handle - operator (binary subtraction, not unary minus) // Try with spaces first, then without (but be careful with unary minus) if parts := e.splitByOperator(expr, " - "); len(parts) == 2 { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) return e.subtract(left, right) } // For - without spaces, only split if both sides would be valid expressions if parts := e.splitByOperator(expr, "-"); len(parts) == 2 && parts[0] != "" { left := e.evaluateExpressionWithContext(parts[0], nodes, rels) right := e.evaluateExpressionWithContext(parts[1], nodes, rels) if left != nil && right != nil { return e.subtract(left, right) } } return nil } // splitByOperator splits expression by operator respecting quotes and parentheses. // // This function carefully handles: // - String literals (single and double quotes) // - Nested parentheses // - Special case for "=" not being part of "<=", ">=", "!=" // // # Parameters // // - expr: The expression to split // - op: The operator to split by (case-insensitive) // // # Returns // // - A slice with exactly 2 elements if operator found // - A slice with 1 element (original expr) if not found // // # Example // // splitByOperator("a + b", " + ") // ["a", "b"] // splitByOperator("'a + b' + c", " + ") // ["'a + b'", "c"] // splitByOperator("(a + b) + c", " + ") // ["(a + b)", "c"] func (e *StorageExecutor) splitByOperator(expr, op string) []string { inQuote := false quoteChar := rune(0) parenDepth := 0 upperExpr := strings.ToUpper(expr) upperOp := strings.ToUpper(op) for i := 0; i <= len(expr)-len(op); i++ { c := rune(expr[i]) switch { case c == '\'' || c == '"': if !inQuote { inQuote = true quoteChar = c } else if c == quoteChar { inQuote = false } case c == '(' && !inQuote: parenDepth++ case c == ')' && !inQuote: parenDepth-- case !inQuote && parenDepth == 0: if upperExpr[i:i+len(upperOp)] == upperOp { // Additional check for = not being part of <= or >= if op == "=" { if i > 0 && (expr[i-1] == '<' || expr[i-1] == '>' || expr[i-1] == '!') { continue } } left := strings.TrimSpace(expr[:i]) right := strings.TrimSpace(expr[i+len(op):]) return []string{left, right} } } } return []string{expr} } // ======================================== // Arithmetic Helper Functions // ======================================== // add handles addition including date + duration. // // This function supports: // - Numeric addition (int64 + int64 = int64, otherwise float64) // - Date arithmetic (date + duration = date, duration + date = date) // // # Parameters // // - left: Left operand // - right: Right operand // // # Returns // // - int64 if both operands are integers // - float64 for other numeric types // - string (formatted date) for date + duration // - nil if operands cannot be added // // # Example // // add(5, 3) // int64(8) // add(5.0, 3) // float64(8.0) // add("2025-01-01", &CypherDuration{Days: 5}) // "2025-01-06T00:00:00Z" func (e *StorageExecutor) add(left, right interface{}) interface{} { // Handle date/datetime + duration if dur, ok := right.(*CypherDuration); ok { result := addDurationToDate(left, dur) if result != "" { return result } } // Handle duration + date/datetime (commutative) if dur, ok := left.(*CypherDuration); ok { result := addDurationToDate(right, dur) if result != "" { return result } } // Standard numeric addition l, okL := toFloat64(left) r, okR := toFloat64(right) if !okL || !okR { return nil } result := l + r // Return integer if both were integers if _, isInt := left.(int64); isInt { if _, isInt := right.(int64); isInt { return int64(result) } } return result } // multiply performs numeric multiplication. // // # Parameters // // - left: Left operand // - right: Right operand // // # Returns // // - int64 if both operands are integers // - float64 otherwise // - nil if operands cannot be multiplied // // # Example // // multiply(5, 3) // int64(15) // multiply(5.0, 3) // float64(15.0) func (e *StorageExecutor) multiply(left, right interface{}) interface{} { l, okL := toFloat64(left) r, okR := toFloat64(right) if !okL || !okR { return nil } result := l * r // Return integer if both were integers if _, isInt := left.(int64); isInt { if _, isInt := right.(int64); isInt { return int64(result) } } return result } // divide performs numeric division. // // Note: Division by zero returns nil (NULL in Cypher). // Returns int64 if both operands are integers and division is exact. // // # Parameters // // - left: Dividend // - right: Divisor // // # Returns // // - int64 if exact integer division, float64 otherwise // - nil if divisor is zero or operands invalid // // # Example // // divide(10, 2) // int64(5) // divide(10, 3) // float64(3.333...) // divide(10, 0) // nil (division by zero) func (e *StorageExecutor) divide(left, right interface{}) interface{} { l, okL := toFloat64(left) r, okR := toFloat64(right) if !okL || !okR || r == 0 { return nil } result := l / r // Check if both operands were integers and result is exact _, leftIsInt64 := left.(int64) _, rightIsInt64 := right.(int64) _, leftIsInt := left.(int) _, rightIsInt := right.(int) leftIsInteger := leftIsInt64 || leftIsInt rightIsInteger := rightIsInt64 || rightIsInt if leftIsInteger && rightIsInteger && result == float64(int64(result)) { return int64(result) } return result } // modulo performs modulo operation (remainder after division). // // Note: Operands are converted to integers for the modulo operation. // // # Parameters // // - left: Dividend // - right: Divisor // // # Returns // // - int64 remainder // - nil if divisor is zero or operands invalid // // # Example // // modulo(10, 3) // int64(1) // modulo(10, 0) // nil (division by zero) func (e *StorageExecutor) modulo(left, right interface{}) interface{} { l, okL := toFloat64(left) r, okR := toFloat64(right) if !okL || !okR || r == 0 { return nil } return int64(l) % int64(r) } // subtract handles subtraction including date arithmetic. // // This function supports: // - Numeric subtraction (int64 - int64 = int64, otherwise float64) // - Date - duration = date // - Date - date = duration // // # Parameters // // - left: Left operand // - right: Right operand // // # Returns // // - int64 if both operands are integers // - float64 for other numeric types // - string (formatted date) for date - duration // - *CypherDuration for date - date // - nil if operands cannot be subtracted // // # Example // // subtract(10, 3) // int64(7) // subtract("2025-01-06", &CypherDuration{Days: 5}) // "2025-01-01T00:00:00Z" // subtract("2025-01-06", "2025-01-01") // &CypherDuration{Days: 5} func (e *StorageExecutor) subtract(left, right interface{}) interface{} { // Handle date - duration = date if dur, ok := right.(*CypherDuration); ok { result := subtractDurationFromDate(left, dur) if result != "" { return result } } // Handle date - date = duration leftTime := parseDateTime(left) rightTime := parseDateTime(right) if !leftTime.IsZero() && !rightTime.IsZero() { return durationBetween(left, right) } // Standard numeric subtraction l, okL := toFloat64(left) r, okR := toFloat64(right) if !okL || !okR { return nil } result := l - r // Return integer if both were integers if _, isInt := left.(int64); isInt { if _, isInt := right.(int64); isInt { return int64(result) } } return result } // ======================================== // String Predicate Helper // ======================================== // hasStringPredicate checks if expression has a string predicate (case-insensitive). // // String predicates include STARTS WITH, ENDS WITH, CONTAINS. // This function respects quotes and nested structures. // // # Parameters // // - expr: The expression to check // - predicate: The predicate to find (e.g., " CONTAINS ", " STARTS WITH ") // // # Returns // // - true if the predicate is found at the top level // // # Example // // hasStringPredicate("n.name CONTAINS 'test'", " CONTAINS ") // true // hasStringPredicate("'CONTAINS' = n.name", " CONTAINS ") // false (in quotes) func (e *StorageExecutor) hasStringPredicate(expr, predicate string) bool { upperExpr := strings.ToUpper(expr) upperPred := strings.ToUpper(predicate) inQuote := false quoteChar := rune(0) parenDepth := 0 bracketDepth := 0 for i := 0; i <= len(upperExpr)-len(upperPred); i++ { c := rune(expr[i]) switch { case c == '\'' || c == '"': if !inQuote { inQuote = true quoteChar = c } else if c == quoteChar { inQuote = false } case c == '(' && !inQuote: parenDepth++ case c == ')' && !inQuote: parenDepth-- case c == '[' && !inQuote: bracketDepth++ case c == ']' && !inQuote: bracketDepth-- case !inQuote && parenDepth == 0 && bracketDepth == 0: if upperExpr[i:i+len(upperPred)] == upperPred { return true } } } return false }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server