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.persistentmodelsto 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 Textdirectly 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
citextcolumn is indeed created: