Skip to content

Instantly share code, notes, and snippets.

@srfrog
Created November 27, 2019 03:49
Show Gist options
  • Save srfrog/ef8c9a3a4282b865fb93f429d671d63b to your computer and use it in GitHub Desktop.
Save srfrog/ef8c9a3a4282b865fb93f429d671d63b to your computer and use it in GitHub Desktop.

Revisions

  1. srfrog created this gist Nov 27, 2019.
    134 changes: 134 additions & 0 deletions cuid.sql
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,134 @@
    -- Collision-resistant ids optimized for horizontal scaling and performance, for PL/PgSQL.
    -- Based on https://github.com/ericelliott/cuid
    -- Version 1.0.0

    -- Usage: SELECT cuid();

    -- BEGIN CONFIG ---

    -- Put a unique host ID (int) here per server instance.
    -- Once set, this value should not be changed.
    SET plpgsql.cuid_hostid = "897892";

    -- Create the sequence used to track ID counts. Used in _safeCounter()
    CREATE SEQUENCE IF NOT EXISTS "cuid_seq";

    -- END OF CONFIG ---

    -- Get the next ID in counter. The sequence will reset after max_id is reached.
    CREATE OR REPLACE FUNCTION _safeCounter() RETURNS VARCHAR AS $$
    DECLARE
    seq_id bigint;
    max_id int := 1679616;
    BEGIN
    SELECT nextval('cuid_seq') INTO seq_id;

    IF seq_id > max_id THEN
    SELECT setval('cuid_seq', 0) INTO seq_id;
    END IF;

    RETURN pad(base36enc(seq_id), 4);
    END;
    $$ LANGUAGE PLPGSQL;

    -- Uses the backend PID and a unique host ID to generate a fingerprint.
    -- Unfortunately getting the hostname of a Pg server is rather expensive so we opt for
    -- a static unique host ID.
    CREATE OR REPLACE FUNCTION _fingerprint() RETURNS VARCHAR AS $$
    DECLARE
    padding int := 2;
    ret varchar;
    BEGIN
    ret := pad(base36enc(pg_backend_pid()), padding);
    ret := ret || pad(base36enc(current_setting('plpgsql.cuid_hostid')::bigint), padding);
    RETURN ret;
    END;
    $$ LANGUAGE PLPGSQL;

    -- Encodes a base-10 bigint to a base-36 string representation.
    CREATE OR REPLACE FUNCTION base36enc(num bigint) RETURNS VARCHAR AS $$
    DECLARE
    base36 char[];
    ret varchar;
    val bigint;
    tmp bigint;
    BEGIN
    base36 := 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'
    ];

    IF num ISNULL THEN
    RETURN NULL;
    END IF;

    val := num;
    ret := '';
    LOOP
    tmp := val % 36 + 1;
    ret := base36[tmp] || ret;
    val := val / 36;
    IF val = 0 THEN
    exit;
    END IF;
    END LOOP;

    IF num < 0 THEN
    ret := '-' || ret;
    END IF;

    RETURN ret;
    END;
    $$ LANGUAGE PLPGSQL;

    -- Adds zero-padding of a size to a num string. If the string is longer than size
    -- it gets truncated to the left.
    CREATE OR REPLACE FUNCTION pad(num varchar, size int) RETURNS VARCHAR AS $$
    BEGIN
    RETURN lpad(right(num, size), size, '0');
    END;
    $$ LANGUAGE PLPGSQL;

    -- Get value of current timestamp encoded as base-36
    CREATE OR REPLACE FUNCTION _timestamp() RETURNS VARCHAR AS $$
    DECLARE
    now_ts bigint;
    BEGIN
    SELECT floor(extract(EPOCH FROM clock_timestamp()))
    INTO now_ts;

    RETURN base36enc(now_ts);
    END;
    $$ LANGUAGE PLPGSQL;

    -- Get a random base-36 valid string of size block_size.
    CREATE OR REPLACE FUNCTION _randomBlock() RETURNS VARCHAR AS $$
    DECLARE
    base36 varchar := '0123456789abcdefghijklmnopqrstuvwxyz';
    block_size int := 4;
    val varchar;
    BEGIN
    SELECT array_to_string(ARRAY(
    SELECT substring(base36 FROM (random() * 36)::int FOR 1)
    FROM generate_series(1, block_size)), '', '0')
    INTO val;

    RETURN val;
    END;
    $$ LANGUAGE PLPGSQL;

    -- Generates a new Collision-resistant ID (CUID)
    CREATE OR REPLACE FUNCTION cuid() RETURNS VARCHAR AS $$
    DECLARE
    ret varchar;
    BEGIN
    ret := 'c';
    ret := ret || _timestamp();
    ret := ret || _safeCounter();
    ret := ret || _fingerprint();
    ret := ret || _randomBlock();
    ret := ret || _randomBlock();
    RETURN ret;
    END;
    $$ LANGUAGE PLPGSQL;