diff options
Diffstat (limited to 'weed/query/engine/datetime_functions_test.go')
| -rw-r--r-- | weed/query/engine/datetime_functions_test.go | 891 |
1 files changed, 891 insertions, 0 deletions
diff --git a/weed/query/engine/datetime_functions_test.go b/weed/query/engine/datetime_functions_test.go new file mode 100644 index 000000000..a4951e825 --- /dev/null +++ b/weed/query/engine/datetime_functions_test.go @@ -0,0 +1,891 @@ +package engine + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" +) + +func TestDateTimeFunctions(t *testing.T) { + engine := NewTestSQLEngine() + + t.Run("CURRENT_DATE function tests", func(t *testing.T) { + before := time.Now() + result, err := engine.CurrentDate() + after := time.Now() + + if err != nil { + t.Errorf("CurrentDate failed: %v", err) + } + + if result == nil { + t.Errorf("CurrentDate returned nil result") + return + } + + stringVal, ok := result.Kind.(*schema_pb.Value_StringValue) + if !ok { + t.Errorf("CurrentDate should return string value, got %T", result.Kind) + return + } + + // Check format (YYYY-MM-DD) with tolerance for midnight boundary crossings + beforeDate := before.Format("2006-01-02") + afterDate := after.Format("2006-01-02") + + if stringVal.StringValue != beforeDate && stringVal.StringValue != afterDate { + t.Errorf("Expected current date %s or %s (due to potential midnight boundary), got %s", + beforeDate, afterDate, stringVal.StringValue) + } + }) + + t.Run("CURRENT_TIMESTAMP function tests", func(t *testing.T) { + before := time.Now() + result, err := engine.CurrentTimestamp() + after := time.Now() + + if err != nil { + t.Errorf("CurrentTimestamp failed: %v", err) + } + + if result == nil { + t.Errorf("CurrentTimestamp returned nil result") + return + } + + timestampVal, ok := result.Kind.(*schema_pb.Value_TimestampValue) + if !ok { + t.Errorf("CurrentTimestamp should return timestamp value, got %T", result.Kind) + return + } + + timestamp := time.UnixMicro(timestampVal.TimestampValue.TimestampMicros) + + // Check that timestamp is within reasonable range with small tolerance buffer + // Allow for small timing variations, clock precision differences, and NTP adjustments + tolerance := 100 * time.Millisecond + beforeWithTolerance := before.Add(-tolerance) + afterWithTolerance := after.Add(tolerance) + + if timestamp.Before(beforeWithTolerance) || timestamp.After(afterWithTolerance) { + t.Errorf("Timestamp %v should be within tolerance of %v to %v (tolerance: %v)", + timestamp, before, after, tolerance) + } + }) + + t.Run("NOW function tests", func(t *testing.T) { + result, err := engine.Now() + if err != nil { + t.Errorf("Now failed: %v", err) + } + + if result == nil { + t.Errorf("Now returned nil result") + return + } + + // Should return same type as CurrentTimestamp + _, ok := result.Kind.(*schema_pb.Value_TimestampValue) + if !ok { + t.Errorf("Now should return timestamp value, got %T", result.Kind) + } + }) + + t.Run("CURRENT_TIME function tests", func(t *testing.T) { + result, err := engine.CurrentTime() + if err != nil { + t.Errorf("CurrentTime failed: %v", err) + } + + if result == nil { + t.Errorf("CurrentTime returned nil result") + return + } + + stringVal, ok := result.Kind.(*schema_pb.Value_StringValue) + if !ok { + t.Errorf("CurrentTime should return string value, got %T", result.Kind) + return + } + + // Check format (HH:MM:SS) + if len(stringVal.StringValue) != 8 || stringVal.StringValue[2] != ':' || stringVal.StringValue[5] != ':' { + t.Errorf("CurrentTime should return HH:MM:SS format, got %s", stringVal.StringValue) + } + }) +} + +func TestExtractFunction(t *testing.T) { + engine := NewTestSQLEngine() + + // Create a test timestamp: 2023-06-15 14:30:45 + // Use local time to avoid timezone conversion issues + testTime := time.Date(2023, 6, 15, 14, 30, 45, 0, time.Local) + testTimestamp := &schema_pb.Value{ + Kind: &schema_pb.Value_TimestampValue{ + TimestampValue: &schema_pb.TimestampValue{ + TimestampMicros: testTime.UnixMicro(), + }, + }, + } + + tests := []struct { + name string + part DatePart + value *schema_pb.Value + expected int64 + expectErr bool + }{ + { + name: "Extract YEAR", + part: PartYear, + value: testTimestamp, + expected: 2023, + expectErr: false, + }, + { + name: "Extract MONTH", + part: PartMonth, + value: testTimestamp, + expected: 6, + expectErr: false, + }, + { + name: "Extract DAY", + part: PartDay, + value: testTimestamp, + expected: 15, + expectErr: false, + }, + { + name: "Extract HOUR", + part: PartHour, + value: testTimestamp, + expected: 14, + expectErr: false, + }, + { + name: "Extract MINUTE", + part: PartMinute, + value: testTimestamp, + expected: 30, + expectErr: false, + }, + { + name: "Extract SECOND", + part: PartSecond, + value: testTimestamp, + expected: 45, + expectErr: false, + }, + { + name: "Extract QUARTER from June", + part: PartQuarter, + value: testTimestamp, + expected: 2, // June is in Q2 + expectErr: false, + }, + { + name: "Extract from string date", + part: PartYear, + value: &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "2023-06-15"}}, + expected: 2023, + expectErr: false, + }, + { + name: "Extract from Unix timestamp", + part: PartYear, + value: &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: testTime.Unix()}}, + expected: 2023, + expectErr: false, + }, + { + name: "Extract from null value", + part: PartYear, + value: nil, + expected: 0, + expectErr: true, + }, + { + name: "Extract invalid part", + part: DatePart("INVALID"), + value: testTimestamp, + expected: 0, + expectErr: true, + }, + { + name: "Extract from invalid string", + part: PartYear, + value: &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "invalid-date"}}, + expected: 0, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := engine.Extract(tt.part, 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 result == nil { + t.Errorf("Extract returned nil result") + return + } + + intVal, ok := result.Kind.(*schema_pb.Value_Int64Value) + if !ok { + t.Errorf("Extract should return int64 value, got %T", result.Kind) + return + } + + if intVal.Int64Value != tt.expected { + t.Errorf("Expected %d, got %d", tt.expected, intVal.Int64Value) + } + }) + } +} + +func TestDateTruncFunction(t *testing.T) { + engine := NewTestSQLEngine() + + // Create a test timestamp: 2023-06-15 14:30:45.123456 + testTime := time.Date(2023, 6, 15, 14, 30, 45, 123456000, time.Local) // nanoseconds + testTimestamp := &schema_pb.Value{ + Kind: &schema_pb.Value_TimestampValue{ + TimestampValue: &schema_pb.TimestampValue{ + TimestampMicros: testTime.UnixMicro(), + }, + }, + } + + tests := []struct { + name string + precision string + value *schema_pb.Value + expectErr bool + expectedCheck func(result time.Time) bool // Custom check function + }{ + { + name: "Truncate to second", + precision: "second", + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + return result.Year() == 2023 && result.Month() == 6 && result.Day() == 15 && + result.Hour() == 14 && result.Minute() == 30 && result.Second() == 45 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate to minute", + precision: "minute", + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + return result.Year() == 2023 && result.Month() == 6 && result.Day() == 15 && + result.Hour() == 14 && result.Minute() == 30 && result.Second() == 0 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate to hour", + precision: "hour", + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + return result.Year() == 2023 && result.Month() == 6 && result.Day() == 15 && + result.Hour() == 14 && result.Minute() == 0 && result.Second() == 0 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate to day", + precision: "day", + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + return result.Year() == 2023 && result.Month() == 6 && result.Day() == 15 && + result.Hour() == 0 && result.Minute() == 0 && result.Second() == 0 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate to month", + precision: "month", + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + return result.Year() == 2023 && result.Month() == 6 && result.Day() == 1 && + result.Hour() == 0 && result.Minute() == 0 && result.Second() == 0 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate to quarter", + precision: "quarter", + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + // June (month 6) should truncate to April (month 4) - start of Q2 + return result.Year() == 2023 && result.Month() == 4 && result.Day() == 1 && + result.Hour() == 0 && result.Minute() == 0 && result.Second() == 0 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate to year", + precision: "year", + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + return result.Year() == 2023 && result.Month() == 1 && result.Day() == 1 && + result.Hour() == 0 && result.Minute() == 0 && result.Second() == 0 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate with plural precision", + precision: "minutes", // Test plural form + value: testTimestamp, + expectErr: false, + expectedCheck: func(result time.Time) bool { + return result.Year() == 2023 && result.Month() == 6 && result.Day() == 15 && + result.Hour() == 14 && result.Minute() == 30 && result.Second() == 0 && + result.Nanosecond() == 0 + }, + }, + { + name: "Truncate from string date", + precision: "day", + value: &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: "2023-06-15 14:30:45"}}, + expectErr: false, + expectedCheck: func(result time.Time) bool { + // The result should be the start of day 2023-06-15 in local timezone + expectedDay := time.Date(2023, 6, 15, 0, 0, 0, 0, result.Location()) + return result.Equal(expectedDay) + }, + }, + { + name: "Truncate null value", + precision: "day", + value: nil, + expectErr: true, + expectedCheck: nil, + }, + { + name: "Invalid precision", + precision: "invalid", + value: testTimestamp, + expectErr: true, + expectedCheck: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := engine.DateTrunc(tt.precision, 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 result == nil { + t.Errorf("DateTrunc returned nil result") + return + } + + timestampVal, ok := result.Kind.(*schema_pb.Value_TimestampValue) + if !ok { + t.Errorf("DateTrunc should return timestamp value, got %T", result.Kind) + return + } + + resultTime := time.UnixMicro(timestampVal.TimestampValue.TimestampMicros) + + if !tt.expectedCheck(resultTime) { + t.Errorf("DateTrunc result check failed for precision %s, got time: %v", tt.precision, resultTime) + } + }) + } +} + +// TestDateTimeConstantsInSQL tests that datetime constants work in actual SQL queries +// This test reproduces the original bug where CURRENT_TIME returned empty values +func TestDateTimeConstantsInSQL(t *testing.T) { + engine := NewTestSQLEngine() + + t.Run("CURRENT_TIME in SQL query", func(t *testing.T) { + // This is the exact case that was failing + result, err := engine.ExecuteSQL(context.Background(), "SELECT CURRENT_TIME FROM user_events LIMIT 1") + + if err != nil { + t.Fatalf("SQL execution failed: %v", err) + } + + if result.Error != nil { + t.Fatalf("Query result has error: %v", result.Error) + } + + // Verify we have the correct column and non-empty values + if len(result.Columns) != 1 || result.Columns[0] != "current_time" { + t.Errorf("Expected column 'current_time', got %v", result.Columns) + } + + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + + timeValue := result.Rows[0][0].ToString() + if timeValue == "" { + t.Error("CURRENT_TIME should not return empty value") + } + + // Verify HH:MM:SS format + if len(timeValue) == 8 && timeValue[2] == ':' && timeValue[5] == ':' { + t.Logf("CURRENT_TIME returned valid time: %s", timeValue) + } else { + t.Errorf("CURRENT_TIME should return HH:MM:SS format, got: %s", timeValue) + } + }) + + t.Run("CURRENT_DATE in SQL query", func(t *testing.T) { + result, err := engine.ExecuteSQL(context.Background(), "SELECT CURRENT_DATE FROM user_events LIMIT 1") + + if err != nil { + t.Fatalf("SQL execution failed: %v", err) + } + + if result.Error != nil { + t.Fatalf("Query result has error: %v", result.Error) + } + + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + + dateValue := result.Rows[0][0].ToString() + if dateValue == "" { + t.Error("CURRENT_DATE should not return empty value") + } + + t.Logf("CURRENT_DATE returned: %s", dateValue) + }) +} + +// TestFunctionArgumentCountHandling tests that the function evaluation correctly handles +// both zero-argument and single-argument functions +func TestFunctionArgumentCountHandling(t *testing.T) { + engine := NewTestSQLEngine() + + t.Run("Zero-argument function should fail appropriately", func(t *testing.T) { + funcExpr := &FuncExpr{ + Name: testStringValue(FuncCURRENT_TIME), + Exprs: []SelectExpr{}, // Zero arguments - should fail since we removed zero-arg support + } + + result, err := engine.evaluateStringFunction(funcExpr, HybridScanResult{}) + if err == nil { + t.Error("Expected error for zero-argument function, but got none") + } + if result != nil { + t.Error("Expected nil result for zero-argument function") + } + + expectedError := "function CURRENT_TIME expects exactly 1 argument" + if err.Error() != expectedError { + t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) + } + }) + + t.Run("Single-argument function should still work", func(t *testing.T) { + funcExpr := &FuncExpr{ + Name: testStringValue(FuncUPPER), + Exprs: []SelectExpr{ + &AliasedExpr{ + Expr: &SQLVal{ + Type: StrVal, + Val: []byte("test"), + }, + }, + }, // Single argument - should work + } + + // Create a mock result + mockResult := HybridScanResult{} + + result, err := engine.evaluateStringFunction(funcExpr, mockResult) + if err != nil { + t.Errorf("Single-argument function failed: %v", err) + } + if result == nil { + t.Errorf("Single-argument function returned nil") + } + }) + + t.Run("Any zero-argument function should fail", func(t *testing.T) { + funcExpr := &FuncExpr{ + Name: testStringValue("INVALID_FUNCTION"), + Exprs: []SelectExpr{}, // Zero arguments - should fail + } + + result, err := engine.evaluateStringFunction(funcExpr, HybridScanResult{}) + if err == nil { + t.Error("Expected error for zero-argument function, got nil") + } + if result != nil { + t.Errorf("Expected nil result for zero-argument function, got %v", result) + } + + expectedError := "function INVALID_FUNCTION expects exactly 1 argument" + if err.Error() != expectedError { + t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) + } + }) + + t.Run("Wrong argument count for single-arg function should fail", func(t *testing.T) { + funcExpr := &FuncExpr{ + Name: testStringValue(FuncUPPER), + Exprs: []SelectExpr{ + &AliasedExpr{Expr: &SQLVal{Type: StrVal, Val: []byte("test1")}}, + &AliasedExpr{Expr: &SQLVal{Type: StrVal, Val: []byte("test2")}}, + }, // Two arguments - should fail for UPPER + } + + result, err := engine.evaluateStringFunction(funcExpr, HybridScanResult{}) + if err == nil { + t.Errorf("Expected error for wrong argument count, got nil") + } + if result != nil { + t.Errorf("Expected nil result for wrong argument count, got %v", result) + } + + expectedError := "function UPPER expects exactly 1 argument" + if err.Error() != expectedError { + t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error()) + } + }) +} + +// Helper function to create a string value for testing +func testStringValue(s string) StringGetter { + return &testStringValueImpl{value: s} +} + +type testStringValueImpl struct { + value string +} + +func (s *testStringValueImpl) String() string { + return s.value +} + +// TestExtractFunctionSQL tests the EXTRACT function through SQL execution +func TestExtractFunctionSQL(t *testing.T) { + engine := NewTestSQLEngine() + + testCases := []struct { + name string + sql string + expectError bool + checkValue func(t *testing.T, result *QueryResult) + }{ + { + name: "Extract YEAR from current_date", + sql: "SELECT EXTRACT(YEAR FROM current_date) AS year_value FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + yearStr := result.Rows[0][0].ToString() + currentYear := time.Now().Year() + if yearStr != fmt.Sprintf("%d", currentYear) { + t.Errorf("Expected current year %d, got %s", currentYear, yearStr) + } + }, + }, + { + name: "Extract MONTH from current_date", + sql: "SELECT EXTRACT('MONTH', current_date) AS month_value FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + monthStr := result.Rows[0][0].ToString() + currentMonth := time.Now().Month() + if monthStr != fmt.Sprintf("%d", int(currentMonth)) { + t.Errorf("Expected current month %d, got %s", int(currentMonth), monthStr) + } + }, + }, + { + name: "Extract DAY from current_date", + sql: "SELECT EXTRACT('DAY', current_date) AS day_value FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + dayStr := result.Rows[0][0].ToString() + currentDay := time.Now().Day() + if dayStr != fmt.Sprintf("%d", currentDay) { + t.Errorf("Expected current day %d, got %s", currentDay, dayStr) + } + }, + }, + { + name: "Extract HOUR from current_timestamp", + sql: "SELECT EXTRACT('HOUR', current_timestamp) AS hour_value FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + hourStr := result.Rows[0][0].ToString() + // Just check it's a valid hour (0-23) + hour, err := strconv.Atoi(hourStr) + if err != nil { + t.Errorf("Expected valid hour integer, got %s", hourStr) + } + if hour < 0 || hour > 23 { + t.Errorf("Expected hour 0-23, got %d", hour) + } + }, + }, + { + name: "Extract MINUTE from current_timestamp", + sql: "SELECT EXTRACT('MINUTE', current_timestamp) AS minute_value FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + minuteStr := result.Rows[0][0].ToString() + // Just check it's a valid minute (0-59) + minute, err := strconv.Atoi(minuteStr) + if err != nil { + t.Errorf("Expected valid minute integer, got %s", minuteStr) + } + if minute < 0 || minute > 59 { + t.Errorf("Expected minute 0-59, got %d", minute) + } + }, + }, + { + name: "Extract QUARTER from current_date", + sql: "SELECT EXTRACT('QUARTER', current_date) AS quarter_value FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + quarterStr := result.Rows[0][0].ToString() + quarter, err := strconv.Atoi(quarterStr) + if err != nil { + t.Errorf("Expected valid quarter integer, got %s", quarterStr) + } + if quarter < 1 || quarter > 4 { + t.Errorf("Expected quarter 1-4, got %d", quarter) + } + }, + }, + { + name: "Multiple EXTRACT functions", + sql: "SELECT EXTRACT(YEAR FROM current_date) AS year_val, EXTRACT(MONTH FROM current_date) AS month_val, EXTRACT(DAY FROM current_date) AS day_val FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + if len(result.Rows[0]) != 3 { + t.Fatalf("Expected 3 columns, got %d", len(result.Rows[0])) + } + + // Check year + yearStr := result.Rows[0][0].ToString() + currentYear := time.Now().Year() + if yearStr != fmt.Sprintf("%d", currentYear) { + t.Errorf("Expected current year %d, got %s", currentYear, yearStr) + } + + // Check month + monthStr := result.Rows[0][1].ToString() + currentMonth := time.Now().Month() + if monthStr != fmt.Sprintf("%d", int(currentMonth)) { + t.Errorf("Expected current month %d, got %s", int(currentMonth), monthStr) + } + + // Check day + dayStr := result.Rows[0][2].ToString() + currentDay := time.Now().Day() + if dayStr != fmt.Sprintf("%d", currentDay) { + t.Errorf("Expected current day %d, got %s", currentDay, dayStr) + } + }, + }, + { + name: "EXTRACT with invalid date part", + sql: "SELECT EXTRACT('INVALID_PART', current_date) FROM user_events LIMIT 1", + expectError: true, + checkValue: nil, + }, + { + name: "EXTRACT with wrong number of arguments", + sql: "SELECT EXTRACT('YEAR') FROM user_events LIMIT 1", + expectError: true, + checkValue: nil, + }, + { + name: "EXTRACT with too many arguments", + sql: "SELECT EXTRACT('YEAR', current_date, 'extra') FROM user_events LIMIT 1", + expectError: true, + checkValue: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := engine.ExecuteSQL(context.Background(), tc.sql) + + if tc.expectError { + if err == nil && result.Error == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if result.Error != nil { + t.Errorf("Query result has error: %v", result.Error) + return + } + + if tc.checkValue != nil { + tc.checkValue(t, result) + } + }) + } +} + +// TestDateTruncFunctionSQL tests the DATE_TRUNC function through SQL execution +func TestDateTruncFunctionSQL(t *testing.T) { + engine := NewTestSQLEngine() + + testCases := []struct { + name string + sql string + expectError bool + checkValue func(t *testing.T, result *QueryResult) + }{ + { + name: "DATE_TRUNC to day", + sql: "SELECT DATE_TRUNC('day', current_timestamp) AS truncated_day FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + // The result should be a timestamp value, just check it's not empty + timestampStr := result.Rows[0][0].ToString() + if timestampStr == "" { + t.Error("Expected non-empty timestamp result") + } + }, + }, + { + name: "DATE_TRUNC to hour", + sql: "SELECT DATE_TRUNC('hour', current_timestamp) AS truncated_hour FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + timestampStr := result.Rows[0][0].ToString() + if timestampStr == "" { + t.Error("Expected non-empty timestamp result") + } + }, + }, + { + name: "DATE_TRUNC to month", + sql: "SELECT DATE_TRUNC('month', current_timestamp) AS truncated_month FROM user_events LIMIT 1", + expectError: false, + checkValue: func(t *testing.T, result *QueryResult) { + if len(result.Rows) == 0 { + t.Fatal("Expected at least one row") + } + timestampStr := result.Rows[0][0].ToString() + if timestampStr == "" { + t.Error("Expected non-empty timestamp result") + } + }, + }, + { + name: "DATE_TRUNC with invalid precision", + sql: "SELECT DATE_TRUNC('invalid', current_timestamp) FROM user_events LIMIT 1", + expectError: true, + checkValue: nil, + }, + { + name: "DATE_TRUNC with wrong number of arguments", + sql: "SELECT DATE_TRUNC('day') FROM user_events LIMIT 1", + expectError: true, + checkValue: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := engine.ExecuteSQL(context.Background(), tc.sql) + + if tc.expectError { + if err == nil && result.Error == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if result.Error != nil { + t.Errorf("Query result has error: %v", result.Error) + return + } + + if tc.checkValue != nil { + tc.checkValue(t, result) + } + }) + } +} |
