packages feed

relocant-1.0.0: src/Relocant/Applied.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}
-- | This module deals with applied migrations and their records in the DB.
module Relocant.Applied
  ( Applied(..)
  , getApplied
  , getAppliedByID
  , record
  , deleteAll
  , deleteByID
  ) where

import Data.Aeson qualified as Aeson
import Data.Aeson ((.=))
import Data.ByteString (ByteString)
import Data.Maybe (listToMaybe)
import Database.PostgreSQL.Simple qualified as DB
import Database.PostgreSQL.Simple.FromRow qualified as DB (RowParser, field)
import Database.PostgreSQL.Simple.SqlQQ qualified as DB (sql)
import GHC.Records (HasField(getField))
import Prelude hiding (id, readFile)

import Relocant.DB qualified as DB (Table)
import Relocant.At (At)
import Relocant.Checksum (Checksum(..))
import Relocant.Content (Content(..))
import Relocant.ID (ID)
import Relocant.Name (Name)
import Relocant.Duration (Duration)


-- | An applied migration. Generally, it's created be either running a
-- migration 'Relocant.Script' with 'Relocant.apply' or getting the records
-- from the DB with 'getApplied' or 'getAppliedByID'.
data Applied = Applied
  { id        :: ID
  , name      :: Name
  , content   :: Content
  , appliedAt :: At
  , durationS :: Duration -- ^ how long it took to apply the migration script
  } deriving (Show, Eq)

-- | The content (as in actual bytes) is skipped.
instance Aeson.ToJSON Applied where
  toJSON a =
    Aeson.object
      [ "id" .= a.id
      , "name" .= a.name
      , "checksum" .= show a.checksum
      , "applied_at" .= a.appliedAt
      , "duration_s" .= a.durationS
      ]

instance HasField "bytes" Applied ByteString where
  getField s =
    s.content.bytes

instance HasField "checksum" Applied Checksum where
  getField s =
    s.content.checksum

-- | Retrieve all 'Applied' migrations' records from the DB.
getApplied :: DB.Table -> DB.Connection -> IO [Applied]
getApplied table conn = do
  DB.queryWith appliedP conn [DB.sql|
    SELECT id
         , name
         , bytes
         , checksum
         , applied_at
         , EXTRACT(epoch FROM duration_s) :: REAL
      FROM ?
  ORDER BY id
  |] (DB.Only table)

-- | Retrieve the specific 'Applied' migration's record from the DB.
getAppliedByID :: ID -> DB.Table -> DB.Connection -> IO (Maybe Applied)
getAppliedByID id table conn = do
  ms <- DB.queryWith appliedP conn [DB.sql|
    SELECT id
         , name
         , bytes
         , checksum
         , applied_at
         , EXTRACT(epoch FROM duration_s) :: REAL
      FROM ?
     WHERE id = ?
  |] (table, id)
  pure (listToMaybe ms)

-- | Record a successfully 'Applied' migration.
--
-- /Note:/ You probably want to run this together with 'Relocant.Script.apply'
-- in a single transaction.
record :: Applied -> DB.Table -> DB.Connection -> IO ()
record a table conn = do
  1 <- DB.execute conn [DB.sql|
    INSERT INTO ?
              ( id
              , name
              , bytes
              , checksum
              , applied_at
              , duration_s
              )
         SELECT ?
              , ?
              , ?
              , ?
              , ?
              , make_interval(secs := ?)
  |] ( table
     , a.id
     , a.name
     , DB.Binary a.bytes
     , a.checksum
     , a.appliedAt
     , a.durationS
     )
  pure ()

-- | Delete all 'Applied' migrations' records from the DB.
deleteAll :: DB.Table -> DB.Connection -> IO ()
deleteAll table conn = do
  _rows <- DB.execute conn [DB.sql|
    TRUNCATE ?
  |] (DB.Only table)
  pure ()

-- | Delete the specific 'Applied' migration's record from the DB.
deleteByID :: ID -> DB.Table -> DB.Connection -> IO Bool
deleteByID id table conn = do
  rows <- DB.execute conn [DB.sql|
    DELETE FROM ?
          WHERE id = ?
  |] (table, id)
  pure (rows > 0)

appliedP :: DB.RowParser Applied
appliedP = do
  id <- DB.field
  name <- DB.field
  DB.Binary bytes <- DB.field
  checksum <- DB.field
  appliedAt <- DB.field
  durationS <- DB.field
  pure Applied
    { content = Content {..}
    , ..
    }