packages feed

postgresql-replicant 0.1.0.1 → 0.2.0.0

raw patch · 13 files changed

+254/−228 lines, 13 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Database.PostgreSQL.Replicant: instance GHC.Classes.Eq Database.PostgreSQL.Replicant.PgSettings
- Database.PostgreSQL.Replicant: instance GHC.Show.Show Database.PostgreSQL.Replicant.PgSettings
- Database.PostgreSQL.Replicant.Queue: BoundedFifoQueue :: MVar (BoundedFifoQueueMeta a) -> BoundedFifoQueue a
- Database.PostgreSQL.Replicant.Queue: BoundedFifoQueueMeta :: Int -> Seq a -> BoundedFifoQueueMeta a
- Database.PostgreSQL.Replicant.Queue: BoundedQueueOverflow :: a -> BoundedQueueException a
- Database.PostgreSQL.Replicant.Queue: FifoQueue :: MVar (Seq a) -> FifoQueue a
- Database.PostgreSQL.Replicant.Queue: [boundedFifoQueueSize] :: BoundedFifoQueueMeta a -> Int
- Database.PostgreSQL.Replicant.Queue: [boundedFifoQueue] :: BoundedFifoQueueMeta a -> Seq a
- Database.PostgreSQL.Replicant.Queue: data BoundedFifoQueueMeta a
- Database.PostgreSQL.Replicant.Queue: dequeue :: FifoQueue a -> IO (Maybe a)
- Database.PostgreSQL.Replicant.Queue: empty :: IO (FifoQueue a)
- Database.PostgreSQL.Replicant.Queue: emptyBounded :: Int -> IO (BoundedFifoQueue a)
- Database.PostgreSQL.Replicant.Queue: enqueue :: FifoQueue a -> a -> IO ()
- Database.PostgreSQL.Replicant.Queue: enqueueBounded :: BoundedFifoQueue a -> a -> IO (Either (BoundedQueueException a) ())
- Database.PostgreSQL.Replicant.Queue: enqueueRight :: FifoQueue a -> a -> IO ()
- Database.PostgreSQL.Replicant.Queue: instance GHC.Classes.Eq a => GHC.Classes.Eq (Database.PostgreSQL.Replicant.Queue.BoundedFifoQueueMeta a)
- Database.PostgreSQL.Replicant.Queue: instance GHC.Classes.Eq a => GHC.Classes.Eq (Database.PostgreSQL.Replicant.Queue.BoundedQueueException a)
- Database.PostgreSQL.Replicant.Queue: instance GHC.Show.Show a => GHC.Show.Show (Database.PostgreSQL.Replicant.Queue.BoundedFifoQueueMeta a)
- Database.PostgreSQL.Replicant.Queue: instance GHC.Show.Show a => GHC.Show.Show (Database.PostgreSQL.Replicant.Queue.BoundedQueueException a)
- Database.PostgreSQL.Replicant.Queue: newtype BoundedFifoQueue a
- Database.PostgreSQL.Replicant.Queue: newtype BoundedQueueException a
- Database.PostgreSQL.Replicant.Queue: newtype FifoQueue a
- Database.PostgreSQL.Replicant.Queue: null :: FifoQueue a -> IO Bool
+ Database.PostgreSQL.Replicant: Change :: LSN -> [WalLogData] -> Change
+ Database.PostgreSQL.Replicant: Column :: !Text -> !Text -> !WalValue -> Column
+ Database.PostgreSQL.Replicant: Delete :: !Text -> !Text -> ![Column] -> Delete
+ Database.PostgreSQL.Replicant: Insert :: !String -> !String -> ![Column] -> Insert
+ Database.PostgreSQL.Replicant: Message :: !Bool -> !Text -> !Text -> Message
+ Database.PostgreSQL.Replicant: Update :: !Text -> !Text -> ![Column] -> Update
+ Database.PostgreSQL.Replicant: WDelete :: !Delete -> WalLogData
+ Database.PostgreSQL.Replicant: WInsert :: !Insert -> WalLogData
+ Database.PostgreSQL.Replicant: WMessage :: !Message -> WalLogData
+ Database.PostgreSQL.Replicant: WUpdate :: !Update -> WalLogData
+ Database.PostgreSQL.Replicant: [changeDeltas] :: Change -> [WalLogData]
+ Database.PostgreSQL.Replicant: [changeNextLSN] :: Change -> LSN
+ Database.PostgreSQL.Replicant: [columnName] :: Column -> !Text
+ Database.PostgreSQL.Replicant: [columnType] :: Column -> !Text
+ Database.PostgreSQL.Replicant: [columnValue] :: Column -> !WalValue
+ Database.PostgreSQL.Replicant: [deleteColumns] :: Delete -> ![Column]
+ Database.PostgreSQL.Replicant: [deleteSchema] :: Delete -> !Text
+ Database.PostgreSQL.Replicant: [deleteTable] :: Delete -> !Text
+ Database.PostgreSQL.Replicant: [insertColumns] :: Insert -> ![Column]
+ Database.PostgreSQL.Replicant: [insertSchema] :: Insert -> !String
+ Database.PostgreSQL.Replicant: [insertTable] :: Insert -> !String
+ Database.PostgreSQL.Replicant: [messageContent] :: Message -> !Text
+ Database.PostgreSQL.Replicant: [messagePrefix] :: Message -> !Text
+ Database.PostgreSQL.Replicant: [messageTransactional] :: Message -> !Bool
+ Database.PostgreSQL.Replicant: [pgPassword] :: PgSettings -> Maybe String
+ Database.PostgreSQL.Replicant: [updateColumns] :: Update -> ![Column]
+ Database.PostgreSQL.Replicant: [updateSchema] :: Update -> !Text
+ Database.PostgreSQL.Replicant: [updateTable] :: Update -> !Text
+ Database.PostgreSQL.Replicant: connect :: PgSettings -> IO ReplicantConnection
+ Database.PostgreSQL.Replicant: data Change
+ Database.PostgreSQL.Replicant: data Column
+ Database.PostgreSQL.Replicant: data Delete
+ Database.PostgreSQL.Replicant: data Insert
+ Database.PostgreSQL.Replicant: data Message
+ Database.PostgreSQL.Replicant: data ReplicantConnection
+ Database.PostgreSQL.Replicant: data Update
+ Database.PostgreSQL.Replicant: data WalLogData
+ Database.PostgreSQL.Replicant: getConnection :: ReplicantConnection -> Connection
+ Database.PostgreSQL.Replicant: unsafeCreateConnection :: Connection -> ReplicantConnection
+ Database.PostgreSQL.Replicant.Connection: connect :: PgSettings -> IO ReplicantConnection
+ Database.PostgreSQL.Replicant.Connection: data ReplicantConnection
+ Database.PostgreSQL.Replicant.Connection: getConnection :: ReplicantConnection -> Connection
+ Database.PostgreSQL.Replicant.Connection: instance GHC.Classes.Eq Database.PostgreSQL.Replicant.Connection.ConnectResult
+ Database.PostgreSQL.Replicant.Connection: instance GHC.Classes.Eq Database.PostgreSQL.Replicant.Connection.ReplicantConnection
+ Database.PostgreSQL.Replicant.Connection: instance GHC.Show.Show Database.PostgreSQL.Replicant.Connection.ConnectResult
+ Database.PostgreSQL.Replicant.Connection: unsafeCreateConnection :: Connection -> ReplicantConnection
+ Database.PostgreSQL.Replicant.Settings: PgSettings :: String -> Maybe String -> String -> String -> String -> String -> String -> PgSettings
+ Database.PostgreSQL.Replicant.Settings: [pgDbName] :: PgSettings -> String
+ Database.PostgreSQL.Replicant.Settings: [pgHost] :: PgSettings -> String
+ Database.PostgreSQL.Replicant.Settings: [pgPassword] :: PgSettings -> Maybe String
+ Database.PostgreSQL.Replicant.Settings: [pgPort] :: PgSettings -> String
+ Database.PostgreSQL.Replicant.Settings: [pgSlotName] :: PgSettings -> String
+ Database.PostgreSQL.Replicant.Settings: [pgUpdateDelay] :: PgSettings -> String
+ Database.PostgreSQL.Replicant.Settings: [pgUser] :: PgSettings -> String
+ Database.PostgreSQL.Replicant.Settings: data PgSettings
+ Database.PostgreSQL.Replicant.Settings: instance GHC.Classes.Eq Database.PostgreSQL.Replicant.Settings.PgSettings
+ Database.PostgreSQL.Replicant.Settings: instance GHC.Show.Show Database.PostgreSQL.Replicant.Settings.PgSettings
+ Database.PostgreSQL.Replicant.Settings: pgConnectionString :: PgSettings -> ByteString
- Database.PostgreSQL.Replicant: PgSettings :: String -> String -> String -> String -> String -> String -> PgSettings
+ Database.PostgreSQL.Replicant: PgSettings :: String -> Maybe String -> String -> String -> String -> String -> String -> PgSettings
- Database.PostgreSQL.Replicant: withLogicalStream :: PgSettings -> (Change -> IO a) -> IO ()
+ Database.PostgreSQL.Replicant: withLogicalStream :: PgSettings -> (Change -> IO LSN) -> IO ()
- Database.PostgreSQL.Replicant.Protocol: handleCopyOutData :: TChan PrimaryKeepAlive -> WalProgressState -> Connection -> (Change -> IO a) -> IO ()
+ Database.PostgreSQL.Replicant.Protocol: handleCopyOutData :: TChan PrimaryKeepAlive -> WalProgressState -> ReplicantConnection -> (Change -> IO LSN) -> IO ()
- Database.PostgreSQL.Replicant.Protocol: handleReplicationError :: Connection -> IO ()
+ Database.PostgreSQL.Replicant.Protocol: handleReplicationError :: ReplicantConnection -> IO ()
- Database.PostgreSQL.Replicant.Protocol: handleReplicationRow :: TChan PrimaryKeepAlive -> WalProgressState -> Connection -> ByteString -> (Change -> IO a) -> IO ()
+ Database.PostgreSQL.Replicant.Protocol: handleReplicationRow :: TChan PrimaryKeepAlive -> WalProgressState -> ReplicantConnection -> ByteString -> (Change -> IO LSN) -> IO ()
- Database.PostgreSQL.Replicant.Protocol: identifySystemSync :: Connection -> IO (Maybe IdentifySystem)
+ Database.PostgreSQL.Replicant.Protocol: identifySystemSync :: ReplicantConnection -> IO (Maybe IdentifySystem)
- Database.PostgreSQL.Replicant.Protocol: keepAliveHandler :: Connection -> TChan PrimaryKeepAlive -> WalProgressState -> IO ()
+ Database.PostgreSQL.Replicant.Protocol: keepAliveHandler :: ReplicantConnection -> TChan PrimaryKeepAlive -> WalProgressState -> IO ()
- Database.PostgreSQL.Replicant.Protocol: sendStatusUpdate :: Connection -> WalProgressState -> IO ()
+ Database.PostgreSQL.Replicant.Protocol: sendStatusUpdate :: ReplicantConnection -> WalProgressState -> IO ()
- Database.PostgreSQL.Replicant.Protocol: startReplicationCommand :: Connection -> ByteString -> LSN -> IO ByteString
+ Database.PostgreSQL.Replicant.Protocol: startReplicationCommand :: ReplicantConnection -> ByteString -> LSN -> IO ByteString
- Database.PostgreSQL.Replicant.Protocol: startReplicationStream :: Connection -> ByteString -> LSN -> Int -> (Change -> IO a) -> IO ()
+ Database.PostgreSQL.Replicant.Protocol: startReplicationStream :: ReplicantConnection -> ByteString -> LSN -> Int -> (Change -> IO LSN) -> IO ()
- Database.PostgreSQL.Replicant.ReplicationSlot: createReplicationSlotCommand :: Connection -> ByteString -> IO ByteString
+ Database.PostgreSQL.Replicant.ReplicationSlot: createReplicationSlotCommand :: ReplicantConnection -> ByteString -> IO ByteString
- Database.PostgreSQL.Replicant.ReplicationSlot: createReplicationSlotSync :: Connection -> ByteString -> IO ReplicationSlotInfo
+ Database.PostgreSQL.Replicant.ReplicationSlot: createReplicationSlotSync :: ReplicantConnection -> ByteString -> IO ReplicationSlotInfo
- Database.PostgreSQL.Replicant.ReplicationSlot: getReplicationSlotInfoCommand :: Connection -> ByteString -> IO ByteString
+ Database.PostgreSQL.Replicant.ReplicationSlot: getReplicationSlotInfoCommand :: ReplicantConnection -> ByteString -> IO ByteString
- Database.PostgreSQL.Replicant.ReplicationSlot: getReplicationSlotSync :: Connection -> ByteString -> IO (Maybe ReplicationSlotInfo)
+ Database.PostgreSQL.Replicant.ReplicationSlot: getReplicationSlotSync :: ReplicantConnection -> ByteString -> IO (Maybe ReplicationSlotInfo)
- Database.PostgreSQL.Replicant.ReplicationSlot: setupReplicationSlot :: Connection -> ByteString -> IO ReplicationSlotInfo
+ Database.PostgreSQL.Replicant.ReplicationSlot: setupReplicationSlot :: ReplicantConnection -> ByteString -> IO ReplicationSlotInfo

