Skip to content

Commit 6de3ffe

Browse files
authored
Fix duplicate entity checks (#1287)
* Fix duplicate entity checks * Fix the issue * nice * chagnelog, version bump * fix type name construction * ok
1 parent 62596ba commit 6de3ffe

9 files changed

Lines changed: 259 additions & 28 deletions

File tree

persistent/ChangeLog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog for persistent
22

3+
## 2.13.0.3
4+
5+
* [#1287](https://github.com/yesodweb/persistent/pull/1287)
6+
* Fix the duplicate entity check for transitive dependencies.
7+
* Fixes an issue where generating code would refer to the `ModelName` when
8+
making a reference to another table when the explicit code only refers to
9+
`ModelNameId`.
10+
311
## 2.13.0.2
412

513
* [#1265](https://github.com/yesodweb/persistent/pull/1265)

persistent/Database/Persist/Quasi/Internal.hs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ data UnboundEntityDef
416416
--
417417
-- @since 2.13.0.0
418418
}
419-
deriving (Show, Lift)
419+
deriving (Eq, Ord, Show, Lift)
420420

421421
-- | Convert an 'EntityDef' into an 'UnboundEntityDef'. This "forgets"
422422
-- information about the 'EntityDef', but it is all kept present on the
@@ -537,7 +537,7 @@ data UnboundFieldDef
537537
--
538538
-- @since 2.13.0.0
539539
}
540-
deriving (Eq, Show, Lift)
540+
deriving (Eq, Ord, Show, Lift)
541541

542542
-- | Forget innformation about a 'FieldDef' so it can beused as an
543543
-- 'UnboundFieldDef'.
@@ -615,7 +615,7 @@ data PrimarySpec
615615
-- have a 'DefaultKey'.
616616
--
617617
-- @since 2.13.0.0
618-
deriving (Show, Lift)
618+
deriving (Eq, Ord, Show, Lift)
619619

620620
-- | Construct an entity definition.
621621
mkUnboundEntityDef
@@ -964,7 +964,7 @@ data UnboundIdDef = UnboundIdDef
964964
, unboundIdCascade :: FieldCascade
965965
, unboundIdType :: Maybe FieldType
966966
}
967-
deriving (Show, Lift)
967+
deriving (Eq, Ord, Show, Lift)
968968

969969
-- TODO: this is hacky (the double takeCols, the setFieldDef stuff, and setIdName.
970970
-- need to re-work takeCols function
@@ -1009,7 +1009,7 @@ data UnboundCompositeDef = UnboundCompositeDef
10091009
--
10101010
-- @since 2.13.0.0
10111011
}
1012-
deriving (Show, Lift)
1012+
deriving (Eq, Ord, Show, Lift)
10131013

10141014
takeComposite
10151015
:: [FieldNameHS]
@@ -1130,7 +1130,7 @@ data UnboundForeignDef
11301130
--
11311131
-- @since 2.13.0.0
11321132
}
1133-
deriving (Eq, Show, Lift)
1133+
deriving (Eq, Ord, Show, Lift)
11341134

11351135
-- | A list of fields present on the foreign reference.
11361136
data UnboundForeignFieldList
@@ -1158,7 +1158,7 @@ data UnboundForeignFieldList
11581158
-- @
11591159
--
11601160
-- @since 2.13.0.0
1161-
deriving (Eq, Show, Lift)
1161+
deriving (Eq, Ord, Show, Lift)
11621162

11631163
-- | A pairing of the 'FieldNameHS' for the source table to the 'FieldNameHS'
11641164
-- for the target table.
@@ -1175,7 +1175,7 @@ data ForeignFieldReference =
11751175
--
11761176
-- @since 2.13.0.0
11771177
}
1178-
deriving (Eq, Show, Lift)
1178+
deriving (Eq, Ord, Show, Lift)
11791179

11801180
unbindForeignDef :: ForeignDef -> UnboundForeignDef
11811181
unbindForeignDef fd =

persistent/Database/Persist/TH.hs

