This is probably a stupid question and I'm somehow overlooking existing content with sub-par Google-fu skills, but is there a way to create a new Text field using Persistent, with a uniqueness constraint on that field, whereby uniqueness is case-insensitive? For example, say I want to create a Username field that is unique with no duplicates so that four different users cannot create the Satan, SATAN, satan, and SaTaN username records?
Or would I have to lean on Postgres-specific features and use raw SQL to achieve this? Or is it perhaps accomplished in esqueleto without using raw SQL?
Update 1:
I tried addiing @MaxGabriel's revision as src/ModelTypes.hs
in a newly scaffolded Yesod site & importing it in src/Model.hs
. To this I seemed to have to add import Database.Persist.Sql
to get rid of one compiler error, now I get this error 3 times while running yesod devel
:
Not in scope: type constructor or class ‘Text’
Perhaps you meant ‘T.Text’ (imported from Data.Text)
Haven't yet updated the scaffodled User model (used by the dummy authentication) in config/models.persistentmodels
to use the new Username
type ...
User
ident Text
password Text Maybe
UniqueUser ident
deriving Typeable
... but on previous attempts to simply change ident
to use citext
, it would work for inserting a new record into the db, but then seemed to balk at retrieving and converting the type of that record when trying to authenticate a user.
Update 2:
Output after adding import Data.Text (Text)
to ModelTypes.hs
>>> stack exec -- yesod devel
Yesod devel server. Enter 'quit' or hit Ctrl-C to quit.
Application can be accessed at:
http://localhost:3000
https://localhost:3443
If you wish to test https capabilities, you should set the following variable:
export APPROOT=https://localhost:3443
uniqueci> configure (lib)
Configuring uniqueci-0.0.0...
uniqueci> build (lib)
Preprocessing library for uniqueci-0.0.0..
Building library for uniqueci-0.0.0..
[ 4 of 13] Compiling ModelTypes
/zd/pj/yesod/uniqueci/src/ModelTypes.hs:16:10: error:
• Illegal instance declaration for ‘PersistField (CI Text)’
(All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Use FlexibleInstances if you want to disable this.)
• In the instance declaration for ‘PersistField (CI Text)’
|
16 | instance PersistField (CI Text) where
| ^^^^^^^^^^^^^^^^^^^^^^
/zd/pj/yesod/uniqueci/src/ModelTypes.hs:21:10: error:
• Illegal instance declaration for ‘PersistFieldSql (CI Text)’
(All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Use FlexibleInstances if you want to disable this.)
• In the instance declaration for ‘PersistFieldSql (CI Text)’
|
21 | instance PersistFieldSql (CI Text) where
| ^^^^^^^^^^^^^^^^^^^^^^^^^
-- While building package uniqueci-0.0.0 using:
/zd/hngnr/.stack_sym_ngnr/setup-exe-cache/x86_64-linux-tinfo6/Cabal-simple_mPHDZzAJ_3.0.1.0_ghc-8.8.4 --builddir=.stack-work/dist/x86_64-linux-tinfo6/Cabal-3.0.1.0 build lib:uniqueci --ghc-options ""
Process exited with code: ExitFailure 1
Type help for available commands. Press enter to force a rebuild.
Update 3:
After adding {-# LANGUAGE FlexibleInstances #-}
to ModelType.hs
, the above error goes away. Upon trying to use the new Username
type in the scaffolded User
model like so
-- config/models.persistentmodels
User
ident Username -- default is ident Text
password Text Maybe
UniqueUser ident
deriving Typeable
Email
email Text
userId UserId Maybe
verkey Text Maybe
UniqueEmail email
Comment json -- Adding "json" causes ToJSON and FromJSON instances to be derived.
message Text
userId UserId Maybe
deriving Eq
deriving Show
a new error occurred:
[ 2 of 13] Compiling Model [config/models.persistentmodels changed]
[ 7 of 13] Compiling Foundation
/zd/pj/yesod/uniqueci/src/Foundation.hs:251:35: error:
• Couldn't match expected type ‘ModelTypes.Username’
with actual type ‘Text’
• In the second argument of ‘($)’, namely ‘credsIdent creds’
In the second argument of ‘($)’, namely
‘UniqueUser $ credsIdent creds’
In a stmt of a 'do' block:
x <- getBy $ UniqueUser $ credsIdent creds
|
251 | x <- getBy $ UniqueUser $ credsIdent creds
| ^^^^^^^^^^^^^^^^
/zd/pj/yesod/uniqueci/src/Foundation.hs:255:31: error:
• Couldn't match expected type ‘ModelTypes.Username’
with actual type ‘Text’
• In the ‘userIdent’ field of a record
In the first argument of ‘insert’, namely
‘User {userIdent = credsIdent creds, userPassword = Nothing}’
In the second argument of ‘(<$>)’, namely
‘insert
User {userIdent = credsIdent creds, userPassword = Nothing}’
|
255 | { userIdent = credsIdent creds
| ^^^^^^^^^^^^^^^^
Yes, that's possible. Taking Carl's comment from above of using the citext column type for case-insensitive character string type, you can use something like this.
First, add PersistField and PersistFieldSql instances for
CI Text
, which is case-insensitiveText
. This must be done in a separate file from where you declare your Persistent models with Template Haskell. In this file, you can add a newtype forUsername
, or you can useCI Text
directly in your Persistent models. I recommend the newtype approach for readability.Then, import that file into the file that loads your Persistent models with Template Haskell:
But note that before executing the code, you need to create the extension for the database:
And then you can execute the code:
And you can then go and actually inspect the database to confirm that
citext
column is indeed created: