Skip to content

Instantly share code, notes, and snippets.

@ashb
Forked from notakaos/create_function_plv8_cuid.sql
Last active June 28, 2024 12:57
Show Gist options
  • Save ashb/d0d95cfe372e1fdf8ad4e52c4016d76f to your computer and use it in GitHub Desktop.
Save ashb/d0d95cfe372e1fdf8ad4e52c4016d76f to your computer and use it in GitHub Desktop.
cuid v2 for PostgreSQL with PL/v8
-- Ported from https://github.com/paralleldrive/cuid2/blob/53e246b0919c8123e492e6b6bbab41fe66f4b462/src/index.js
-- Adjusted by Ash Berlin to use pgcrypto gen_random_bytes instead of Math.random
-- Host fingerprint adjusted to include hostname and backend pid
-- This gist is MIT licensed.
-- Add the "plv8" extension
CREATE EXTENSION IF NOT EXISTS "plv8";
-- Add the "pgcrypto" extension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE OR REPLACE FUNCTION cuidv2(IN firstletter varchar(1) DEFAULT '' ) RETURNS text AS $$
const defaultLength = 24;
const bigLength = 32;
const createEntropy = (length) => {
// Use pgcrypto
return plv8.execute("SELECT gen_random_bytes($1) as entropy", [length])[0].entropy;
};
const randomInt = (max) => {
const byteLen = Math.ceil((max+1)/256.0);
const bytes = plv8.execute("SELECT gen_random_bytes($1) as b", [byteLen])[0].b;
let ret = 0
for (let byte of bytes) {
ret << 8;
ret += byte;
}
return ret % max
}
let byteHash = (input) => {
try {
return plv8.execute("SELECT digest($1, 'sha3-512') as bytes", [input])[0].bytes;
}
catch (e) {
// sha3-512 not available, fallback to just sha512
plv8.elog(NOTICE, 'sha3-512 not available, falling back to sha512')
byteHash = (input) => plv8.execute("SELECT digest($1, 'sha512') as bytes", [input])[0].bytes;
return byteHash(input)
}
};
/*
* Adapted from https://github.com/juanelas/bigint-conversion
* MIT License Copyright (c) 2018 Juan Hernández Serrano
*/
function bufToBigInt(bytea) {
let bits = 8n;
let value = BigInt(0n);
for (const i of bytea) {
value = (value << bits) + BigInt(i);
}
return value;
}
const hash = (input = "") => {
// Drop the first character because it will bias the histogram
// to the left.
return bufToBigInt(byteHash(input)).toString(36).slice(1);
};
const alphabet = Array.from({ length: 26 }, (x, i) =>
String.fromCharCode(i + 97)
);
const randomLetter = (random) =>
alphabet[randomInt(alphabet.length)];
/*
This is a fingerprint of the host environment. It is used to help
prevent collisions when generating ids in a distributed system.
If no global object is available, you can pass in your own, or fall back
on a random string.
*/
const createFingerprint = ({
globalObj = typeof global !== "undefined"
? global
: typeof window !== "undefined"
? window
: {},
} = {}) => {
const hostInfo = plv8.execute(`select pg_backend_pid()::varchar as pid, coalesce(inet_server_addr()::varchar, '') as hostname`)[0];
const globals = Object.keys(globalObj).toString();
const sourceString = (globals.length ? globals : '') +
hostInfo.hostname + hostInfo.pid +
createEntropy(bigLength);
return hash(sourceString).substring(0, bigLength);
};
const createCounter = (count) => () => {
// Ensure we never get to loss of integer precision (huge number, but better safe than sorry)
if (count >= Number.MAX_SAFE_INTEGER) count=0;
return count++;
};
// ~22k hosts before 50% chance of initial counter collision
// with a remaining counter range of 9.0e+15 in JavaScript.
const initialCountMax = 476782367;
const init = ({
// We also use the host fingerprint, current time, and a session counter.
counter = createCounter(Math.floor(Math.random() * initialCountMax)),
length = defaultLength,
fingerprint = ""
} = {}) => {
return function cuid2(firstLetter) {
if (!firstLetter) {
firstLetter = randomLetter();
}
if (!fingerprint) {
fingerprint = createFingerprint()
}
// If we're lucky, the `.toString(36)` calls may reduce hashing rounds
// by shortening the input to the hash function a little.
const time = Date.now().toString(36);
const count = counter().toString(36);
// The salt should be long enough to be globally unique across the full
// length of the hash. For simplicity, we use the same length as the
// intended id output.
// Array.prototype.map, as bytea is a Unit8Array, and it's .map returns an array of the same type, not what we want!
const salt = Array.prototype.map.call(createEntropy(length), String.fromCharCode).join('');
const hashInput = time + salt + count + fingerprint;
let hashed = hash(hashInput);
let result = `${firstLetter + hash(hashInput).substring(1, length)}`;
return result
};
};
if (typeof plv8.__cuidv2 === "undefined") {
plv8.__cuidv2 = { createId: init() };
}
let res = plv8.__cuidv2.createId(firstletter);
return res;
$$ LANGUAGE plv8 STRICT;
-- select cuidv2();
-- select cuidv2('c');
test=# select cuidv2(); -- random first letter
cuidv2
--------------------------
tf6cu8w4nbnu7gte0bdtgucy
(1 row)
test=# select cuidv2('c'); -- always start with a 'c' if that is your preference.
cuidv2
--------------------------
c6o4l51nf5wzk61t7ct4e4pa
(1 row)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment