transactional_enum-additions-v1x.patch
binary/octet-stream
Filename: transactional_enum-additions-v1x.patch
Type: binary/octet-stream
Part: 0
Patch
Format: unified
| File | + | − |
|---|---|---|
| src/backend/commands/typecmds.c | 7 | 17 |
| src/backend/utils/adt/enum.c | 73 | 0 |
| src/backend/utils/errcodes.txt | 1 | 0 |
| src/test/regress/expected/enum.out | 35 | 5 |
| src/test/regress/sql/enum.sql | 13 | 3 |
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 71d4df9..13231bb 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1237,23 +1237,13 @@ AlterEnum(AlterEnumStmt *stmt, bool isTopLevel)
elog(ERROR, "cache lookup failed for type %u", enum_type_oid);
/*
- * Ordinarily we disallow adding values within transaction blocks, because
- * we can't cope with enum OID values getting into indexes and then having
- * their defining pg_enum entries go away. However, it's okay if the enum
- * type was created in the current transaction, since then there can be no
- * such indexes that wouldn't themselves go away on rollback. (We support
- * this case because pg_dump --binary-upgrade needs it.) We test this by
- * seeing if the pg_type row has xmin == current XID and is not
- * HEAP_UPDATED. If it is HEAP_UPDATED, we can't be sure whether the type
- * was created or only modified in this xact. So we are disallowing some
- * cases that could theoretically be safe; but fortunately pg_dump only
- * needs the simplest case.
- */
- if (HeapTupleHeaderGetXmin(tup->t_data) == GetCurrentTransactionId() &&
- !(tup->t_data->t_infomask & HEAP_UPDATED))
- /* safe to do inside transaction block */ ;
- else
- PreventTransactionChain(isTopLevel, "ALTER TYPE ... ADD");
+ * We used to prevent this from happening inside a transaction block in
+ * situations where the new value could get into an upper level index page
+ * and cause corruption on rollback. Now we instead detect use of the new
+ * value in such situations and raise an exception, so that just adding to
+ * an enum in a transaction block is now safe.
+ * See enum.c:check_safe_enum_use()
+ */
/* Check it's an enum and check user has permission to ALTER the enum */
checkEnumOwner(tup);
diff --git a/src/backend/utils/adt/enum.c b/src/backend/utils/adt/enum.c
index 135a544..7e0977e 100644
--- a/src/backend/utils/adt/enum.c
+++ b/src/backend/utils/adt/enum.c
@@ -16,6 +16,7 @@
#include "access/genam.h"
#include "access/heapam.h"
#include "access/htup_details.h"
+#include "access/xact.h"
#include "catalog/indexing.h"
#include "catalog/pg_enum.h"
#include "libpq/pqformat.h"
@@ -30,6 +31,60 @@
static Oid enum_endpoint(Oid enumtypoid, ScanDirection direction);
static ArrayType *enum_range_internal(Oid enumtypoid, Oid lower, Oid upper);
+/*
+ * We need to make sure that new enum values don't get into the upper levels
+ * of indexes where we can't roll them back. To do that we prevent the use
+ * of enum values where the enum row is not committed, unless the type itself
+ * has been created in the current transaction. In the latter case any
+ * rollback would involve rolling back the whole index, since it couldn't
+ * exist before the enum type, and thus we couldn't get a corrupted index.
+ * We test this by seeing if the pg_type row has xmin == current XID and is not
+ * HEAP_UPDATED. If it is HEAP_UPDATED, we can't be sure whether the type
+ * was created or only modified in this xact. So we are disallowing some
+ * cases that could theoretically be safe. Narrowing down the unsafe conditions
+ * is left as an exercise for another day.
+ *
+ * This function needs to be called directly or indirectly in any of the
+ * functions below that could produce an enum value, particulary, enum_in,
+ * enum_recv, enum_first, enum_last, and all the enum_range variants.
+ */
+static void
+check_safe_enum_use(char * name, Oid enumtypoid, HeapTuple enum_tup)
+{
+ /* First see if it's a new enum value - if not it's safe */
+ if (!HeapTupleHeaderXminCommitted(enum_tup->t_data))
+ {
+
+ /*
+ * It is a new enum value, so now check to see if the whole enum is
+ * new. If it is, we're safe.
+ */
+ HeapTuple tup;
+
+ tup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(enumtypoid));
+ Assert(HeapTupleIsValid(tup));
+
+ if (HeapTupleHeaderGetXmin(tup->t_data) ==GetCurrentTransactionId() &&
+ !(tup->t_data->t_infomask & HEAP_UPDATED))
+ /* safe, must have been created in this transaction */ ;
+ else
+ /*
+ * We can't be in a top level transaction here, because the
+ * enum row hasn't been committed. There might well be other
+ * tests we could do here to narrow down the unsafe conditions,
+ * but for now just raise an exception.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNSAFE_NEW_ENUM_VALUE_USAGE),
+ errmsg("unsafe use of new value for enum %s: \"%s\"",
+ format_type_be(enumtypoid),
+ name),
+ errhint("New enum values must be committed before they can be used.")));
+
+ ReleaseSysCache(tup);
+ }
+}
+
/* Basic I/O support */
@@ -59,6 +114,8 @@ enum_in(PG_FUNCTION_ARGS)
format_type_be(enumtypoid),
name)));
+ check_safe_enum_use(name, enumtypoid, tup);
+
/*
* This comes from pg_enum.oid and stores system oids in user tables. This
* oid must be preserved by binary upgrades.
@@ -124,6 +181,8 @@ enum_recv(PG_FUNCTION_ARGS)
format_type_be(enumtypoid),
name)));
+ check_safe_enum_use(name, enumtypoid, tup);
+
enumoid = HeapTupleGetOid(tup);
ReleaseSysCache(tup);
@@ -327,9 +386,18 @@ enum_endpoint(Oid enumtypoid, ScanDirection direction)
enum_tuple = systable_getnext_ordered(enum_scan, direction);
if (HeapTupleIsValid(enum_tuple))
+ {
+ Form_pg_enum en;
+
+ en = (Form_pg_enum) GETSTRUCT(enum_tuple);
+ check_safe_enum_use(NameStr(en->enumlabel), enumtypoid, enum_tuple);
minmax = HeapTupleGetOid(enum_tuple);
+ }
else
+ {
+ /* should only happen with an empty enum */
minmax = InvalidOid;
+ }
systable_endscan_ordered(enum_scan);
index_close(enum_idx, AccessShareLock);
@@ -490,6 +558,11 @@ enum_range_internal(Oid enumtypoid, Oid lower, Oid upper)
if (left_found)
{
+ Form_pg_enum en;
+
+ en = (Form_pg_enum) GETSTRUCT(enum_tuple);
+ check_safe_enum_use(NameStr(en->enumlabel), enumtypoid, enum_tuple);
+
if (cnt >= max)
{
max *= 2;
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index be924d5..c6495d2 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -199,6 +199,7 @@ Section: Class 22 - Data Exception
22P03 E ERRCODE_INVALID_BINARY_REPRESENTATION invalid_binary_representation
22P04 E ERRCODE_BAD_COPY_FILE_FORMAT bad_copy_file_format
22P05 E ERRCODE_UNTRANSLATABLE_CHARACTER untranslatable_character
+22P07 E ERRCODE_UNSAFE_NEW_ENUM_VALUE_USAGE unsafe_new_enum_value_usage
2200L E ERRCODE_NOT_AN_XML_DOCUMENT not_an_xml_document
2200M E ERRCODE_INVALID_XML_DOCUMENT invalid_xml_document
2200N E ERRCODE_INVALID_XML_CONTENT invalid_xml_content
diff --git a/src/test/regress/expected/enum.out b/src/test/regress/expected/enum.out
index 1a61a5b..a89fc30 100644
--- a/src/test/regress/expected/enum.out
+++ b/src/test/regress/expected/enum.out
@@ -560,25 +560,55 @@ DROP TYPE bogus;
-- check transactional behaviour of ALTER TYPE ... ADD VALUE
--
CREATE TYPE bogus AS ENUM('good');
--- check that we can't add new values to existing enums in a transaction
+-- check that we can add new values to existing enums in a transaction
+-- but we can't use them
BEGIN;
ALTER TYPE bogus ADD VALUE 'bad';
-ERROR: ALTER TYPE ... ADD cannot run inside a transaction block
+SAVEPOINT x;
+SELECT 'bad'::bogus;
+ERROR: unsafe use of new value for enum bogus: "bad"
+LINE 1: SELECT 'bad'::bogus;
+ ^
+HINT: New enum values must be committed before they can be used.
+ROLLBACK TO x;
+SELECT enum_first(null::bogus) as should_suceed;
+ should_suceed
+---------------
+ good
+(1 row)
+
+SELECT enum_last(null::bogus);
+ERROR: unsafe use of new value for enum bogus: "bad"
+HINT: New enum values must be committed before they can be used.
+ROLLBACK TO x;
+SELECT enum_range(null::bogus);
+ERROR: unsafe use of new value for enum bogus: "bad"
+HINT: New enum values must be committed before they can be used.
COMMIT;
-- check that we recognize the case where the enum already existed but was
-- modified in the current txn
BEGIN;
ALTER TYPE bogus RENAME TO bogon;
-ALTER TYPE bogon ADD VALUE 'bad';
-ERROR: ALTER TYPE ... ADD cannot run inside a transaction block
+ALTER TYPE bogon ADD VALUE 'alsobad';
+SELECT 'alsobad'::bogon;
+ERROR: unsafe use of new value for enum bogon: "alsobad"
+LINE 1: SELECT 'alsobad'::bogon;
+ ^
+HINT: New enum values must be committed before they can be used.
ROLLBACK;
DROP TYPE bogus;
-- check that we *can* add new values to existing enums in a transaction,
--- if the type is new as well
+-- and to use them, if the type is new as well
BEGIN;
CREATE TYPE bogus AS ENUM();
ALTER TYPE bogus ADD VALUE 'good';
ALTER TYPE bogus ADD VALUE 'ugly';
+SELECT enum_range(null::bogus);
+ enum_range
+-------------
+ {good,ugly}
+(1 row)
+
ROLLBACK;
--
-- Cleanup
diff --git a/src/test/regress/sql/enum.sql b/src/test/regress/sql/enum.sql
index 88a835e..6203c44 100644
--- a/src/test/regress/sql/enum.sql
+++ b/src/test/regress/sql/enum.sql
@@ -262,26 +262,36 @@ DROP TYPE bogus;
--
CREATE TYPE bogus AS ENUM('good');
--- check that we can't add new values to existing enums in a transaction
+-- check that we can add new values to existing enums in a transaction
+-- but we can't use them
BEGIN;
ALTER TYPE bogus ADD VALUE 'bad';
+SAVEPOINT x;
+SELECT 'bad'::bogus;
+ROLLBACK TO x;
+SELECT enum_first(null::bogus) as should_suceed;
+SELECT enum_last(null::bogus);
+ROLLBACK TO x;
+SELECT enum_range(null::bogus);
COMMIT;
-- check that we recognize the case where the enum already existed but was
-- modified in the current txn
BEGIN;
ALTER TYPE bogus RENAME TO bogon;
-ALTER TYPE bogon ADD VALUE 'bad';
+ALTER TYPE bogon ADD VALUE 'alsobad';
+SELECT 'alsobad'::bogon;
ROLLBACK;
DROP TYPE bogus;
-- check that we *can* add new values to existing enums in a transaction,
--- if the type is new as well
+-- and to use them, if the type is new as well
BEGIN;
CREATE TYPE bogus AS ENUM();
ALTER TYPE bogus ADD VALUE 'good';
ALTER TYPE bogus ADD VALUE 'ugly';
+SELECT enum_range(null::bogus);
ROLLBACK;
--