Skip to content
Open
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
38 changes: 37 additions & 1 deletion packages/db/src/services/chart-sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
import type { IChartBreakdown, IChartEvent } from '@openpanel/validation';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { ch } from '../clickhouse/client';
import { ch, formatClickhouseDate } from '../clickhouse/client';
import {
getAggregateChartSql as _getAggregateChartSql,
getChartSql as _getChartSql,
Expand Down Expand Up @@ -713,3 +713,39 @@ describe('chart.service / single-pass total_count', () => {
await explain(sql);
});
});

describe('chart.service / profile event window', () => {
for (const [name, build] of [
['timeseries', getChartSql],
['aggregate', getAggregateChartSql],
] as const) {
it(`${name} restricts profile reads to the same project, event and dates`, async () => {
const sql = await build({
event: event(),
breakdowns: [breakdown('profile.properties.plan')],
interval: 'day',
startDate: '2026-09-01 00:00:00',
endDate: '2026-09-02 00:00:00',
projectId: PROJECT_ID,
timezone: 'UTC',
});
expect(sql).toContain(`id IN (SELECT profile_id FROM events WHERE project_id = '${PROJECT_ID}' AND created_at >= toDateTime('${formatClickhouseDate('2026-09-01 00:00:00')}') AND created_at <= toDateTime('${formatClickhouseDate('2026-09-02 00:00:00')}') AND name = ('screen_view'))`);
expect(sql).toContain('FROM profiles FINAL');
if (chReachable) await explain(sql);
});

it(`${name} keeps all event names for wildcard series`, async () => {
const sql = await build({
event: event({ name: '*' }),
breakdowns: [breakdown('profile.properties.plan')],
interval: 'day',
startDate: '',
endDate: '',
projectId: PROJECT_ID,
timezone: 'UTC',
});
expect(sql).toContain(`id IN (SELECT profile_id FROM events WHERE project_id = '${PROJECT_ID}')`);
expect(sql).not.toContain("name = '*'");
});
}
});
33 changes: 30 additions & 3 deletions packages/db/src/services/chart.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
type IReportInput,
} from '@openpanel/validation';
import sqlstring from 'sqlstring';
import { formatClickhouseDate, TABLE_NAMES } from '../clickhouse/client';
import { ch, formatClickhouseDate, TABLE_NAMES } from '../clickhouse/client';
import { clix } from '../clickhouse/query-builder';
import { db } from '../prisma-client';
import { createSqlBuilder } from '../sql-builder';
import { buildTypedClause, hasTypedCast, isTypedOperator } from './filter-cast';
Expand Down Expand Up @@ -462,6 +463,30 @@ export function rewriteProfilePropertyRefs(sql: string, keys: string[]): string
return out;
}

function profileEventWindow({
projectId,
startDate,
endDate,
event,
}: Pick<IGetChartDataInput, 'projectId' | 'startDate' | 'endDate' | 'event'>) {
const query = clix(ch)
.select(['profile_id'])
.from(TABLE_NAMES.events)
.where('project_id', '=', projectId);
if (startDate) {
query.where('created_at', '>=', clix.datetime(startDate, 'toDateTime'));
}
if (endDate) {
query.where('created_at', '<=', clix.datetime(endDate, 'toDateTime'));
}
if (event.name !== '*') {
// Event names must remain string literals, even when they look like dates.
query.where('name', '=', clix.exp(sqlstring.escape(event.name)));
}
// Filter by the join key before FINAL without excluding profiles updated outside the event window.
return `id IN (${query.toSQL()})`;
}

export async function getChartSql({
event,
breakdowns: initialBreakdowns,
Expand Down Expand Up @@ -691,7 +716,8 @@ export async function getChartSql({
'profile',
`SELECT ${selectFields.join(', ')}
FROM ${TABLE_NAMES.profiles} FINAL
WHERE project_id = ${sqlstring.escape(projectId)}`
WHERE project_id = ${sqlstring.escape(projectId)}
AND ${profileEventWindow({ projectId, startDate, endDate, event })}`
);

// Use the CTE reference in the main query
Expand Down Expand Up @@ -1073,7 +1099,8 @@ export async function getAggregateChartSql({
'profile',
`SELECT ${selectFields.join(', ')}
FROM ${TABLE_NAMES.profiles} FINAL
WHERE project_id = ${sqlstring.escape(projectId)}`
WHERE project_id = ${sqlstring.escape(projectId)}
AND ${profileEventWindow({ projectId, startDate, endDate, event })}`
);

sb.joins.profiles = profilesJoinRef;
Expand Down