Lines changed: 130 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,6 @@ embedEntityDefsMap existingEnts rawEnts =
237237
(embedEntityMap, noCycleEnts)
238238
where
239239
noCycleEnts = entsWithEmbeds
240-
-- every EntityDef could reference each-other (as an EmbedRef)
241-
-- let Haskell tie the knot
242240
embedEntityMap = constructEmbedEntityMap entsWithEmbeds
243241
entsWithEmbeds = fmap setEmbedEntity (rawEnts <> map unbindEntityDef existingEnts)
244242
setEmbedEntity ubEnt =
@@ -773,7 +771,13 @@ mkPersistWith mps preexistingEntities ents' = do
773771
$ predefs
774772
entityMap =
775773
constructEntityMap allEnts
776-
ents <- filterM shouldGenerateCode allEnts
774+
preexistingSet =
775+
Set.fromList $ map getEntityHaskellName preexistingEntities
776+
newEnts =
777+
filter
778+
(\e -> getUnboundEntityNameHS e `Set.notMember` preexistingSet)
779+
allEnts
780+
ents <- filterM shouldGenerateCode newEnts
777781
requireExtensions
778782
[ [TypeFamilies], [GADTs, ExistentialQuantification]
779783
, [DerivingStrategies], [GeneralizedNewtypeDeriving], [StandaloneDeriving]
@@ -1037,9 +1041,15 @@ dataTypeDec mps entityMap entDef = do
10371041
cols :: [VarBangType]
10381042
cols = do
10391043
fieldDef <- getUnboundFieldDefs entDef
1040-
let recordName = fieldDefToRecordName mps entDef fieldDef
1041-
strictness = if unboundFieldStrict fieldDef then isStrict else notStrict
1042-
fieldIdType = maybeIdType mps entityMap fieldDef Nothing Nothing
1044+
let
1045+
recordName =
1046+
fieldDefToRecordName mps entDef fieldDef
1047+
strictness =
1048+
if unboundFieldStrict fieldDef
1049+
then isStrict
1050+
else notStrict
1051+
fieldIdType =
1052+
maybeIdType mps entityMap fieldDef Nothing Nothing
10431053
pure (recordName, strictness, fieldIdType)
10441054

10451055
constrs
@@ -1097,6 +1107,45 @@ mkUnique mps entityMap entDef (UniqueDef constr _ fields attrs) =
10971107
, "on the end of the line that defines your uniqueness "
10981108
, "constraint in order to disable this check. ***" ]
10991109

