{-# LANGUAGE FlexibleContexts, OverloadedStrings, RecordWildCards , ScopedTypeVariables, TypeFamilies #-} {-# OPTIONS_GHC -Wall #-} import Control.Arrow (second) import Control.Monad import Control.Monad.Base import Control.Monad.Catch import Data.Function import Data.Int import Data.Monoid import Data.Monoid.Utils import Database.PostgreSQL.PQTypes import Database.PostgreSQL.PQTypes.Internal.Utils (mread) import System.Console.Readline import System.Environment import qualified Data.ByteString.Char8 as BS -- | Generic 'putStrLn'. printLn :: MonadBase IO m => String -> m () printLn = liftBase . putStrLn -- | Get connection string from command line argument. getConnSettings :: IO ConnectionSettings getConnSettings = do args <- getArgs case args of [conninfo] -> return def { csConnInfo = BS.pack conninfo } _ -> do prog <- getProgName error $ "Usage:" <+> prog <+> "" ---------------------------------------- -- | Representation of a book. data Book = Book { bookID :: Int64 , bookName :: String , bookYear :: Int32 } deriving (Read, Show) -- | Intermediate representation of 'Book'. type instance CompositeRow Book = (Int64, String, Int32) instance PQFormat Book where pqFormat _ = "%book_" instance CompositeFromSQL Book where toComposite (bid, name, year) = Book { bookID = bid , bookName = name , bookYear = year } withCatalog :: ConnectionSettings -> IO () -> IO () withCatalog cs = bracket_ createStructure dropStructure where -- | Create needed tables and types. createStructure = runDBT (simpleSource cs) def $ do printLn "Creating tables..." runSQL_ $ mconcat [ "CREATE TABLE authors_ (" , " id BIGSERIAL NOT NULL" , ", name TEXT NOT NULL" , ", PRIMARY KEY (id)" , ")" ] runSQL_ $ mconcat [ "CREATE TABLE books_ (" , " id BIGSERIAL NOT NULL" , ", name TEXT NOT NULL" , ", year INTEGER NOT NULL" , ", author_id BIGINT NOT NULL" , ", PRIMARY KEY (id)" , ", FOREIGN KEY (author_id) REFERENCES authors_ (id)" , ")" ] runSQL_ $ mconcat [ "CREATE TYPE book_ AS (" , " id BIGINT" , ", name TEXT" , ", year INTEGER" , ")" ] -- | Drop previously created database structures. dropStructure = runDBT (simpleSource cs) def $ do printLn "Dropping tables..." runSQL_ "DROP TYPE book_" runSQL_ "DROP TABLE books_" runSQL_ "DROP TABLE authors_" ---------------------------------------- processCommand :: ConnectionSource -> String -> IO () processCommand cs cmd = case parse cmd of -- | Display authors. ("authors", "") -> runDBT cs def $ do runSQL_ "SELECT * FROM authors_ ORDER BY name" mapDB_ $ \(aid::Int64, name) -> printLn $ show aid <> ":" <+> name -- | Display books. ("books", "") -> runDBT cs def $ do runSQL_ "SELECT a.name, ARRAY(SELECT (b.id, b.name, b.year)::book_ FROM books_ b WHERE b.author_id = a.id) FROM authors_ a ORDER BY a.name" mapDB_ $ \(author, CompositeArray1 (books::[Book])) -> do printLn $ author <> ":" forM_ books $ \book -> printLn $ "*" <+> show book -- | Insert an author. ("insert_author", mname) -> case mread mname of Just (name::String) -> runDBT cs def . runQuery_ $ "INSERT INTO authors_ (name) VALUES (" name <+> ")" Nothing -> printLn $ "Invalid name" -- | Insert a book. ("insert_book", mbook) -> case mread mbook of Just record -> runDBT cs def . runQuery_ $ rawSQL "INSERT INTO books_ (name, year, author_id) VALUES ($1, $2, $3)" (record::(String, Int32, Int64)) Nothing -> printLn $ "Invalid book record" -- | Handle unknown commands. _ -> printLn $ "Unknown command:" <+> cmd where parse = second (drop 1) . break (==' ') -- | Example chain of commands: -- -- > insert_author "John Doe" -- > insert_author "Mary Jane" -- > authors -- > insert_book ("The Sunset", 2006, 1) -- > insert_book ("Waterfall", 2011, 2) -- > insert_book ("The Sunrise", 2013, 1) -- > books -- -- If you want to check out exceptions in action, -- try inserting a book with invalid author id. main :: IO () main = do cs <- getConnSettings withCatalog cs $ do pool <- poolSource (cs { csComposites = ["book_"] }) 1 10 4 fix $ \next -> readline "> " >>= maybe (printLn "") (\cmd -> do when (cmd /= "quit") $ do processCommand pool cmd addHistory cmd next )