[Bug] DROP COLUMN of a distribution key after ROLLBACK TO SAVEPOINT leaves the table distributed by a dropped column
Los mantenedores suelen responder en 2 días
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 48/100
Línea de trabajo
Start with set_random_distribution_if_drop_distkey() in tablecmds.c and the duplicate logic in RemoveAttributeById() in heap.c; trace how policy changes interact with relcache invalidation and subtransaction rollback. Add the described isolation2 regression spec and verify it covers both append-optimized and heap tables. Done means retrying DROP COLUMN after ROLLBACK TO SAVEPOINT sets random distribution, and both tables remain readable.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Apache Cloudberry version
main @ 845fdeccd06 (2026-10-27). The code involved (set_random_distribution_if_drop_distkey() in tablecmds.c and the duplicate block in RemoveAttributeById() in heap.c) has not changed since the initial Cloudberry code dump, so older releases are affected as well.
What happened
Inside a subtransaction, if ALTER TABLE ... DROP COLUMN <distribution key> fails partway through and is rolled back with ROLLBACK TO SAVEPOINT, retrying the same DROP COLUMN in that transaction no longer switches the table to random distribution. After COMMIT, gp_distribution_policy.distkey still points at the dropped column:
\d t
...
Distributed by: ("........pg.dropped.1........" int4_ops)
The table is then unusable. Every read and write fails:
SELECT count(*) FROM t;
ERROR: could not find hash function for type 0 in operator family 1977 (cdbhash.c:449)
INSERT INTO t VALUES (100);
ERROR: could not find hash function for type 0 in operator family 1977 (cdbhash.c:449)
The retried ALTER TABLE also skips the usual NOTICE: dropping a column that is part of the distribution policy forces a NULL distribution policy, which shows it never took the "make it random" branch.
Both heap and append-optimized tables are affected.
What you think should happen instead
The retried DROP COLUMN should behave exactly as it does when nothing failed before it: emit the NOTICE, set the policy to DISTRIBUTED RANDOMLY, and leave a table that can still be read and written.
How to reproduce
You only need SQL and two sessions; no debugger or fault injector. Session 2 holds a lock on gp_distribution_policy, so session 1's ALTER TABLE times out inside set_random_distribution_if_drop_distkey(). That happens after rd_cdbpolicy has been modified in place and before the catalog is updated.
isolation2 spec (passes pg_isolation2_regress as written):
CREATE TABLE drop_distkey_ao (a int, b int) WITH (appendonly=true) DISTRIBUTED BY (a);
CREATE TABLE drop_distkey_heap (a int, b int) DISTRIBUTED BY (a);
INSERT INTO drop_distkey_ao SELECT i, i FROM generate_series(1, 10) i;
INSERT INTO drop_distkey_heap SELECT i, i FROM generate_series(1, 10) i;
-- Load both relcache entries in session 1 before the catalog is locked.
1: SELECT count(*) FROM drop_distkey_ao;
1: SELECT count(*) FROM drop_distkey_heap;
1: BEGIN;
1: SAVEPOINT s1;
2: BEGIN;
2: LOCK TABLE gp_distribution_policy IN ACCESS EXCLUSIVE MODE;
1: SET LOCAL lock_timeout = '1s';
1: ALTER TABLE drop_distkey_ao DROP COLUMN a; -- ERROR: canceling statement due to lock timeout
1: ROLLBACK TO s1;
1: SAVEPOINT s2;
1: SET LOCAL lock_timeout = '1s';
1: ALTER TABLE drop_distkey_heap DROP COLUMN a; -- ERROR: canceling statement due to lock timeout
1: ROLLBACK TO s2;
2: COMMIT;
-- Catalog is still correct here: distkey = 1 for both.
1: SELECT localoid::regclass, distkey FROM gp_distribution_policy
WHERE localoid IN ('drop_distkey_ao'::regclass, 'drop_distkey_heap'::regclass) ORDER BY 1;
1: ALTER TABLE drop_distkey_ao DROP COLUMN a; -- no NOTICE
1: ALTER TABLE drop_distkey_heap DROP COLUMN a; -- no NOTICE
1: COMMIT;
SELECT localoid::regclass, distkey FROM gp_distribution_policy
WHERE localoid IN ('drop_distkey_ao'::regclass, 'drop_distkey_heap'::regclass) ORDER BY 1;
SELECT count(*) FROM drop_distkey_ao;
SELECT count(*) FROM drop_distkey_heap;
Actual output of the last part:
localoid | distkey
-------------------+---------
drop_distkey_ao | 1
drop_distkey_heap | 1
(2 rows)
SELECT count(*) FROM drop_distkey_ao;
ERROR: could not find hash function for type 0 in operator family 1977 (cdbhash.c:449)
SELECT count(*) FROM drop_distkey_heap;
ERROR: could not find hash function for type 0 in operator family 1977 (cdbhash.c:449)
Expected: distkey empty for both tables, and count(*) = 10.
The relcache must be loaded before the lock is taken (the two warm-up SELECTs). Otherwise the ALTER blocks earlier, while building the relcache entry, and never reaches the faulty code. A pg_cancel_backend() / statement_timeout, or any other ERROR raised in the same window, reproduces it too. The window is real in production: the lock wait just makes it deterministic.
Without the savepoint, i.e. a top-level ROLLBACK, the problem does not reproduce (see below for why).
Root cause
/* tablecmds.c, set_random_distribution_if_drop_distkey() */
rel->rd_cdbpolicy->nattrs = 0; /* (1) mutate the relcache entry in place */
...
policy = GpPolicyCopy(rel->rd_cdbpolicy);
GpPolicyReplace(RelationGetRelid(rel), policy); /* (2) update gp_distribution_policy */
rel->rd_cdbpolicy = policy;
- Step (1) writes an uncommitted, not-yet-CCI'd change straight into the relcache entry.
- If an error is raised after (1) and before the next
CommandCounterIncrement()(the one at the end ofdeleteOneObject()), the subtransaction aborts.AtEOSubXact_Inval(false)then replays onlyPriorCmdInvalidMsgs; the current command's messages are discarded. This is upstream PostgreSQL's design: it assumes that no cache yet reflects catalog changes the current command has not made visible. Step (1) breaks that assumption, so the stale entry (nattrs = 0) survivesROLLBACK TO, while the catalog correctly still saysdistkey = 1. - On the retry,
GpPolicyIsPartitioned(rel->rd_cdbpolicy)seesnattrs == 0, and the function treats the table as already random and returns without touching the catalog.pg_attributemarks column 1 dropped, andCOMMITmakes it permanent, leaving the catalog policy pointing at a dropped column.
The problem does not show up at top level only because of 1073dbdf59b ("Invalidate CatCache in AbortTransaction to clear reader gang's cache", GPDB #14048). That commit makes AbortTransaction() call AtEOXact_Inval(true) on the QD and writer QEs, which also processes the current command's messages and happens to hide the bug. There is no equivalent for subtransaction abort.
Also note that RemoveAttributeById() in heap.c (around line 2165) has a second copy of the same logic, with the same in-place mutation. Today it is dead code: the first copy has already set nattrs = 0, so GpPolicyIsHashPartitioned() is false by the time it runs.
Proposed fix
Follow the normal PostgreSQL pattern: change the catalog, and let the invalidation machinery update the relcache.
- In
set_random_distribution_if_drop_distkey(), build the new policy as a copy (GpPolicyCopy()in the current memory context, then setnattrs = 0on the copy). CallGpPolicyReplace(), and do not modifyrel->rd_cdbpolicyor assign to it. Updatinggp_distribution_policyalready registers a relcache invalidation forlocaloid(inval.c,tupleRelId == GpPolicyRelationId). TheCommandCounterIncrement()at the end ofdeleteOneObject()processes it locally, and the rebuild reads the new random policy.- Error before that CCI: the relcache was never touched, so it is consistent with the rolled-back catalog.
- Error after that CCI: the messages are in
PriorCmdInvalidMsgs, the subtransaction abort replays them, and the entry is rebuilt from the rolled-back catalog.
- Remove the duplicate block in
RemoveAttributeById(). Once (1) is in place it would no longer be dead: before the CCI,rd_cdbpolicystill shows the old key, so it would callGpPolicyReplace()a second time on the same tuple in the same command (tuple already updated by self). - Add the isolation2 spec above as a regression test, checking both AO and heap tables.
Two related spots use a similar pattern and are worth a look, though the window is much narrower because they don't mutate in place and a CCI follows soon after. Both call GpPolicyReplace() and then assign a new pointer to rel->rd_cdbpolicy before any CCI:
ATExecExpandTable()/ leaf-partition expand- the CTAS path of
ALTER TABLE ... SET DISTRIBUTED BY, which sets a temporary random policy and later restoresoriginal_policy
These could be fixed the same way, separately.
Operating System
Rocky/CentOS 7 (x86_64), demo cluster with 3 segments. The behavior is not OS-specific.
Anything else
The same issue was first found in a downstream fork, using gdb to inject a cancel at the same point. The lock-timeout reproduction above needs only SQL.
Are you willing to submit PR?
- Yes, I am willing to submit a PR!
Code of Conduct
- I agree to follow this project's Code of Conduct
- Lenguaje dominante
- C
- Estrellas
- 1.4k
- Forks
- 265
- Merge medio
- 3 d 17 h
- PR fusionados (30 d)
- 68
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de apache/cloudberry
-
[Bug] Expand fails to recognize user-defined tablespacesPosiblemente ocupada @nix-oss la tomó hace 72 días. Abiertotype: Bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
apache/cloudberry#1885 · 2 reacciones ·
Los mantenedores suelen responder en 2 días
-
gpexpand first stage fails: 'SyncPackages' object has no attribute 'ret' (extension sync never runs; regression)Posiblemente ocupada @talmacschen-arch la tomó hace 134 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
apache/cloudberry#1825 ·
Los mantenedores suelen responder en 2 días
-
[Bug] GPORCA: “could not find MotionState” for correlated EXISTS with LIMIT and a scalar subqueryPosiblemente ocupada @Alena0704 la tomó hace 1 día. Abiertotype: Bug
apache/cloudberry#2221 · 1 asignado ·
Los mantenedores suelen responder en 2 días
-
Dificultad 4/5 3-5 días Aptitud para principiantes 42/100
apache/cloudberry#2214 · 1 comentario ·
Los mantenedores suelen responder en 2 días
-
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
apache/cloudberry#2213 · 4 comentarios ·
Los mantenedores suelen responder en 2 días
Todos los issues de apache/cloudberry
Issues similares
-
vim.filetype.match() prefixes a URL buffer name with the cwd before passing it to filetype functionsAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 73/100
Los mantenedores suelen responder en 1 día
-
stream_curl: retry_count never resets after a successful reconnect, so long sequential streams die after 5 dropsPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
mpv-player/mpv#18728 ·
Los mantenedores suelen responder en 1 día
-
followup Prio 📌
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
cyrusimap/cyrus-imapd#6640 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 60/100
Los mantenedores suelen responder en 1 día