From 114bb649d1b79c1f7b8519e689296dfb6bc0d67c Mon Sep 17 00:00:00 2001
From: Sebastian Webber <sebastian@swebber.me>
Date: Wed, 14 Oct 2026 11:18:18 -0300
Subject: [PATCH v1] Fix ALTER COLUMN TYPE USING for inheritance children

ALTER TABLE ... ALTER COLUMN TYPE ... USING maps the parent's transformed
USING expression to each child's attribute numbers, but since 1281a5c90
the child's own pass through transformAlterTableStmt then transforms the
raw expression again and throws the mapped one away.  That second pass
goes wrong in two ways.  transformExpr modifies some raw nodes in place
(NullTest, BooleanTest, NamedArgExpr), so after the parent's pass their
arguments are Vars with the parent's attribute numbers, which the
child's pass keeps as they are: the child reads the wrong column, or
fails with "unrecognized node type" when the argument is a Const.  And
a column qualified with the parent's name fails with "missing
FROM-clause entry", since only the child is in the namespace.

Transform the USING expression only when it has not been transformed
yet, so children keep the mapped expression, as before 1281a5c90.

Reported-by: kalakala <zj4204@gmail.com>
Discussion: https://postgr.es/m/19756-046a8a872ba33d27@postgresql.org
Backpatch-through: 14
---
 src/backend/parser/parse_utilcmd.c        |  9 ++++++--
 src/test/regress/expected/alter_table.out | 25 +++++++++++++++++++++++
 src/test/regress/sql/alter_table.sql      | 13 ++++++++++++
 3 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f0c7755b8fb..10e09014daa 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3687,9 +3687,14 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 
 					/*
 					 * For ALTER COLUMN TYPE, transform the USING clause if
-					 * one was specified.
+					 * one was specified.  When recursing to a child table,
+					 * cooked_default already holds the parent's expression
+					 * with its attribute numbers mapped to the child (see
+					 * ATPrepAlterColumnType); keep that, since the raw
+					 * expression need not mean the same thing against the
+					 * child, and transformExpr may have scribbled on it.
 					 */
-					if (def->raw_default)
+					if (def->raw_default && def->cooked_default == NULL)
 					{
 						def->cooked_default =
 							transformExpr(pstate, def->raw_default,
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 20707c186b3..6a7b948e4dc 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -2591,6 +2591,31 @@ ALTER TABLE test_type_diff2 ALTER COLUMN int_four TYPE int8 USING int_four::int8
 ALTER TABLE test_type_diff2 ALTER COLUMN int_four TYPE int4 USING (pg_column_size(test_type_diff2));
 ERROR:  cannot convert whole-row table reference
 DETAIL:  USING expression contains a whole-row table reference.
+-- USING expressions on children with other column numbers (bug #19756)
+CREATE TABLE test_type_diff3 (a int, b int, c bool, d bool);
+CREATE TABLE test_type_diff3_c (b int, a int, d bool, c bool);
+ALTER TABLE test_type_diff3_c INHERIT test_type_diff3;
+INSERT INTO test_type_diff3 VALUES (1, NULL, true, NULL);
+INSERT INTO test_type_diff3_c VALUES (2, 3, true, NULL);
+ALTER TABLE test_type_diff3 ALTER COLUMN a TYPE int8
+  USING a * 1000 + (b IS NULL)::int * 100 + (c IS UNKNOWN)::int * 10
+    + coalesce(extract(day from make_interval(days => b))::int, 0);
+SELECT tableoid::regclass, a, b, c, d FROM test_type_diff3 ORDER BY a;
+     tableoid      |  a   | b | c | d 
+-------------------+------+---+---+---
+ test_type_diff3   | 1100 |   | t | 
+ test_type_diff3_c | 3012 | 2 |   | t
+(2 rows)
+
+ALTER TABLE test_type_diff3 ALTER COLUMN a TYPE int4 USING a + (1 IS NULL)::int;
+ALTER TABLE test_type_diff3 ALTER COLUMN a TYPE int8 USING test_type_diff3.a + 1;
+SELECT tableoid::regclass, a, b, c, d FROM test_type_diff3 ORDER BY a;
+     tableoid      |  a   | b | c | d 
+-------------------+------+---+---+---
+ test_type_diff3   | 1101 |   | t | 
+ test_type_diff3_c | 3013 | 2 |   | t
+(2 rows)
+
 -- check for rollback of ANALYZE corrupting table property flags (bug #11638)
 CREATE TABLE check_fk_presence_1 (id int PRIMARY KEY, t text);
 CREATE TABLE check_fk_presence_2 (id int REFERENCES check_fk_presence_1, t text);
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 18f3fdac57a..99e35226942 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1646,6 +1646,19 @@ INSERT INTO test_type_diff2_c3 VALUES (7, 8, 9);
 ALTER TABLE test_type_diff2 ALTER COLUMN int_four TYPE int8 USING int_four::int8;
 -- whole-row references are disallowed
 ALTER TABLE test_type_diff2 ALTER COLUMN int_four TYPE int4 USING (pg_column_size(test_type_diff2));
+-- USING expressions on children with other column numbers (bug #19756)
+CREATE TABLE test_type_diff3 (a int, b int, c bool, d bool);
+CREATE TABLE test_type_diff3_c (b int, a int, d bool, c bool);
+ALTER TABLE test_type_diff3_c INHERIT test_type_diff3;
+INSERT INTO test_type_diff3 VALUES (1, NULL, true, NULL);
+INSERT INTO test_type_diff3_c VALUES (2, 3, true, NULL);
+ALTER TABLE test_type_diff3 ALTER COLUMN a TYPE int8
+  USING a * 1000 + (b IS NULL)::int * 100 + (c IS UNKNOWN)::int * 10
+    + coalesce(extract(day from make_interval(days => b))::int, 0);
+SELECT tableoid::regclass, a, b, c, d FROM test_type_diff3 ORDER BY a;
+ALTER TABLE test_type_diff3 ALTER COLUMN a TYPE int4 USING a + (1 IS NULL)::int;
+ALTER TABLE test_type_diff3 ALTER COLUMN a TYPE int8 USING test_type_diff3.a + 1;
+SELECT tableoid::regclass, a, b, c, d FROM test_type_diff3 ORDER BY a;
 
 -- check for rollback of ANALYZE corrupting table property flags (bug #11638)
 CREATE TABLE check_fk_presence_1 (id int PRIMARY KEY, t text);
-- 
2.55.0

