ALTER INDEX .. RENAME allows to rename tables/views as well

Started by Onder Kalaciover 4 years ago14 messages
#1Onder Kalaci
onderk@microsoft.com

Hi hackers,

I realized a subtle behavior with ALTER INDEX .. RENAME. It seems like a bug to me, please see the steps below.

Test 1: Rename table via RENAME .. INDEX

CREATE TABLE test_table (a int);
SELECT 'test_table'::regclass::oid;
oid
-------
34470
(1 row)
-- rename table using ALTER INDEX ..
ALTER INDEX test_table RENAME TO test_table_2;

-- see that table is rename
SELECT 34470::regclass;
regclass
--------------
test_table_2
(1 row)

Test 2: Rename view via RENAME .. INDEX
CREATE VIEW test_view AS SELECT * FROM pg_class;
SELECT 'test_view'::regclass::oid;
oid
-------
34473
(1 row)

ALTER INDEX test_view RENAME TO test_view_2;
ELECT 34473::regclass;
regclass
-------------
test_view_2
(1 row)

It seems like an oversight in ExecRenameStmt(), and probably applies to sequences, mat. views and foreign tables as well.

I can reproduce this on both 13.2 and 14.0. Though haven’t checked earlier versions.

Thanks,
Onder

#2Bruce Momjian
bruce@momjian.us
In reply to: Onder Kalaci (#1)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

I can confirm this bug in git head, and I think it should be fixed.

---------------------------------------------------------------------------

On Mon, Oct 4, 2021 at 10:23:23AM +0000, Onder Kalaci wrote:

Hi hackers,

I realized a subtle behavior with ALTER INDEX .. RENAME. It seems like a bug to
me, please see the steps below.

Test 1: Rename table via RENAME .. INDEX

CREATE TABLE test_table (a int);

SELECT 'test_table'::regclass::oid;

oid

-------

34470

(1 row)

-- rename table using ALTER INDEX ..

ALTER INDEX test_table RENAME TO test_table_2;

-- see that table is rename

SELECT 34470::regclass;

regclass

--------------

test_table_2

(1 row)

Test 2: Rename view via RENAME .. INDEX
CREATE VIEW test_view AS SELECT * FROM pg_class;

SELECT 'test_view'::regclass::oid;

oid

-------

34473

(1 row)

ALTER INDEX test_view RENAME TO test_view_2;

ELECT 34473::regclass;

regclass

-------------

test_view_2

(1 row)

It seems like an oversight in ExecRenameStmt(), and probably applies to
sequences, mat. views and foreign tables as well.

I can reproduce this on both 13.2 and 14.0. Though haven’t checked earlier
versions.

Thanks,

Onder

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

If only the physical world exists, free will is an illusion.

#3Bossart, Nathan
bossartn@amazon.com
In reply to: Bruce Momjian (#2)
1 attachment(s)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 10/6/21, 1:52 PM, "Bruce Momjian" <bruce@momjian.us> wrote:

I can confirm this bug in git head, and I think it should be fixed.

Here's a patch that ERRORs if the object type and statement type do
not match. Interestingly, some of the regression tests were relying
on this behavior. I considered teaching RenameRelation() how to
handle such mismatches, but we have to choose the lock level before we
know the object type, so that might be more trouble than it's worth.

I'm not too happy with the error message format, but I'm not sure we
can do much better without listing all the object types or doing some
more invasive refactoring.

Nathan

Attachments:

v1-0001-Add-object-type-validation-in-RenameRelation.patchapplication/octet-stream; name=v1-0001-Add-object-type-validation-in-RenameRelation.patchDownload
From c516a0c36e246a0c746be73cc75cda441a5f748d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <bossartn@amazon.com>
Date: Wed, 6 Oct 2021 22:11:42 +0000
Subject: [PATCH v1 1/1] Add object type validation in RenameRelation().

RenameRelation() is used for renaming several object types, but
there is presently no verification that the object type matches the
statement type.  For example, ALTER TABLE ... RENAME can be used to
rename indexes.  Since the renaming logic can differ between object
types, it is important to ensure that the object type and statement
type match.
---
 src/backend/commands/cluster.c            |  4 ++--
 src/backend/commands/tablecmds.c          | 16 ++++++++++++----
 src/backend/commands/typecmds.c           |  2 +-
 src/include/commands/tablecmds.h          |  2 +-
 src/test/regress/expected/alter_table.out |  6 +++++-
 src/test/regress/expected/sequence.out    |  4 ++--
 src/test/regress/sql/alter_table.sql      |  5 ++++-
 src/test/regress/sql/sequence.sql         |  4 ++--
 8 files changed, 29 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 9d22f648a8..7ee00691ab 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1518,14 +1518,14 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 			snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u",
 					 OIDOldHeap);
 			RenameRelationInternal(newrel->rd_rel->reltoastrelid,
-								   NewToastName, true, false);
+								   NewToastName, true, OBJECT_TABLE);
 
 			/* ... and its valid index too. */
 			snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u_index",
 					 OIDOldHeap);
 
 			RenameRelationInternal(toastidx,
-								   NewToastName, true, true);
+								   NewToastName, true, OBJECT_INDEX);
 
 			/*
 			 * Reset the relrewrite for the toast. The command-counter
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ff97b618e6..f8f5b6f85d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3660,7 +3660,7 @@ rename_constraint_internal(Oid myrelid,
 			|| con->contype == CONSTRAINT_UNIQUE
 			|| con->contype == CONSTRAINT_EXCLUSION))
 		/* rename the index; this renames the constraint as well */