1110+
-- | This function renders a Template Haskell 'Type' for an 'UnboundFieldDef'.
1111+
-- It takes care to respect the 'mpsGeneric' setting to render an Id faithfully,
1112+
-- and it also ensures that the generated Haskell type is 'Maybe' if the
1113+
-- database column has that attribute.
1114+
--
1115+
-- For a database schema with @'mpsGeneric' = False@, this is simple - it uses
1116+
-- the @ModelNameId@ type directly. This resolves just fine.
1117+
--
1118+
-- If 'mpsGeneric' is @True@, then we have to do something a bit more
1119+
-- complicated. We can't refer to a @ModelNameId@ directly, because that @Id@
1120+
-- alias hides the backend type variable. Instead, we need to refer to:
1121+
--
1122+
-- > Key (ModelNameGeneric backend)
1123+
--
1124+
-- This means that the client code will need both the term @ModelNameId@ in
1125+
-- scope, as well as the @ModelNameGeneric@ constructor, despite the fact that
1126+
-- the @ModelNameId@ is the only term explicitly used (and imported).
1127+
--
1128+
-- However, we're not guaranteed to have @ModelName@ in scope - we've only
1129+
-- referenced @ModelNameId@ in code, and so code generation *should* work even
1130+
-- without this. Consider an explicit-style import:
1131+
--
1132+
-- @
1133+
-- import Model.Foo (FooId)
1134+
--
1135+
-- mkPersistWith sqlSettings $(discoverEntities) [persistLowerCase|
1136+
-- Bar
1137+
-- foo FooId
1138+
-- |]
1139+
-- @
1140+
--
1141+
-- This looks like it ought to work, but it would fail with @mpsGeneric@ being
1142+
-- enabled. One hacky work-around is to perform a @'lookupTypeName' :: String ->
1143+
-- Q (Maybe Name)@ on the @"ModelNameId"@ type string. If the @Id@ is
1144+
-- a reference in the 'EntityMap' and @lookupTypeName@ returns @'Just' name@,
1145+
-- then that 'Name' contains the fully qualified information needed to use the
1146+
-- 'Name' without importing it at the client-site. Then we can perform a bit of
1147+
-- surgery on the 'Name' to strip the @Id@ suffix, turn it into a 'Type', and
1148+
-- apply the 'Key' constructor.
11001149
maybeIdType
11011150
:: MkPersistSettings
11021151
-> EntityMap
@@ -1113,25 +1162,90 @@ maybeIdType mps entityMap fieldDef mbackend mnull =
11131162
True
11141163
_ ->
11151164
maybeNullable fieldDef
1116-
idType = fromMaybe (ftToType $ unboundFieldType fieldDef) $ do
1117-
typ <- extractForeignRef entityMap fieldDef
1118-
pure $
1119-
ConT ''Key
1120-
`AppT` genericDataType mps typ (VarT $ fromMaybe backendName mbackend)
1165+
idType =
1166+
fromMaybe (ftToType $ unboundFieldType fieldDef) $ do
1167+
typ <- extractForeignRef entityMap fieldDef
1168+
guard ((mpsGeneric mps))
1169+
pure $
1170+
ConT ''Key
1171+
`AppT` genericDataType mps typ (VarT $ fromMaybe backendName mbackend)
1172+
1173+
-- TODO: if we keep mpsGeneric, this needs to check 'mpsGeneric' and then
1174+
-- append Generic to the model name, probably
1175+
_removeIdFromTypeSuffix :: Name -> Type
1176+
_removeIdFromTypeSuffix oldName@(Name (OccName nm) nameFlavor) =
1177+
case stripSuffix "Id" (T.pack nm) of
1178+
Nothing ->
1179+
ConT oldName
1180+
Just name ->
1181+
ConT ''Key
1182+
`AppT` do
1183+
ConT $ Name (OccName (T.unpack name)) nameFlavor
1184+
1185+
-- | TODO: if we keep mpsGeneric, let's incorporate this behavior here, so
1186+
-- end users don't need to import the constructor type as well as the id type
1187+
--
1188+
-- Returns 'Nothing' if the given text does not appear to be a table reference.
1189+
-- In that case, do the usual thing for generating a type name.
1190+
--
1191+
-- Returns a @Just typ@ if the text appears to be a model name, and if the
1192+
-- @ModelId@ type is in scope. The 'Type' is a fully qualified reference to
1193+
-- @'Key' ModelName@ such that end users won't have to import it directly.
1194+
_lookupReferencedTable :: EntityMap -> Text -> Q (Maybe Type)
1195+
_lookupReferencedTable em fieldTypeText = do
1196+
let
1197+
mmodelIdString = do
1198+
fieldTypeNoId <- stripSuffix "Id" fieldTypeText
1199+
_ <- M.lookup (EntityNameHS fieldTypeNoId) em
1200+
pure (T.unpack fieldTypeText)
1201+
case mmodelIdString of
1202+
Nothing ->
1203+
pure Nothing
1204+
Just modelIdString -> do
1205+
mIdName <- lookupTypeName modelIdString
1206+
pure $ fmap _removeIdFromTypeSuffix mIdName
1207+
1208+
_fieldNameEndsWithId :: UnboundFieldDef -> Maybe String
1209+
_fieldNameEndsWithId ufd = go (unboundFieldType ufd)
1210+
where
1211+
go = \case
1212+
FTTypeCon mmodule name -> do
1213+
a <- stripSuffix "Id" name
1214+
pure $
1215+
T.unpack $ mconcat
1216+
[ case mmodule of
1217+
Nothing ->
1218+
""
1219+
Just m ->
1220+
mconcat [m, "."]
1221+
, a
1222+
, "Id"
1223+
]
1224+
_ ->
1225+
Nothing
11211226

11221227
backendDataType :: MkPersistSettings -> Type
11231228
backendDataType mps
11241229
| mpsGeneric mps = backendT
11251230
| otherwise = mpsBackend mps
11261231

1232+
-- | TODO:
1233+
--
1234+
-- if we keep mpsGeneric
1235+
-- then
1236+
-- let's make this fully qualify the generic name
1237+
-- else
1238+
-- let's delete it
11271239
genericDataType
11281240
:: MkPersistSettings
11291241
-> EntityNameHS
11301242
-> Type -- ^ backend
11311243
-> Type
11321244
genericDataType mps name backend
1133-
| mpsGeneric mps = ConT (mkEntityNameHSGenericName name) `AppT` backend
1134-
| otherwise = ConT $ mkEntityNameHSName name
1245+
| mpsGeneric mps =
1246+
ConT (mkEntityNameHSGenericName name) `AppT` backend
1247+
| otherwise =
1248+
ConT $ mkEntityNameHSName name
11351249

