Skip to content

Commit 6ef6ba3

Browse files
committed
Convert to extension and add timestamp parsing function
1 parent d6187a0 commit 6ef6ba3

File tree

5 files changed

+139
-87
lines changed

5 files changed

+139
-87
lines changed

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
EXTENSION = pgulid
2+
DATA = pgulid--1.0.sql
3+
PGFILEDESC = "pgULID - An ULID generation extension for PostgreSQL"
4+
5+
PG_CONFIG = pg_config
6+
PGXS := $(shell $(PG_CONFIG) --pgxs)
7+
include $(PGXS)

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ A ULID however:
2727

2828
```sql
2929
SELECT generate_ulid(); -- Output: 01D45VGTV648329YZFE7HYVGWC
30+
SELECT parse_ulid_timestamp('01D45VGTV648329YZFE7HYVGWC'); -- Output: 2019-02-20 16:23:49.35+00
3031
```
3132

3233
## Specification
@@ -35,17 +36,16 @@ Below is the current specification of ULID as implemented in this repository.
3536

3637
### Components
3738

38-
**Timestamp**
39-
- 48 bits
40-
- UNIX-time in milliseconds
41-
- Won't run out of space till the year 10895 AD
42-
43-
**Entropy**
44-
- 80 bits
39+
- Timestamp
40+
- 48 bits
41+
- UNIX-time in milliseconds
42+
- Won't run out of space till the year 10895 AD
43+
- Entropy
44+
- 80 bits
4545

4646
### String Representation
4747

48-
```
48+
```text
4949
01AN4Z07BY 79KA1307SR9X4MV3
5050
|----------| |----------------|
5151
Timestamp Entropy

pgulid--1.0.sql

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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;

pgulid.control

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
# pgulid extension
3+
comment = 'provides ULID generation and parsing'
4+
default_version = '1.0'
5+
relocatable = true
6+
requires = pgcrypto

pgulid.sql

Lines changed: 0 additions & 79 deletions
This file was deleted.

0 commit comments

Comments
 (0)