Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
1 change: 1 addition & 0 deletions packages/query-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"effect": "catalog:effect"
},
"devDependencies": {
"@effect/vitest": "catalog:effect",
"@types/node": "catalog:tooling",
"typescript": "catalog:tooling",
"vitest": "catalog:"
Expand Down
44 changes: 22 additions & 22 deletions packages/query-engine/src/__sql_baseline__/catalog.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
21 changes: 11 additions & 10 deletions packages/query-engine/src/ch/queries/ci-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CH.Condition | undefined>(
(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, []),
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = <A>(n: number, make: (i: number) => A): ReadonlyArray<A> =>
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)
})
}
})
8 changes: 3 additions & 5 deletions packages/query-engine/src/ch/queries/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,11 @@ function logBodySearchCondition(body: CH.Expr<string>, 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)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'))",
)
})

Expand Down
5 changes: 1 addition & 4 deletions packages/query-engine/src/ch/queries/product-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CH.Condition | undefined>(
(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")),
Expand Down
Loading
Loading