@@ -19,6 +19,31 @@ import type {
1919 TableSchema ,
2020} from '@/lib/table/types'
2121
22+ function readExecutionIsManualRun ( value : Record < string , unknown > ) : boolean {
23+ const isManualRun = value . isManualRun
24+ if ( isManualRun === undefined ) return false
25+ if ( typeof isManualRun !== 'boolean' ) {
26+ throw new Error ( 'Execution metadata isManualRun must be a boolean' )
27+ }
28+ return isManualRun
29+ }
30+
31+ function readEnrichmentRunDetail ( value : unknown ) : EnrichmentRunDetail | null {
32+ if ( value === null || value === undefined ) return null
33+ if ( typeof value !== 'object' || Array . isArray ( value ) ) {
34+ throw new Error ( 'Enrichment run detail must be a JSON object' )
35+ }
36+ const metadata = value as Record < string , unknown >
37+ if ( metadata . providers === undefined ) return null
38+ if ( ! Array . isArray ( metadata . providers ) ) {
39+ throw new Error ( 'Enrichment run detail providers must be an array' )
40+ }
41+ return {
42+ ...( value as Omit < EnrichmentRunDetail , 'isManualRun' > ) ,
43+ isManualRun : readExecutionIsManualRun ( metadata ) ,
44+ }
45+ }
46+
2247/**
2348 * Loads `tableRowExecutions` rows for the given row ids and groups them into a
2449 * `Map<rowId, RowExecutions>` suitable for plugging into `TableRow.executions`.
@@ -30,9 +55,9 @@ export async function loadExecutionsByRow(
3055 const ids = Array . from ( new Set ( rowIds ) )
3156 const result = new Map < string , RowExecutions > ( )
3257 if ( ids . length === 0 ) return result
33- // Explicit column list, never `select()` — `enrichmentDetails` is large and
34- // must stay off the hot grid read path (fetched on demand via
35- // `loadEnrichmentDetail`) .
58+ // Explicit column list, never `select()` — project only the small provenance
59+ // scalar from `enrichmentDetails`; the provider cascade stays off the hot
60+ // grid read path and is fetched on demand via `loadEnrichmentDetail`.
3661 const rows = await trx
3762 . select ( {
3863 rowId : tableRowExecutions . rowId ,
@@ -41,7 +66,7 @@ export async function loadExecutionsByRow(
4166 executionId : tableRowExecutions . executionId ,
4267 jobId : tableRowExecutions . jobId ,
4368 workflowId : tableRowExecutions . workflowId ,
44- isManualRun : tableRowExecutions . isManualRun ,
69+ isManualRun : sql < boolean > `coalesce(( ${ tableRowExecutions . enrichmentDetails } ->> ' isManualRun')::boolean, false)` ,
4570 error : tableRowExecutions . error ,
4671 runningBlockIds : tableRowExecutions . runningBlockIds ,
4772 blockErrors : tableRowExecutions . blockErrors ,
@@ -100,7 +125,20 @@ export async function loadEnrichmentDetail(
100125 ) as SQL
101126 )
102127 . limit ( 1 )
103- return ( row ?. enrichmentDetails as EnrichmentRunDetail | null | undefined ) ?? null
128+ return readEnrichmentRunDetail ( row ?. enrichmentDetails )
129+ }
130+
131+ function buildStoredExecutionMetadata (
132+ value : RowExecutionMetadata
133+ ) : EnrichmentRunDetail | { isManualRun : boolean } | null {
134+ const detail = value . enrichmentDetails
135+ if ( detail ) {
136+ if ( value . isManualRun !== undefined && detail . isManualRun !== value . isManualRun ) {
137+ throw new Error ( 'Execution state and enrichment detail disagree on manual-run provenance' )
138+ }
139+ return detail
140+ }
141+ return value . isManualRun === undefined ? null : { isManualRun : value . isManualRun }
104142}
105143
106144/**
@@ -265,14 +303,18 @@ export async function writeExecutionsPatch(
265303 executionId : value . executionId ,
266304 jobId : value . jobId ,
267305 workflowId : value . workflowId ,
268- isManualRun : value . isManualRun ?? false ,
269306 error : value . error ,
270307 runningBlockIds : value . runningBlockIds ?? [ ] ,
271308 blockErrors : value . blockErrors ?? { } ,
272309 cancelledAt : value . cancelledAt ? new Date ( value . cancelledAt ) : null ,
273- enrichmentDetails : value . enrichmentDetails ?? null ,
310+ enrichmentDetails : buildStoredExecutionMetadata ( value ) ,
274311 updatedAt : new Date ( ) ,
275312 } as const
313+ const enrichmentDetailsUpdate = value . enrichmentDetails
314+ ? insertValues . enrichmentDetails
315+ : value . isManualRun !== undefined
316+ ? sql `coalesce(${ tableRowExecutions . enrichmentDetails } , '{}'::jsonb) || excluded.enrichment_details`
317+ : tableRowExecutions . enrichmentDetails
276318
277319 if ( isGuarded ) {
278320 // Gate by guard semantics. The original JSONB guard had two AND'd
@@ -303,19 +345,11 @@ export async function writeExecutionsPatch(
303345 executionId : insertValues . executionId ,
304346 jobId : insertValues . jobId ,
305347 workflowId : insertValues . workflowId ,
306- isManualRun :
307- value . isManualRun === undefined
308- ? tableRowExecutions . isManualRun
309- : insertValues . isManualRun ,
310348 error : insertValues . error ,
311349 runningBlockIds : insertValues . runningBlockIds ,
312350 blockErrors : insertValues . blockErrors ,
313351 cancelledAt : insertValues . cancelledAt ,
314- // Sticky: preserve a prior cascade breakdown when this write omits
315- // it (e.g. the running pickup stamp) so only an explicit detail
316- // overwrites it. Re-runs delete the row first, so this never serves
317- // stale detail across runs.
318- enrichmentDetails : sql `coalesce(excluded.enrichment_details, ${ tableRowExecutions . enrichmentDetails } )` ,
352+ enrichmentDetails : enrichmentDetailsUpdate ,
319353 updatedAt : insertValues . updatedAt ,
320354 } ,
321355 where : guardCondition as SQL ,
@@ -335,18 +369,11 @@ export async function writeExecutionsPatch(
335369 executionId : insertValues . executionId ,
336370 jobId : insertValues . jobId ,
337371 workflowId : insertValues . workflowId ,
338- isManualRun :
339- value . isManualRun === undefined
340- ? tableRowExecutions . isManualRun
341- : insertValues . isManualRun ,
342372 error : insertValues . error ,
343373 runningBlockIds : insertValues . runningBlockIds ,
344374 blockErrors : insertValues . blockErrors ,
345375 cancelledAt : insertValues . cancelledAt ,
346- // Sticky: preserve a prior cascade breakdown when this write omits it
347- // (e.g. the running pickup stamp) so only an explicit detail overwrites
348- // it. Re-runs delete the row first, so this never serves stale detail.
349- enrichmentDetails : sql `coalesce(excluded.enrichment_details, ${ tableRowExecutions . enrichmentDetails } )` ,
376+ enrichmentDetails : enrichmentDetailsUpdate ,
350377 updatedAt : insertValues . updatedAt ,
351378 } ,
352379 } )
0 commit comments