Skip to content

Commit c95c9aa

Browse files
authored
profile event mv (#133)
1 parent 48834ec commit c95c9aa

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import {
2+
chMigrationClient,
3+
runClickhouseMigrationCommands,
4+
} from '../src/clickhouse/migration';
5+
import { getIsCluster } from './helpers';
6+
7+
/**
8+
* Backfill profile_event_summary_mv_v2 in daily batches
9+
*
10+
* This MV is for simple event-based cohorts WITHOUT property filters
11+
* Much simpler than profile_event_property_summary_mv - no ARRAY JOIN
12+
*
13+
* Usage:
14+
* # For clustered (production):
15+
* npm run migrate -- 12 --start-date=2025-01-01 --end-date=2026-01-10 --table=profile_event_summary_mv_v2_replicated
16+
*
17+
* # For self-hosted:
18+
* npm run migrate -- 12 --start-date=2025-01-01 --end-date=2026-01-10 --table=profile_event_summary_mv_v2
19+
*
20+
* # Optional flags:
21+
* npm run migrate -- 12 ... --dry (dry run only)
22+
*/
23+
24+
interface BackfillOptions {
25+
startDate: string;
26+
endDate: string;
27+
targetTable: string;
28+
isDryRun: boolean;
29+
}
30+
31+
function parseArgs(): BackfillOptions {
32+
const args = process.argv;
33+
34+
const startDateArg = args.find(arg => arg.startsWith('--start-date='));
35+
const endDateArg = args.find(arg => arg.startsWith('--end-date='));
36+
const tableArg = args.find(arg => arg.startsWith('--table='));
37+
38+
if (!startDateArg || !endDateArg || !tableArg) {
39+
console.error('❌ Missing required arguments');
40+
console.log('');
41+
console.log('Usage:');
42+
console.log(' npm run migrate -- 12 \\');
43+
console.log(' --start-date=2025-01-01 \\');
44+
console.log(' --end-date=2026-01-10 \\');
45+
console.log(' --table=profile_event_summary_mv_v2');
46+
console.log('');
47+
console.log('Optional flags:');
48+
console.log(' --dry (generate SQL only, don\'t execute)');
49+
console.log('');
50+
process.exit(1);
51+
}
52+
53+
return {
54+
startDate: startDateArg.split('=')[1]!,
55+
endDate: endDateArg.split('=')[1]!,
56+
targetTable: tableArg.split('=')[1]!,
57+
isDryRun: args.includes('--dry'),
58+
};
59+
}
60+
61+
export async function up() {
62+
const options = parseArgs();
63+
64+
console.log('🚀 Profile Event Summary Backfill (Daily Batches)');
65+
console.log('='.repeat(60));
66+
console.log(`📅 Date Range: ${options.startDate} to ${options.endDate}`);
67+
console.log(`📦 Target Table: ${options.targetTable}`);
68+
console.log(`🔧 Mode: ${options.isDryRun ? 'DRY RUN' : 'EXECUTION'}`);
69+
console.log('='.repeat(60));
70+
console.log('');
71+
72+
// Skip analysis
73+
console.log('⏩ Skipping analysis - will process all events in date range');
74+
console.log('');
75+
76+
// Generate daily batches
77+
const batches = generateDailyBatches(
78+
options.startDate,
79+
options.endDate,
80+
options.targetTable
81+
);
82+
83+
console.log(`📦 Generated ${batches.length} daily batches`);
84+
console.log(`⏱️ Est. Time: ${formatEstimatedTime(batches.length)} (at ~2min/day)`);
85+
console.log('');
86+
87+
if (options.isDryRun) {
88+
console.log('🔍 DRY RUN - Sample batch SQL:');
89+
console.log('─'.repeat(80));
90+
console.log(batches[0]?.sql.trim() || 'No batches generated');
91+
console.log('─'.repeat(80));
92+
console.log('');
93+
console.log(`💡 Total batches: ${batches.length}`);
94+
console.log('💡 Remove --dry to execute');
95+
return;
96+
}
97+
98+
// Execute batches
99+
await executeBatches(batches);
100+
}
101+
102+
function generateDailyBatches(
103+
startDate: string,
104+
endDate: string,
105+
targetTable: string
106+
) {
107+
const batches: Array<{ date: string; sql: string }> = [];
108+
109+
const start = new Date(startDate);
110+
const end = new Date(endDate);
111+
let current = new Date(start);
112+
113+
while (current <= end) {
114+
const dateStr = current.toISOString().split('T')[0];
115+
116+
const sql = `
117+
INSERT INTO ${targetTable}
118+
SELECT
119+
project_id,
120+
profile_id,
121+
name,
122+
toStartOfDay(created_at) AS event_date,
123+
countState() AS event_count,
124+
minState(created_at) AS first_event_time,
125+
maxState(created_at) AS last_event_time,
126+
sumState(duration) AS total_duration
127+
FROM events
128+
PREWHERE
129+
toDate(created_at) = '${dateStr}'
130+
AND profile_id != device_id
131+
GROUP BY project_id, profile_id, name, event_date
132+
SETTINGS
133+
max_memory_usage = 30000000000,
134+
max_execution_time = 7200,
135+
max_threads = 16`;
136+
137+
batches.push({ date: dateStr, sql });
138+
139+
// Move to next day
140+
current.setDate(current.getDate() + 1);
141+
}
142+
143+
return batches;
144+
}
145+
146+
async function executeBatches(
147+
batches: Array<{ date: string; sql: string }>
148+
) {
149+
console.log('🚀 Starting execution...');
150+
console.log('💡 AggregatingMergeTree will merge any duplicate data');
151+
console.log('');
152+
153+
let completed = 0;
154+
const total = batches.length;
155+
const startTime = Date.now();
156+
157+
for (const batch of batches) {
158+
try {
159+
// Execute INSERT
160+
const execStart = Date.now();
161+
await runClickhouseMigrationCommands([batch.sql]);
162+
const execTime = Date.now() - execStart;
163+
164+
completed++;
165+
166+
// Show progress every 10 batches or if query took > 2 minutes
167+
if (completed % 10 === 0 || execTime > 120000 || completed === total) {
168+
logProgress(completed, total, startTime, batch.date, execTime);
169+
}
170+
171+
} catch (error: any) {
172+
console.error(`\n❌ Error processing ${batch.date}:`, error.message);
173+
throw error;
174+
}
175+
}
176+
177+
const totalTime = Date.now() - startTime;
178+
179+
console.log('');
180+
console.log('✅ Backfill complete!');
181+
console.log(` Time: ${formatTime(Math.round(totalTime / 1000))}`);
182+
console.log(` Processed: ${completed}/${total} days`);
183+
console.log(` Avg/day: ${Math.round(totalTime / completed / 1000)}s`);
184+
console.log('');
185+
}
186+
187+
function logProgress(
188+
completed: number,
189+
total: number,
190+
startTime: number,
191+
date: string,
192+
execTime: number
193+
) {
194+
const pct = Math.round((completed / total) * 100);
195+
const elapsed = (Date.now() - startTime) / 1000;
196+
const remaining = ((total - completed) / completed) * elapsed;
197+
198+
const msg = ` [${pct}%] ${completed}/${total} | ETA: ${formatTime(Math.round(remaining))}\n Last: ${date} (${Math.round(execTime / 1000)}s)`;
199+
console.log(msg);
200+
}
201+
202+
function formatTime(sec: number): string {
203+
if (sec < 60) return `${sec}s`;
204+
const min = Math.floor(sec / 60);
205+
if (min < 60) return `${min}m`;
206+
const hrs = Math.floor(min / 60);
207+
return `${hrs}h ${min % 60}m`;
208+
}
209+
210+
function formatEstimatedTime(days: number): string {
211+
// Assume ~2 minutes per day on average
212+
return formatTime(days * 120);
213+
}
214+
215+
export async function down() {
216+
console.log('⚠️ No down migration - backfill is data only');
217+
}

0 commit comments

Comments
 (0)