|
| 1 | +-- complain if script is sourced in psql, rather than via CREATE EXTENSION |
| 2 | +\echo Use "CREATE EXTENSION pgulid" to load this file.\quit |
| 3 | + |
| 4 | +-- pgulid is based on OK Log's Go implementation of the ULID spec |
| 5 | +-- |
| 6 | +-- https://github.com/oklog/ulid |
| 7 | +-- https://github.com/ulid/spec |
| 8 | +-- |
| 9 | +-- Copyright 2016 The Oklog Authors |
| 10 | +-- Licensed under the Apache License, Version 2.0 (the "License"); |
| 11 | +-- you may not use this file except in compliance with the License. |
| 12 | +-- You may obtain a copy of the License at |
| 13 | +-- |
| 14 | +-- http://www.apache.org/licenses/LICENSE-2.0 |
| 15 | +-- |
| 16 | +-- Unless required by applicable law or agreed to in writing, software |
| 17 | +-- distributed under the License is distributed on an "AS IS" BASIS, |
| 18 | +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 19 | +-- See the License for the specific language governing permissions and |
| 20 | +-- limitations under the License. |
| 21 | + |
| 22 | +CREATE OR REPLACE FUNCTION generate_ulid() |
| 23 | +RETURNS TEXT |
| 24 | +AS $$ |
| 25 | +DECLARE |
| 26 | + -- Crockford's Base32 |
| 27 | + encoding BYTEA = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; |
| 28 | + timestamp BYTEA = E'\\000\\000\\000\\000\\000\\000'; |
| 29 | + |
| 30 | + unix_time BIGINT; |
| 31 | + ulid BYTEA; |
| 32 | +BEGIN |
| 33 | + -- 6 timestamp bytes |
| 34 | + unix_time = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT; |
| 35 | + timestamp = SET_BYTE(timestamp, 0, (unix_time >> 40)::BIT(8)::INTEGER); |
| 36 | + timestamp = SET_BYTE(timestamp, 1, (unix_time >> 32)::BIT(8)::INTEGER); |
| 37 | + timestamp = SET_BYTE(timestamp, 2, (unix_time >> 24)::BIT(8)::INTEGER); |
| 38 | + timestamp = SET_BYTE(timestamp, 3, (unix_time >> 16)::BIT(8)::INTEGER); |
| 39 | + timestamp = SET_BYTE(timestamp, 4, (unix_time >> 8)::BIT(8)::INTEGER); |
| 40 | + timestamp = SET_BYTE(timestamp, 5, unix_time::BIT(8)::INTEGER); |
| 41 | + |
| 42 | + -- 10 entropy bytes |
| 43 | + ulid = timestamp || gen_random_bytes(10); |
| 44 | + |
| 45 | + -- Encode the timestamp |
| 46 | + RETURN CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 0) & 224) >> 5)) |
| 47 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 0) & 31))) |
| 48 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 1) & 248) >> 3)) |
| 49 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 1) & 7) << 2) | ((GET_BYTE(ulid, 2) & 192) >> 6))) |
| 50 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 2) & 62) >> 1)) |
| 51 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 2) & 1) << 4) | ((GET_BYTE(ulid, 3) & 240) >> 4))) |
| 52 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 3) & 15) << 1) | ((GET_BYTE(ulid, 4) & 128) >> 7))) |
| 53 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 4) & 124) >> 2)) |
| 54 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 4) & 3) << 3) | ((GET_BYTE(ulid, 5) & 224) >> 5))) |
| 55 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 5) & 31))) |
| 56 | + -- Encode the entropy |
| 57 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 6) & 248) >> 3)) |
| 58 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 6) & 7) << 2) | ((GET_BYTE(ulid, 7) & 192) >> 6))) |
| 59 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 7) & 62) >> 1)) |
| 60 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 7) & 1) << 4) | ((GET_BYTE(ulid, 8) & 240) >> 4))) |
| 61 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 8) & 15) << 1) | ((GET_BYTE(ulid, 9) & 128) >> 7))) |
| 62 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 9) & 124) >> 2)) |
| 63 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 9) & 3) << 3) | ((GET_BYTE(ulid, 10) & 224) >> 5))) |
| 64 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 10) & 31))) |
| 65 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 11) & 248) >> 3)) |
| 66 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 11) & 7) << 2) | ((GET_BYTE(ulid, 12) & 192) >> 6))) |
| 67 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 12) & 62) >> 1)) |
| 68 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 12) & 1) << 4) | ((GET_BYTE(ulid, 13) & 240) >> 4))) |
| 69 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 13) & 15) << 1) | ((GET_BYTE(ulid, 14) & 128) >> 7))) |
| 70 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 14) & 124) >> 2)) |
| 71 | + || CHR(GET_BYTE(encoding, ((GET_BYTE(ulid, 14) & 3) << 3) | ((GET_BYTE(ulid, 15) & 224) >> 5))) |
| 72 | + || CHR(GET_BYTE(encoding, (GET_BYTE(ulid, 15) & 31))); |
| 73 | +END |
| 74 | +$$ |
| 75 | +LANGUAGE plpgsql |
| 76 | +VOLATILE; |
| 77 | + |
| 78 | +CREATE OR REPLACE FUNCTION parse_ulid_timestamp(ulid TEXT) RETURNS TIMESTAMP |
| 79 | +AS $$ |
| 80 | +DECLARE |
| 81 | + -- Crockford's Base32 |
| 82 | + -- Drop the 0 because strpos() returns 0 for not-found |
| 83 | + -- We've pre-validated already, so this is safe |
| 84 | + encoding TEXT = '123456789ABCDEFGHJKMNPQRSTVWXYZ'; |
| 85 | + ts BIGINT; |
| 86 | + v CHAR[]; |
| 87 | +BEGIN |
| 88 | + IF ulid IS NULL THEN |
| 89 | + RETURN null; |
| 90 | + END IF; |
| 91 | + |
| 92 | + ulid = upper(ulid); |
| 93 | + |
| 94 | + IF NOT ulid ~ '^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$' THEN |
| 95 | + RAISE EXCEPTION 'Invalid ULID: %', ulid; |
| 96 | + END IF; |
| 97 | + |
| 98 | + -- first 10 ULID characters are the timestamp |
| 99 | + v = regexp_split_to_array(substring(ulid for 10), ''); |
| 100 | + |
| 101 | + -- base32 is 5 bits / character |
| 102 | + -- posix milliseconds (6 bytes) |
| 103 | + ts = (strpos(encoding, v[1])::bigint << 45) |
| 104 | + + (strpos(encoding, v[2])::bigint << 40) |
| 105 | + + (strpos(encoding, v[3])::bigint << 35) |
| 106 | + + (strpos(encoding, v[4])::bigint << 30) |
| 107 | + + (strpos(encoding, v[5]) << 25) |
| 108 | + + (strpos(encoding, v[6]) << 20) |
| 109 | + + (strpos(encoding, v[7]) << 15) |
| 110 | + + (strpos(encoding, v[8]) << 10) |
| 111 | + + (strpos(encoding, v[9]) << 5) |
| 112 | + + strpos(encoding, v[10]); |
| 113 | + |
| 114 | + RETURN to_timestamp(ts / 1000.0); |
| 115 | +END |
| 116 | +$$ |
| 117 | +LANGUAGE plpgsql |
| 118 | +IMMUTABLE; |
0 commit comments