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.
		
		
		
		
		
			
		
			
				
					
					
						
							272 lines
						
					
					
						
							9.0 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							272 lines
						
					
					
						
							9.0 KiB
						
					
					
				
								package engine
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"testing"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// TestFastPathPredicateValidation tests the critical fix for fast-path aggregation
							 | 
						|
								// to ensure non-time predicates are properly detected and fast-path is blocked
							 | 
						|
								func TestFastPathPredicateValidation(t *testing.T) {
							 | 
						|
									engine := NewTestSQLEngine()
							 | 
						|
								
							 | 
						|
									testCases := []struct {
							 | 
						|
										name                string
							 | 
						|
										whereClause         string
							 | 
						|
										expectedTimeOnly    bool
							 | 
						|
										expectedStartTimeNs int64
							 | 
						|
										expectedStopTimeNs  int64
							 | 
						|
										description         string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name:             "No WHERE clause",
							 | 
						|
											whereClause:      "",
							 | 
						|
											expectedTimeOnly: true, // No WHERE means time-only is true
							 | 
						|
											description:      "Queries without WHERE clause should allow fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:                "Time-only predicate (greater than)",
							 | 
						|
											whereClause:         "_ts > 1640995200000000000",
							 | 
						|
											expectedTimeOnly:    true,
							 | 
						|
											expectedStartTimeNs: 1640995200000000000,
							 | 
						|
											expectedStopTimeNs:  0,
							 | 
						|
											description:         "Pure time predicates should allow fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:                "Time-only predicate (less than)",
							 | 
						|
											whereClause:         "_ts < 1640995200000000000",
							 | 
						|
											expectedTimeOnly:    true,
							 | 
						|
											expectedStartTimeNs: 0,
							 | 
						|
											expectedStopTimeNs:  1640995200000000000,
							 | 
						|
											description:         "Pure time predicates should allow fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:                "Time-only predicate (range with AND)",
							 | 
						|
											whereClause:         "_ts > 1640995200000000000 AND _ts < 1641081600000000000",
							 | 
						|
											expectedTimeOnly:    true,
							 | 
						|
											expectedStartTimeNs: 1640995200000000000,
							 | 
						|
											expectedStopTimeNs:  1641081600000000000,
							 | 
						|
											description:         "Time range predicates should allow fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:             "Mixed predicate (time + non-time)",
							 | 
						|
											whereClause:      "_ts > 1640995200000000000 AND user_id = 'user123'",
							 | 
						|
											expectedTimeOnly: false,
							 | 
						|
											description:      "CRITICAL: Mixed predicates must block fast path to prevent incorrect results",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:             "Non-time predicate only",
							 | 
						|
											whereClause:      "user_id = 'user123'",
							 | 
						|
											expectedTimeOnly: false,
							 | 
						|
											description:      "Non-time predicates must block fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:             "Multiple non-time predicates",
							 | 
						|
											whereClause:      "user_id = 'user123' AND status = 'active'",
							 | 
						|
											expectedTimeOnly: false,
							 | 
						|
											description:      "Multiple non-time predicates must block fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:             "OR with time predicate (unsafe)",
							 | 
						|
											whereClause:      "_ts > 1640995200000000000 OR user_id = 'user123'",
							 | 
						|
											expectedTimeOnly: false,
							 | 
						|
											description:      "OR expressions are complex and must block fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:             "OR with only time predicates (still unsafe)",
							 | 
						|
											whereClause:      "_ts > 1640995200000000000 OR _ts < 1640908800000000000",
							 | 
						|
											expectedTimeOnly: false,
							 | 
						|
											description:      "Even time-only OR expressions must block fast path due to complexity",
							 | 
						|
										},
							 | 
						|
										// Note: Parenthesized expressions are not supported by the current parser
							 | 
						|
										// These test cases are commented out until parser support is added
							 | 
						|
										{
							 | 
						|
											name:             "String column comparison",
							 | 
						|
											whereClause:      "event_type = 'click'",
							 | 
						|
											expectedTimeOnly: false,
							 | 
						|
											description:      "String column comparisons must block fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:             "Numeric column comparison",
							 | 
						|
											whereClause:      "id > 1000",
							 | 
						|
											expectedTimeOnly: false,
							 | 
						|
											description:      "Numeric column comparisons must block fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:                "Internal timestamp column",
							 | 
						|
											whereClause:         "_timestamp_ns > 1640995200000000000",
							 | 
						|
											expectedTimeOnly:    true,
							 | 
						|
											expectedStartTimeNs: 1640995200000000000,
							 | 
						|
											description:         "Internal timestamp column should allow fast path",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tc := range testCases {
							 | 
						|
										t.Run(tc.name, func(t *testing.T) {
							 | 
						|
											// Parse the WHERE clause if present
							 | 
						|
											var whereExpr ExprNode
							 | 
						|
											if tc.whereClause != "" {
							 | 
						|
												sql := "SELECT COUNT(*) FROM test WHERE " + tc.whereClause
							 | 
						|
												stmt, err := ParseSQL(sql)
							 | 
						|
												if err != nil {
							 | 
						|
													t.Fatalf("Failed to parse SQL: %v", err)
							 | 
						|
												}
							 | 
						|
												selectStmt := stmt.(*SelectStatement)
							 | 
						|
												whereExpr = selectStmt.Where.Expr
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Test the validation function
							 | 
						|
											var startTimeNs, stopTimeNs int64
							 | 
						|
											var onlyTimePredicates bool
							 | 
						|
								
							 | 
						|
											if whereExpr == nil {
							 | 
						|
												// No WHERE clause case
							 | 
						|
												onlyTimePredicates = true
							 | 
						|
											} else {
							 | 
						|
												startTimeNs, stopTimeNs, onlyTimePredicates = engine.SQLEngine.extractTimeFiltersWithValidation(whereExpr)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Verify the results
							 | 
						|
											if onlyTimePredicates != tc.expectedTimeOnly {
							 | 
						|
												t.Errorf("Expected onlyTimePredicates=%v, got %v. %s",
							 | 
						|
													tc.expectedTimeOnly, onlyTimePredicates, tc.description)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Check time filters if expected
							 | 
						|
											if tc.expectedStartTimeNs != 0 && startTimeNs != tc.expectedStartTimeNs {
							 | 
						|
												t.Errorf("Expected startTimeNs=%d, got %d", tc.expectedStartTimeNs, startTimeNs)
							 | 
						|
											}
							 | 
						|
											if tc.expectedStopTimeNs != 0 && stopTimeNs != tc.expectedStopTimeNs {
							 | 
						|
												t.Errorf("Expected stopTimeNs=%d, got %d", tc.expectedStopTimeNs, stopTimeNs)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											t.Logf("✅ %s: onlyTimePredicates=%v, startTimeNs=%d, stopTimeNs=%d",
							 | 
						|
												tc.name, onlyTimePredicates, startTimeNs, stopTimeNs)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestFastPathAggregationSafety tests that fast-path aggregation is only attempted
							 | 
						|
								// when it's safe to do so (no non-time predicates)
							 | 
						|
								func TestFastPathAggregationSafety(t *testing.T) {
							 | 
						|
									engine := NewTestSQLEngine()
							 | 
						|
								
							 | 
						|
									testCases := []struct {
							 | 
						|
										name              string
							 | 
						|
										sql               string
							 | 
						|
										shouldUseFastPath bool
							 | 
						|
										description       string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name:              "No WHERE - should use fast path",
							 | 
						|
											sql:               "SELECT COUNT(*) FROM test",
							 | 
						|
											shouldUseFastPath: true,
							 | 
						|
											description:       "Queries without WHERE should use fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:              "Time-only WHERE - should use fast path",
							 | 
						|
											sql:               "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000",
							 | 
						|
											shouldUseFastPath: true,
							 | 
						|
											description:       "Time-only predicates should use fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:              "Mixed WHERE - should NOT use fast path",
							 | 
						|
											sql:               "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000 AND user_id = 'user123'",
							 | 
						|
											shouldUseFastPath: false,
							 | 
						|
											description:       "CRITICAL: Mixed predicates must NOT use fast path to prevent wrong results",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:              "Non-time WHERE - should NOT use fast path",
							 | 
						|
											sql:               "SELECT COUNT(*) FROM test WHERE user_id = 'user123'",
							 | 
						|
											shouldUseFastPath: false,
							 | 
						|
											description:       "Non-time predicates must NOT use fast path",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:              "OR expression - should NOT use fast path",
							 | 
						|
											sql:               "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000 OR user_id = 'user123'",
							 | 
						|
											shouldUseFastPath: false,
							 | 
						|
											description:       "OR expressions must NOT use fast path due to complexity",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tc := range testCases {
							 | 
						|
										t.Run(tc.name, func(t *testing.T) {
							 | 
						|
											// Parse the SQL
							 | 
						|
											stmt, err := ParseSQL(tc.sql)
							 | 
						|
											if err != nil {
							 | 
						|
												t.Fatalf("Failed to parse SQL: %v", err)
							 | 
						|
											}
							 | 
						|
											selectStmt := stmt.(*SelectStatement)
							 | 
						|
								
							 | 
						|
											// Test the fast path decision logic
							 | 
						|
											startTimeNs, stopTimeNs := int64(0), int64(0)
							 | 
						|
											onlyTimePredicates := true
							 | 
						|
											if selectStmt.Where != nil {
							 | 
						|
												startTimeNs, stopTimeNs, onlyTimePredicates = engine.SQLEngine.extractTimeFiltersWithValidation(selectStmt.Where.Expr)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											canAttemptFastPath := selectStmt.Where == nil || onlyTimePredicates
							 | 
						|
								
							 | 
						|
											// Verify the decision
							 | 
						|
											if canAttemptFastPath != tc.shouldUseFastPath {
							 | 
						|
												t.Errorf("Expected canAttemptFastPath=%v, got %v. %s",
							 | 
						|
													tc.shouldUseFastPath, canAttemptFastPath, tc.description)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											t.Logf("✅ %s: canAttemptFastPath=%v (onlyTimePredicates=%v, startTimeNs=%d, stopTimeNs=%d)",
							 | 
						|
												tc.name, canAttemptFastPath, onlyTimePredicates, startTimeNs, stopTimeNs)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestTimestampColumnDetection tests that the engine correctly identifies timestamp columns
							 | 
						|
								func TestTimestampColumnDetection(t *testing.T) {
							 | 
						|
									engine := NewTestSQLEngine()
							 | 
						|
								
							 | 
						|
									testCases := []struct {
							 | 
						|
										columnName  string
							 | 
						|
										isTimestamp bool
							 | 
						|
										description string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											columnName:  "_ts",
							 | 
						|
											isTimestamp: true,
							 | 
						|
											description: "System timestamp display column should be detected",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											columnName:  "_timestamp_ns",
							 | 
						|
											isTimestamp: true,
							 | 
						|
											description: "Internal timestamp column should be detected",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											columnName:  "user_id",
							 | 
						|
											isTimestamp: false,
							 | 
						|
											description: "Non-timestamp column should not be detected as timestamp",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											columnName:  "id",
							 | 
						|
											isTimestamp: false,
							 | 
						|
											description: "ID column should not be detected as timestamp",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											columnName:  "status",
							 | 
						|
											isTimestamp: false,
							 | 
						|
											description: "Status column should not be detected as timestamp",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											columnName:  "event_type",
							 | 
						|
											isTimestamp: false,
							 | 
						|
											description: "Event type column should not be detected as timestamp",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tc := range testCases {
							 | 
						|
										t.Run(tc.columnName, func(t *testing.T) {
							 | 
						|
											isTimestamp := engine.SQLEngine.isTimestampColumn(tc.columnName)
							 | 
						|
											if isTimestamp != tc.isTimestamp {
							 | 
						|
												t.Errorf("Expected isTimestampColumn(%s)=%v, got %v. %s",
							 | 
						|
													tc.columnName, tc.isTimestamp, isTimestamp, tc.description)
							 | 
						|
											}
							 | 
						|
											t.Logf("✅ Column '%s': isTimestamp=%v", tc.columnName, isTimestamp)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 |