Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions uuid/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.3.17

- Added support for UUIDv7 in `Data.UUID.V7`.

1.3.16

- Support GHC-8.6.5...9.10.1
Expand Down
70 changes: 70 additions & 0 deletions uuid/src/Data/UUID/V7.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- |
-- Module : Data.UUID.V7
-- Copyright : (c) Taylor Fausak
--
-- License : BSD-style
--
-- Maintainer : aslatter@gmail.com
-- Stability : experimental
-- Portability : portable
--
--
-- This module implements Version 7 UUIDs as specified
-- in RFC 9562 accessible here: <https://datatracker.ietf.org/doc/html/rfc9562>.
--
-- These UUIDs are partly time-ordered, which improves database locality.

module Data.UUID.V7 ( generate, build ) where

import Data.Bits ( (.&.), (.|.), shift )
import qualified Data.ByteString as ByteString
import qualified Data.Int as Int
import qualified Data.Time.Clock.System as Time
import qualified Data.UUID.Types as UUID
import qualified Data.Word as Word
import qualified System.Entropy as Entropy

-- | Generates a 'UUID' using the current time (from 'Time.getSystemTime') and
-- random data (from 'Entropy.getEntropy').
--
-- @since 1.3.17
generate :: IO UUID.UUID
generate = do
t <- Time.getSystemTime
-- Note that we only need 74 bits (12 + 62) of randomness. That's a little
-- more than 9 bytes (72 bits), so we have to request 10 bytes (80 bits) of
-- entropy. The extra 6 bits are discarded.
b <- Entropy.getEntropy 10
pure $
let u8_u64 = fromIntegral :: Word.Word8 -> Word.Word64
f = shift . u8_u64 . ByteString.index b
r = f 0 0 + f 1 8
s = f 2 0 + f 3 8 + f 4 16 + f 5 24 + f 6 32 + f 7 40 + f 8 48 + f 9 56
in build t r s

-- | Builds a 'UUID' using the provided fields. Typically you will want to use
-- the 'generate' function instead.
--
-- @since 1.3.17
build ::
-- | Corresponds to the @unix_ts_ms@ field.
Time.SystemTime ->
-- | Corresponds to the @rand_a@ field. Only the low 12 bits are used.
Word.Word64 ->
-- | Corresponds to the @rand_b@ field. Only the low 62 bits are used.
Word.Word64 ->
UUID.UUID
build t r s =
let i64_u64 = fromIntegral :: Int.Int64 -> Word.Word64
u32_u64 = fromIntegral :: Word.Word32 -> Word.Word64
unix_ts_ms =
shift
( (i64_u64 (Time.systemSeconds t) * 1000)
+ u32_u64 (div (Time.systemNanoseconds t) 1000000)
)
16
ver = shift 0x7 12 :: Word.Word64
rand_a = r .&. 0x0fff -- 0x0fff = 2^12 - 1
var = shift 0x2 62 :: Word.Word64
rand_b = s .&. 0x3fffffffffffffff -- 0x3fffffffffffffff = 2^62 - 1
in UUID.fromWords64 (unix_ts_ms .|. ver .|. rand_a) (var .|. rand_b)
32 changes: 31 additions & 1 deletion uuid/tests/TestUUID.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import qualified Data.ByteString.Lazy as BL
import Data.Char (ord)
import Data.List (nub, (\\))
import Data.Maybe
import qualified Data.Time as Time
import qualified Data.Time.Clock.System as Time
import Data.Word
import qualified Data.UUID as U
import qualified Data.UUID.V1 as U
import qualified Data.UUID.V3 as U3
import qualified Data.UUID.V5 as U5
import qualified Data.UUID.V7 as U7

import Test.QuickCheck ( Arbitrary(arbitrary), choose )
import Test.Tasty ( TestTree, testGroup, defaultMain )
Expand Down Expand Up @@ -65,6 +68,32 @@ test_v5 =
where name = map (fromIntegral . ord) "www.widgets.com" :: [Word8]
uV5 = fromJust $ U.fromString "21f7f8de-8051-5b89-8680-0195ef798b6a"

test_v7 :: Test
test_v7 = testGroup
"version 7"
[ testGroup
"generate"
[ testCase "works" $ do
uuid <- U7.generate
length (U.toString uuid) @?= 36
],
testGroup
"build"
[ testCase "works with nil" $ do
let epoch = Time.MkSystemTime 0 0
let uuid = U7.build epoch 0 0
U.toString uuid @?= "00000000-0000-7000-8000-000000000000",
testCase "works with test vector" $ do
-- https://datatracker.ietf.org/doc/html/rfc9562#name-example-of-a-uuidv7-value
let time =
Time.utcToSystemTime
. Time.localTimeToUTC (Time.hoursToTimeZone (-5))
. Time.LocalTime (Time.fromGregorian 2022 2 22)
$ Time.TimeOfDay 14 22 22
let uuid = U7.build time 0xcc3 0x18C4DC0C0C07398F
U.toString uuid @?= "017f22e2-79b0-7cc3-98c4-dc0c0c07398f"
]
]

prop_randomsValid :: Test
prop_randomsValid = testProperty "Random valid" randomsValid
Expand Down Expand Up @@ -101,7 +130,8 @@ main = do
test_null,
test_v1 v1s,
test_v3,
test_v5
test_v5,
test_v7
]
, [ prop_randomsValid,
prop_v3NotNull,
Expand Down
2 changes: 2 additions & 0 deletions uuid/uuid.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ library
Data.UUID.V3
Data.UUID.V4
Data.UUID.V5
Data.UUID.V7

other-modules:
Data.UUID.Named
Expand All @@ -86,6 +87,7 @@ test-suite testuuid
base
, bytestring
, random
, time
, uuid

-- deps w/o inherited constraints
Expand Down