aboutsummaryrefslogtreecommitdiff
path: root/src/backend/utils/adt/enum.c
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2016-09-05 12:59:55 -0400
committerTom Lane <tgl@sss.pgh.pa.us>2016-09-05 12:59:55 -0400
commit15bc038f9bcd1a9af3f625caffafc7c20322202d (patch)
treedd929ef19d7f5bc751ec5528be37a66eae6bb0ce /src/backend/utils/adt/enum.c
parent016abf1fb8333de82a259af0cc7572a4b868023b (diff)
downloadpostgresql-15bc038f9bcd1a9af3f625caffafc7c20322202d.tar.gz
postgresql-15bc038f9bcd1a9af3f625caffafc7c20322202d.zip
Relax transactional restrictions on ALTER TYPE ... ADD VALUE.
To prevent possibly breaking indexes on enum columns, we must keep uncommitted enum values from getting stored in tables, unless we can be sure that any such column is new in the current transaction. Formerly, we enforced this by disallowing ALTER TYPE ... ADD VALUE from being executed at all in a transaction block, unless the target enum type had been created in the current transaction. This patch removes that restriction, and instead insists that an uncommitted enum value can't be referenced unless it belongs to an enum type created in the same transaction as the value. Per discussion, this should be a bit less onerous. It does require each function that could possibly return a new enum value to SQL operations to check this restriction, but there aren't so many of those that this seems unmaintainable. Andrew Dunstan and Tom Lane Discussion: <4075.1459088427@sss.pgh.pa.us>
Diffstat (limited to 'src/backend/utils/adt/enum.c')
-rw-r--r--src/backend/utils/adt/enum.c104
1 files changed, 104 insertions, 0 deletions
diff --git a/src/backend/utils/adt/enum.c b/src/backend/utils/adt/enum.c
index 135a54428a0..47d53550279 100644
--- a/src/backend/utils/adt/enum.c
+++ b/src/backend/utils/adt/enum.c
@@ -19,6 +19,7 @@
#include "catalog/indexing.h"
#include "catalog/pg_enum.h"
#include "libpq/pqformat.h"
+#include "storage/procarray.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -31,6 +32,93 @@ static Oid enum_endpoint(Oid enumtypoid, ScanDirection direction);
static ArrayType *enum_range_internal(Oid enumtypoid, Oid lower, Oid upper);
+/*
+ * Disallow use of an uncommitted pg_enum tuple.
+ *
+ * We need to make sure that uncommitted enum values don't get into indexes.
+ * If they did, and if we then rolled back the pg_enum addition, we'd have
+ * broken the index because value comparisons will not work reliably without
+ * an underlying pg_enum entry. (Note that removal of the heap entry
+ * containing an enum value is not sufficient to ensure that it doesn't appear
+ * in upper levels of indexes.) To do this we prevent an uncommitted row from
+ * being used for any SQL-level purpose. This is stronger than necessary,
+ * since the value might not be getting inserted into a table or there might
+ * be no index on its column, but it's easy to enforce centrally.
+ *
+ * However, it's okay to allow use of uncommitted values belonging to enum
+ * types that were themselves created in the same transaction, because then
+ * any such index would also be new and would go away altogether on rollback.
+ * (This case is required by pg_upgrade.)
+ *
+ * This function needs to be called (directly or indirectly) in any of the
+ * functions below that could return an enum value to SQL operations.
+ */
+static void
+check_safe_enum_use(HeapTuple enumval_tup)
+{
+ TransactionId xmin;
+ Form_pg_enum en;
+ HeapTuple enumtyp_tup;
+
+ /*
+ * If the row is hinted as committed, it's surely safe. This provides a
+ * fast path for all normal use-cases.
+ */
+ if (HeapTupleHeaderXminCommitted(enumval_tup->t_data))
+ return;
+
+ /*
+ * Usually, a row would get hinted as committed when it's read or loaded
+ * into syscache; but just in case not, let's check the xmin directly.
+ */
+ xmin = HeapTupleHeaderGetXmin(enumval_tup->t_data);
+ if (!TransactionIdIsInProgress(xmin) &&
+ TransactionIdDidCommit(xmin))
+ return;
+
+ /* It is a new enum value, so check to see if the whole enum is new */
+ en = (Form_pg_enum) GETSTRUCT(enumval_tup);
+ enumtyp_tup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(en->enumtypid));
+ if (!HeapTupleIsValid(enumtyp_tup))
+ elog(ERROR, "cache lookup failed for type %u", en->enumtypid);
+
+ /*
+ * We insist that the type have been created in the same (sub)transaction
+ * as the enum value. It would be safe to allow the type's originating
+ * xact to be a subcommitted child of the enum value's xact, but not vice
+ * versa (since we might now be in a subxact of the type's originating
+ * xact, which could roll back along with the enum value's subxact). The
+ * former case seems a sufficiently weird usage pattern as to not be worth
+ * spending code for, so we're left with a simple equality check.
+ *
+ * We also insist that the type's pg_type row not be HEAP_UPDATED. If it
+ * is, we can't tell whether the row was created or only modified in the
+ * apparent originating xact, so it might be older than that xact. (We do
+ * not worry whether the enum value is HEAP_UPDATED; if it is, we might
+ * think it's too new and throw an unnecessary error, but we won't allow
+ * an unsafe case.)
+ */
+ if (xmin == HeapTupleHeaderGetXmin(enumtyp_tup->t_data) &&
+ !(enumtyp_tup->t_data->t_infomask & HEAP_UPDATED))
+ {
+ /* same (sub)transaction, so safe */
+ ReleaseSysCache(enumtyp_tup);
+ return;
+ }
+
+ /*
+ * 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 \"%s\" of enum type %s",
+ NameStr(en->enumlabel),
+ format_type_be(en->enumtypid)),
+ errhint("New enum values must be committed before they can be used.")));
+}
+
+
/* Basic I/O support */
Datum
@@ -59,6 +147,9 @@ enum_in(PG_FUNCTION_ARGS)
format_type_be(enumtypoid),
name)));
+ /* check it's safe to use in SQL */
+ check_safe_enum_use(tup);
+
/*
* This comes from pg_enum.oid and stores system oids in user tables. This
* oid must be preserved by binary upgrades.
@@ -124,6 +215,9 @@ enum_recv(PG_FUNCTION_ARGS)
format_type_be(enumtypoid),
name)));
+ /* check it's safe to use in SQL */
+ check_safe_enum_use(tup);
+
enumoid = HeapTupleGetOid(tup);
ReleaseSysCache(tup);
@@ -327,9 +421,16 @@ enum_endpoint(Oid enumtypoid, ScanDirection direction)
enum_tuple = systable_getnext_ordered(enum_scan, direction);
if (HeapTupleIsValid(enum_tuple))
+ {
+ /* check it's safe to use in SQL */
+ check_safe_enum_use(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 +591,9 @@ enum_range_internal(Oid enumtypoid, Oid lower, Oid upper)
if (left_found)
{
+ /* check it's safe to use in SQL */
+ check_safe_enum_use(enum_tuple);
+
if (cnt >= max)
{
max *= 2;