Adding a UUID Column to a Persistent Table
This is just a quick snippet I’ve been meaning to post for a few weeks. A few weeks ago I needed to add a Postgres UUID column to one of my tables using Persistent. I dug around and all I found were vague, closed tickets, and old irrelevant blog posts on the Yesod site that mentioned UUIDs but didn’t give any good examples. The solution ended up being simple but I hope it helps someone else who is having this problem, or more likely, future me when I forget how this is done.
Let’s say you’ve got some schema TH that looks like:
"migrateAll"] [persistLowerCase|
share [mkPersist sqlSettings, mkMigrate
Person
name String
age Int Maybe
deriving Show |]
We want to:
- Add a UUID column to Person.
- Give it a default value so any existing columns are backfilled.
- Give it a unique index for fast lookups and guaranteed uniqueness.
- Wrap it in a distinct type to avoid confusing it with other UUIDs, much like how persistent gives us distinct types for primary keys.
Our schema TH now looks like:
"migrateAll"] [persistLowerCase|
share [mkPersist sqlSettings, mkMigrate
Person
name String
age Int Maybe
uuid PersonUUID default=uuid_generate_v4()
UniquePersonUUID uuid
deriving Show |]
In a module accessible from your schema’s you’ll also add something like:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
module MyApp.Schema where
import Control.Error
import Control.Lens
import Data.ByteString (ByteString)
import Data.UUID
import Data.Text (Text)
import qualified Data.Text as T
import System.Random
newtype PersonUUID = PersonUUID {
_personUuid :: UUID
deriving (Show, Eq, Ord, Random)
}
'PersonUUID
makeLenses '
instance PersistFieldSql PersonUUID where
= const $ SqlOther "uuid"
sqlType
instance PersistField PersonUUID where
= toPersistValueUUID personUuid
toPersistValue = fromPersistValueUUID personUuid
fromPersistValue
_ASCIIBytes :: Prism' ByteString UUID
= prism toASCIIBytes $ \bs -> note bs $ fromASCIIBytes bs
_ASCIIBytes
toPersistValueUUID :: Iso' a UUID -> a -> PersistValue
= PersistDbSpecific $ a ^. i . re _ASCIIBytes
toPersistValueUUID i a
fromPersistValueUUID :: Iso' a UUID -> PersistValue -> Either Text a
PersistDbSpecific bs) =
fromPersistValueUUID i ("Could not parse UUID" $ bs ^? _ASCIIBytes . from i
note = Left $ "Invalid value for UUID: " <> showT x
fromPersistValueUUID _ x
showT :: Show a => a -> Text
= T.pack . show showT
Let’s break this down a bit. First, we create a newtype around UUID
to distinguish the type and then derive an Iso
that can get us to and from the UUID
via makeLenses
. We also create a Prism
between ByteString and UUID. I like to read prisms as the left type variable (ByteString
) is the “wider” type and the right type variable (UUID
) is the “narrow” one. That is to say, you know you can always go from the narrow type to the wide one but not necessarily the other way. Conveniently, PersistDbSpecific
expects a ByteString
so this is exactly what we need to serialize our type to the database.
note
is a great little function from the errors
package of type e -> Maybe a -> Either e a
that upgrades a Maybe
into an Either
.
Lastly, you’ll want to make sure that the uuid-ossp
extension is enabled in your database. You can issue the command CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
to do so.