Skip to main content
Glama
orneryd

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

by orneryd
set_helpers.go15.8 kB
// SET clause helpers for NornicDB Cypher. // // This file contains helper functions for processing SET clauses in Cypher queries. // These functions handle property assignments, expression evaluation, and string // operations used during SET operations. // // # SET Clause Syntax // // SET operations modify node and relationship properties: // // SET n.name = 'Alice' - Single property // SET n.name = 'Alice', n.age = 30 - Multiple properties // SET n += {name: 'Alice'} - Map merge // SET n:Label - Add label // // # Expression Evaluation // // SET clauses can use various expressions: // // SET n.id = randomUUID() - Function call // SET n.ts = timestamp() - Built-in function // SET n.name = 'prefix-' + toString(n.id) - String concatenation // SET n.list = [1, 2, 3] - Array literal // // # ELI12 // // Think of SET like filling out a form: // // SET n.name = 'Alice' // // You're writing "Alice" in the "name" box on the form. These helper // functions make sure the value goes in the right format: // - Strings stay as strings: 'hello' → "hello" // - Numbers become numbers: 42 → 42 // - Arrays become lists: [1,2] → [1, 2] // - Functions run and give results: timestamp() → 1234567890 // // # Neo4j Compatibility // // These helpers ensure SET clauses work identically to Neo4j. package cypher import ( "crypto/rand" "fmt" "strconv" "strings" "github.com/orneryd/nornicdb/pkg/storage" ) // applySetToNode applies SET clause assignments to a node. // // This function parses SET clauses like "n.name = 'Alice', n.age = 30" // and updates the node's properties accordingly. // // # Parameters // // - node: The node to update // - varName: The variable name used in the SET clause (e.g., "n") // - setClause: The SET clause string (without "SET" keyword) // // # Example // // applySetToNode(node, "n", "n.name = 'Alice', n.age = 30") // // node.Properties["name"] = "Alice" // // node.Properties["age"] = int64(30) func (e *StorageExecutor) applySetToNode(node *storage.Node, varName string, setClause string) { // Split SET clause into individual assignments, respecting parentheses and quotes assignments := e.splitSetAssignments(setClause) for _, assignment := range assignments { assignment = strings.TrimSpace(assignment) if !strings.HasPrefix(assignment, varName+".") { continue } eqIdx := strings.Index(assignment, "=") if eqIdx <= 0 { continue } propName := strings.TrimSpace(assignment[len(varName)+1 : eqIdx]) propValue := strings.TrimSpace(assignment[eqIdx+1:]) // Evaluate the expression and set the property setNodeProperty(node, propName, e.evaluateSetExpression(propValue)) } } // setNodeProperty sets a property on a node. // // Special handling for "embedding" property which goes to node.Embedding // instead of node.Properties. // // # Parameters // // - node: The node to update // - propName: The property name // - value: The value to set func setNodeProperty(node *storage.Node, propName string, value interface{}) { if propName == "embedding" { node.Embedding = toFloat32Slice(value) return } if node.Properties == nil { node.Properties = make(map[string]interface{}) } node.Properties[propName] = value } // splitSetAssignments splits a SET clause into individual assignments, // respecting parentheses and quotes. // // # Parameters // // - setClause: The SET clause string (without "SET" keyword) // // # Returns // // - Slice of individual assignments // // # Example // // splitSetAssignments("n.name = 'Alice', n.age = 30") // // Returns: ["n.name = 'Alice'", "n.age = 30"] // // splitSetAssignments("n.name = concat('a', 'b'), n.x = 1") // // Returns: ["n.name = concat('a', 'b')", "n.x = 1"] func (e *StorageExecutor) splitSetAssignments(setClause string) []string { var assignments []string var current strings.Builder parenDepth := 0 inQuote := false quoteChar := rune(0) for i, c := range setClause { switch { case c == '\'' || c == '"': if !inQuote { inQuote = true quoteChar = c } else if c == quoteChar { // Check for escaped quote if i > 0 && setClause[i-1] != '\\' { inQuote = false } } current.WriteRune(c) case c == '(' && !inQuote: parenDepth++ current.WriteRune(c) case c == ')' && !inQuote: parenDepth-- current.WriteRune(c) case c == ',' && !inQuote && parenDepth == 0: if s := strings.TrimSpace(current.String()); s != "" { assignments = append(assignments, s) } current.Reset() default: current.WriteRune(c) } } // Add final assignment if s := strings.TrimSpace(current.String()); s != "" { assignments = append(assignments, s) } return assignments } // splitSetAssignmentsRespectingBrackets splits a SET clause into individual assignments, // respecting brackets (for arrays), parentheses, and quotes. // // # Parameters // // - setClause: The SET clause string // // # Returns // // - Slice of individual assignments // // # Example // // splitSetAssignmentsRespectingBrackets("n.embedding = [0.1, 0.2], n.dim = 4") // // Returns: ["n.embedding = [0.1, 0.2]", "n.dim = 4"] func (e *StorageExecutor) splitSetAssignmentsRespectingBrackets(setClause string) []string { var assignments []string var current strings.Builder depth := 0 // Tracks both () and [] inQuote := false quoteChar := rune(0) for i, c := range setClause { switch { case c == '\'' || c == '"': if !inQuote { inQuote = true quoteChar = c } else if c == quoteChar { // Check for escaped quote if i > 0 && setClause[i-1] != '\\' { inQuote = false } } current.WriteRune(c) case (c == '(' || c == '[') && !inQuote: depth++ current.WriteRune(c) case (c == ')' || c == ']') && !inQuote: depth-- current.WriteRune(c) case c == ',' && !inQuote && depth == 0: if s := strings.TrimSpace(current.String()); s != "" { assignments = append(assignments, s) } current.Reset() default: current.WriteRune(c) } } // Add final assignment if s := strings.TrimSpace(current.String()); s != "" { assignments = append(assignments, s) } return assignments } // evaluateSetExpression evaluates a Cypher expression for SET clauses. // // Handles various expression types including literals, arrays, function calls, // and string concatenation. // // # Parameters // // - expr: The expression string to evaluate // // # Returns // // - The evaluated value // // # Example // // evaluateSetExpression("'Alice'") // "Alice" // evaluateSetExpression("42") // int64(42) // evaluateSetExpression("true") // true // evaluateSetExpression("[1, 2, 3]") // []interface{}{1, 2, 3} // evaluateSetExpression("timestamp()") // current timestamp func (e *StorageExecutor) evaluateSetExpression(expr string) interface{} { expr = strings.TrimSpace(expr) // Handle null if strings.EqualFold(expr, "null") { return nil } // Handle simple literals if strings.HasPrefix(expr, "'") && strings.HasSuffix(expr, "'") { return expr[1 : len(expr)-1] } if strings.HasPrefix(expr, "\"") && strings.HasSuffix(expr, "\"") { return expr[1 : len(expr)-1] } if expr == "true" { return true } if expr == "false" { return false } // Handle numbers if val, err := strconv.ParseInt(expr, 10, 64); err == nil { return val } if val, err := strconv.ParseFloat(expr, 64); err == nil { return val } // Handle arrays (simplified) if strings.HasPrefix(expr, "[") && strings.HasSuffix(expr, "]") { inner := strings.TrimSpace(expr[1 : len(expr)-1]) if inner == "" { return []interface{}{} } // Simple split for basic arrays parts := strings.Split(inner, ",") result := make([]interface{}, len(parts)) for i, p := range parts { result[i] = e.evaluateSetExpression(strings.TrimSpace(p)) } return result } // Handle function calls and expressions lowerExpr := strings.ToLower(expr) // timestamp() - returns current timestamp if lowerExpr == "timestamp()" { return e.idCounter() } // datetime() - returns ISO date string if lowerExpr == "datetime()" { return fmt.Sprintf("%d", e.idCounter()) } // randomUUID() or randomuuid() if lowerExpr == "randomuuid()" { return e.generateUUID() } // Handle string concatenation: 'prefix-' + toString(timestamp()) + '-' + substring(randomUUID(), 0, 8) if strings.Contains(expr, " + ") { return e.evaluateStringConcat(expr) } // Handle toString(expr) if matchFuncStartAndSuffix(expr, "tostring") { inner := extractFuncArgs(expr, "tostring") val := e.evaluateSetExpression(inner) return fmt.Sprintf("%v", val) } // Handle substring(str, start, length) if matchFuncStartAndSuffix(expr, "substring") { return e.evaluateSubstringForSet(expr) } // If nothing else matched, return as-is (already substituted parameter value) return expr } // evaluateStringConcat handles string concatenation with + // // # Parameters // // - expr: The expression with + operators // // # Returns // // - The concatenated string // // # Example // // evaluateStringConcat("'Hello' + ' ' + 'World'") // // Returns: "Hello World" func (e *StorageExecutor) evaluateStringConcat(expr string) string { var result strings.Builder // Split by + but respect quotes and parentheses parts := e.splitByPlus(expr) for _, part := range parts { part = strings.TrimSpace(part) val := e.evaluateSetExpression(part) result.WriteString(fmt.Sprintf("%v", val)) } return result.String() } // hasConcatOperator checks if the expression has a + operator outside of quotes. // This prevents infinite recursion when property values contain " + " in text. // // # Parameters // // - expr: The expression to check // // # Returns // // - true if + operator exists at top level func (e *StorageExecutor) hasConcatOperator(expr string) bool { inQuote := false quoteChar := rune(0) parenDepth := 0 for i := 0; i < len(expr); 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 && parenDepth == 0: // Check for space before and after (to avoid matching ++ or += etc) hasBefore := i > 0 && expr[i-1] == ' ' hasAfter := i < len(expr)-1 && expr[i+1] == ' ' if hasBefore && hasAfter { return true } } } return false } // splitByPlus splits an expression by + operator, respecting quotes and parentheses. // // # Parameters // // - expr: The expression to split // // # Returns // // - Slice of parts separated by + func (e *StorageExecutor) splitByPlus(expr string) []string { var parts []string var current strings.Builder parenDepth := 0 inQuote := false quoteChar := rune(0) for i := 0; i < len(expr); i++ { c := rune(expr[i]) switch { case c == '\'' || c == '"': if !inQuote { inQuote = true quoteChar = c } else if c == quoteChar { inQuote = false } current.WriteRune(c) case c == '(' && !inQuote: parenDepth++ current.WriteRune(c) case c == ')' && !inQuote: parenDepth-- current.WriteRune(c) case c == '+' && !inQuote && parenDepth == 0: if s := strings.TrimSpace(current.String()); s != "" { parts = append(parts, s) } current.Reset() default: current.WriteRune(c) } } if s := strings.TrimSpace(current.String()); s != "" { parts = append(parts, s) } return parts } // evaluateSubstringForSet handles substring(str, start, length) for SET expressions. // // # Parameters // // - expr: The substring function call // // # Returns // // - The extracted substring func (e *StorageExecutor) evaluateSubstringForSet(expr string) string { // Extract arguments from substring(str, start, length) inner := expr[10 : len(expr)-1] // Remove "substring(" and ")" // Split by comma, respecting parentheses args := e.splitFunctionArgs(inner) if len(args) < 2 { return "" } // Evaluate the string argument str := fmt.Sprintf("%v", e.evaluateSetExpression(args[0])) // Parse start start, err := strconv.Atoi(strings.TrimSpace(args[1])) if err != nil { start = 0 } // Parse optional length length := len(str) - start if len(args) >= 3 { if l, err := strconv.Atoi(strings.TrimSpace(args[2])); err == nil { length = l } } // Apply substring if start >= len(str) { return "" } end := start + length if end > len(str) { end = len(str) } return str[start:end] } // splitFunctionArgs splits function arguments by comma, respecting parentheses and quotes. // // This function intelligently parses function arguments, handling: // - Nested parentheses: function(a, func(b, c), d) // - Quoted strings: "hello, world" treated as single argument // - Escape sequences: \"escaped quote\" within strings // - Mixed quotes: 'single' and "double" quotes // // # Parameters // // - args: The argument string to split (without outer parentheses) // // # Returns // // - Slice of individual arguments, trimmed of whitespace // // # Example // // splitFunctionArgs("name, age, email") // // Returns: ["name", "age", "email"] // // splitFunctionArgs("toLower(n.name), toUpper(n.city)") // // Returns: ["toLower(n.name)", "toUpper(n.city)"] // // # ELI12 // // Imagine you're reading a sentence: "I like pizza, pasta, and ice cream" // You need to split it by commas, but what if someone says: // "I like pizza, 'pasta, with sauce', and ice cream" // // You can't just split at every comma! The comma inside the quotes is PART of // the item, not a separator. This function is smart enough to know: // - Commas outside quotes = split here // - Commas inside quotes = keep them // - Parentheses = keep everything inside together func (e *StorageExecutor) splitFunctionArgs(args string) []string { var result []string var current strings.Builder parenDepth := 0 bracketDepth := 0 inSingleQuote := false inDoubleQuote := false for i := 0; i < len(args); i++ { c := args[i] switch c { case '\'': if !inDoubleQuote { // Check for escape sequence if i > 0 && args[i-1] == '\\' { current.WriteByte(c) } else { inSingleQuote = !inSingleQuote current.WriteByte(c) } } else { current.WriteByte(c) } case '"': if !inSingleQuote { // Check for escape sequence if i > 0 && args[i-1] == '\\' { current.WriteByte(c) } else { inDoubleQuote = !inDoubleQuote current.WriteByte(c) } } else { current.WriteByte(c) } case '(': if !inSingleQuote && !inDoubleQuote { parenDepth++ } current.WriteByte(c) case ')': if !inSingleQuote && !inDoubleQuote { parenDepth-- } current.WriteByte(c) case '[': if !inSingleQuote && !inDoubleQuote { bracketDepth++ } current.WriteByte(c) case ']': if !inSingleQuote && !inDoubleQuote { bracketDepth-- } current.WriteByte(c) case ',': if parenDepth == 0 && bracketDepth == 0 && !inSingleQuote && !inDoubleQuote { result = append(result, strings.TrimSpace(current.String())) current.Reset() } else { current.WriteByte(c) } default: current.WriteByte(c) } } if s := strings.TrimSpace(current.String()); s != "" { result = append(result, s) } return result } // generateUUID generates a simple UUID-like string. // // Uses crypto/rand for cryptographically secure random bytes. // // # Returns // // - A UUID-formatted string func (e *StorageExecutor) generateUUID() string { // Use crypto/rand for proper UUID b := make([]byte, 16) _, _ = rand.Read(b) return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) }

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