diff --git a/weed/query/engine/sql_functions.go b/weed/query/engine/sql_functions.go index 1b2638d55..b1f9de3bc 100644 --- a/weed/query/engine/sql_functions.go +++ b/weed/query/engine/sql_functions.go @@ -4,6 +4,8 @@ import ( "fmt" "math" "strconv" + "strings" + "time" "github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" ) @@ -250,3 +252,149 @@ func (e *SQLEngine) Abs(value *schema_pb.Value) (*schema_pb.Value, error) { Kind: &schema_pb.Value_DoubleValue{DoubleValue: result}, }, nil } + +// =============================== +// DATE/TIME CONSTANTS +// =============================== + +// CurrentDate returns the current date as a string in YYYY-MM-DD format +func (e *SQLEngine) CurrentDate() (*schema_pb.Value, error) { + now := time.Now() + dateStr := now.Format("2006-01-02") + + return &schema_pb.Value{ + Kind: &schema_pb.Value_StringValue{StringValue: dateStr}, + }, nil +} + +// CurrentTimestamp returns the current timestamp +func (e *SQLEngine) CurrentTimestamp() (*schema_pb.Value, error) { + now := time.Now() + + // Return as TimestampValue with microseconds + timestampMicros := now.UnixMicro() + + return &schema_pb.Value{ + Kind: &schema_pb.Value_TimestampValue{ + TimestampValue: &schema_pb.TimestampValue{ + TimestampMicros: timestampMicros, + }, + }, + }, nil +} + +// CurrentTime returns the current time as a string in HH:MM:SS format +func (e *SQLEngine) CurrentTime() (*schema_pb.Value, error) { + now := time.Now() + timeStr := now.Format("15:04:05") + + return &schema_pb.Value{ + Kind: &schema_pb.Value_StringValue{StringValue: timeStr}, + }, nil +} + +// Now is an alias for CurrentTimestamp (common SQL function name) +func (e *SQLEngine) Now() (*schema_pb.Value, error) { + return e.CurrentTimestamp() +} + +// =============================== +// EXTRACT FUNCTION +// =============================== + +// DatePart represents the part of a date/time to extract +type DatePart string + +const ( + PartYear DatePart = "YEAR" + PartMonth DatePart = "MONTH" + PartDay DatePart = "DAY" + PartHour DatePart = "HOUR" + PartMinute DatePart = "MINUTE" + PartSecond DatePart = "SECOND" + PartWeek DatePart = "WEEK" + PartDayOfYear DatePart = "DOY" + PartDayOfWeek DatePart = "DOW" + PartQuarter DatePart = "QUARTER" + PartEpoch DatePart = "EPOCH" +) + +// Extract extracts a specific part from a date/time value +func (e *SQLEngine) Extract(part DatePart, value *schema_pb.Value) (*schema_pb.Value, error) { + if value == nil { + return nil, fmt.Errorf("EXTRACT function requires non-null value") + } + + // Convert value to time + t, err := e.valueToTime(value) + if err != nil { + return nil, fmt.Errorf("EXTRACT function time conversion error: %v", err) + } + + var result int64 + + switch strings.ToUpper(string(part)) { + case string(PartYear): + result = int64(t.Year()) + case string(PartMonth): + result = int64(t.Month()) + case string(PartDay): + result = int64(t.Day()) + case string(PartHour): + result = int64(t.Hour()) + case string(PartMinute): + result = int64(t.Minute()) + case string(PartSecond): + result = int64(t.Second()) + case string(PartWeek): + _, week := t.ISOWeek() + result = int64(week) + case string(PartDayOfYear): + result = int64(t.YearDay()) + case string(PartDayOfWeek): + result = int64(t.Weekday()) + case string(PartQuarter): + month := t.Month() + result = int64((month-1)/3 + 1) + case string(PartEpoch): + result = t.Unix() + default: + return nil, fmt.Errorf("unsupported date part: %s", part) + } + + return &schema_pb.Value{ + Kind: &schema_pb.Value_Int64Value{Int64Value: result}, + }, nil +} + +// Helper function to convert schema_pb.Value to time.Time +func (e *SQLEngine) valueToTime(value *schema_pb.Value) (time.Time, error) { + switch v := value.Kind.(type) { + case *schema_pb.Value_TimestampValue: + if v.TimestampValue == nil { + return time.Time{}, fmt.Errorf("null timestamp value") + } + return time.UnixMicro(v.TimestampValue.TimestampMicros), nil + case *schema_pb.Value_StringValue: + // Try to parse various date/time string formats + dateFormats := []string{ + "2006-01-02 15:04:05", + "2006-01-02T15:04:05Z", + "2006-01-02T15:04:05", + "2006-01-02", + "15:04:05", + } + + for _, format := range dateFormats { + if t, err := time.Parse(format, v.StringValue); err == nil { + return t, nil + } + } + return time.Time{}, fmt.Errorf("unable to parse date/time string: %s", v.StringValue) + case *schema_pb.Value_Int64Value: + // Assume Unix timestamp (seconds) + return time.Unix(v.Int64Value, 0), nil + default: + return time.Time{}, fmt.Errorf("cannot convert value type to date/time") + } +} diff --git a/weed/query/engine/sql_functions_test.go b/weed/query/engine/sql_functions_test.go index 9746dca79..06d4a8037 100644 --- a/weed/query/engine/sql_functions_test.go +++ b/weed/query/engine/sql_functions_test.go @@ -2,6 +2,7 @@ package engine import ( "testing" + "time" "github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" ) @@ -528,3 +529,241 @@ func TestMathematicalFunctions(t *testing.T) { } }) } + +func TestDateTimeFunctions(t *testing.T) { + engine := NewTestSQLEngine() + + t.Run("CURRENT_DATE function tests", func(t *testing.T) { + result, err := engine.CurrentDate() + 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) + today := time.Now().Format("2006-01-02") + if stringVal.StringValue != today { + t.Errorf("Expected current date %s, got %s", today, 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 + if timestamp.Before(before) || timestamp.After(after) { + t.Errorf("Timestamp %v should be between %v and %v", timestamp, before, after) + } + }) + + 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) + } + }) + } +}