@@ -82,6 +82,10 @@ export interface PLpgSQLDeparserContext {
8282 options : PLpgSQLDeparserOptions ;
8383 datums ?: PLpgSQLDatum [ ] ;
8484 returnInfo ?: ReturnInfo ;
85+ /** Set of linenos for loop-introduced variables (to exclude from DECLARE) */
86+ loopVarLinenos ?: Set < number > ;
87+ /** Map of block lineno to the set of datum indices that belong to that block */
88+ blockDatumMap ?: Map < number , Set < number > > ;
8589}
8690
8791/**
@@ -154,11 +158,30 @@ export class PLpgSQLDeparser {
154158 * @param returnInfo - Optional return type info for correct RETURN statement handling
155159 */
156160 deparseFunction ( func : PLpgSQL_function , returnInfo ?: ReturnInfo ) : string {
161+ // Collect loop-introduced variables before generating DECLARE section
162+ const loopVarLinenos = new Set < number > ( ) ;
163+ if ( func . action ) {
164+ this . collectLoopVariables ( func . action , loopVarLinenos ) ;
165+ }
166+
167+ // Build the block-to-datum mapping for nested DECLARE sections
168+ const blockDatumMap = this . buildBlockDatumMap ( func . datums , func . action , loopVarLinenos ) ;
169+
170+ // Collect all datum indices that belong to nested blocks (to exclude from top-level)
171+ const nestedDatumIndices = new Set < number > ( ) ;
172+ for ( const indices of blockDatumMap . values ( ) ) {
173+ for ( const idx of indices ) {
174+ nestedDatumIndices . add ( idx ) ;
175+ }
176+ }
177+
157178 const context : PLpgSQLDeparserContext = {
158179 indentLevel : 0 ,
159180 options : this . options ,
160181 datums : func . datums ,
161182 returnInfo,
183+ loopVarLinenos,
184+ blockDatumMap,
162185 } ;
163186
164187 const parts : string [ ] = [ ] ;
@@ -175,14 +198,14 @@ export class PLpgSQLDeparser {
175198 parts . push ( `<<${ blockLabel } >>` ) ;
176199 }
177200
178- // Collect loop-introduced variables before generating DECLARE section
179- const loopVarLinenos = new Set < number > ( ) ;
180- if ( func . action ) {
181- this . collectLoopVariables ( func . action , loopVarLinenos ) ;
182- }
183-
184- // Deparse DECLARE section (local variables, excluding loop variables)
185- const declareSection = this . deparseDeclareSection ( func . datums , context , loopVarLinenos ) ;
201+ // Deparse DECLARE section (local variables, excluding loop variables and nested block variables)
202+ const declareSection = this . deparseDeclareSection (
203+ func . datums ,
204+ context ,
205+ loopVarLinenos ,
206+ undefined , // includedIndices - not used for top-level
207+ nestedDatumIndices // excludedIndices - exclude datums that belong to nested blocks
208+ ) ;
186209 if ( declareSection ) {
187210 parts . push ( declareSection ) ;
188211 }
@@ -339,20 +362,221 @@ export class PLpgSQLDeparser {
339362 }
340363 }
341364
365+ /**
366+ * Build a mapping of block linenos to the datum indices that belong to each block.
367+ * This is used to emit DECLARE sections at the correct nesting level.
368+ *
369+ * The algorithm:
370+ * 1. Get the top-level block's lineno (the BEGIN line of the function body)
371+ * 2. Collect all nested PLpgSQL_stmt_block linenos from the AST
372+ * 3. For each datum with a lineno GREATER than the top-level block's lineno:
373+ * - Assign it to the nested block whose lineno is the smallest value greater than the datum's lineno
374+ * 4. Datums with lineno <= top-level block lineno belong to the top-level DECLARE (not added to map)
375+ */
376+ private buildBlockDatumMap (
377+ datums : PLpgSQLDatum [ ] | undefined ,
378+ action : PLpgSQLStmtNode | undefined ,
379+ loopVarLinenos : Set < number >
380+ ) : Map < number , Set < number > > {
381+ const blockDatumMap = new Map < number , Set < number > > ( ) ;
382+
383+ if ( ! datums || ! action ) {
384+ return blockDatumMap ;
385+ }
386+
387+ // Get the top-level block's lineno
388+ let topLevelBlockLineno : number | undefined ;
389+ if ( 'PLpgSQL_stmt_block' in action ) {
390+ topLevelBlockLineno = action . PLpgSQL_stmt_block . lineno ;
391+ }
392+
393+ // Collect all nested block linenos (excluding the top-level block)
394+ const nestedBlockLinenos : number [ ] = [ ] ;
395+ this . collectNestedBlockLinenos ( action , nestedBlockLinenos , true ) ;
396+ nestedBlockLinenos . sort ( ( a , b ) => a - b ) ;
397+
398+ // For each datum, find which block it belongs to
399+ datums . forEach ( ( datum , index ) => {
400+ let lineno : number | undefined ;
401+
402+ if ( 'PLpgSQL_var' in datum ) {
403+ lineno = datum . PLpgSQL_var . lineno ;
404+ } else if ( 'PLpgSQL_rec' in datum ) {
405+ lineno = datum . PLpgSQL_rec . lineno ;
406+ } else if ( 'PLpgSQL_row' in datum ) {
407+ lineno = datum . PLpgSQL_row . lineno ;
408+ }
409+
410+ // Skip datums without lineno or loop variables
411+ if ( lineno === undefined || loopVarLinenos . has ( lineno ) ) {
412+ return ;
413+ }
414+
415+ // Only consider datums declared AFTER the top-level BEGIN for nested blocks
416+ // Datums declared before the top-level BEGIN belong to the top-level DECLARE
417+ // If topLevelBlockLineno is undefined, we can't determine scope, so keep all at top-level
418+ if ( topLevelBlockLineno === undefined || lineno <= topLevelBlockLineno ) {
419+ return ; // This datum belongs to top-level DECLARE
420+ }
421+
422+ // Find the block this datum belongs to (the next BEGIN after the datum's lineno)
423+ for ( const blockLineno of nestedBlockLinenos ) {
424+ if ( blockLineno > lineno ) {
425+ // This datum belongs to this block
426+ if ( ! blockDatumMap . has ( blockLineno ) ) {
427+ blockDatumMap . set ( blockLineno , new Set ( ) ) ;
428+ }
429+ blockDatumMap . get ( blockLineno ) ! . add ( index ) ;
430+ return ;
431+ }
432+ }
433+ // If no nested block found, datum belongs to top-level (not added to map)
434+ } ) ;
435+
436+ return blockDatumMap ;
437+ }
438+
439+ /**
440+ * Collect linenos of all nested PLpgSQL_stmt_block nodes in the AST.
441+ * @param isTopLevel - If true, skip the current block (it's the top-level block)
442+ */
443+ private collectNestedBlockLinenos (
444+ stmt : PLpgSQLStmtNode ,
445+ linenos : number [ ] ,
446+ isTopLevel : boolean = false
447+ ) : void {
448+ if ( 'PLpgSQL_stmt_block' in stmt ) {
449+ const block = stmt . PLpgSQL_stmt_block ;
450+ // Only add nested blocks, not the top-level block
451+ if ( ! isTopLevel && block . lineno !== undefined ) {
452+ linenos . push ( block . lineno ) ;
453+ }
454+ if ( block . body ) {
455+ for ( const s of block . body ) {
456+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
457+ }
458+ }
459+ } else if ( 'PLpgSQL_stmt_fori' in stmt ) {
460+ const fori = stmt . PLpgSQL_stmt_fori ;
461+ if ( fori . body ) {
462+ for ( const s of fori . body ) {
463+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
464+ }
465+ }
466+ } else if ( 'PLpgSQL_stmt_fors' in stmt ) {
467+ const fors = stmt . PLpgSQL_stmt_fors ;
468+ if ( fors . body ) {
469+ for ( const s of fors . body ) {
470+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
471+ }
472+ }
473+ } else if ( 'PLpgSQL_stmt_forc' in stmt ) {
474+ const forc = stmt . PLpgSQL_stmt_forc ;
475+ if ( forc . body ) {
476+ for ( const s of forc . body ) {
477+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
478+ }
479+ }
480+ } else if ( 'PLpgSQL_stmt_foreach_a' in stmt ) {
481+ const foreach = stmt . PLpgSQL_stmt_foreach_a ;
482+ if ( foreach . body ) {
483+ for ( const s of foreach . body ) {
484+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
485+ }
486+ }
487+ } else if ( 'PLpgSQL_stmt_loop' in stmt ) {
488+ const loop = stmt . PLpgSQL_stmt_loop ;
489+ if ( loop . body ) {
490+ for ( const s of loop . body ) {
491+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
492+ }
493+ }
494+ } else if ( 'PLpgSQL_stmt_while' in stmt ) {
495+ const whileStmt = stmt . PLpgSQL_stmt_while ;
496+ if ( whileStmt . body ) {
497+ for ( const s of whileStmt . body ) {
498+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
499+ }
500+ }
501+ } else if ( 'PLpgSQL_stmt_if' in stmt ) {
502+ const ifStmt = stmt . PLpgSQL_stmt_if ;
503+ if ( ifStmt . then_body ) {
504+ for ( const s of ifStmt . then_body ) {
505+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
506+ }
507+ }
508+ if ( ifStmt . elsif_list ) {
509+ for ( const elsif of ifStmt . elsif_list ) {
510+ if ( 'PLpgSQL_if_elsif' in elsif && elsif . PLpgSQL_if_elsif . stmts ) {
511+ for ( const s of elsif . PLpgSQL_if_elsif . stmts ) {
512+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
513+ }
514+ }
515+ }
516+ }
517+ if ( ifStmt . else_body ) {
518+ for ( const s of ifStmt . else_body ) {
519+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
520+ }
521+ }
522+ } else if ( 'PLpgSQL_stmt_case' in stmt ) {
523+ const caseStmt = stmt . PLpgSQL_stmt_case ;
524+ if ( caseStmt . case_when_list ) {
525+ for ( const when of caseStmt . case_when_list ) {
526+ if ( 'PLpgSQL_case_when' in when && when . PLpgSQL_case_when . stmts ) {
527+ for ( const s of when . PLpgSQL_case_when . stmts ) {
528+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
529+ }
530+ }
531+ }
532+ }
533+ if ( caseStmt . have_else && caseStmt . else_stmts ) {
534+ for ( const s of caseStmt . else_stmts ) {
535+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
536+ }
537+ }
538+ } else if ( 'PLpgSQL_stmt_dynfors' in stmt ) {
539+ const dynfors = stmt . PLpgSQL_stmt_dynfors ;
540+ if ( dynfors . body ) {
541+ for ( const s of dynfors . body ) {
542+ this . collectNestedBlockLinenos ( s , linenos , false ) ;
543+ }
544+ }
545+ }
546+ }
547+
342548 /**
343549 * Deparse the DECLARE section
550+ * @param datums - All datums from the function
551+ * @param context - Deparser context
552+ * @param loopVarLinenos - Set of linenos for loop-introduced variables to exclude
553+ * @param includedIndices - Optional set of datum indices to include (for nested blocks).
554+ * If provided, only datums at these indices are included.
555+ * @param excludedIndices - Optional set of datum indices to exclude (for top-level).
556+ * If provided, datums at these indices are excluded.
344557 */
345558 private deparseDeclareSection (
346559 datums : PLpgSQLDatum [ ] | undefined ,
347560 context : PLpgSQLDeparserContext ,
348- loopVarLinenos : Set < number > = new Set ( )
561+ loopVarLinenos : Set < number > = new Set ( ) ,
562+ includedIndices ?: Set < number > ,
563+ excludedIndices ?: Set < number >
349564 ) : string {
350565 if ( ! datums || datums . length === 0 ) {
351566 return '' ;
352567 }
353568
354569 // Filter out internal variables (like 'found', parameters, etc.) and loop variables
355- const localVars = datums . filter ( datum => {
570+ const localVars = datums . filter ( ( datum , index ) => {
571+ // If includedIndices is provided, only include datums at those indices
572+ if ( includedIndices !== undefined && ! includedIndices . has ( index ) ) {
573+ return false ;
574+ }
575+ // If excludedIndices is provided, exclude datums at those indices
576+ if ( excludedIndices !== undefined && excludedIndices . has ( index ) ) {
577+ return false ;
578+ }
579+
356580 if ( 'PLpgSQL_var' in datum ) {
357581 const v = datum . PLpgSQL_var ;
358582 // Skip internal variables:
@@ -580,6 +804,20 @@ export class PLpgSQLDeparser {
580804 parts . push ( `<<${ block . label } >>` ) ;
581805 }
582806
807+ // Check if this block has any datums assigned to it (nested DECLARE)
808+ if ( block . lineno !== undefined && context . blockDatumMap ?. has ( block . lineno ) ) {
809+ const includedIndices = context . blockDatumMap . get ( block . lineno ) ! ;
810+ const declareSection = this . deparseDeclareSection (
811+ context . datums ,
812+ context ,
813+ context . loopVarLinenos || new Set ( ) ,
814+ includedIndices
815+ ) ;
816+ if ( declareSection ) {
817+ parts . push ( declareSection ) ;
818+ }
819+ }
820+
583821 parts . push ( kw ( 'BEGIN' ) ) ;
584822
585823 // Body statements
0 commit comments