| 
 Module : Squeal . PostgreSQL.Session . Migration 
 Description : migrations 
 Copyright : ( c )   , 2019 
 Maintainer :  
 Stability : experimental 

 This module defines a ` Migration ` type to safely 
 change the schema of your database over time . Let 's see an example ! 

 First turn on some extensions . 

 > > > : set -XDataKinds -XOverloadedLabels 
 > > > : set -XOverloadedStrings -XFlexibleContexts -XTypeOperators 

 Next , let 's define our ` TableType`s . 

 > > > : { 
 type UsersTable = 
   ' [ " pk_users " : : : ' PrimaryKey ' [ " i d " ] ] : = > 
   ' [ " i d " : : : ' Def : = > ' NotNull ' PGint4 
    , " name " : : : ' NoDef : = > ' NotNull ' PGtext 
    ] 
 :} 

 > > > : { 
 type EmailsTable = 
   ' [ " pk_emails " : : : ' PrimaryKey ' [ " i d " ] 
    , " fk_user_id " : : : ' ForeignKey ' [ " user_id " ] " public " " users " ' [ " i d " ] 
    ] : = > 
   ' [ " i d " : : : ' Def : = > ' NotNull ' PGint4 
    , " user_id " : : : ' NoDef : = > ' NotNull ' PGint4 
    , " email " : : : ' NoDef : = > ' Null ' PGtext 
    ] 
 :} 

 Now we can define some ` Migration`s to make our tables . 

 ` Migration`s are parameterized giving the option of a 

 * pure one - way ` Migration ` ` Definition ` 
 * impure one - way ` Migration ` @(@`Indexed ` ` PQ ` ` IO`@)@ 
 * pure reversible ` Migration ` @(@`IsoQ ` ` Definition`@)@ 
 * impure reversible ` Migration ` @(@`IsoQ ` @(@`Indexed ` ` PQ ` ` IO`@)@@)@ 

 For this example , we 'll use pure reversible ` Migration`s . 

 > > > : { 
 let 
   makeUsers : : Migration ( IsoQ Definition ) 
     ' [ " public " : : : ' [ ] ] 
     ' [ " public " : : : ' [ " users " : : : ' Table UsersTable ] ] 
   makeUsers = Migration " make users table " IsoQ 
     { up = createTable # users 
         ( serial ` as ` # i d :* 
           notNullable text ` as ` # name ) 
         ( primaryKey # i d ` as ` # pk_users ) 
     , down = dropTable # users 
     } 
 :} 

 > > > : { 
 let 
   makeEmails : : Migration ( IsoQ Definition ) 
     ' [ " public " : : : ' [ " users " : : : ' Table UsersTable ] ] 
     ' [ " public " : : : ' [ " users " : : : ' Table UsersTable , " emails " : : : ' Table EmailsTable ] ] 
   makeEmails = Migration " make emails table " IsoQ 
     { up = createTable # emails 
           ( serial ` as ` # i d :* 
             notNullable int ` as ` # user_id :* 
             nullable text ` as ` # email ) 
           ( primaryKey # i d ` as ` # pk_emails :* 
             foreignKey # user_id # users # i d 
               (   ) (   ) ` as ` # fk_user_id ) 
     , down = dropTable # emails 
     } 
 :} 

 Now that we have a couple migrations we can chain them together into a ` Path ` . 

 > > > let migrations = makeUsers :> > makeEmails :> > Done 

 Now run the migrations . 

 > > > import Control . Monad . IO.Class 
 > > > : { 
 withConnection " host = localhost port=5432 dbname = exampledb user = postgres password = postgres " $ 
   manipulate _ ( UnsafeManipulation " SET client_min_messages TO WARNING ; " ) 
     -- suppress notices 
   & pqThen ( liftIO ( putStrLn " Migrate " ) ) 
   & pqThen (  migrations ) 
   & pqThen ( liftIO ( putStrLn " Rollback " ) ) 
   & pqThen ( migrateDown migrations ) 
 :} 
 Migrate 
 Rollback 

 We can also create a simple executable using ` mainMigrateIso ` . 

 > > > let main = mainMigrateIso " host = localhost port=5432 dbname = exampledb user = postgres password = postgres " migrations 

 > > > withArgs [ ] main 
 Invalid command : " " . Use : 
 migrate     to run all available migrations 
 rollback    to rollback all available migrations 
 status      to display migrations run and migrations left to run 

 > > > withArgs [ " status " ] main 
 Migrations already run : 
   None 
 Migrations left to run : 
   - make users table 
   - make emails table 

 > > > withArgs [ " migrate " ] main 
 Migrations already run : 
   - make users table 
   - make emails table 
 Migrations left to run : 
   None 

 > > > withArgs [ " rollback " ] main 
 Migrations already run : 
   None 
 Migrations left to run : 
   - make users table 
   - make emails table 

 In addition to enabling ` Migration`s using pure SQL ` Definition`s for 
 the ` up ` and ` down ` migrations , you can also perform impure ` IO ` actions 
 by using a ` Migration`s over the ` Indexed ` ` PQ ` ` IO ` category . 

Module: Squeal.PostgreSQL.Session.Migration
Description: migrations
Copyright: (c) Eitan Chatav, 2019
Maintainer: 
Stability: experimental

This module defines a `Migration` type to safely
change the schema of your database over time. Let's see an example!

First turn on some extensions.

>>> :set -XDataKinds -XOverloadedLabels
>>> :set -XOverloadedStrings -XFlexibleContexts -XTypeOperators

Next, let's define our `TableType`s.

>>> :{
type UsersTable =
  '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
  '[ "id" ::: 'Def :=> 'NotNull 'PGint4
   , "name" ::: 'NoDef :=> 'NotNull 'PGtext
   ]
:}

>>> :{
type EmailsTable =
  '[ "pk_emails" ::: 'PrimaryKey '["id"]
   , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"]
   ] :=>
  '[ "id" ::: 'Def :=> 'NotNull 'PGint4
   , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
   , "email" ::: 'NoDef :=> 'Null 'PGtext
   ]
:}

Now we can define some `Migration`s to make our tables.

`Migration`s are parameterized giving the option of a

* pure one-way `Migration` `Definition`
* impure one-way `Migration` @(@`Indexed` `PQ` `IO`@)@
* pure reversible `Migration` @(@`IsoQ` `Definition`@)@
* impure reversible `Migration` @(@`IsoQ` @(@`Indexed` `PQ` `IO`@)@@)@

For this example, we'll use pure reversible `Migration`s.

>>> :{
let
  makeUsers :: Migration (IsoQ Definition)
    '["public" ::: '[]]
    '["public" ::: '["users" ::: 'Table UsersTable]]
  makeUsers = Migration "make users table" IsoQ
    { up = createTable #users
        ( serial `as` #id :*
          notNullable text `as` #name )
        ( primaryKey #id `as` #pk_users )
    , down = dropTable #users
    }
:}

>>> :{
let
  makeEmails :: Migration (IsoQ Definition)
    '["public" ::: '["users" ::: 'Table UsersTable]]
    '["public" ::: '["users" ::: 'Table UsersTable, "emails" ::: 'Table EmailsTable]]
  makeEmails = Migration "make emails table" IsoQ
    { up = createTable #emails
          ( serial `as` #id :*
            notNullable int `as` #user_id :*
            nullable text `as` #email )
          ( primaryKey #id `as` #pk_emails :*
            foreignKey #user_id #users #id
              (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id )
    , down = dropTable #emails
    }
:}

Now that we have a couple migrations we can chain them together into a `Path`.

>>> let migrations = makeUsers :>> makeEmails :>> Done

Now run the migrations.

>>> import Control.Monad.IO.Class
>>> :{
withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $
  manipulate_ (UnsafeManipulation "SET client_min_messages TO WARNING;")
    -- suppress notices
  & pqThen (liftIO (putStrLn "Migrate"))
  & pqThen (migrateUp migrations)
  & pqThen (liftIO (putStrLn "Rollback"))
  & pqThen (migrateDown migrations)
:}
Migrate
Rollback

We can also create a simple executable using `mainMigrateIso`.

>>> let main = mainMigrateIso "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" migrations

>>> withArgs [] main
Invalid command: "". Use:
migrate    to run all available migrations
rollback   to rollback all available migrations
status     to display migrations run and migrations left to run

>>> withArgs ["status"] main
Migrations already run:
  None
Migrations left to run:
  - make users table
  - make emails table

>>> withArgs ["migrate"] main
Migrations already run:
  - make users table
  - make emails table
Migrations left to run:
  None

>>> withArgs ["rollback"] main
Migrations already run:
  None
Migrations left to run:
  - make users table
  - make emails table

In addition to enabling `Migration`s using pure SQL `Definition`s for
the `up` and `down` migrations, you can also perform impure `IO` actions
by using a `Migration`s over the `Indexed` `PQ` `IO` category.
-}

# LANGUAGE 
     DataKinds 
   , DeriveGeneric 
   , FlexibleContexts 
   , FlexibleInstances 
   , FunctionalDependencies 
   , GADTs 
   , LambdaCase 
   , MultiParamTypeClasses 
   , OverloadedLabels 
   , OverloadedStrings 
   , PolyKinds 
   , QuantifiedConstraints 
   , RankNTypes 
   , TypeApplications 
   , TypeOperators 
 #
    DataKinds
  , DeriveGeneric
  , FlexibleContexts
  , FlexibleInstances
  , FunctionalDependencies
  , GADTs
  , LambdaCase
  , MultiParamTypeClasses
  , OverloadedLabels
  , OverloadedStrings
  , PolyKinds
  , QuantifiedConstraints
  , RankNTypes
  , TypeApplications
  , TypeOperators
#-}

module Squeal.PostgreSQL.Session.Migration
  ( -- * Migration
    Migration (..)
  , Migratory (..)
  , migrate
  , migrateUp
  , migrateDown
  , MigrationsTable
    -- * Executable
  , mainMigrate
  , mainMigrateIso
    -- * Re-export
  , IsoQ (..)
  ) where

import Control.Category
import Control.Category.Free
import Control.Monad
import Control.Monad.IO.Class
import Data.ByteString (ByteString)
import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.List ((\\))
import Data.Quiver
import Data.Quiver.Functor
import Data.Text (Text)
import Data.Time (UTCTime)
import Prelude hiding ((.), id)
import System.Environment

import qualified Data.Text.IO as Text (putStrLn)
import qualified Generics.SOP as SOP
import qualified GHC.Generics as GHC

import Squeal.PostgreSQL.Definition
import Squeal.PostgreSQL.Definition.Constraint
import Squeal.PostgreSQL.Definition.Table
import Squeal.PostgreSQL.Expression.Comparison
import Squeal.PostgreSQL.Expression.Default
import Squeal.PostgreSQL.Expression.Parameter
import Squeal.PostgreSQL.Expression.Time
import Squeal.PostgreSQL.Expression.Type
import Squeal.PostgreSQL.Manipulation
import Squeal.PostgreSQL.Manipulation.Delete
import Squeal.PostgreSQL.Manipulation.Insert
import Squeal.PostgreSQL.Session
import Squeal.PostgreSQL.Session.Decode
import Squeal.PostgreSQL.Session.Encode
import Squeal.PostgreSQL.Session.Indexed
import Squeal.PostgreSQL.Session.Monad
import Squeal.PostgreSQL.Session.Result
import Squeal.PostgreSQL.Session.Statement
import Squeal.PostgreSQL.Session.Transaction.Unsafe
import Squeal.PostgreSQL.Query.From
import Squeal.PostgreSQL.Query.Select
import Squeal.PostgreSQL.Query.Table
import Squeal.PostgreSQL.Type.Alias
import Squeal.PostgreSQL.Type.List
import Squeal.PostgreSQL.Type.Schema

-- | A `Migration` consists of a name and a migration definition.
data Migration def db0 db1 = Migration
  { migrationName :: Text -- ^ The name of a `Migration`.
    -- Each `migrationName` should be unique.
  , migrationDef :: def db0 db1 -- ^ The migration of a `Migration`.
  } deriving (GHC.Generic)
instance QFunctor Migration where
  qmap f (Migration n i) = Migration n (f i)

  | 
 A ` Migratory ` ` Category ` can run or 
 possibly rewind a ` Path ` of ` Migration`s . 

A `Migratory` `Category` can run or
possibly rewind a `Path` of `Migration`s.
-}
class (Category def, Category run) => Migratory def run | def -> run where
  | Run a ` Path ` of ` Migration`s .
  runMigrations :: Path (Migration def) db0 db1 -> run db0 db1
-- | impure migrations
instance Migratory (Indexed PQ IO ()) (Indexed PQ IO ()) where
  runMigrations path = Indexed . unsafePQ . transactionally_ $ do
    define createMigrations
    qtoMonoid upMigration path
    where
      upMigration step = do
        executed <- do
          result <- executeParams selectMigration (migrationName step)
          ntuples (result :: Result UTCTime)
        unless (executed == 1) $ do
          _ <- unsafePQ . runIndexed $ migrationDef step
          executeParams_ insertMigration (migrationName step)
-- | pure migrations
instance Migratory Definition (Indexed PQ IO ()) where
  runMigrations = runMigrations . qmap (qmap ixDefine)
-- | impure rewinds
instance Migratory (OpQ (Indexed PQ IO ())) (OpQ (Indexed PQ IO ())) where
  runMigrations path = OpQ . Indexed . unsafePQ . transactionally_ $ do
    define createMigrations
    qtoMonoid @FoldPath downMigration (reversePath path)
    where
      downMigration (OpQ step) = do
        executed <- do
          result <- executeParams selectMigration (migrationName step)
          ntuples (result :: Result UTCTime)
        unless (executed == 0) $ do
          _ <- unsafePQ . runIndexed . getOpQ $ migrationDef step
          executeParams_ deleteMigration (migrationName step)
-- | pure rewinds
instance Migratory (OpQ Definition) (OpQ (Indexed PQ IO ())) where
  runMigrations = runMigrations . qmap (qmap (qmap ixDefine))
-- | impure rewindable migrations
instance Migratory
  (IsoQ (Indexed PQ IO ()))
  (IsoQ (Indexed PQ IO ())) where
    runMigrations path = IsoQ
      (runMigrations (qmap (qmap up) path))
      (getOpQ (runMigrations (qmap (qmap (OpQ . down)) path)))
-- | pure rewindable migrations
instance Migratory (IsoQ Definition) (IsoQ (Indexed PQ IO ())) where
  runMigrations = runMigrations . qmap (qmap (qmap ixDefine))

unsafePQ :: (Functor m) => PQ db0 db1 m x -> PQ db0' db1' m x
unsafePQ (PQ pq) = PQ $ fmap (SOP.K . SOP.unK) . pq . SOP.K . SOP.unK

-- | Run migrations.
migrate
  :: Migratory def (Indexed PQ IO ())
  => Path (Migration def) db0 db1
  -> PQ db0 db1 IO ()
migrate = runIndexed . runMigrations

-- | Run rewindable migrations.
migrateUp
  :: Migratory def (IsoQ (Indexed PQ IO ()))
  => Path (Migration def) db0 db1
  -> PQ db0 db1 IO ()
migrateUp = runIndexed . up . runMigrations

-- | Rewind migrations.
migrateDown
  :: Migratory def (IsoQ (Indexed PQ IO ()))
  => Path (Migration def) db0 db1
  -> PQ db1 db0 IO ()
migrateDown = runIndexed . down . runMigrations

ixDefine :: Definition db0 db1 -> Indexed PQ IO () db0 db1
ixDefine = indexedDefine

  | The ` TableType ` for a Squeal migration .
type MigrationsTable =
  '[ "migrations_unique_name" ::: 'Unique '["name"]] :=>
  '[ "name"        ::: 'NoDef :=> 'NotNull 'PGtext
   , "executed_at" :::   'Def :=> 'NotNull 'PGtimestamptz
   ]

data MigrationRow =
  MigrationRow { name :: Text
               , executed_at :: UTCTime }
  deriving (GHC.Generic, Show)

instance SOP.Generic MigrationRow
instance SOP.HasDatatypeInfo MigrationRow

type MigrationsSchema = '["schema_migrations" ::: 'Table MigrationsTable]
type MigrationsSchemas = Public MigrationsSchema

  | Creates a ` MigrationsTable ` if it does not already exist .
createMigrations :: Definition MigrationsSchemas MigrationsSchemas
createMigrations =
  createTableIfNotExists #schema_migrations
    ( (text & notNullable) `as` #name :*
      (timestampWithTimeZone & notNullable & default_ currentTimestamp)
        `as` #executed_at )
    ( unique #name `as` #migrations_unique_name )

  | Inserts a ` Migration ` into the ` MigrationsTable ` , returning
-- the time at which it was inserted.
insertMigration :: Statement MigrationsSchemas Text ()
insertMigration = Manipulation aParam genericRow $
  insertInto_ #schema_migrations $
    Values_ (Set (param @1) `as` #name :* Default `as` #executed_at)

  | Deletes a ` Migration ` from the ` MigrationsTable ` , returning
-- the time at which it was inserted.
deleteMigration :: Statement MigrationsSchemas Text ()
deleteMigration = Manipulation aParam genericRow $
  deleteFrom_ #schema_migrations (#name .== param @1)

  | Selects a ` Migration ` from the ` MigrationsTable ` , returning
-- the time at which it was inserted.
selectMigration :: Statement MigrationsSchemas Text UTCTime
selectMigration = Query aParam #executed_at $
  select_ #executed_at
    $ from (table (#schema_migrations))
    & where_ (#name .== param @1)

selectMigrations :: Statement MigrationsSchemas () MigrationRow
selectMigrations = query $ select Star (from (table #schema_migrations))

  | ` mainMigrate ` creates a simple executable 
 from a connection string and a ` Path ` of ` Migration`s .
from a connection string and a `Path` of `Migration`s. -}
mainMigrate
  :: Migratory p (Indexed PQ IO ())
  => ByteString
  -- ^ connection string
  -> Path (Migration p) db0 db1
  -- ^ migrations
  -> IO ()
mainMigrate connectTo migrations = do
  command <- getArgs
  performCommand command

  where

    performCommand :: [String] -> IO ()
    performCommand = \case
      ["status"] -> withConnection connectTo $
        suppressNotices >> migrateStatus
      ["migrate"] -> withConnection connectTo $
        suppressNotices
        & pqThen (runIndexed (runMigrations migrations))
        & pqThen migrateStatus
      args -> displayUsage args

    migrateStatus :: PQ schema schema IO ()
    migrateStatus = unsafePQ $ do
      runNames <- getRunMigrationNames
      let names = qtoList migrationName migrations
          unrunNames = names \\ runNames
      liftIO $ displayRunned runNames >> displayUnrunned unrunNames

    suppressNotices :: PQ schema schema IO ()
    suppressNotices = manipulate_ $
      UnsafeManipulation "SET client_min_messages TO WARNING;"

    displayUsage :: [String] -> IO ()
    displayUsage args = do
      putStrLn $ "Invalid command: \"" <> unwords args <> "\". Use:"
      putStrLn "migrate    to run all available migrations"
      putStrLn "rollback   to rollback all available migrations"

  | ` mainMigrateIso ` creates a simple executable 
 from a connection string and a ` Path ` of ` Migration ` ` IsoQ`s .
from a connection string and a `Path` of `Migration` `IsoQ`s. -}
mainMigrateIso
  :: Migratory (IsoQ def) (IsoQ (Indexed PQ IO ()))
  => ByteString
  -- ^ connection string
  -> Path (Migration (IsoQ def)) db0 db1
  -- ^ migrations
  -> IO ()
mainMigrateIso connectTo migrations = performCommand =<< getArgs

  where

    performCommand :: [String] -> IO ()
    performCommand = \case
      ["status"] -> withConnection connectTo $
        suppressNotices >> migrateStatus
      ["migrate"] -> withConnection connectTo $
        suppressNotices
        & pqThen (migrateUp migrations)
        & pqThen migrateStatus
      ["rollback"] -> withConnection connectTo $
        suppressNotices
        & pqThen (migrateDown migrations)
        & pqThen migrateStatus
      args -> displayUsage args

    migrateStatus :: PQ schema schema IO ()
    migrateStatus = unsafePQ $ do
      runNames <- getRunMigrationNames
      let names = qtoList migrationName migrations
          unrunNames = names \\ runNames
      liftIO $ displayRunned runNames >> displayUnrunned unrunNames

    suppressNotices :: PQ schema schema IO ()
    suppressNotices = manipulate_ $
      UnsafeManipulation "SET client_min_messages TO WARNING;"

    displayUsage :: [String] -> IO ()
    displayUsage args = do
      putStrLn $ "Invalid command: \"" <> unwords args <> "\". Use:"
      putStrLn "migrate    to run all available migrations"
      putStrLn "rollback   to rollback all available migrations"
      putStrLn "status     to display migrations run and migrations left to run"

getRunMigrationNames :: PQ db0 db0 IO [Text]
getRunMigrationNames =
  fmap name <$>
    (unsafePQ (define createMigrations
    & pqThen (execute selectMigrations)) >>= getRows)

displayListOfNames :: [Text] -> IO ()
displayListOfNames [] = Text.putStrLn "  None"
displayListOfNames xs =
  let singleName n = Text.putStrLn $ "  - " <> n
  in traverse_ singleName xs

displayUnrunned :: [Text] -> IO ()
displayUnrunned unrunned =
  Text.putStrLn "Migrations left to run:"
  >> displayListOfNames unrunned

displayRunned :: [Text] -> IO ()
displayRunned runned =
  Text.putStrLn "Migrations already run:"
  >> displayListOfNames runned

 suppress notices 
 suppress notices
 * Migration
 * Executable
 * Re-export
 | A `Migration` consists of a name and a migration definition.
 ^ The name of a `Migration`.
 Each `migrationName` should be unique.
 ^ The migration of a `Migration`.
 | impure migrations
 | pure migrations
 | impure rewinds
 | pure rewinds
 | impure rewindable migrations
 | pure rewindable migrations
 | Run migrations.
 | Run rewindable migrations.
 | Rewind migrations.
 the time at which it was inserted.
 the time at which it was inserted.
 the time at which it was inserted.
 ^ connection string
 ^ migrations
 ^ connection string
 ^ migrations
| 
 Module : Squeal . PostgreSQL.Session . Migration 
 Description : migrations 
 Copyright : ( c )   , 2019 
 Maintainer :  
 Stability : experimental 

 This module defines a ` Migration ` type to safely 
 change the schema of your database over time . Let 's see an example ! 

 First turn on some extensions . 

 > > > : set -XDataKinds -XOverloadedLabels 
 > > > : set -XOverloadedStrings -XFlexibleContexts -XTypeOperators 

 Next , let 's define our ` TableType`s . 

 > > > : { 
 type UsersTable = 
   ' [ " pk_users " : : : ' PrimaryKey ' [ " i d " ] ] : = > 
   ' [ " i d " : : : ' Def : = > ' NotNull ' PGint4 
    , " name " : : : ' NoDef : = > ' NotNull ' PGtext 
    ] 
 :} 

 > > > : { 
 type EmailsTable = 
   ' [ " pk_emails " : : : ' PrimaryKey ' [ " i d " ] 
    , " fk_user_id " : : : ' ForeignKey ' [ " user_id " ] " public " " users " ' [ " i d " ] 
    ] : = > 
   ' [ " i d " : : : ' Def : = > ' NotNull ' PGint4 
    , " user_id " : : : ' NoDef : = > ' NotNull ' PGint4 
    , " email " : : : ' NoDef : = > ' Null ' PGtext 
    ] 
 :} 

 Now we can define some ` Migration`s to make our tables . 

 ` Migration`s are parameterized giving the option of a 

 * pure one - way ` Migration ` ` Definition ` 
 * impure one - way ` Migration ` @(@`Indexed ` ` PQ ` ` IO`@)@ 
 * pure reversible ` Migration ` @(@`IsoQ ` ` Definition`@)@ 
 * impure reversible ` Migration ` @(@`IsoQ ` @(@`Indexed ` ` PQ ` ` IO`@)@@)@ 

 For this example , we 'll use pure reversible ` Migration`s . 

 > > > : { 
 let 
   makeUsers : : Migration ( IsoQ Definition ) 
     ' [ " public " : : : ' [ ] ] 
     ' [ " public " : : : ' [ " users " : : : ' Table UsersTable ] ] 
   makeUsers = Migration " make users table " IsoQ 
     { up = createTable # users 
         ( serial ` as ` # i d :* 
           notNullable text ` as ` # name ) 
         ( primaryKey # i d ` as ` # pk_users ) 
     , down = dropTable # users 
     } 
 :} 

 > > > : { 
 let 
   makeEmails : : Migration ( IsoQ Definition ) 
     ' [ " public " : : : ' [ " users " : : : ' Table UsersTable ] ] 
     ' [ " public " : : : ' [ " users " : : : ' Table UsersTable , " emails " : : : ' Table EmailsTable ] ] 
   makeEmails = Migration " make emails table " IsoQ 
     { up = createTable # emails 
           ( serial ` as ` # i d :* 
             notNullable int ` as ` # user_id :* 
             nullable text ` as ` # email ) 
           ( primaryKey # i d ` as ` # pk_emails :* 
             foreignKey # user_id # users # i d 
               (   ) (   ) ` as ` # fk_user_id ) 
     , down = dropTable # emails 
     } 
 :} 

 Now that we have a couple migrations we can chain them together into a ` Path ` . 

 > > > let migrations = makeUsers :> > makeEmails :> > Done 

 Now run the migrations . 

 > > > import Control . Monad . IO.Class 
 > > > : { 
 withConnection " host = localhost port=5432 dbname = exampledb user = postgres password = postgres " $ 
   manipulate _ ( UnsafeManipulation " SET client_min_messages TO WARNING ; " ) 
   & pqThen ( liftIO ( putStrLn " Migrate " ) ) 
   & pqThen (  migrations ) 
   & pqThen ( liftIO ( putStrLn " Rollback " ) ) 
   & pqThen ( migrateDown migrations ) 
 :} 
 Migrate 
 Rollback 

 We can also create a simple executable using ` mainMigrateIso ` . 

 > > > let main = mainMigrateIso " host = localhost port=5432 dbname = exampledb user = postgres password = postgres " migrations 

 > > > withArgs [ ] main 
 Invalid command : " " . Use : 
 migrate     to run all available migrations 
 rollback    to rollback all available migrations 
 status      to display migrations run and migrations left to run 

 > > > withArgs [ " status " ] main 
 Migrations already run : 
   None 
 Migrations left to run : 
   - make users table 
   - make emails table 

 > > > withArgs [ " migrate " ] main 
 Migrations already run : 
   - make users table 
   - make emails table 
 Migrations left to run : 
   None 

 > > > withArgs [ " rollback " ] main 
 Migrations already run : 
   None 
 Migrations left to run : 
   - make users table 
   - make emails table 

 In addition to enabling ` Migration`s using pure SQL ` Definition`s for 
 the ` up ` and ` down ` migrations , you can also perform impure ` IO ` actions 
 by using a ` Migration`s over the ` Indexed ` ` PQ ` ` IO ` category . 

Module: Squeal.PostgreSQL.Session.Migration
Description: migrations
Copyright: (c) Eitan Chatav, 2019
Maintainer: 
Stability: experimental

This module defines a `Migration` type to safely
change the schema of your database over time. Let's see an example!

First turn on some extensions.

>>> :set -XDataKinds -XOverloadedLabels
>>> :set -XOverloadedStrings -XFlexibleContexts -XTypeOperators

Next, let's define our `TableType`s.

>>> :{
type UsersTable =
  '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
  '[ "id" ::: 'Def :=> 'NotNull 'PGint4
   , "name" ::: 'NoDef :=> 'NotNull 'PGtext
   ]
:}

>>> :{
type EmailsTable =
  '[ "pk_emails" ::: 'PrimaryKey '["id"]
   , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"]
   ] :=>
  '[ "id" ::: 'Def :=> 'NotNull 'PGint4
   , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
   , "email" ::: 'NoDef :=> 'Null 'PGtext
   ]
:}

Now we can define some `Migration`s to make our tables.

`Migration`s are parameterized giving the option of a

* pure one-way `Migration` `Definition`
* impure one-way `Migration` @(@`Indexed` `PQ` `IO`@)@
* pure reversible `Migration` @(@`IsoQ` `Definition`@)@
* impure reversible `Migration` @(@`IsoQ` @(@`Indexed` `PQ` `IO`@)@@)@

For this example, we'll use pure reversible `Migration`s.

>>> :{
let
  makeUsers :: Migration (IsoQ Definition)
    '["public" ::: '[]]
    '["public" ::: '["users" ::: 'Table UsersTable]]
  makeUsers = Migration "make users table" IsoQ
    { up = createTable #users
        ( serial `as` #id :*
          notNullable text `as` #name )
        ( primaryKey #id `as` #pk_users )
    , down = dropTable #users
    }
:}

>>> :{
let
  makeEmails :: Migration (IsoQ Definition)
    '["public" ::: '["users" ::: 'Table UsersTable]]
    '["public" ::: '["users" ::: 'Table UsersTable, "emails" ::: 'Table EmailsTable]]
  makeEmails = Migration "make emails table" IsoQ
    { up = createTable #emails
          ( serial `as` #id :*
            notNullable int `as` #user_id :*
            nullable text `as` #email )
          ( primaryKey #id `as` #pk_emails :*
            foreignKey #user_id #users #id
              (OnDelete Cascade) (OnUpdate Cascade) `as` #fk_user_id )
    , down = dropTable #emails
    }
:}

Now that we have a couple migrations we can chain them together into a `Path`.

>>> let migrations = makeUsers :>> makeEmails :>> Done

Now run the migrations.

>>> import Control.Monad.IO.Class
>>> :{
withConnection "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" $
  manipulate_ (UnsafeManipulation "SET client_min_messages TO WARNING;")
  & pqThen (liftIO (putStrLn "Migrate"))
  & pqThen (migrateUp migrations)
  & pqThen (liftIO (putStrLn "Rollback"))
  & pqThen (migrateDown migrations)
:}
Migrate
Rollback

We can also create a simple executable using `mainMigrateIso`.

>>> let main = mainMigrateIso "host=localhost port=5432 dbname=exampledb user=postgres password=postgres" migrations

>>> withArgs [] main
Invalid command: "". Use:
migrate    to run all available migrations
rollback   to rollback all available migrations
status     to display migrations run and migrations left to run

>>> withArgs ["status"] main
Migrations already run:
  None
Migrations left to run:
  - make users table
  - make emails table

>>> withArgs ["migrate"] main
Migrations already run:
  - make users table
  - make emails table
Migrations left to run:
  None

>>> withArgs ["rollback"] main
Migrations already run:
  None
Migrations left to run:
  - make users table
  - make emails table

In addition to enabling `Migration`s using pure SQL `Definition`s for
the `up` and `down` migrations, you can also perform impure `IO` actions
by using a `Migration`s over the `Indexed` `PQ` `IO` category.
-}

# LANGUAGE 
     DataKinds 
   , DeriveGeneric 
   , FlexibleContexts 
   , FlexibleInstances 
   , FunctionalDependencies 
   , GADTs 
   , LambdaCase 
   , MultiParamTypeClasses 
   , OverloadedLabels 
   , OverloadedStrings 
   , PolyKinds 
   , QuantifiedConstraints 
   , RankNTypes 
   , TypeApplications 
   , TypeOperators 
 #
    DataKinds
  , DeriveGeneric
  , FlexibleContexts
  , FlexibleInstances
  , FunctionalDependencies
  , GADTs
  , LambdaCase
  , MultiParamTypeClasses
  , OverloadedLabels
  , OverloadedStrings
  , PolyKinds
  , QuantifiedConstraints
  , RankNTypes
  , TypeApplications
  , TypeOperators
#-}

module Squeal.PostgreSQL.Session.Migration
    Migration (..)
  , Migratory (..)
  , migrate
  , migrateUp
  , migrateDown
  , MigrationsTable
  , mainMigrate
  , mainMigrateIso
  , IsoQ (..)
  ) where

import Control.Category
import Control.Category.Free
import Control.Monad
import Control.Monad.IO.Class
import Data.ByteString (ByteString)
import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.List ((\\))
import Data.Quiver
import Data.Quiver.Functor
import Data.Text (Text)
import Data.Time (UTCTime)
import Prelude hiding ((.), id)
import System.Environment

import qualified Data.Text.IO as Text (putStrLn)
import qualified Generics.SOP as SOP
import qualified GHC.Generics as GHC

import Squeal.PostgreSQL.Definition
import Squeal.PostgreSQL.Definition.Constraint
import Squeal.PostgreSQL.Definition.Table
import Squeal.PostgreSQL.Expression.Comparison
import Squeal.PostgreSQL.Expression.Default
import Squeal.PostgreSQL.Expression.Parameter
import Squeal.PostgreSQL.Expression.Time
import Squeal.PostgreSQL.Expression.Type
import Squeal.PostgreSQL.Manipulation
import Squeal.PostgreSQL.Manipulation.Delete
import Squeal.PostgreSQL.Manipulation.Insert
import Squeal.PostgreSQL.Session
import Squeal.PostgreSQL.Session.Decode
import Squeal.PostgreSQL.Session.Encode
import Squeal.PostgreSQL.Session.Indexed
import Squeal.PostgreSQL.Session.Monad
import Squeal.PostgreSQL.Session.Result
import Squeal.PostgreSQL.Session.Statement
import Squeal.PostgreSQL.Session.Transaction.Unsafe
import Squeal.PostgreSQL.Query.From
import Squeal.PostgreSQL.Query.Select
import Squeal.PostgreSQL.Query.Table
import Squeal.PostgreSQL.Type.Alias
import Squeal.PostgreSQL.Type.List
import Squeal.PostgreSQL.Type.Schema

data Migration def db0 db1 = Migration
  } deriving (GHC.Generic)
instance QFunctor Migration where
  qmap f (Migration n i) = Migration n (f i)

  | 
 A ` Migratory ` ` Category ` can run or 
 possibly rewind a ` Path ` of ` Migration`s . 

A `Migratory` `Category` can run or
possibly rewind a `Path` of `Migration`s.
-}
class (Category def, Category run) => Migratory def run | def -> run where
  | Run a ` Path ` of ` Migration`s .
  runMigrations :: Path (Migration def) db0 db1 -> run db0 db1
instance Migratory (Indexed PQ IO ()) (Indexed PQ IO ()) where
  runMigrations path = Indexed . unsafePQ . transactionally_ $ do
    define createMigrations
    qtoMonoid upMigration path
    where
      upMigration step = do
        executed <- do
          result <- executeParams selectMigration (migrationName step)
          ntuples (result :: Result UTCTime)
        unless (executed == 1) $ do
          _ <- unsafePQ . runIndexed $ migrationDef step
          executeParams_ insertMigration (migrationName step)
instance Migratory Definition (Indexed PQ IO ()) where
  runMigrations = runMigrations . qmap (qmap ixDefine)
instance Migratory (OpQ (Indexed PQ IO ())) (OpQ (Indexed PQ IO ())) where
  runMigrations path = OpQ . Indexed . unsafePQ . transactionally_ $ do
    define createMigrations
    qtoMonoid @FoldPath downMigration (reversePath path)
    where
      downMigration (OpQ step) = do
        executed <- do
          result <- executeParams selectMigration (migrationName step)
          ntuples (result :: Result UTCTime)
        unless (executed == 0) $ do
          _ <- unsafePQ . runIndexed . getOpQ $ migrationDef step
          executeParams_ deleteMigration (migrationName step)
instance Migratory (OpQ Definition) (OpQ (Indexed PQ IO ())) where
  runMigrations = runMigrations . qmap (qmap (qmap ixDefine))
instance Migratory
  (IsoQ (Indexed PQ IO ()))
  (IsoQ (Indexed PQ IO ())) where
    runMigrations path = IsoQ
      (runMigrations (qmap (qmap up) path))
      (getOpQ (runMigrations (qmap (qmap (OpQ . down)) path)))
instance Migratory (IsoQ Definition) (IsoQ (Indexed PQ IO ())) where
  runMigrations = runMigrations . qmap (qmap (qmap ixDefine))

unsafePQ :: (Functor m) => PQ db0 db1 m x -> PQ db0' db1' m x
unsafePQ (PQ pq) = PQ $ fmap (SOP.K . SOP.unK) . pq . SOP.K . SOP.unK

migrate
  :: Migratory def (Indexed PQ IO ())
  => Path (Migration def) db0 db1
  -> PQ db0 db1 IO ()
migrate = runIndexed . runMigrations

migrateUp
  :: Migratory def (IsoQ (Indexed PQ IO ()))
  => Path (Migration def) db0 db1
  -> PQ db0 db1 IO ()
migrateUp = runIndexed . up . runMigrations

migrateDown
  :: Migratory def (IsoQ (Indexed PQ IO ()))
  => Path (Migration def) db0 db1
  -> PQ db1 db0 IO ()
migrateDown = runIndexed . down . runMigrations

ixDefine :: Definition db0 db1 -> Indexed PQ IO () db0 db1
ixDefine = indexedDefine

  | The ` TableType ` for a Squeal migration .
type MigrationsTable =
  '[ "migrations_unique_name" ::: 'Unique '["name"]] :=>
  '[ "name"        ::: 'NoDef :=> 'NotNull 'PGtext
   , "executed_at" :::   'Def :=> 'NotNull 'PGtimestamptz
   ]

data MigrationRow =
  MigrationRow { name :: Text
               , executed_at :: UTCTime }
  deriving (GHC.Generic, Show)

instance SOP.Generic MigrationRow
instance SOP.HasDatatypeInfo MigrationRow

type MigrationsSchema = '["schema_migrations" ::: 'Table MigrationsTable]
type MigrationsSchemas = Public MigrationsSchema

  | Creates a ` MigrationsTable ` if it does not already exist .
createMigrations :: Definition MigrationsSchemas MigrationsSchemas
createMigrations =
  createTableIfNotExists #schema_migrations
    ( (text & notNullable) `as` #name :*
      (timestampWithTimeZone & notNullable & default_ currentTimestamp)
        `as` #executed_at )
    ( unique #name `as` #migrations_unique_name )

  | Inserts a ` Migration ` into the ` MigrationsTable ` , returning
insertMigration :: Statement MigrationsSchemas Text ()
insertMigration = Manipulation aParam genericRow $
  insertInto_ #schema_migrations $
    Values_ (Set (param @1) `as` #name :* Default `as` #executed_at)

  | Deletes a ` Migration ` from the ` MigrationsTable ` , returning
deleteMigration :: Statement MigrationsSchemas Text ()
deleteMigration = Manipulation aParam genericRow $
  deleteFrom_ #schema_migrations (#name .== param @1)

  | Selects a ` Migration ` from the ` MigrationsTable ` , returning
selectMigration :: Statement MigrationsSchemas Text UTCTime
selectMigration = Query aParam #executed_at $
  select_ #executed_at
    $ from (table (#schema_migrations))
    & where_ (#name .== param @1)

selectMigrations :: Statement MigrationsSchemas () MigrationRow
selectMigrations = query $ select Star (from (table #schema_migrations))

  | ` mainMigrate ` creates a simple executable 
 from a connection string and a ` Path ` of ` Migration`s .
from a connection string and a `Path` of `Migration`s. -}
mainMigrate
  :: Migratory p (Indexed PQ IO ())
  => ByteString
  -> Path (Migration p) db0 db1
  -> IO ()
mainMigrate connectTo migrations = do
  command <- getArgs
  performCommand command

  where

    performCommand :: [String] -> IO ()
    performCommand = \case
      ["status"] -> withConnection connectTo $
        suppressNotices >> migrateStatus
      ["migrate"] -> withConnection connectTo $
        suppressNotices
        & pqThen (runIndexed (runMigrations migrations))
        & pqThen migrateStatus
      args -> displayUsage args

    migrateStatus :: PQ schema schema IO ()
    migrateStatus = unsafePQ $ do
      runNames <- getRunMigrationNames
      let names = qtoList migrationName migrations
          unrunNames = names \\ runNames
      liftIO $ displayRunned runNames >> displayUnrunned unrunNames

    suppressNotices :: PQ schema schema IO ()
    suppressNotices = manipulate_ $
      UnsafeManipulation "SET client_min_messages TO WARNING;"

    displayUsage :: [String] -> IO ()
    displayUsage args = do
      putStrLn $ "Invalid command: \"" <> unwords args <> "\". Use:"
      putStrLn "migrate    to run all available migrations"
      putStrLn "rollback   to rollback all available migrations"

  | ` mainMigrateIso ` creates a simple executable 
 from a connection string and a ` Path ` of ` Migration ` ` IsoQ`s .
from a connection string and a `Path` of `Migration` `IsoQ`s. -}
mainMigrateIso
  :: Migratory (IsoQ def) (IsoQ (Indexed PQ IO ()))
  => ByteString
  -> Path (Migration (IsoQ def)) db0 db1
  -> IO ()
mainMigrateIso connectTo migrations = performCommand =<< getArgs

  where

    performCommand :: [String] -> IO ()
    performCommand = \case
      ["status"] -> withConnection connectTo $
        suppressNotices >> migrateStatus
      ["migrate"] -> withConnection connectTo $
        suppressNotices
        & pqThen (migrateUp migrations)
        & pqThen migrateStatus
      ["rollback"] -> withConnection connectTo $
        suppressNotices
        & pqThen (migrateDown migrations)
        & pqThen migrateStatus
      args -> displayUsage args

    migrateStatus :: PQ schema schema IO ()
    migrateStatus = unsafePQ $ do
      runNames <- getRunMigrationNames
      let names = qtoList migrationName migrations
          unrunNames = names \\ runNames
      liftIO $ displayRunned runNames >> displayUnrunned unrunNames

    suppressNotices :: PQ schema schema IO ()
    suppressNotices = manipulate_ $
      UnsafeManipulation "SET client_min_messages TO WARNING;"

    displayUsage :: [String] -> IO ()
    displayUsage args = do
      putStrLn $ "Invalid command: \"" <> unwords args <> "\". Use:"
      putStrLn "migrate    to run all available migrations"
      putStrLn "rollback   to rollback all available migrations"
      putStrLn "status     to display migrations run and migrations left to run"

getRunMigrationNames :: PQ db0 db0 IO [Text]
getRunMigrationNames =
  fmap name <$>
    (unsafePQ (define createMigrations
    & pqThen (execute selectMigrations)) >>= getRows)

displayListOfNames :: [Text] -> IO ()
displayListOfNames [] = Text.putStrLn "  None"
displayListOfNames xs =
  let singleName n = Text.putStrLn $ "  - " <> n
  in traverse_ singleName xs

displayUnrunned :: [Text] -> IO ()
displayUnrunned unrunned =
  Text.putStrLn "Migrations left to run:"
  >> displayListOfNames unrunned

displayRunned :: [Text] -> IO ()
displayRunned runned =
  Text.putStrLn "Migrations already run:"
  >> displayListOfNames runned
