-- allows automatic derivation from Entity type class {-# LANGUAGE DeriveAnyClass #-} module OneToManySafeSpec ( test, spec, ) where import Database.GP.GenericPersistenceSafe import Database.HDBC import Database.HDBC.Sqlite3 import GHC.Generics import Test.Hspec import Data.Either (fromRight) -- `test` is here so that this module can be run from GHCi on its own. It is -- not needed for automatic spec discovery. -- (start up stack repl -- test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec prepareDB :: IO Conn prepareDB = do conn <- connect AutoCommit <$> connectSqlite3 ":memory:" setupTableFor @Article SQLite conn setupTableFor @Author SQLite conn return conn data Article = Article { articleID :: Int, title :: String, authorId :: Int, year :: Int } deriving (Generic, Show, Eq) instance Entity Article where autoIncrement = False data Author = Author { authorID :: Int, name :: String, address :: String, articles :: [Article] } deriving (Generic, Show, Eq) instance Entity Author where fieldsToColumns :: [(String, String)] -- ommitting the articles field, fieldsToColumns = -- as this can not be mapped to a single column [ ("authorID", "authorID"), ("name", "name"), ("address", "address") ] fromRow :: Conn -> [SqlValue] -> IO Author fromRow conn row = do let authID = head row -- authorID is the first column articlesBy <- (fromRight []) <$> select @Article conn (field "authorId" =. authID) -- retrieve all articles by this author return rawAuthor {articles = articlesBy} -- add the articles to the author where rawAuthor = Author (col 0) (col 1) (col 2) [] -- create the author from row (w/o articles) col i = fromSql (row !! i) -- helper function to convert SqlValue to Haskell type toRow :: Conn -> Author -> IO [SqlValue] toRow conn a = do mapM_ (persist conn) (articles a) -- persist all articles of this author (update or insert) return [toSql (authorID a), -- return the author as a list of SqlValues toSql (name a), toSql (address a)] autoIncrement = False article1 :: Article article1 = Article { articleID = 1, title = "Persistence without Boilerplate", authorId = 1, year = 2018 } article2 :: Article article2 = Article { articleID = 2, title = "Boilerplate for Dummies", authorId = 2, year = 2020 } article3 :: Article article3 = Article { articleID = 3, title = "The return of the boilerplate", authorId = 2, year = 2022 } arthur :: Author arthur = Author { authorID = 2, name = "Arthur Miller", address = "Denver", articles = [article2, article3] } spec :: Spec spec = do describe "Handling of 1:N References" $ do it "works like a charm" $ do conn <- prepareDB _ <- insert conn arthur _ <- insert conn article1 authors <- fromRight [] <$> select @Author conn allEntries length authors `shouldBe` 1 articles' <- fromRight [] <$> select @Article conn allEntries length articles' `shouldBe` 3 eitherPeAuthor <- selectById @Author conn "2" eitherPeAuthor `shouldBe` Right arthur case eitherPeAuthor of Left _ -> fail "should not happen" Right author -> do length (articles author) `shouldBe` 2 _ <- persist conn arthur {address = "New York"} eitherPeAuthor' <- selectById @Author conn "2" eitherPeAuthor' `shouldBe` Right arthur {address = "New York"} it "delete returns unit in case of success" $ do conn <- prepareDB _ <- insert conn arthur eitherPeUnit <- delete conn arthur eitherPeUnit `shouldBe` Right () it "delete handles exceptions" $ do conn <- connect AutoCommit <$> connectSqlite3 ":memory:" eitherPeUnit <- delete conn arthur case eitherPeUnit of Left (DatabaseError msg) -> msg `shouldContain` "no such table: Author" _ -> fail "should not happen" it "insertMany works with references" $ do conn <- prepareDB let authors = [arthur, arthur{name="Bob", authorID=3, articles=[]}] eitherPeUnit <- insertMany conn authors eitherPeUnit `shouldBe` Right () eitherPeAuthors <- select @Author conn allEntries eitherPeAuthors `shouldBe` Right authors it "update works with references" $ do conn <- prepareDB _ <- insert conn arthur eitherPeUnit <- update conn arthur {address = "New York"} eitherPeUnit `shouldBe` Right () eitherPeAuthor <- selectById @Author conn "2" eitherPeAuthor `shouldBe` Right arthur {address = "New York"} it "updateMany works with references" $ do conn <- prepareDB let authors = [arthur, arthur{name="Bob", authorID=3, articles=[]}] _ <- insertMany conn authors eitherPeUnit <- updateMany conn (map (\a -> a {address = "New York"}) authors) eitherPeUnit `shouldBe` Right () eitherPeAuthors <- select @Author conn allEntries eitherPeAuthors `shouldBe` Right (map (\a -> a {address = "New York"}) authors) it "deleteMany works with references" $ do conn <- prepareDB let authors = [arthur, arthur{name="Bob", authorID=3, articles=[]}] _ <- insertMany conn authors eitherPeUnit <- deleteMany conn authors eitherPeUnit `shouldBe` Right () eitherPeAuthors <- select @Author conn allEntries eitherPeAuthors `shouldBe` Right []