I have a char field whose default value should be a ksuid. How to generate a ksuid in postgres?
This function generates KSUID on PostgreSQL. It uses numeric
data type to convert time and payload to base62.
It creates a pseudo-random payload using the native MD5()
function. If you want to use pgcrypto
, check out the @ssz's comment. Thank you @ssz!
KSUIDs generated by the function are compliant with the reference implementation.
Last updates:
fn_ksuid()
to ksuid()
;numeric(50)
to avoid floor()
./**
* Returns a Segment's KSUID.
*
* Reference implementation: https://github.com/segmentio/ksuid
* Also read: https://segment.com/blog/a-brief-history-of-the-uuid/
*/
create or replace function ksuid() returns text as $$
declare
v_time timestamp with time zone := null;
v_seconds numeric(50) := null;
v_payload bytea := null;
v_numeric numeric(50) := null;
v_base62 text := '';
v_epoch numeric(50) = 1400000000; -- 2014-05-13T16:53:20Z
v_alphabet char array[62] := array[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z'];
i integer := 0;
begin
-- Get the current time
v_time := clock_timestamp();
-- Extract seconds from the current time and apply epoch
v_seconds := EXTRACT(EPOCH FROM v_time) - v_epoch;
-- Generate a numeric value from the seconds
v_numeric := v_seconds * pow(2::numeric, 128);
-- Generate a pseudo-random payload
-- v_payload := gen_random_bytes(16); -- to be used with `pgcrypto`
v_payload := decode(md5(v_time::text || random()::text || random()::text), 'hex');
-------------------------------------------------------------------
-- FOR TEST: the expected result is '0ujtsYcgvSTl8PAuAdqWYSMnLOv'
-------------------------------------------------------------------
-- v_numeric := 107608047 * pow(2::numeric, 128);
-- v_payload := decode('B5A1CD34B5F99D1154FB6853345C9735', 'hex');
-- Add the payload to the numeric value
while i < 16 loop
i := i + 1;
v_numeric := v_numeric + (get_byte(v_payload, i - 1) * pow(2::numeric, (16 - i) * 8));
end loop;
-- Encode the numeric value to base62
while v_numeric <> 0 loop
v_base62 := v_base62 || v_alphabet[mod(v_numeric, 62) + 1];
v_numeric := div(v_numeric, 62);
end loop;
v_base62 := reverse(v_base62);
v_base62 := lpad(v_base62, 27, '0');
return v_base62;
end $$ language plpgsql;
See the updated function on GitHub Gist.
I'd like to suggest a slight change to the fabiolimace`s solution. Instead:
v_payload := decode(md5(v_time::text || random()::text || random()::text), 'hex');
use more strong function gen_random_bytes()
:
v_payload := gen_random_bytes(16);
It requires pgcrypto
extension to be enabled:
CREATE EXTENSION pgcrypto;
Important update
In some rare cases this function returns NULL
. An error occurs on line:
v_base62 := v_base62 || v_alphabet[mod(v_numeric, 62) + 1];
Example:
mod(v_numeric, 62) 'Let`s say this function returns 61.7977600000000000000000'
mod(v_numeric, 62) + 1 '62.7977600000000000000000'
v_alphabet[62.7977600000000000000000 => 63] => NULL '62.7977600000000000000000 rounds to 63, there is no element with index 63'
v_base62 || NULL 'v_base62 becomes NULL because || operator always returns NULL if at least one of the operands is null'
How to reproduce:
SELECT COUNT(*)
FROM (SELECT fn_ksuid() AS id FROM GENERATE_SERIES(1, 1000)) q
WHERE q.id IS NULL;
Solution:
v_base62 := v_base62 || v_alphabet[floor(mod(v_numeric, 62)) + 1];
Always cast the mod value to the integer part using floor()
function.
In real life the error occurs when a large number of rows are inserted at the same time.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With