-		RenameRelationInternal(con->conindid, newconname, false, true);
+		RenameRelationInternal(con->conindid, newconname, false, OBJECT_INDEX);
 	else
 		RenameConstraintById(constraintOid, newconname);
 
@@ -3762,7 +3762,7 @@ RenameRelation(RenameStmt *stmt)
 	}
 
 	/* Do the work */
-	RenameRelationInternal(relid, stmt->newname, false, is_index);
+	RenameRelationInternal(relid, stmt->newname, false, stmt->renameType);
 
 	ObjectAddressSet(address, RelationRelationId, relid);
 
@@ -3773,13 +3773,14 @@ RenameRelation(RenameStmt *stmt)
  *		RenameRelationInternal - change the name of a relation
  */
 void
-RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bool is_index)
+RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, ObjectType renameType)
 {
 	Relation	targetrelation;
 	Relation	relrelation;	/* for RELATION relation */
 	HeapTuple	reltup;
 	Form_pg_class relform;
 	Oid			namespaceId;
+	bool		is_index = renameType == OBJECT_INDEX;
 
 	/*
 	 * Grab a lock on the target relation, which we will NOT release until end
@@ -3804,6 +3805,13 @@ RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bo
 		elog(ERROR, "cache lookup failed for relation %u", myrelid);
 	relform = (Form_pg_class) GETSTRUCT(reltup);
 
+	if (get_relkind_objtype(relform->relkind) != renameType)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("cannot rename \"%s\"",
+						NameStr(relform->relname)),
+				 errdetail_relkind_not_supported(relform->relkind)));
+
 	if (get_relname_relid(newrelname, namespaceId) != InvalidOid)
 		ereport(ERROR,
 				(errcode(ERRCODE_DUPLICATE_TABLE),
@@ -8620,7 +8628,7 @@ ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel,
 		ereport(NOTICE,
 				(errmsg("ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index \"%s\" to \"%s\"",
 						indexName, constraintName)));
-		RenameRelationInternal(index_oid, constraintName, false, true);
+		RenameRelationInternal(index_oid, constraintName, false, OBJECT_INDEX);
 	}
 
 	/* Extra checks needed if making primary key */
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index b290629a45..4a971a103d 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3659,7 +3659,7 @@ RenameType(RenameStmt *stmt)
 	 * RenameRelationInternal will call RenameTypeInternal automatically.
 	 */
 	if (typTup->typtype == TYPTYPE_COMPOSITE)
-		RenameRelationInternal(typTup->typrelid, newTypeName, false, false);
+		RenameRelationInternal(typTup->typrelid, newTypeName, false, stmt->renameType);
 	else
 		RenameTypeInternal(typeOid, newTypeName,
 						   typTup->typnamespace);
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 336549cc5f..48d3a782af 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -76,7 +76,7 @@ extern ObjectAddress RenameRelation(RenameStmt *stmt);
 
 extern void RenameRelationInternal(Oid myrelid,
 								   const char *newrelname, bool is_internal,
-								   bool is_index);
+								   ObjectType renameType);
 
 extern void ResetRelRewrite(Oid myrelid);
 
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 4bee0c1173..fb1909de3c 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -232,9 +232,13 @@ SET ROLE regress_alter_table_user1;
 ALTER INDEX onek_unique1 RENAME TO fail;  -- permission denied
 ERROR:  must be owner of index onek_unique1
 RESET ROLE;
