Files
streamsql/expr/expression_test.go
T
2025-08-04 14:45:43 +08:00

468 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package expr
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExpressionEvaluation(t *testing.T) {
tests := []struct {
name string
expr string
data map[string]interface{}
expected float64
hasError bool
}{
// Basic arithmetic tests
{"Simple Addition", "a + b", map[string]interface{}{"a": 5, "b": 3}, 8, false},
{"Simple Subtraction", "a - b", map[string]interface{}{"a": 5, "b": 3}, 2, false},
{"Simple Multiplication", "a * b", map[string]interface{}{"a": 5, "b": 3}, 15, false},
{"Simple Division", "a / b", map[string]interface{}{"a": 6, "b": 3}, 2, false},
{"Modulo", "a % b", map[string]interface{}{"a": 7, "b": 4}, 3, false},
{"Power", "a ^ b", map[string]interface{}{"a": 2, "b": 3}, 8, false},
// Compound expression tests
{"Complex Expression", "a + b * c", map[string]interface{}{"a": 5, "b": 3, "c": 2}, 11, false},
{"Complex Expression With Parentheses", "(a + b) * c", map[string]interface{}{"a": 5, "b": 3, "c": 2}, 16, false},
{"Multiple Operations", "a + b * c - d / e", map[string]interface{}{"a": 5, "b": 3, "c": 2, "d": 8, "e": 4}, 9, false},
// Function call tests
{"Abs Function", "abs(a - b)", map[string]interface{}{"a": 3, "b": 5}, 2, false},
{"Sqrt Function", "sqrt(a)", map[string]interface{}{"a": 16}, 4, false},
{"Round Function", "round(a)", map[string]interface{}{"a": 3.7}, 4, false},
// Conversion tests
{"String to Number", "a + b", map[string]interface{}{"a": "5", "b": 3}, 8, false},
// Complex expression tests
{"Temperature Conversion", "temperature * 1.8 + 32", map[string]interface{}{"temperature": 25}, 77, false},
{"Complex Math", "sqrt(abs(a * b - c / d))", map[string]interface{}{"a": 10, "b": 2, "c": 5, "d": 1}, 3.872983346207417, false},
// Error tests
{"Division by Zero", "a / b", map[string]interface{}{"a": 5, "b": 0}, 0, true},
{"Missing Field", "a + b", map[string]interface{}{"a": 5}, 0, true},
{"Invalid Function", "unknown(a)", map[string]interface{}{"a": 5}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
expr, err := NewExpression(tt.expr)
assert.NoError(t, err, "Expression parsing should not fail")
result, err := expr.Evaluate(tt.data)
if tt.hasError {
assert.Error(t, err, "Expected error")
} else {
assert.NoError(t, err, "Evaluation should not fail")
assert.InDelta(t, tt.expected, result, 0.001, "Result should match expected value")
}
})
}
}
// TestCaseExpressionParsing tests CASE expression parsing functionality
func TestCaseExpressionParsing(t *testing.T) {
tests := []struct {
name string
exprStr string
data map[string]interface{}
expected float64
wantErr bool
}{
{
name: "Simple search CASE expression",
exprStr: "CASE WHEN temperature > 30 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 35.0},
expected: 1.0,
wantErr: false,
},
{
name: "Simple CASE expression - value matching",
exprStr: "CASE status WHEN 'active' THEN 1 WHEN 'inactive' THEN 0 ELSE -1 END",
data: map[string]interface{}{"status": "active"},
expected: 1.0,
wantErr: false,
},
{
name: "CASE expression - ELSE branch",
exprStr: "CASE WHEN temperature > 50 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 25.5},
expected: 0.0,
wantErr: false,
},
{
name: "Complex search CASE expression",
exprStr: "CASE WHEN temperature > 30 THEN 'HOT' WHEN temperature > 20 THEN 'WARM' ELSE 'COLD' END",
data: map[string]interface{}{"temperature": 25.0},
expected: 4.0, // Length of string "WARM"
wantErr: false,
},
{
name: "Simple CASE with numeric comparison",
exprStr: "CASE temperature WHEN 25 THEN 1 WHEN 30 THEN 2 ELSE 0 END",
data: map[string]interface{}{"temperature": 30.0},
expected: 2.0,
wantErr: false,
},
{
name: "Boolean CASE expression",
exprStr: "CASE WHEN temperature > 25 AND humidity > 50 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 30.0, "humidity": 60.0},
expected: 1.0,
wantErr: false,
},
{
name: "Multi-condition CASE expression with AND",
exprStr: "CASE WHEN temperature > 30 AND humidity < 60 THEN 1 WHEN temperature > 20 THEN 2 ELSE 0 END",
data: map[string]interface{}{"temperature": 35.0, "humidity": 50.0},
expected: 1.0,
wantErr: false,
},
{
name: "Multi-condition CASE expression with OR",
exprStr: "CASE WHEN temperature > 40 OR humidity > 80 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 25.0, "humidity": 85.0},
expected: 1.0,
wantErr: false,
},
{
name: "Function call in CASE - ABS",
exprStr: "CASE WHEN ABS(temperature) > 30 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": -35.0},
expected: 1.0,
wantErr: false,
},
{
name: "Function call in CASE - ROUND",
exprStr: "CASE WHEN ROUND(temperature) = 25 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 24.7},
expected: 1.0,
wantErr: false,
},
{
name: "Complex condition combination",
exprStr: "CASE WHEN temperature > 30 AND (humidity > 60 OR pressure < 1000) THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 35.0, "humidity": 55.0, "pressure": 950.0},
expected: 1.0,
wantErr: false,
},
{
name: "Arithmetic expression in CASE",
exprStr: "CASE WHEN temperature * 1.8 + 32 > 100 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 40.0}, // 40*1.8+32 = 104
expected: 1.0,
wantErr: false,
},
{
name: "String function in CASE",
exprStr: "CASE WHEN LENGTH(device_name) > 5 THEN 1 ELSE 0 END",
data: map[string]interface{}{"device_name": "sensor123"},
expected: 1.0, // LENGTH function works normally, "sensor123" length is 9 > 5, returns 1
wantErr: false,
},
{
name: "Simple CASE with function",
exprStr: "CASE ABS(temperature) WHEN 30 THEN 1 WHEN 25 THEN 2 ELSE 0 END",
data: map[string]interface{}{"temperature": -30.0},
expected: 1.0,
wantErr: false,
},
{
name: "Function in CASE result",
exprStr: "CASE WHEN temperature > 30 THEN ABS(temperature) ELSE ROUND(temperature) END",
data: map[string]interface{}{"temperature": 35.5},
expected: 35.5,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
expression, err := NewExpression(tt.exprStr)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err, "Expression creation should not fail")
assert.NotNil(t, expression, "Expression should not be nil")
// Test expression evaluation
result, err := expression.Evaluate(tt.data)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err, "Expression evaluation should not fail")
assert.Equal(t, tt.expected, result, "Expression result should match expected value")
})
}
}
// TestCaseExpressionFieldExtraction 测试CASE表达式的字段提取功能
func TestCaseExpressionFieldExtraction(t *testing.T) {
testCases := []struct {
name string
exprStr string
expectedFields []string
}{
{
name: "简单CASE字段提取",
exprStr: "CASE WHEN temperature > 30 THEN 1 ELSE 0 END",
expectedFields: []string{"temperature"},
},
{
name: "多字段CASE字段提取",
exprStr: "CASE WHEN temperature > 30 AND humidity < 60 THEN 1 ELSE 0 END",
expectedFields: []string{"temperature", "humidity"},
},
{
name: "简单CASE字段提取",
exprStr: "CASE status WHEN 'active' THEN temperature ELSE humidity END",
expectedFields: []string{"status", "temperature", "humidity"},
},
{
name: "函数CASE字段提取",
exprStr: "CASE WHEN ABS(temperature) > 30 THEN device_id ELSE location END",
expectedFields: []string{"temperature", "device_id", "location"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expression, err := NewExpression(tc.exprStr)
assert.NoError(t, err, "表达式创建应该成功")
fields := expression.GetFields()
// 验证所有期望的字段都被提取到了
for _, expectedField := range tc.expectedFields {
assert.Contains(t, fields, expectedField, "应该包含字段: %s", expectedField)
}
})
}
}
// TestCaseExpressionWithNullComparisons 测试CASE表达式中的NULL比较
func TestCaseExpressionWithNullComparisons(t *testing.T) {
tests := []struct {
name string
exprStr string
data map[string]interface{}
expected interface{} // 使用interface{}以支持NULL值
isNull bool
}{
{
name: "NULL值在CASE条件中 - 应该走ELSE分支",
exprStr: "CASE WHEN temperature > 30 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": nil},
expected: 0.0,
isNull: false,
},
{
name: "IS NULL条件 - 应该匹配",
exprStr: "CASE WHEN temperature IS NULL THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": nil},
expected: 1.0,
isNull: false,
},
{
name: "IS NOT NULL条件 - 不应该匹配",
exprStr: "CASE WHEN temperature IS NOT NULL THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": nil},
expected: 0.0,
isNull: false,
},
{
name: "CASE表达式返回NULL",
exprStr: "CASE WHEN temperature > 30 THEN temperature ELSE NULL END",
data: map[string]interface{}{"temperature": 25.0},
expected: nil,
isNull: true,
},
{
name: "CASE表达式返回有效值",
exprStr: "CASE WHEN temperature > 30 THEN temperature ELSE NULL END",
data: map[string]interface{}{"temperature": 35.0},
expected: 35.0,
isNull: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
expression, err := NewExpression(tt.exprStr)
assert.NoError(t, err, "表达式解析应该成功")
// 测试支持NULL的计算方法
result, isNull, err := expression.EvaluateWithNull(tt.data)
assert.NoError(t, err, "表达式计算应该成功")
if tt.isNull {
assert.True(t, isNull, "表达式应该返回NULL")
} else {
assert.False(t, isNull, "表达式不应该返回NULL")
assert.Equal(t, tt.expected, result, "表达式结果应该匹配期望值")
}
})
}
}
// TestNegativeNumberSupport 专门测试负数支持
func TestNegativeNumberSupport(t *testing.T) {
tests := []struct {
name string
exprStr string
data map[string]interface{}
expected float64
wantErr bool
}{
{
name: "负数常量在THEN中",
exprStr: "CASE WHEN temperature > 0 THEN 1 ELSE -1 END",
data: map[string]interface{}{"temperature": -5.0},
expected: -1.0,
wantErr: false,
},
{
name: "负数常量在WHEN中",
exprStr: "CASE WHEN temperature < -10 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": -15.0},
expected: 1.0,
wantErr: false,
},
{
name: "负数小数",
exprStr: "CASE WHEN temperature > 0 THEN 1.5 ELSE -2.5 END",
data: map[string]interface{}{"temperature": -1.0},
expected: -2.5,
wantErr: false,
},
{
name: "负数在算术表达式中",
exprStr: "CASE WHEN temperature + (-10) > 0 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 15.0},
expected: 1.0,
wantErr: false,
},
{
name: "负数与函数",
exprStr: "CASE WHEN ABS(temperature) > 10 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": -15.0},
expected: 1.0,
wantErr: false,
},
{
name: "负数在简单CASE中",
exprStr: "CASE temperature WHEN -10 THEN 1 WHEN -20 THEN 2 ELSE 0 END",
data: map[string]interface{}{"temperature": -10.0},
expected: 1.0,
wantErr: false,
},
{
name: "负零",
exprStr: "CASE WHEN temperature = -0 THEN 1 ELSE 0 END",
data: map[string]interface{}{"temperature": 0.0},
expected: 1.0,
wantErr: false,
},
// 基本负数运算
{
name: "直接负数",
exprStr: "-5",
data: map[string]interface{}{},
expected: -5.0,
wantErr: false,
},
{
name: "负数加法",
exprStr: "-5 + 3",
data: map[string]interface{}{},
expected: -2.0,
wantErr: false,
},
{
name: "负数乘法",
exprStr: "-3 * 4",
data: map[string]interface{}{},
expected: -12.0,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
expression, err := NewExpression(tt.exprStr)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err, "负数表达式解析应该成功")
assert.NotNil(t, expression, "表达式不应为空")
// 测试表达式计算
result, err := expression.Evaluate(tt.data)
assert.NoError(t, err, "负数表达式计算应该成功")
assert.Equal(t, tt.expected, result, "负数表达式结果应该匹配期望值")
})
}
}
func TestGetFields(t *testing.T) {
tests := []struct {
expr string
expectedFields []string
}{
{"a + b", []string{"a", "b"}},
{"a + b * c", []string{"a", "b", "c"}},
{"temperature * 1.8 + 32", []string{"temperature"}},
{"abs(humidity - 50)", []string{"humidity"}},
{"sqrt(x^2 + y^2)", []string{"x", "y"}},
}
for _, tt := range tests {
t.Run(tt.expr, func(t *testing.T) {
expr, err := NewExpression(tt.expr)
assert.NoError(t, err, "Expression parsing should not fail")
fields := expr.GetFields()
// 由于map迭代顺序不确定我们只检查长度和包含关系
assert.Equal(t, len(tt.expectedFields), len(fields), "Number of fields should match")
for _, field := range tt.expectedFields {
found := false
for _, f := range fields {
if f == field {
found = true
break
}
}
assert.True(t, found, "Field %s should be found", field)
}
})
}
}
func TestParseError(t *testing.T) {
tests := []struct {
name string
expr string
}{
{"Empty Expression", ""},
{"Mismatched Parentheses", "a + (b * c"},
{"Invalid Character", "a # b"},
{"Double Operator", "a + * b"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewExpression(tt.expr)
assert.Error(t, err, "Expression parsing should fail")
})
}
}