Created
July 23, 2025 15:29
-
-
Save pwelter34/ad2eb2a10cb65bbe268951e1e7d9a60e to your computer and use it in GitHub Desktop.
SQL Server ULID Implementation
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
CREATE FUNCTION [dbo].[GenerateULID] | |
( | |
@random AS BINARY(10), | |
@timestamp AS DATETIME2 = NULL | |
) | |
RETURNS VARCHAR(26) | |
AS | |
BEGIN | |
-- Crockford's base32 alphabet | |
DECLARE @alphabet VARCHAR(32) = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' | |
DECLARE @timeValue BIGINT | |
DECLARE @timeEncoded VARCHAR(10) = '' | |
-- Use current datetime if timestamp is not provided | |
IF @timestamp IS NULL | |
SET @timestamp = SYSUTCDATETIME() | |
-- Get timestamp in milliseconds since Unix epoch | |
SET @timeValue = DATEDIFF_BIG(MILLISECOND, '1970-01-01', @timestamp) | |
-- Convert timestamp to base32 (10 characters) | |
DECLARE @i INT = 1 | |
WHILE @i <= 10 | |
BEGIN | |
SET @timeEncoded = SUBSTRING(@alphabet, (@timeValue % 32) + 1, 1) + @timeEncoded | |
SET @timeValue = @timeValue / 32 | |
SET @i = @i + 1 | |
END | |
-- Convert 10 bytes to a large integer representation, process the bytes in chunks to handle the large numbers | |
DECLARE @byteValue INT | |
DECLARE @chunk1 BIGINT = 0 -- First 5 bytes | |
DECLARE @chunk2 BIGINT = 0 -- Last 5 bytes | |
-- Process first 5 bytes (40 bits) | |
SET @i = 1 | |
WHILE @i <= 5 | |
BEGIN | |
SET @byteValue = CAST(SUBSTRING(@random, @i, 1) AS INT) | |
SET @chunk1 = (@chunk1 * 256) + @byteValue | |
SET @i = @i + 1 | |
END | |
-- Process last 5 bytes (40 bits) | |
SET @i = 6 | |
WHILE @i <= 10 | |
BEGIN | |
SET @byteValue = CAST(SUBSTRING(@random, @i, 1) AS INT) | |
SET @chunk2 = (@chunk2 * 256) + @byteValue | |
SET @i = @i + 1 | |
END | |
-- Convert first chunk to base32 (8 characters) | |
DECLARE @randomEncoded1 VARCHAR(8) = '' | |
SET @i = 1 | |
WHILE @i <= 8 | |
BEGIN | |
SET @randomEncoded1 = SUBSTRING(@alphabet, (@chunk1 % 32) + 1, 1) + @randomEncoded1 | |
SET @chunk1 = @chunk1 / 32 | |
SET @i = @i + 1 | |
END | |
-- Convert second chunk to base32 (8 characters) | |
DECLARE @randomEncoded2 VARCHAR(8) = '' | |
SET @i = 1 | |
WHILE @i <= 8 | |
BEGIN | |
SET @randomEncoded2 = SUBSTRING(@alphabet, (@chunk2 % 32) + 1, 1) + @randomEncoded2 | |
SET @chunk2 = @chunk2 / 32 | |
SET @i = @i + 1 | |
END | |
RETURN @timeEncoded + @randomEncoded1 + @randomEncoded2 | |
END |
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
SELECT dbo.[GenerateULID](CRYPT_GEN_RANDOM(10), DEFAULT) AS ULID |
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
CREATE TABLE [dbo].[Transaction] | |
( | |
[Id] INT IDENTITY (1000, 1) NOT NULL, | |
-- the unique identifier (ULID) for the transaction | |
[TransactionId] NVARCHAR(50) NOT NULL CONSTRAINT [DF_Transaction_TransactionId] DEFAULT (dbo.[GenerateULID](CRYPT_GEN_RANDOM(10), DEFAULT)), | |
CONSTRAINT [PK_Transaction] PRIMARY KEY CLUSTERED ([Id] ASC), | |
INDEX [UX_Transaction_TransactionId] UNIQUE NONCLUSTERED ([TransactionId] ASC), | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment