Hi, Robert, Mark, CCing you because of the amcheck aspect (see below).
On 2023-01-09 17:50:10 +0100, Matthias van de Meent wrote: > On Sun, 8 Jan 2023 at 04:09, Andres Freund <and...@anarazel.de> wrote: > > > For a bit I was thinking that we should introduce the notion that a > > > FullTransactionId can be from the past. Specifically that when the upper > > > 32bit > > > are all set, we treat the lower 32bit as a value from before xid 0 using > > > the > > > normal 32bit xid arithmetic. But it sucks to add overhead for that > > > everywhere. > > > > > > It might be a bit more palatable to designate one individual value, > > > e.g. 2^32-1<<32, as a "before xid 0" indicator - it doesn't matter how far > > > before the start of the universe an xid point to... > > > > On IM Thomas suggested we could reserve the 2^32-1 epoch for invalid > > values. I > > hacked up a patch that converts various fxid functions to inline functions > > with such assertions, and it indeed quickly catches the problem this thread > > reported, close to the source of the use. > > Wouldn't it be enough to only fix the constructions in > FullXidRelativeTo() and widen_snapshot_xid() (as attached, $topic does > not occur with the patch), and (optionally) bump the first XID > available for any cluster to (FirstNormalXid + 1) to retain the 'older > than any running transaction' property? It's not too hard to fix in individual places, but I suspect that we'll introduce the bug in future places without some more fundamental protection. Locally I fixed it by clamping vacuum_defer_cleanup_age to a reasonable value in ComputeXidHorizons() and GetSnapshotData(). Fixing it in FullXidRelativeTo() doesn't seem quite right, while it's ok to just return FirstNormalTransactionId in the case of vacuum_defer_cleanup_age, it doesn't see necessarily correct for other cases. > The change only fixes the issue for FullTransactionId, which IMO is OK > - I don't think we need to keep xid->xid8->xid symmetric in cases of > xid8 wraparound. I think we should keep that symmetric, it just gets too confusing / easy to miss bugs otherwise. > > One issue with that is is that it'd reduce what can be input for the xid8 > > type. But it's hard to believe that'd be a real issue? > > Yes, it's unlikely anyone would ever hit that with our current WAL > format - we use 24 bytes /xid just to log it's use, so we'd use at > most epoch 0x1000_0000 in unrealistic scenarios. In addition; > technically, we already have (3*2^32 - 3) "invalid" xid8 values that > can never be produced in FullXidRelativeTo - those few extra invalid > values don't matter much to me except "even more special casing". Yep. The attached 0002 is a first implementation of this. The new assertions found at least one bug in amcheck, and one further example of the problem of representing past 32 xids in 64bit: 1) Because ctx->next_xid is set after the XidFromFullTransactionId() call in update_cached_xid_range(), we end up using the xid 0 (or an outdated value in subsequent calls) to determine whether epoch needs to be reduced. 2) One test generates includes an xid from the future (4026531839). Which causes epoch to wrap around (via the epoch--) in FullTransactionIdFromXidAndCtx(). I've hackily fixed that by just representing it as an xid from the future instead. But not sure that's a good answer. A different approach would be to represent fxids as *signed* 64bit integers. That'd of course loose more range, but could represent everything accurately, and would have a compatible on-disk representation on two's complement platforms (all our platforms). I think the only place that'd need special treatment is U64FromFullTransactionId() / its callers. I think this might be the most robust approach. Greetings, Andres Freund
>From 13450d14a1e064dd958b516ffa36b7a24bb49d7b Mon Sep 17 00:00:00 2001 From: Andres Freund <and...@anarazel.de> Date: Mon, 9 Jan 2023 10:23:10 -0800 Subject: [PATCH v2 1/3] Fix corruption due to vacuum_defer_cleanup_age underflowing 64bit xids Author: Reviewed-by: Discussion: https://postgr.es/m/20230108002923.cyoser3ttmt63...@awork3.anarazel.de Backpatch: --- src/backend/storage/ipc/procarray.c | 73 ++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c index 4340bf96416..d7d18ba8c12 100644 --- a/src/backend/storage/ipc/procarray.c +++ b/src/backend/storage/ipc/procarray.c @@ -367,6 +367,7 @@ static inline void ProcArrayEndTransactionInternal(PGPROC *proc, TransactionId l static void ProcArrayGroupClearXid(PGPROC *proc, TransactionId latestXid); static void MaintainLatestCompletedXid(TransactionId latestXid); static void MaintainLatestCompletedXidRecovery(TransactionId latestXid); +static int ClampVacuumDeferCleanupAge(FullTransactionId rel, TransactionId xid); static inline FullTransactionId FullXidRelativeTo(FullTransactionId rel, TransactionId xid); @@ -1888,17 +1889,32 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h) * so guc.c should limit it to no more than the xidStopLimit threshold * in varsup.c. Also note that we intentionally don't apply * vacuum_defer_cleanup_age on standby servers. + * + * Be careful to clamp vacuum_defer_cleanup age to prevent it from + * creating an xid before FirstNormalTransactionId. */ - h->oldest_considered_running = - TransactionIdRetreatedBy(h->oldest_considered_running, - vacuum_defer_cleanup_age); - h->shared_oldest_nonremovable = - TransactionIdRetreatedBy(h->shared_oldest_nonremovable, - vacuum_defer_cleanup_age); - h->data_oldest_nonremovable = - TransactionIdRetreatedBy(h->data_oldest_nonremovable, - vacuum_defer_cleanup_age); - /* defer doesn't apply to temp relations */ + Assert(TransactionIdPrecedesOrEquals(h->oldest_considered_running, + h->shared_oldest_nonremovable)); + Assert(TransactionIdPrecedesOrEquals(h->shared_oldest_nonremovable, + h->data_oldest_nonremovable)); + + if (vacuum_defer_cleanup_age > 0) + { + int clamped_cleanup_age = + ClampVacuumDeferCleanupAge(h->latest_completed, + h->oldest_considered_running); + + h->oldest_considered_running = + TransactionIdRetreatedBy(h->oldest_considered_running, + clamped_cleanup_age); + h->shared_oldest_nonremovable = + TransactionIdRetreatedBy(h->shared_oldest_nonremovable, + clamped_cleanup_age); + h->data_oldest_nonremovable = + TransactionIdRetreatedBy(h->data_oldest_nonremovable, + clamped_cleanup_age); + /* defer doesn't apply to temp relations */ + } } /* @@ -2469,9 +2485,17 @@ GetSnapshotData(Snapshot snapshot) */ oldestfxid = FullXidRelativeTo(latest_completed, oldestxid); + def_vis_xid_data = xmin; + /* apply vacuum_defer_cleanup_age */ - def_vis_xid_data = - TransactionIdRetreatedBy(xmin, vacuum_defer_cleanup_age); + if (vacuum_defer_cleanup_age > 0) + { + int clamped_cleanup_age = + ClampVacuumDeferCleanupAge(oldestfxid, oldestxid); + + def_vis_xid_data = + TransactionIdRetreatedBy(xmin, clamped_cleanup_age); + } /* Check whether there's a replication slot requiring an older xmin. */ def_vis_xid_data = @@ -4295,6 +4319,31 @@ GlobalVisCheckRemovableXid(Relation rel, TransactionId xid) return GlobalVisTestIsRemovableXid(state, xid); } +/* + * Clamp vacuum_defer_cleanup_age, to prevent it from retreating below + * FirstNormalTransactionId during epoch 0. This is important to prevent + * generating xids that cannot be converted to a FullTransactionId without + * wrapping around. + */ +static int +ClampVacuumDeferCleanupAge(FullTransactionId rel, TransactionId oldest_xid) +{ + FullTransactionId oldest_fxid; + uint64 oldest_fxid_i; + + if (vacuum_defer_cleanup_age == 0) + return 0; + + oldest_fxid = FullXidRelativeTo(rel, oldest_xid); + oldest_fxid_i = U64FromFullTransactionId(oldest_fxid); + + if (oldest_fxid_i < vacuum_defer_cleanup_age || + (oldest_fxid_i - vacuum_defer_cleanup_age) < FirstNormalTransactionId) + return (int) (oldest_fxid_i - FirstNormalTransactionId); + + return vacuum_defer_cleanup_age; +} + /* * Convert a 32 bit transaction id into 64 bit transaction id, by assuming it * is within MaxTransactionId / 2 of XidFromFullTransactionId(rel). -- 2.38.0
>From 1b85be918858172cec0c2884169e79576608ed3f Mon Sep 17 00:00:00 2001 From: Andres Freund <and...@anarazel.de> Date: Mon, 9 Jan 2023 11:24:37 -0800 Subject: [PATCH v2 2/3] wip: amcheck: Fix bugs relating to 64bit xids Discussion: https://postgr.es/m/20230108002923.cyoser3ttmt63...@awork3.anarazel.de --- contrib/amcheck/verify_heapam.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index 4fcfd6df72d..56a3b3e9974 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -1576,11 +1576,25 @@ FullTransactionIdFromXidAndCtx(TransactionId xid, const HeapCheckContext *ctx) { uint32 epoch; + Assert(TransactionIdIsValid(ctx->next_xid)); + Assert(FullTransactionIdIsValid(ctx->next_fxid)); + if (!TransactionIdIsNormal(xid)) return FullTransactionIdFromEpochAndXid(0, xid); epoch = EpochFromFullTransactionId(ctx->next_fxid); if (xid > ctx->next_xid) + { + /* + * FIXME, doubtful this is the best fix. + * + * Can't represent the 32bit xid as a 64bit xid, as it's before fxid + * 0. Represent it as an xid from the future instead. + */ + if (epoch == 0) + return FullTransactionIdFromEpochAndXid(0, xid); + epoch--; + } return FullTransactionIdFromEpochAndXid(epoch, xid); } @@ -1597,8 +1611,8 @@ update_cached_xid_range(HeapCheckContext *ctx) LWLockRelease(XidGenLock); /* And compute alternate versions of the same */ - ctx->oldest_fxid = FullTransactionIdFromXidAndCtx(ctx->oldest_xid, ctx); ctx->next_xid = XidFromFullTransactionId(ctx->next_fxid); + ctx->oldest_fxid = FullTransactionIdFromXidAndCtx(ctx->oldest_xid, ctx); } /* -- 2.38.0
>From 2fccf780a517416283ad739acb6df4cbcf2f2b28 Mon Sep 17 00:00:00 2001 From: Andres Freund <and...@anarazel.de> Date: Mon, 9 Jan 2023 11:25:43 -0800 Subject: [PATCH v2 3/3] wip: stricter validation for 64bit xids 64bit xids can't represent xids before xid 0, but 32bit xids can. This has lead to a number of bugs, some data corrupting. To make it easier to catch such bugs, disallow 64bit where the upper 32bit are all set, which is what one gets when naively trying to convert such 32bit xids. Additionally explicitly disallow 64bit xids that are already aren't allowed because they'd map to special 32bit xids. This changes the input routines for 64bit xids to error out in more cases. Discussion: https://postgr.es/m/20230108002923.cyoser3ttmt63...@awork3.anarazel.de --- src/include/access/transam.h | 136 ++++++++++++++++++++++++++++-- src/backend/utils/adt/xid.c | 7 ++ src/backend/utils/adt/xid8funcs.c | 17 ++-- src/test/regress/expected/xid.out | 44 ++++++++-- src/test/regress/sql/xid.sql | 12 ++- 5 files changed, 192 insertions(+), 24 deletions(-) diff --git a/src/include/access/transam.h b/src/include/access/transam.h index f5af6d30556..0d4bf57e63a 100644 --- a/src/include/access/transam.h +++ b/src/include/access/transam.h @@ -44,15 +44,6 @@ #define TransactionIdStore(xid, dest) (*(dest) = (xid)) #define StoreInvalidTransactionId(dest) (*(dest) = InvalidTransactionId) -#define EpochFromFullTransactionId(x) ((uint32) ((x).value >> 32)) -#define XidFromFullTransactionId(x) ((uint32) (x).value) -#define U64FromFullTransactionId(x) ((x).value) -#define FullTransactionIdEquals(a, b) ((a).value == (b).value) -#define FullTransactionIdPrecedes(a, b) ((a).value < (b).value) -#define FullTransactionIdPrecedesOrEquals(a, b) ((a).value <= (b).value) -#define FullTransactionIdFollows(a, b) ((a).value > (b).value) -#define FullTransactionIdFollowsOrEquals(a, b) ((a).value >= (b).value) -#define FullTransactionIdIsValid(x) TransactionIdIsValid(XidFromFullTransactionId(x)) #define InvalidFullTransactionId FullTransactionIdFromEpochAndXid(0, InvalidTransactionId) #define FirstNormalFullTransactionId FullTransactionIdFromEpochAndXid(0, FirstNormalTransactionId) #define FullTransactionIdIsNormal(x) FullTransactionIdFollowsOrEquals(x, FirstNormalFullTransactionId) @@ -61,12 +52,127 @@ * A 64 bit value that contains an epoch and a TransactionId. This is * wrapped in a struct to prevent implicit conversion to/from TransactionId. * Not all values represent valid normal XIDs. + * + * 64bit xids that would map to a non-normal 32bit xid are not allowed, for + * obvious reasons. + + * In addition, no xids with all the upper 32bit sets are allowed to exist, + * this is to make it easier to catch conversion errors from 32bit xids (which + * can point to "before" xid 0). */ typedef struct FullTransactionId { uint64 value; } FullTransactionId; +#define MAX_FULL_TRANSACTION_ID ((((uint64)~(uint32)0)<<32) - 1) + +/* + * This is separate from FullTransactionIdValidRange() so that input routines + * can check for invalid values without triggering an assert. + */ +static inline bool +InFullTransactionIdRange(uint64 val) +{ + /* 64bit xid above the upper bound */ + if (val > MAX_FULL_TRANSACTION_ID) + return false; + + /* + * normal 64bit xids that'd map to special 32 xids aren't allowed. + */ + if (val >= (uint64) FirstNormalTransactionId && + (uint32) val < FirstNormalTransactionId) + return false; + return true; +} + +/* + * Validation routine, typically used in asserts. + */ +static inline bool +FullTransactionIdValidRange(FullTransactionId x) +{ + return InFullTransactionIdRange(x.value); +} + +static inline uint64 +U64FromFullTransactionId(FullTransactionId x) +{ + Assert(FullTransactionIdValidRange(x)); + + return x.value; +} + +static inline TransactionId +XidFromFullTransactionId(FullTransactionId x) +{ + Assert(FullTransactionIdValidRange(x)); + + return (TransactionId) x.value; +} + +static inline uint32 +EpochFromFullTransactionId(FullTransactionId x) +{ + Assert(FullTransactionIdValidRange(x)); + + return (uint32) (x.value >> 32); +} + +static inline bool +FullTransactionIdEquals(FullTransactionId a, FullTransactionId b) +{ + Assert(FullTransactionIdValidRange(a)); + Assert(FullTransactionIdValidRange(b)); + + return a.value == b.value; +} + +static inline bool +FullTransactionIdPrecedes(FullTransactionId a, FullTransactionId b) +{ + Assert(FullTransactionIdValidRange(a)); + Assert(FullTransactionIdValidRange(b)); + + return a.value < b.value; +} + +static inline bool +FullTransactionIdPrecedesOrEquals(FullTransactionId a, FullTransactionId b) +{ + Assert(FullTransactionIdValidRange(a)); + Assert(FullTransactionIdValidRange(b)); + + return a.value <= b.value; +} + +static inline bool +FullTransactionIdFollows(FullTransactionId a, FullTransactionId b) +{ + Assert(FullTransactionIdValidRange(a)); + Assert(FullTransactionIdValidRange(b)); + + return a.value > b.value; +} + +static inline bool +FullTransactionIdFollowsOrEquals(FullTransactionId a, FullTransactionId b) +{ + Assert(FullTransactionIdValidRange(a)); + Assert(FullTransactionIdValidRange(b)); + + return a.value >= b.value; +} + +static inline bool +FullTransactionIdIsValid(FullTransactionId x) +{ + Assert(FullTransactionIdValidRange(x)); + + return x.value != (uint64) InvalidTransactionId; +} + static inline FullTransactionId FullTransactionIdFromEpochAndXid(uint32 epoch, TransactionId xid) { @@ -74,6 +180,8 @@ FullTransactionIdFromEpochAndXid(uint32 epoch, TransactionId xid) result.value = ((uint64) epoch) << 32 | xid; + Assert(FullTransactionIdValidRange(result)); + return result; } @@ -84,6 +192,8 @@ FullTransactionIdFromU64(uint64 value) result.value = value; + Assert(FullTransactionIdValidRange(result)); + return result; } @@ -102,6 +212,8 @@ FullTransactionIdFromU64(uint64 value) static inline void FullTransactionIdRetreat(FullTransactionId *dest) { + Assert(FullTransactionIdValidRange(*dest)); + dest->value--; /* @@ -118,6 +230,8 @@ FullTransactionIdRetreat(FullTransactionId *dest) */ while (XidFromFullTransactionId(*dest) < FirstNormalTransactionId) dest->value--; + + Assert(FullTransactionIdValidRange(*dest)); } /* @@ -127,6 +241,8 @@ FullTransactionIdRetreat(FullTransactionId *dest) static inline void FullTransactionIdAdvance(FullTransactionId *dest) { + Assert(FullTransactionIdValidRange(*dest)); + dest->value++; /* see FullTransactionIdAdvance() */ @@ -135,6 +251,8 @@ FullTransactionIdAdvance(FullTransactionId *dest) while (XidFromFullTransactionId(*dest) < FirstNormalTransactionId) dest->value++; + + Assert(FullTransactionIdValidRange(*dest)); } /* back up a transaction ID variable, handling wraparound correctly */ diff --git a/src/backend/utils/adt/xid.c b/src/backend/utils/adt/xid.c index 8ac1679c381..21c0f4c67df 100644 --- a/src/backend/utils/adt/xid.c +++ b/src/backend/utils/adt/xid.c @@ -188,6 +188,13 @@ xid8in(PG_FUNCTION_ARGS) uint64 result; result = uint64in_subr(str, NULL, "xid8", fcinfo->context); + + if (!InFullTransactionIdRange(result)) + ereturn(fcinfo->context, 0, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value \"%s\" is out of range for type %s", + str, "xid8"))); + PG_RETURN_FULLTRANSACTIONID(FullTransactionIdFromU64(result)); } diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c index e616303a292..b8d8d92f793 100644 --- a/src/backend/utils/adt/xid8funcs.c +++ b/src/backend/utils/adt/xid8funcs.c @@ -302,15 +302,18 @@ parse_snapshot(const char *str, Node *escontext) const char *str_start = str; char *endp; StringInfo buf; + uint64 raw_fxid; - xmin = FullTransactionIdFromU64(strtou64(str, &endp, 10)); - if (*endp != ':') + raw_fxid = strtou64(str, &endp, 10); + if (*endp != ':' || !InFullTransactionIdRange(raw_fxid)) goto bad_format; + xmin = FullTransactionIdFromU64(raw_fxid); str = endp + 1; - xmax = FullTransactionIdFromU64(strtou64(str, &endp, 10)); - if (*endp != ':') + raw_fxid = strtou64(str, &endp, 10); + if (*endp != ':' || !InFullTransactionIdRange(raw_fxid)) goto bad_format; + xmax = FullTransactionIdFromU64(raw_fxid); str = endp + 1; /* it should look sane */ @@ -326,7 +329,11 @@ parse_snapshot(const char *str, Node *escontext) while (*str != '\0') { /* read next value */ - val = FullTransactionIdFromU64(strtou64(str, &endp, 10)); + raw_fxid = strtou64(str, &endp, 10); + + val = FullTransactionIdFromU64(raw_fxid); + if (!InFullTransactionIdRange(raw_fxid)) + goto bad_format; str = endp; /* require the input to be in order */ diff --git a/src/test/regress/expected/xid.out b/src/test/regress/expected/xid.out index e62f7019434..4348eb66bd5 100644 --- a/src/test/regress/expected/xid.out +++ b/src/test/regress/expected/xid.out @@ -6,11 +6,11 @@ select '010'::xid, '-1'::xid, '010'::xid8, '42'::xid8, - '0xffffffffffffffff'::xid8, - '-1'::xid8; - xid | xid | xid | xid | xid8 | xid8 | xid8 | xid8 ------+-----+------------+------------+------+------+----------------------+---------------------- - 8 | 42 | 4294967295 | 4294967295 | 8 | 42 | 18446744073709551615 | 18446744073709551615 + '0xefffffffffffffff'::xid8, + '0'::xid8; + xid | xid | xid | xid | xid8 | xid8 | xid8 | xid8 +-----+-----+------------+------------+------+------+----------------------+------ + 8 | 42 | 4294967295 | 4294967295 | 8 | 42 | 17293822569102704639 | 0 (1 row) -- garbage values @@ -30,6 +30,18 @@ select 'asdf'::xid8; ERROR: invalid input syntax for type xid8: "asdf" LINE 1: select 'asdf'::xid8; ^ +select '-1'::xid8; +ERROR: value "-1" is out of range for type xid8 +LINE 1: select '-1'::xid8; + ^ +select '0xffffffffffffffff'::xid8; +ERROR: value "0xffffffffffffffff" is out of range for type xid8 +LINE 1: select '0xffffffffffffffff'::xid8; + ^ +select '0x0000300000000001'::xid8; +ERROR: value "0x0000300000000001" is out of range for type xid8 +LINE 1: select '0x0000300000000001'::xid8; + ^ -- Also try it with non-error-throwing API SELECT pg_input_is_valid('42', 'xid'); pg_input_is_valid @@ -67,6 +79,24 @@ SELECT pg_input_error_message('0xffffffffffffffffffff', 'xid8'); value "0xffffffffffffffffffff" is out of range for type xid8 (1 row) +SELECT pg_input_is_valid('-1', 'xid8'); + pg_input_is_valid +------------------- + f +(1 row) + +SELECT pg_input_is_valid('0xffffffffffffffff', 'xid8'); + pg_input_is_valid +------------------- + f +(1 row) + +SELECT pg_input_is_valid('0x0000300000000001', 'xid8'); + pg_input_is_valid +------------------- + f +(1 row) + -- equality select '1'::xid = '1'::xid; ?column? @@ -160,11 +190,11 @@ select xid8cmp('1', '2'), xid8cmp('2', '2'), xid8cmp('2', '1'); -- min() and max() for xid8 create table xid8_t1 (x xid8); -insert into xid8_t1 values ('0'), ('010'), ('42'), ('0xffffffffffffffff'), ('-1'); +insert into xid8_t1 values ('0'), ('010'), ('42'), ('0xefffffffffffffff'); select min(x), max(x) from xid8_t1; min | max -----+---------------------- - 0 | 18446744073709551615 + 0 | 17293822569102704639 (1 row) -- xid8 has btree and hash opclasses diff --git a/src/test/regress/sql/xid.sql b/src/test/regress/sql/xid.sql index b6996588ef6..b2104e4ce20 100644 --- a/src/test/regress/sql/xid.sql +++ b/src/test/regress/sql/xid.sql @@ -7,14 +7,17 @@ select '010'::xid, '-1'::xid, '010'::xid8, '42'::xid8, - '0xffffffffffffffff'::xid8, - '-1'::xid8; + '0xefffffffffffffff'::xid8, + '0'::xid8; -- garbage values select ''::xid; select 'asdf'::xid; select ''::xid8; select 'asdf'::xid8; +select '-1'::xid8; +select '0xffffffffffffffff'::xid8; +select '0x0000300000000001'::xid8; -- Also try it with non-error-throwing API SELECT pg_input_is_valid('42', 'xid'); @@ -23,6 +26,9 @@ SELECT pg_input_error_message('0xffffffffff', 'xid'); SELECT pg_input_is_valid('42', 'xid8'); SELECT pg_input_is_valid('asdf', 'xid8'); SELECT pg_input_error_message('0xffffffffffffffffffff', 'xid8'); +SELECT pg_input_is_valid('-1', 'xid8'); +SELECT pg_input_is_valid('0xffffffffffffffff', 'xid8'); +SELECT pg_input_is_valid('0x0000300000000001', 'xid8'); -- equality select '1'::xid = '1'::xid; @@ -51,7 +57,7 @@ select xid8cmp('1', '2'), xid8cmp('2', '2'), xid8cmp('2', '1'); -- min() and max() for xid8 create table xid8_t1 (x xid8); -insert into xid8_t1 values ('0'), ('010'), ('42'), ('0xffffffffffffffff'), ('-1'); +insert into xid8_t1 values ('0'), ('010'), ('42'), ('0xefffffffffffffff'); select min(x), max(x) from xid8_t1; -- xid8 has btree and hash opclasses -- 2.38.0