From 56e9da620f54141afdca2af37b9df78ae15b4f42 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Sat, 31 Oct 2026 17:48:45 +0100 Subject: [PATCH] fix(query-engine): keep list inputs from nesting SQL past max_parser_depth The Code Review Team tab failed with TOO_DEEP_RECURSION for orgs with a few hundred repositories: ciActorStats chained one `.or()` per default branch, nesting a paren per repository until ClickHouse's max_parser_depth (1000). Log body search (one `.and()` per word) and attribute-filter OR groups had the same shape and failed at ~300 items. Each is now one flat CH.or / CH.and. A property test checks that list inputs from 1 to 2000 items never make the SQL deeper than two items do, and a ClickHouse e2e case runs actor stats over 500 repositories. --- bun.lock | 1 + .../warehouse/ci-runs.clickhouse.e2e.test.ts | 14 +++ packages/query-engine/package.json | 1 + .../src/__sql_baseline__/catalog.sql | 44 ++++----- .../query-engine/src/ch/queries/ci-runs.ts | 21 ++--- .../list-input-nesting.property.test.ts | 90 +++++++++++++++++++ packages/query-engine/src/ch/queries/logs.ts | 8 +- .../src/ch/queries/product-events.test.ts | 2 +- .../src/ch/queries/product-events.ts | 5 +- packages/query-engine/src/traces-shared.ts | 5 +- 10 files changed, 146 insertions(+), 45 deletions(-) create mode 100644 packages/query-engine/src/ch/queries/list-input-nesting.property.test.ts diff --git a/bun.lock b/bun.lock index 2c656df51e..601860fc3d 100644 --- a/bun.lock +++ b/bun.lock @@ -825,6 +825,7 @@ "effect": "catalog:effect", }, "devDependencies": { + "@effect/vitest": "catalog:effect", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", "vitest": "catalog:", diff --git a/packages/backend/src/services/warehouse/ci-runs.clickhouse.e2e.test.ts b/packages/backend/src/services/warehouse/ci-runs.clickhouse.e2e.test.ts index 61b53475b0..27d40b8def 100644 --- a/packages/backend/src/services/warehouse/ci-runs.clickhouse.e2e.test.ts +++ b/packages/backend/src/services/warehouse/ci-runs.clickhouse.e2e.test.ts @@ -361,6 +361,20 @@ describe.skipIf(!clickhouseE2eEnabled)("ci_runs / ci_jobs materialization", () = assert.deepStrictEqual(traceIds(await read(CH.ciRunsQuery({ pullRequest: 5 }))), ["r2", "r1"]) }) + it("counts actors against an org with hundreds of repositories", async () => { + // One clause per default branch: a nested OR chain here passed ClickHouse's max_parser_depth. + const defaultBranches = Array.from({ length: 500 }, (_, i) => ({ + owner: "acme", + repository: `repo-${i}`, + branch: "main", + })) + const rows = await read(CH.ciActorStatsQuery({ defaultBranches })) + assert.deepStrictEqual( + rows.map(({ actor, runs, defaultBranchRuns }) => ({ actor, runs, defaultBranchRuns })), + [{ actor: "octocat", runs: 3, defaultBranchRuns: 0 }], + ) + }) + it("finds a workflow's red streak and the failed jobs of its runs", async () => { const ran = await read(CH.ciRunsQuery({ pipeline: "CI", ranOnly: true })) assert.deepStrictEqual(traceIds(ran), ["r5", "r2", "r1"]) diff --git a/packages/query-engine/package.json b/packages/query-engine/package.json index 963b34595d..280cf0a26d 100644 --- a/packages/query-engine/package.json +++ b/packages/query-engine/package.json @@ -35,6 +35,7 @@ "effect": "catalog:effect" }, "devDependencies": { + "@effect/vitest": "catalog:effect", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", "vitest": "catalog:" diff --git a/packages/query-engine/src/__sql_baseline__/catalog.sql b/packages/query-engine/src/__sql_baseline__/catalog.sql index 528ab6aa16..6df7c90588 100644 --- a/packages/query-engine/src/__sql_baseline__/catalog.sql +++ b/packages/query-engine/src/__sql_baseline__/catalog.sql @@ -407,7 +407,7 @@ SELECT LIMIT 200 FORMAT JSON --- builder:ci-runs:ciActorStatsQuery:allWorkflows [d856dc60] +-- builder:ci-runs:ciActorStatsQuery:allWorkflows [890fdba4] SELECT ci_runs.Actor AS actor, count() AS runs, @@ -416,8 +416,8 @@ SELECT countIf(ci_runs.Attempt > 1) AS reruns, countIf(ci_runs.Result = 'cancellation') AS cancelledRuns, sum(jobs.runnerMs) AS runnerMs, - countIf(((ci_runs.TriggerEvent = 'push' AND ((ci_runs.Owner = 'acme' AND ci_runs.Repository = 'web') AND ci_runs.Branch = 'main')) AND (ci_runs.Result = 'success' OR ci_runs.Result IN ('failure', 'timeout', 'error')))) AS defaultBranchRuns, - countIf(((ci_runs.TriggerEvent = 'push' AND ((ci_runs.Owner = 'acme' AND ci_runs.Repository = 'web') AND ci_runs.Branch = 'main')) AND ci_runs.Result IN ('failure', 'timeout', 'error'))) AS defaultBranchFailed + countIf(((ci_runs.TriggerEvent = 'push' AND (ci_runs.Owner = 'acme' AND ci_runs.Repository = 'web' AND ci_runs.Branch = 'main')) AND (ci_runs.Result = 'success' OR ci_runs.Result IN ('failure', 'timeout', 'error')))) AS defaultBranchRuns, + countIf(((ci_runs.TriggerEvent = 'push' AND (ci_runs.Owner = 'acme' AND ci_runs.Repository = 'web' AND ci_runs.Branch = 'main')) AND ci_runs.Result IN ('failure', 'timeout', 'error'))) AS defaultBranchFailed FROM ci_runs INNER JOIN (SELECT ci_jobs.TraceId AS TraceId, @@ -3229,7 +3229,7 @@ SELECT LIMIT 50 FORMAT JSON --- builder:product-events:productEventsFunnelBreakdownQuery:attribute-session-step [b3496b8e] +-- builder:product-events:productEventsFunnelBreakdownQuery:attribute-session-step [c28317af] SELECT groups.group AS group, arrayJoin([1, 2, 3, 4]) AS step, @@ -3272,7 +3272,7 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND e.UserId != '' ) AS funnel_events GROUP BY key) AS levels @@ -3282,7 +3282,7 @@ SELECT ORDER BY group ASC, step ASC FORMAT JSON --- builder:product-events:productEventsFunnelBreakdownQuery:session-dimension [bb00cfab] +-- builder:product-events:productEventsFunnelBreakdownQuery:session-dimension [f210b7b0] SELECT groups.group AS group, arrayJoin([1, 2, 3]) AS step, @@ -3326,7 +3326,7 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND multiIf(e.UserId != '', e.UserId, coalesce(link.UserId, '') != '', coalesce(link.UserId, ''), e.VisitorId) != '') AS funnel_events GROUP BY key) AS levels GROUP BY group @@ -3335,7 +3335,7 @@ SELECT ORDER BY group ASC, step ASC FORMAT JSON --- builder:product-events:productEventsFunnelLeaversQuery:visitor-filtered [ad6d68b1] +-- builder:product-events:productEventsFunnelLeaversQuery:visitor-filtered [95bb693c] SELECT hops.step AS step, tupleElement(hop, 1) AS next, @@ -3424,7 +3424,7 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND e.VisitorId != '' AND e.VisitorId IN (SELECT session_replays.VisitorId AS key @@ -3472,7 +3472,7 @@ SELECT ORDER BY step ASC, count DESC, next ASC FORMAT JSON --- builder:product-events:productEventsFunnelQuery:person [0ee5feae] +-- builder:product-events:productEventsFunnelQuery:person [877c57db] SELECT arrayJoin([1, 2, 3]) AS step, arrayElement(totals.counts, step) AS count @@ -3503,13 +3503,13 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND multiIf(e.UserId != '', e.UserId, coalesce(link.UserId, '') != '', coalesce(link.UserId, ''), e.VisitorId) != '') AS funnel_events GROUP BY key) AS levels) AS totals ORDER BY step ASC FORMAT JSON --- builder:product-events:productEventsFunnelQuery:session-key [76d377e9] +-- builder:product-events:productEventsFunnelQuery:session-key [118d45ba] SELECT arrayJoin([1, 2, 3]) AS step, arrayElement(totals.counts, step) AS count @@ -3529,13 +3529,13 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND e.SessionId != '') AS funnel_events GROUP BY key) AS levels) AS totals ORDER BY step ASC FORMAT JSON --- builder:product-events:productEventsFunnelQuery:session-step-filtered [dbec8c07] +-- builder:product-events:productEventsFunnelQuery:session-step-filtered [7bae7ef8] SELECT arrayJoin([1, 2, 3, 4]) AS step, arrayElement(totals.counts, step) AS count @@ -3641,7 +3641,7 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND multiIf(e.UserId != '', e.UserId, coalesce(link.UserId, '') != '', coalesce(link.UserId, ''), e.VisitorId) != '' AND multiIf(e.UserId != '', e.UserId, coalesce(link.UserId, '') != '', coalesce(link.UserId, ''), e.VisitorId) IN (SELECT multiIf(s.UserId != '', s.UserId, coalesce(link.UserId, '') != '', coalesce(link.UserId, ''), s.VisitorId) AS key @@ -3695,7 +3695,7 @@ SELECT ORDER BY step ASC FORMAT JSON --- builder:product-events:productEventsFunnelQuery:visitor-session-step [c63a28ff] +-- builder:product-events:productEventsFunnelQuery:visitor-session-step [59b281c2] SELECT arrayJoin([1, 2, 3, 4]) AS step, arrayElement(totals.counts, step) AS count @@ -3732,14 +3732,14 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND e.VisitorId != '' ) AS funnel_events GROUP BY key) AS levels) AS totals ORDER BY step ASC FORMAT JSON --- builder:product-events:productEventsFunnelTimingQuery:person [78104129] +-- builder:product-events:productEventsFunnelTimingQuery:person [82b59764] SELECT arrayJoin([2, 3]) AS step, arrayElement(totals.p50s, step) AS p50Ms, @@ -3779,7 +3779,7 @@ SELECT WHERE e.OrgId = 'org_sql_catalog' AND e.Timestamp >= '2026-01-01 10:30:00' AND e.Timestamp <= '2026-01-03 14:15:00' - AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) + AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AND multiIf(e.UserId != '', e.UserId, coalesce(link.UserId, '') != '', coalesce(link.UserId, ''), e.VisitorId) != '') AS funnel_events GROUP BY key) AS chain_events) AS chain) AS totals ORDER BY step ASC @@ -11795,7 +11795,7 @@ SELECT LIMIT 50 FORMAT JSON --- pipe:list_logs:searched:bloom [74bedc54] +-- pipe:list_logs:searched:bloom [226ed830] SELECT logs.Timestamp AS timestamp, toString(logs.Timestamp) AS exactTimestamp, @@ -11817,7 +11817,7 @@ SELECT AND logs.ServiceName = 'api' AND logs.SeverityText IN ('ERROR', 'Error', 'error') AND logs.TraceId = '0af7651916cd43dd8448eb211c80319c' - AND (((hasToken(lower(logs.Body), 'connection') AND hasToken(lower(logs.Body), 'refused')) AND hasToken(lower(logs.Body), 'by')) AND logs.Body ILIKE '%upstream connection refused by peer%') + AND (hasToken(lower(logs.Body), 'connection') AND hasToken(lower(logs.Body), 'refused') AND hasToken(lower(logs.Body), 'by') AND logs.Body ILIKE '%upstream connection refused by peer%') AND logs.Timestamp >= (SELECT min(ts) FROM (SELECT logs.Timestamp AS ts FROM logs @@ -11829,7 +11829,7 @@ SELECT AND logs.ServiceName = 'api' AND logs.SeverityText IN ('ERROR', 'Error', 'error') AND logs.TraceId = '0af7651916cd43dd8448eb211c80319c' - AND (((hasToken(lower(logs.Body), 'connection') AND hasToken(lower(logs.Body), 'refused')) AND hasToken(lower(logs.Body), 'by')) AND logs.Body ILIKE '%upstream connection refused by peer%') + AND (hasToken(lower(logs.Body), 'connection') AND hasToken(lower(logs.Body), 'refused') AND hasToken(lower(logs.Body), 'by') AND logs.Body ILIKE '%upstream connection refused by peer%') ORDER BY ts DESC LIMIT 50)) ORDER BY timestamp DESC, serviceName ASC, traceId ASC, spanId ASC, recordIdentity ASC diff --git a/packages/query-engine/src/ch/queries/ci-runs.ts b/packages/query-engine/src/ch/queries/ci-runs.ts index c3568aa11b..24d0a50fdb 100644 --- a/packages/query-engine/src/ch/queries/ci-runs.ts +++ b/packages/query-engine/src/ch/queries/ci-runs.ts @@ -297,16 +297,17 @@ export function ciActorStatsQuery(opts: CiActorStatsOpts) { const firstTry = $.Attempt.lte(1) const passed = $.Result.eq("success") const failed = CH.inList($.Result, CI_FAILED_RESULTS) - const isDefaultBranch = opts.defaultBranches - .map((repo) => - $.Owner.eq(repo.owner) - .and($.Repository.eq(repo.repository)) - .and($.Branch.eq(repo.branch)), - ) - .reduce( - (all, next) => (all === undefined ? next : all.or(next)), - undefined, - ) + // One flat OR: chaining `.or()` nests a paren per repository and an org with a few + // hundred repositories passes ClickHouse's max_parser_depth (1000). + const isDefaultBranch = CH.or( + ...opts.defaultBranches.map((repo) => + CH.and( + $.Owner.eq(repo.owner), + $.Repository.eq(repo.repository), + $.Branch.eq(repo.branch), + ), + ), + ) // No repository: an empty `inList` compiles to `1 = 0`, so nothing counts as the default branch. const onDefaultBranch = $.TriggerEvent.eq("push").and( isDefaultBranch ?? CH.inList($.Branch, []), diff --git a/packages/query-engine/src/ch/queries/list-input-nesting.property.test.ts b/packages/query-engine/src/ch/queries/list-input-nesting.property.test.ts new file mode 100644 index 0000000000..1d7ce2228e --- /dev/null +++ b/packages/query-engine/src/ch/queries/list-input-nesting.property.test.ts @@ -0,0 +1,90 @@ +// ClickHouse refuses a query nested deeper than max_parser_depth (1000). A builder that chains +// `.or()` / `.and()` once per list item nests one level per item and reaches it at a few hundred, +// so a list input must never make the SQL deeper. Fixtures use one or two items and cannot see it. +import { describe, expect, it } from "@effect/vitest" +import { Schema } from "effect" +import { compileUnsafe } from "@maple-dev/effect-orm/clickhouse" +import { ciActorStatsQuery } from "./ci-runs" +import { logsCountQuery } from "./logs" +import { tracesListQuery } from "./traces" + +const params = { orgId: "org_nesting", startTime: "2026-10-01 00:00:00", endTime: "2026-10-02 00:00:00" } + +/** Deepest parenthesis nesting outside string literals. */ +const nestingDepth = (sql: string): number => { + let depth = 0 + let deepest = 0 + let quoted = false + for (let i = 0; i < sql.length; i++) { + const char = sql[i] + if (char === "\\" && quoted) i++ + else if (char === "'") quoted = !quoted + else if (!quoted && char === "(") deepest = Math.max(deepest, ++depth) + else if (!quoted && char === ")") depth-- + } + return deepest +} + +const listSize = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 2_000 })) + +const items = (n: number, make: (i: number) => A): ReadonlyArray => + Array.from({ length: n }, (_, i) => make(i)) +const words = (n: number) => items(n + 2, (i) => `word${i}`).join(" ") +const orGroup = (n: number) => ({ + key: "http.route", + value: "/0", + mode: "equals" as const, + or: items(n, (i) => ({ key: "http.route", value: `/${i + 1}`, mode: "equals" as const })), +}) +const filters = (n: number) => items(n, (i) => ({ key: `attr.${i}`, value: "v", mode: "equals" as const })) + +const cases: ReadonlyArray<{ readonly name: string; readonly sql: (n: number) => string }> = [ + { + name: "CI actor stats over many repositories' default branches", + sql: (n) => + compileUnsafe( + ciActorStatsQuery({ + defaultBranches: items(n, (i) => ({ + owner: "acme", + repository: `repo-${i}`, + branch: "main", + })), + }), + params, + ).sql, + }, + { + name: "log search with many words (token index)", + sql: (n) => + compileUnsafe(logsCountQuery({ search: words(n), bodySearchMode: "tokenbf" }), params).sql, + }, + { + name: "log search with many words (text index)", + sql: (n) => compileUnsafe(logsCountQuery({ search: words(n), bodySearchMode: "text" }), params).sql, + }, + { + name: "log attribute OR group", + sql: (n) => compileUnsafe(logsCountQuery({ attributeFilters: [orGroup(n)] }), params).sql, + }, + { + name: "many log attribute filters", + sql: (n) => compileUnsafe(logsCountQuery({ attributeFilters: filters(n) }), params).sql, + }, + { + name: "span attribute OR group", + sql: (n) => compileUnsafe(tracesListQuery({ attributeFilters: [orGroup(n)] }), params).sql, + }, + { + name: "many span attribute filters", + sql: (n) => compileUnsafe(tracesListQuery({ attributeFilters: filters(n) }), params).sql, + }, +] + +describe("list inputs never deepen the SQL", () => { + for (const { name, sql } of cases) { + const twoItems = nestingDepth(sql(2)) + it.prop(name, [listSize], ([n]) => { + expect(nestingDepth(sql(n))).toBeLessThanOrEqual(twoItems) + }) + } +}) diff --git a/packages/query-engine/src/ch/queries/logs.ts b/packages/query-engine/src/ch/queries/logs.ts index dca47607d0..c84dff2b03 100644 --- a/packages/query-engine/src/ch/queries/logs.ts +++ b/packages/query-engine/src/ch/queries/logs.ts @@ -93,13 +93,11 @@ function logBodySearchCondition(body: CH.Expr, opts: LogsQueryOpts): CH. for (let offset = 0; offset < tokens.length; offset += 50) { batches.push(CH.hasAllTokens(normalizedBody, tokens.slice(offset, offset + 50).join(" "))) } - return batches.reduce((condition, next) => condition.and(next)).and(exact) + return CH.and(...batches, exact) } - return tokens - .map((token) => CH.hasToken(normalizedBody, token)) - .reduce((condition, next) => condition.and(next)) - .and(exact) + // One flat AND: a chained `.and()` per word passes ClickHouse's max_parser_depth on a pasted stack trace. + return CH.and(...tokens.map((token) => CH.hasToken(normalizedBody, token)), exact) } /** diff --git a/packages/query-engine/src/ch/queries/product-events.test.ts b/packages/query-engine/src/ch/queries/product-events.test.ts index 20c508235a..445e3279e8 100644 --- a/packages/query-engine/src/ch/queries/product-events.test.ts +++ b/packages/query-engine/src/ch/queries/product-events.test.ts @@ -89,7 +89,7 @@ describe("productEventsFunnelQuery", () => { "toUInt8((e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup')) AS s3", ) expect(oneLine(sql)).toContain( - "AND ((((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed') OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup'))", + "AND (((e.Kind = 'navigation' AND e.PagePath = '/pricing') AND e.Host = 'maple.dev') OR e.EventName = 'signup_completed' OR (e.EventName = 'plan_started' AND e.Attributes['plan'] = 'startup'))", ) }) diff --git a/packages/query-engine/src/ch/queries/product-events.ts b/packages/query-engine/src/ch/queries/product-events.ts index cbddc25c78..3cccb283c8 100644 --- a/packages/query-engine/src/ch/queries/product-events.ts +++ b/packages/query-engine/src/ch/queries/product-events.ts @@ -453,10 +453,7 @@ function eventsBranch(plan: FunnelPlan): FunnelBranch { const stepConditions = opts.steps .map((step) => eventStepCondition($, step)) .filter((cond): cond is CH.Condition => cond !== undefined) - const anyStep = stepConditions.reduce( - (acc, cond) => (acc ? acc.or(cond) : cond), - undefined, - ) + const anyStep = stepConditions.length === 0 ? undefined : CH.or(...stepConditions) return [ $.OrgId.eq(orgIdParam), $.Timestamp.gte(param.dateTime("startTime")), diff --git a/packages/query-engine/src/traces-shared.ts b/packages/query-engine/src/traces-shared.ts index 91477e3656..f7ccd4641e 100644 --- a/packages/query-engine/src/traces-shared.ts +++ b/packages/query-engine/src/traces-shared.ts @@ -127,9 +127,8 @@ export function buildAttrFilterCondition( // than the exact OR and only adds granule reads. if (af.or?.length) { const { or, ...first } = af - return [first, ...or] - .map((member) => buildAttrFilterCondition(member, mapName, "none")) - .reduce((acc, cond) => acc.or(cond)) + // One flat OR: a chained `.or()` per member nests past ClickHouse's max_parser_depth. + return CH.or(...[first, ...or].map((member) => buildAttrFilterCondition(member, mapName, "none"))) } // `Attributes` (product events, metric datapoints) carries no skip index, so the exact predicate stands alone. const indexMode: AttributeIndexMode = mapName === "Attributes" ? "none" : requestedIndexMode