fast_path_predicate_validation_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package engine
  2. import (
  3. "testing"
  4. )
  5. // TestFastPathPredicateValidation tests the critical fix for fast-path aggregation
  6. // to ensure non-time predicates are properly detected and fast-path is blocked
  7. func TestFastPathPredicateValidation(t *testing.T) {
  8. engine := NewTestSQLEngine()
  9. testCases := []struct {
  10. name string
  11. whereClause string
  12. expectedTimeOnly bool
  13. expectedStartTimeNs int64
  14. expectedStopTimeNs int64
  15. description string
  16. }{
  17. {
  18. name: "No WHERE clause",
  19. whereClause: "",
  20. expectedTimeOnly: true, // No WHERE means time-only is true
  21. description: "Queries without WHERE clause should allow fast path",
  22. },
  23. {
  24. name: "Time-only predicate (greater than)",
  25. whereClause: "_ts > 1640995200000000000",
  26. expectedTimeOnly: true,
  27. expectedStartTimeNs: 1640995200000000000,
  28. expectedStopTimeNs: 0,
  29. description: "Pure time predicates should allow fast path",
  30. },
  31. {
  32. name: "Time-only predicate (less than)",
  33. whereClause: "_ts < 1640995200000000000",
  34. expectedTimeOnly: true,
  35. expectedStartTimeNs: 0,
  36. expectedStopTimeNs: 1640995200000000000,
  37. description: "Pure time predicates should allow fast path",
  38. },
  39. {
  40. name: "Time-only predicate (range with AND)",
  41. whereClause: "_ts > 1640995200000000000 AND _ts < 1641081600000000000",
  42. expectedTimeOnly: true,
  43. expectedStartTimeNs: 1640995200000000000,
  44. expectedStopTimeNs: 1641081600000000000,
  45. description: "Time range predicates should allow fast path",
  46. },
  47. {
  48. name: "Mixed predicate (time + non-time)",
  49. whereClause: "_ts > 1640995200000000000 AND user_id = 'user123'",
  50. expectedTimeOnly: false,
  51. description: "CRITICAL: Mixed predicates must block fast path to prevent incorrect results",
  52. },
  53. {
  54. name: "Non-time predicate only",
  55. whereClause: "user_id = 'user123'",
  56. expectedTimeOnly: false,
  57. description: "Non-time predicates must block fast path",
  58. },
  59. {
  60. name: "Multiple non-time predicates",
  61. whereClause: "user_id = 'user123' AND status = 'active'",
  62. expectedTimeOnly: false,
  63. description: "Multiple non-time predicates must block fast path",
  64. },
  65. {
  66. name: "OR with time predicate (unsafe)",
  67. whereClause: "_ts > 1640995200000000000 OR user_id = 'user123'",
  68. expectedTimeOnly: false,
  69. description: "OR expressions are complex and must block fast path",
  70. },
  71. {
  72. name: "OR with only time predicates (still unsafe)",
  73. whereClause: "_ts > 1640995200000000000 OR _ts < 1640908800000000000",
  74. expectedTimeOnly: false,
  75. description: "Even time-only OR expressions must block fast path due to complexity",
  76. },
  77. // Note: Parenthesized expressions are not supported by the current parser
  78. // These test cases are commented out until parser support is added
  79. {
  80. name: "String column comparison",
  81. whereClause: "event_type = 'click'",
  82. expectedTimeOnly: false,
  83. description: "String column comparisons must block fast path",
  84. },
  85. {
  86. name: "Numeric column comparison",
  87. whereClause: "id > 1000",
  88. expectedTimeOnly: false,
  89. description: "Numeric column comparisons must block fast path",
  90. },
  91. {
  92. name: "Internal timestamp column",
  93. whereClause: "_timestamp_ns > 1640995200000000000",
  94. expectedTimeOnly: true,
  95. expectedStartTimeNs: 1640995200000000000,
  96. description: "Internal timestamp column should allow fast path",
  97. },
  98. }
  99. for _, tc := range testCases {
  100. t.Run(tc.name, func(t *testing.T) {
  101. // Parse the WHERE clause if present
  102. var whereExpr ExprNode
  103. if tc.whereClause != "" {
  104. sql := "SELECT COUNT(*) FROM test WHERE " + tc.whereClause
  105. stmt, err := ParseSQL(sql)
  106. if err != nil {
  107. t.Fatalf("Failed to parse SQL: %v", err)
  108. }
  109. selectStmt := stmt.(*SelectStatement)
  110. whereExpr = selectStmt.Where.Expr
  111. }
  112. // Test the validation function
  113. var startTimeNs, stopTimeNs int64
  114. var onlyTimePredicates bool
  115. if whereExpr == nil {
  116. // No WHERE clause case
  117. onlyTimePredicates = true
  118. } else {
  119. startTimeNs, stopTimeNs, onlyTimePredicates = engine.SQLEngine.extractTimeFiltersWithValidation(whereExpr)
  120. }
  121. // Verify the results
  122. if onlyTimePredicates != tc.expectedTimeOnly {
  123. t.Errorf("Expected onlyTimePredicates=%v, got %v. %s",
  124. tc.expectedTimeOnly, onlyTimePredicates, tc.description)
  125. }
  126. // Check time filters if expected
  127. if tc.expectedStartTimeNs != 0 && startTimeNs != tc.expectedStartTimeNs {
  128. t.Errorf("Expected startTimeNs=%d, got %d", tc.expectedStartTimeNs, startTimeNs)
  129. }
  130. if tc.expectedStopTimeNs != 0 && stopTimeNs != tc.expectedStopTimeNs {
  131. t.Errorf("Expected stopTimeNs=%d, got %d", tc.expectedStopTimeNs, stopTimeNs)
  132. }
  133. t.Logf("✅ %s: onlyTimePredicates=%v, startTimeNs=%d, stopTimeNs=%d",
  134. tc.name, onlyTimePredicates, startTimeNs, stopTimeNs)
  135. })
  136. }
  137. }
  138. // TestFastPathAggregationSafety tests that fast-path aggregation is only attempted
  139. // when it's safe to do so (no non-time predicates)
  140. func TestFastPathAggregationSafety(t *testing.T) {
  141. engine := NewTestSQLEngine()
  142. testCases := []struct {
  143. name string
  144. sql string
  145. shouldUseFastPath bool
  146. description string
  147. }{
  148. {
  149. name: "No WHERE - should use fast path",
  150. sql: "SELECT COUNT(*) FROM test",
  151. shouldUseFastPath: true,
  152. description: "Queries without WHERE should use fast path",
  153. },
  154. {
  155. name: "Time-only WHERE - should use fast path",
  156. sql: "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000",
  157. shouldUseFastPath: true,
  158. description: "Time-only predicates should use fast path",
  159. },
  160. {
  161. name: "Mixed WHERE - should NOT use fast path",
  162. sql: "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000 AND user_id = 'user123'",
  163. shouldUseFastPath: false,
  164. description: "CRITICAL: Mixed predicates must NOT use fast path to prevent wrong results",
  165. },
  166. {
  167. name: "Non-time WHERE - should NOT use fast path",
  168. sql: "SELECT COUNT(*) FROM test WHERE user_id = 'user123'",
  169. shouldUseFastPath: false,
  170. description: "Non-time predicates must NOT use fast path",
  171. },
  172. {
  173. name: "OR expression - should NOT use fast path",
  174. sql: "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000 OR user_id = 'user123'",
  175. shouldUseFastPath: false,
  176. description: "OR expressions must NOT use fast path due to complexity",
  177. },
  178. }
  179. for _, tc := range testCases {
  180. t.Run(tc.name, func(t *testing.T) {
  181. // Parse the SQL
  182. stmt, err := ParseSQL(tc.sql)
  183. if err != nil {
  184. t.Fatalf("Failed to parse SQL: %v", err)
  185. }
  186. selectStmt := stmt.(*SelectStatement)
  187. // Test the fast path decision logic
  188. startTimeNs, stopTimeNs := int64(0), int64(0)
  189. onlyTimePredicates := true
  190. if selectStmt.Where != nil {
  191. startTimeNs, stopTimeNs, onlyTimePredicates = engine.SQLEngine.extractTimeFiltersWithValidation(selectStmt.Where.Expr)
  192. }
  193. canAttemptFastPath := selectStmt.Where == nil || onlyTimePredicates
  194. // Verify the decision
  195. if canAttemptFastPath != tc.shouldUseFastPath {
  196. t.Errorf("Expected canAttemptFastPath=%v, got %v. %s",
  197. tc.shouldUseFastPath, canAttemptFastPath, tc.description)
  198. }
  199. t.Logf("✅ %s: canAttemptFastPath=%v (onlyTimePredicates=%v, startTimeNs=%d, stopTimeNs=%d)",
  200. tc.name, canAttemptFastPath, onlyTimePredicates, startTimeNs, stopTimeNs)
  201. })
  202. }
  203. }
  204. // TestTimestampColumnDetection tests that the engine correctly identifies timestamp columns
  205. func TestTimestampColumnDetection(t *testing.T) {
  206. engine := NewTestSQLEngine()
  207. testCases := []struct {
  208. columnName string
  209. isTimestamp bool
  210. description string
  211. }{
  212. {
  213. columnName: "_ts",
  214. isTimestamp: true,
  215. description: "System timestamp display column should be detected",
  216. },
  217. {
  218. columnName: "_timestamp_ns",
  219. isTimestamp: true,
  220. description: "Internal timestamp column should be detected",
  221. },
  222. {
  223. columnName: "user_id",
  224. isTimestamp: false,
  225. description: "Non-timestamp column should not be detected as timestamp",
  226. },
  227. {
  228. columnName: "id",
  229. isTimestamp: false,
  230. description: "ID column should not be detected as timestamp",
  231. },
  232. {
  233. columnName: "status",
  234. isTimestamp: false,
  235. description: "Status column should not be detected as timestamp",
  236. },
  237. {
  238. columnName: "event_type",
  239. isTimestamp: false,
  240. description: "Event type column should not be detected as timestamp",
  241. },
  242. }
  243. for _, tc := range testCases {
  244. t.Run(tc.columnName, func(t *testing.T) {
  245. isTimestamp := engine.SQLEngine.isTimestampColumn(tc.columnName)
  246. if isTimestamp != tc.isTimestamp {
  247. t.Errorf("Expected isTimestampColumn(%s)=%v, got %v. %s",
  248. tc.columnName, tc.isTimestamp, isTimestamp, tc.description)
  249. }
  250. t.Logf("✅ Column '%s': isTimestamp=%v", tc.columnName, isTimestamp)
  251. })
  252. }
  253. }