fix(api): handle profile filters/breakdowns better
This commit is contained in:
@@ -24,7 +24,7 @@ type CTE = {
|
|||||||
query: Query | string;
|
query: Query | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS';
|
type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS' | 'LEFT ANY';
|
||||||
|
|
||||||
type WhereCondition = {
|
type WhereCondition = {
|
||||||
condition: string;
|
condition: string;
|
||||||
@@ -61,7 +61,7 @@ export class Query<T = any> {
|
|||||||
private _ctes: CTE[] = [];
|
private _ctes: CTE[] = [];
|
||||||
private _joins: {
|
private _joins: {
|
||||||
type: JoinType;
|
type: JoinType;
|
||||||
table: string | Expression;
|
table: string | Expression | Query;
|
||||||
condition: string;
|
condition: string;
|
||||||
alias?: string;
|
alias?: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
@@ -280,13 +280,21 @@ export class Query<T = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
leftJoin(
|
leftJoin(
|
||||||
table: string | Expression,
|
table: string | Expression | Query,
|
||||||
condition: string,
|
condition: string,
|
||||||
alias?: string,
|
alias?: string,
|
||||||
): this {
|
): this {
|
||||||
return this.joinWithType('LEFT', table, condition, alias);
|
return this.joinWithType('LEFT', table, condition, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leftAnyJoin(
|
||||||
|
table: string | Expression | Query,
|
||||||
|
condition: string,
|
||||||
|
alias?: string,
|
||||||
|
): this {
|
||||||
|
return this.joinWithType('LEFT ANY', table, condition, alias);
|
||||||
|
}
|
||||||
|
|
||||||
rightJoin(
|
rightJoin(
|
||||||
table: string | Expression,
|
table: string | Expression,
|
||||||
condition: string,
|
condition: string,
|
||||||
@@ -309,7 +317,7 @@ export class Query<T = any> {
|
|||||||
|
|
||||||
private joinWithType(
|
private joinWithType(
|
||||||
type: JoinType,
|
type: JoinType,
|
||||||
table: string | Expression,
|
table: string | Expression | Query,
|
||||||
condition: string,
|
condition: string,
|
||||||
alias?: string,
|
alias?: string,
|
||||||
): this {
|
): this {
|
||||||
@@ -382,7 +390,7 @@ export class Query<T = any> {
|
|||||||
const aliasClause = join.alias ? ` ${join.alias} ` : ' ';
|
const aliasClause = join.alias ? ` ${join.alias} ` : ' ';
|
||||||
const conditionStr = join.condition ? `ON ${join.condition}` : '';
|
const conditionStr = join.condition ? `ON ${join.condition}` : '';
|
||||||
parts.push(
|
parts.push(
|
||||||
`${join.type} JOIN ${join.table instanceof Expression ? `(${join.table.toString()})` : join.table}${aliasClause}${conditionStr}`,
|
`${join.type} JOIN ${join.table instanceof Query ? `(${join.table.toSQL()})` : join.table instanceof Expression ? `(${join.table.toString()})` : join.table}${aliasClause}${conditionStr}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export function transformPropertyKey(property: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectPropertyKey(property: string) {
|
export function getSelectPropertyKey(property: string) {
|
||||||
|
if (property === 'has_profile') {
|
||||||
|
return `if(profile_id != device_id, 'true', 'false')`;
|
||||||
|
}
|
||||||
|
|
||||||
const propertyPatterns = ['properties', 'profile.properties'];
|
const propertyPatterns = ['properties', 'profile.properties'];
|
||||||
|
|
||||||
const match = propertyPatterns.find((pattern) =>
|
const match = propertyPatterns.find((pattern) =>
|
||||||
@@ -87,7 +91,13 @@ export function getChartSql({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (anyFilterOnProfile || anyBreakdownOnProfile) {
|
if (anyFilterOnProfile || anyBreakdownOnProfile) {
|
||||||
sb.joins.profiles = `LEFT ANY JOIN (SELECT * FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${escape(projectId)}) as profile on profile.id = profile_id`;
|
sb.joins.profiles = `LEFT ANY JOIN (SELECT
|
||||||
|
id as "profile.id",
|
||||||
|
email as "profile.email",
|
||||||
|
first_name as "profile.first_name",
|
||||||
|
last_name as "profile.last_name",
|
||||||
|
properties as "profile.properties"
|
||||||
|
FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${escape(projectId)}) as profile on profile.id = profile_id`;
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.select.count = 'count(*) as count';
|
sb.select.count = 'count(*) as count';
|
||||||
@@ -182,20 +192,25 @@ export function getChartSql({
|
|||||||
|
|
||||||
if (event.segment === 'one_event_per_user') {
|
if (event.segment === 'one_event_per_user') {
|
||||||
sb.from = `(
|
sb.from = `(
|
||||||
SELECT DISTINCT ON (profile_id) * from ${TABLE_NAMES.events} WHERE ${join(
|
SELECT DISTINCT ON (profile_id) * from ${TABLE_NAMES.events} ${getJoins()} WHERE ${join(
|
||||||
sb.where,
|
sb.where,
|
||||||
' AND ',
|
' AND ',
|
||||||
)}
|
)}
|
||||||
ORDER BY profile_id, created_at DESC
|
ORDER BY profile_id, created_at DESC
|
||||||
) as subQuery`;
|
) as subQuery`;
|
||||||
|
sb.joins = {};
|
||||||
|
|
||||||
const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`;
|
const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`;
|
||||||
console.log('CHART SQL', sql);
|
console.log('-- Report --');
|
||||||
|
console.log(sql.replaceAll(/[\n\r]/g, ' '));
|
||||||
|
console.log('-- End --');
|
||||||
return sql;
|
return sql;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`;
|
const sql = `${getSelect()} ${getFrom()} ${getJoins()} ${getWhere()} ${getGroupBy()} ${getOrderBy()} ${getFill()}`;
|
||||||
console.log('CHART SQL', sql);
|
console.log('-- Report --');
|
||||||
|
console.log(sql.replaceAll(/[\n\r]/g, ' '));
|
||||||
|
console.log('-- End --');
|
||||||
return sql;
|
return sql;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { escape } from 'sqlstring';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IClickhouseProfile,
|
||||||
type IServiceProfile,
|
type IServiceProfile,
|
||||||
TABLE_NAMES,
|
TABLE_NAMES,
|
||||||
ch,
|
ch,
|
||||||
@@ -97,7 +98,7 @@ export const chartRouter = createTRPCRouter({
|
|||||||
.where('project_id', '=', projectId)
|
.where('project_id', '=', projectId)
|
||||||
.where('is_external', '=', true)
|
.where('is_external', '=', true)
|
||||||
.orderBy('created_at', 'DESC')
|
.orderBy('created_at', 'DESC')
|
||||||
.limit(100)
|
.limit(10000)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const profileProperties: string[] = [];
|
const profileProperties: string[] = [];
|
||||||
@@ -109,16 +110,21 @@ export const chartRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await chQuery<{ property_key: string; created_at: string }>(
|
const query = clix(ch)
|
||||||
`SELECT
|
.select<{ property_key: string; created_at: string }>([
|
||||||
distinct property_key,
|
'distinct property_key',
|
||||||
max(created_at) as created_at
|
'max(created_at) as created_at',
|
||||||
FROM ${TABLE_NAMES.event_property_values_mv}
|
])
|
||||||
WHERE project_id = ${escape(projectId)}
|
.from(TABLE_NAMES.event_property_values_mv)
|
||||||
${event && event !== '*' ? `AND name = ${escape(event)}` : ''}
|
.where('project_id', '=', projectId)
|
||||||
GROUP BY property_key
|
.groupBy(['property_key'])
|
||||||
ORDER BY created_at DESC`,
|
.orderBy('created_at', 'DESC');
|
||||||
);
|
|
||||||
|
if (event && event !== '*') {
|
||||||
|
query.where('name', '=', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await query.execute();
|
||||||
|
|
||||||
const properties = res
|
const properties = res
|
||||||
.map((item) => item.property_key)
|
.map((item) => item.property_key)
|
||||||
@@ -179,35 +185,51 @@ export const chartRouter = createTRPCRouter({
|
|||||||
const values: string[] = [];
|
const values: string[] = [];
|
||||||
|
|
||||||
if (property.startsWith('properties.')) {
|
if (property.startsWith('properties.')) {
|
||||||
const propertyKey = property.replace(/^properties\./, '');
|
const query = clix(ch)
|
||||||
|
.select<{
|
||||||
const res = await chQuery<{
|
|
||||||
property_value: string;
|
property_value: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}>(
|
}>(['distinct property_value', 'max(created_at) as created_at'])
|
||||||
`SELECT
|
.from(TABLE_NAMES.event_property_values_mv)
|
||||||
distinct property_value,
|
.where('project_id', '=', projectId)
|
||||||
max(created_at) as created_at
|
.where('property_key', '=', property.replace(/^properties\./, ''))
|
||||||
FROM ${TABLE_NAMES.event_property_values_mv}
|
.groupBy(['property_value'])
|
||||||
WHERE project_id = ${escape(projectId)}
|
.orderBy('created_at', 'DESC');
|
||||||
AND property_key = ${escape(propertyKey)}
|
|
||||||
${event && event !== '*' ? `AND name = ${escape(event)}` : ''}
|
if (event && event !== '*') {
|
||||||
GROUP BY property_value
|
query.where('name', '=', event);
|
||||||
ORDER BY created_at DESC`,
|
}
|
||||||
);
|
|
||||||
|
const res = await query.execute();
|
||||||
|
|
||||||
values.push(...res.map((e) => e.property_value));
|
values.push(...res.map((e) => e.property_value));
|
||||||
} else {
|
} else {
|
||||||
const { sb, getSql } = createSqlBuilder();
|
const query = clix(ch)
|
||||||
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
.select<{ values: string[] }>([
|
||||||
|
`distinct ${getSelectPropertyKey(property)} as values`,
|
||||||
|
])
|
||||||
|
.from(TABLE_NAMES.events)
|
||||||
|
.where('project_id', '=', projectId)
|
||||||
|
.where('created_at', '>', clix.exp('now() - INTERVAL 6 MONTH'))
|
||||||
|
.orderBy('created_at', 'DESC')
|
||||||
|
.limit(100_000);
|
||||||
|
|
||||||
if (event !== '*') {
|
if (event !== '*') {
|
||||||
sb.where.event = `name = ${escape(event)}`;
|
query.where('name', '=', event);
|
||||||
}
|
}
|
||||||
sb.select.values = `distinct ${getSelectPropertyKey(property)} as values`;
|
|
||||||
sb.where.date = `${toDate('created_at', 'month')} > now() - INTERVAL 6 MONTH`;
|
if (property.startsWith('profile.')) {
|
||||||
sb.orderBy.created_at = 'created_at DESC';
|
query.leftAnyJoin(
|
||||||
sb.limit = 100_000;
|
clix(ch)
|
||||||
const events = await chQuery<{ values: string[] }>(getSql());
|
.select<IClickhouseProfile>([])
|
||||||
|
.from(TABLE_NAMES.profiles)
|
||||||
|
.where('project_id', '=', projectId),
|
||||||
|
'profile.id = profile_id',
|
||||||
|
'profile',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await query.execute();
|
||||||
|
|
||||||
values.push(
|
values.push(
|
||||||
...pipe(
|
...pipe(
|
||||||
|
|||||||
Reference in New Issue
Block a user