aboutsummaryrefslogtreecommitdiff
path: root/weed/query/engine/datetime_functions_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/query/engine/datetime_functions_test.go')
-rw-r--r--weed/query/engine/datetime_functions_test.go891
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)
+ }
+ })
+ }
+}