packages feed

postgresql-replicant-0.1.0.1: src/Database/PostgreSQL/Replicant/ReplicationSlot.hs

{-|
Module      : Database.PostgreSQL.Replicant.ReplicationSlot
Description : Replication slot query commands
Copyright   : (c) James King, 2020, 2021
License     : BSD3
Maintainer  : james@agentultra.com
Stability   : experimental
Portability : POSIX

This module contains the PostgreSQL queries, types, and functions for
working with querying, creating, and working with replication slots.
-}
module Database.PostgreSQL.Replicant.ReplicationSlot where

import Control.Exception
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Database.PostgreSQL.LibPQ

import Database.PostgreSQL.Replicant.Exception
import Database.PostgreSQL.Replicant.Types.Lsn

data ReplicationSlotInfo
  = ReplicationSlotInfo
  { slotName    :: ByteString
  , slotPlugin  :: ByteString
  , slotType    :: ReplicationSlotType
  , slotActive  :: ReplicationSlotActive
  , slotRestart :: LSN
  }
  deriving (Eq, Show)

data ReplicationSlotType = Logical | Physical | UnknownSlotType
  deriving (Eq, Show)

parseSlotType :: ByteString -> ReplicationSlotType
parseSlotType "logical"  = Logical
parseSlotType "physical" = Physical
parseSlotType _          = UnknownSlotType

data ReplicationSlotActive = Active | Inactive
  deriving (Eq, Show)

parseSlotActive :: ByteString -> ReplicationSlotActive
parseSlotActive "t" = Active
parseSlotActive "f" = Inactive
parseSlotActive _   = Inactive

createReplicationSlotCommand :: Connection -> ByteString -> IO ByteString
createReplicationSlotCommand conn slotName = do
  escapedName <- escapeIdentifier conn slotName
  case escapedName of
    Nothing -> throwIO $ ReplicantException $ "Invalid slot name: " ++ show slotName
    Just escaped ->
      pure $
      B.intercalate
      ""
      [ "CREATE_REPLICATION_SLOT"
      , escaped
      , "LOGICAL wal2json"
      ]

-- | Create a replication slot using synchronous query execution.
--
-- May throw an exception if the command fails.
createReplicationSlotSync :: Connection -> ByteString -> IO ReplicationSlotInfo
createReplicationSlotSync conn slotName = do
  createReplicationSlotQuery <- createReplicationSlotCommand conn slotName
  result <- exec conn createReplicationSlotQuery
  case result of
    Just r -> do
      resultStatus <- resultStatus r
      case resultStatus of
        TuplesOk -> do
          sName           <- getvalue' r (toRow 0) (toColumn 0)
          consistentPoint <- getvalue' r (toRow 0) (toColumn 1)
          outputPlugin    <- getvalue' r (toRow 0) (toColumn 3)
          case (sName, consistentPoint, outputPlugin) of
            (Just s, Just c, Just op) ->
              case fromByteString c of
                Left _ -> throwIO $ ReplicantException "createReplicationSlotSync: invalid LSN detected"
                Right lsn -> pure $ ReplicationSlotInfo s op Logical Active lsn
            _ -> do
              err <- maybe "createReplicationSlotSync: unknown error" id <$> errorMessage conn
              throwIO $ ReplicantException (B8.unpack err)
        _ -> do
          err <- maybe "createReplicationSlotSync: unknown error" id <$> errorMessage conn
          throwIO $ ReplicantException (B8.unpack err)
    _ -> do
      err <- maybe "createReplicationSlotSync: unknown error" id <$> errorMessage conn
      throwIO $ ReplicantException (B8.unpack err)

getReplicationSlotInfoCommand :: Connection -> ByteString -> IO ByteString
getReplicationSlotInfoCommand conn slotName = do
  escapedName <- escapeStringConn conn slotName
  case escapedName of
    Nothing -> throwIO $ ReplicantException $ "Invalid slot name: " ++ show slotName
    Just escaped ->
      pure $
      B.intercalate
      ""
      [ "select slot_name, plugin, slot_type, active, restart_lsn from pg_replication_slots where slot_name = '"
      , escaped
      , "';"
      ]

-- | Get information about an existing replication slot.  Returns
-- @Nothing@ when the requested slot cannot be found.
--
-- May throw an exception if the command query fails.
getReplicationSlotSync :: Connection -> ByteString -> IO (Maybe ReplicationSlotInfo)
getReplicationSlotSync conn slotName = do
  replicationSlotInfoQuery <- getReplicationSlotInfoCommand conn slotName
  result <- exec conn replicationSlotInfoQuery
  case result of
    Just r -> do
      resultStatus <- resultStatus r
      case resultStatus of
        TuplesOk -> do
          nRows <- ntuples r
          if nRows == 0
            then pure Nothing
            else do
            slotName    <- getvalue' r (toRow 0) (toColumn 0)
            slotPlugin  <- getvalue' r (toRow 0) (toColumn 1)
            slotType    <- getvalue' r (toRow 0) (toColumn 2)
            slotActive  <- getvalue' r (toRow 0) (toColumn 3)
            slotRestart <- getvalue' r (toRow 0) (toColumn 4)
            case (slotName, slotPlugin, slotType, slotActive, slotRestart) of
              (Just n, Just p, Just t, Just a, Just restart) -> do
                case fromByteString restart of
                  Left _ -> pure Nothing -- TODO: this shouldn't happen...
                  Right lsn -> pure $ Just $ ReplicationSlotInfo n p (parseSlotType t) (parseSlotActive a) lsn
              _ ->  pure Nothing
        _ -> pure Nothing
    _ -> do
      err <- maybe "getReplicationSlotSync: unknown error" id <$> errorMessage conn
      throwIO $ ReplicantException (B8.unpack err)

-- | Create replication slot or retrieve an existing slot.
--
-- Can throw exceptions from @getReplicationSlotSync@ or
-- @createReplicationSlotSync@.
setupReplicationSlot :: Connection -> ByteString -> IO ReplicationSlotInfo
setupReplicationSlot conn slotName = do
  maybeSlot <- getReplicationSlotSync conn slotName
  case maybeSlot of
    Just slot -> pure $ slot
    Nothing   -> createReplicationSlotSync conn slotName