@@ -25,6 +25,85 @@ function stageRootNodeId(root) {
2525 return null ;
2626}
2727
28+ // The planner writes operators as `{"NonASAP": {"Scan": {...}}}` or
29+ // `{"ASAP": {"SummaryAgg": {...}}}`, children as node ids inside the
30+ // operator, and logical stages as flat DAGs (node `i` is `nodes[i]`, no
31+ // edge list). The viewer reads `{kind, ...}` payloads and explicit edges;
32+ // these functions convert, and leave documents already in that shape alone.
33+ const STAGE_CHILD_FIELDS = [ 'child' , 'children' , 'left' , 'right' , 'summary_input' , 'outer' , 'inner' ] ;
34+ const STAGE_RENAMED_KIND = { Join : 'join_kind' , SetOp : 'set_kind' , TimeRange : 'range_kind' , PromqlSeriesSample : 'sample_kind' } ;
35+
36+ function stageSnake ( name ) {
37+ if ( name === 'SQLWindowFunc' ) return 'sql_window_func' ;
38+ return name . replace ( / ( [ a - z 0 - 9 ] ) ( [ A - Z ] ) / g, '$1_$2' ) . toLowerCase ( ) ;
39+ }
40+
41+ function stageOperatorParts ( operator ) {
42+ const [ category , inner ] = Object . entries ( operator ) [ 0 ] ;
43+ const [ variant , fields ] = typeof inner === 'string' ? [ inner , { } ] : Object . entries ( inner ) [ 0 ] ;
44+ const body = fields && typeof fields === 'object' && ! Array . isArray ( fields ) ? { ...fields } : { value : fields } ;
45+ return [ category , variant , body ] ;
46+ }
47+
48+ function stagePayloadOf ( operator ) {
49+ if ( ! operator || typeof operator !== 'object' || typeof operator . kind === 'string' ) return operator ;
50+ const [ category , variant , body ] = stageOperatorParts ( operator ) ;
51+ if ( 'kind' in body ) { body [ STAGE_RENAMED_KIND [ variant ] || 'variant_kind' ] = body . kind ; delete body . kind ; }
52+ return category === 'NonASAP'
53+ ? { kind : 'relational' , operator : { kind : stageSnake ( variant ) , ...body } }
54+ : { kind : stageSnake ( variant ) , ...body } ;
55+ }
56+
57+ // Node ids an operator reads: its inputs, then scalar-subquery references.
58+ function stageOperatorChildren ( operator ) {
59+ const [ , , body ] = stageOperatorParts ( operator ) ;
60+ const ids = [ ] ;
61+ STAGE_CHILD_FIELDS . forEach ( ( field ) => {
62+ const value = body [ field ] ;
63+ if ( typeof value === 'number' ) ids . push ( value ) ;
64+ else if ( Array . isArray ( value ) ) value . forEach ( ( id ) => { if ( typeof id === 'number' ) ids . push ( id ) ; } ) ;
65+ } ) ;
66+ const walk = ( value ) => {
67+ if ( ! value || typeof value !== 'object' ) return ;
68+ if ( typeof value . ScalarSubquery === 'number' ) ids . push ( value . ScalarSubquery ) ;
69+ Object . values ( value ) . forEach ( walk ) ;
70+ } ;
71+ Object . entries ( body ) . forEach ( ( [ key , value ] ) => { if ( ! STAGE_CHILD_FIELDS . includes ( key ) ) walk ( value ) ; } ) ;
72+ return ids ;
73+ }
74+
75+ function normalizeStageDag ( dag ) {
76+ if ( ! dag || ! Array . isArray ( dag . nodes ) ) return dag ;
77+ if ( dag . nodes . every ( ( node ) => node && node . operator && ! node . payload ) ) {
78+ const edges = [ ] ;
79+ const nodes = dag . nodes . map ( ( node , id ) => {
80+ stageOperatorChildren ( node . operator ) . forEach ( ( producer ) => edges . push ( { producer, consumer : id } ) ) ;
81+ return {
82+ id,
83+ payload : stagePayloadOf ( node . operator ) ,
84+ result_kind : node . result_kind ,
85+ output_schema : node . schema ,
86+ guarantee : node . guarantee ,
87+ timing : node . timing ,
88+ } ;
89+ } ) ;
90+ return { ...dag , nodes, edges } ;
91+ }
92+ return { ...dag , nodes : dag . nodes . map ( ( node ) => ( node && node . payload ? { ...node , payload : stagePayloadOf ( node . payload ) } : node ) ) } ;
93+ }
94+
95+ function normalizeStagePipeline ( doc ) {
96+ if ( ! isStagePipelineDocument ( doc ) ) return doc ;
97+ const candidates = ( stage ) => ( stage && Array . isArray ( stage . candidates )
98+ ? { ...stage , candidates : stage . candidates . map ( ( c ) => ( c ? { ...c , dag : normalizeStageDag ( c . dag ) } : c ) ) }
99+ : stage ) ;
100+ const out = { ...doc } ;
101+ if ( doc . stage0_logical ) out . stage0_logical = { ...doc . stage0_logical , dag : normalizeStageDag ( doc . stage0_logical . dag ) } ;
102+ if ( doc . stage1_logical_asap !== undefined ) out . stage1_logical_asap = candidates ( doc . stage1_logical_asap ) ;
103+ if ( doc . stage2_physical_asap !== undefined ) out . stage2_physical_asap = candidates ( doc . stage2_physical_asap ) ;
104+ return out ;
105+ }
106+
28107function validateStageDag ( dag , where , physical , queryCount ) {
29108 const errors = [ ] ;
30109 if ( ! dag || typeof dag !== 'object' ) return [ `${ where } : dag is missing` ] ;
0 commit comments