diff --git a/doc/src/sgml/ref/prepare_transaction.sgml b/doc/src/sgml/ref/prepare_transaction.sgml index d958f7a06f..5016ca287e 100644 --- a/doc/src/sgml/ref/prepare_transaction.sgml +++ b/doc/src/sgml/ref/prepare_transaction.sgml @@ -98,9 +98,9 @@ PREPARE TRANSACTION transaction_id It is not currently allowed to PREPARE a transaction that - has executed any operations involving temporary tables, - created any cursors WITH HOLD, or executed - LISTEN, UNLISTEN, or + has executed any operations involving temporary tables or the session's + temporary namespace, created any cursors WITH HOLD, or + executed LISTEN, UNLISTEN, or NOTIFY. Those features are too tightly tied to the current session to be useful in a transaction to be prepared. diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index b94c764c2c..2c517756c0 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -2295,6 +2295,18 @@ PrepareTransaction(void) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot PREPARE a transaction that has operated on temporary tables"))); + /* + * Similarly, PREPARE TRANSACTION is not allowed if the temporary + * namespace has been involved in this transaction as we cannot allow it + * to create, lock, or even drop objects within the temporary namespace + * as this can mess up with this session or even a follow-up session + * trying to use the same temporary namespace. + */ + if ((MyXactFlags & XACT_FLAGS_ACCESSEDTEMPNAMESPACE)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot PREPARE a transaction that has operated on temporary namespace"))); + /* * Likewise, don't allow PREPARE after pg_export_snapshot. This could be * supported if we added cleanup logic to twophase.c, but for now it diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 5d13e6a3d7..19928e9f15 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -192,6 +192,7 @@ char *namespace_search_path = NULL; /* Local functions */ static void recomputeNamespacePath(void); +static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); static void RemoveTempRelations(Oid tempNamespaceId); static void RemoveTempRelationsCallback(int code, Datum arg); @@ -459,9 +460,8 @@ RangeVarGetCreationNamespace(const RangeVar *newRelation) /* check for pg_temp alias */ if (strcmp(newRelation->schemaname, "pg_temp") == 0) { - /* Initialize temp namespace if first time through */ - if (!OidIsValid(myTempNamespace)) - InitTempTableNamespace(); + /* Initialize temp namespace */ + AccessTempTableNamespace(false); return myTempNamespace; } /* use exact schema given */ @@ -470,9 +470,8 @@ RangeVarGetCreationNamespace(const RangeVar *newRelation) } else if (newRelation->relpersistence == RELPERSISTENCE_TEMP) { - /* Initialize temp namespace if first time through */ - if (!OidIsValid(myTempNamespace)) - InitTempTableNamespace(); + /* Initialize temp namespace */ + AccessTempTableNamespace(false); return myTempNamespace; } else @@ -482,7 +481,7 @@ RangeVarGetCreationNamespace(const RangeVar *newRelation) if (activeTempCreationPending) { /* Need to initialize temp namespace */ - InitTempTableNamespace(); + AccessTempTableNamespace(true); return myTempNamespace; } namespaceId = activeCreationNamespace; @@ -2920,9 +2919,8 @@ LookupCreationNamespace(const char *nspname) /* check for pg_temp alias */ if (strcmp(nspname, "pg_temp") == 0) { - /* Initialize temp namespace if first time through */ - if (!OidIsValid(myTempNamespace)) - InitTempTableNamespace(); + /* Initialize temp namespace */ + AccessTempTableNamespace(false); return myTempNamespace; } @@ -2985,9 +2983,8 @@ QualifiedNameGetCreationNamespace(List *names, char **objname_p) /* check for pg_temp alias */ if (strcmp(schemaname, "pg_temp") == 0) { - /* Initialize temp namespace if first time through */ - if (!OidIsValid(myTempNamespace)) - InitTempTableNamespace(); + /* Initialize temp namespace */ + AccessTempTableNamespace(false); return myTempNamespace; } /* use exact schema given */ @@ -3001,7 +2998,7 @@ QualifiedNameGetCreationNamespace(List *names, char **objname_p) if (activeTempCreationPending) { /* Need to initialize temp namespace */ - InitTempTableNamespace(); + AccessTempTableNamespace(true); return myTempNamespace; } namespaceId = activeCreationNamespace; @@ -3830,6 +3827,38 @@ recomputeNamespacePath(void) list_free(oidlist); } +/* + * AccessTempTableNamespace + * Provide access to a temporary namespace, potentially creating it + * if not present yet. This routine registers if the namespace gets + * in use in this transaction. 'force' can be set to true to allow + * the caller to enforce the creation of the temporary namespace for + * use in this backend, which happens if its creation is pending. + */ +static void +AccessTempTableNamespace(bool force) +{ + /* + * Make note that this temporary namespace has been accessed in this + * transaction. + */ + MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE; + + /* + * If the caller attempting to access a temporary schema expects the + * creation of the namespace to be pending and should be enforced, then go + * through the creation. + */ + if (!force && OidIsValid(myTempNamespace)) + return; + + /* + * The temporary tablespace does not exist yet and is wanted, so + * initialize it. + */ + InitTempTableNamespace(); +} + /* * InitTempTableNamespace * Initialize temp table namespace on first use in a particular backend @@ -4273,7 +4302,7 @@ fetch_search_path(bool includeImplicit) */ if (activeTempCreationPending) { - InitTempTableNamespace(); + AccessTempTableNamespace(true); recomputeNamespacePath(); } diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 4b38ef68d9..f6d665239b 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/xact.h" #include "access/heapam.h" #include "access/htup_details.h" #include "catalog/dependency.h" @@ -107,6 +108,13 @@ RemoveObjects(DropStmt *stmt) check_object_ownership(GetUserId(), stmt->removeType, address, object, relation); + /* + * Make note if a temporary namespace has been accessed in this + * transaction. + */ + if (OidIsValid(namespaceId) && isTempNamespace(namespaceId)) + MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE; + /* Release any relcache reference count, but keep lock until commit. */ if (relation) heap_close(relation, NoLock); diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 2e4538146d..5d9825f082 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -1474,6 +1474,13 @@ CreateExtensionInternal(char *extensionName, list_free(search_path); } + /* + * Make note if a temporary namespace has been accessed in this + * transaction. + */ + if (isTempNamespace(schemaOid)) + MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPNAMESPACE; + /* * We don't check creation rights on the target namespace here. If the * extension script actually creates any objects there, it will fail if diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c index 71278b38cf..f672bfbdd7 100644 --- a/src/backend/commands/lockcmds.c +++ b/src/backend/commands/lockcmds.c @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/xact.h" #include "catalog/namespace.h" #include "catalog/pg_inherits.h" #include "commands/lockcmds.h" @@ -83,6 +84,7 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid, { LOCKMODE lockmode = *(LOCKMODE *) arg; char relkind; + char relpersistence; AclResult aclresult; if (!OidIsValid(relid)) @@ -100,6 +102,14 @@ RangeVarCallbackForLockTable(const RangeVar *rv, Oid relid, Oid oldrelid, errmsg("\"%s\" is not a table or a view", rv->relname))); + /* + * Make note if a temporary relation has been accessed in this + * transaction. + */ + relpersistence = get_rel_persistence(relid); + if (relpersistence == RELPERSISTENCE_TEMP) + MyXactFlags |= XACT_FLAGS_ACCESSEDTEMPREL; + /* Check permissions. */ aclresult = LockTableAclCheck(relid, lockmode, GetUserId()); if (aclresult != ACLCHECK_OK) diff --git a/src/include/access/xact.h b/src/include/access/xact.h index 689c57c592..8eee897337 100644 --- a/src/include/access/xact.h +++ b/src/include/access/xact.h @@ -98,6 +98,11 @@ extern int MyXactFlags; */ #define XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK (1U << 1) +/* + * XACT_FLAGS_ACCESSEDTEMPNAMESPACE - set when a temporary namespace is + * accessed. We don't allow PREPARE TRANSACTION in that case. + */ +#define XACT_FLAGS_ACCESSEDTEMPNAMESPACE (1U << 2) /* * start- and end-of-transaction callbacks for dynamically loaded modules diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 28d86c4b87..1eec5a37d3 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -121,3 +121,36 @@ Objects in extension "test_ext8" -- dropping it should still work drop extension test_ext8; +-- Test creation of extension in temporary schema with two-phase commit, +-- which should not work. This function wrapper is useful for portability. +-- Avoid noise caused by CONTEXT and NOTICE messages including the temporary +-- schema name. +\set SHOW_CONTEXT never +SET client_min_messages TO 'warning'; +-- First enforce presence of temporary schema. +CREATE TEMP TABLE test_ext4_tab (); +CREATE OR REPLACE FUNCTION create_extension_with_temp_schema() + RETURNS VOID AS $$ + DECLARE + tmpschema text; + query text; + BEGIN + SELECT INTO tmpschema pg_my_temp_schema()::regnamespace; + query := 'CREATE EXTENSION test_ext4 SCHEMA ' || tmpschema || ' CASCADE;'; + RAISE NOTICE 'query %', query; + EXECUTE query; + END; $$ LANGUAGE plpgsql; +BEGIN; +SELECT create_extension_with_temp_schema(); + create_extension_with_temp_schema +----------------------------------- + +(1 row) + +PREPARE TRANSACTION 'twophase_extension'; +ERROR: cannot PREPARE a transaction that has operated on temporary namespace +-- Clean up +DROP TABLE test_ext4_tab; +DROP FUNCTION create_extension_with_temp_schema(); +RESET client_min_messages; +\unset SHOW_CONTEXT diff --git a/src/test/modules/test_extensions/sql/test_extensions.sql b/src/test/modules/test_extensions/sql/test_extensions.sql index 9e64503eb5..f505466ab4 100644 --- a/src/test/modules/test_extensions/sql/test_extensions.sql +++ b/src/test/modules/test_extensions/sql/test_extensions.sql @@ -64,3 +64,32 @@ end'; -- dropping it should still work drop extension test_ext8; + +-- Test creation of extension in temporary schema with two-phase commit, +-- which should not work. This function wrapper is useful for portability. + +-- Avoid noise caused by CONTEXT and NOTICE messages including the temporary +-- schema name. +\set SHOW_CONTEXT never +SET client_min_messages TO 'warning'; +-- First enforce presence of temporary schema. +CREATE TEMP TABLE test_ext4_tab (); +CREATE OR REPLACE FUNCTION create_extension_with_temp_schema() + RETURNS VOID AS $$ + DECLARE + tmpschema text; + query text; + BEGIN + SELECT INTO tmpschema pg_my_temp_schema()::regnamespace; + query := 'CREATE EXTENSION test_ext4 SCHEMA ' || tmpschema || ' CASCADE;'; + RAISE NOTICE 'query %', query; + EXECUTE query; + END; $$ LANGUAGE plpgsql; +BEGIN; +SELECT create_extension_with_temp_schema(); +PREPARE TRANSACTION 'twophase_extension'; +-- Clean up +DROP TABLE test_ext4_tab; +DROP FUNCTION create_extension_with_temp_schema(); +RESET client_min_messages; +\unset SHOW_CONTEXT diff --git a/src/test/regress/expected/temp.out b/src/test/regress/expected/temp.out index f018f17ca0..860f58a3bf 100644 --- a/src/test/regress/expected/temp.out +++ b/src/test/regress/expected/temp.out @@ -301,3 +301,74 @@ select relname from pg_class where relname like 'temp_inh_oncommit_test%'; (1 row) drop table temp_inh_oncommit_test; +-- Tests with two-phase commit +-- Transactions creating objects in a temporary namespace cannot be used +-- with two-phase commit. +-- These cases generate errors about temporary namespace. +-- Function creation +begin; +create function pg_temp.twophase_func() returns void as + $$ select '2pc_func'::text $$ language sql; +prepare transaction 'twophase_func'; +ERROR: cannot PREPARE a transaction that has operated on temporary namespace +-- Function drop +create function pg_temp.twophase_func() returns void as + $$ select '2pc_func'::text $$ language sql; +begin; +drop function pg_temp.twophase_func(); +prepare transaction 'twophase_func'; +ERROR: cannot PREPARE a transaction that has operated on temporary namespace +-- Operator creation +begin; +create operator pg_temp.@@ (leftarg = int4, rightarg = int4, procedure = int4mi); +prepare transaction 'twophase_operator'; +ERROR: cannot PREPARE a transaction that has operated on temporary namespace +-- These generate errors about temporary tables. +begin; +create type pg_temp.twophase_type as (a int); +prepare transaction 'twophase_type'; +ERROR: cannot PREPARE a transaction that has operated on temporary tables +begin; +create view pg_temp.twophase_view as select 1; +prepare transaction 'twophase_view'; +ERROR: cannot PREPARE a transaction that has operated on temporary tables +begin; +create sequence pg_temp.twophase_seq; +prepare transaction 'twophase_sequence'; +ERROR: cannot PREPARE a transaction that has operated on temporary tables +-- Temporary tables cannot be used with two-phase commit. +create temp table twophase_tab (a int); +begin; +select a from twophase_tab; + a +--- +(0 rows) + +prepare transaction 'twophase_tab'; +ERROR: cannot PREPARE a transaction that has operated on temporary tables +begin; +insert into twophase_tab values (1); +prepare transaction 'twophase_tab'; +ERROR: cannot PREPARE a transaction that has operated on temporary tables +begin; +lock twophase_tab in access exclusive mode; +prepare transaction 'twophase_tab'; +ERROR: cannot PREPARE a transaction that has operated on temporary tables +begin; +drop table twophase_tab; +prepare transaction 'twophase_tab'; +ERROR: cannot PREPARE a transaction that has operated on temporary tables +-- Corner case: current_schema may create a temporary schema if namespace +-- creation is pending, so check after that. First reset the connection +-- to remove the temporary namespace. +\c - +SET search_path TO 'pg_temp'; +BEGIN; +SELECT current_schema() ~ 'pg_temp' AS is_temp_schema; + is_temp_schema +---------------- + t +(1 row) + +PREPARE TRANSACTION 'twophase_search'; +ERROR: cannot PREPARE a transaction that has operated on temporary namespace diff --git a/src/test/regress/sql/temp.sql b/src/test/regress/sql/temp.sql index 1beccc6ceb..e634ddb9ca 100644 --- a/src/test/regress/sql/temp.sql +++ b/src/test/regress/sql/temp.sql @@ -224,3 +224,59 @@ select * from temp_inh_oncommit_test; -- one relation remains select relname from pg_class where relname like 'temp_inh_oncommit_test%'; drop table temp_inh_oncommit_test; + +-- Tests with two-phase commit +-- Transactions creating objects in a temporary namespace cannot be used +-- with two-phase commit. + +-- These cases generate errors about temporary namespace. +-- Function creation +begin; +create function pg_temp.twophase_func() returns void as + $$ select '2pc_func'::text $$ language sql; +prepare transaction 'twophase_func'; +-- Function drop +create function pg_temp.twophase_func() returns void as + $$ select '2pc_func'::text $$ language sql; +begin; +drop function pg_temp.twophase_func(); +prepare transaction 'twophase_func'; +-- Operator creation +begin; +create operator pg_temp.@@ (leftarg = int4, rightarg = int4, procedure = int4mi); +prepare transaction 'twophase_operator'; + +-- These generate errors about temporary tables. +begin; +create type pg_temp.twophase_type as (a int); +prepare transaction 'twophase_type'; +begin; +create view pg_temp.twophase_view as select 1; +prepare transaction 'twophase_view'; +begin; +create sequence pg_temp.twophase_seq; +prepare transaction 'twophase_sequence'; + +-- Temporary tables cannot be used with two-phase commit. +create temp table twophase_tab (a int); +begin; +select a from twophase_tab; +prepare transaction 'twophase_tab'; +begin; +insert into twophase_tab values (1); +prepare transaction 'twophase_tab'; +begin; +lock twophase_tab in access exclusive mode; +prepare transaction 'twophase_tab'; +begin; +drop table twophase_tab; +prepare transaction 'twophase_tab'; + +-- Corner case: current_schema may create a temporary schema if namespace +-- creation is pending, so check after that. First reset the connection +-- to remove the temporary namespace. +\c - +SET search_path TO 'pg_temp'; +BEGIN; +SELECT current_schema() ~ 'pg_temp' AS is_temp_schema; +PREPARE TRANSACTION 'twophase_search';