1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526
|
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
--
-- DeriveAnyClass is not actually used by persistent-template
-- But a long standing bug was that if it was enabled, it was used to derive instead of GeneralizedNewtypeDeriving
-- This was fixed by using DerivingStrategies to specify newtype deriving should be used.
-- This pragma is left here as a "test" that deriving works when DeriveAnyClass is enabled.
-- See https://github.com/yesodweb/persistent/issues/578
{-# LANGUAGE DeriveAnyClass #-}
module Database.Persist.THSpec where
import Control.Applicative (Const(..))
import Data.Aeson (decode, encode)
import Data.Bits (bitSizeMaybe)
import Data.ByteString.Lazy.Char8 ()
import Data.Coerce
import Data.Functor.Identity (Identity(..))
import Data.Int
import qualified Data.List as List
import Data.Proxy
import Data.Text (Text, pack)
import Data.Time
import GHC.Generics (Generic)
import System.Environment
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck.Arbitrary
import Test.QuickCheck.Gen (Gen)
import Database.Persist
import Database.Persist.EntityDef.Internal
import Database.Persist.Sql
import Database.Persist.Sql.Util
import Database.Persist.TH
import TemplateTestImports
import qualified Database.Persist.TH.CommentSpec as CommentSpec
import qualified Database.Persist.TH.CompositeKeyStyleSpec as CompositeKeyStyleSpec
import qualified Database.Persist.TH.DiscoverEntitiesSpec as DiscoverEntitiesSpec
import qualified Database.Persist.TH.EmbedSpec as EmbedSpec
import qualified Database.Persist.TH.EntityHaddockSpec as EntityHaddockSpec
import qualified Database.Persist.TH.ForeignRefSpec as ForeignRefSpec
import qualified Database.Persist.TH.ImplicitIdColSpec as ImplicitIdColSpec
import qualified Database.Persist.TH.JsonEncodingSpec as JsonEncodingSpec
import qualified Database.Persist.TH.KindEntitiesSpec as KindEntitiesSpec
import qualified Database.Persist.TH.MaybeFieldDefsSpec as MaybeFieldDefsSpec
import qualified Database.Persist.TH.MigrationOnlySpec as MigrationOnlySpec
import qualified Database.Persist.TH.MultiBlockSpec as MultiBlockSpec
import qualified Database.Persist.TH.NestedSymbolsInTypeSpec as NestedSymbolsInTypeSpec
import qualified Database.Persist.TH.NoFieldSelectorsSpec as NoFieldSelectorsSpec
import qualified Database.Persist.TH.OverloadedLabelSpec as OverloadedLabelSpec
import qualified Database.Persist.TH.PersistWithSpec as PersistWithSpec
import qualified Database.Persist.TH.RequireOnlyPersistImportSpec as RequireOnlyPersistImportSpec
import qualified Database.Persist.TH.SharedPrimaryKeyImportedSpec as SharedPrimaryKeyImportedSpec
import qualified Database.Persist.TH.SharedPrimaryKeySpec as SharedPrimaryKeySpec
import qualified Database.Persist.TH.SumSpec as SumSpec
import qualified Database.Persist.TH.ToFromPersistValuesSpec as ToFromPersistValuesSpec
import qualified Database.Persist.TH.TypeLitFieldDefsSpec as TypeLitFieldDefsSpec
-- test to ensure we can have types ending in Id that don't trash the TH
-- machinery
type TextId = Text
share [mkPersistWith sqlSettings { mpsGeneric = False, mpsDeriveInstances = [''Generic] } [entityDef @JsonEncodingSpec.JsonEncoding Proxy]] [persistUpperCase|
Person json
name Text
age Int Maybe
foo Foo
address Address
deriving Show Eq
HasSimpleCascadeRef
person PersonId OnDeleteCascade
deriving Show Eq
Address json
street Text
city Text
zip Int Maybe
deriving Show Eq
NoJson
foo Text
deriving Show Eq
CustomIdName
Id sql=id_col
name Text
deriving Show Eq
QualifiedReference
jsonEncoding JsonEncodingSpec.JsonEncodingId
|]
mkPersist sqlSettings [persistLowerCase|
HasPrimaryDef
userId Int
name String
Primary userId
HasMultipleColPrimaryDef
foobar Int
barbaz String
Primary foobar barbaz
TestDefaultKeyCol
Id TestDefaultKeyColId
name String
HasIdDef
Id Int
name String
HasDefaultId
name String
HasCustomSqlId
Id String sql=my_id
name String
SharedPrimaryKey
Id HasDefaultIdId
name String
SharedPrimaryKeyWithCascade
Id (Key HasDefaultId) OnDeleteCascade
name String
SharedPrimaryKeyWithCascadeAndCustomName
Id (Key HasDefaultId) OnDeleteCascade sql=my_id
name String
Top
name Text
Middle
top TopId
Primary top
Bottom
middle MiddleId
Primary middle
-- Test that a field can be named Key
KeyTable
key Text
|]
share [mkPersist sqlSettings { mpsGeneric = False, mpsGenerateLenses = True }] [persistLowerCase|
Lperson json
name Text
age Int Maybe
address Laddress
deriving Show Eq
Laddress json
street Text
city Text
zip Int Maybe
deriving Show Eq
CustomPrimaryKey
anInt Int
Primary anInt
|]
arbitraryT :: Gen Text
arbitraryT = pack <$> arbitrary
instance Arbitrary Person where
arbitrary = Person <$> arbitraryT <*> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary Address where
arbitrary = Address <$> arbitraryT <*> arbitraryT <*> arbitrary
spec :: Spec
spec = describe "THSpec" $ do
describe "SumSpec" $ SumSpec.spec
PersistWithSpec.spec
KindEntitiesSpec.spec
NestedSymbolsInTypeSpec.spec
OverloadedLabelSpec.spec
SharedPrimaryKeySpec.spec
SharedPrimaryKeyImportedSpec.spec
ImplicitIdColSpec.spec
MaybeFieldDefsSpec.spec
TypeLitFieldDefsSpec.spec
MigrationOnlySpec.spec
NoFieldSelectorsSpec.spec
EmbedSpec.spec
DiscoverEntitiesSpec.spec
MultiBlockSpec.spec
ForeignRefSpec.spec
ToFromPersistValuesSpec.spec
JsonEncodingSpec.spec
CommentSpec.spec
EntityHaddockSpec.spec
CompositeKeyStyleSpec.spec
it "QualifiedReference" $ do
let ed = entityDef @QualifiedReference Proxy
[FieldDef {..}] = entityFields ed
fieldType `shouldBe` FTTypeCon (Just "JsonEncodingSpec") "JsonEncodingId"
fieldSqlType `shouldBe` sqlType @JsonEncodingSpec.JsonEncodingId Proxy
fieldReference `shouldBe` ForeignRef (EntityNameHS "JsonEncoding")
describe "TestDefaultKeyCol" $ do
let EntityIdField FieldDef{..} =
entityId (entityDef (Proxy @TestDefaultKeyCol))
it "should be a BackendKey SqlBackend" $ do
-- the purpose of this test is to verify that a custom Id column of
-- the form:
-- > ModelName
-- > Id ModelNameId
--
-- should behave like an implicit id column.
(TestDefaultKeyColKey (SqlBackendKey 32) :: Key TestDefaultKeyCol)
`shouldBe`
(toSqlKey 32 :: Key TestDefaultKeyCol)
describe "HasDefaultId" $ do
let EntityIdField FieldDef{..} =
entityId (entityDef (Proxy @HasDefaultId))
it "should have usual db name" $ do
fieldDB `shouldBe` FieldNameDB "id"
it "should have usual haskell name" $ do
fieldHaskell `shouldBe` FieldNameHS "Id"
it "should have correct underlying sql type" $ do
fieldSqlType `shouldBe` SqlInt64
it "persistfieldsql should be right" $ do
sqlType (Proxy @HasDefaultIdId) `shouldBe` SqlInt64
it "should have correct haskell type" $ do
fieldType `shouldBe` FTTypeCon Nothing "HasDefaultIdId"
describe "HasCustomSqlId" $ do
let EntityIdField FieldDef{..} =
entityId (entityDef (Proxy @HasCustomSqlId))
it "should have custom db name" $ do
fieldDB `shouldBe` FieldNameDB "my_id"
it "should have usual haskell name" $ do
fieldHaskell `shouldBe` FieldNameHS "Id"
it "should have correct underlying sql type" $ do
fieldSqlType `shouldBe` SqlString
it "should have correct haskell type" $ do
fieldType `shouldBe` FTTypeCon Nothing "String"
describe "HasIdDef" $ do
let EntityIdField FieldDef{..} =
entityId (entityDef (Proxy @HasIdDef))
it "should have usual db name" $ do
fieldDB `shouldBe` FieldNameDB "id"
it "should have usual haskell name" $ do
fieldHaskell `shouldBe` FieldNameHS "Id"
it "should have correct underlying sql type" $ do
fieldSqlType `shouldBe`
if bitSizeMaybe (0 :: Int) <= Just 32
then SqlInt32
else SqlInt64
it "should have correct haskell type" $ do
fieldType `shouldBe` FTTypeCon Nothing "Int"
describe "SharedPrimaryKey" $ do
let sharedDef = entityDef (Proxy @SharedPrimaryKey)
EntityIdField FieldDef{..} =
entityId sharedDef
it "should have usual db name" $ do
fieldDB `shouldBe` FieldNameDB "id"
it "should have usual haskell name" $ do
fieldHaskell `shouldBe` FieldNameHS "Id"
it "should have correct underlying sql type" $ do
fieldSqlType `shouldBe` SqlInt64
it "should have correct underlying (as reported by sqltype)" $ do
fieldSqlType `shouldBe` sqlType (Proxy :: Proxy HasDefaultIdId)
it "should have correct haskell type" $ do
fieldType `shouldBe` (FTTypeCon Nothing "HasDefaultIdId")
it "should have correct sql type from PersistFieldSql" $ do
sqlType (Proxy @SharedPrimaryKeyId)
`shouldBe`
SqlInt64
it "should have same sqlType as underlying record" $ do
sqlType (Proxy @SharedPrimaryKeyId)
`shouldBe`
sqlType (Proxy @HasDefaultIdId)
it "should be a coercible newtype" $ do
coerce @Int64 3
`shouldBe`
SharedPrimaryKeyKey (toSqlKey 3)
describe "SharedPrimaryKeyWithCascade" $ do
let EntityIdField FieldDef{..} =
entityId (entityDef (Proxy @SharedPrimaryKeyWithCascade))
it "should have usual db name" $ do
fieldDB `shouldBe` FieldNameDB "id"
it "should have usual haskell name" $ do
fieldHaskell `shouldBe` FieldNameHS "Id"
it "should have correct underlying sql type" $ do
fieldSqlType `shouldBe` SqlInt64
it "should have correct haskell type" $ do
fieldType
`shouldBe`
FTApp (FTTypeCon Nothing "Key") (FTTypeCon Nothing "HasDefaultId")
it "should have cascade in field def" $ do
fieldCascade `shouldBe` noCascade { fcOnDelete = Just Cascade }
describe "OnCascadeDelete" $ do
let subject :: FieldDef
Just subject =
List.find ((FieldNameHS "person" ==) . fieldHaskell)
$ entityFields
$ simpleCascadeDef
simpleCascadeDef =
entityDef (Proxy :: Proxy HasSimpleCascadeRef)
expected =
FieldCascade
{ fcOnDelete = Just Cascade
, fcOnUpdate = Nothing
}
describe "entityDef" $ do
it "works" $ do
simpleCascadeDef
`shouldBe`
EntityDef
{ entityHaskell = EntityNameHS "HasSimpleCascadeRef"
, entityDB = EntityNameDB "HasSimpleCascadeRef"
, entityId =
EntityIdField FieldDef
{ fieldHaskell = FieldNameHS "Id"
, fieldDB = FieldNameDB "id"
, fieldType = FTTypeCon Nothing "HasSimpleCascadeRefId"
, fieldSqlType = SqlInt64
, fieldReference =
NoReference
, fieldAttrs = []
, fieldStrict = True
, fieldComments = Nothing
, fieldCascade = noCascade
, fieldGenerated = Nothing
, fieldIsImplicitIdColumn = True
}
, entityAttrs = []
, entityFields =
[ FieldDef
{ fieldHaskell = FieldNameHS "person"
, fieldDB = FieldNameDB "person"
, fieldType = FTTypeCon Nothing "PersonId"
, fieldSqlType = SqlInt64
, fieldAttrs = []
, fieldStrict = True
, fieldReference =
ForeignRef
(EntityNameHS "Person")
, fieldCascade =
FieldCascade { fcOnUpdate = Nothing, fcOnDelete = Just Cascade }
, fieldComments = Nothing
, fieldGenerated = Nothing
, fieldIsImplicitIdColumn = False
}
]
, entityUniques = []
, entityForeigns = []
, entityDerives = ["Show", "Eq"]
, entityExtra = mempty
, entitySum = False
, entityComments = Nothing
}
it "has the cascade on the field def" $ do
fieldCascade subject `shouldBe` expected
it "doesn't have any extras" $ do
entityExtra simpleCascadeDef
`shouldBe`
mempty
describe "hasNaturalKey" $ do
let subject :: PersistEntity a => Proxy a -> Bool
subject p = hasNaturalKey (entityDef p)
it "is True for Primary keyword" $ do
subject (Proxy @HasPrimaryDef)
`shouldBe`
True
it "is True for multiple Primary columns " $ do
subject (Proxy @HasMultipleColPrimaryDef)
`shouldBe`
True
it "is False for Id keyword" $ do
subject (Proxy @HasIdDef)
`shouldBe`
False
it "is False for unspecified/default id" $ do
subject (Proxy @HasDefaultId)
`shouldBe`
False
describe "hasCompositePrimaryKey" $ do
let subject :: PersistEntity a => Proxy a -> Bool
subject p = hasCompositePrimaryKey (entityDef p)
it "is False for Primary with single column" $ do
subject (Proxy @HasPrimaryDef)
`shouldBe`
False
it "is True for multiple Primary columns " $ do
subject (Proxy @HasMultipleColPrimaryDef)
`shouldBe`
True
it "is False for Id keyword" $ do
subject (Proxy @HasIdDef)
`shouldBe`
False
it "is False for unspecified/default id" $ do
subject (Proxy @HasDefaultId)
`shouldBe`
False
describe "JSON serialization" $ do
prop "to/from is idempotent" $ \person ->
decode (encode person) == Just (person :: Person)
it "decode" $
decode "{\"name\":\"Michael\",\"age\":27,\"foo\":\"Bar\",\"address\":{\"street\":\"Narkis\",\"city\":\"Maalot\"}}" `shouldBe` Just
(Person "Michael" (Just 27) Bar $ Address "Narkis" "Maalot" Nothing)
describe "JSON serialization for Entity" $ do
let key = PersonKey 0
prop "to/from is idempotent" $ \person ->
decode (encode (Entity key person)) == Just (Entity key (person :: Person))
it "decode" $
decode "{\"id\": 0, \"name\":\"Michael\",\"age\":27,\"foo\":\"Bar\",\"address\":{\"street\":\"Narkis\",\"city\":\"Maalot\"}}" `shouldBe` Just
(Entity key (Person "Michael" (Just 27) Bar $ Address "Narkis" "Maalot" Nothing))
it "lens operations" $ do
let street1 = "street1"
city1 = "city1"
city2 = "city2"
zip1 = Just 12345
address1 = Laddress street1 city1 zip1
address2 = Laddress street1 city2 zip1
name1 = "name1"
age1 = Just 27
person1 = Lperson name1 age1 address1
person2 = Lperson name1 age1 address2
(person1 ^. lpersonAddress) `shouldBe` address1
(person1 ^. (lpersonAddress . laddressCity)) `shouldBe` city1
(person1 & ((lpersonAddress . laddressCity) .~ city2)) `shouldBe` person2
describe "Derived Show/Read instances" $ do
-- This tests confirms https://github.com/yesodweb/persistent/issues/1104 remains fixed
it "includes the name of the newtype when showing/reading a Key, i.e. uses the stock strategy when deriving Show/Read" $ do
show (PersonKey 0) `shouldBe` "PersonKey {unPersonKey = SqlBackendKey {unSqlBackendKey = 0}}"
read (show (PersonKey 0)) `shouldBe` PersonKey 0
show (CustomPrimaryKeyKey 0) `shouldBe` "CustomPrimaryKeyKey {unCustomPrimaryKeyKey = 0}"
read (show (CustomPrimaryKeyKey 0)) `shouldBe` CustomPrimaryKeyKey 0
describe "tabulateEntityA" $ do
it "works" $ do
person <-
tabulateEntityA $ \case
PersonName ->
pure "Matt"
PersonAge -> do
(year, _, _) <- toGregorian . utctDay <$> getCurrentTime
pure $ Just (fromInteger year - 1988)
PersonFoo -> do
_ <- lookupEnv "PERSON_FOO" :: IO (Maybe String)
pure Bar
PersonAddress ->
pure $ Address "lol no" "Denver" Nothing
PersonId ->
pure $ toSqlKey 123
expectedAge <- fromInteger . subtract 1988 . (\(a, _, _) -> a) . toGregorian . utctDay <$> getCurrentTime
person `shouldBe` Entity (toSqlKey 123) Person
{ personName =
"Matt"
, personAge =
Just expectedAge
, personFoo =
Bar
, personAddress =
Address "lol no" "Denver" Nothing
}
describe "tabulateEntity" $ do
it "works" $ do
let
addressTabulate =
tabulateEntity $ \case
AddressId ->
toSqlKey 123
AddressStreet ->
"nope"
AddressCity ->
"Denver"
AddressZip ->
Nothing
addressTabulate `shouldBe`
Entity (toSqlKey 123) Address
{ addressStreet = "nope"
, addressCity = "Denver"
, addressZip = Nothing
}
describe "CustomIdName" $ do
it "has a good safe to insert class instance" $ do
let proxy = Proxy :: SafeToInsert CustomIdName => Proxy CustomIdName
proxy `shouldBe` Proxy
(&) :: a -> (a -> b) -> b
x & f = f x
(^.) :: s
-> ((a -> Const a b) -> (s -> Const a t))
-> a
x ^. lens = getConst $ lens Const x
(.~) :: ((a -> Identity b) -> (s -> Identity t))
-> b
-> s
-> t
lens .~ val = runIdentity . lens (\_ -> Identity val)
|