// Comprehensive unit tests for all Cypher functions in NornicDB.
// This file provides complete coverage for all scalar, aggregation, list, string,
// math, date/time, spatial, and vector functions.
package cypher
import (
"math"
"strings"
"testing"
"github.com/orneryd/nornicdb/pkg/storage"
)
// ========================================
// Test Helper Functions
// ========================================
func setupTestExecutor(t *testing.T) *StorageExecutor {
store := storage.NewMemoryEngine()
return NewStorageExecutor(store)
}
func createTestNode(t *testing.T, e *StorageExecutor, id string, labels []string, props map[string]interface{}) *storage.Node {
node := &storage.Node{
ID: storage.NodeID(id),
Labels: labels,
Properties: props,
}
if err := e.storage.CreateNode(node); err != nil {
t.Fatalf("Failed to create test node: %v", err)
}
return node
}
// ========================================
// Scalar Functions Tests
// ========================================
func TestFunctionId(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{"name": "Alice"})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("id(n)", nodes, nil)
if result != "node-1" {
t.Errorf("id(n) = %v, want node-1", result)
}
}
func TestFunctionElementId(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{"name": "Alice"})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("elementId(n)", nodes, nil)
expected := "4:nornicdb:node-1"
if result != expected {
t.Errorf("elementId(n) = %v, want %v", result, expected)
}
}
func TestFunctionLabels(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person", "Employee"}, nil)
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("labels(n)", nodes, nil)
labels, ok := result.([]interface{})
if !ok {
t.Fatalf("labels(n) should return []interface{}, got %T", result)
}
if len(labels) != 2 {
t.Errorf("labels(n) should return 2 labels, got %d", len(labels))
}
}
func TestFunctionType(t *testing.T) {
e := setupTestExecutor(t)
rel := &storage.Edge{
ID: "rel-1",
Type: "KNOWS",
}
rels := map[string]*storage.Edge{"r": rel}
result := e.evaluateExpressionWithContext("type(r)", nil, rels)
if result != "KNOWS" {
t.Errorf("type(r) = %v, want KNOWS", result)
}
}
func TestFunctionKeys(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"name": "Alice",
"age": 30,
})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("keys(n)", nodes, nil)
keys, ok := result.([]interface{})
if !ok {
t.Fatalf("keys(n) should return []interface{}, got %T", result)
}
if len(keys) != 2 {
t.Errorf("keys(n) should return 2 keys, got %d", len(keys))
}
}
func TestFunctionProperties(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"name": "Alice",
"age": 30,
})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("properties(n)", nodes, nil)
props, ok := result.(map[string]interface{})
if !ok {
t.Fatalf("properties(n) should return map, got %T", result)
}
if props["name"] != "Alice" {
t.Errorf("properties(n)['name'] = %v, want Alice", props["name"])
}
}
func TestFunctionCoalesce(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
{"coalesce(null, 'default')", "default"},
{"coalesce('first', 'second')", "first"},
{"coalesce(null, null, 'third')", "third"},
{"coalesce(1, 2)", int64(1)},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
func TestFunctionExists(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"name": "Alice",
})
nodes := map[string]*storage.Node{"n": node}
tests := []struct {
expr string
expected bool
}{
{"exists(n.name)", true},
{"exists(n.missing)", false},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nodes, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// String Functions Tests
// ========================================
func TestStringFunctions(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
// Case conversion
{"toLower('HELLO')", "hello"},
{"toUpper('hello')", "HELLO"},
{"lower('WORLD')", "world"},
{"upper('world')", "WORLD"},
// Trimming
{"trim(' hello ')", "hello"},
{"ltrim(' hello')", "hello"},
{"rtrim('hello ')", "hello"},
{"btrim('xxhelloxx', 'x')", "hello"},
// String manipulation
{"replace('hello', 'l', 'L')", "heLLo"},
{"reverse('hello')", "olleh"},
{"left('hello', 2)", "he"},
{"right('hello', 2)", "lo"},
{"substring('hello', 1, 3)", "ell"},
// Split
{"toString(123)", "123"},
{"toString(true)", "true"},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
func TestSplitFunction(t *testing.T) {
e := setupTestExecutor(t)
// Create a node with property for split test
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"data": "a,b,c",
"sep": ",",
})
nodes := map[string]*storage.Node{"n": node}
// Test with node properties
result := e.evaluateExpressionWithContext("split(n.data, n.sep)", nodes, nil)
list, ok := result.([]interface{})
if !ok {
t.Fatalf("split should return list, got %T", result)
}
if len(list) != 3 {
t.Errorf("split(n.data, n.sep) returned %d elements, want 3", len(list))
}
}
func TestCharLengthFunctions(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected int64
}{
{"char_length('hello')", 5},
{"character_length('世界')", 2}, // Unicode characters
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// List Functions Tests
// ========================================
func TestListFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Test head
result := e.evaluateExpressionWithContext("head([1, 2, 3])", nil, nil)
if result != int64(1) {
t.Errorf("head([1,2,3]) = %v, want 1", result)
}
// Test last
result = e.evaluateExpressionWithContext("last([1, 2, 3])", nil, nil)
if result != int64(3) {
t.Errorf("last([1,2,3]) = %v, want 3", result)
}
// Test tail
tail := e.evaluateExpressionWithContext("tail([1, 2, 3])", nil, nil)
tailList, ok := tail.([]interface{})
if !ok || len(tailList) != 2 {
t.Errorf("tail([1,2,3]) = %v, want [2,3]", tail)
}
// Test reverse
rev := e.evaluateExpressionWithContext("reverse([1, 2, 3])", nil, nil)
revList, ok := rev.([]interface{})
if !ok || len(revList) != 3 {
t.Errorf("reverse([1,2,3]) failed")
}
}
func TestRangeFunction(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected int // length of result
}{
{"range(0, 5)", 6}, // 0,1,2,3,4,5
{"range(0, 10, 2)", 6}, // 0,2,4,6,8,10
{"range(5, 0, -1)", 6}, // 5,4,3,2,1,0
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
list, ok := result.([]interface{})
if !ok {
t.Fatalf("%s should return list", tt.expr)
}
if len(list) != tt.expected {
t.Errorf("%s returned %d elements, want %d", tt.expr, len(list), tt.expected)
}
})
}
}
func TestSizeFunction(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected int64
}{
{"size([1, 2, 3])", 3},
{"size('hello')", 5},
{"length([1, 2])", 2},
{"length('abc')", 3},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// Math Functions Tests
// ========================================
func TestMathFunctions(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
delta float64 // for float comparisons
}{
{"abs(-5)", int64(5), 0},
{"abs(5)", int64(5), 0},
{"ceil(4.2)", int64(5), 0},
{"floor(4.8)", int64(4), 0},
{"round(4.5)", int64(5), 0},
{"sign(10)", int64(1), 0},
{"sign(-10)", int64(-1), 0},
{"sign(0)", int64(0), 0},
{"sqrt(16)", float64(4), 0.001},
{"exp(0)", float64(1), 0.001},
{"log(1)", float64(0), 0.001},
{"log10(100)", float64(2), 0.001},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if tt.delta > 0 {
resultF, ok := result.(float64)
if !ok {
t.Fatalf("%s should return float64", tt.expr)
}
expectedF := tt.expected.(float64)
if math.Abs(resultF-expectedF) > tt.delta {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
} else {
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
}
})
}
}
func TestTrigFunctions(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected float64
delta float64
}{
{"sin(0)", 0, 0.001},
{"cos(0)", 1, 0.001},
{"tan(0)", 0, 0.001},
{"asin(0)", 0, 0.001},
{"acos(1)", 0, 0.001},
{"atan(0)", 0, 0.001},
{"atan2(0, 1)", 0, 0.001},
{"radians(180)", math.Pi, 0.001},
{"degrees(3.14159265)", 180, 0.1},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
resultF, ok := result.(float64)
if !ok {
t.Fatalf("%s should return float64, got %T", tt.expr, result)
}
if math.Abs(resultF-tt.expected) > tt.delta {
t.Errorf("%s = %v, want %v", tt.expr, resultF, tt.expected)
}
})
}
}
func TestHyperbolicFunctions(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected float64
delta float64
}{
{"sinh(0)", 0, 0.001},
{"sinh(1)", 1.1752011936438014, 0.001},
{"sinh(0.7)", 0.7585837018395334, 0.001},
{"cosh(0)", 1, 0.001},
{"cosh(1)", 1.5430806348152437, 0.001},
{"cosh(0.7)", 1.255169005630943, 0.001},
{"tanh(0)", 0, 0.001},
{"tanh(1)", 0.7615941559557649, 0.001},
{"tanh(0.7)", 0.6043677771171636, 0.001},
{"coth(1)", 1.3130352854993312, 0.001},
{"coth(0.7)", 1.6546216358026298, 0.001},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
resultF, ok := result.(float64)
if !ok {
t.Fatalf("%s should return float64, got %T", tt.expr, result)
}
if math.Abs(resultF-tt.expected) > tt.delta {
t.Errorf("%s = %v, want %v", tt.expr, resultF, tt.expected)
}
})
}
}
func TestPowerFunction(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected float64
delta float64
}{
{"power(2, 3)", 8, 0.001},
{"power(2, 10)", 1024, 0.001},
{"power(10, 2)", 100, 0.001},
{"power(4, 0.5)", 2, 0.001},
{"power(27, 0.333333)", 3, 0.01},
{"power(2, -1)", 0.5, 0.001},
{"power(0, 5)", 0, 0.001},
{"power(5, 0)", 1, 0.001},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
resultF, ok := result.(float64)
if !ok {
t.Fatalf("%s should return float64, got %T", tt.expr, result)
}
if math.Abs(resultF-tt.expected) > tt.delta {
t.Errorf("%s = %v, want %v", tt.expr, resultF, tt.expected)
}
})
}
}
func TestMathConstants(t *testing.T) {
e := setupTestExecutor(t)
// Test pi()
pi := e.evaluateExpressionWithContext("pi()", nil, nil)
piF, ok := pi.(float64)
if !ok || math.Abs(piF-math.Pi) > 0.0001 {
t.Errorf("pi() = %v, want %v", pi, math.Pi)
}
// Test e()
eVal := e.evaluateExpressionWithContext("e()", nil, nil)
eF, ok := eVal.(float64)
if !ok || math.Abs(eF-math.E) > 0.0001 {
t.Errorf("e() = %v, want %v", eVal, math.E)
}
}
func TestRandomFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Test rand() returns value between 0 and 1
result := e.evaluateExpressionWithContext("rand()", nil, nil)
randF, ok := result.(float64)
if !ok {
t.Fatalf("rand() should return float64")
}
if randF < 0 || randF > 1 {
t.Errorf("rand() = %v, should be between 0 and 1", randF)
}
// Test randomUUID() returns a string
uuid := e.evaluateExpressionWithContext("randomUUID()", nil, nil)
uuidStr, ok := uuid.(string)
if !ok {
t.Fatalf("randomUUID() should return string")
}
if len(uuidStr) < 32 {
t.Errorf("randomUUID() = %v, should be valid UUID format", uuidStr)
}
}
// ========================================
// Type Conversion Functions Tests
// ========================================
func TestTypeConversionFunctions(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
{"toInteger('123')", int64(123)},
{"toInteger(123.7)", int64(123)},
{"toInt('456')", int64(456)},
{"toFloat('3.14')", float64(3.14)},
{"toFloat(42)", float64(42)},
{"toBoolean('true')", true},
{"toBoolean('false')", false},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v (%T), want %v (%T)", tt.expr, result, result, tt.expected, tt.expected)
}
})
}
}
// ========================================
// Null Check Functions Tests
// ========================================
func TestNullCheckFunctions(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
{"isEmpty([])", true},
{"isEmpty([1])", false},
{"isEmpty('')", true},
{"isEmpty('a')", false},
{"isNaN(0)", false},
{"nullIf('a', 'a')", nil},
{"nullIf('a', 'b')", "a"},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// Timestamp Functions Tests
// ========================================
func TestTimestampFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Test timestamp() returns a value
ts := e.evaluateExpressionWithContext("timestamp()", nil, nil)
if ts == nil {
t.Error("timestamp() should return a value")
}
// Test datetime() returns a value
dt := e.evaluateExpressionWithContext("datetime()", nil, nil)
if dt == nil {
t.Error("datetime() should return a value")
}
// Test date()
d := e.evaluateExpressionWithContext("date()", nil, nil)
if d == nil {
t.Error("date() should return a value")
}
// Test time()
tm := e.evaluateExpressionWithContext("time()", nil, nil)
if tm == nil {
t.Error("time() should return a value")
}
}
// ========================================
// Relationship Functions Tests
// ========================================
func TestRelationshipFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Create nodes and relationship
node1 := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{"name": "Alice"})
node2 := createTestNode(t, e, "node-2", []string{"Person"}, map[string]interface{}{"name": "Bob"})
rel := &storage.Edge{
ID: "rel-1",
Type: "KNOWS",
StartNode: node1.ID,
EndNode: node2.ID,
Properties: map[string]interface{}{"since": 2020},
}
e.storage.CreateEdge(rel)
nodes := map[string]*storage.Node{"a": node1, "b": node2}
rels := map[string]*storage.Edge{"r": rel}
// Test startNode
result := e.evaluateExpressionWithContext("startNode(r)", nodes, rels)
if result == nil {
t.Error("startNode(r) should return a value")
}
// Test endNode
result = e.evaluateExpressionWithContext("endNode(r)", nodes, rels)
if result == nil {
t.Error("endNode(r) should return a value")
}
// Test type(r)
result = e.evaluateExpressionWithContext("type(r)", nodes, rels)
if result != "KNOWS" {
t.Errorf("type(r) = %v, want KNOWS", result)
}
}
// ========================================
// Vector Functions Tests
// ========================================
func TestVectorSimilarityFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Create nodes with vector embeddings
node1 := createTestNode(t, e, "node-1", []string{"Doc"}, map[string]interface{}{
"vec": []interface{}{float64(1), float64(0), float64(0)},
})
node2 := createTestNode(t, e, "node-2", []string{"Doc"}, map[string]interface{}{
"vec": []interface{}{float64(1), float64(0), float64(0)},
})
nodes := map[string]*storage.Node{"a": node1, "b": node2}
// Test cosine similarity of identical vectors = 1
result := e.evaluateExpressionWithContext("vector.similarity.cosine(a.vec, b.vec)", nodes, nil)
if result == nil {
t.Error("vector.similarity.cosine should return a value")
}
// Test euclidean similarity
result = e.evaluateExpressionWithContext("vector.similarity.euclidean(a.vec, b.vec)", nodes, nil)
if result == nil {
t.Error("vector.similarity.euclidean should return a value")
}
}
// ========================================
// Spatial Functions Tests
// ========================================
func TestSpatialFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Test point function
result := e.evaluateExpressionWithContext("point({x: 1.0, y: 2.0})", nil, nil)
pointMap, ok := result.(map[string]interface{})
if !ok {
t.Fatalf("point() should return map, got %T", result)
}
if pointMap["x"] != float64(1.0) {
t.Errorf("point().x = %v, want 1.0", pointMap["x"])
}
// Test distance function with x/y coordinates
node1 := createTestNode(t, e, "node-1", []string{"Location"}, map[string]interface{}{
"loc": map[string]interface{}{"x": float64(0), "y": float64(0)},
})
node2 := createTestNode(t, e, "node-2", []string{"Location"}, map[string]interface{}{
"loc": map[string]interface{}{"x": float64(3), "y": float64(4)},
})
nodes := map[string]*storage.Node{"a": node1, "b": node2}
dist := e.evaluateExpressionWithContext("distance(a.loc, b.loc)", nodes, nil)
distF, ok := dist.(float64)
if !ok {
t.Fatalf("distance() should return float64, got %T", dist)
}
// Distance from (0,0) to (3,4) should be 5
if math.Abs(distF-5.0) > 0.001 {
t.Errorf("distance(a.loc, b.loc) = %v, want 5.0", distF)
}
}
// ========================================
// Reduce Function Tests
// ========================================
func TestReduceFunction(t *testing.T) {
e := setupTestExecutor(t)
// reduce(acc = 0, x IN [1,2,3] | acc + x) should return 6
result := e.evaluateExpressionWithContext("reduce(acc = 0, x IN [1, 2, 3] | acc + x)", nil, nil)
if result == nil {
t.Error("reduce should return a value")
}
}
// ========================================
// Property Access Tests
// ========================================
func TestFunctionPropertyAccess(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"name": "Alice",
"age": int64(30),
"active": true,
"balance": float64(100.50),
})
nodes := map[string]*storage.Node{"n": node}
tests := []struct {
expr string
expected interface{}
}{
{"n.name", "Alice"},
{"n.age", int64(30)},
{"n.active", true},
{"n.balance", float64(100.50)},
{"n.missing", nil},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nodes, nil)
if result != tt.expected {
t.Errorf("%s = %v (%T), want %v (%T)", tt.expr, result, result, tt.expected, tt.expected)
}
})
}
}
// ========================================
// Literals Tests
// ========================================
func TestLiterals(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
{"null", nil},
{"true", true},
{"false", false},
{"123", int64(123)},
{"3.14", float64(3.14)},
{"'hello'", "hello"},
{`"world"`, "world"},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// String Concatenation Tests
// ========================================
func TestStringConcatenation(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"firstName": "John",
"lastName": "Doe",
})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("n.firstName + ' ' + n.lastName", nodes, nil)
if result != "John Doe" {
t.Errorf("concatenation = %v, want 'John Doe'", result)
}
}
// ========================================
// Count Function Tests
// ========================================
func TestCountFunction(t *testing.T) {
e := setupTestExecutor(t)
// count(*) in expression context should NOT be evaluated here
// Aggregation functions must be handled by executeAggregation() or executeMatchWithRelationships()
result := e.evaluateExpressionWithContext("count(*)", nil, nil)
if result != nil {
t.Errorf("count(*) in expression context should return nil, got %v", result)
}
// count(n) also should NOT be evaluated in expression context
node := createTestNode(t, e, "node-1", []string{"Person"}, nil)
nodes := map[string]*storage.Node{"n": node}
result = e.evaluateExpressionWithContext("count(n)", nodes, nil)
if result != nil {
t.Errorf("count(n) in expression context should return nil, got %v", result)
}
}
// ========================================
// Embedding Property Filter Tests
// ========================================
func TestEmbeddingPropertyFilter(t *testing.T) {
e := setupTestExecutor(t)
// Create node with embedding (should be filtered)
node := createTestNode(t, e, "node-1", []string{"Doc"}, map[string]interface{}{
"name": "Test",
"embedding": []float64{0.1, 0.2, 0.3},
"vector": []float64{0.4, 0.5, 0.6},
})
nodes := map[string]*storage.Node{"n": node}
// keys() should not include embedding/vector
result := e.evaluateExpressionWithContext("keys(n)", nodes, nil)
keys, ok := result.([]interface{})
if !ok {
t.Fatalf("keys() should return list")
}
for _, k := range keys {
kStr := k.(string)
if strings.Contains(strings.ToLower(kStr), "embedding") || strings.Contains(strings.ToLower(kStr), "vector") {
t.Errorf("keys() should not include %s", kStr)
}
}
// properties() should also filter embeddings
propsResult := e.evaluateExpressionWithContext("properties(n)", nodes, nil)
props, ok := propsResult.(map[string]interface{})
if !ok {
t.Fatalf("properties() should return map")
}
if _, exists := props["embedding"]; exists {
t.Error("properties() should not include embedding")
}
if _, exists := props["vector"]; exists {
t.Error("properties() should not include vector")
}
}
// ========================================
// OrNull Variants Tests
// ========================================
func TestOrNullVariants(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
// toIntegerOrNull
{"toIntegerOrNull('123')", int64(123)},
{"toIntegerOrNull('invalid')", nil},
{"toIntegerOrNull(45.6)", int64(45)},
// toFloatOrNull
{"toFloatOrNull('3.14')", float64(3.14)},
{"toFloatOrNull('invalid')", nil},
{"toFloatOrNull(42)", float64(42)},
// toBooleanOrNull
{"toBooleanOrNull('true')", true},
{"toBooleanOrNull('false')", false},
{"toBooleanOrNull('maybe')", nil},
// toStringOrNull
{"toStringOrNull(123)", "123"},
{"toStringOrNull(null)", nil},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v (%T), want %v (%T)", tt.expr, result, result, tt.expected, tt.expected)
}
})
}
}
// ========================================
// List Conversion Functions Tests
// ========================================
func TestListConversionFunctions(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"intList": []interface{}{"1", "2", "3"},
"floatList": []interface{}{"1.1", "2.2", "3.3"},
"boolList": []interface{}{"true", "false", "true"},
"stringList": []interface{}{int64(1), int64(2), int64(3)},
})
nodes := map[string]*storage.Node{"n": node}
// toIntegerList
result := e.evaluateExpressionWithContext("toIntegerList(n.intList)", nodes, nil)
if intList, ok := result.([]interface{}); ok {
if len(intList) != 3 {
t.Errorf("toIntegerList should return 3 elements, got %d", len(intList))
}
if intList[0] != int64(1) {
t.Errorf("toIntegerList[0] = %v, want 1", intList[0])
}
} else {
t.Errorf("toIntegerList should return []interface{}, got %T", result)
}
// toFloatList
result = e.evaluateExpressionWithContext("toFloatList(n.floatList)", nodes, nil)
if floatList, ok := result.([]interface{}); ok {
if len(floatList) != 3 {
t.Errorf("toFloatList should return 3 elements")
}
}
// toBooleanList
result = e.evaluateExpressionWithContext("toBooleanList(n.boolList)", nodes, nil)
if boolList, ok := result.([]interface{}); ok {
if len(boolList) != 3 {
t.Errorf("toBooleanList should return 3 elements")
}
if boolList[0] != true {
t.Errorf("toBooleanList[0] = %v, want true", boolList[0])
}
}
// toStringList
result = e.evaluateExpressionWithContext("toStringList(n.stringList)", nodes, nil)
if stringList, ok := result.([]interface{}); ok {
if len(stringList) != 3 {
t.Errorf("toStringList should return 3 elements")
}
if stringList[0] != "1" {
t.Errorf("toStringList[0] = %v, want '1'", stringList[0])
}
}
}
// ========================================
// valueType Function Tests
// ========================================
func TestValueTypeFunction(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected string
}{
{"valueType(null)", "NULL"},
{"valueType(true)", "BOOLEAN"},
{"valueType(123)", "INTEGER"},
{"valueType(3.14)", "FLOAT"},
{"valueType('hello')", "STRING"},
{"valueType([1, 2, 3])", "LIST"},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// Aggregation Functions (Expression Context) Tests
// ========================================
func TestAggregationInExpressionContext(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{"val": int64(42)})
nodes := map[string]*storage.Node{"n": node}
// In single row context, aggregation functions just return the value
tests := []string{"sum(n.val)", "avg(n.val)", "min(n.val)", "max(n.val)"}
for _, expr := range tests {
t.Run(expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(expr, nodes, nil)
if result != int64(42) {
t.Errorf("%s = %v, want 42", expr, result)
}
})
}
// collect returns a list
result := e.evaluateExpressionWithContext("collect(n.val)", nodes, nil)
if list, ok := result.([]interface{}); ok {
if len(list) != 1 || list[0] != int64(42) {
t.Errorf("collect(n.val) = %v, want [42]", list)
}
} else {
t.Errorf("collect should return list, got %T", result)
}
}
// ========================================
// List Predicate Functions Tests
// ========================================
func TestListPredicateFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Test with simple predicates that can be evaluated
// Note: these functions parse the WHERE predicate and substitute values
// Test none() with empty result - no match expected
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"empty": []interface{}{},
})
nodes := map[string]*storage.Node{"n": node}
// none() on empty list should return true
result := e.evaluateExpressionWithContext("none(x IN n.empty WHERE true)", nodes, nil)
if result != true {
t.Errorf("none(x IN empty list) = %v, want true", result)
}
// Test basic parsing - ensure functions don't crash
node2 := createTestNode(t, e, "node-2", []string{"Test"}, map[string]interface{}{
"nums": []interface{}{int64(1), int64(2), int64(3)},
})
nodes2 := map[string]*storage.Node{"n": node2}
// all() should not crash even if predicate evaluation is limited
_ = e.evaluateExpressionWithContext("all(x IN n.nums WHERE x = x)", nodes2, nil)
// any() should not crash
_ = e.evaluateExpressionWithContext("any(x IN n.nums WHERE x = x)", nodes2, nil)
// single() should not crash
_ = e.evaluateExpressionWithContext("single(x IN n.nums WHERE x = x)", nodes2, nil)
}
// ========================================
// Filter and Extract Functions Tests
// ========================================
func TestFilterAndExtractFunctions(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"nums": []interface{}{int64(1), int64(2), int64(3), int64(4), int64(5)},
})
nodes := map[string]*storage.Node{"n": node}
// Test extract() - transforms each element
result := e.evaluateExpressionWithContext("extract(x IN n.nums | x)", nodes, nil)
if list, ok := result.([]interface{}); ok {
if len(list) != 5 {
t.Errorf("extract should return 5 elements, got %d", len(list))
}
} else {
t.Errorf("extract should return list, got %T", result)
}
// Test filter() - returns a list (predicate evaluation limited)
result = e.evaluateExpressionWithContext("filter(x IN n.nums WHERE true)", nodes, nil)
_, ok := result.([]interface{})
if !ok {
t.Errorf("filter should return list, got %T", result)
}
}
// ========================================
// withinBBox Function Tests
// ========================================
func TestWithinBBoxFunction(t *testing.T) {
e := setupTestExecutor(t)
// Create points
node := createTestNode(t, e, "node-1", []string{"Location"}, map[string]interface{}{
"inside": map[string]interface{}{"x": float64(5), "y": float64(5)},
"outside": map[string]interface{}{"x": float64(15), "y": float64(15)},
"ll": map[string]interface{}{"x": float64(0), "y": float64(0)},
"ur": map[string]interface{}{"x": float64(10), "y": float64(10)},
})
nodes := map[string]*storage.Node{"n": node}
// Point inside bbox
result := e.evaluateExpressionWithContext("withinBBox(n.inside, n.ll, n.ur)", nodes, nil)
if result != true {
t.Errorf("withinBBox(inside) = %v, want true", result)
}
// Point outside bbox
result = e.evaluateExpressionWithContext("withinBBox(n.outside, n.ll, n.ur)", nodes, nil)
if result != false {
t.Errorf("withinBBox(outside) = %v, want false", result)
}
}
// ========================================
// List Comprehension Tests
// ========================================
func TestListComprehension(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"nums": []interface{}{int64(1), int64(2), int64(3)},
})
nodes := map[string]*storage.Node{"n": node}
// [x IN list | x] - identity transformation
result := e.evaluateExpressionWithContext("[x IN n.nums | x]", nodes, nil)
if list, ok := result.([]interface{}); ok {
if len(list) != 3 {
t.Errorf("list comprehension should return 3 elements, got %d", len(list))
}
}
}
// ========================================
// Slice Function Tests
// ========================================
func TestSliceFunction(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"list": []interface{}{int64(1), int64(2), int64(3), int64(4), int64(5)},
})
nodes := map[string]*storage.Node{"n": node}
// slice(list, 1, 3)
result := e.evaluateExpressionWithContext("slice(n.list, 1, 3)", nodes, nil)
if list, ok := result.([]interface{}); ok {
if len(list) != 2 {
t.Errorf("slice(list, 1, 3) should return 2 elements, got %d", len(list))
}
} else {
t.Errorf("slice should return list, got %T", result)
}
// slice(list, 2) - to end
result = e.evaluateExpressionWithContext("slice(n.list, 2)", nodes, nil)
if list, ok := result.([]interface{}); ok {
if len(list) != 3 {
t.Errorf("slice(list, 2) should return 3 elements, got %d", len(list))
}
}
}
// ========================================
// indexOf Function Tests
// ========================================
func TestIndexOfFunction(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"list": []interface{}{"a", "b", "c", "d"},
})
nodes := map[string]*storage.Node{"n": node}
// Found
result := e.evaluateExpressionWithContext("indexOf(n.list, 'b')", nodes, nil)
if result != int64(1) {
t.Errorf("indexOf(list, 'b') = %v, want 1", result)
}
// Not found
result = e.evaluateExpressionWithContext("indexOf(n.list, 'z')", nodes, nil)
if result != int64(-1) {
t.Errorf("indexOf(list, 'z') = %v, want -1", result)
}
}
// ========================================
// Degree Functions Tests
// ========================================
func TestDegreeFunctions(t *testing.T) {
e := setupTestExecutor(t)
// Create nodes
node1 := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{"name": "Alice"})
node2 := createTestNode(t, e, "node-2", []string{"Person"}, map[string]interface{}{"name": "Bob"})
node3 := createTestNode(t, e, "node-3", []string{"Person"}, map[string]interface{}{"name": "Carol"})
// Create edges: Alice -> Bob, Carol -> Alice
e.storage.CreateEdge(&storage.Edge{ID: "e1", Type: "KNOWS", StartNode: node1.ID, EndNode: node2.ID})
e.storage.CreateEdge(&storage.Edge{ID: "e2", Type: "KNOWS", StartNode: node3.ID, EndNode: node1.ID})
nodes := map[string]*storage.Node{"a": node1, "b": node2, "c": node3}
// Alice has 1 outgoing, 1 incoming = degree 2
result := e.evaluateExpressionWithContext("degree(a)", nodes, nil)
if result != int64(2) {
t.Errorf("degree(a) = %v, want 2", result)
}
// Alice has 1 outgoing
result = e.evaluateExpressionWithContext("outDegree(a)", nodes, nil)
if result != int64(1) {
t.Errorf("outDegree(a) = %v, want 1", result)
}
// Alice has 1 incoming
result = e.evaluateExpressionWithContext("inDegree(a)", nodes, nil)
if result != int64(1) {
t.Errorf("inDegree(a) = %v, want 1", result)
}
// Bob has 0 outgoing, 1 incoming
result = e.evaluateExpressionWithContext("degree(b)", nodes, nil)
if result != int64(1) {
t.Errorf("degree(b) = %v, want 1", result)
}
}
// ========================================
// hasLabels Function Tests
// ========================================
func TestHasLabelsFunction(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person", "Employee", "Manager"}, map[string]interface{}{
"requiredLabels": []interface{}{"Person", "Employee"},
"wrongLabels": []interface{}{"Person", "Director"},
})
nodes := map[string]*storage.Node{"n": node}
// Has all labels (using property with list)
result := e.evaluateExpressionWithContext("hasLabels(n, n.requiredLabels)", nodes, nil)
if result != true {
t.Errorf("hasLabels with matching labels = %v, want true", result)
}
// Missing a label
result = e.evaluateExpressionWithContext("hasLabels(n, n.wrongLabels)", nodes, nil)
if result != false {
t.Errorf("hasLabels with missing label = %v, want false", result)
}
}
// ========================================
// Haversine Distance Tests
// ========================================
func TestHaversineDistance(t *testing.T) {
e := setupTestExecutor(t)
// Test distance with lat/lon coordinates
node1 := createTestNode(t, e, "node-1", []string{"City"}, map[string]interface{}{
"loc": map[string]interface{}{"latitude": float64(40.7128), "longitude": float64(-74.0060)}, // NYC
})
node2 := createTestNode(t, e, "node-2", []string{"City"}, map[string]interface{}{
"loc": map[string]interface{}{"latitude": float64(34.0522), "longitude": float64(-118.2437)}, // LA
})
nodes := map[string]*storage.Node{"nyc": node1, "la": node2}
dist := e.evaluateExpressionWithContext("distance(nyc.loc, la.loc)", nodes, nil)
if dist == nil {
t.Fatal("haversine distance should return a value")
}
distF, ok := dist.(float64)
if !ok {
t.Fatalf("distance() should return float64, got %T", dist)
}
// Distance from NYC to LA is approximately 3,944 km or 3,944,000 meters
if distF < 3800000 || distF > 4100000 {
t.Errorf("haversine distance = %v, expected ~3944000 meters", distF)
}
}
// ========================================
// CASE WHEN Expression Tests
// ========================================
func TestCaseWhenExpression(t *testing.T) {
e := setupTestExecutor(t)
// Simple CASE expression
result := e.evaluateExpressionWithContext("CASE 1 WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'other' END", nil, nil)
if result != "one" {
t.Errorf("CASE 1 WHEN 1 = %v, want 'one'", result)
}
// Searched CASE expression
result = e.evaluateExpressionWithContext("CASE WHEN true THEN 'yes' ELSE 'no' END", nil, nil)
if result != "yes" {
t.Errorf("CASE WHEN true = %v, want 'yes'", result)
}
// CASE with ELSE
result = e.evaluateExpressionWithContext("CASE 3 WHEN 1 THEN 'one' ELSE 'other' END", nil, nil)
if result != "other" {
t.Errorf("CASE 3 WHEN 1 = %v, want 'other'", result)
}
// CASE without matching WHEN (no ELSE)
result = e.evaluateExpressionWithContext("CASE 5 WHEN 1 THEN 'one' END", nil, nil)
if result != nil {
t.Errorf("CASE without match = %v, want nil", result)
}
}
func TestCaseWhenWithNode(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"status": "active",
"score": int64(85),
})
nodes := map[string]*storage.Node{"n": node}
// CASE with property
result := e.evaluateExpressionWithContext("CASE n.status WHEN 'active' THEN 'online' ELSE 'offline' END", nodes, nil)
if result != "online" {
t.Errorf("CASE n.status = %v, want 'online'", result)
}
}
// ========================================
// Logical Operators Tests
// ========================================
func TestLogicalOperators(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
// AND
{"true AND true", true},
{"true AND false", false},
{"false AND true", false},
// OR
{"true OR false", true},
{"false OR true", true},
{"false OR false", false},
// XOR
{"true XOR false", true},
{"true XOR true", false},
{"false XOR false", false},
// NOT
{"NOT true", false},
{"NOT false", true},
// Combined
{"true AND NOT false", true},
{"false OR NOT false", true},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// Comparison Operators Tests
// ========================================
func TestComparisonOperators(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
// Equality
{"1 = 1", true},
{"1 = 2", false},
{"'a' = 'a'", true},
// Not equal
{"1 <> 2", true},
{"1 != 2", true},
{"1 <> 1", false},
// Less than
{"1 < 2", true},
{"2 < 1", false},
// Greater than
{"2 > 1", true},
{"1 > 2", false},
// Less than or equal
{"1 <= 1", true},
{"1 <= 2", true},
{"2 <= 1", false},
// Greater than or equal
{"1 >= 1", true},
{"2 >= 1", true},
{"1 >= 2", false},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// Arithmetic Operators Tests
// ========================================
func TestArithmeticOperators(t *testing.T) {
e := setupTestExecutor(t)
tests := []struct {
expr string
expected interface{}
}{
// Multiplication
{"2 * 3", int64(6)},
{"2.5 * 2", float64(5)},
// Division - Neo4j returns int64 for exact division, float64 otherwise
{"6 / 2", int64(3)},
{"7 / 2", float64(3.5)},
// Modulo
{"7 % 3", int64(1)},
{"10 % 5", int64(0)},
// Subtraction
{"5 - 3", int64(2)},
{"3.5 - 1.5", float64(2)},
// Unary minus
{"-5", int64(-5)},
{"-3.14", float64(-3.14)},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nil, nil)
if result != tt.expected {
t.Errorf("%s = %v (%T), want %v (%T)", tt.expr, result, result, tt.expected, tt.expected)
}
})
}
}
func TestArithmeticWithProperties(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"a": int64(10),
"b": int64(3),
})
nodes := map[string]*storage.Node{"n": node}
// n.a * n.b
result := e.evaluateExpressionWithContext("n.a * n.b", nodes, nil)
if result != int64(30) {
t.Errorf("n.a * n.b = %v, want 30", result)
}
// n.a - n.b
result = e.evaluateExpressionWithContext("n.a - n.b", nodes, nil)
if result != int64(7) {
t.Errorf("n.a - n.b = %v, want 7", result)
}
}
// ========================================
// Combined Expression Tests
// ========================================
func TestCombinedExpressions(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"age": int64(25),
"active": true,
})
nodes := map[string]*storage.Node{"n": node}
// Comparison with property
result := e.evaluateExpressionWithContext("n.age > 18", nodes, nil)
if result != true {
t.Errorf("n.age > 18 = %v, want true", result)
}
// Multiple conditions with AND
result = e.evaluateExpressionWithContext("n.age > 18 AND n.active = true", nodes, nil)
if result != true {
t.Errorf("n.age > 18 AND n.active = %v, want true", result)
}
}
// ========================================
// IS NULL / IS NOT NULL Tests
// ========================================
func TestIsNullPredicates(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"name": "Alice",
})
nodes := map[string]*storage.Node{"n": node}
tests := []struct {
expr string
expected interface{}
}{
{"null IS NULL", true},
{"null IS NOT NULL", false},
{"'hello' IS NULL", false},
{"'hello' IS NOT NULL", true},
{"n.name IS NOT NULL", true},
{"n.missing IS NULL", true},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nodes, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// String Predicates Tests (STARTS WITH, ENDS WITH, CONTAINS)
// ========================================
func TestStringPredicates(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"name": "Alice Johnson",
})
nodes := map[string]*storage.Node{"n": node}
tests := []struct {
expr string
expected interface{}
}{
// STARTS WITH
{"'hello world' STARTS WITH 'hello'", true},
{"'hello world' STARTS WITH 'world'", false},
{"n.name STARTS WITH 'Alice'", true},
{"n.name STARTS WITH 'Bob'", false},
// ENDS WITH
{"'hello world' ENDS WITH 'world'", true},
{"'hello world' ENDS WITH 'hello'", false},
{"n.name ENDS WITH 'Johnson'", true},
// CONTAINS
{"'hello world' CONTAINS 'lo wo'", true},
{"'hello world' CONTAINS 'xyz'", false},
{"n.name CONTAINS 'ice'", true},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nodes, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// IN Operator Tests
// ========================================
func TestInOperatorExpression(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"status": "active",
"list": []interface{}{"a", "b", "c"},
})
nodes := map[string]*storage.Node{"n": node}
tests := []struct {
expr string
expected interface{}
}{
{"1 IN [1, 2, 3]", true},
{"4 IN [1, 2, 3]", false},
{"'a' IN ['a', 'b', 'c']", true},
{"'x' IN ['a', 'b', 'c']", false},
{"n.status IN ['active', 'pending']", true},
{"n.status IN ['closed', 'cancelled']", false},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nodes, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// BETWEEN Operator Tests
// ========================================
func TestBetweenOperator(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Person"}, map[string]interface{}{
"age": int64(25),
})
nodes := map[string]*storage.Node{"n": node}
tests := []struct {
expr string
expected interface{}
}{
{"5 BETWEEN 1 AND 10", true},
{"15 BETWEEN 1 AND 10", false},
{"1 BETWEEN 1 AND 10", true}, // inclusive
{"10 BETWEEN 1 AND 10", true}, // inclusive
{"n.age BETWEEN 18 AND 30", true},
{"n.age BETWEEN 30 AND 40", false},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
result := e.evaluateExpressionWithContext(tt.expr, nodes, nil)
if result != tt.expected {
t.Errorf("%s = %v, want %v", tt.expr, result, tt.expected)
}
})
}
}
// ========================================
// APOC Map Functions Tests
// ========================================
func TestApocMapMerge(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"map1": map[string]interface{}{"a": int64(1), "b": int64(2)},
"map2": map[string]interface{}{"b": int64(3), "c": int64(4)},
})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("apoc.map.merge(n.map1, n.map2)", nodes, nil)
if m, ok := result.(map[string]interface{}); ok {
// map2's "b" should override map1's "b"
if m["a"] != int64(1) {
t.Errorf("merged map should have a=1, got %v", m["a"])
}
if m["b"] != int64(3) {
t.Errorf("merged map should have b=3 (from map2), got %v", m["b"])
}
if m["c"] != int64(4) {
t.Errorf("merged map should have c=4, got %v", m["c"])
}
} else {
t.Errorf("apoc.map.merge should return map, got %T", result)
}
}
func TestApocMapSetKey(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"myMap": map[string]interface{}{"a": int64(1)},
})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("apoc.map.setKey(n.myMap, 'b', 2)", nodes, nil)
if m, ok := result.(map[string]interface{}); ok {
if m["a"] != int64(1) {
t.Errorf("map should still have a=1, got %v", m["a"])
}
// The '2' was parsed as a string, so we need to check for string or int
if m["b"] == nil {
t.Errorf("map should have b set")
}
} else {
t.Errorf("apoc.map.setKey should return map, got %T", result)
}
}
func TestApocMapRemoveKey(t *testing.T) {
e := setupTestExecutor(t)
node := createTestNode(t, e, "node-1", []string{"Test"}, map[string]interface{}{
"myMap": map[string]interface{}{"a": int64(1), "b": int64(2), "c": int64(3)},
})
nodes := map[string]*storage.Node{"n": node}
result := e.evaluateExpressionWithContext("apoc.map.removeKey(n.myMap, 'b')", nodes, nil)
if m, ok := result.(map[string]interface{}); ok {
if _, exists := m["b"]; exists {
t.Errorf("map should not have key 'b' after removeKey")
}
if m["a"] != int64(1) {
t.Errorf("map should still have a=1")
}
if m["c"] != int64(3) {
t.Errorf("map should still have c=3")
}
} else {
t.Errorf("apoc.map.removeKey should return map, got %T", result)
}
}