From 76d15a7ee9de91afda672cd7ed56cdcd9909f4ef Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Thu, 30 Apr 2026 11:04:57 -0400 Subject: [PATCH] Fix attnum remapping in generateClonedExtStatsStmt() When cloning extended statistics via CREATE TABLE ... LIKE ... INCLUDING STATISTICS, stxkeys holds attribute numbers from the source (parent) table, but get_attname() was being called with the child relation's OID. If the parent has dropped columns, the child's attribute numbers are renumbered sequentially and no longer match, so the lookup either returns the wrong column name (silent corruption) or errors out when the attnum does not exist in the child. Fix it by remapping the parent attnum through attmap before the lookup, consistent with how expression statistics are already handled a few lines below. Add a regression test covering both manifestations: a 3-column parent where the stale attnum refers to no child column (cache-lookup error), and a 4-column parent where the stale attnum silently refers to the wrong child column. Author: Julien Tachoires Reviewed-by: Srinath Reddy Sadipiralla Discussion: https://postgr.es/m/20260415105718.tomuncfbmlt67oel@poseidon.home.virt Backpatch-through: 14 --- src/backend/parser/parse_utilcmd.c | 8 +++-- .../regress/expected/create_table_like.out | 31 +++++++++++++++++++ src/test/regress/sql/create_table_like.sql | 26 ++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index dd582f97cbb..406c8c10e34 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -1911,7 +1911,10 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, * extended statistic "source_statsid", for the rel identified by heapRel and * heapRelid. * - * Attribute numbers in expression Vars are adjusted according to attmap. + * stxkeys in the source statistic holds attribute numbers from the parent + * relation. Those attnums, along with the attribute numbers referenced by + * Vars inside the expression tree, are remapped to the new relation's + * numbering according to attmap. */ static CreateStatsStmt * generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid, @@ -1970,7 +1973,8 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid, StatsElem *selem = makeNode(StatsElem); AttrNumber attnum = statsrec->stxkeys.values[i]; - selem->name = get_attname(heapRelid, attnum, false); + selem->name = + get_attname(heapRelid, attmap->attnums[attnum - 1], false); selem->expr = NULL; def_names = lappend(def_names, selem); diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out index 6bfc6d040ff..f49f2684a3a 100644 --- a/src/test/regress/expected/create_table_like.out +++ b/src/test/regress/expected/create_table_like.out @@ -533,3 +533,34 @@ DROP TYPE ctlty1; DROP VIEW ctlv1; DROP TABLE IF EXISTS ctlt4, ctlt10, ctlt11, ctlt11a, ctlt12; NOTICE: table "ctlt10" does not exist, skipping +-- LIKE ... INCLUDING STATISTICS with dropped columns in the parent, +-- so stxkeys attnums are not contiguous. +CREATE TABLE ctl_stats3_parent (a int, b int, c int); +ALTER TABLE ctl_stats3_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats3_stat ON a, c FROM ctl_stats3_parent; +CREATE TABLE ctl_stats3_child (LIKE ctl_stats3_parent INCLUDING STATISTICS); +CREATE TABLE ctl_stats4_parent (a int, b int, c int, d int); +ALTER TABLE ctl_stats4_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats4_stat ON a, c FROM ctl_stats4_parent; +CREATE TABLE ctl_stats4_child (LIKE ctl_stats4_parent INCLUDING STATISTICS); +SELECT s.stxrelid::regclass AS relation, + array_agg(a.attname ORDER BY u.ord) AS stats_columns +FROM pg_statistic_ext s +CROSS JOIN LATERAL + unnest(s.stxkeys::int2[]) WITH ORDINALITY AS u(attnum, ord) +JOIN pg_attribute a + ON a.attrelid = s.stxrelid AND a.attnum = u.attnum +WHERE s.stxrelid IN ('ctl_stats3_child'::regclass, + 'ctl_stats4_child'::regclass) +GROUP BY s.stxrelid +ORDER BY s.stxrelid::regclass::text; + relation | stats_columns +------------------+--------------- + ctl_stats3_child | {a,c} + ctl_stats4_child | {a,c} +(2 rows) + +DROP TABLE ctl_stats3_parent; +DROP TABLE ctl_stats3_child; +DROP TABLE ctl_stats4_parent; +DROP TABLE ctl_stats4_child; diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql index 04008a027b8..7c7206c66ee 100644 --- a/src/test/regress/sql/create_table_like.sql +++ b/src/test/regress/sql/create_table_like.sql @@ -223,3 +223,29 @@ DROP SEQUENCE ctlseq1; DROP TYPE ctlty1; DROP VIEW ctlv1; DROP TABLE IF EXISTS ctlt4, ctlt10, ctlt11, ctlt11a, ctlt12; + +-- LIKE ... INCLUDING STATISTICS with dropped columns in the parent, +-- so stxkeys attnums are not contiguous. +CREATE TABLE ctl_stats3_parent (a int, b int, c int); +ALTER TABLE ctl_stats3_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats3_stat ON a, c FROM ctl_stats3_parent; +CREATE TABLE ctl_stats3_child (LIKE ctl_stats3_parent INCLUDING STATISTICS); +CREATE TABLE ctl_stats4_parent (a int, b int, c int, d int); +ALTER TABLE ctl_stats4_parent DROP COLUMN b; +CREATE STATISTICS ctl_stats4_stat ON a, c FROM ctl_stats4_parent; +CREATE TABLE ctl_stats4_child (LIKE ctl_stats4_parent INCLUDING STATISTICS); +SELECT s.stxrelid::regclass AS relation, + array_agg(a.attname ORDER BY u.ord) AS stats_columns +FROM pg_statistic_ext s +CROSS JOIN LATERAL + unnest(s.stxkeys::int2[]) WITH ORDINALITY AS u(attnum, ord) +JOIN pg_attribute a + ON a.attrelid = s.stxrelid AND a.attnum = u.attnum +WHERE s.stxrelid IN ('ctl_stats3_child'::regclass, + 'ctl_stats4_child'::regclass) +GROUP BY s.stxrelid +ORDER BY s.stxrelid::regclass::text; +DROP TABLE ctl_stats3_parent; +DROP TABLE ctl_stats3_child; +DROP TABLE ctl_stats4_parent; +DROP TABLE ctl_stats4_child;