@@ -3,9 +3,11 @@ import sqlstring from 'sqlstring';
33import {
44 TABLE_NAMES ,
55 ch ,
6+ chInsertCSV ,
67 convertClickhouseDateToJs ,
78 formatClickhouseDate ,
89} from '../clickhouse/client' ;
10+ import { csvEscapeField , csvEscapeJson } from '../clickhouse/csv' ;
911import { type Prisma , db } from '../prisma-client' ;
1012import type { IClickhouseEvent } from './event.service' ;
1113
@@ -33,20 +35,54 @@ export async function insertImportBatch(
3335 return { importId, totalEvents : 0 , insertedEvents : 0 } ;
3436 }
3537
36- // Add import metadata to each event
37- const eventsWithMetadata = events . map ( ( event ) => ( {
38- ...event ,
39- import_id : importId ,
40- import_status : 'pending' ,
41- imported_at_meta : new Date ( ) ,
42- } ) ) ;
43-
44- await ch . insert ( {
45- table : TABLE_NAMES . events_imports ,
46- values : eventsWithMetadata ,
47- format : 'JSONEachRow' ,
38+ // Important to have same order as events_imports table
39+ // CSV format: properly quotes fields that need it
40+ const csvRows = events . map ( ( event ) => {
41+ // Properties need to be converted to JSON for Map(String, String)
42+ // All fields must be CSV-escaped when joining with commas
43+ const fields = [
44+ csvEscapeField ( event . id || '' ) ,
45+ csvEscapeField ( event . name ) ,
46+ csvEscapeField ( event . sdk_name || '' ) ,
47+ csvEscapeField ( event . sdk_version || '' ) ,
48+ csvEscapeField ( event . device_id || '' ) ,
49+ csvEscapeField ( event . profile_id || '' ) ,
50+ csvEscapeField ( event . project_id || '' ) ,
51+ csvEscapeField ( event . session_id || '' ) ,
52+ csvEscapeField ( event . path ) ,
53+ csvEscapeField ( event . origin || '' ) ,
54+ csvEscapeField ( event . referrer || '' ) ,
55+ csvEscapeField ( event . referrer_name || '' ) ,
56+ csvEscapeField ( event . referrer_type || '' ) ,
57+ csvEscapeField ( event . duration ?? 0 ) ,
58+ csvEscapeJson ( event . properties ) ,
59+ csvEscapeField (
60+ event . created_at
61+ ? formatClickhouseDate ( event . created_at )
62+ : formatClickhouseDate ( new Date ( ) ) ,
63+ ) ,
64+ csvEscapeField ( event . country || '' ) ,
65+ csvEscapeField ( event . city || '' ) ,
66+ csvEscapeField ( event . region || '' ) ,
67+ csvEscapeField ( event . longitude != null ? event . longitude : '\\N' ) ,
68+ csvEscapeField ( event . latitude != null ? event . latitude : '\\N' ) ,
69+ csvEscapeField ( event . os || '' ) ,
70+ csvEscapeField ( event . os_version || '' ) ,
71+ csvEscapeField ( event . browser || '' ) ,
72+ csvEscapeField ( event . browser_version || '' ) ,
73+ csvEscapeField ( event . device || '' ) ,
74+ csvEscapeField ( event . brand || '' ) ,
75+ csvEscapeField ( event . model || '' ) ,
76+ csvEscapeField ( '\\N' ) , // imported_at (Nullable)
77+ csvEscapeField ( importId ) ,
78+ csvEscapeField ( 'pending' ) , // import_status
79+ csvEscapeField ( formatClickhouseDate ( new Date ( ) ) . replace ( / \. \d { 3 } $ / , '' ) ) , // imported_at_meta (DateTime, not DateTime64, so no milliseconds)
80+ ] ;
81+ return fields . join ( ',' ) ;
4882 } ) ;
4983
84+ await chInsertCSV ( TABLE_NAMES . events_imports , csvRows ) ;
85+
5086 return {
5187 importId,
5288 totalEvents : events . length ,
0 commit comments