+-- ALTER TABLE ... RENAME on index fails
+ALTER TABLE onek_unique1 RENAME TO fail;
+ERROR:  cannot rename "onek_unique1"
+DETAIL:  This operation is not supported for indexes.
 -- renaming views
 CREATE VIEW attmp_view (unique1) AS SELECT unique1 FROM tenk1;
-ALTER TABLE attmp_view RENAME TO attmp_view_new;
+ALTER VIEW attmp_view RENAME TO attmp_view_new;
 SET ROLE regress_alter_table_user1;
 ALTER VIEW attmp_view_new RENAME TO fail;  -- permission denied
 ERROR:  must be owner of view attmp_view_new
diff --git a/src/test/regress/expected/sequence.out b/src/test/regress/expected/sequence.out
index 71c2b0f1df..d0a5dd8d76 100644
--- a/src/test/regress/expected/sequence.out
+++ b/src/test/regress/expected/sequence.out
@@ -241,7 +241,7 @@ ERROR:  currval of sequence "sequence_test" is not yet defined in this session
 DROP SEQUENCE sequence_test;
 -- renaming sequences
 CREATE SEQUENCE foo_seq;
-ALTER TABLE foo_seq RENAME TO foo_seq_new;
+ALTER SEQUENCE foo_seq RENAME TO foo_seq_new;
 SELECT * FROM foo_seq_new;
  last_value | log_cnt | is_called 
 ------------+---------+-----------
@@ -270,7 +270,7 @@ SELECT last_value, log_cnt IN (31, 32) AS log_cnt_ok, is_called FROM foo_seq_new
 
 DROP SEQUENCE foo_seq_new;
 -- renaming serial sequences
-ALTER TABLE serialtest1_f2_seq RENAME TO serialtest1_f2_foo;
+ALTER SEQUENCE serialtest1_f2_seq RENAME TO serialtest1_f2_foo;
 INSERT INTO serialTest1 VALUES ('more');
 SELECT * FROM serialTest1;
   f1   | f2  
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index dc0200adcb..8106c8617b 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -227,9 +227,12 @@ SET ROLE regress_alter_table_user1;
 ALTER INDEX onek_unique1 RENAME TO fail;  -- permission denied
 RESET ROLE;
 
+-- ALTER TABLE ... RENAME on index fails
+ALTER TABLE onek_unique1 RENAME TO fail;
+
 -- renaming views
 CREATE VIEW attmp_view (unique1) AS SELECT unique1 FROM tenk1;
-ALTER TABLE attmp_view RENAME TO attmp_view_new;
+ALTER VIEW attmp_view RENAME TO attmp_view_new;
 
 SET ROLE regress_alter_table_user1;
 ALTER VIEW attmp_view_new RENAME TO fail;  -- permission denied
diff --git a/src/test/regress/sql/sequence.sql b/src/test/regress/sql/sequence.sql
index 7928ee23ee..3d3a90b544 100644
--- a/src/test/regress/sql/sequence.sql
+++ b/src/test/regress/sql/sequence.sql
@@ -132,7 +132,7 @@ DROP SEQUENCE sequence_test;
 
 -- renaming sequences
 CREATE SEQUENCE foo_seq;
-ALTER TABLE foo_seq RENAME TO foo_seq_new;
+ALTER SEQUENCE foo_seq RENAME TO foo_seq_new;
 SELECT * FROM foo_seq_new;
 SELECT nextval('foo_seq_new');
 SELECT nextval('foo_seq_new');
@@ -142,7 +142,7 @@ SELECT last_value, log_cnt IN (31, 32) AS log_cnt_ok, is_called FROM foo_seq_new
 DROP SEQUENCE foo_seq_new;
 
 -- renaming serial sequences
-ALTER TABLE serialtest1_f2_seq RENAME TO serialtest1_f2_foo;
+ALTER SEQUENCE serialtest1_f2_seq RENAME TO serialtest1_f2_foo;
 INSERT INTO serialTest1 VALUES ('more');
 SELECT * FROM serialTest1;
 
-- 
2.16.6

#4Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bossart, Nathan (#3)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

"Bossart, Nathan" <bossartn@amazon.com> writes:

On 10/6/21, 1:52 PM, "Bruce Momjian" <bruce@momjian.us> wrote:

I can confirm this bug in git head, and I think it should be fixed.

Here's a patch that ERRORs if the object type and statement type do
not match. Interestingly, some of the regression tests were relying
on this behavior.

