@@ -114,66 +114,76 @@ export async function gcsParquetToStaging(input: GcsToStagingInput): Promise<Gcs
114114 const qx = await getPackagesDb ( )
115115
116116 await markJobStatus ( qx , jobId , 'loading' )
117- if ( stagingDdl ) {
118- const stmts = Array . isArray ( stagingDdl ) ? stagingDdl : [ stagingDdl ]
119- for ( const stmt of stmts ) await qx . result ( stmt )
120- }
121- await qx . result ( `TRUNCATE ${ stagingTable } ` )
122- // Reset progress at the start of a fresh run (first chunk). This handles job ID reuse
123- // via --export-name: a previous run's 193/193 would otherwise persist due to GREATEST.
124- if ( filesOffset === 0 && input . totalFiles != null ) {
125- await updateLoadingProgress ( qx , jobId , 0 , input . totalFiles , true )
126- }
127-
128- let parquetFileNames : string [ ]
129- if ( input . fileNames ) {
130- parquetFileNames = input . fileNames
131- } else if ( input . gcsPrefix ) {
132- const objectPrefix = gcsPrefixToObjectPrefix ( input . gcsPrefix )
133- const [ files ] = await bucket . getFiles ( { prefix : objectPrefix } )
134- parquetFileNames = files . filter ( ( f ) => f . name . endsWith ( '.parquet' ) ) . map ( ( f ) => f . name )
135- } else {
136- throw new Error ( 'gcsParquetToStaging: must provide either fileNames or gcsPrefix' )
137- }
138-
139- const totalFiles = input . totalFiles ?? parquetFileNames . length
117+ // Row is 'loading' from here; a staging-load failure must flip it to 'failed' with the reason
118+ // rather than leave it stuck 'loading'. Mirrors the rankPackages pattern (criticality/activities).
119+ try {
120+ if ( stagingDdl ) {
121+ const stmts = Array . isArray ( stagingDdl ) ? stagingDdl : [ stagingDdl ]
122+ for ( const stmt of stmts ) await qx . result ( stmt )
123+ }
124+ await qx . result ( `TRUNCATE ${ stagingTable } ` )
125+ // Reset progress at the start of a fresh run (first chunk). This handles job ID reuse
126+ // via --export-name: a previous run's 193/193 would otherwise persist due to GREATEST.
127+ if ( filesOffset === 0 && input . totalFiles != null ) {
128+ await updateLoadingProgress ( qx , jobId , 0 , input . totalFiles , true )
129+ }
140130
141- log . info (
142- { jobId, stagingTable, fileCount : parquetFileNames . length , filesOffset, totalFiles } ,
143- 'Loading parquet files into staging' ,
144- )
131+ let parquetFileNames : string [ ]
132+ if ( input . fileNames ) {
133+ parquetFileNames = input . fileNames
134+ } else if ( input . gcsPrefix ) {
135+ const objectPrefix = gcsPrefixToObjectPrefix ( input . gcsPrefix )
136+ const [ files ] = await bucket . getFiles ( { prefix : objectPrefix } )
137+ parquetFileNames = files . filter ( ( f ) => f . name . endsWith ( '.parquet' ) ) . map ( ( f ) => f . name )
138+ } else {
139+ throw new Error ( 'gcsParquetToStaging: must provide either fileNames or gcsPrefix' )
140+ }
145141
146- let totalLoaded = 0
142+ const totalFiles = input . totalFiles ?? parquetFileNames . length
147143
148- for ( let i = 0 ; i < parquetFileNames . length ; i += MAX_CONCURRENT ) {
149- const chunk = parquetFileNames . slice ( i , i + MAX_CONCURRENT )
150- const counts = await Promise . all (
151- chunk . map ( ( name ) => loadParquetFile ( qx , stagingTable , pgColumns , name , tsCols , decCols ) ) ,
152- )
153- totalLoaded += counts . reduce ( ( a , b ) => a + b , 0 )
154- const doneInBatch = i + chunk . length
155- const doneGlobal = filesOffset + doneInBatch
156144 log . info (
157- {
158- jobId,
159- totalLoaded,
160- progress : `${ doneGlobal } /${ totalFiles } (${ Math . round ( ( doneGlobal / totalFiles ) * 100 ) } %)` ,
161- } ,
162- 'Staging load progress' ,
145+ { jobId, stagingTable, fileCount : parquetFileNames . length , filesOffset, totalFiles } ,
146+ 'Loading parquet files into staging' ,
163147 )
164- Context . current ( ) . heartbeat ( { done : doneGlobal , total : totalFiles } )
165- await updateLoadingProgress ( qx , jobId , doneGlobal , totalFiles )
166- }
167148
168- const cumulativeStagingRows = ( input . priorStagingRows ?? 0 ) + totalLoaded
169- await markJobStatus ( qx , jobId , 'loading' , {
170- rowCountStaging : cumulativeStagingRows ,
171- tableRowCounts : { [ `staging:${ stagingTable } ` ] : totalLoaded } ,
172- } )
149+ let totalLoaded = 0
150+
151+ for ( let i = 0 ; i < parquetFileNames . length ; i += MAX_CONCURRENT ) {
152+ const chunk = parquetFileNames . slice ( i , i + MAX_CONCURRENT )
153+ const counts = await Promise . all (
154+ chunk . map ( ( name ) => loadParquetFile ( qx , stagingTable , pgColumns , name , tsCols , decCols ) ) ,
155+ )
156+ totalLoaded += counts . reduce ( ( a , b ) => a + b , 0 )
157+ const doneInBatch = i + chunk . length
158+ const doneGlobal = filesOffset + doneInBatch
159+ log . info (
160+ {
161+ jobId,
162+ totalLoaded,
163+ progress : `${ doneGlobal } /${ totalFiles } (${ Math . round ( ( doneGlobal / totalFiles ) * 100 ) } %)` ,
164+ } ,
165+ 'Staging load progress' ,
166+ )
167+ Context . current ( ) . heartbeat ( { done : doneGlobal , total : totalFiles } )
168+ await updateLoadingProgress ( qx , jobId , doneGlobal , totalFiles )
169+ }
170+
171+ const cumulativeStagingRows = ( input . priorStagingRows ?? 0 ) + totalLoaded
172+ await markJobStatus ( qx , jobId , 'loading' , {
173+ rowCountStaging : cumulativeStagingRows ,
174+ tableRowCounts : { [ `staging:${ stagingTable } ` ] : totalLoaded } ,
175+ } )
173176
174- await qx . result ( `ANALYZE ${ stagingTable } ` )
177+ await qx . result ( `ANALYZE ${ stagingTable } ` )
175178
176- log . info ( { jobId, stagingTable, totalLoaded } , 'Staging load complete' )
179+ log . info ( { jobId, stagingTable, totalLoaded } , 'Staging load complete' )
177180
178- return { rowsLoaded : totalLoaded }
181+ return { rowsLoaded : totalLoaded }
182+ } catch ( err ) {
183+ await markJobStatus ( qx , jobId , 'failed' , {
184+ errorMessage : ( err as Error ) . message ,
185+ finishedAt : new Date ( ) ,
186+ } )
187+ throw err
188+ }
179189}
0 commit comments