Skip to main content
Glama
orneryd

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

by orneryd
apoc_collections.go22.7 kB
// APOC collection functions for NornicDB Cypher. // // This file contains implementations of APOC (Awesome Procedures on Cypher) // collection manipulation functions. These provide enhanced list processing // capabilities beyond standard Cypher. // // # Available Functions // // List Manipulation: // - apoc.coll.flatten - Flatten nested lists // - apoc.coll.toSet - Remove duplicates preserving order // - apoc.coll.sort - Sort list ascending // - apoc.coll.sortNodes - Sort nodes by property // - apoc.coll.reverse - Reverse list order // // Set Operations: // - apoc.coll.union - Union of two lists (unique) // - apoc.coll.unionAll - Union of two lists (with duplicates) // - apoc.coll.intersection - Common elements // - apoc.coll.subtract - Difference of two lists // // Aggregations: // - apoc.coll.sum - Sum numeric values // - apoc.coll.avg - Average of numeric values // - apoc.coll.min - Minimum value // - apoc.coll.max - Maximum value // // Search & Analysis: // - apoc.coll.contains - Check if list contains value // - apoc.coll.containsAll - Check if list contains all values // - apoc.coll.containsAny - Check if list contains any value // - apoc.coll.indexOf - Find index of value // - apoc.coll.frequencies - Count occurrences of each value // - apoc.coll.occurrences - Count occurrences of specific value // // Transformation: // - apoc.coll.split - Split list by separator // - apoc.coll.partition - Split into fixed-size chunks // - apoc.coll.pairs - Create [value, nextValue] pairs // - apoc.coll.zip - Combine two lists into pairs // // Map Functions: // - apoc.map.merge - Merge two maps // - apoc.map.fromPairs - Create map from [key, value] pairs // - apoc.map.fromLists - Create map from parallel key/value lists // // # ELI12 // // APOC collection functions are like special tools for working with lists: // // - flatten: Like unstacking all the boxes-within-boxes to see everything // - toSet: Like removing duplicate trading cards from your collection // - union: Like combining two friend groups (no duplicates) // - intersection: Like finding friends you have in common // - frequencies: Like counting how many of each candy type you have // // They make it easy to work with groups of things in your database! // // # Neo4j Compatibility // // These functions match Neo4j APOC library behavior for compatibility // with existing queries and applications. package cypher import ( "fmt" "github.com/orneryd/nornicdb/pkg/storage" ) // ======================================== // List Utility Functions // ======================================== // flattenList recursively flattens nested lists into a single list. // // This function recursively processes nested lists to produce a single-level list. // // # Parameters // // - val: A list that may contain nested lists // // # Returns // // - A single-level list with all elements // // # Example // // flattenList([[1, 2], [3, [4, 5]]]) // [1, 2, 3, 4, 5] // flattenList([1, 2, 3]) // [1, 2, 3] func flattenList(val interface{}) []interface{} { var result []interface{} switch v := val.(type) { case []interface{}: for _, item := range v { // Check if item is also a list switch inner := item.(type) { case []interface{}: result = append(result, flattenList(inner)...) default: result = append(result, item) } } case []string: for _, s := range v { result = append(result, s) } default: result = append(result, val) } return result } // toSet removes duplicates from a list while preserving order. // // Uses a map for O(1) lookup to track seen elements. // // # Parameters // // - val: A list that may contain duplicates // // # Returns // // - A list with duplicates removed, preserving first occurrence order // // # Example // // toSet([1, 2, 2, 3, 1]) // [1, 2, 3] // toSet(["a", "b", "a"]) // ["a", "b"] func toSet(val interface{}) []interface{} { var result []interface{} seen := make(map[string]bool) addUnique := func(item interface{}) { key := fmt.Sprintf("%T:%v", item, item) if !seen[key] { seen[key] = true result = append(result, item) } } switch v := val.(type) { case []interface{}: for _, item := range v { addUnique(item) } case []string: for _, s := range v { addUnique(s) } } return result } // ======================================== // APOC Collection Aggregations // ======================================== // apocCollSum sums numeric values in a list. // // Non-numeric values are ignored. // // # Parameters // // - val: A list of values (numeric values summed) // // # Returns // // - The sum as float64 // // # Example // // apocCollSum([1, 2, 3, 4]) // 10.0 // apocCollSum([1.5, 2.5]) // 4.0 func apocCollSum(val interface{}) float64 { var sum float64 switch v := val.(type) { case []interface{}: for _, item := range v { if f, ok := toFloat64(item); ok { sum += f } } } return sum } // apocCollAvg calculates average of numeric values in a list. // // # Parameters // // - val: A list of numeric values // // # Returns // // - The average as float64, or nil if empty // // # Example // // apocCollAvg([1, 2, 3, 4]) // 2.5 // apocCollAvg([]) // nil func apocCollAvg(val interface{}) interface{} { switch v := val.(type) { case []interface{}: if len(v) == 0 { return nil } var sum float64 for _, item := range v { if f, ok := toFloat64(item); ok { sum += f } } return sum / float64(len(v)) } return nil } // apocCollMin finds minimum value in a list. // // # Parameters // // - val: A list of comparable values // // # Returns // // - The minimum value, or nil if empty // // # Example // // apocCollMin([3, 1, 4, 1, 5]) // 1 // apocCollMin([]) // nil func apocCollMin(val interface{}) interface{} { switch v := val.(type) { case []interface{}: if len(v) == 0 { return nil } min := v[0] minFloat, _ := toFloat64(min) for _, item := range v[1:] { if f, ok := toFloat64(item); ok && f < minFloat { min = item minFloat = f } } return min } return nil } // apocCollMax finds maximum value in a list. // // # Parameters // // - val: A list of comparable values // // # Returns // // - The maximum value, or nil if empty // // # Example // // apocCollMax([3, 1, 4, 1, 5]) // 5 // apocCollMax([]) // nil func apocCollMax(val interface{}) interface{} { switch v := val.(type) { case []interface{}: if len(v) == 0 { return nil } max := v[0] maxFloat, _ := toFloat64(max) for _, item := range v[1:] { if f, ok := toFloat64(item); ok && f > maxFloat { max = item maxFloat = f } } return max } return nil } // ======================================== // APOC Collection Sorting // ======================================== // apocCollSort sorts a list in ascending order. // // Uses bubble sort for simplicity; values are compared as float64. // // # Parameters // // - val: A list to sort // // # Returns // // - A new sorted list (does not modify original) // // # Example // // apocCollSort([3, 1, 4, 1, 5]) // [1, 1, 3, 4, 5] func apocCollSort(val interface{}) []interface{} { switch v := val.(type) { case []interface{}: result := make([]interface{}, len(v)) copy(result, v) // Sort by converting to float64 for comparison for i := 0; i < len(result)-1; i++ { for j := i + 1; j < len(result); j++ { fj, _ := toFloat64(result[j]) fi, _ := toFloat64(result[i]) if fj < fi { result[i], result[j] = result[j], result[i] } } } return result } return nil } // apocCollSortNodes sorts nodes by a property value. // // # Parameters // // - val: A list of nodes // - propName: The property to sort by // // # Returns // // - A new list of nodes sorted by the property // // # Example // // apocCollSortNodes(nodes, "age") // Nodes sorted by age property func apocCollSortNodes(val interface{}, propName string) []interface{} { switch v := val.(type) { case []interface{}: result := make([]interface{}, len(v)) copy(result, v) // Sort by property value for i := 0; i < len(result)-1; i++ { for j := i + 1; j < len(result); j++ { vi := getNodeProperty(result[i], propName) vj := getNodeProperty(result[j], propName) fj, _ := toFloat64(vj) fi, _ := toFloat64(vi) if fj < fi { result[i], result[j] = result[j], result[i] } } } return result } return nil } // getNodeProperty extracts a property from a node (map or *storage.Node). // // # Parameters // // - node: A node (map or *storage.Node) // - propName: The property name to extract // // # Returns // // - The property value, or nil if not found func getNodeProperty(node interface{}, propName string) interface{} { switch n := node.(type) { case map[string]interface{}: if props, ok := n["properties"].(map[string]interface{}); ok { return props[propName] } return n[propName] case *storage.Node: return n.Properties[propName] } return nil } // apocCollReverse reverses a list. // // # Parameters // // - val: A list to reverse // // # Returns // // - A new list in reverse order // // # Example // // apocCollReverse([1, 2, 3]) // [3, 2, 1] func apocCollReverse(val interface{}) []interface{} { switch v := val.(type) { case []interface{}: result := make([]interface{}, len(v)) for i, item := range v { result[len(v)-1-i] = item } return result } return nil } // ======================================== // APOC Set Operations // ======================================== // apocCollUnion returns union of two lists (unique elements). // // # Parameters // // - list1: First list // - list2: Second list // // # Returns // // - A list with all unique elements from both lists // // # Example // // apocCollUnion([1, 2], [2, 3]) // [1, 2, 3] func apocCollUnion(list1, list2 interface{}) []interface{} { combined := apocCollUnionAll(list1, list2) return toSet(combined) } // apocCollUnionAll returns union of two lists (including duplicates). // // # Parameters // // - list1: First list // - list2: Second list // // # Returns // // - A list with all elements from both lists // // # Example // // apocCollUnionAll([1, 2], [2, 3]) // [1, 2, 2, 3] func apocCollUnionAll(list1, list2 interface{}) []interface{} { var result []interface{} if l1, ok := list1.([]interface{}); ok { result = append(result, l1...) } if l2, ok := list2.([]interface{}); ok { result = append(result, l2...) } return result } // apocCollIntersection returns common elements of two lists. // // # Parameters // // - list1: First list // - list2: Second list // // # Returns // // - A list with elements present in both lists // // # Example // // apocCollIntersection([1, 2, 3], [2, 3, 4]) // [2, 3] func apocCollIntersection(list1, list2 interface{}) []interface{} { var result []interface{} l1, ok1 := list1.([]interface{}) l2, ok2 := list2.([]interface{}) if !ok1 || !ok2 { return result } // Create lookup set from list2 set2 := make(map[string]bool) for _, item := range l2 { key := fmt.Sprintf("%T:%v", item, item) set2[key] = true } // Find elements in list1 that are also in list2 seen := make(map[string]bool) for _, item := range l1 { key := fmt.Sprintf("%T:%v", item, item) if set2[key] && !seen[key] { result = append(result, item) seen[key] = true } } return result } // apocCollSubtract returns list1 minus elements in list2. // // # Parameters // // - list1: List to subtract from // - list2: List of elements to remove // // # Returns // // - Elements in list1 that are not in list2 // // # Example // // apocCollSubtract([1, 2, 3], [2]) // [1, 3] func apocCollSubtract(list1, list2 interface{}) []interface{} { var result []interface{} l1, ok1 := list1.([]interface{}) l2, ok2 := list2.([]interface{}) if !ok1 || !ok2 { return result } // Create lookup set from list2 set2 := make(map[string]bool) for _, item := range l2 { key := fmt.Sprintf("%T:%v", item, item) set2[key] = true } // Find elements in list1 that are NOT in list2 for _, item := range l1 { key := fmt.Sprintf("%T:%v", item, item) if !set2[key] { result = append(result, item) } } return result } // ======================================== // APOC Search Functions // ======================================== // apocCollContains checks if list contains a value. // // # Parameters // // - listVal: The list to search // - value: The value to find // // # Returns // // - true if value is in list // // # Example // // apocCollContains([1, 2, 3], 2) // true // apocCollContains([1, 2, 3], 5) // false func apocCollContains(listVal, value interface{}) bool { list, ok := listVal.([]interface{}) if !ok { return false } valKey := fmt.Sprintf("%T:%v", value, value) for _, item := range list { itemKey := fmt.Sprintf("%T:%v", item, item) if itemKey == valKey { return true } } return false } // apocCollContainsAll checks if list contains all values from another list. // // # Parameters // // - list1: The list to search in // - list2: The list of values to find // // # Returns // // - true if all values from list2 are in list1 // // # Example // // apocCollContainsAll([1, 2, 3, 4], [2, 3]) // true // apocCollContainsAll([1, 2, 3], [2, 5]) // false func apocCollContainsAll(list1, list2 interface{}) bool { l1, ok1 := list1.([]interface{}) l2, ok2 := list2.([]interface{}) if !ok1 || !ok2 { return false } // Create lookup set from list1 set1 := make(map[string]bool) for _, item := range l1 { key := fmt.Sprintf("%T:%v", item, item) set1[key] = true } // Check all items in list2 are in set1 for _, item := range l2 { key := fmt.Sprintf("%T:%v", item, item) if !set1[key] { return false } } return true } // apocCollContainsAny checks if list contains any value from another list. // // # Parameters // // - list1: The list to search in // - list2: The list of values to find // // # Returns // // - true if any value from list2 is in list1 // // # Example // // apocCollContainsAny([1, 2, 3], [5, 2]) // true // apocCollContainsAny([1, 2, 3], [5, 6]) // false func apocCollContainsAny(list1, list2 interface{}) bool { l1, ok1 := list1.([]interface{}) l2, ok2 := list2.([]interface{}) if !ok1 || !ok2 { return false } // Create lookup set from list1 set1 := make(map[string]bool) for _, item := range l1 { key := fmt.Sprintf("%T:%v", item, item) set1[key] = true } // Check if any item in list2 is in set1 for _, item := range l2 { key := fmt.Sprintf("%T:%v", item, item) if set1[key] { return true } } return false } // apocCollIndexOf finds the index of a value in a list. // // # Parameters // // - listVal: The list to search // - value: The value to find // // # Returns // // - The zero-based index, or -1 if not found // // # Example // // apocCollIndexOf([1, 2, 3], 2) // 1 // apocCollIndexOf([1, 2, 3], 5) // -1 func apocCollIndexOf(listVal, value interface{}) int64 { list, ok := listVal.([]interface{}) if !ok { return -1 } valKey := fmt.Sprintf("%T:%v", value, value) for i, item := range list { itemKey := fmt.Sprintf("%T:%v", item, item) if itemKey == valKey { return int64(i) } } return -1 } // ======================================== // APOC Transformation Functions // ======================================== // apocCollSplit splits a list by a separator value. // // # Parameters // // - listVal: The list to split // - value: The separator value // // # Returns // // - A list of sublists split by the separator // // # Example // // apocCollSplit([1, 0, 2, 0, 3], 0) // [[1], [2], [3]] func apocCollSplit(listVal, value interface{}) []interface{} { list, ok := listVal.([]interface{}) if !ok { return nil } var result []interface{} var current []interface{} valKey := fmt.Sprintf("%T:%v", value, value) for _, item := range list { itemKey := fmt.Sprintf("%T:%v", item, item) if itemKey == valKey { if len(current) > 0 { result = append(result, current) current = []interface{}{} } } else { current = append(current, item) } } if len(current) > 0 { result = append(result, current) } return result } // apocCollPartition splits a list into fixed-size chunks. // // # Parameters // // - listVal: The list to partition // - sizeVal: The size of each partition // // # Returns // // - A list of sublists of the specified size // // # Example // // apocCollPartition([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]] func apocCollPartition(listVal, sizeVal interface{}) []interface{} { list, ok := listVal.([]interface{}) if !ok { return nil } size := toInt64(sizeVal) if size <= 0 { return nil } var result []interface{} for i := int64(0); i < int64(len(list)); i += size { end := i + size if end > int64(len(list)) { end = int64(len(list)) } result = append(result, list[i:end]) } return result } // apocCollPairs creates [value, nextValue] pairs from a list. // // # Parameters // // - val: The list to process // // # Returns // // - A list of [current, next] pairs, last element paired with nil // // # Example // // apocCollPairs([1, 2, 3]) // [[1, 2], [2, 3], [3, nil]] func apocCollPairs(val interface{}) []interface{} { list, ok := val.([]interface{}) if !ok { return nil } var result []interface{} for i := 0; i < len(list); i++ { var next interface{} if i+1 < len(list) { next = list[i+1] } result = append(result, []interface{}{list[i], next}) } return result } // apocCollZip combines two lists into pairs. // // # Parameters // // - list1: First list // - list2: Second list // // # Returns // // - A list of [list1[i], list2[i]] pairs // // # Example // // apocCollZip([1, 2], ["a", "b"]) // [[1, "a"], [2, "b"]] func apocCollZip(list1, list2 interface{}) []interface{} { l1, ok1 := list1.([]interface{}) l2, ok2 := list2.([]interface{}) if !ok1 || !ok2 { return nil } // Use length of shorter list length := len(l1) if len(l2) < length { length = len(l2) } result := make([]interface{}, length) for i := 0; i < length; i++ { result[i] = []interface{}{l1[i], l2[i]} } return result } // ======================================== // APOC Analysis Functions // ======================================== // apocCollFrequencies counts occurrences of each value. // // # Parameters // // - val: The list to analyze // // # Returns // // - A map of value -> count // // # Example // // apocCollFrequencies([1, 2, 2, 3, 3, 3]) // {1: 1, 2: 2, 3: 3} func apocCollFrequencies(val interface{}) map[string]interface{} { result := make(map[string]interface{}) list, ok := val.([]interface{}) if !ok { return result } counts := make(map[string]int64) for _, item := range list { key := fmt.Sprintf("%v", item) counts[key]++ } for k, v := range counts { result[k] = v } return result } // apocCollOccurrences counts occurrences of a specific value. // // # Parameters // // - listVal: The list to search // - value: The value to count // // # Returns // // - The count of occurrences // // # Example // // apocCollOccurrences([1, 2, 2, 3], 2) // 2 // apocCollOccurrences([1, 2, 3], 5) // 0 func apocCollOccurrences(listVal, value interface{}) int64 { list, ok := listVal.([]interface{}) if !ok { return 0 } valKey := fmt.Sprintf("%T:%v", value, value) var count int64 for _, item := range list { itemKey := fmt.Sprintf("%T:%v", item, item) if itemKey == valKey { count++ } } return count } // ======================================== // Type and Map Functions // ======================================== // getCypherType returns the Cypher type name for a value. // // # Parameters // // - val: The value to check // // # Returns // // - The Cypher type name as a string // // # Example // // getCypherType(42) // "INTEGER" // getCypherType(3.14) // "FLOAT" // getCypherType("hello") // "STRING" // getCypherType(nil) // "NULL" func getCypherType(val interface{}) string { if val == nil { return "NULL" } switch v := val.(type) { case bool: return "BOOLEAN" case int, int32, int64: return "INTEGER" case float32, float64: return "FLOAT" case string: return "STRING" case []interface{}, []string: return "LIST" case map[string]interface{}: return "MAP" case *storage.Node: return "NODE" case *storage.Edge: return "RELATIONSHIP" case *CypherDuration: return "DURATION" default: _ = v return "ANY" } } // mergeMaps merges two maps, with map2 values overriding map1. // // # Parameters // // - map1: The base map // - map2: The map to merge (overrides map1) // // # Returns // // - A new map with merged values // // # Example // // mergeMaps({a: 1, b: 2}, {b: 3, c: 4}) // {a: 1, b: 3, c: 4} func mergeMaps(map1, map2 interface{}) map[string]interface{} { result := make(map[string]interface{}) // Copy map1 if m1, ok := map1.(map[string]interface{}); ok { for k, v := range m1 { result[k] = v } } // Override with map2 if m2, ok := map2.(map[string]interface{}); ok { for k, v := range m2 { result[k] = v } } return result } // fromPairs creates a map from a list of [key, value] pairs. // // # Parameters // // - val: A list of [key, value] pairs // // # Returns // // - A map constructed from the pairs // // # Example // // fromPairs([["name", "Alice"], ["age", 30]]) // {name: "Alice", age: 30} func fromPairs(val interface{}) map[string]interface{} { result := make(map[string]interface{}) list, ok := val.([]interface{}) if !ok { return result } for _, item := range list { pair, ok := item.([]interface{}) if !ok || len(pair) < 2 { continue } key, ok := pair[0].(string) if !ok { key = fmt.Sprintf("%v", pair[0]) } result[key] = pair[1] } return result } // fromLists creates a map from parallel lists of keys and values. // // # Parameters // // - keys: List of keys (strings) // - values: List of corresponding values // // # Returns // // - A map where keys[i] maps to values[i] // // # Example // // fromLists(["name", "age"], ["Alice", 30]) // {name: "Alice", age: 30} func fromLists(keys, values interface{}) map[string]interface{} { result := make(map[string]interface{}) keyList, ok1 := keys.([]interface{}) valList, ok2 := values.([]interface{}) if !ok1 || !ok2 { return result } for i := 0; i < len(keyList) && i < len(valList); i++ { key, ok := keyList[i].(string) if !ok { key = fmt.Sprintf("%v", keyList[i]) } result[key] = valList[i] } return result }

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