@@ -411,6 +411,22 @@ export class ClickHouseRunsRepository implements IRunsRepository {
411411 }
412412}
413413
414+ /**
415+ * Builds the shared filter clauses for the runs list against `task_runs_v2 FINAL`.
416+ *
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 columns that are only ever set
420+ * once and never change: trigger-time identity columns (task_identifier, task_version, schedule_id,
421+ * is_test, root_run_id, batch_id, friendly_id, queue, task_kind), append-only arrays under
422+ * `hasAny`/`hasAll` (tags, bulk_action_group_ids), `region` (set once at dequeue, `''` -> value,
423+ * never changes), and `error_fingerprint` (empty until a terminal error status, then fixed). Those
424+ * go in PREWHERE to filter (and, for tags, use the skip index) before FINAL and before materialising
425+ * the wide columns, which is what bounds memory on these scans. Columns that change as a run runs
426+ * stay in WHERE (post-FINAL): `status` (lifecycle) and `machine_preset` (escalates on OOM retry).
427+ * The `(organization_id, project_id, environment_id)` primary-key prefix and the `created_at` range
428+ * also stay in WHERE so they keep driving primary-key and partition pruning.
429+ */
414430function applyRunFiltersToQueryBuilder < T > (
415431 queryBuilder : ClickhouseQueryBuilder < T > ,
416432 options : FilterRunsOptions
@@ -426,28 +442,14 @@ function applyRunFiltersToQueryBuilder<T>(
426442 environmentId : options . environmentId ,
427443 } ) ;
428444
429- if ( options . tasks && options . tasks . length > 0 ) {
430- queryBuilder . where ( "task_identifier IN {tasks: Array(String)}" , { tasks : options . tasks } ) ;
431- }
432-
433- if ( options . versions && options . versions . length > 0 ) {
434- queryBuilder . where ( "task_version IN {versions: Array(String)}" , {
435- versions : options . versions ,
436- } ) ;
437- }
438-
439445 if ( options . statuses && options . statuses . length > 0 ) {
440446 queryBuilder . where ( "status IN {statuses: Array(String)}" , { statuses : options . statuses } ) ;
441447 }
442448
443- if ( options . tags && options . tags . length > 0 ) {
444- // Both hasAny and hasAll are served by the tags bloom_filter skip index.
445- const tagsFn = options . tagsMatch === "all" ? "hasAll" : "hasAny" ;
446- queryBuilder . where ( `${ tagsFn } (tags, {tags: Array(String)})` , { tags : options . tags } ) ;
447- }
448-
449- if ( options . scheduleId ) {
450- queryBuilder . where ( "schedule_id = {scheduleId: String}" , { scheduleId : options . scheduleId } ) ;
449+ if ( options . machines && options . machines . length > 0 ) {
450+ queryBuilder . where ( "machine_preset IN {machines: Array(String)}" , {
451+ machines : options . machines ,
452+ } ) ;
451453 }
452454
453455 // Period is a number of milliseconds duration
@@ -467,49 +469,63 @@ function applyRunFiltersToQueryBuilder<T>(
467469 queryBuilder . where ( "created_at <= fromUnixTimestamp64Milli({to: Int64})" , { to : options . to } ) ;
468470 }
469471
472+ if ( options . tasks && options . tasks . length > 0 ) {
473+ queryBuilder . prewhere ( "task_identifier IN {tasks: Array(String)}" , { tasks : options . tasks } ) ;
474+ }
475+
476+ if ( options . versions && options . versions . length > 0 ) {
477+ queryBuilder . prewhere ( "task_version IN {versions: Array(String)}" , {
478+ versions : options . versions ,
479+ } ) ;
480+ }
481+
482+ if ( options . tags && options . tags . length > 0 ) {
483+ // Both hasAny and hasAll are served by the tags bloom_filter skip index.
484+ const tagsFn = options . tagsMatch === "all" ? "hasAll" : "hasAny" ;
485+ queryBuilder . prewhere ( `${ tagsFn } (tags, {tags: Array(String)})` , { tags : options . tags } ) ;
486+ }
487+
488+ if ( options . scheduleId ) {
489+ queryBuilder . prewhere ( "schedule_id = {scheduleId: String}" , {
490+ scheduleId : options . scheduleId ,
491+ } ) ;
492+ }
493+
470494 if ( typeof options . isTest === "boolean" ) {
471- queryBuilder . where ( "is_test = {isTest: Boolean}" , { isTest : options . isTest } ) ;
495+ queryBuilder . prewhere ( "is_test = {isTest: Boolean}" , { isTest : options . isTest } ) ;
472496 }
473497
474498 if ( options . rootOnly ) {
475- queryBuilder . where ( "root_run_id = ''" ) ;
499+ queryBuilder . prewhere ( "root_run_id = ''" ) ;
476500 }
477501
478502 if ( options . batchId ) {
479- queryBuilder . where ( "batch_id = {batchId: String}" , { batchId : options . batchId } ) ;
503+ queryBuilder . prewhere ( "batch_id = {batchId: String}" , { batchId : options . batchId } ) ;
480504 }
481505
482506 if ( options . bulkId ) {
483- queryBuilder . where ( "hasAny(bulk_action_group_ids, {bulkActionGroupIds: Array(String)})" , {
507+ queryBuilder . prewhere ( "hasAny(bulk_action_group_ids, {bulkActionGroupIds: Array(String)})" , {
484508 bulkActionGroupIds : [ options . bulkId ] ,
485509 } ) ;
486510 }
487511
488512 if ( options . runId && options . runId . length > 0 ) {
489513 // it's important that in the query it's "runIds", otherwise it clashes with the cursor which is called "runId"
490- queryBuilder . where ( "friendly_id IN {runIds: Array(String)}" , {
514+ queryBuilder . prewhere ( "friendly_id IN {runIds: Array(String)}" , {
491515 runIds : options . runId . map ( ( runId ) => RunId . toFriendlyId ( runId ) ) ,
492516 } ) ;
493517 }
494518
495519 if ( options . queues && options . queues . length > 0 ) {
496- queryBuilder . where ( "queue IN {queues: Array(String)}" , { queues : options . queues } ) ;
520+ queryBuilder . prewhere ( "queue IN {queues: Array(String)}" , { queues : options . queues } ) ;
497521 }
498522
499523 if ( options . regions && options . regions . length > 0 ) {
500- queryBuilder . where ( "if(region != '', region, worker_queue) IN {regions: Array(String)}" , {
501- regions : options . regions ,
502- } ) ;
503- }
504-
505- if ( options . machines && options . machines . length > 0 ) {
506- queryBuilder . where ( "machine_preset IN {machines: Array(String)}" , {
507- machines : options . machines ,
508- } ) ;
524+ queryBuilder . prewhere ( "region IN {regions: Array(String)}" , { regions : options . regions } ) ;
509525 }
510526
511527 if ( options . errorId ) {
512- queryBuilder . where ( "error_fingerprint = {errorFingerprint: String}" , {
528+ queryBuilder . prewhere ( "error_fingerprint = {errorFingerprint: String}" , {
513529 errorFingerprint : ErrorId . toId ( options . errorId ) ,
514530 } ) ;
515531 }
@@ -520,11 +536,11 @@ function applyRunFiltersToQueryBuilder<T>(
520536 const effectiveKinds = includesStandard ? [ ...options . taskKinds , "" ] : options . taskKinds ;
521537
522538 if ( effectiveKinds . length === 1 ) {
523- queryBuilder . where ( "task_kind = {taskKind: String}" , {
539+ queryBuilder . prewhere ( "task_kind = {taskKind: String}" , {
524540 taskKind : effectiveKinds [ 0 ] ! ,
525541 } ) ;
526542 } else {
527- queryBuilder . where ( "task_kind IN {taskKinds: Array(String)}" , {
543+ queryBuilder . prewhere ( "task_kind IN {taskKinds: Array(String)}" , {
528544 taskKinds : effectiveKinds ,
529545 } ) ;
530546 }
0 commit comments