Skip to content

Commit e6992ce

Browse files
committed
whereOr helper and use existing time filter
1 parent 5a625b2 commit e6992ce

File tree

3 files changed

+75
-58
lines changed

3 files changed

+75
-58
lines changed

apps/webapp/app/components/runs/v3/SharedFilters.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export function timeFilterFromTo(props: {
219219
from?: string | number;
220220
to?: string | number;
221221
defaultPeriod: string;
222-
}): { from: Date; to: Date } {
222+
}): { from: Date; to: Date; isDefault: boolean } {
223223
const time = timeFilters(props);
224224

225225
const periodMs = time.period ? parse(time.period) : undefined;
@@ -228,27 +228,31 @@ export function timeFilterFromTo(props: {
228228
return {
229229
from: new Date(Date.now() - periodMs),
230230
to: new Date(),
231+
isDefault: time.isDefault,
231232
};
232233
}
233234

234235
if (time.from && time.to) {
235236
return {
236237
from: time.from,
237238
to: time.to,
239+
isDefault: time.isDefault,
238240
};
239241
}
240242

241243
if (time.from) {
242244
return {
243245
from: time.from,
244246
to: new Date(),
247+
isDefault: time.isDefault,
245248
};
246249
}
247250

248251
const defaultPeriodMs = parse(props.defaultPeriod) ?? 24 * 60 * 60 * 1_000;
249252
return {
250253
from: new Date(Date.now() - defaultPeriodMs),
251254
to: time.to ?? new Date(),
255+
isDefault: time.isDefault,
252256
};
253257
}
254258

apps/webapp/app/presenters/v3/LogsListPresenter.server.ts

Lines changed: 37 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { z } from "zod";
2-
import { type ClickHouse } from "@internal/clickhouse";
3-
import {
4-
type PrismaClientOrTransaction,
5-
} from "@trigger.dev/database";
2+
import { type ClickHouse, type WhereCondition } from "@internal/clickhouse";
3+
import { type PrismaClientOrTransaction } from "@trigger.dev/database";
64
import { EVENT_STORE_TYPES, getConfiguredEventRepository } from "~/v3/eventRepository/index.server";
75

86
import parseDuration from "parse-duration";
97
import { type Direction } from "~/components/ListPagination";
10-
import { timeFilters } from "~/components/runs/v3/SharedFilters";
8+
import { timeFilterFromTo, timeFilters } from "~/components/runs/v3/SharedFilters";
119
import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
1210
import { getAllTaskIdentifiers } from "~/models/task.server";
1311
import { ServiceValidationError } from "~/v3/services/baseService.server";
@@ -28,14 +26,9 @@ type ErrorAttributes = {
2826
};
2927

3028
function escapeClickHouseString(val: string): string {
31-
return val
32-
.replace(/\\/g, "\\\\")
33-
.replace(/\//g, "\\/")
34-
.replace(/%/g, "\\%")
35-
.replace(/_/g, "\\_");
29+
return val.replace(/\\/g, "\\\\").replace(/\//g, "\\/").replace(/%/g, "\\%").replace(/_/g, "\\_");
3630
}
3731

38-
3932
export type LogsListOptions = {
4033
userId?: string;
4134
projectId: string;
@@ -153,24 +146,16 @@ export class LogsListPresenter extends BasePresenter {
153146
retentionLimitDays,
154147
}: LogsListOptions
155148
) {
156-
const time = timeFilters({
149+
const time = timeFilterFromTo({
157150
period,
158151
from,
159152
to,
160-
defaultPeriod,
153+
defaultPeriod: defaultPeriod ?? "1h",
161154
});
162155

163156
let effectiveFrom = time.from;
164157
let effectiveTo = time.to;
165158

166-
if (!effectiveFrom && !effectiveTo && time.period) {
167-
const periodMs = parseDuration(time.period);
168-
if (periodMs) {
169-
effectiveFrom = new Date(Date.now() - periodMs);
170-
effectiveTo = new Date();
171-
}
172-
}
173-
174159
// Apply retention limit if provided
175160
let wasClampedByRetention = false;
176161
if (retentionLimitDays !== undefined && effectiveFrom) {
@@ -250,11 +235,10 @@ export class LogsListPresenter extends BasePresenter {
250235
});
251236
queryBuilder.where("project_id = {projectId: String}", { projectId });
252237

253-
254238
if (effectiveFrom) {
255-
queryBuilder.where("triggered_timestamp >= {triggeredAtStart: DateTime64(3)}", {
256-
triggeredAtStart: convertDateToClickhouseDateTime(effectiveFrom),
257-
});
239+
queryBuilder.where("triggered_timestamp >= {triggeredAtStart: DateTime64(3)}", {
240+
triggeredAtStart: convertDateToClickhouseDateTime(effectiveFrom),
241+
});
258242
}
259243

260244
if (effectiveTo) {
@@ -283,50 +267,43 @@ export class LogsListPresenter extends BasePresenter {
283267
queryBuilder.where(
284268
"(lower(message) like {searchPattern: String} OR lower(attributes_text) like {searchPattern: String})",
285269
{
286-
searchPattern: `%${searchTerm}%`
270+
searchPattern: `%${searchTerm}%`,
287271
}
288272
);
289273
}
290274

291275
if (levels && levels.length > 0) {
292-
const conditions: string[] = [];
293-
const params: Record<string, string[]> = {};
276+
const conditions: WhereCondition[] = [];
294277

295-
for (const level of levels) {
296-
const filter = levelToKindsAndStatuses(level);
297-
const levelConditions: string[] = [];
278+
for (let i = 0; i < levels.length; i++) {
279+
const filter = levelToKindsAndStatuses(levels[i]);
298280

299281
if (filter.kinds && filter.kinds.length > 0) {
300-
const kindsKey = `kinds_${level}`;
301-
let kindCondition = `kind IN {${kindsKey}: Array(String)}`;
302-
303-
304-
kindCondition += ` AND status NOT IN {excluded_statuses: Array(String)}`;
305-
params["excluded_statuses"] = ["ERROR", "CANCELLED"];
306-
307-
308-
levelConditions.push(kindCondition);
309-
params[kindsKey] = filter.kinds;
282+
conditions.push({
283+
clause: `kind IN {kinds_${i}: Array(String)} AND status NOT IN {excluded_statuses: Array(String)}`,
284+
params: {
285+
[`kinds_${i}`]: filter.kinds,
286+
excluded_statuses: ["ERROR", "CANCELLED"],
287+
},
288+
});
310289
}
311290

312291
if (filter.statuses && filter.statuses.length > 0) {
313-
const statusesKey = `statuses_${level}`;
314-
levelConditions.push(`status IN {${statusesKey}: Array(String)}`);
315-
params[statusesKey] = filter.statuses;
316-
}
317-
318-
if (levelConditions.length > 0) {
319-
conditions.push(`(${levelConditions.join(" OR ")})`);
292+
conditions.push({
293+
clause: `status IN {statuses_${i}: Array(String)}`,
294+
params: { [`statuses_${i}`]: filter.statuses },
295+
});
320296
}
321297
}
322298

323-
if (conditions.length > 0) {
324-
queryBuilder.where(`(${conditions.join(" OR ")})`, params);
325-
}
299+
queryBuilder.whereOr(conditions);
326300
}
327301

328-
// Cursor pagination using explicit lexicographic comparison
329-
// Must mirror the ORDER BY columns: (organization_id, environment_id, triggered_timestamp, trace_id)
302+
// Cursor-based pagination using lexicographic comparison on (triggered_timestamp, trace_id).
303+
// Since ORDER BY is DESC, "next page" means rows that sort *after* the cursor, i.e. less-than.
304+
// The OR handles the tiebreaker: rows with an earlier timestamp always qualify, and rows
305+
// with the *same* timestamp only qualify if their trace_id is also smaller.
306+
// Equivalent to: WHERE (triggered_timestamp, trace_id) < (cursor.triggered_timestamp, cursor.trace_id)
330307
const decodedCursor = cursor ? decodeCursor(cursor) : null;
331308
if (decodedCursor) {
332309
queryBuilder.where(
@@ -428,10 +405,13 @@ export class LogsListPresenter extends BasePresenter {
428405
hasFilters,
429406
hasAnyLogs: transformedLogs.length > 0,
430407
searchTerm: search,
431-
retention: retentionLimitDays !== undefined ? {
432-
limitDays: retentionLimitDays,
433-
wasClamped: wasClampedByRetention,
434-
} : undefined,
408+
retention:
409+
retentionLimitDays !== undefined
410+
? {
411+
limitDays: retentionLimitDays,
412+
wasClamped: wasClampedByRetention,
413+
}
414+
: undefined,
435415
};
436416
}
437417
}

internal-packages/clickhouse/src/client/queryBuilder.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { ClickHouseSettings } from "@clickhouse/client";
44
export type QueryParamValue = string | number | boolean | Array<string | number | boolean> | null;
55
export type QueryParams = Record<string, QueryParamValue>;
66

7+
export type WhereCondition = {
8+
clause: string;
9+
params?: QueryParams;
10+
};
11+
712
export class ClickhouseQueryBuilder<TOutput> {
813
private name: string;
914
private baseQuery: string;
@@ -45,6 +50,20 @@ export class ClickhouseQueryBuilder<TOutput> {
4550
return this;
4651
}
4752

53+
whereOr(conditions: WhereCondition[]): this {
54+
if (conditions.length === 0) {
55+
return this;
56+
}
57+
const combinedClause = conditions.map((c) => `(${c.clause})`).join(" OR ");
58+
this.whereClauses.push(`(${combinedClause})`);
59+
for (const condition of conditions) {
60+
if (condition.params) {
61+
Object.assign(this.params, condition.params);
62+
}
63+
}
64+
return this;
65+
}
66+
4867
groupBy(clause: string): this {
4968
this.groupByClause = clause;
5069
return this;
@@ -153,6 +172,20 @@ export class ClickhouseQueryFastBuilder<TOutput extends Record<string, any>> {
153172
return this;
154173
}
155174

175+
whereOr(conditions: WhereCondition[]): this {
176+
if (conditions.length === 0) {
177+
return this;
178+
}
179+
const combinedClause = conditions.map((c) => `(${c.clause})`).join(" OR ");
180+
this.whereClauses.push(`(${combinedClause})`);
181+
for (const condition of conditions) {
182+
if (condition.params) {
183+
Object.assign(this.params, condition.params);
184+
}
185+
}
186+
return this;
187+
}
188+
156189
groupBy(clause: string): this {
157190
this.groupByClause = clause;
158191
return this;

0 commit comments

Comments
 (0)