... as, no doubt, are a lot of applications that this will gratuitously
break. We've long had a policy that ALTER TABLE will work on relations
that aren't tables, so long as the requested operation is sensible.

The situation for "ALTER some-other-relation-kind" is a bit more
confused, because some cases throw errors and some don't; but I really
doubt that tightening things up here will earn you anything but
brickbats. I *definitely* don't agree with discarding the policy
about ALTER TABLE, especially if it's only done for RENAME.

In short: no, I do not agree that this is a bug to be fixed. Perhaps
we should have done things differently years ago, but it's too late to
redefine it.

regards, tom lane

#5Bossart, Nathan
bossartn@amazon.com
In reply to: Tom Lane (#4)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 10/6/21, 3:44 PM, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:

"Bossart, Nathan" <bossartn@amazon.com> writes:

Here's a patch that ERRORs if the object type and statement type do
not match. Interestingly, some of the regression tests were relying
on this behavior.

... as, no doubt, are a lot of applications that this will gratuitously
break. We've long had a policy that ALTER TABLE will work on relations
that aren't tables, so long as the requested operation is sensible.

Right.

The situation for "ALTER some-other-relation-kind" is a bit more
confused, because some cases throw errors and some don't; but I really
doubt that tightening things up here will earn you anything but
brickbats. I *definitely* don't agree with discarding the policy
about ALTER TABLE, especially if it's only done for RENAME.

I think we should at least consider adding this check for ALTER INDEX
since we choose a different lock level in that case.

Nathan

#6Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Bossart, Nathan (#5)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 2021-Oct-06, Bossart, Nathan wrote:

On 10/6/21, 3:44 PM, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:

The situation for "ALTER some-other-relation-kind" is a bit more
confused, because some cases throw errors and some don't; but I really
doubt that tightening things up here will earn you anything but
brickbats. I *definitely* don't agree with discarding the policy
about ALTER TABLE, especially if it's only done for RENAME.

I think we should at least consider adding this check for ALTER INDEX
since we choose a different lock level in that case.

I agree -- letting ALTER INDEX process relations that aren't indexes is
dangerous, with its current coding that uses a reduced lock level. But
maybe erroring out is not necessary; can we instead loop, locking the
object with ShareUpdateExclusive first, assuming it *is* an index, and
if it isn't then we release and restart using the stronger lock this
time?

--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
Syntax error: function hell() needs an argument.
Please choose what hell you want to involve.

#7Bossart, Nathan
bossartn@amazon.com
In reply to: Alvaro Herrera (#6)
1 attachment(s)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 10/6/21, 4:45 PM, "Alvaro Herrera" <alvherre@alvh.no-ip.org> wrote:

On 2021-Oct-06, Bossart, Nathan wrote:

I think we should at least consider adding this check for ALTER INDEX
since we choose a different lock level in that case.

I agree -- letting ALTER INDEX process relations that aren't indexes is
dangerous, with its current coding that uses a reduced lock level. But
maybe erroring out is not necessary; can we instead loop, locking the
object with ShareUpdateExclusive first, assuming it *is* an index, and
if it isn't then we release and restart using the stronger lock this
time?

Good idea. Patch attached.

Nathan

Attachments:

v2-0001-Ensure-correct-lock-level-is-used-for-rename-stat.patchapplication/octet-stream; name=v2-0001-Ensure-correct-lock-level-is-used-for-rename-stat.patchDownload
From c7db185e7c75f197a55d7e73b898b3b0858796cc Mon Sep 17 00:00:00 2001
From: Nathan Bossart <bossartn@amazon.com>
Date: Thu, 7 Oct 2021 00:36:06 +0000
Subject: [PATCH v2 1/1] Ensure correct lock level is used for rename
 statements.

We allow mismatching statement and object types for some rename
statements, but that can cause the wrong lock level to be used.
When that happens, retry the relation lookup with the correct lock
level.
---
 src/backend/commands/tablecmds.c          | 45 ++++++++++++++++++++++---------
 src/test/regress/expected/alter_table.out |  6 +++++
 src/test/regress/sql/alter_table.sql      |  7 +++++
 3 files changed, 45 insertions(+), 13 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ff97b618e6..d42ed13344 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3735,7 +3735,7 @@ RenameConstraint(RenameStmt *stmt)
 ObjectAddress
 RenameRelation(RenameStmt *stmt)
 {
-	bool		is_index = stmt->renameType == OBJECT_INDEX;
+	bool		is_index_stmt = stmt->renameType == OBJECT_INDEX;
 	Oid			relid;
 	ObjectAddress address;
 
@@ -3747,22 +3747,41 @@ RenameRelation(RenameStmt *stmt)
 	 * Lock level used here should match RenameRelationInternal, to avoid lock
 	 * escalation.
 	 */
-	relid = RangeVarGetRelidExtended(stmt->relation,
-									 is_index ? ShareUpdateExclusiveLock : AccessExclusiveLock,
-									 stmt->missing_ok ? RVR_MISSING_OK : 0,
-									 RangeVarCallbackForAlterRelation,
-									 (void *) stmt);
-
-	if (!OidIsValid(relid))
+	for (;;)
 	{
-		ereport(NOTICE,
-				(errmsg("relation \"%s\" does not exist, skipping",
-						stmt->relation->relname)));
-		return InvalidObjectAddress;
+		LOCKMODE	lockmode;
+		bool		obj_is_index;
+
+		lockmode = is_index_stmt ? ShareUpdateExclusiveLock : AccessExclusiveLock;
+
+		relid = RangeVarGetRelidExtended(stmt->relation, lockmode,
+										 stmt->missing_ok ? RVR_MISSING_OK : 0,
+										 RangeVarCallbackForAlterRelation,
+										 (void *) stmt);
+
+		if (!OidIsValid(relid))
+		{
+			ereport(NOTICE,
+					(errmsg("relation \"%s\" does not exist, skipping",
+							stmt->relation->relname)));
+			return InvalidObjectAddress;
+		}
+
+		/*
+		 * We allow mismatched statement and object types (e.g., ALTER INDEX to
+		 * rename a table), but we might've used the wrong lock level.  If that
+		 * happens, retry with the correct lock level.
+		 */
+		obj_is_index = (get_rel_relkind(relid) == RELKIND_INDEX);
+		if (is_index_stmt == obj_is_index)
+			break;
+
+		UnlockRelationOid(relid, lockmode);
+		is_index_stmt = obj_is_index;
 	}
 
 	/* Do the work */
-	RenameRelationInternal(relid, stmt->newname, false, is_index);
+	RenameRelationInternal(relid, stmt->newname, false, is_index_stmt);
 
 	ObjectAddressSet(address, RelationRelationId, relid);
 
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 4bee0c1173..b39430f201 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -232,6 +232,12 @@ SET ROLE regress_alter_table_user1;
 ALTER INDEX onek_unique1 RENAME TO fail;  -- permission denied
 ERROR:  must be owner of index onek_unique1
 RESET ROLE;
+-- rename statements with mismatching statement and object types
+CREATE TABLE alter_idx_rename_test (a INT);
+CREATE INDEX alter_idx_rename_test_idx ON alter_idx_rename_test (a);
+ALTER INDEX alter_idx_rename_test RENAME TO alter_idx_rename_test_2;
+ALTER TABLE alter_idx_rename_test_idx RENAME TO alter_idx_rename_test_idx_2;
+DROP TABLE alter_idx_rename_test_2;
 -- renaming views
 CREATE VIEW attmp_view (unique1) AS SELECT unique1 FROM tenk1;
 ALTER TABLE attmp_view RENAME TO attmp_view_new;
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index dc0200adcb..a56926f915 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -227,6 +227,13 @@ SET ROLE regress_alter_table_user1;
 ALTER INDEX onek_unique1 RENAME TO fail;  -- permission denied
 RESET ROLE;
 
+-- rename statements with mismatching statement and object types
+CREATE TABLE alter_idx_rename_test (a INT);
+CREATE INDEX alter_idx_rename_test_idx ON alter_idx_rename_test (a);
+ALTER INDEX alter_idx_rename_test RENAME TO alter_idx_rename_test_2;
+ALTER TABLE alter_idx_rename_test_idx RENAME TO alter_idx_rename_test_idx_2;
+DROP TABLE alter_idx_rename_test_2;
+
 -- renaming views
 CREATE VIEW attmp_view (unique1) AS SELECT unique1 FROM tenk1;
 ALTER TABLE attmp_view RENAME TO attmp_view_new;
-- 
2.16.6

#8Michael Paquier
michael@paquier.xyz
In reply to: Tom Lane (#4)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On Wed, Oct 06, 2021 at 06:43:25PM -0400, Tom Lane wrote:

... as, no doubt, are a lot of applications that this will gratuitously
break. We've long had a policy that ALTER TABLE will work on relations
that aren't tables, so long as the requested operation is sensible.

Yeah, that was my first thought after seeing this thread. There is a
risk in breaking something that was working previously. Perhaps it
was just working by accident, but that could be surprising if an
application relied on the existing behavior.
--
Michael

#9Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Bossart, Nathan (#7)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 2021-Oct-07, Bossart, Nathan wrote:

Good idea. Patch attached.

Yeah, that sounds exactly what I was thinking.

Now, what is the worst that can happen if we rename a table under SUE
and somebody else is using the table concurrently? Is there any way to
cause a backend crash or something like that? As far as I can see,
because we grab a fresh catalog snapshot for each query, you can't cause
anything worse than reading from a different table. I do lack
imagination for creating attacks, though.

So my inclination would be to apply this to master only.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"The Gord often wonders why people threaten never to come back after they've
been told never to return" (www.actsofgord.com)

#10Bossart, Nathan
bossartn@amazon.com
In reply to: Alvaro Herrera (#9)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 10/18/21, 4:56 PM, "Alvaro Herrera" <alvherre@alvh.no-ip.org> wrote:

Now, what is the worst that can happen if we rename a table under SUE
and somebody else is using the table concurrently? Is there any way to
cause a backend crash or something like that? As far as I can see,
because we grab a fresh catalog snapshot for each query, you can't cause
anything worse than reading from a different table. I do lack
imagination for creating attacks, though.

This message [0]/messages/by-id/CA+TgmobtmFT5g-0dA=vEFFtogjRAuDHcYPw+qEdou5dZPnF=pg@mail.gmail.com in the thread for lowering the lock level for
renaming indexes seems to indicate that there may be some risk of
crashing.

Nathan

[0]: /messages/by-id/CA+TgmobtmFT5g-0dA=vEFFtogjRAuDHcYPw+qEdou5dZPnF=pg@mail.gmail.com

#11Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Bossart, Nathan (#7)
1 attachment(s)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

I was about to push this when it occurred to me that it seems a bit
pointless to release AEL in order to retry with the lighter lock; once
we have AEL, let's just keep it and proceed. So how about the attached?

I'm now thinking that this is to back-patch all the way to 12.

--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/

Attachments:

v3-0001-Ensure-correct-lock-level-is-used-in-ALTER-.-RENA.patchtext/x-diff; charset=utf-8Download
From 04334bc719efe69f63c8a590b9c682a22c6fb413 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 19 Oct 2021 17:02:17 -0300
Subject: [PATCH v3] Ensure correct lock level is used in ALTER ... RENAME

Commit 1b5d797cd4f7 intended to relax the lock level used to rename
indexes, but inadvertently allowed *any* relation to be renamed with a
lowered lock level, as long as the command is spelled ALTER INDEX.
That's undesirable for other relation types, so retry the operation with
the higher lock if the relation turns out not to be an index.

After this fix, ALTER INDEX <sometable> RENAME will require access
exclusive lock.

Author: Nathan Bossart <bossartn@amazon.com>
Reported-by: Onder Kalaci <onderk@microsoft.com>
Discussion: https://postgr.es/m/PH0PR21MB1328189E2821CDEC646F8178D8AE9@PH0PR21MB1328.namprd21.prod.outlook.com
---
 src/backend/commands/tablecmds.c          | 60 +++++++++++++++++------
 src/test/regress/expected/alter_table.out | 39 +++++++++++++++
 src/test/regress/sql/alter_table.sql      | 25 ++++++++++
 3 files changed, 110 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 487852a14e..f02399f699 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3735,7 +3735,7 @@ RenameConstraint(RenameStmt *stmt)
 ObjectAddress
 RenameRelation(RenameStmt *stmt)
 {
-	bool		is_index = stmt->renameType == OBJECT_INDEX;
+	bool		is_index_stmt = stmt->renameType == OBJECT_INDEX;
 	Oid			relid;
 	ObjectAddress address;
 
@@ -3745,24 +3745,45 @@ RenameRelation(RenameStmt *stmt)
 	 * end of transaction.
 	 *
 	 * Lock level used here should match RenameRelationInternal, to avoid lock
-	 * escalation.
+	 * escalation.  However, because ALTER INDEX can be used with any relation
+	 * type, we mustn't believe without verification.
 	 */
-	relid = RangeVarGetRelidExtended(stmt->relation,
-									 is_index ? ShareUpdateExclusiveLock : AccessExclusiveLock,
-									 stmt->missing_ok ? RVR_MISSING_OK : 0,
-									 RangeVarCallbackForAlterRelation,
-									 (void *) stmt);
-
-	if (!OidIsValid(relid))
+	for (;;)
 	{
-		ereport(NOTICE,
-				(errmsg("relation \"%s\" does not exist, skipping",
-						stmt->relation->relname)));
-		return InvalidObjectAddress;
+		LOCKMODE	lockmode;
+		bool		obj_is_index;
+
+		lockmode = is_index_stmt ? ShareUpdateExclusiveLock : AccessExclusiveLock;
+
+		relid = RangeVarGetRelidExtended(stmt->relation, lockmode,
+										 stmt->missing_ok ? RVR_MISSING_OK : 0,
+										 RangeVarCallbackForAlterRelation,
+										 (void *) stmt);
+
+		if (!OidIsValid(relid))
+		{
+			ereport(NOTICE,
+					(errmsg("relation \"%s\" does not exist, skipping",
+							stmt->relation->relname)));
+			return InvalidObjectAddress;
+		}
+
+		/*
+		 * We allow mismatched statement and object types (e.g., ALTER INDEX
+		 * to rename a table), but we might've used the wrong lock level.  If
+		 * that happens, retry with the correct lock level.  We don't bother
+		 * if we already acquired AccessExclusiveLock with an index, however.
+		 */
+		obj_is_index = (get_rel_relkind(relid) == RELKIND_INDEX);
+		if (obj_is_index || is_index_stmt == obj_is_index)
+			break;
+
+		UnlockRelationOid(relid, lockmode);
+		is_index_stmt = obj_is_index;
 	}
 
 	/* Do the work */
-	RenameRelationInternal(relid, stmt->newname, false, is_index);
+	RenameRelationInternal(relid, stmt->newname, false, is_index_stmt);
 
 	ObjectAddressSet(address, RelationRelationId, relid);
 
@@ -3810,6 +3831,17 @@ RenameRelationInternal(Oid myrelid, const char *newrelname, bool is_internal, bo
 				 errmsg("relation \"%s\" already exists",
 						newrelname)));
 
+	/*
+	 * RenameRelation is careful not to believe the caller's idea of the
+	 * relation kind being handled.  We don't have to worry about this (at
+	 * least not until ALTER TABLE .. ADD CONSTRAINT ... USING INDEX is
+	 * implemented for partitioned tables), but let's not be totally oblivious
+	 * to it.  We can process an index as not-an-index, but not the other way
+	 * around.
+	 */
+	Assert(!is_index ||
+		   is_index == (targetrelation->rd_rel->relkind == RELKIND_INDEX));
+
 	/*
 	 * Update pg_class tuple with new relname.  (Scribbling on reltup is OK
 	 * because it's a copy...)
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index fa54bef89a..a3a18b23bf 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -232,6 +232,45 @@ SET ROLE regress_alter_table_user1;
 ALTER INDEX onek_unique1 RENAME TO fail;  -- permission denied
 ERROR:  must be owner of index onek_unique1
 RESET ROLE;
+-- rename statements with mismatching statement and object types
+CREATE TABLE alter_idx_rename_test (a INT);
+CREATE INDEX alter_idx_rename_test_idx ON alter_idx_rename_test (a);
+BEGIN;
+ALTER INDEX alter_idx_rename_test RENAME TO alter_idx_rename_test_2;
+SELECT relation::regclass, mode FROM pg_locks
+WHERE pid = pg_backend_pid() AND locktype = 'relation'
+  AND relation::regclass::text LIKE 'alter\_idx%';
+        relation         |        mode         
+-------------------------+---------------------
+ alter_idx_rename_test_2 | AccessExclusiveLock
+(1 row)
+
+COMMIT;
+BEGIN;
+ALTER INDEX alter_idx_rename_test_idx RENAME TO alter_idx_rename_test_idx_2;
+SELECT relation::regclass, mode FROM pg_locks
+WHERE pid = pg_backend_pid() AND locktype = 'relation'
+  AND relation::regclass::text LIKE 'alter\_idx%'
+ORDER BY relation::regclass::text;
+          relation           |           mode           
+-----------------------------+--------------------------
+ alter_idx_rename_test_idx_2 | ShareUpdateExclusiveLock
+(1 row)
+
+COMMIT;
+BEGIN;
+ALTER TABLE alter_idx_rename_test_idx_2 RENAME TO alter_idx_rename_test_idx_3;
+SELECT relation::regclass, mode FROM pg_locks
+WHERE pid = pg_backend_pid() AND locktype = 'relation'
+  AND relation::regclass::text LIKE 'alter\_idx%'
+ORDER BY relation::regclass::text;
+          relation           |        mode         
+-----------------------------+---------------------
+ alter_idx_rename_test_idx_3 | AccessExclusiveLock
+(1 row)
+
+COMMIT;
+DROP TABLE alter_idx_rename_test_2;
 -- renaming views
 CREATE VIEW attmp_view (unique1) AS SELECT unique1 FROM tenk1;
 ALTER TABLE attmp_view RENAME TO attmp_view_new;
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 0c61604456..d9b9e4b02b 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -227,6 +227,31 @@ SET ROLE regress_alter_table_user1;
 ALTER INDEX onek_unique1 RENAME TO fail;  -- permission denied
 RESET ROLE;
 
+-- rename statements with mismatching statement and object types
+CREATE TABLE alter_idx_rename_test (a INT);
+CREATE INDEX alter_idx_rename_test_idx ON alter_idx_rename_test (a);
+BEGIN;
+ALTER INDEX alter_idx_rename_test RENAME TO alter_idx_rename_test_2;
+SELECT relation::regclass, mode FROM pg_locks
+WHERE pid = pg_backend_pid() AND locktype = 'relation'
+  AND relation::regclass::text LIKE 'alter\_idx%';
+COMMIT;
+BEGIN;
+ALTER INDEX alter_idx_rename_test_idx RENAME TO alter_idx_rename_test_idx_2;
+SELECT relation::regclass, mode FROM pg_locks
+WHERE pid = pg_backend_pid() AND locktype = 'relation'
+  AND relation::regclass::text LIKE 'alter\_idx%'
+ORDER BY relation::regclass::text;
+COMMIT;
+BEGIN;
+ALTER TABLE alter_idx_rename_test_idx_2 RENAME TO alter_idx_rename_test_idx_3;
+SELECT relation::regclass, mode FROM pg_locks
+WHERE pid = pg_backend_pid() AND locktype = 'relation'
+  AND relation::regclass::text LIKE 'alter\_idx%'
+ORDER BY relation::regclass::text;
+COMMIT;
+DROP TABLE alter_idx_rename_test_2;
+
 -- renaming views
 CREATE VIEW attmp_view (unique1) AS SELECT unique1 FROM tenk1;
 ALTER TABLE attmp_view RENAME TO attmp_view_new;
-- 
2.30.2

#12Bossart, Nathan
bossartn@amazon.com
In reply to: Alvaro Herrera (#11)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 10/19/21, 1:36 PM, "Alvaro Herrera" <alvherre@alvh.no-ip.org> wrote:

I was about to push this when it occurred to me that it seems a bit
pointless to release AEL in order to retry with the lighter lock; once
we have AEL, let's just keep it and proceed. So how about the attached?

I did consider this, but I figured it might be better to keep the lock
level consistent for a given object type no matter what the statement
type is. I don't have a strong opinion about this, though.

I'm now thinking that this is to back-patch all the way to 12.

+1. The patch LGTM. I like the test additions to check the lock
level.

Nathan

#13Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Bossart, Nathan (#12)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 2021-Oct-19, Bossart, Nathan wrote:

I did consider this, but I figured it might be better to keep the lock
level consistent for a given object type no matter what the statement
type is. I don't have a strong opinion about this, though.

Yeah, the problem is that if there is a concurrent process waiting on
your lock, we'll release ours and they'll grab theirs, so we'll be
waiting on them afterwards, which is worse.

BTW I noticed that the case of partitioned indexes was wrong too. I
fixed that, added it to the tests, and pushed.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"People get annoyed when you try to debug them." (Larry Wall)

#14Bossart, Nathan
bossartn@amazon.com
In reply to: Alvaro Herrera (#13)
Re: ALTER INDEX .. RENAME allows to rename tables/views as well

On 10/19/21, 3:13 PM, "Alvaro Herrera" <alvherre@alvh.no-ip.org> wrote:

On 2021-Oct-19, Bossart, Nathan wrote:

I did consider this, but I figured it might be better to keep the lock
level consistent for a given object type no matter what the statement
type is. I don't have a strong opinion about this, though.

Yeah, the problem is that if there is a concurrent process waiting on
your lock, we'll release ours and they'll grab theirs, so we'll be
waiting on them afterwards, which is worse.

Makes sense.

BTW I noticed that the case of partitioned indexes was wrong too. I
fixed that, added it to the tests, and pushed.

Ah, good catch. Thanks!

Nathan