Skip to content

Commit eec5a55

Browse files
committed
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'.
1 parent 9c75607 commit eec5a55

File tree

3 files changed

+30
-13
lines changed

3 files changed

+30
-13
lines changed

packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ BEGIN
172172
GET DIAGNOSTICS v_rowcount = ;
173173
v_orders_upserted := v_rowcount;
174174
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');
175-
EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts;
175+
EXECUTE v_sql INTO v_rowcount USING p_org_id, p_from_ts, p_to_ts;
176176
IF p_debug THEN
177177
RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount;
178178
END IF;

packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ begin
142142
'app_public',
143143
'app_order'
144144
);
145-
execute v_sql into (unnamed row) using p_org_id, p_from_ts, p_to_ts;
145+
execute v_sql into v_rowcount using p_org_id, p_from_ts, p_to_ts;
146146
if p_debug then
147147
raise notice 'dynamic count(app_order)=%', v_rowcount;
148148
end if;
@@ -343,7 +343,7 @@ BEGIN
343343
'app_public',
344344
'app_order'
345345
);
346-
EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts;
346+
EXECUTE v_sql INTO v_rowcount USING p_org_id, p_from_ts, p_to_ts;
347347
IF p_debug THEN
348348
RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount;
349349
END IF;

packages/plpgsql-deparser/src/plpgsql-deparser.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ export class PLpgSQLDeparser {
769769
parts.push(`<<${fori.label}>>`);
770770
}
771771

772-
const varName = fori.var ? this.deparseDatumName(fori.var) : 'i';
772+
const varName = fori.var ? this.deparseDatumName(fori.var, context) : 'i';
773773
const lower = fori.lower ? this.deparseExpr(fori.lower) : '1';
774774
const upper = fori.upper ? this.deparseExpr(fori.upper) : '10';
775775

@@ -810,7 +810,7 @@ export class PLpgSQLDeparser {
810810
parts.push(`<<${fors.label}>>`);
811811
}
812812

813-
const varName = fors.var ? this.deparseDatumName(fors.var) : 'rec';
813+
const varName = fors.var ? this.deparseDatumName(fors.var, context) : 'rec';
814814
const query = fors.query ? this.deparseExpr(fors.query) : '';
815815
parts.push(`${kw('FOR')} ${varName} ${kw('IN')} ${query} ${kw('LOOP')}`);
816816

@@ -841,7 +841,7 @@ export class PLpgSQLDeparser {
841841
parts.push(`<<${forc.label}>>`);
842842
}
843843

844-
const varName = forc.var ? this.deparseDatumName(forc.var) : 'rec';
844+
const varName = forc.var ? this.deparseDatumName(forc.var, context) : 'rec';
845845
const cursorName = this.getVarName(forc.curvar, context);
846846

847847
let forClause = `${kw('FOR')} ${varName} ${kw('IN')} ${cursorName}`;
@@ -1057,7 +1057,7 @@ export class PLpgSQLDeparser {
10571057
let sql = exec.sqlstmt ? this.deparseExpr(exec.sqlstmt) : '';
10581058

10591059
if (exec.into && exec.target) {
1060-
const targetName = this.deparseDatumName(exec.target);
1060+
const targetName = this.deparseDatumName(exec.target, context);
10611061
// Check if the SQL already contains INTO
10621062
if (!sql.toUpperCase().includes(' INTO ')) {
10631063
// Insert INTO clause after SELECT
@@ -1085,7 +1085,7 @@ export class PLpgSQLDeparser {
10851085

10861086
if (exec.into && exec.target) {
10871087
const strict = exec.strict ? kw('STRICT') + ' ' : '';
1088-
parts.push(`${kw('INTO')} ${strict}${this.deparseDatumName(exec.target)}`);
1088+
parts.push(`${kw('INTO')} ${strict}${this.deparseDatumName(exec.target, context)}`);
10891089
}
10901090

10911091
if (exec.params && exec.params.length > 0) {
@@ -1107,7 +1107,7 @@ export class PLpgSQLDeparser {
11071107
parts.push(`<<${fors.label}>>`);
11081108
}
11091109

1110-
const varName = fors.var ? this.deparseDatumName(fors.var) : 'rec';
1110+
const varName = fors.var ? this.deparseDatumName(fors.var, context) : 'rec';
11111111
let forClause = `${kw('FOR')} ${varName} ${kw('IN EXECUTE')} ${fors.query ? this.deparseExpr(fors.query) : ''}`;
11121112

11131113
if (fors.params && fors.params.length > 0) {
@@ -1224,7 +1224,7 @@ export class PLpgSQLDeparser {
12241224

12251225
// INTO target
12261226
if (!fetch.is_move && fetch.target) {
1227-
parts.push(`${kw('INTO')} ${this.deparseDatumName(fetch.target)}`);
1227+
parts.push(`${kw('INTO')} ${this.deparseDatumName(fetch.target, context)}`);
12281228
}
12291229

12301230
return parts.join(' ');
@@ -1304,21 +1304,38 @@ export class PLpgSQLDeparser {
13041304
return `$${varno}`;
13051305
}
13061306

1307-
return this.deparseDatumName(datum);
1307+
return this.deparseDatumName(datum, context);
13081308
}
13091309

13101310
/**
13111311
* Get the name from a datum
1312+
* For PLpgSQL_row with refname "(unnamed row)", expand the fields array
1313+
* to get the actual variable names
13121314
*/
1313-
private deparseDatumName(datum: PLpgSQLDatum): string {
1315+
private deparseDatumName(datum: PLpgSQLDatum, context?: PLpgSQLDeparserContext): string {
13141316
if ('PLpgSQL_var' in datum) {
13151317
return datum.PLpgSQL_var.refname;
13161318
}
13171319
if ('PLpgSQL_rec' in datum) {
13181320
return datum.PLpgSQL_rec.refname;
13191321
}
13201322
if ('PLpgSQL_row' in datum) {
1321-
return datum.PLpgSQL_row.refname;
1323+
const row = datum.PLpgSQL_row;
1324+
// If this is an "(unnamed row)" with fields, expand the fields to get actual variable names
1325+
if (row.refname === '(unnamed row)' && row.fields && row.fields.length > 0 && context?.datums) {
1326+
const fieldNames = row.fields.map(field => {
1327+
// Try to resolve the varno to get the actual variable name
1328+
const fieldDatum = context.datums[field.varno];
1329+
if (fieldDatum) {
1330+
// Recursively get the name, but without context to avoid infinite loops
1331+
return this.deparseDatumName(fieldDatum);
1332+
}
1333+
// Fall back to the field name if we can't resolve the varno
1334+
return field.name;
1335+
});
1336+
return fieldNames.join(', ');
1337+
}
1338+
return row.refname;
13221339
}
13231340
if ('PLpgSQL_recfield' in datum) {
13241341
return datum.PLpgSQL_recfield.fieldname;

0 commit comments

Comments
 (0)