1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

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 ec8719ccbf 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
This commit is contained in:
Masahiko Sawada
2026-09-15 15:07:08 -07:00
parent 862092932c
commit 72af14bccd
3 changed files with 194 additions and 5 deletions

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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;