Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"db:pull": "bun scripts/db/pull.ts",
"db:push": " bun -F @autumn/shared db:push",
"db:generate": "bun -F @autumn/shared db:generate",
"db:migrate": " bun -F @autumn/shared db:migrate"
"db:migrate": " bun -F @autumn/shared db:migrate",
"db:studio": "bun -F @autumn/shared db:studio"
},
"dependencies": {
"@wooorm/starry-night": "^3.8.0",
Expand Down
46 changes: 36 additions & 10 deletions server/src/external/clickhouse/ClickHouseManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum ClickHouseQuery {
CREATE_DATE_RANGE_VIEW = "CREATE_DATE_RANGE_VIEW",
CREATE_DATE_RANGE_BC_VIEW = "CREATE_DATE_RANGE_BC_VIEW",
CREATE_ORG_EVENTS_VIEW = "CREATE_ORG_EVENTS_VIEW",
CREATE_EVENTS_USAGE_MATERIALIZED_VIEW = "CREATE_EVENTS_USAGE_MATERIALIZED_VIEW",
// CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION = "CREATE_GENERATE_EVENT_COUNTS_EXPRESSIONS",
// CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_NO_COUNT_FUNCTION = "CREATE_GENERATE_EVENT_COUNTS_EXPRESSIONS_NO_COUNT",
GENERATE_EVENT_COUNT_EXPRESSIONS = "GENERATE_EVENT_COUNT_EXPRESSIONS",
Expand Down Expand Up @@ -105,11 +106,35 @@ export class ClickHouseManager {
static async createDateRangeBcView() {}
static async createOrgEventsView() {}

static async createEventsUsageMaterializedView() {
const manager = await ClickHouseManager.getInstance();
if (!manager.client) {
throw new Error("ClickHouse client not initialized");
}
if (!ClickHouseManager.clickhouseAvailable) {
console.log(
"ClickHouse is not available, cannot create materialized view",
);
return;
}
try {
await manager.executeQuery(
ClickHouseQuery.CREATE_EVENTS_USAGE_MATERIALIZED_VIEW,
manager.client,
);
console.log("✓ Successfully created events_usage_mv materialized view");
} catch (error) {
console.error("✗ Failed to create materialized view:", error);
throw error;
}
}

static async ensureSQLFilesExist() {
const requiredQueries = [
ClickHouseQuery.CREATE_DATE_RANGE_VIEW,
ClickHouseQuery.CREATE_DATE_RANGE_BC_VIEW,
ClickHouseQuery.CREATE_ORG_EVENTS_VIEW,
ClickHouseQuery.CREATE_EVENTS_USAGE_MATERIALIZED_VIEW,
// ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION,
// ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_NO_COUNT_FUNCTION,
ClickHouseQuery.GENERATE_EVENT_COUNT_EXPRESSIONS,
Expand Down Expand Up @@ -171,6 +196,7 @@ export class ClickHouseManager {
ClickHouseQuery.CREATE_DATE_RANGE_BC_VIEW,
ClickHouseQuery.CREATE_DATE_RANGE_VIEW,
ClickHouseQuery.CREATE_ORG_EVENTS_VIEW,
ClickHouseQuery.CREATE_EVENTS_USAGE_MATERIALIZED_VIEW,

// ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION,
];
Expand Down Expand Up @@ -214,16 +240,16 @@ export class ClickHouseManager {
throw new Error(`Query ${query} not found`);
}

// For CREATE FUNCTION queries, use command() instead of query() to avoid FORMAT clause
// if (
// query === ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION
// ) {
// const result = await client.command({
// query: queryContent,
// ...options,
// });
// return result;
// }
// For CREATE MATERIALIZED VIEW queries, use command() instead of query() to avoid FORMAT clause
if (
query === ClickHouseQuery.CREATE_EVENTS_USAGE_MATERIALIZED_VIEW
) {
const result = await client.command({
query: queryContent,
...options,
});
return result;
}

const result = await client.query({
query: queryContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- Materialized view for analytics usage aggregations
-- Pre-aggregates events by org_id, env, customer_id, event_name, and time periods
-- Optimizes timeseries queries that group events by hour/day

CREATE MATERIALIZED VIEW IF NOT EXISTS events_usage_mv
ENGINE = SummingMergeTree(value)
PARTITION BY toYYYYMM(period_hour)
ORDER BY (org_id, env, customer_id, event_name, period_hour)
SETTINGS allow_nullable_key = 1
POPULATE
AS
SELECT
org_id,
env,
customer_id,
event_name,
date_trunc('hour', timestamp) as period_hour,
sum(
case
when isNotNull(JSONExtractString(properties, 'value')) AND JSONExtractString(properties, 'value') != ''
then round(toFloat64OrZero(JSONExtractString(properties, 'value')), 6)
when isNotNull(value)
then round(toFloat64(value), 6)
else 1.0
end
) as value
FROM events
WHERE set_usage = false
AND timestamp IS NOT NULL
GROUP BY
org_id,
env,
customer_id,
event_name,
date_trunc('hour', timestamp);

Loading