11361250
degen :: [Clause] -> [Clause]
11371251
degen [] =
@@ -2429,8 +2543,10 @@ mkField mps entityMap et fieldDef = do
24292543
con =
24302544
ForallC
24312545
[]
2432-
[mkEqualP (VarT $ mkName "typ") $ maybeIdType mps entityMap fieldDef Nothing Nothing]
2546+
[mkEqualP (VarT $ mkName "typ") fieldT]
24332547
$ NormalC name []
2548+
fieldT =
2549+
maybeIdType mps entityMap fieldDef Nothing Nothing
24342550
bod <- mkLookupEntityField et (unboundFieldNameHS fieldDef)
24352551
let cla = normalClause
24362552
[ConP name []]
@@ -2679,7 +2795,6 @@ mkSymbolToFieldInstances mps entityMap (fixEntityDef -> ed) = do
26792795
symbolToField = $(entityFieldConstr)
26802796
|]
26812797

2682-
26832798
-- | Pass in a list of lists of extensions, where any of the given
26842799
-- extensions will satisfy it. For example, you might need either GADTs or
26852800
-- ExistentialQuantification, so you'd write:

persistent/persistent.cabal

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: persistent
2-
version: 2.13.0.2
2+
version: 2.13.0.3
33
license: MIT
44
license-file: LICENSE
55
author: Michael Snoyman <michael@snoyman.com>
@@ -164,6 +164,9 @@ test-suite test
164164
Database.Persist.TH.DiscoverEntitiesSpec
165165
Database.Persist.TH.EmbedSpec
166166
Database.Persist.TH.ForeignRefSpec
167+
Database.Persist.TH.PersistWith.Model
168+
Database.Persist.TH.PersistWith.Model2
169+
Database.Persist.TH.PersistWithSpec
167170
Database.Persist.TH.ImplicitIdColSpec
168171
Database.Persist.TH.JsonEncodingSpec
169172
Database.Persist.TH.KindEntitiesSpec
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE DerivingStrategies #-}
3+
{-# LANGUAGE ExistentialQuantification #-}
4+
{-# LANGUAGE FlexibleInstances #-}
5+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+
{-# LANGUAGE MultiParamTypeClasses #-}
7+
{-# LANGUAGE OverloadedStrings #-}
8+
{-# LANGUAGE QuasiQuotes #-}
9+
{-# LANGUAGE StandaloneDeriving #-}
10+
{-# LANGUAGE TemplateHaskell #-}
11+
{-# LANGUAGE TypeApplications #-}
12+
{-# LANGUAGE TypeFamilies #-}
13+
{-# LANGUAGE UndecidableInstances #-}
14+
15+
module Database.Persist.TH.PersistWith.Model where
16+
17+
import TemplateTestImports
18+
19+
import Database.Persist.TH.PersistWith.Model2
20+
21+
mkPersistWith sqlSettings $(discoverEntities) [persistLowerCase|
22+
23+
IceCream
24+
flavor FlavorId
25+
26+
|]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE DerivingStrategies #-}
3+
{-# LANGUAGE ExistentialQuantification #-}
4+
{-# LANGUAGE FlexibleInstances #-}
5+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+
{-# LANGUAGE MultiParamTypeClasses #-}
7+
{-# LANGUAGE OverloadedStrings #-}
8+
{-# LANGUAGE QuasiQuotes #-}
9+
{-# LANGUAGE StandaloneDeriving #-}
10+
{-# LANGUAGE TemplateHaskell #-}
11+
{-# LANGUAGE TypeApplications #-}
12+
{-# LANGUAGE TypeFamilies #-}
13+
{-# LANGUAGE UndecidableInstances #-}
14+
15+
module Database.Persist.TH.PersistWith.Model2 where
16+
17+
import TemplateTestImports
18+
19+
mkPersist sqlSettings [persistLowerCase|
20+
21+
Flavor
22+
name Text
23+
24+
|]

0 commit comments

Comments
 (0)