postgres.git / summary / log / commit / refs
commit 72af14bccd2127bf20ddeaf198b78de46e9e892b
Author: Masahiko Sawada <msawada@postgresql.org>
Date: Tue Sep 15 22:07:08 2026 +0000
Use hex_decode_safe() to speed up UUID input.
Previously, string_to_uuid() decoded one byte at a time, calling
isxdigit() twice and strtoul() once for every pair of hexadecimal
digits. Commit ec8719ccbfcd made hex_decode_safe() decode a run of
hexadecimal digits in bulk, so add a fast path for the two common
shapes: 32 contiguous hexadecimal digits, and the canonical
8x-4x-4x-4x-12x form, each optionally wrapped in braces.
Anything else, and any decoding error, falls back to the
byte-at-a-time parser, so the accepted grammar and the error messages
don't change. hex_decode_safe() also skips whitespace, which the UUID
grammar forbids, so we treat a short decode as an error too. Rejecting
an input that has a fast-path shape therefore costs a second
parse. But this only happens for syntax-error cases, so it's not a
problem in practice.
Reviewed-by: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reviewed-by: Haibo Yan <tristan.yim@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: John Naylor <johncnaylorls@gmail.com>
Discussion: https://postgr.es/m/CAD21AoCqeR4UQU77Q_yOMNNzJ7AVeiO5QZT+4HnzPm4Wm-e02Q@mail.gmail.com
src/backend/utils/adt/uuid.c | 101 +++++++++++++++++++++++++++++++++++--
src/test/regress/expected/uuid.out | 74 +++++++++++++++++++++++++++
src/test/regress/sql/uuid.sql | 24 +++++++++
3 files changed, 194 insertions(+), 5 deletions(-)
diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c
index 28e18940a9d..c019e17f21b 100644
--- a/src/backend/utils/adt/uuid.c
+++ b/src/backend/utils/adt/uuid.c
@@ -19,7 +19,9 @@
#include "common/hashfn.h"
#include "lib/hyperloglog.h"
#include "libpq/pqformat.h"
+#include "nodes/miscnodes.h"
#include "port/pg_bswap.h"
+#include "utils/builtins.h"
#include "utils/fmgrprotos.h"
#include "utils/guc.h"
#include "utils/skipsupport.h"
@@ -139,13 +141,13 @@ uuid_out(PG_FUNCTION_ARGS)
}
/*
- * We allow UUIDs as a series of 32 hexadecimal digits with an optional dash
- * after each group of 4 hexadecimal digits, and optionally surrounded by {}.
- * (The canonical format 8x-4x-4x-4x-12x, where "nx" means n hexadecimal
- * digits, is the only one used for output.)
+ * Reference implementation of the UUID grammar, parsing one byte at a time.
+ * string_to_uuid() recognizes the common shapes more cheaply and defers to
+ * this function for everything else, so this is also the only place that
+ * reports a syntax error.
*/
static void
-string_to_uuid(const char *source, pg_uuid_t *uuid, Node *escontext)
+string_to_uuid_scalar(const char *source, pg_uuid_t *uuid, Node *escontext)
{
const char *src = source;
bool braces = false;
@@ -194,6 +196,95 @@ syntax_error:
"uuid", source)));
}
+/*
+ * We allow UUIDs as a series of 32 hexadecimal digits with an optional dash
+ * after each group of 4 hexadecimal digits, and optionally surrounded by {}.
+ * (The canonical format 8x-4x-4x-4x-12x, where "nx" means n hexadecimal
+ * digits, is the only one used for output.)
+ *
+ * The two common shapes -- a bare string of 32 hexadecimal digits and the
+ * canonical form, each optionally wrapped in braces -- are compacted into 32
+ * contiguous hex digits and decoded with hex_decode_safe(), which is much
+ * faster than the byte-at-a-time loop. Any other shape, or any decoding
+ * error, is handed off to string_to_uuid_scalar() so that the accepted
+ * grammar and the error messages are unchanged.
+ */
+static void
+string_to_uuid(const char *source, pg_uuid_t *uuid, Node *escontext)
+{
+ const char *body = source;
+ const char *hexsrc = NULL;
+ char hexbuf[32];
+ uint64 written;
+ size_t len;
+ ErrorSaveContext private_escontext = {T_ErrorSaveContext};
+
+ /*
+ * Measure the input only far enough to classify its shape. The bound must
+ * exceed the longest shape handled here, the braced canonical form at 38
+ * characters: strnlen() returns the bound for anything at least that
+ * long, so stopping at an accepted length would accept a longer string
+ * that merely starts with a valid UUID.
+ */
+ len = strnlen(source, 64);
+
+ /* Strip one optional surrounding brace pair */
+ if (len >= 2 && source[0] == '{' && source[len - 1] == '}')
+ {
+ body = source + 1;
+ len -= 2;
+ }
+
+ if (len == 32)
+ {
+ /*
+ * Body is already 32 contiguous hex digits -- decode straight from
+ * the input. hex_decode_safe() reads exactly body[0..31], so it never
+ * touches the trailing NUL or '}'.
+ */
+ hexsrc = body;
+ }
+ else if (len == 36 && body[8] == '-' && body[13] == '-' &&
+ body[18] == '-' && body[23] == '-')
+ {
+ /*
+ * Canonical 8x-4x-4x-4x-12x form; compact them into hexbuf with
+ * fixed-offset copies, dropping the dashes.
+ */
+ memcpy(&hexbuf[0], &body[0], 8);
+ memcpy(&hexbuf[8], &body[9], 4);
+ memcpy(&hexbuf[12], &body[14], 4);
+ memcpy(&hexbuf[16], &body[19], 4);
+ memcpy(&hexbuf[20], &body[24], 12);
+ hexsrc = hexbuf;
+ }
+
+ if (hexsrc == NULL)
+ {
+ /* Uncommon shape; let the general parse handle it */
+ string_to_uuid_scalar(source, uuid, escontext);
+ return;
+ }
+
+ /*
+ * The shape matched, so the decode is expected to succeed. Any error is
+ * routed into a private context and discarded, leaving
+ * string_to_uuid_scalar() to parse the input again and report the syntax
+ * error, so that the message does not depend on which path rejected the
+ * input.
+ */
+ written = hex_decode_safe(hexsrc, 32, (char *) uuid->data,
+ (Node *) &private_escontext);
+
+ /*
+ * A short result must be rejected as well as an error: hex_decode_safe()
+ * skips whitespace, so it can succeed yet write fewer than UUID_LEN
+ * bytes, whereas the UUID grammar forbids whitespace.
+ */
+ if (private_escontext.error_occurred || written != UUID_LEN)
+ string_to_uuid_scalar(source, uuid, escontext);
+}
+
Datum
uuid_recv(PG_FUNCTION_ARGS)
{
diff --git a/src/test/regress/expected/uuid.out b/src/test/regress/expected/uuid.out
index d542eb14b26..a37bdd4245c 100644
--- a/src/test/regress/expected/uuid.out
+++ b/src/test/regress/expected/uuid.out
@@ -375,5 +375,79 @@ SELECT v = v::bytea::uuid as matched FROM gen_random_uuid() v;
t
(1 row)
+-- Test the UUID shapes for which the parser uses the fast path.
+SELECT '5b35380a-7143-4912-9b55-f322699c6770'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+SELECT '{5b35380a-7143-4912-9b55-f322699c6770}'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+SELECT '5b35380a714349129b55f322699c6770'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+SELECT '{5b35380a714349129b55f322699c6770}'::uuid;
+ uuid
+--------------------------------------
+ 5b35380a-7143-4912-9b55-f322699c6770
+(1 row)
+
+-- Test that the fast path correctly rejects invalid UUID strings.
+SELECT '5b35380a714349129b55f32 99c6770'::uuid;
+ERROR: invalid input syntax for type uuid: "5b35380a714349129b55f32 99c6770"
+LINE 1: SELECT '5b35380a714349129b55f32 99c6770'::uuid;
+ ^
+SELECT '5b35380a-7143-4912-9b55-f322699c67 '::uuid;
+ERROR: invalid input syntax for type uuid: "5b35380a-7143-4912-9b55-f322699c67 "
+LINE 1: SELECT '5b35380a-7143-4912-9b55-f322699c67 '::uuid;
+ ^
+SELECT ' 35380a-7143-4912-9b55-f322699c6770'::uuid;
+ERROR: invalid input syntax for type uuid: " 35380a-7143-4912-9b55-f322699c6770"
+LINE 1: SELECT ' 35380a-7143-4912-9b55-f322699c6770'::uuid;
+ ^
+SELECT 'AZ35380a-7143-4912-9b55-f322699c6770'::uuid;
+ERROR: invalid input syntax for type uuid: "AZ35380a-7143-4912-9b55-f322699c6770"
+LINE 1: SELECT 'AZ35380a-7143-4912-9b55-f322699c6770'::uuid;
+ ^
+SELECT '{AZ35380a-7143-4912-9b55-f322699c6770}'::uuid;
+ERROR: invalid input syntax for type uuid: "{AZ35380a-7143-4912-9b55-f322699c6770}"
+LINE 1: SELECT '{AZ35380a-7143-4912-9b55-f322699c6770}'::uuid;
+ ^
+SELECT '{AZ35380a714349129b55f322699c6770}'::uuid;
+ERROR: invalid input syntax for type uuid: "{AZ35380a714349129b55f322699c6770}"
+LINE 1: SELECT '{AZ35380a714349129b55f322699c6770}'::uuid;
+ ^
+SELECT '{AZ35380a714349129b55f322699c67 }'::uuid;
+ERROR: invalid input syntax for type uuid: "{AZ35380a714349129b55f322699c67 }"
+LINE 1: SELECT '{AZ35380a714349129b55f322699c67 }'::uuid;
+ ^
+-- The parser only measures the input far enough to classify its shape. If it
+-- stopped measuring at one of the accepted lengths, a longer string that
+-- merely starts with a valid UUID would look like that UUID and be accepted
+-- with the rest silently ignored, so check that trailing data is rejected.
+SELECT '5b35380a714349129b55f322699c6770TRAILING'::uuid;
+ERROR: invalid input syntax for type uuid: "5b35380a714349129b55f322699c6770TRAILING"
+LINE 1: SELECT '5b35380a714349129b55f322699c6770TRAILING'::uuid;
+ ^
+SELECT '{5b35380a714349129b55f322699c6770}TRAILING'::uuid;
+ERROR: invalid input syntax for type uuid: "{5b35380a714349129b55f322699c6770}TRAILING"
+LINE 1: SELECT '{5b35380a714349129b55f322699c6770}TRAILING'::uuid;
+ ^
+SELECT '5b35380a-7143-4912-9b55-f322699c6770TRAILING'::uuid;
+ERROR: invalid input syntax for type uuid: "5b35380a-7143-4912-9b55-f322699c6770TRAILING"
+LINE 1: SELECT '5b35380a-7143-4912-9b55-f322699c6770TRAILING'::uuid;
+ ^
+SELECT '{5b35380a-7143-4912-9b55-f322699c6770}TRAILING'::uuid;
+ERROR: invalid input syntax for type uuid: "{5b35380a-7143-4912-9b55-f322699c6770}TRAILING"
+LINE 1: SELECT '{5b35380a-7143-4912-9b55-f322699c6770}TRAILING'::uui...
+ ^
-- clean up
DROP TABLE guid1, guid2, guid3 CASCADE;
diff --git a/src/test/regress/sql/uuid.sql b/src/test/regress/sql/uuid.sql
index 54f0d8f8255..279bc94381e 100644
--- a/src/test/regress/sql/uuid.sql
+++ b/src/test/regress/sql/uuid.sql
@@ -178,5 +178,29 @@ SELECT '\x019a2f859ced7225b99d9c55044a2563'::bytea::uuid;
SELECT '\x1234567890abcdef'::bytea::uuid; -- error
SELECT v = v::bytea::uuid as matched FROM gen_random_uuid() v;
+-- Test the UUID shapes for which the parser uses the fast path.
+SELECT '5b35380a-7143-4912-9b55-f322699c6770'::uuid;
+SELECT '{5b35380a-7143-4912-9b55-f322699c6770}'::uuid;
+SELECT '5b35380a714349129b55f322699c6770'::uuid;
+SELECT '{5b35380a714349129b55f322699c6770}'::uuid;
+
+-- Test that the fast path correctly rejects invalid UUID strings.
+SELECT '5b35380a714349129b55f32 99c6770'::uuid;
+SELECT '5b35380a-7143-4912-9b55-f322699c67 '::uuid;
+SELECT ' 35380a-7143-4912-9b55-f322699c6770'::uuid;
+SELECT 'AZ35380a-7143-4912-9b55-f322699c6770'::uuid;
+SELECT '{AZ35380a-7143-4912-9b55-f322699c6770}'::uuid;
+SELECT '{AZ35380a714349129b55f322699c6770}'::uuid;
+SELECT '{AZ35380a714349129b55f322699c67 }'::uuid;
+
+-- The parser only measures the input far enough to classify its shape. If it
+-- stopped measuring at one of the accepted lengths, a longer string that
+-- merely starts with a valid UUID would look like that UUID and be accepted
+-- with the rest silently ignored, so check that trailing data is rejected.
+SELECT '5b35380a714349129b55f322699c6770TRAILING'::uuid;
+SELECT '{5b35380a714349129b55f322699c6770}TRAILING'::uuid;
+SELECT '5b35380a-7143-4912-9b55-f322699c6770TRAILING'::uuid;
+SELECT '{5b35380a-7143-4912-9b55-f322699c6770}TRAILING'::uuid;
+
-- clean up
DROP TABLE guid1, guid2, guid3 CASCADE;