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;