|
| 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