Forked from notakaos/create_function_plv8_cuid.sql
Last active
December 16, 2024 13:33
-
-
Save ashb/d0d95cfe372e1fdf8ad4e52c4016d76f to your computer and use it in GitHub Desktop.
cuid v2 for PostgreSQL with PL/v8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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