From eec5a55d24282868836daade8ab38e631acb7bb0 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 6 Jan 2026 04:38:59 +0000 Subject: [PATCH] fix(plpgsql-deparser): expand PLpgSQL_row fields when refname is '(unnamed row)' When SELECT INTO targets multiple variables, PostgreSQL creates a PLpgSQL_row datum with refname '(unnamed row)' and stores the actual variable names in the fields array with varno references. This fix modifies deparseDatumName() to: 1. Accept an optional context parameter to access the datums array 2. Check if PLpgSQL_row.refname is '(unnamed row)' 3. If so, expand the fields array by resolving varno references to get actual variable names 4. Return the comma-separated list of variable names All callers of deparseDatumName() have been updated to pass the context parameter where available. This fixes the issue where generated PL/pgSQL functions contained 'INTO (unnamed row)' instead of the actual variable names like 'INTO v_rowcount'. --- .../__snapshots__/hydrate-demo.test.ts.snap | 2 +- .../__snapshots__/plpgsql-pretty.test.ts.snap | 4 +- .../plpgsql-deparser/src/plpgsql-deparser.ts | 37 ++++++++++++++----- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap b/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap index ac5cd8bb..3b2c9ed2 100644 --- a/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap +++ b/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap @@ -172,7 +172,7 @@ BEGIN GET DIAGNOSTICS v_rowcount = ; v_orders_upserted := v_rowcount; v_sql := format('SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3', 'app_public', 'app_order'); - EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts; + EXECUTE v_sql INTO v_rowcount USING p_org_id, p_from_ts, p_to_ts; IF p_debug THEN RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount; END IF; diff --git a/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap b/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap index f6810cc3..82aa09d0 100644 --- a/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap +++ b/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap @@ -142,7 +142,7 @@ begin 'app_public', 'app_order' ); - execute v_sql into (unnamed row) using p_org_id, p_from_ts, p_to_ts; + execute v_sql into v_rowcount using p_org_id, p_from_ts, p_to_ts; if p_debug then raise notice 'dynamic count(app_order)=%', v_rowcount; end if; @@ -343,7 +343,7 @@ BEGIN 'app_public', 'app_order' ); - EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts; + EXECUTE v_sql INTO v_rowcount USING p_org_id, p_from_ts, p_to_ts; IF p_debug THEN RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount; END IF; diff --git a/packages/plpgsql-deparser/src/plpgsql-deparser.ts b/packages/plpgsql-deparser/src/plpgsql-deparser.ts index 85bdb30e..a8087a21 100644 --- a/packages/plpgsql-deparser/src/plpgsql-deparser.ts +++ b/packages/plpgsql-deparser/src/plpgsql-deparser.ts @@ -769,7 +769,7 @@ export class PLpgSQLDeparser { parts.push(`<<${fori.label}>>`); } - const varName = fori.var ? this.deparseDatumName(fori.var) : 'i'; + const varName = fori.var ? this.deparseDatumName(fori.var, context) : 'i'; const lower = fori.lower ? this.deparseExpr(fori.lower) : '1'; const upper = fori.upper ? this.deparseExpr(fori.upper) : '10'; @@ -810,7 +810,7 @@ export class PLpgSQLDeparser { parts.push(`<<${fors.label}>>`); } - const varName = fors.var ? this.deparseDatumName(fors.var) : 'rec'; + const varName = fors.var ? this.deparseDatumName(fors.var, context) : 'rec'; const query = fors.query ? this.deparseExpr(fors.query) : ''; parts.push(`${kw('FOR')} ${varName} ${kw('IN')} ${query} ${kw('LOOP')}`); @@ -841,7 +841,7 @@ export class PLpgSQLDeparser { parts.push(`<<${forc.label}>>`); } - const varName = forc.var ? this.deparseDatumName(forc.var) : 'rec'; + const varName = forc.var ? this.deparseDatumName(forc.var, context) : 'rec'; const cursorName = this.getVarName(forc.curvar, context); let forClause = `${kw('FOR')} ${varName} ${kw('IN')} ${cursorName}`; @@ -1057,7 +1057,7 @@ export class PLpgSQLDeparser { let sql = exec.sqlstmt ? this.deparseExpr(exec.sqlstmt) : ''; if (exec.into && exec.target) { - const targetName = this.deparseDatumName(exec.target); + const targetName = this.deparseDatumName(exec.target, context); // Check if the SQL already contains INTO if (!sql.toUpperCase().includes(' INTO ')) { // Insert INTO clause after SELECT @@ -1085,7 +1085,7 @@ export class PLpgSQLDeparser { if (exec.into && exec.target) { const strict = exec.strict ? kw('STRICT') + ' ' : ''; - parts.push(`${kw('INTO')} ${strict}${this.deparseDatumName(exec.target)}`); + parts.push(`${kw('INTO')} ${strict}${this.deparseDatumName(exec.target, context)}`); } if (exec.params && exec.params.length > 0) { @@ -1107,7 +1107,7 @@ export class PLpgSQLDeparser { parts.push(`<<${fors.label}>>`); } - const varName = fors.var ? this.deparseDatumName(fors.var) : 'rec'; + const varName = fors.var ? this.deparseDatumName(fors.var, context) : 'rec'; let forClause = `${kw('FOR')} ${varName} ${kw('IN EXECUTE')} ${fors.query ? this.deparseExpr(fors.query) : ''}`; if (fors.params && fors.params.length > 0) { @@ -1224,7 +1224,7 @@ export class PLpgSQLDeparser { // INTO target if (!fetch.is_move && fetch.target) { - parts.push(`${kw('INTO')} ${this.deparseDatumName(fetch.target)}`); + parts.push(`${kw('INTO')} ${this.deparseDatumName(fetch.target, context)}`); } return parts.join(' '); @@ -1304,13 +1304,15 @@ export class PLpgSQLDeparser { return `$${varno}`; } - return this.deparseDatumName(datum); + return this.deparseDatumName(datum, context); } /** * Get the name from a datum + * For PLpgSQL_row with refname "(unnamed row)", expand the fields array + * to get the actual variable names */ - private deparseDatumName(datum: PLpgSQLDatum): string { + private deparseDatumName(datum: PLpgSQLDatum, context?: PLpgSQLDeparserContext): string { if ('PLpgSQL_var' in datum) { return datum.PLpgSQL_var.refname; } @@ -1318,7 +1320,22 @@ export class PLpgSQLDeparser { return datum.PLpgSQL_rec.refname; } if ('PLpgSQL_row' in datum) { - return datum.PLpgSQL_row.refname; + const row = datum.PLpgSQL_row; + // If this is an "(unnamed row)" with fields, expand the fields to get actual variable names + if (row.refname === '(unnamed row)' && row.fields && row.fields.length > 0 && context?.datums) { + const fieldNames = row.fields.map(field => { + // Try to resolve the varno to get the actual variable name + const fieldDatum = context.datums[field.varno]; + if (fieldDatum) { + // Recursively get the name, but without context to avoid infinite loops + return this.deparseDatumName(fieldDatum); + } + // Fall back to the field name if we can't resolve the varno + return field.name; + }); + return fieldNames.join(', '); + } + return row.refname; } if ('PLpgSQL_recfield' in datum) { return datum.PLpgSQL_recfield.fieldname;