diff --git a/weed/query/engine/sql_functions.go b/weed/query/engine/sql_functions.go index 6bd07763e..1b2638d55 100644 --- a/weed/query/engine/sql_functions.go +++ b/weed/query/engine/sql_functions.go @@ -140,3 +140,113 @@ func (e *SQLEngine) Divide(left, right *schema_pb.Value) (*schema_pb.Value, erro func (e *SQLEngine) Modulo(left, right *schema_pb.Value) (*schema_pb.Value, error) { return e.EvaluateArithmeticExpression(left, right, OpMod) } + +// =============================== +// MATHEMATICAL FUNCTIONS +// =============================== + +// Round rounds a numeric value to the nearest integer or specified decimal places +func (e *SQLEngine) Round(value *schema_pb.Value, precision ...*schema_pb.Value) (*schema_pb.Value, error) { + if value == nil { + return nil, fmt.Errorf("ROUND function requires non-null value") + } + + num, err := e.valueToFloat64(value) + if err != nil { + return nil, fmt.Errorf("ROUND function conversion error: %v", err) + } + + // Default precision is 0 (round to integer) + precisionValue := 0 + if len(precision) > 0 && precision[0] != nil { + precFloat, err := e.valueToFloat64(precision[0]) + if err != nil { + return nil, fmt.Errorf("ROUND precision conversion error: %v", err) + } + precisionValue = int(precFloat) + } + + // Apply rounding + multiplier := math.Pow(10, float64(precisionValue)) + rounded := math.Round(num*multiplier) / multiplier + + // Return as integer if precision is 0 and original was integer, otherwise as double + if precisionValue == 0 && e.isIntegerValue(value) { + return &schema_pb.Value{ + Kind: &schema_pb.Value_Int64Value{Int64Value: int64(rounded)}, + }, nil + } + + return &schema_pb.Value{ + Kind: &schema_pb.Value_DoubleValue{DoubleValue: rounded}, + }, nil +} + +// Ceil returns the smallest integer greater than or equal to the value +func (e *SQLEngine) Ceil(value *schema_pb.Value) (*schema_pb.Value, error) { + if value == nil { + return nil, fmt.Errorf("CEIL function requires non-null value") + } + + num, err := e.valueToFloat64(value) + if err != nil { + return nil, fmt.Errorf("CEIL function conversion error: %v", err) + } + + result := math.Ceil(num) + + return &schema_pb.Value{ + Kind: &schema_pb.Value_Int64Value{Int64Value: int64(result)}, + }, nil +} + +// Floor returns the largest integer less than or equal to the value +func (e *SQLEngine) Floor(value *schema_pb.Value) (*schema_pb.Value, error) { + if value == nil { + return nil, fmt.Errorf("FLOOR function requires non-null value") + } + + num, err := e.valueToFloat64(value) + if err != nil { + return nil, fmt.Errorf("FLOOR function conversion error: %v", err) + } + + result := math.Floor(num) + + return &schema_pb.Value{ + Kind: &schema_pb.Value_Int64Value{Int64Value: int64(result)}, + }, nil +} + +// Abs returns the absolute value of a number +func (e *SQLEngine) Abs(value *schema_pb.Value) (*schema_pb.Value, error) { + if value == nil { + return nil, fmt.Errorf("ABS function requires non-null value") + } + + num, err := e.valueToFloat64(value) + if err != nil { + return nil, fmt.Errorf("ABS function conversion error: %v", err) + } + + result := math.Abs(num) + + // Return same type as input if possible + if e.isIntegerValue(value) { + return &schema_pb.Value{ + Kind: &schema_pb.Value_Int64Value{Int64Value: int64(result)}, + }, nil + } + + // Check if original was float32 + if _, ok := value.Kind.(*schema_pb.Value_FloatValue); ok { + return &schema_pb.Value{ + Kind: &schema_pb.Value_FloatValue{FloatValue: float32(result)}, + }, nil + } + + // Default to double + return &schema_pb.Value{ + Kind: &schema_pb.Value_DoubleValue{DoubleValue: result}, + }, nil +} diff --git a/weed/query/engine/sql_functions_test.go b/weed/query/engine/sql_functions_test.go index 748d9e030..9746dca79 100644 --- a/weed/query/engine/sql_functions_test.go +++ b/weed/query/engine/sql_functions_test.go @@ -257,3 +257,274 @@ func valuesEqual(v1, v2 *schema_pb.Value) bool { return false } + +func TestMathematicalFunctions(t *testing.T) { + engine := NewTestSQLEngine() + + t.Run("ROUND function tests", func(t *testing.T) { + tests := []struct { + name string + value *schema_pb.Value + precision *schema_pb.Value + expected *schema_pb.Value + expectErr bool + }{ + { + name: "Round float to integer", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.7}}, + precision: nil, + expected: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 4.0}}, + expectErr: false, + }, + { + name: "Round integer stays integer", + value: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + precision: nil, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expectErr: false, + }, + { + name: "Round with precision 2", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.14159}}, + precision: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 2}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.14}}, + expectErr: false, + }, + { + name: "Round negative number", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: -3.7}}, + precision: nil, + expected: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: -4.0}}, + expectErr: false, + }, + { + name: "Round null value", + value: nil, + precision: nil, + expected: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result *schema_pb.Value + var err error + + if tt.precision != nil { + result, err = engine.Round(tt.value, tt.precision) + } else { + result, err = engine.Round(tt.value) + } + + if tt.expectErr { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !valuesEqual(result, tt.expected) { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } + }) + + t.Run("CEIL function tests", func(t *testing.T) { + tests := []struct { + name string + value *schema_pb.Value + expected *schema_pb.Value + expectErr bool + }{ + { + name: "Ceil positive decimal", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.2}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 4}}, + expectErr: false, + }, + { + name: "Ceil negative decimal", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: -3.2}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: -3}}, + expectErr: false, + }, + { + name: "Ceil integer", + value: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expectErr: false, + }, + { + name: "Ceil null value", + value: nil, + expected: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := engine.Ceil(tt.value) + + if tt.expectErr { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !valuesEqual(result, tt.expected) { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } + }) + + t.Run("FLOOR function tests", func(t *testing.T) { + tests := []struct { + name string + value *schema_pb.Value + expected *schema_pb.Value + expectErr bool + }{ + { + name: "Floor positive decimal", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.8}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 3}}, + expectErr: false, + }, + { + name: "Floor negative decimal", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: -3.2}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: -4}}, + expectErr: false, + }, + { + name: "Floor integer", + value: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expectErr: false, + }, + { + name: "Floor null value", + value: nil, + expected: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := engine.Floor(tt.value) + + if tt.expectErr { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !valuesEqual(result, tt.expected) { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } + }) + + t.Run("ABS function tests", func(t *testing.T) { + tests := []struct { + name string + value *schema_pb.Value + expected *schema_pb.Value + expectErr bool + }{ + { + name: "Abs positive integer", + value: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expectErr: false, + }, + { + name: "Abs negative integer", + value: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: -5}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 5}}, + expectErr: false, + }, + { + name: "Abs positive double", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.14}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.14}}, + expectErr: false, + }, + { + name: "Abs negative double", + value: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: -3.14}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: 3.14}}, + expectErr: false, + }, + { + name: "Abs positive float", + value: &schema_pb.Value{Kind: &schema_pb.Value_FloatValue{FloatValue: 2.5}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_FloatValue{FloatValue: 2.5}}, + expectErr: false, + }, + { + name: "Abs negative float", + value: &schema_pb.Value{Kind: &schema_pb.Value_FloatValue{FloatValue: -2.5}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_FloatValue{FloatValue: 2.5}}, + expectErr: false, + }, + { + name: "Abs zero", + value: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 0}}, + expected: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: 0}}, + expectErr: false, + }, + { + name: "Abs null value", + value: nil, + expected: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := engine.Abs(tt.value) + + if tt.expectErr { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !valuesEqual(result, tt.expected) { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } + }) +}