-
Notifications
You must be signed in to change notification settings - Fork 292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type error on upsert. #885
Changes from 2 commits
41bb51c
dcc89b5
1da4150
456c0f1
dd13ba6
11464fe
9b033dc
1cd45ce
a1c76a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,11 +47,16 @@ module Database.Persist.TH | |
, packPTH | ||
, lensPTH | ||
, parseReferences | ||
, AtLeastOneUniqueKey(..) | ||
, OnlyOneUniqueKey(..) | ||
) where | ||
|
||
import Prelude hiding ((++), take, concat, splitAt, exp) | ||
|
||
import qualified Data.List.NonEmpty as NEL | ||
import Database.Persist | ||
import Database.Persist.Sql (Migration, migrate, SqlBackend, PersistFieldSql) | ||
-- import Database.Persist.Class | ||
import Database.Persist.Quasi | ||
import Language.Haskell.TH.Lib ( | ||
#if MIN_VERSION_template_haskell(2,11,0) | ||
|
@@ -61,7 +66,7 @@ import Language.Haskell.TH.Lib ( | |
import Language.Haskell.TH.Quote | ||
import Language.Haskell.TH.Syntax | ||
import Data.Char (toLower, toUpper) | ||
import Control.Monad (forM, (<=<), mzero) | ||
import Control.Monad (forM, unless, (<=<), mzero) | ||
import qualified System.IO as SIO | ||
import Data.Text (pack, Text, append, unpack, concat, uncons, cons, stripPrefix, stripSuffix) | ||
import qualified Data.Text as T | ||
|
@@ -87,6 +92,8 @@ import Web.HttpApiData (ToHttpApiData(..), FromHttpApiData(..)) | |
import GHC.Generics (Generic) | ||
import qualified Data.Text.Encoding as TE | ||
|
||
import GHC.TypeLits | ||
|
||
-- | This special-cases "type_" and strips out its underscore. When | ||
-- used for JSON serialization and deserialization, it works around | ||
-- <https://github.com/yesodweb/persistent/issues/412> | ||
|
@@ -116,14 +123,14 @@ persistFileWith :: PersistSettings -> FilePath -> Q Exp | |
persistFileWith ps fp = persistManyFileWith ps [fp] | ||
|
||
-- | Same as 'persistFileWith', but uses several external files instead of | ||
-- one. Splitting your Persistent definitions into multiple modules can | ||
-- one. Splitting your Persistent definitions into multiple modules can | ||
-- potentially dramatically speed up compile times. | ||
-- | ||
-- The recommended file extension is @.persistentmodels@. | ||
-- | ||
-- ==== __Examples__ | ||
-- | ||
-- Split your Persistent definitions into multiple files (@models1@, @models2@), | ||
-- Split your Persistent definitions into multiple files (@models1@, @models2@), | ||
-- then create a new module for each new file and run 'mkPersist' there: | ||
-- | ||
-- @ | ||
|
@@ -145,13 +152,13 @@ persistFileWith ps fp = persistManyFileWith ps [fp] | |
-- -- Migrate.hs | ||
-- 'share' | ||
-- ['mkMigrate' "migrateAll"] | ||
-- $('persistManyFileWith' 'lowerCaseSettings' ["models1.persistentmodels","models2.persistentmodels"]) | ||
-- $('persistManyFileWith' 'lowerCaseSettings' ["models1.persistentmodels","models2.persistentmodels"]) | ||
-- @ | ||
-- | ||
-- Tip: To get the same import behavior as if you were declaring all your models in | ||
-- one file, import your new files @as Name@ into another file, then export @module Name@. | ||
-- | ||
-- This approach may be used in the future to reduce memory usage during compilation, | ||
-- This approach may be used in the future to reduce memory usage during compilation, | ||
-- but so far we've only seen mild reductions. | ||
-- | ||
-- See <https://github.com/yesodweb/persistent/issues/778 persistent#778> and | ||
|
@@ -367,7 +374,8 @@ mkPersist mps ents' = do | |
x <- fmap Data.Monoid.mconcat $ mapM (persistFieldFromEntity mps) ents | ||
y <- fmap mconcat $ mapM (mkEntity entMap mps) ents | ||
z <- fmap mconcat $ mapM (mkJSON mps) ents | ||
return $ mconcat [x, y, z] | ||
uniqueKeyInstances <- fmap mconcat $ mapM (mkUniqueKeyInstances mps) ents | ||
return $ mconcat [x, y, z, uniqueKeyInstances] | ||
where | ||
ents = map fixEntityDef ents' | ||
entMap = M.fromList $ map (\ent -> (entityHaskell ent, ent)) ents | ||
|
@@ -1106,6 +1114,83 @@ mkEntity entMap mps t = do | |
genDataType = genericDataType mps entName backendT | ||
entName = entityHaskell t | ||
|
||
mkUniqueKeyInstances :: MkPersistSettings -> EntityDef -> Q [Dec] | ||
mkUniqueKeyInstances mps t = do | ||
undecidableInstancesEnabled <- isExtEnabled UndecidableInstances | ||
unless undecidableInstancesEnabled . fail | ||
$ "Generating Persistent entities now requires the 'UndecidableInstances' " | ||
<> "language extension. Please enable it in your file by copy/pasting " | ||
<> "this line into the top of your file: \n\n" | ||
<> "{-# LANGUAGE UndecidableInstances #-}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gives a pretty pleasant error message. The alternative is a bit nasty. |
||
case entityUniques t of | ||
[] -> mappend <$> typeErrorSingle <*> typeErrorAtLeastOne | ||
[_] -> mappend <$> singleUniqueKey <*> atLeastOneKey | ||
(_:_) -> mappend <$> typeErrorMultiple <*> atLeastOneKey | ||
where | ||
requireUniquesPName = mkName "requireUniquesP" | ||
onlyUniquePName = mkName "onlyUniqueP" | ||
typeErrorSingle = mkOnlyUniqueError typeErrorNoneCtx | ||
typeErrorMultiple = mkOnlyUniqueError typeErrorMultipleCtx | ||
|
||
withPersistStoreWriteCxt = | ||
if mpsGeneric mps | ||
then do | ||
write <- [t|PersistStoreWrite $(pure (VarT $ mkName "backend")) |] | ||
pure [write] | ||
else do | ||
pure [] | ||
|
||
typeErrorNoneCtx = do | ||
tyErr <- [t|TypeError (NoUniqueKeysError $(pure genDataType))|] | ||
(tyErr :) <$> withPersistStoreWriteCxt | ||
|
||
typeErrorMultipleCtx = do | ||
tyErr <- [t|TypeError (MultipleUniqueKeysError $(pure genDataType))|] | ||
(tyErr :) <$> withPersistStoreWriteCxt | ||
|
||
mkOnlyUniqueError :: Q Cxt -> Q [Dec] | ||
mkOnlyUniqueError mkCtx = do | ||
ctx <- mkCtx | ||
let impl = mkImpossible onlyUniquePName | ||
pure [instanceD ctx onlyOneUniqueKeyClass impl] | ||
|
||
mkImpossible name = | ||
[ FunD name | ||
[ Clause | ||
[ WildP ] | ||
(NormalB | ||
(VarE (mkName "error") `AppE` LitE (StringL "impossible")) | ||
) | ||
[] | ||
] | ||
] | ||
|
||
typeErrorAtLeastOne :: Q [Dec] | ||
typeErrorAtLeastOne = do | ||
let impl = mkImpossible requireUniquesPName | ||
cxt <- typeErrorMultipleCtx | ||
pure [instanceD cxt atLeastOneUniqueKeyClass impl] | ||
|
||
singleUniqueKey :: Q [Dec] | ||
singleUniqueKey = do | ||
expr <- [e|\p -> head (persistUniqueKeysP p)|] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is safe because we have already guaranteed that there are multiple unique keys |
||
let impl = [FunD onlyUniquePName [Clause [] (NormalB expr) []]] | ||
cxt <- withPersistStoreWriteCxt | ||
pure [instanceD cxt onlyOneUniqueKeyClass impl] | ||
|
||
atLeastOneUniqueKeyClass = ConT ''AtLeastOneUniqueKey `AppT` genDataType | ||
onlyOneUniqueKeyClass = ConT ''OnlyOneUniqueKey `AppT` genDataType | ||
|
||
atLeastOneKey :: Q [Dec] | ||
atLeastOneKey = do | ||
expr <- [e|\p -> NEL.fromList (persistUniqueKeysP p)|] | ||
let impl = [FunD requireUniquesPName [Clause [] (NormalB expr) []]] | ||
cxt <- withPersistStoreWriteCxt | ||
pure [instanceD cxt atLeastOneUniqueKeyClass impl] | ||
|
||
genDataType = genericDataType mps (entityHaskell t) backendT | ||
|
||
|
||
entityText :: EntityDef -> Text | ||
entityText = unHaskellName . entityHaskell | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change requires
UndecidableInstances
. I give a nice error message for upgrading.