From 89e4bd766393342b095246fec388fa9e7f31b1fc Mon Sep 17 00:00:00 2001 From: Manuel Reyes Bravo Date: Fri, 2 Oct 2026 10:50:40 -0300 Subject: [PATCH v7] Fix index corruption after rolling back ALTER TABLE SET TABLESPACE ALTER TABLE ... SET TABLESPACE rewrites a table's heap to a new relfilenode but leaves the table's indexes on their existing relfilenodes. The two then roll back by different mechanisms: on abort the heap's new file is discarded, freeing the heap TIDs consumed by rows inserted after the SET TABLESPACE, while the index entries for those rows were written to the unchanged index files and survive. A later insert can reuse a freed heap TID, leaving two index entries that point at the same live heap tuple. This surfaces as a _bt_posting_valid assertion failure in nbtree deduplication, and as duplicate rows through an index-only scan in gist. A TRUNCATE after the move in the same transaction truncates the old index files in place, which a rollback cannot undo either. Fix by giving each index of the table a new relfilenumber in the same ALTER, copying it within its own tablespace (the indexes stay where they are, as documented), so that an abort discards the new heap and index files together. Two cases need no copy: - The ALTER is a plain ALTER TABLE ... SET TABLESPACE of a single table, with no other subcommand, sent by a client as the only statement of a simple Query message outside a transaction block, so the transaction ends right after it. Any other subcommand could run user code after the move: the validation of an inheritance child's new CHECK constraint, a sql_drop event trigger, and so on. This also requires that no ddl_command_end event trigger runs after it. A statement sent through the extended query protocol always copies: a later statement of the same pipeline shares its transaction until the next Sync, and the backend cannot tell whether one will follow. IsExtendedQueryMessage() is added to postgres.c to tell the two apart. - The index's current file was created in the current subtransaction, for example an index created, or moved, earlier in it. An abort discards that file together with the heap's new one. This also lets a transaction move the indexes first and then the table without copying the indexes twice. The regression test checks that the ALTER on its own leaves the index files untouched; that combined with a CHECK constraint whose validation on an inheritance child writes to the table after the move and fails, it leaves no stale index entry; that in a transaction block a rollback after inserting or truncating leaves the index file at its pre- transaction size; that an index created or moved earlier in the subtransaction is not copied; and that after a rollback to a savepoint, an index created, reindexed or already copied before the savepoint returns no row for the rolled-back values. Bug: #19686 Reported-by: Alexander Lakhin Suggested-by: Andres Freund Reviewed-by: Alexandre Felipe Reviewed-by: Shihao Zhong --- src/backend/commands/tablecmds.c | 150 ++++++++++++++++++++- src/backend/tcop/postgres.c | 17 +++ src/backend/tcop/utility.c | 1 + src/include/tcop/tcopprot.h | 1 + src/include/tcop/utility.h | 1 + src/test/regress/input/tablespace.source | 102 +++++++++++++++ src/test/regress/output/tablespace.source | 153 ++++++++++++++++++++++ 7 files changed, 420 insertions(+), 5 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index e445b801a6b..77e7bc4d105 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -89,6 +89,7 @@ #include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/evtcache.h" #include "utils/fmgroids.h" #include "utils/inval.h" #include "utils/lsyscache.h" @@ -549,7 +550,11 @@ static void ATExecDropCluster(Relation rel, LOCKMODE lockmode); static bool ATPrepChangePersistence(Relation rel, bool toLogged); static void ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacename, LOCKMODE lockmode); -static void ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode); +static void ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode, + bool copyIndexes); +static bool ATSetTableSpaceCopyIndexes(AlterTableStmt *parsetree, List *wqueue, + AlterTableUtilityContext *context); +static void ATExecSetTableSpaceNewIndexRelfilenode(Oid indexOid, LOCKMODE lockmode); static void ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace); static void ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, @@ -5582,7 +5587,9 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode, * just do a block-by-block copy. */ if (tab->newTableSpace) - ATExecSetTableSpace(tab->relid, tab->newTableSpace, lockmode); + ATExecSetTableSpace(tab->relid, tab->newTableSpace, lockmode, + ATSetTableSpaceCopyIndexes(parsetree, *wqueue, + context)); } } @@ -14198,13 +14205,16 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, * rewriting to be done, so we just want to copy the data as fast as possible. */ static void -ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) +ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode, + bool copyIndexes) { Relation rel; Oid reltoastrelid; + char relkind; Oid newrelfilenode; RelFileNode newrnode; List *reltoastidxids = NIL; + List *reltabidxids = NIL; ListCell *lc; /* @@ -14222,6 +14232,7 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) } reltoastrelid = rel->rd_rel->reltoastrelid; + relkind = rel->rd_rel->relkind; /* Fetch the list of indexes on toast relation if necessary */ if (OidIsValid(reltoastrelid)) { @@ -14270,6 +14281,14 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) RelationAssumeNewRelfilenode(rel); + /* + * If this is a table, collect its index list now, while the relation is + * still open, so we can give each index a fresh relfilenode below. + */ + if (copyIndexes && + (relkind == RELKIND_RELATION || relkind == RELKIND_MATVIEW)) + reltabidxids = RelationGetIndexList(rel); + relation_close(rel, NoLock); /* Make sure the reltablespace change is visible */ @@ -14277,12 +14296,133 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode) /* Move associated toast relation and/or indexes, too */ if (OidIsValid(reltoastrelid)) - ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode); + ATExecSetTableSpace(reltoastrelid, newTableSpace, lockmode, copyIndexes); foreach(lc, reltoastidxids) - ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode); + ATExecSetTableSpace(lfirst_oid(lc), newTableSpace, lockmode, copyIndexes); /* Clean up */ list_free(reltoastidxids); + + /* + * The heap now has a new relfilenode. Give each of the table's indexes a + * fresh relfilenode too, so that the indexes share the heap's rewrite fate + * across commit and abort. See ATExecSetTableSpaceNewIndexRelfilenode. + */ + foreach(lc, reltabidxids) + ATExecSetTableSpaceNewIndexRelfilenode(lfirst_oid(lc), lockmode); + list_free(reltabidxids); +} + +/* + * Does ALTER TABLE SET TABLESPACE need to give the table's indexes new files? + * + * Only if something can still modify the table in the same transaction after + * the move. Rather than listing every place where user code can run after + * it -- the validation of other tables in the work queue, such as an + * inheritance child getting a new CHECK constraint, foreign key validation, + * sql_drop and table_rewrite event triggers, and more -- the files are kept + * only for the one case where none of them exists: a plain + * "ALTER TABLE ... SET TABLESPACE" of a single table, with no other + * subcommand, sent by a client as the only statement of a simple Query + * message, outside any transaction block: the transaction then ends right + * after it. What can still run after it is a ddl_command_end event trigger, + * so that case also requires that there is none. + * + * A statement sent by an extended-protocol Execute message always copies: + * later statements of the same pipeline share its transaction until the next + * Sync, and the backend cannot tell whether any will follow. Forcing a commit + * after the ALTER would make it commit on its own when a later statement of + * the pipeline fails, which is not how ALTER TABLE behaves otherwise. + */ +static bool +ATSetTableSpaceCopyIndexes(AlterTableStmt *parsetree, List *wqueue, + AlterTableUtilityContext *context) +{ + if (context == NULL || IsInTransactionBlock(context->isTopLevel)) + return true; + if (MyBackendType != B_BACKEND || IsExtendedQueryMessage()) + return true; + if (parsetree == NULL || list_length(parsetree->cmds) != 1 || + castNode(AlterTableCmd, linitial(parsetree->cmds))->subtype != AT_SetTableSpace || + list_length(wqueue) != 1) + return true; + if (EventCacheLookup(EVT_DDLCommandEnd) != NIL) + return true; + + return false; +} + +/* + * Give one of a table's indexes a fresh relfilenode within its existing + * tablespace, copying the current index file to the new relfilenode. + * + * ATExecSetTableSpace() calls this for each index of a table whose heap it has + * just rewritten to a new relfilenode. The indexes must share the heap's + * transactional fate: an abort has to discard the index entries written during + * the transaction together with the heap's new file, since the heap TIDs those + * entries point at are freed by the abort and can be reused by later inserts. + * Copying the existing index file keeps the added cost close to that of the + * heap move itself; the index stays in its own tablespace, as documented. + */ +static void +ATExecSetTableSpaceNewIndexRelfilenode(Oid indexOid, LOCKMODE lockmode) +{ + Relation ind; + Oid newrelfilenode; + RelFileNode newrnode; + Relation pg_class; + HeapTuple tuple; + ItemPointerData otid; + Form_pg_class rd_rel; + + ind = relation_open(indexOid, lockmode); + + /* + * Only plain indexes have storage that can hold the stale entries. An + * index whose current file was created in this subtransaction (the index + * itself, or a new file for it) is discarded together with the heap's new + * file on abort, so it needs no copy. rd_newRelfilenodeSubid can be zero + * after a rollback to a savepoint even though the file is new; then this + * falls back to rd_createSubid, and copies if that does not match either. + */ + if (ind->rd_rel->relkind != RELKIND_INDEX || + !RELKIND_HAS_STORAGE(ind->rd_rel->relkind) || + Max(ind->rd_newRelfilenodeSubid, ind->rd_createSubid) == + GetCurrentSubTransactionId()) + { + relation_close(ind, NoLock); + return; + } + + /* Allocate a new relfilenode in the index's current tablespace. */ + newrelfilenode = GetNewRelFileNode(ind->rd_rel->reltablespace, NULL, + ind->rd_rel->relpersistence); + newrnode = ind->rd_node; + newrnode.relNode = newrelfilenode; + + /* Copy the index into the new file and schedule the old one for cleanup. */ + index_copy_data(ind, newrnode); + + /* Update the pg_class row; only the relfilenode changes. */ + pg_class = table_open(RelationRelationId, RowExclusiveLock); + tuple = SearchSysCacheLockedCopy1(RELOID, ObjectIdGetDatum(indexOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for index %u", indexOid); + otid = tuple->t_self; + rd_rel = (Form_pg_class) GETSTRUCT(tuple); + rd_rel->relfilenode = newrelfilenode; + CatalogTupleUpdate(pg_class, &otid, tuple); + UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock); + heap_freetuple(tuple); + table_close(pg_class, RowExclusiveLock); + + InvokeObjectPostAlterHook(RelationRelationId, indexOid, 0); + RelationAssumeNewRelfilenode(ind); + + relation_close(ind, NoLock); + + /* Make the relfilenode change visible. */ + CommandCounterIncrement(); } /* diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 0e64b04bd16..34b82cbb24b 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -2309,6 +2309,23 @@ check_log_statement(List *stmt_list) return false; } +/* + * IsExtendedQueryMessage + * Is the current statement being run by an extended-query-protocol + * message? + * + * Such a statement shares its implicit transaction with whatever else the + * client sends before the next Sync, which the backend cannot know while it + * runs the statement. The first statement of a pipeline cannot be told from + * one followed directly by Sync: XACT_FLAGS_PIPELINING is set only once an + * Execute completes. + */ +bool +IsExtendedQueryMessage(void) +{ + return doing_extended_query_message; +} + /* * check_log_duration * Determine whether current command's duration should be logged diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 51509efbfc1..cbfd800ad35 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -1308,6 +1308,7 @@ ProcessUtilitySlow(ParseState *pstate, atcontext.relid = relid; atcontext.params = params; atcontext.queryEnv = queryEnv; + atcontext.isTopLevel = isTopLevel; /* ... ensure we have an event trigger context ... */ EventTriggerAlterTableStart(parsetree); diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h index 00da5e66e70..e85e47b241c 100644 --- a/src/include/tcop/tcopprot.h +++ b/src/include/tcop/tcopprot.h @@ -90,6 +90,7 @@ extern void PostgresMain(int argc, char *argv[], extern long get_stack_depth_rlimit(void); extern void ResetUsage(void); extern void ShowUsage(const char *title); +extern bool IsExtendedQueryMessage(void); extern int check_log_duration(char *msec_str, bool was_logged); extern void set_debug_options(int debug_flag, GucContext context, GucSource source); diff --git a/src/include/tcop/utility.h b/src/include/tcop/utility.h index 212e9b32806..7cd9a891d2b 100644 --- a/src/include/tcop/utility.h +++ b/src/include/tcop/utility.h @@ -34,6 +34,7 @@ typedef struct AlterTableUtilityContext Oid relid; /* OID of ALTER's target table */ ParamListInfo params; /* any parameters available to ALTER TABLE */ QueryEnvironment *queryEnv; /* execution environment for ALTER TABLE */ + bool isTopLevel; /* ALTER TABLE is a top-level statement */ } AlterTableUtilityContext; /* diff --git a/src/test/regress/input/tablespace.source b/src/test/regress/input/tablespace.source index fd003d805e7..a6d699f4b65 100644 --- a/src/test/regress/input/tablespace.source +++ b/src/test/regress/input/tablespace.source @@ -400,6 +400,108 @@ REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail REINDEX (TABLESPACE regress_tblspace, CONCURRENTLY) TABLE tablespace_table; -- fail RESET ROLE; +-- ALTER TABLE SET TABLESPACE moves the heap and leaves the indexes in their +-- tablespace. Run on its own, as here, it leaves their files alone too. +CREATE TABLE tbspace_rollback (a int); +INSERT INTO tbspace_rollback SELECT generate_series(1, 100); +CREATE INDEX tbspace_rollback_idx ON tbspace_rollback (a); +SELECT pg_relation_size('tbspace_rollback_idx') AS idx_size_before \gset +SELECT pg_relation_filepath('tbspace_rollback_idx') AS idx_path \gset +ALTER TABLE tbspace_rollback SET TABLESPACE regress_tblspace; +SELECT pg_relation_filepath('tbspace_rollback_idx') = :'idx_path' AS idx_file_kept; +-- In a transaction block the indexes get new files, so that a rollback +-- discards the entries added after the move together with the heap's file. +BEGIN; +ALTER TABLE tbspace_rollback SET TABLESPACE pg_default; +SELECT pg_relation_filepath('tbspace_rollback_idx') <> :'idx_path' AS idx_file_new; +INSERT INTO tbspace_rollback SELECT generate_series(101, 2000); +SELECT pg_relation_size('tbspace_rollback_idx') > :idx_size_before AS idx_grew; +ROLLBACK; +SELECT pg_relation_size('tbspace_rollback_idx') = :idx_size_before AS idx_size_restored; +-- the same for a TRUNCATE after the move, which is done in place +BEGIN; +ALTER TABLE tbspace_rollback SET TABLESPACE pg_default; +TRUNCATE tbspace_rollback; +ROLLBACK; +SELECT pg_relation_size('tbspace_rollback_idx') = :idx_size_before AS idx_size_restored; +SELECT count(*) FROM tbspace_rollback; +-- An index created in the same subtransaction is discarded with the heap +-- anyway and is not copied. Moving the indexes first and then the table +-- copies each index only once. +BEGIN; +CREATE INDEX tbspace_rollback_idx2 ON tbspace_rollback (a); +SELECT pg_relation_filepath('tbspace_rollback_idx2') AS idx2_path \gset +ALTER INDEX tbspace_rollback_idx SET TABLESPACE regress_tblspace; +SELECT pg_relation_filepath('tbspace_rollback_idx') AS idx_moved_path \gset +ALTER TABLE tbspace_rollback SET TABLESPACE pg_default; +SELECT pg_relation_filepath('tbspace_rollback_idx2') = :'idx2_path' AS idx2_not_copied, + pg_relation_filepath('tbspace_rollback_idx') = :'idx_moved_path' AS idx_not_copied_again; +ROLLBACK; +DROP TABLE tbspace_rollback; +-- An index file that is new in this transaction but older than a savepoint +-- survives a rollback to it, so it must still follow a move made after the +-- savepoint: an index created or reindexed before it, or already copied by +-- an earlier move. Rows inserted afterwards reuse the TIDs freed by the +-- rollbacks; a stale index entry would match them. +CREATE TABLE tbspace_subxact (a int); +INSERT INTO tbspace_subxact SELECT generate_series(1, 100); +SET enable_seqscan = off; +SET enable_bitmapscan = off; +BEGIN; +CREATE INDEX tbspace_subxact_idx ON tbspace_subxact (a); +SAVEPOINT s; +ALTER TABLE tbspace_subxact SET TABLESPACE regress_tblspace; +INSERT INTO tbspace_subxact SELECT -generate_series(1, 50); +ROLLBACK TO s; +COMMIT; +BEGIN; +REINDEX INDEX tbspace_subxact_idx; +SAVEPOINT s; +ALTER TABLE tbspace_subxact SET TABLESPACE regress_tblspace; +INSERT INTO tbspace_subxact SELECT -generate_series(1, 50); +ROLLBACK TO s; +COMMIT; +BEGIN; +ALTER TABLE tbspace_subxact SET TABLESPACE regress_tblspace; +INSERT INTO tbspace_subxact VALUES (0); +SAVEPOINT s; +ALTER TABLE tbspace_subxact SET TABLESPACE pg_default; +INSERT INTO tbspace_subxact SELECT -generate_series(1, 50); +ROLLBACK TO s; +COMMIT; +INSERT INTO tbspace_subxact SELECT generate_series(101, 300); +SELECT count(*) FROM tbspace_subxact WHERE a < 0; +SELECT count(*) FROM tbspace_subxact; +RESET enable_seqscan; +RESET enable_bitmapscan; +DROP TABLE tbspace_subxact; +-- Combined with another subcommand, the ALTER can run user code after the +-- move, such as a CHECK constraint validated on an inheritance child after +-- the parent was moved, so the indexes get new files even outside a +-- transaction block. +CREATE TABLE tbspace_combined (a int); +INSERT INTO tbspace_combined SELECT generate_series(1, 100); +CREATE INDEX tbspace_combined_idx ON tbspace_combined (a); +CREATE TABLE tbspace_combined_child () INHERITS (tbspace_combined); +INSERT INTO tbspace_combined_child VALUES (5000); +CREATE FUNCTION tbspace_combined_chk(v int) RETURNS bool LANGUAGE plpgsql AS $$ +BEGIN + IF v < 5000 THEN RETURN true; END IF; + INSERT INTO tbspace_combined SELECT -generate_series(1, 50); + RAISE EXCEPTION 'abort after writing'; +END $$; +ALTER TABLE tbspace_combined SET TABLESPACE regress_tblspace, + ADD CONSTRAINT tbspace_combined_k CHECK (tbspace_combined_chk(a)); +DROP TABLE tbspace_combined_child; +INSERT INTO tbspace_combined SELECT generate_series(101, 300); +SET enable_seqscan = off; +SET enable_bitmapscan = off; +SELECT count(*) FROM tbspace_combined WHERE a < 0; +RESET enable_seqscan; +RESET enable_bitmapscan; +DROP TABLE tbspace_combined; +DROP FUNCTION tbspace_combined_chk(int); + ALTER TABLESPACE regress_tblspace RENAME TO regress_tblspace_renamed; ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; diff --git a/src/test/regress/output/tablespace.source b/src/test/regress/output/tablespace.source index 1b60b99ff3e..5369b98c8de 100644 --- a/src/test/regress/output/tablespace.source +++ b/src/test/regress/output/tablespace.source @@ -921,6 +921,159 @@ ERROR: permission denied for tablespace regress_tblspace REINDEX (TABLESPACE regress_tblspace, CONCURRENTLY) TABLE tablespace_table; -- fail ERROR: permission denied for tablespace regress_tblspace RESET ROLE; +-- ALTER TABLE SET TABLESPACE moves the heap and leaves the indexes in their +-- tablespace. Run on its own, as here, it leaves their files alone too. +CREATE TABLE tbspace_rollback (a int); +INSERT INTO tbspace_rollback SELECT generate_series(1, 100); +CREATE INDEX tbspace_rollback_idx ON tbspace_rollback (a); +SELECT pg_relation_size('tbspace_rollback_idx') AS idx_size_before \gset +SELECT pg_relation_filepath('tbspace_rollback_idx') AS idx_path \gset +ALTER TABLE tbspace_rollback SET TABLESPACE regress_tblspace; +SELECT pg_relation_filepath('tbspace_rollback_idx') = :'idx_path' AS idx_file_kept; + idx_file_kept +--------------- + t +(1 row) + +-- In a transaction block the indexes get new files, so that a rollback +-- discards the entries added after the move together with the heap's file. +BEGIN; +ALTER TABLE tbspace_rollback SET TABLESPACE pg_default; +SELECT pg_relation_filepath('tbspace_rollback_idx') <> :'idx_path' AS idx_file_new; + idx_file_new +-------------- + t +(1 row) + +INSERT INTO tbspace_rollback SELECT generate_series(101, 2000); +SELECT pg_relation_size('tbspace_rollback_idx') > :idx_size_before AS idx_grew; + idx_grew +---------- + t +(1 row) + +ROLLBACK; +SELECT pg_relation_size('tbspace_rollback_idx') = :idx_size_before AS idx_size_restored; + idx_size_restored +------------------- + t +(1 row) + +-- the same for a TRUNCATE after the move, which is done in place +BEGIN; +ALTER TABLE tbspace_rollback SET TABLESPACE pg_default; +TRUNCATE tbspace_rollback; +ROLLBACK; +SELECT pg_relation_size('tbspace_rollback_idx') = :idx_size_before AS idx_size_restored; + idx_size_restored +------------------- + t +(1 row) + +SELECT count(*) FROM tbspace_rollback; + count +------- + 100 +(1 row) + +-- An index created in the same subtransaction is discarded with the heap +-- anyway and is not copied. Moving the indexes first and then the table +-- copies each index only once. +BEGIN; +CREATE INDEX tbspace_rollback_idx2 ON tbspace_rollback (a); +SELECT pg_relation_filepath('tbspace_rollback_idx2') AS idx2_path \gset +ALTER INDEX tbspace_rollback_idx SET TABLESPACE regress_tblspace; +SELECT pg_relation_filepath('tbspace_rollback_idx') AS idx_moved_path \gset +ALTER TABLE tbspace_rollback SET TABLESPACE pg_default; +SELECT pg_relation_filepath('tbspace_rollback_idx2') = :'idx2_path' AS idx2_not_copied, + pg_relation_filepath('tbspace_rollback_idx') = :'idx_moved_path' AS idx_not_copied_again; + idx2_not_copied | idx_not_copied_again +-----------------+---------------------- + t | t +(1 row) + +ROLLBACK; +DROP TABLE tbspace_rollback; +-- An index file that is new in this transaction but older than a savepoint +-- survives a rollback to it, so it must still follow a move made after the +-- savepoint: an index created or reindexed before it, or already copied by +-- an earlier move. Rows inserted afterwards reuse the TIDs freed by the +-- rollbacks; a stale index entry would match them. +CREATE TABLE tbspace_subxact (a int); +INSERT INTO tbspace_subxact SELECT generate_series(1, 100); +SET enable_seqscan = off; +SET enable_bitmapscan = off; +BEGIN; +CREATE INDEX tbspace_subxact_idx ON tbspace_subxact (a); +SAVEPOINT s; +ALTER TABLE tbspace_subxact SET TABLESPACE regress_tblspace; +INSERT INTO tbspace_subxact SELECT -generate_series(1, 50); +ROLLBACK TO s; +COMMIT; +BEGIN; +REINDEX INDEX tbspace_subxact_idx; +SAVEPOINT s; +ALTER TABLE tbspace_subxact SET TABLESPACE regress_tblspace; +INSERT INTO tbspace_subxact SELECT -generate_series(1, 50); +ROLLBACK TO s; +COMMIT; +BEGIN; +ALTER TABLE tbspace_subxact SET TABLESPACE regress_tblspace; +INSERT INTO tbspace_subxact VALUES (0); +SAVEPOINT s; +ALTER TABLE tbspace_subxact SET TABLESPACE pg_default; +INSERT INTO tbspace_subxact SELECT -generate_series(1, 50); +ROLLBACK TO s; +COMMIT; +INSERT INTO tbspace_subxact SELECT generate_series(101, 300); +SELECT count(*) FROM tbspace_subxact WHERE a < 0; + count +------- + 0 +(1 row) + +SELECT count(*) FROM tbspace_subxact; + count +------- + 301 +(1 row) + +RESET enable_seqscan; +RESET enable_bitmapscan; +DROP TABLE tbspace_subxact; +-- Combined with another subcommand, the ALTER can run user code after the +-- move, such as a CHECK constraint validated on an inheritance child after +-- the parent was moved, so the indexes get new files even outside a +-- transaction block. +CREATE TABLE tbspace_combined (a int); +INSERT INTO tbspace_combined SELECT generate_series(1, 100); +CREATE INDEX tbspace_combined_idx ON tbspace_combined (a); +CREATE TABLE tbspace_combined_child () INHERITS (tbspace_combined); +INSERT INTO tbspace_combined_child VALUES (5000); +CREATE FUNCTION tbspace_combined_chk(v int) RETURNS bool LANGUAGE plpgsql AS $$ +BEGIN + IF v < 5000 THEN RETURN true; END IF; + INSERT INTO tbspace_combined SELECT -generate_series(1, 50); + RAISE EXCEPTION 'abort after writing'; +END $$; +ALTER TABLE tbspace_combined SET TABLESPACE regress_tblspace, + ADD CONSTRAINT tbspace_combined_k CHECK (tbspace_combined_chk(a)); +ERROR: abort after writing +CONTEXT: PL/pgSQL function tbspace_combined_chk(integer) line 5 at RAISE +DROP TABLE tbspace_combined_child; +INSERT INTO tbspace_combined SELECT generate_series(101, 300); +SET enable_seqscan = off; +SET enable_bitmapscan = off; +SELECT count(*) FROM tbspace_combined WHERE a < 0; + count +------- + 0 +(1 row) + +RESET enable_seqscan; +RESET enable_bitmapscan; +DROP TABLE tbspace_combined; +DROP FUNCTION tbspace_combined_chk(int); ALTER TABLESPACE regress_tblspace RENAME TO regress_tblspace_renamed; ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; ALTER INDEX ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; -- 2.55.0