-- | -- Module : Quokka.Functions -- Copyright : © 2019 Shirren Premaratne -- License : MIT -- -- Maintainer : Shirren Premaratne -- Stability : stable -- Portability : portable -- -- Functions to generate Postgres data via the postgres-simple library. {-# LANGUAGE OverloadedStrings #-} module Quokka.Functions ( build , build1 , buildWith1Rel , buildWith1CustomRel , build1With1Rel , build1With1CustomRel , buildWithManyRels , buildWithManyCustomRels , build1WithManyRels , build1WithManyCustomRels , delete , deleteStatement , id' , insertStatement , insertStatementWith1Rel , insertStatementWith1CustomRel , insertStatementWithManyRels , insertStatementWithManyCustomRels , mapFromIdToResult ) where import Data.Functor (void) import Data.Int (Int64) import Data.Text (intercalate) import Data.Text.Encoding (encodeUtf8) import Database.PostgreSQL.Simple (Connection, ToRow, execute_, returning, query) import Database.PostgreSQL.Simple.Types (Query (Query)) import Quokka.Types (ChildTable (ChildTable) , FK (FK) , Id (getId) , ParentTable (ParentTable) , Table (Table) , Relation (Relation) , Result (SingleResult)) import Quokka.Text.Countable (singularize) -- | Build a prepared statement to insert data into the database` build :: (ToRow q) => Connection -> ParentTable -> [q] -> IO [Id] build conn tbl = let qry = insertStatement tbl in returning conn qry -- | Similar to the build function but we only ever return -- a single optional result, and only take 1 value. build1 :: (ToRow q) => Connection -> ParentTable -> q -> IO (Maybe Id) build1 conn tbl = let qry = insertStatement tbl in fmap build1Helper . query conn qry -- | Build a prepared statement for a child table with a single foreign -- key to the nominated parent table. buildWith1Rel :: (ToRow q) => Connection -> ParentTable -> ChildTable -> [q] -> IO [Id] buildWith1Rel conn parent child = let qry = insertStatementWith1Rel parent child in returning conn qry -- | Build a prepared statement for a child table with a single foreign -- key to the nominated parent table through a custom relation. buildWith1CustomRel :: (ToRow q) => Connection -> Relation -> ChildTable -> [q] -> IO [Id] buildWith1CustomRel conn relation child = let qry = insertStatementWith1CustomRel relation child in returning conn qry -- | Build a prepared statement for a child table with a single foreign -- key table build1With1Rel :: (ToRow q) => Connection -> ParentTable -> ChildTable -> q -> IO (Maybe Id) build1With1Rel conn parent child = let qry = insertStatementWith1Rel parent child in fmap build1Helper . query conn qry -- | Build a prepared statement for a child table with a single foreign -- key table mapped through a custom relation. build1With1CustomRel :: (ToRow q) => Connection -> Relation -> ChildTable -> q -> IO (Maybe Id) build1With1CustomRel conn relation child = let qry = insertStatementWith1CustomRel relation child in fmap build1Helper . query conn qry -- | Build a prepared statement for a child table with more than 1 parent buildWithManyRels :: (ToRow q) => Connection -> [ParentTable] -> ChildTable -> [q] -> IO [Id] buildWithManyRels conn parents child = let qry = insertStatementWithManyRels parents child in returning conn qry -- | Build a prepared statement for a child table with more than 1 parent -- mapped through a custom relation. buildWithManyCustomRels :: (ToRow q) => Connection -> [Relation] -> ChildTable -> [q] -> IO [Id] buildWithManyCustomRels conn relations child = let qry = insertStatementWithManyCustomRels relations child in returning conn qry -- | Build a prepared statement for a child table with more than 1 parent build1WithManyRels :: (ToRow q) => Connection -> [ParentTable] -> ChildTable -> q -> IO (Maybe Id) build1WithManyRels conn parents child = let qry = insertStatementWithManyRels parents child in fmap build1Helper . query conn qry -- | Build a prepared statement for a child table with more than 1 parent mapped -- through a custom relation. build1WithManyCustomRels :: (ToRow q) => Connection -> [Relation] -> ChildTable -> q -> IO (Maybe Id) build1WithManyCustomRels conn relations child = let qry = insertStatementWithManyCustomRels relations child in fmap build1Helper . query conn qry -- | Perform a truncate with cascade action on the Table delete :: Connection -> Table -> IO Int64 delete conn tbl = do let alter = alterSequenceStatement tbl qry = deleteStatement tbl void $ execute_ conn alter execute_ conn qry -- | Create an insert statement for a table insertStatement :: ParentTable -> Query insertStatement (ParentTable name columns) = let columnsAsText = intercalate "," columns valuesAsText = intercalate "," (map (const "?") columns) baseInsert = "insert into " <> name <> " (" <> columnsAsText <> ")" in Query (encodeUtf8 $ baseInsert <> " values (" <> valuesAsText <> ") returning id;") -- | Creates an insert statement for a table, and uses the parent table to also incude -- a foreign key in the generation of the statement. insertStatementWith1Rel :: ParentTable -> ChildTable -> Query insertStatementWith1Rel parent = insertStatementWithManyRels [parent] -- | Creates an insert statement for a table, and uses the parent table custom relation -- to build an insert statement for a child table using the relation insertStatementWith1CustomRel :: Relation -> ChildTable -> Query insertStatementWith1CustomRel relation = insertStatementWithManyCustomRels [relation] -- | Creates an insert statement for a table, and uses multiple parent tables to also include -- foreign keys in the generation of the statement. insertStatementWithManyRels :: [ParentTable] -> ChildTable -> Query insertStatementWithManyRels parents child = let buildFK name = FK (singularize name <> "_id") relations = map (\p@(ParentTable name _) -> Relation p (buildFK name)) parents in insertStatementWithManyCustomRels relations child -- | Creates an insert statement for a table where the relationship between the parent and child -- is modelled using a custom key. insertStatementWithManyCustomRels :: [Relation] -> ChildTable -> Query insertStatementWithManyCustomRels relations (ChildTable name columns) = let updatedColumns = columns ++ map (\(Relation _ (FK fkName)) -> fkName) relations columnsAsText = intercalate "," updatedColumns valuesAsText = intercalate "," (map (const "?") updatedColumns) baseInsert = "insert into " <> name <> " (" <> columnsAsText <> ")" in Query (encodeUtf8 $ baseInsert <> " values (" <> valuesAsText <> ") returning id;") -- | Generate an alter sequence statement for a table alterSequenceStatement :: Table -> Query alterSequenceStatement (Table name) = Query (encodeUtf8 $ "alter sequence " <> name <> "_id_seq restart;") -- | Generate a delete statement for a table deleteStatement :: Table -> Query deleteStatement (Table name) = Query (encodeUtf8 $ "truncate table " <> name <> " cascade;") -- | Helper function to extract the underlying Int value from -- the first value in the list id' :: [Id] -> Int id' (x:_) = getId x id' [] = -1 -- | Postgres Simple does not have a function which maps from [r] -> Maybe r -- so we've written one that takes the head or returns Nothing in a safe -- manner. build1Helper :: [Id] -> Maybe Id build1Helper (x:_) = Just x build1Helper [] = Nothing -- | Function to map from IO [Id] -> IO [Result] mapFromIdToResult :: ParentTable -> [Id] -> [Result] mapFromIdToResult tbl = map (SingleResult tbl)