@@ -414,15 +414,19 @@ export class ClickHouseRunsRepository implements IRunsRepository {
414414/**
415415 * Builds the shared filter clauses for the runs list against `task_runs_v2 FINAL`.
416416 *
417- * Immutable / additive-only columns go in PREWHERE so ClickHouse filters (and, for `tags`, uses
418- * the skip index) before FINAL reconciles versions and before materialising the wide columns,
419- * which is what bounds memory on these scans. A filter stays in WHERE (post-FINAL) when its truth
420- * value can flip across a run's versions, since PREWHERE could then keep a stale version and drop
421- * the winner: `status` (lifecycle-mutable), and `regions` (its `if(region != '', region,
422- * worker_queue)` expression yields the worker_queue before dequeue and the region after, two
423- * different non-empty values). The `(organization_id, project_id, environment_id)` primary-key
424- * prefix and the `created_at` range stay in WHERE so they keep driving primary-key and partition
425- * pruning.
417+ * A filter may go in PREWHERE only if its truth value can never flip true->false across a run's
418+ * versions, because PREWHERE is evaluated before FINAL reconciles versions and would otherwise keep
419+ * a stale matching version and drop the winning one. That holds for trigger-time identity columns
420+ * that never change (task_identifier, task_version, schedule_id, is_test, root_run_id, batch_id,
421+ * friendly_id, queue, task_kind) and for append-only arrays under `hasAny`/`hasAll` (tags,
422+ * bulk_action_group_ids), so those go in PREWHERE to filter (and, for tags, use the skip index)
423+ * before FINAL and before materialising the wide columns, which is what bounds memory on these
424+ * scans. Columns that reflect execution/outcome and change as a run runs stay in WHERE (post-FINAL):
425+ * `status`, `machine_preset` (can escalate on OOM retry), `error_fingerprint` (set/cleared with
426+ * status), and the `regions` expression (`if(region != '', region, worker_queue)` yields the
427+ * worker_queue before dequeue and the region after). The `(organization_id, project_id,
428+ * environment_id)` primary-key prefix and the `created_at` range also stay in WHERE so they keep
429+ * driving primary-key and partition pruning.
426430 */
427431function applyRunFiltersToQueryBuilder < T > (
428432 queryBuilder : ClickhouseQueryBuilder < T > ,
@@ -449,6 +453,18 @@ function applyRunFiltersToQueryBuilder<T>(
449453 } ) ;
450454 }
451455
456+ if ( options . machines && options . machines . length > 0 ) {
457+ queryBuilder . where ( "machine_preset IN {machines: Array(String)}" , {
458+ machines : options . machines ,
459+ } ) ;
460+ }
461+
462+ if ( options . errorId ) {
463+ queryBuilder . where ( "error_fingerprint = {errorFingerprint: String}" , {
464+ errorFingerprint : ErrorId . toId ( options . errorId ) ,
465+ } ) ;
466+ }
467+
452468 // Period is a number of milliseconds duration
453469 if ( options . period ) {
454470 queryBuilder . where ( "created_at >= fromUnixTimestamp64Milli({period: Int64})" , {
@@ -517,18 +533,6 @@ function applyRunFiltersToQueryBuilder<T>(
517533 queryBuilder . prewhere ( "queue IN {queues: Array(String)}" , { queues : options . queues } ) ;
518534 }
519535
520- if ( options . machines && options . machines . length > 0 ) {
521- queryBuilder . prewhere ( "machine_preset IN {machines: Array(String)}" , {
522- machines : options . machines ,
523- } ) ;
524- }
525-
526- if ( options . errorId ) {
527- queryBuilder . prewhere ( "error_fingerprint = {errorFingerprint: String}" , {
528- errorFingerprint : ErrorId . toId ( options . errorId ) ,
529- } ) ;
530- }
531-
532536 if ( options . taskKinds && options . taskKinds . length > 0 ) {
533537 const includesStandard = options . taskKinds . includes ( "STANDARD" ) ;
534538 // Include empty string when filtering for STANDARD (default value for pre-existing runs)
0 commit comments