Files

ChangeLog.md view
@@ -5,6 +5,17 @@  ## [Unreleased] +## [0.2.0.0] - 2021-07-27++This is a minor update that adds a replication connection type,+updates the root module API with more types, and is just a better+quality of life release.++### Added+- ReplicantConnection+- Connection handling functions to Replicant module+- Message types to main Replicant module+ ## [0.1.0.1] - 2021-06-22  This is a minor update to our experimental release, fixing some pretty@@ -16,7 +27,7 @@ - Minor cleanup, throw better errors instead of printing debug   information -## [0.1.0.0] - 2021-07-27+## [0.1.0.0] - 2021-05-04  This is an experimental release to test out this library and get it ready and polished for 1.0!
README.md view
@@ -1,4 +1,4 @@-# postgres-replicant+# postgresql-replicant  A PostgreSQL streaming logical replication client for Haskell. @@ -58,12 +58,27 @@   let settings = PgSettings "my-user" "my-database" "localhost" "5432" "testing"   withLogicalStream settings $ \change -> do     print change+    return $ changeNextLSN change       `catch` \err ->       print err ```  Which should connect, create a `testing` replication slot, and start sending your callback the changes to your database as they arrive.++The type of the callback to `withLogicalStream` is:++    Change -> IO LSN++Note the return type.  The *LSN* or _Log Sequence Number_ is used by+replicant after running your callback to update its internal stream+state and tell PostgreSQL that you have consumed this change.  This+means that if the connection fails and replicant reconnects to the+_same_ slot it will restart the stream at the last *LSN* replicant was+able to successfully send to the server.++All you have to do is return `changeNextLSN change` at the end of your+callback.  ### WAL Output Plugins 
app/Main.hs view
@@ -1,5 +1,6 @@ module Main where +import Data.Aeson import Data.Maybe import Control.Exception import System.Environment@@ -8,16 +9,17 @@  main :: IO () main = do-  dbUser <- maybe "postgresql" id <$> lookupEnv "PG_USER"-  dbName <- maybe "postgresql" id <$> lookupEnv "PG_DATABASE"-  dbHost <- maybe "localhost" id <$> lookupEnv "PG_HOST"-  dbPort <- maybe "5432" id <$> lookupEnv "PG_PORT"-  dbSlot <- maybe "replicant_test" id <$> lookupEnv "PG_SLOTNAME"-  dbUpdateDelay <- maybe "3000" id <$> lookupEnv "PG_UPDATEDELAY"-  let settings = PgSettings dbUser dbName dbHost dbPort dbSlot dbUpdateDelay+  dbUser <- fromMaybe "postgresql" <$> lookupEnv "PG_USER"+  dbName <- fromMaybe "postgresql" <$> lookupEnv "PG_DATABASE"+  dbHost <- fromMaybe "localhost" <$> lookupEnv "PG_HOST"+  dbPort <- fromMaybe "5432" <$> lookupEnv "PG_PORT"+  dbSlot <- fromMaybe "replicant_test" <$> lookupEnv "PG_SLOTNAME"+  dbUpdateDelay <- fromMaybe "3000" <$> lookupEnv "PG_UPDATEDELAY"+  let settings = PgSettings dbUser Nothing dbName dbHost dbPort dbSlot dbUpdateDelay   withLogicalStream settings $ \change -> do     putStrLn "Change received!"-    print change+    print $ encode change+    pure $ changeNextLSN change   `catch`   \exc -> do     putStrLn "Something bad happened: "
postgresql-replicant.cabal view
@@ -4,12 +4,12 @@ -- -- see: https://github.com/sol/hpack ----- hash: b9e10bcacd11fef9dd3cf878a5223b111e9355f146fc121702189290e50c23ed+-- hash: 79c5f5333f3b93e29420feb572c62237df61efdaa111a3a4425edb8247a2239f  name:           postgresql-replicant-version:        0.1.0.1+version:        0.2.0.0 synopsis:       PostgreSQL logical streaming replication library-description:    Please see the README on GitHub at <https://github.com/agentultra/postgres-replicant#readme>+description:    Please see the README on GitHub at <https://github.com/agentultra/postgresql-replicant#readme> category:       Experimental, Database homepage:       https://github.com/agentultra/postgresql-replicant#readme bug-reports:    https://github.com/agentultra/postgresql-replicant/issues@@ -31,13 +31,14 @@ library   exposed-modules:       Database.PostgreSQL.Replicant+      Database.PostgreSQL.Replicant.Connection       Database.PostgreSQL.Replicant.Exception       Database.PostgreSQL.Replicant.Message       Database.PostgreSQL.Replicant.PostgresUtils       Database.PostgreSQL.Replicant.Protocol-      Database.PostgreSQL.Replicant.Queue       Database.PostgreSQL.Replicant.ReplicationSlot       Database.PostgreSQL.Replicant.Serialize+      Database.PostgreSQL.Replicant.Settings       Database.PostgreSQL.Replicant.State       Database.PostgreSQL.Replicant.Types.Lsn       Database.PostgreSQL.Replicant.Util@@ -45,7 +46,7 @@       Paths_postgresql_replicant   hs-source-dirs:       src-  default-extensions: DeriveGeneric GADTs LambdaCase OverloadedStrings TypeApplications+  default-extensions: DeriveGeneric GADTs LambdaCase OverloadedStrings RecordWildCards TypeApplications   build-depends:       aeson     , async@@ -69,10 +70,11 @@       Paths_postgresql_replicant   hs-source-dirs:       app-  default-extensions: DeriveGeneric GADTs LambdaCase OverloadedStrings TypeApplications+  default-extensions: DeriveGeneric GADTs LambdaCase OverloadedStrings RecordWildCards TypeApplications   ghc-options: -threaded -rtsopts -with-rtsopts=-N   build-depends:-      base >=4.7 && <5+      aeson+    , base >=4.7 && <5     , postgresql-libpq     , postgresql-replicant   default-language: Haskell2010@@ -84,7 +86,7 @@       Paths_postgresql_replicant   hs-source-dirs:       test-  default-extensions: DeriveGeneric GADTs LambdaCase OverloadedStrings TypeApplications+  default-extensions: DeriveGeneric GADTs LambdaCase OverloadedStrings RecordWildCards TypeApplications   ghc-options: -threaded -rtsopts -with-rtsopts=-N   build-depends:       base >=4.7 && <5
src/Database/PostgreSQL/Replicant.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE RecordWildCards #-}- {-| Module      : Database.PostgreSQL.Replicant Description : A PostgreSQL streaming replication library@@ -28,8 +26,23 @@ -}  module Database.PostgreSQL.Replicant-    ( withLogicalStream+    (+    -- * Types+      Change (..)+    , Column (..)+    , Delete (..)+    , Insert (..)+    , Message (..)     , PgSettings (..)+    , ReplicantConnection+    , Update (..)+    , WalLogData (..)+    -- * Connection Handling+    , connect+    , getConnection+    , unsafeCreateConnection+    -- * Functions+    , withLogicalStream     ) where  import Control.Concurrent@@ -38,38 +51,17 @@ import qualified Data.ByteString.Char8 as B import qualified Data.Text as T import qualified Data.Text.Read as T-import Database.PostgreSQL.LibPQ-import Network.Socket.KeepAlive-import System.Posix.Types+import Database.PostgreSQL.LibPQ hiding (Column) +import Database.PostgreSQL.Replicant.Connection import Database.PostgreSQL.Replicant.Exception import Database.PostgreSQL.Replicant.Protocol import Database.PostgreSQL.Replicant.Message import Database.PostgreSQL.Replicant.ReplicationSlot+import Database.PostgreSQL.Replicant.Settings+import Database.PostgreSQL.Replicant.Types.Lsn import Database.PostgreSQL.Replicant.Util -data PgSettings-  = PgSettings-  { pgUser        :: String-  , pgDbName      :: String-  , pgHost        :: String-  , pgPort        :: String-  , pgSlotName    :: String-  , pgUpdateDelay :: String -- ^ Controls how frequently the-                            -- primaryKeepAlive thread updates-                            -- PostgresSQL in @ms@-  }-  deriving (Eq, Show)--pgConnectionString :: PgSettings -> ByteString-pgConnectionString PgSettings {..} = B.intercalate " "-  [ "user=" <> (B.pack pgUser)-  , "dbname=" <> (B.pack pgDbName)-  , "host=" <> (B.pack pgHost)-  , "port=" <> (B.pack pgPort)-  , "replication=database"-  ]- -- | Connect to a PostgreSQL database as a user with the replication -- attribute and start receiving changes using the logical replication -- protocol.  Logical replication happens at the query level so the@@ -82,36 +74,16 @@ -- -- This function can throw exceptions in @IO@ and shut-down the -- socket in case of any error.-withLogicalStream :: PgSettings -> (Change -> IO a) -> IO ()+withLogicalStream :: PgSettings -> (Change -> IO LSN) -> IO () withLogicalStream settings cb = do-  conn <- connectStart $ pgConnectionString settings-  mFd <- socket conn-  sockFd <- maybeThrow (ReplicantException "withLogicalStream: could not get socket fd") mFd-  pollResult <- pollConnectStart conn sockFd+  conn <- connect settings   let updateFreq = getUpdateDelay settings-  case pollResult of-    PollingFailed -> throwIO $ ReplicantException "withLogicalStream: Unable to connect to the database"-    PollingOk -> do-      maybeInfo <- identifySystemSync conn-      _ <- maybeThrow (ReplicantException "withLogicalStream: could not get system information") maybeInfo-      repSlot <- setupReplicationSlot conn $ B.pack . pgSlotName $ settings-      startReplicationStream conn (slotName repSlot) (slotRestart repSlot) updateFreq cb-      pure ()+  maybeInfo <- identifySystemSync conn+  _ <- maybeThrow (ReplicantException "withLogicalStream: could not get system information") maybeInfo+  repSlot <- setupReplicationSlot conn $ B.pack . pgSlotName $ settings+  startReplicationStream conn (slotName repSlot) (slotRestart repSlot) updateFreq cb+  pure ()   where-    pollConnectStart :: Connection -> Fd -> IO PollingStatus-    pollConnectStart conn fd@(Fd cint) = do-      pollStatus <- connectPoll conn-      case pollStatus of-        PollingReading -> do-          threadWaitRead fd-          pollConnectStart conn fd-        PollingWriting -> do-          threadWaitWrite fd-          pollConnectStart conn fd-        PollingOk -> do-          _ <- setKeepAlive cint $ KeepAlive True 60 2-          pure PollingOk-        PollingFailed -> pure PollingFailed     getUpdateDelay :: PgSettings -> Int     getUpdateDelay PgSettings {..} =       case T.decimal . T.pack $ pgUpdateDelay of
+ src/Database/PostgreSQL/Replicant/Connection.hs view
@@ -0,0 +1,71 @@+{-+Module : Database.PostgreSQL.Replicant.Connection+Description : Create replication handling connections to PostgreSQL++A ReplicantConnection is different from a regular Connection because+it uses a special mode that can send replication commands that regular+Connection objects cannots send.+-}+module Database.PostgreSQL.Replicant.Connection+  ( -- * Types+    ReplicantConnection+    -- * Constructor+  , connect+  , getConnection+  , unsafeCreateConnection+  )+where++import Control.Concurrent+import Control.Exception+import Database.PostgreSQL.LibPQ+import Network.Socket.KeepAlive+import System.Posix.Types++import Database.PostgreSQL.Replicant.Exception+import Database.PostgreSQL.Replicant.Settings+import Database.PostgreSQL.Replicant.Util++newtype ReplicantConnection+  = ReplicantConnection { getConnection :: Connection }+  deriving Eq++data ConnectResult+  = ConnectSuccess+  | ConnectFailure+  deriving (Eq, Show)++-- | Connect to the PostgreSQL server in replication mode+connect :: PgSettings -> IO ReplicantConnection+connect settings = do+  conn <- connectStart $ pgConnectionString settings+  mFd <- socket conn+  sockFd <- maybeThrow+    (ReplicantException "withLogicalStream: could not get socket fd") mFd+  pollResult <- pollConnectStart conn sockFd+  case pollResult of+    ConnectFailure -> throwIO+      $ ReplicantException "withLogicalStream: Unable to connect to the database"+    ConnectSuccess -> pure $ ReplicantConnection conn++pollConnectStart :: Connection -> Fd -> IO ConnectResult+pollConnectStart conn fd@(Fd cint) = do+  pollStatus <- connectPoll conn+  case pollStatus of+    PollingReading -> do+      threadWaitRead fd+      pollConnectStart conn fd+    PollingWriting -> do+      threadWaitWrite fd+      pollConnectStart conn fd+    PollingOk -> do+      _ <- setKeepAlive cint $ KeepAlive True 60 2+      pure ConnectSuccess+    PollingFailed -> pure ConnectFailure++-- | Unsafe function for wrapping regular libpq Connection.  This is+-- unsafe because the Connection needs to be set up to send+-- replication commands.  Improperly constructed connections can lead+-- to runtime exceptions.+unsafeCreateConnection :: Connection -> ReplicantConnection+unsafeCreateConnection = ReplicantConnection
src/Database/PostgreSQL/Replicant/Message.hs view
@@ -392,7 +392,10 @@ data Change   = Change   { changeNextLSN :: LSN+    -- ^ Return this LSN in your callback to update the stream state+    -- in replicant   , changeDeltas  :: [WalLogData]+    -- ^ The list of WAL log changes in this transaction.   }   deriving (Eq, Generic, Show) 
src/Database/PostgreSQL/Replicant/Protocol.hs view
@@ -26,6 +26,7 @@ import Data.Serialize hiding (flush) import Database.PostgreSQL.LibPQ +import Database.PostgreSQL.Replicant.Connection import Database.PostgreSQL.Replicant.Exception import Database.PostgreSQL.Replicant.Message import Database.PostgreSQL.Replicant.PostgresUtils@@ -49,9 +50,9 @@  -- | Synchronously execute the @IDENTIFY SYSTEM@ command which returns -- some basic system information about the server.-identifySystemSync :: Connection -> IO (Maybe IdentifySystem)+identifySystemSync :: ReplicantConnection -> IO (Maybe IdentifySystem) identifySystemSync conn = do-  result <- exec conn identifySystemCommand+  result <- exec (getConnection conn) identifySystemCommand   case result of     Just r -> do       resultStatus <- resultStatus r@@ -69,17 +70,19 @@                   pure $ Just (IdentifySystem s t logPosLsn d)             _ -> pure Nothing         _ -> do-          err <- fromMaybe "identifySystemSync: unknown error" <$> errorMessage conn+          err <- fromMaybe "identifySystemSync: unknown error"+            <$> errorMessage (getConnection conn)           throwIO $ ReplicantException (B.unpack err)     _ -> do-      err <- fromMaybe "identifySystemSync: unknown error" <$> errorMessage conn+      err <- fromMaybe "identifySystemSync: unknown error"+        <$> errorMessage (getConnection conn)       throwIO $ ReplicantException (B.unpack err)  -- | Create a @START_REPLICATION_SLOT@ query, escaping the slot name -- passed in by the user.-startReplicationCommand :: Connection -> ByteString -> LSN -> IO ByteString+startReplicationCommand :: ReplicantConnection -> ByteString -> LSN -> IO ByteString startReplicationCommand conn slotName systemLogPos = do-  escapedName <- escapeIdentifier conn slotName+  escapedName <- escapeIdentifier (getConnection conn) slotName   case escapedName of     Nothing -> throwIO $ ReplicantException $ "Invalid slot name: " ++ show slotName     Just escaped ->@@ -89,7 +92,7 @@       [ "START_REPLICATION SLOT "       , escaped       , " LOGICAL "-      , (toByteString systemLogPos)+      , toByteString systemLogPos       , " (\"include-lsn\" 'on')"       ] @@ -99,11 +102,11 @@ handleCopyOutData   :: TChan PrimaryKeepAlive   -> WalProgressState-  -> Connection-  -> (Change -> IO a)+  -> ReplicantConnection+  -> (Change -> IO LSN)   -> IO () handleCopyOutData chan walState conn cb = forever $ do-  d <- getCopyData conn False+  d <- getCopyData (getConnection conn) False   case d of     CopyOutRow row -> handleReplicationRow chan walState conn row cb     CopyOutError   -> handleReplicationError conn@@ -112,9 +115,9 @@ handleReplicationRow   :: TChan PrimaryKeepAlive   -> WalProgressState-  -> Connection+  -> ReplicantConnection   -> ByteString-  -> (Change -> IO a)+  -> (Change -> IO LSN)   -> IO () handleReplicationRow keepAliveChan walState _ row cb =   case decode @WalCopyData row of@@ -130,15 +133,14 @@             $ ReplicantException             $ "handleReplicationRow (parse error): " ++ err           Right walLogData -> do-            _ <- updateWalProgress walState (changeNextLSN walLogData)-            _ <- cb walLogData-            pure ()+            consumedLSN <- cb walLogData+            updateWalProgress walState consumedLSN       KeepAliveM keepAlive -> atomically $ writeTChan keepAliveChan keepAlive  -- | Used to re-throw an exception received from the server.-handleReplicationError :: Connection -> IO ()+handleReplicationError :: ReplicantConnection -> IO () handleReplicationError conn = do-  err <- errorMessage conn+  err <- errorMessage $ getConnection conn   throwIO (ReplicantException $ B.unpack . fromMaybe "Unknown error" $ err)   pure () @@ -149,16 +151,16 @@ -- race the /keep-alive/ and /copy data/ handler threads.  It will -- catch and rethrow exceptions from either thread if any fails or -- returns.-startReplicationStream :: Connection -> ByteString -> LSN -> Int -> (Change -> IO a) -> IO ()+startReplicationStream :: ReplicantConnection -> ByteString -> LSN -> Int -> (Change -> IO LSN) -> IO () startReplicationStream conn slotName systemLogPos _ cb = do   let initialWalProgress = WalProgress systemLogPos systemLogPos systemLogPos   walProgressState <- WalProgressState <$> newMVar initialWalProgress   replicationCommandQuery <- startReplicationCommand conn slotName systemLogPos-  result <- exec conn replicationCommandQuery+  result <- exec (getConnection conn) replicationCommandQuery   case result of     Nothing -> do       err <- fromMaybe "startReplicationStream: unknown error starting stream"-        <$> errorMessage conn+        <$> errorMessage (getConnection conn)       throwIO $ ReplicantException $ "startReplicationStream: " ++ B.unpack err     Just r  -> do       status <- resultStatus r@@ -170,18 +172,18 @@             (handleCopyOutData keepAliveChan walProgressState conn cb)             `catch`             \exc -> do-              finish conn+              finish (getConnection conn)               throwIO @SomeException exc           return ()         _ -> do-          err <- fromMaybe "startReplicationStream: unknown error entering COPY mode" <$> errorMessage conn+          err <- fromMaybe "startReplicationStream: unknown error entering COPY mode" <$> errorMessage (getConnection conn)           throwIO $ ReplicantException $ B.unpack err  -- | This listens on the channel for /primary keep-alive messages/ -- from the server and responds to them with the /update status/ -- message using the current WAL stream state.  It will attempt to -- buffer prior update messages when the socket is blocked.-keepAliveHandler :: Connection -> TChan PrimaryKeepAlive -> WalProgressState -> IO ()+keepAliveHandler :: ReplicantConnection -> TChan PrimaryKeepAlive -> WalProgressState -> IO () keepAliveHandler conn msgs walProgressState = forever $ do   mKeepAlive <- atomically $ tryReadTChan msgs   case mKeepAlive of@@ -196,7 +198,7 @@           sendStatusUpdate conn walProgressState  sendStatusUpdate-  :: Connection+  :: ReplicantConnection   -> WalProgressState   -> IO () sendStatusUpdate conn w@(WalProgressState walState) = do@@ -209,20 +211,20 @@         applied         timestamp         DoNotRespond-  copyResult <- putCopyData conn $ encode statusUpdate+  copyResult <- putCopyData (getConnection conn) $ encode statusUpdate   case copyResult of     CopyInOk -> do-      flushResult <- flush conn+      flushResult <- flush (getConnection conn)       case flushResult of         FlushOk -> pure ()         FlushFailed -> do-          err <- fromMaybe "sendStatusUpdate: error flushing message to server" <$> errorMessage conn+          err <- fromMaybe "sendStatusUpdate: error flushing message to server" <$> errorMessage (getConnection conn)           throwIO $ ReplicantException $ B.unpack err-        FlushWriting -> tryAgain conn w+        FlushWriting -> tryAgain (getConnection conn) w     CopyInError -> do-      err <- fromMaybe "sendStatusUpdate: unknown error sending COPY IN" <$> errorMessage conn+      err <- fromMaybe "sendStatusUpdate: unknown error sending COPY IN" <$> errorMessage (getConnection conn)       throwIO $ ReplicantException $ B.unpack err-    CopyInWouldBlock -> tryAgain conn w+    CopyInWouldBlock -> tryAgain (getConnection conn) w   where     tryAgain c ws = do       mSockFd <- socket c
− src/Database/PostgreSQL/Replicant/Queue.hs
@@ -1,77 +0,0 @@-{-|-Module      : Database.PostgreSQL.Replicant.Queue-Description : Bounded and unbounded FIFO queues-Copyright   : (c) James King, 2020, 2021-License     : BSD3-Maintainer  : james@agentultra.com-Stability   : experimental-Portability : POSIX--Shared FIFO queues--}-module Database.PostgreSQL.Replicant.Queue where--import Control.Concurrent.MVar-import Data.Sequence (Seq, ViewR (..), (<|), (|>))-import qualified Data.Sequence as S--data BoundedFifoQueueMeta a-  = BoundedFifoQueueMeta-  { boundedFifoQueueSize :: Int-  , boundedFifoQueue     :: Seq a-  }-  deriving (Eq, Show)--newtype BoundedFifoQueue a = BoundedFifoQueue (MVar (BoundedFifoQueueMeta a))--newtype BoundedQueueException a-  = BoundedQueueOverflow a-  deriving (Eq, Show)--emptyBounded :: Int -> IO (BoundedFifoQueue a)-emptyBounded size =-  BoundedFifoQueue <$> newMVar (BoundedFifoQueueMeta size S.empty)--enqueueBounded :: BoundedFifoQueue a -> a -> IO (Either (BoundedQueueException a) ())-enqueueBounded (BoundedFifoQueue mQueue) x = do-  b@(BoundedFifoQueueMeta size queue) <- takeMVar mQueue-  if size == S.length queue-    then pure $ Left $ BoundedQueueOverflow x-    else do-    putMVar mQueue $ b { boundedFifoQueue = x <| queue }-    pure $ Right ()--newtype FifoQueue a = FifoQueue (MVar (Seq a))--empty :: IO (FifoQueue a)-empty = FifoQueue <$> newMVar S.empty---- | Return @True@ if the queue is empty-null :: FifoQueue a -> IO Bool-null (FifoQueue mQueue) = do-  queue <- readMVar mQueue-  pure $ S.null queue---- | Remove an item from the end of the non-empty queue.-dequeue :: FifoQueue a -> IO (Maybe a)-dequeue (FifoQueue mQueue) = do-  queue <- takeMVar mQueue-  case S.viewr queue of-    S.EmptyR -> do-      putMVar mQueue queue-      pure Nothing-    rest :> x -> do-      putMVar mQueue rest-      pure $ Just x---- | Put an item on the front of the queue.-enqueue :: FifoQueue a -> a -> IO ()-enqueue (FifoQueue mQueue) x = do-  queue <- takeMVar mQueue-  putMVar mQueue $ x <| queue---- | Put an item on the end of the queue so that it will be dequeued first.-enqueueRight :: FifoQueue a -> a -> IO ()-enqueueRight (FifoQueue mQueue) x = do-  queue <- takeMVar mQueue-  putMVar mQueue $ queue |> x
src/Database/PostgreSQL/Replicant/ReplicationSlot.hs view
@@ -14,10 +14,12 @@  import Control.Exception import Data.ByteString (ByteString)+import Data.Maybe import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as B8 import Database.PostgreSQL.LibPQ +import Database.PostgreSQL.Replicant.Connection import Database.PostgreSQL.Replicant.Exception import Database.PostgreSQL.Replicant.Types.Lsn @@ -47,9 +49,9 @@ parseSlotActive "f" = Inactive parseSlotActive _   = Inactive -createReplicationSlotCommand :: Connection -> ByteString -> IO ByteString+createReplicationSlotCommand :: ReplicantConnection -> ByteString -> IO ByteString createReplicationSlotCommand conn slotName = do-  escapedName <- escapeIdentifier conn slotName+  escapedName <- escapeIdentifier (getConnection conn) slotName   case escapedName of     Nothing -> throwIO $ ReplicantException $ "Invalid slot name: " ++ show slotName     Just escaped ->@@ -64,10 +66,10 @@ -- | Create a replication slot using synchronous query execution. -- -- May throw an exception if the command fails.-createReplicationSlotSync :: Connection -> ByteString -> IO ReplicationSlotInfo+createReplicationSlotSync :: ReplicantConnection -> ByteString -> IO ReplicationSlotInfo createReplicationSlotSync conn slotName = do   createReplicationSlotQuery <- createReplicationSlotCommand conn slotName-  result <- exec conn createReplicationSlotQuery+  result <- exec (getConnection conn) createReplicationSlotQuery   case result of     Just r -> do       resultStatus <- resultStatus r@@ -82,18 +84,18 @@                 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+              err <- fromMaybe "createReplicationSlotSync: unknown error" <$> errorMessage (getConnection conn)               throwIO $ ReplicantException (B8.unpack err)         _ -> do-          err <- maybe "createReplicationSlotSync: unknown error" id <$> errorMessage conn+          err <- fromMaybe "createReplicationSlotSync: unknown error" <$> errorMessage (getConnection conn)           throwIO $ ReplicantException (B8.unpack err)     _ -> do-      err <- maybe "createReplicationSlotSync: unknown error" id <$> errorMessage conn+      err <- fromMaybe "createReplicationSlotSync: unknown error" <$> errorMessage (getConnection conn)       throwIO $ ReplicantException (B8.unpack err) -getReplicationSlotInfoCommand :: Connection -> ByteString -> IO ByteString+getReplicationSlotInfoCommand :: ReplicantConnection -> ByteString -> IO ByteString getReplicationSlotInfoCommand conn slotName = do-  escapedName <- escapeStringConn conn slotName+  escapedName <- escapeStringConn (getConnection conn) slotName   case escapedName of     Nothing -> throwIO $ ReplicantException $ "Invalid slot name: " ++ show slotName     Just escaped ->@@ -109,10 +111,10 @@ -- @Nothing@ when the requested slot cannot be found. -- -- May throw an exception if the command query fails.-getReplicationSlotSync :: Connection -> ByteString -> IO (Maybe ReplicationSlotInfo)+getReplicationSlotSync :: ReplicantConnection -> ByteString -> IO (Maybe ReplicationSlotInfo) getReplicationSlotSync conn slotName = do   replicationSlotInfoQuery <- getReplicationSlotInfoCommand conn slotName-  result <- exec conn replicationSlotInfoQuery+  result <- exec (getConnection conn) replicationSlotInfoQuery   case result of     Just r -> do       resultStatus <- resultStatus r@@ -135,16 +137,16 @@               _ ->  pure Nothing         _ -> pure Nothing     _ -> do-      err <- maybe "getReplicationSlotSync: unknown error" id <$> errorMessage conn+      err <- fromMaybe "getReplicationSlotSync: unknown error" <$> errorMessage (getConnection 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 :: ReplicantConnection -> ByteString -> IO ReplicationSlotInfo setupReplicationSlot conn slotName = do   maybeSlot <- getReplicationSlotSync conn slotName   case maybeSlot of-    Just slot -> pure $ slot+    Just slot -> pure slot     Nothing   -> createReplicationSlotSync conn slotName
+ src/Database/PostgreSQL/Replicant/Settings.hs view
@@ -0,0 +1,28 @@+module Database.PostgreSQL.Replicant.Settings where++import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as B++data PgSettings+  = PgSettings+  { pgUser        :: String+  , pgPassword    :: Maybe String+  , pgDbName      :: String+  , pgHost        :: String+  , pgPort        :: String+  , pgSlotName    :: String+  , pgUpdateDelay :: String -- ^ Controls how frequently the+                            -- primaryKeepAlive thread updates+                            -- PostgresSQL in @ms@+  }+  deriving (Eq, Show)++pgConnectionString :: PgSettings -> ByteString+pgConnectionString PgSettings {..} = B.intercalate " "+  [ "user=" <> B.pack pgUser+  , maybe "" (\pgPass -> "pass=" <> B.pack pgPass) pgPassword+  , "dbname=" <> B.pack pgDbName+  , "host=" <> B.pack pgHost+  , "port=" <> B.pack pgPort+  , "replication=database"+  ]
src/Database/PostgreSQL/Replicant/Types/Lsn.hs view
@@ -33,6 +33,7 @@ import Data.ByteString.Lazy.Builder.ASCII (word32Hex) import Data.Serialize import Data.Word+import qualified Data.Text as T import qualified Data.Text.Encoding as T import GHC.Int @@ -51,7 +52,7 @@   get = fromInt64 <$> getInt64be  instance ToJSON LSN where-  toJSON = String . T.decodeUtf8 . toByteString+  toJSON = String . T.toUpper . T.decodeUtf8 . toByteString  instance FromJSON LSN where   parseJSON = withText "LSN" $ \txt ->
test/Spec.hs view
@@ -8,8 +8,8 @@ import GHC.Int  import Database.PostgreSQL.Replicant.Message+import Database.PostgreSQL.Replicant.Settings import Database.PostgreSQL.Replicant.Types.Lsn-import qualified Database.PostgreSQL.Replicant.Queue as Q  examplePrimaryKeepAliveMessage :: ByteString examplePrimaryKeepAliveMessage@@ -85,36 +85,30 @@           let (Right lsn) = fromByteString "16/3002D50"           (toByteString <$> (decode . encode @LSN $ lsn)) `shouldBe` Right "16/3002d50" -  describe "FifoQueue" $ do-    it "should dequeue Nothing if the queue is empty" $ do-      queue <- Q.empty @Int-      res <- Q.dequeue queue-      res `shouldBe` Nothing--    it "should respect first-in, first-out" $ do-      queue <- Q.empty-      Q.enqueue queue 1-      Q.enqueue queue 2-      Q.enqueue queue 3-      res <- Q.dequeue queue-      res `shouldBe` Just 1--    context "null" $ do-      it "should return True if the queue is empty" $ do-        queue <- Q.empty-        res <- Q.null queue-        res `shouldBe` True--      it "should return False if the queue has at least one element" $ do-        queue <- Q.empty-        Q.enqueue queue 1-        res <- Q.null queue-        res `shouldBe` False+  context "Settings" $ do+    describe "pgConnectionString" $ do+      it "should include the password when specified" $ do+        let settings+              = PgSettings+              { pgUser = "foo"+              , pgPassword = Just "bar"+              , pgDbName = "test"+              , pgHost = "hostname"+              , pgPort = "5432"+              , pgSlotName = "test-slot"+              , pgUpdateDelay = "43"+              }+        pgConnectionString settings `shouldBe` "user=foo pass=bar dbname=test host=hostname port=5432 replication=database" -    context "enqueueRight" $ do-      it "should enqueue an item at the head of the queue" $ do-        queue <- Q.empty-        Q.enqueue queue 1-        Q.enqueueRight queue 2-        res <- Q.dequeue queue-        res `shouldBe` Just 2+      it "should omit the password when not specified" $ do+        let settings+              = PgSettings+              { pgUser = "foo"+              , pgPassword = Nothing+              , pgDbName = "test"+              , pgHost = "hostname"+              , pgPort = "5432"+              , pgSlotName = "test-slot"+              , pgUpdateDelay = "43"+              }+        pgConnectionString settings `shouldBe` "user=foo  dbname=test host=hostname port=5432 replication=database"