66 findExportedJobByGcsPrefix ,
77 findLatestExportedJobByKind ,
88 markJobStatus ,
9+ mergeJobTableRowCounts ,
910} from '@crowd/data-access-layer'
1011import { getServiceChildLogger } from '@crowd/logging'
1112
@@ -32,6 +33,12 @@ export interface BqExportToGcsInput {
3233 // is replaced by a server-side maximumBytesBilled cap, since a dry-run only validates the first
3334 // statement and cannot predict the WHILE loop's total scan. See ADR-0004.
3435 isScript ?: boolean
36+ // Fill-constraints run (package_dependencies only): the export is a full Option-A scan but the
37+ // downstream merge upserts version_constraint (ON CONFLICT DO UPDATE) instead of DO NOTHING. Full
38+ // and fill produce identical parquet, so sync_mode alone can't tell them apart — persist it in the
39+ // job meta so a --resume-job run reprocesses the export with the correct merge instead of silently
40+ // reverting to DO NOTHING (which skips the backfill).
41+ isFill ?: boolean
3542}
3643
3744export interface BqExportToGcsOutput {
@@ -53,6 +60,7 @@ export async function bqExportToGcs(input: BqExportToGcsInput): Promise<BqExport
5360 exportName,
5461 ecosystems,
5562 isScript,
63+ isFill,
5664 } = input
5765
5866 // Named exports use a stable GCS path independent of runId so they survive across bootstrap runs.
@@ -79,6 +87,12 @@ export async function bqExportToGcs(input: BqExportToGcsInput): Promise<BqExport
7987 { jobKind, exportName, jobId : prior . id , gcsPrefix : prior . gcsPrefix } ,
8088 'exportName match — skipping BQ, loading from named export' ,
8189 )
90+ // The reusing run drives the chunk merge on this same job row, so meta:fill must reflect
91+ // THIS run's intent — write it both ways. Setting it (fill run) stops a later --resume-job
92+ // reverting to ON CONFLICT DO NOTHING and skipping the version_constraint backfill; clearing
93+ // it (non-fill run reusing a row an earlier fill run set) stops resume forcing an unintended
94+ // upsert. Absent and 0 both read back as fill=false (COALESCE in getIngestJobForResume).
95+ await mergeJobTableRowCounts ( qx , prior . id , { 'meta:fill' : isFill ? 1 : 0 } )
8296 return {
8397 gcsPrefix : prior . gcsPrefix ,
8498 rowCount : prior . rowCountBq ,
@@ -115,6 +129,7 @@ export async function bqExportToGcs(input: BqExportToGcsInput): Promise<BqExport
115129 tableRowCounts : {
116130 'bq:export' : 0 ,
117131 ...( ecosystems ? { 'meta:ecosystems' : ecosystems } : { } ) ,
132+ ...( isFill ? { 'meta:fill' : 1 } : { } ) ,
118133 } ,
119134 } )
120135 return { gcsPrefix : namedPrefix , rowCount : 0 , bqBytesBilled : 0 , jobId }
@@ -135,6 +150,9 @@ export async function bqExportToGcs(input: BqExportToGcsInput): Promise<BqExport
135150 { jobKind, jobId : prior . id , gcsPrefix : prior . gcsPrefix } ,
136151 'reuseExports=true — skipping BQ, loading from prior export' ,
137152 )
153+ // See named-export path above: write THIS run's fill intent both ways so a later
154+ // --resume-job matches the most recent run instead of a stale meta value.
155+ await mergeJobTableRowCounts ( qx , prior . id , { 'meta:fill' : isFill ? 1 : 0 } )
138156 return {
139157 gcsPrefix : prior . gcsPrefix ,
140158 rowCount : prior . rowCountBq ,
@@ -163,6 +181,9 @@ export async function bqExportToGcs(input: BqExportToGcsInput): Promise<BqExport
163181 { jobKind, jobId : existing . id , gcsPrefix } ,
164182 'GCS files already exist — reusing export' ,
165183 )
184+ // Same-runId reuse (Temporal retry): re-stamp THIS run's fill intent both ways so the flag
185+ // always matches the most recent run and can never be lost or left stale on reuse.
186+ await mergeJobTableRowCounts ( qx , existing . id , { 'meta:fill' : isFill ? 1 : 0 } )
166187 return { gcsPrefix, rowCount : existing . rowCountBq , bqBytesBilled : 0 , jobId : existing . id }
167188 }
168189 }
@@ -212,9 +233,13 @@ export async function bqExportToGcs(input: BqExportToGcsInput): Promise<BqExport
212233 const provisionalDate = snapshotAt ? new Date ( snapshotAt ) : null
213234 const jobId = await createIngestJob ( qx , jobKind , syncMode , provisionalDate , exportName )
214235
215- // H7: mark exporting before we start the BQ job; store ecosystems filter in table_row_counts JSONB.
236+ // H7: mark exporting before we start the BQ job; store ecosystems filter + fill flag in the
237+ // table_row_counts JSONB so --resume-job can restore the original export's settings.
238+ const exportMeta : Record < string , string | number | string [ ] > = { }
239+ if ( ecosystems ) exportMeta [ 'meta:ecosystems' ] = ecosystems
240+ if ( isFill ) exportMeta [ 'meta:fill' ] = 1
216241 await markJobStatus ( qx , jobId , 'exporting' , {
217- ...( ecosystems ? { tableRowCounts : { 'meta:ecosystems' : ecosystems } } : { } ) ,
242+ ...( Object . keys ( exportMeta ) . length > 0 ? { tableRowCounts : exportMeta } : { } ) ,
218243 } )
219244
220245 // From here the row is 'exporting'; any BQ failure (incl. script-mode maximumBytesBilled aborts)
0 commit comments