TheKojuEffect
TheKojuEffect

Reputation: 21081

Base58 Encoder function in PostgreSQL

I found this MySQL function for Base58 Encoder in a Github Gist.

DELIMITER $$

CREATE FUNCTION base58_encode (num int) RETURNS varchar(255)
  DETERMINISTIC

BEGIN
  DECLARE alphabet varchar(255);
  DECLARE base_count int DEFAULT 0;
  DECLARE encoded varchar(255);
  DECLARE divisor DECIMAL(10,4);
  DECLARE mode int DEFAULT 0;

  SET alphabet = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
  SET base_count = CHAR_LENGTH(alphabet);
  SET encoded = "";

  WHILE num  >= base_count DO
    SET divisor = num / base_count;
    SET mode = (num - (base_count* TRUNCATE(divisor,0)));
    SET encoded = CONCAT(SUBSTRING(alphabet FROM mode+1 FOR 1), encoded);
    SET num = TRUNCATE(divisor,0);
  END WHILE;

  SET encoded = CONCAT(SUBSTRING(alphabet FROM num+1 FOR 1), encoded);

  RETURN (encoded);

END

I am new to PostgreSQL and having difficulty converting above function to PostgreSQL function.

How would be the equivalent PostgreSQL function of above SQL snippet for Base58 Encoder?

Upvotes: 5

Views: 3526

Answers (6)

Jonathan
Jonathan

Reputation: 4689

Here is a function to convert a uuid to base58. Since postgres can't generally handle the BigInt conversion from UUID, you can use an extension to put the JS directly in postgres:

CREATE EXTENSION IF NOT EXISTS plv8;

CREATE OR REPLACE FUNCTION uuid_to_base58(uuid uuid)
  RETURNS text AS $$
    const base58Alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
    let bigintValue = BigInt('0x' + uuid.toString().replace(/-/g, ''));
    let base58 = '';
    while (bigintValue > BigInt(0)) {
        const remainder = bigintValue % BigInt(base58Alphabet.length);
        bigintValue /= BigInt(base58Alphabet.length);
        base58 = base58Alphabet.charAt(Number(remainder)) + base58;
    }
    return base58;
$$ LANGUAGE plv8 IMMUTABLE;

Good for short IDs where you could convert back and forth:

d0269693-c9e5-4a0d-998e-a2e1579accc3  --->  Sho37CJBR5FqrV4FXPWhGN

Upvotes: 2

Mykhailo Tiutin
Mykhailo Tiutin

Reputation: 21

I've modified a sample for the Postgres 9.x above. It's proved to work with large numbers (blockchain addresses, for example)

CREATE OR REPLACE FUNCTION base58_encode (num numeric)
RETURNS text AS
$body$
declare
  --alphabet text = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
  alphabet text[] = array[
    '1','2','3','4','5','6','7','8','9', 
    'A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z',
    'a','b','c','d','e','f','g','h','i','j','k','m','n','o','p','q','r','s','t','u','v','w','x','y','z'
   
  ];
  cnt integer = 58;
  dst text = '';
  _mod numeric;
begin
  while (num >= cnt) loop
    _mod = num % cnt;
    num = (num - _mod) / cnt;
    dst = alphabet[_mod+1] || dst;
  end loop;
  return alphabet[num+1] || dst;
end;
$body$
LANGUAGE 'plpgsql'
IMMUTABLE
CALLED ON NULL INPUT
COST 100;

Upvotes: 2

Krtko
Krtko

Reputation: 1065

To continue the completeness, if you need to decode to a UUID (Int16) I built on @mike-blackwell's answer to do that. Gist

I would love some feedback and improvements. I have to imagine theres a better way to go from a 128 bit numeric to a UUID

Code:

CREATE OR REPLACE FUNCTION base58_decode(encoded_id VARCHAR(22))
    RETURNS UUID AS $$
DECLARE
    -- Bitcoin base58 alphabet
    alphabet CHAR(58) := '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
    c CHAR(1) := null;
    p INT := null;
    raw_num NUMERIC := 0;
    uuid_str VARCHAR(32);
BEGIN
    /*
    Parses a UUID encoded with the Bitcoin base58 standard
    Use sparingly, any application connecting to the database should handle decoding the ID itself
    */

    -- Decode id to numeric
    FOR i IN 1..CHAR_LENGTH(encoded_id) LOOP
        c = SUBSTRING(encoded_id FROM i FOR 1);
        p = POSITION(c IN alphabet);
        raw_num = (raw_num * 58) + (p - 1);
    END LOOP;

    -- Parse NUMERIC into bytes
    -- There must be a better way to go from a NUMERIC -> UUID
    uuid_str := '';
    FOR i IN 0..31 LOOP
        uuid_str = CONCAT(uuid_str, TO_HEX(MOD(raw_num, 16)::INT));
        raw_num = DIV(raw_num, 16);
    END LOOP;

    return REVERSE(uuid_str)::UUID;
END;$$
LANGUAGE PLPGSQL;

Upvotes: 0

Mike Blackwell
Mike Blackwell

Reputation: 497

For completeness here's a quick and dirty swipe at the inverse base58_decode() function:

CREATE OR REPLACE FUNCTION base58_decode(str VARCHAR(255))
  RETURNS BIGINT AS $$
DECLARE
  alphabet VARCHAR(255);
  c CHAR(1);
  p INT;
  v BIGINT;
BEGIN
  alphabet := '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
  v := 0;
  FOR i IN 1..char_length(str) LOOP
    c := substring(str FROM i FOR 1);
    -- This is probably wildly inefficient, but we're just using this function for diagnostics...
    p := position(c IN alphabet);
    IF p = 0 THEN
      RAISE 'Illegal base58 character ''%'' in ''%''', c, str;
    END IF;
    v := (v * 58) + (p - 1);
  END LOOP;
  RETURN v;
END;$$
LANGUAGE PLPGSQL;

Upvotes: 3

Postgres 9.x

CREATE OR REPLACE FUNCTION base58_encode (num bigint)
RETURNS text AS
$body$
declare
  --alphabet text = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
  alphabet text[] = array[
    '1','2','3','4','5','6','7','8','9', 
    'a','b','c','d','e','f','g','h','i','j','k','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
    'A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z'
  ];
  cnt integer = 58;
  dst text = '';
  mod integer;
begin
  while (num >= cnt) loop
    num = num / cnt;
    mod = num % cnt + 1;
    dst = alphabet[mod] || dst;
  end loop;

  return alphabet[num] || dst;
end;
$body$
LANGUAGE 'plpgsql'
IMMUTABLE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;

Upvotes: 0

TheKojuEffect
TheKojuEffect

Reputation: 21081

Equivalent function in PostgreSQL that I came up with is as follows.

CREATE FUNCTION base58_encode(num INT)
  RETURNS VARCHAR(255) AS $encoded$

DECLARE
  alphabet   VARCHAR(255);
  base_count INT DEFAULT 0;
  encoded    VARCHAR(255);
  divisor    DECIMAL(10, 4);
  mod        INT DEFAULT 0;

BEGIN
  alphabet := '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ';
  base_count := char_length(alphabet);
  encoded := '';

  WHILE num >= base_count LOOP
    divisor := num / base_count;
    mod := (num - (base_count * trunc(divisor, 0)));
    encoded := concat(substring(alphabet FROM mod + 1 FOR 1), encoded);
    num := trunc(divisor, 0);
  END LOOP;

  encoded = concat(substring(alphabet FROM num + 1 FOR 1), encoded);

  RETURN (encoded);

END; $encoded$
LANGUAGE PLPGSQL;

Upvotes: 5

Related Questions