You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							891 lines
						
					
					
						
							25 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							891 lines
						
					
					
						
							25 KiB
						
					
					
				| 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) | |
| 			} | |
| 		}) | |
| 	} | |
| }
 |