-- | Module: Squeal.PostgreSQL
-- Description: Squeel export module
-- Copyright: (c) Eitan Chatav, 2017
-- Maintainer: eitan@morphism.tech
-- Stability: experimental
--
-- Squeal is a deep embedding of [PostgreSQL](https://www.postgresql.org) in Haskell.
-- Let's see an example!
--
-- First, we need some language extensions because Squeal uses modern GHC
-- features.
--
-- >>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels
-- >>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators
-- 
-- We'll need some imports.
--
-- >>> import Control.Monad (void)
-- >>> import Control.Monad.Base (liftBase)
-- >>> import Data.Int (Int32)
-- >>> import Data.Text (Text)
-- >>> import Squeal.PostgreSQL
--
-- We'll use generics to easily convert between Haskell and PostgreSQL values.
--
-- >>> import qualified Generics.SOP as SOP
-- >>> import qualified GHC.Generics as GHC
--
-- The first step is to define the schema of our database. This is where
-- we use @DataKinds@ and @TypeOperators@.
--
-- >>> :{
-- type Schema =
--   '[ "users" :::
--        '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
--        '[ "id"   :::   'Def :=> 'NotNull 'PGint4
--         , "name" ::: 'NoDef :=> 'NotNull 'PGtext
--         ]
--    , "emails" :::
--        '[ "pk_emails"  ::: 'PrimaryKey '["id"]
--         , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
--         ] :=>
--        '[ "id"      :::   'Def :=> 'NotNull 'PGint4
--         , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
--         , "email"   ::: 'NoDef :=>    'Null 'PGtext
--         ]
--    ]
-- :}
--
-- Notice the use of type operators. `:::` is used
-- to pair an alias `Symbol` with either a `TableType` or a `ColumnType`.
-- `:=>` is used to pair a `TableConstraint`s with a `ColumnsType`,
-- yielding a `TableType`, or to pair a `ColumnConstraint` with a `NullityType`,
-- yielding a `ColumnType`.
--
-- Next, we'll write `Definition`s to set up and tear down the schema. In
-- Squeal, a `Definition` is a `createTable`, `alterTable` or `dropTable`
-- command and has two type parameters, corresponding to the schema
-- before being run and the schema after. We can compose definitions using
-- `>>>`. Here and in the rest of our commands we make use of overloaded
-- labels to refer to named tables and columns in our schema.
--
-- >>> :{
-- let
--   setup :: Definition '[] Schema
--   setup = 
--    createTable #users
--      ( serial `As` #id :*
--        (text & notNull) `As` #name :* Nil )
--      ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil ) >>>
--    createTable #emails
--      ( serial `As` #id :*
--        (int & notNull) `As` #user_id :*
--        text `As` #email :* Nil )
--      ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
--        foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
--          OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
-- :}
--
-- We can easily see the generated SQL is unsuprising looking.
--
-- >>> renderDefinition setup
-- "CREATE TABLE \"users\" (\"id\" serial, \"name\" text NOT NULL, CONSTRAINT \"pk_users\" PRIMARY KEY (\"id\")); CREATE TABLE \"emails\" (\"id\" serial, \"user_id\" int NOT NULL, \"email\" text, CONSTRAINT \"pk_emails\" PRIMARY KEY (\"id\"), CONSTRAINT \"fk_user_id\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE);"
--
-- Notice that @setup@ starts with an empty schema @'[]@ and produces @Schema@.
-- In our `createTable` commands we included `TableConstraint`s to define
-- primary and foreign keys, making them somewhat complex. Our tear down
-- `Definition` is simpler.
--
-- >>> :{
-- let
--   teardown :: Definition Schema '[]
--   teardown = dropTable #emails >>> dropTable #users
-- :}
--
-- >>> renderDefinition teardown
-- "DROP TABLE \"emails\"; DROP TABLE \"users\";"
--
-- Next, we'll write `Manipulation`s to insert data into our two tables.
-- A `Manipulation` is an `insertRow` (or other inserts), `update`
-- or `deleteFrom` command and
-- has three type parameters, the schema it refers to, a list of parameters
-- it can take as input, and a list of columns it produces as output. When
-- we insert into the users table, we will need a parameter for the @name@
-- field but not for the @id@ field. Since it's optional, we can use a default
-- value. However, since the emails table refers to the users table, we will
-- need to retrieve the user id that the insert generates and insert it into
-- the emails table. Take a careful look at the type and definition of both
-- of our inserts.
--
-- >>> :{
-- let
--   insertUser :: Manipulation Schema '[ 'NotNull 'PGtext ] '[ "fromOnly" ::: 'NotNull 'PGint4 ]
--   insertUser = insertRow #users
--     (Default `As` #id :* Set (param @1) `As` #name :* Nil)
--     OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
-- :}
--
-- >>> :{
-- let
--   insertEmail :: Manipulation Schema '[ 'NotNull 'PGint4, 'Null 'PGtext] '[]
--   insertEmail = insertRow #emails
--     ( Default `As` #id :*
--       Set (param @1) `As` #user_id :*
--       Set (param @2) `As` #email :* Nil )
--     OnConflictDoNothing (Returning Nil)
-- :}
--
-- >>> renderManipulation insertUser
-- "INSERT INTO \"users\" (\"id\", \"name\") VALUES (DEFAULT, ($1 :: text)) ON CONFLICT DO NOTHING RETURNING \"id\" AS \"fromOnly\";"
-- >>> renderManipulation insertEmail
-- "INSERT INTO \"emails\" (\"id\", \"user_id\", \"email\") VALUES (DEFAULT, ($1 :: int4), ($2 :: text)) ON CONFLICT DO NOTHING;"
--
-- Next we write a `Query` to retrieve users from the database. We're not
-- interested in the ids here, just the usernames and email addresses. We
-- need to use an inner join to get the right result. A `Query` is like a
-- `Manipulation` with the same kind of type parameters.
--
-- >>> :{
-- let
--   getUsers :: Query Schema '[]
--     '[ "userName"  ::: 'NotNull 'PGtext
--      , "userEmail" :::    'Null 'PGtext ]
--   getUsers = select
--     (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
--     ( from (table (#users `As` #u)
--       & innerJoin (table (#emails `As` #e))
--         (#u ! #id .== #e ! #user_id)) )
-- :}
--
-- >>> renderQuery getUsers
-- "SELECT \"u\".\"name\" AS \"userName\", \"e\".\"email\" AS \"userEmail\" FROM \"users\" AS \"u\" INNER JOIN \"emails\" AS \"e\" ON (\"u\".\"id\" = \"e\".\"user_id\")"
--
-- Now that we've defined the SQL side of things, we'll need a Haskell type
-- for users. We give the type `Generics.SOP.Generic` and
-- `Generics.SOP.HasDatatypeInfo` instances so that we can decode the rows
-- we receive when we run @getUsers@. Notice that the record fields of the
-- @User@ type match the column names of @getUsers@.
-- 
-- >>> data User = User { userName :: Text, userEmail :: Maybe Text } deriving (Show, GHC.Generic)
-- >>> instance SOP.Generic User
-- >>> instance SOP.HasDatatypeInfo User
--
-- Let's also create some users to add to the database.
--
-- >>> :{
-- let
--   users :: [User]
--   users = 
--     [ User "Alice" (Just "alice@gmail.com")
--     , User "Bob" Nothing
--     , User "Carole" (Just "carole@hotmail.com")
--     ]
-- :}
--
-- Now we can put together all the pieces into a program. The program
-- connects to the database, sets up the schema, inserts the user data
-- (using prepared statements as an optimization), queries the user
-- data and prints it out and finally closes the connection. We can thread
-- the changing schema information through by using the indexed `PQ` monad
-- transformer and when the schema doesn't change we can use `Monad` and
-- `MonadPQ` functionality.
--
-- >>> :{
-- let
--   session :: PQ Schema Schema IO ()
--   session = do
--     idResults <- traversePrepared insertUser (Only . userName <$> users)
--     ids <- traverse (fmap fromOnly . getRow 0) idResults
--     traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
--     usersResult <- runQuery getUsers
--     usersRows <- getRows usersResult
--     liftBase $ print (usersRows :: [User])
-- :}
--
-- >>> :{
-- void . withConnection "host=localhost port=5432 dbname=exampledb" $
--   define setup
--   & pqThen session
--   & pqThen (define teardown)
-- :}
-- [User {userName = "Alice", userEmail = Just "alice@gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just "carole@hotmail.com"}]

module Squeal.PostgreSQL
  ( module Squeal.PostgreSQL.Binary
  , module Squeal.PostgreSQL.Definition
  , module Squeal.PostgreSQL.Expression
  , module Squeal.PostgreSQL.Manipulation
  , module Squeal.PostgreSQL.PQ
  , module Squeal.PostgreSQL.Query
  , module Squeal.PostgreSQL.Schema
  , module Squeal.PostgreSQL.Transaction
  ) where

import Squeal.PostgreSQL.Binary
import Squeal.PostgreSQL.Definition
import Squeal.PostgreSQL.Expression
import Squeal.PostgreSQL.Manipulation
import Squeal.PostgreSQL.PQ
import Squeal.PostgreSQL.Query
import Squeal.PostgreSQL.Schema
import Squeal.PostgreSQL.Transaction