Skip to content

Instantly share code, notes, and snippets.

@disintegrator
Last active March 12, 2025 20:51
Show Gist options
  • Save disintegrator/b9b073e7cf73ce114aac314e8e7d2a46 to your computer and use it in GitHub Desktop.
Save disintegrator/b9b073e7cf73ce114aac314e8e7d2a46 to your computer and use it in GitHub Desktop.
An attempt at making a ulid data type in postgres that is just a bytea with casts
OS: Ubuntu 24.04.2 LTS aarch64
Host: VMware20,1 1
Kernel: 6.8.0-53-generic
CPU: Apple M2 Pro
GPU: Apple M2 Pro
postgres=# EXPLAIN ANALYSE SELECT generate_ulid() FROM generate_series(1, 10000000);
                                                               QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..325000.00 rows=10000000 width=32) (actual time=471.340..10482.295 rows=10000000 loops=1)
 Planning Time: 0.424 ms
 JIT:
   Functions: 3
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 3.572 ms (Deform 0.000 ms), Inlining 0.000 ms, Optimization 2.533 ms, Emission 12.681 ms, Total 18.785 ms
 Execution Time: 10736.979 ms
(7 rows)

postgres=# EXPLAIN ANALYSE SELECT gen_random_uuid() FROM generate_series(1, 10000000);
                                                              QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..125000.00 rows=10000000 width=16) (actual time=400.925..7509.077 rows=10000000 loops=1)
 Planning Time: 0.109 ms
 JIT:
   Functions: 3
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 0.359 ms (Deform 0.000 ms), Inlining 0.000 ms, Optimization 0.198 ms, Emission 2.376 ms, Total 2.933 ms
 Execution Time: 7738.440 ms
(7 rows)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
DROP DOMAIN IF EXISTS ulid CASCADE;
CREATE DOMAIN ulid AS bytea;
CREATE OR REPLACE FUNCTION generate_ulid()
RETURNS ulid
AS
$$
-- concatenate(6 timestamp bytes, 10 entropy bytes)
SELECT DECODE(LPAD(TO_HEX((EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT), 12, '0'), 'hex') || gen_random_bytes(10);
$$
LANGUAGE sql
VOLATILE;
CREATE OR REPLACE FUNCTION ulid_to_base32(id ulid) RETURNS TEXT AS
$$
DECLARE
encoding BYTEA = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
output TEXT = '';
ulid BYTEA = id;
BEGIN
-- Encode the timestamp
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 0) & 224) >> 5));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 0) & 31)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 1) & 248) >> 3));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 1) & 7) << 2) | ((GET_BYTE(ulid, 2) & 192) >> 6)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 2) & 62) >> 1));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 2) & 1) << 4) | ((GET_BYTE(ulid, 3) & 240) >> 4)));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 3) & 15) << 1) | ((GET_BYTE(ulid, 4) & 128) >> 7)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 4) & 124) >> 2));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 4) & 3) << 3) | ((GET_BYTE(ulid, 5) & 224) >> 5)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 5) & 31)));
-- Encode the entropy
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 6) & 248) >> 3));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 6) & 7) << 2) | ((GET_BYTE(ulid, 7) & 192) >> 6)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 7) & 62) >> 1));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 7) & 1) << 4) | ((GET_BYTE(ulid, 8) & 240) >> 4)));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 8) & 15) << 1) | ((GET_BYTE(ulid, 9) & 128) >> 7)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 9) & 124) >> 2));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 9) & 3) << 3) | ((GET_BYTE(ulid, 10) & 224) >> 5)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 10) & 31)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 11) & 248) >> 3));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 11) & 7) << 2) | ((GET_BYTE(ulid, 12) & 192) >> 6)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 12) & 62) >> 1));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 12) & 1) << 4) | ((GET_BYTE(ulid, 13) & 240) >> 4)));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 13) & 15) << 1) | ((GET_BYTE(ulid, 14) & 128) >> 7)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 14) & 124) >> 2));
output = output || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 14) & 3) << 3) | ((GET_BYTE(ulid, 15) & 224) >> 5)));
output = output || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 15) & 31)));
RETURN output;
END
$$
LANGUAGE plpgsql
VOLATILE;
CREATE CAST (ulid AS bytea)
WITH INOUT
AS IMPLICIT;
CREATE CAST (ulid AS TEXT)
WITH FUNCTION ulid_to_base32
AS IMPLICIT;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment