PostgreSQL Logical Replication support.
Provides types and procedures for consuming a logical replication stream via the PostgreSQL streaming replication protocol. The streaming API is plugin-agnostic (raw WAL bytes are delivered to a callback). A built-in decoder for the pgoutput logical decoding plugin is included.
Quick start
let conn = await connectReplication("postgresql://user:pass@host/db") defer: await conn.close() let slot = await conn.createReplicationSlot("my_slot", "pgoutput", temporary = true) await conn.startReplication("my_slot", slot.consistentPoint, options = {"proto_version": "1", "publication_names": "my_pub"}, callback = myCallback)
Types
BeginMessage = object finalLsn*: Lsn ## LSN of the commit record commitTime*: int64 ## Commit timestamp (microseconds since PG epoch) xid*: int32 ## Transaction ID
- Transaction begin.
CommitMessage = object flags*: byte commitLsn*: Lsn endLsn*: Lsn commitTime*: int64
- Transaction commit.
DeleteMessage = object relationId*: int32 keyKind*: char ## 'K' if oldTuple holds only the replica identity key, ## 'O' if it holds the full old row (REPLICA IDENTITY FULL). oldTuple*: seq[TupleField]
- Row deletion.
InsertMessage = object relationId*: int32 newTuple*: seq[TupleField]
- Row insertion.
LogicalMessage = object flags*: byte ## Bit 0: transactional lsn*: Lsn prefix*: string content*: seq[byte]
- Generic logical decoding message (via pg_logical_emit_message).
Lsn = distinct uint64
- LSN (Log Sequence Number) PostgreSQL Log Sequence Number. Displayed as "X/Y" where X and Y are hex-encoded upper and lower 32-bit halves.
OriginMessage = object originLsn*: Lsn originName*: string
- Replication origin.
PgOutputMessage = object case kind*: PgOutputMessageKind of pomkBegin: begin*: BeginMessage of pomkCommit: commit*: CommitMessage of pomkOrigin: origin*: OriginMessage of pomkRelation: relation*: RelationInfo of pomkType: typeMsg*: TypeMessage of pomkInsert: insert*: InsertMessage of pomkUpdate: update*: UpdateMessage of pomkDelete: delete*: DeleteMessage of pomkTruncate: truncate*: TruncateMessage of pomkMessage: message*: LogicalMessage
- A decoded pgoutput plugin message.
PgOutputMessageKind = enum pomkBegin, pomkCommit, pomkOrigin, pomkRelation, pomkType, pomkInsert, pomkUpdate, pomkDelete, pomkTruncate, pomkMessage
- Message types within the pgoutput logical decoding plugin.
PrimaryKeepalive = object walEnd*: Lsn ## Current end of WAL on the server sendTime*: int64 ## Server send time (microseconds since PG epoch) replyRequested*: bool ## Whether the server wants an immediate status reply
- Keepalive message from the server.
RelationCache = Table[int32, RelationInfo]
- Cache of relation metadata received during replication. The server sends a Relation message before the first DML for each table in a transaction; clients must cache them.
RelationColumn = object flags*: byte ## Bit 0: part of replica identity key name*: string typeOid*: int32 typeMod*: int32
- A single column in a relation definition.
RelationInfo = object relationId*: int32 namespace*: string ## Schema name name*: string ## Table name replicaIdentity*: char ## 'd' (default), 'n' (nothing), 'f' (full), 'i' (index) columns*: seq[RelationColumn]
- Relation (table) metadata sent by pgoutput before DML events.
ReplicationCallback = proc (msg: ReplicationMessage): Future[void] {....gcsafe.}
- Callback invoked for each replication message during streaming.
ReplicationMessage = object case kind*: ReplicationMessageKind of rmkXLogData: xlogData*: XLogData of rmkPrimaryKeepalive: keepalive*: PrimaryKeepalive
- A single message received during replication streaming.
ReplicationMessageKind = enum rmkXLogData, rmkPrimaryKeepalive
- Replication message types (decoded from CopyData during streaming)
ReplicationMode = enum rmDatabase, rmPhysical
- Replication mode selected at connection time. rmDatabase sends replication=database (logical replication + ability to run SQL on the chosen database). rmPhysical sends replication=true (physical replication; no SQL on user databases).
ReplicationSlotInfo = object slotName*: string consistentPoint*: Lsn ## confirmed_flush_lsn (logical) or restart_lsn (physical) snapshotName*: string ## Snapshot name (only available at CREATE time) outputPlugin*: string ## Output plugin (only available at CREATE time) slotType*: string ## Slot type as reported by READ_REPLICATION_SLOT ("physical"). ## Empty for CREATE_REPLICATION_SLOT results, which do not return it. restartTli*: int64 ## Timeline ID associated with restart_lsn. ## Only populated by READ_REPLICATION_SLOT; 0 when NULL or not applicable.
- Information about a replication slot.
SystemInfo = object systemId*: string timeline*: int32 xLogPos*: Lsn dbName*: string
- Result of IDENTIFY_SYSTEM command.
TimelineHistory = object filename*: string ## Timeline history file name (e.g. "00000002.history"). content*: seq[byte] ## Raw history file content.
- Result of TIMELINE_HISTORY command.
TruncateMessage = object options*: byte ## Bit 0: CASCADE, bit 1: RESTART IDENTITY relationIds*: seq[int32]
- Table truncation.
TupleDataKind = enum tdkNull = 110, ## NULL value tdkText = 116, ## Text-formatted value tdkBinary = 98, ## Binary-formatted value (protocol_version >= 2) tdkUnchanged = 117 ## TOAST value unchanged
- Kind of a single field value in a pgoutput tuple.
TupleField = object kind*: TupleDataKind data*: seq[byte] ## Empty for null/unchanged
- A single field value in a pgoutput tuple.
TypeMessage = object typeId*: int32 namespace*: string name*: string
- Custom type definition.
UpdateMessage = object relationId*: int32 keyKind*: char ## 'K' if oldTuple holds only the replica identity key, ## 'O' if it holds the full old row (REPLICA IDENTITY FULL), ## '\0' when no old tuple is present. oldTuple*: seq[TupleField] newTuple*: seq[TupleField]
- Row update.
XLogData = object startLsn*: Lsn ## Start LSN of the WAL data in this message walEnd*: Lsn ## Current end of WAL on the server at the time this message was sent. ## This is *not* the end of the WAL data contained in this message; it ## reflects how far WAL has advanced on the server and is informational. ## To acknowledge what was actually received, use ``receivedEndLsn`` ## (``startLsn + data.len``), never ``walEnd`` — ``walEnd`` may be ahead ## of what this message contains. sendTime*: int64 ## Server send time (microseconds since PG epoch) data*: seq[byte] ## Raw WAL data (plugin-dependent format)
- WAL data payload from the server.
Consts
InvalidLsn = 0'u
- Sentinel value representing an invalid or unset LSN.
Procs
proc confirmedFlushLsn(conn: PgConnection): Lsn {.inline, ...raises: [], tags: [], forbids: [].}
- Confirmed flush LSN for current stream, or InvalidLsn outside stream.
proc confirmFlushed(conn: PgConnection; lsn: Lsn): bool {. ...raises: [PgStateError, PgConnectionError], tags: [], forbids: [].}
- Confirm WAL up to lsn as durable. Clamped to received WAL, monotonic. Returns true if advanced. Must be in csReplicating.
proc connectReplication(config: ConnConfig; mode: ReplicationMode = rmDatabase): Future[ PgConnection] {....raises: [Exception, ValueError, PgConfigError, PgConnectionError, CatchableError], tags: [RootEffect, WriteIOEffect, TimeEffect], forbids: [].}
- Connect with replication param. rmPhysical allows only replication cmds.
proc connectReplication(dsn: string; mode: ReplicationMode = rmDatabase): Future[ PgConnection] {....raises: [PgConfigError, ValueError, Exception, PgConnectionError, CatchableError], tags: [ ReadIOEffect, RootEffect, WriteIOEffect, TimeEffect], forbids: [].}
- DSN-string variant of connectReplication. See the ConnConfig overload for the meaning of mode.
proc createReplicationSlot(conn: PgConnection; slotName: string; plugin: string = "pgoutput"; temporary: bool = false; timeout: async_backend.Duration = ZeroDuration): Future[ ReplicationSlotInfo] {....stackTrace: false, raises: [Exception, ValueError, CatchableError, PgConnectionError, PgTypeError, PgError, PgProtocolError], tags: [RootEffect, TimeEffect], forbids: [].}
-
Create a logical replication slot. Returns slot info including the consistent point LSN.
On timeout, the connection is retired (csClosed) unless the wire had settled (asyncdispatch always retires: the timed-out op stays on the socket).
proc currentPgTimestamp(): int64 {....raises: [], tags: [TimeEffect], forbids: [].}
- Current time as microseconds since the PostgreSQL epoch (2000-01-01 UTC).
proc decodePgOutput(msg: XLogData): PgOutputMessage {....raises: [PgProtocolError], tags: [], forbids: [].}
- Convenience: decode the pgoutput message from an XLogData's data field.
proc dropReplicationSlot(conn: PgConnection; slotName: string; wait: bool = false; timeout: async_backend.Duration = ZeroDuration): Future[ void] {....stackTrace: false, raises: [Exception, ValueError, CatchableError], tags: [RootEffect, TimeEffect], forbids: [].}
-
Drop a replication slot.
On timeout, the connection is retired (csClosed) unless the wire had settled (asyncdispatch always retires: the timed-out op stays on the socket).
proc hasOldTuple(msg: UpdateMessage): bool {.inline, ...raises: [], tags: [], forbids: [].}
- True if the update carries an old tuple (replica identity key or full row).
proc identifySystem(conn: PgConnection; timeout: async_backend.Duration = ZeroDuration): Future[ SystemInfo] {....stackTrace: false, raises: [Exception, ValueError, CatchableError, PgConnectionError, PgTypeError, PgError, PgProtocolError], tags: [RootEffect, TimeEffect], forbids: [].}
-
Execute IDENTIFY_SYSTEM and return system identification info.
On timeout, the connection is retired (csClosed) unless the wire had settled (asyncdispatch always retires: the timed-out op stays on the socket).
proc parseLsn(s: string): Lsn {....raises: [PgTypeError, PgTypeError], tags: [], forbids: [].}
- Parse an LSN from "X/Y" hex string. Converts a malformed value (wrong shape, non-hex halves, or a half wider than 32 bits) into PgTypeError so callers stay under the except PgError contract, mirroring parseTimelineId.
proc parsePgOutputMessage(data: openArray[byte]): PgOutputMessage {. ...raises: [PgProtocolError], tags: [], forbids: [].}
- Decode a pgoutput logical decoding message from raw WAL bytes.
proc parseReplicationMessage(copyData: sink seq[byte]): ReplicationMessage {. ...raises: [PgProtocolError], tags: [], forbids: [].}
- Parse a CopyData payload into a ReplicationMessage. Takes ownership of copyData so the XLogData path can reuse the incoming buffer for xlogData.data instead of slicing into a fresh allocation.
proc parseTimelineId(s: string): int32 {....raises: [PgTypeError], tags: [], forbids: [].}
- Parse the timeline id from an IDENTIFY_SYSTEM result row (text format). Converts a non-numeric value and an out-of-int32-range value into PgTypeError so callers stay under the except PgError contract. Range-check before narrowing: a bare parseInt(...).int32 would raise RangeDefect (a Defect, outside PgError) on an out-of-range value.
proc readReplicationSlot(conn: PgConnection; slotName: string; timeout: async_backend.Duration = ZeroDuration): Future[ ReplicationSlotInfo] {....stackTrace: false, raises: [Exception, ValueError, CatchableError, PgConnectionError, PgTypeError, PgError, PgProtocolError], tags: [RootEffect, TimeEffect], forbids: [].}
-
Read information about an existing physical replication slot.
Only physical slots are supported: the server rejects a logical slot with PgQueryError ("cannot use READ_REPLICATION_SLOT with logical replication slot") and reports a nonexistent slot as PgConnectionError. consistentPoint carries restart_lsn (InvalidLsn when the slot never reserved WAL) and restartTli its timeline (0 when NULL).
On timeout, the connection is retired (csClosed) unless the wire had settled (asyncdispatch always retires: the timed-out op stays on the socket).
proc receivedEndLsn(msg: XLogData): Lsn {....raises: [PgProtocolError], tags: [], forbids: [].}
- End LSN of the WAL data actually contained in this message (startLsn + len(data)). Use this when acknowledging received data via sendStandbyStatus; do not use walEnd, which is the server's current WAL position and may point past data this message does not carry.
proc sendCopyData(conn: PgConnection; data: openArray[byte]): Future[void] {....raises: [ PgStateError, PgConnectionError, PgTypeError, PgProtocolError, Exception, CancelledError, CatchableError], tags: [RootEffect], forbids: [].}
- Send CopyData during csReplicating. Raises PgStateError (not replicating) / PgConnectionError (connection lost) / PgTypeError synchronously before first suspension. data is encoded into the frame there too, so the caller's buffer need not outlive the returned Future.
proc sendStandbyStatus(conn: PgConnection; receiveLsn: Lsn; flushLsn: Lsn = InvalidLsn; applyLsn: Lsn = InvalidLsn; replyRequested: bool = false): Future[void] {. ...stackTrace: false, raises: [Exception, PgStateError, PgConnectionError, CancelledError, CatchableError, ValueError], tags: [RootEffect, TimeEffect], forbids: [].}
- Send Standby Status Update. InvalidLsn defaults up to receiveLsn. Raises PgStateError unless the connection is csReplicating, or PgConnectionError when the connection was lost.
proc startPhysicalReplication(conn: PgConnection; startLsn: Lsn; slotName: string = ""; timeline: int32 = 0; autoKeepaliveReply: bool = true; statusInterval: async_backend.Duration = ZeroDuration; callback: ReplicationCallback): Future[void] {. ...stackTrace: false, raises: [Exception, PgStateError, PgConnectionError, PgTypeError, PgProtocolError, CancelledError, CatchableError, ValueError, PgQueryError, AsyncTimeoutError], tags: [RootEffect, TimeEffect], forbids: [].}
-
Physical replication streaming. Callback per message, raw WAL in XLogData. Raises PgConnectionError (closed) / PgStateError (busy) unless csReady. Error handling matches startReplication: a callback exception or any other mid-stream failure poisons the connection (marked closed) and propagates, so reconnect and resume from the last LSN tracked.
slotName = "" streams without a slot. A non-zero timeline is appended as TIMELINE n, so the server aborts the stream if it advanced past that timeline. statusInterval behaves as on startReplication.
On a timeline switch the server may send a result set describing the next timeline between CopyDone and ReadyForQuery; this proc drains and discards it — re-issue IDENTIFY_SYSTEM if you need that information.
Synchronous standbys: the flush LSN governs how much WAL the primary may recycle, so a standby in synchronous_standby_names that relies on the auto-reply must call confirmFlushed (or reply manually) or the primary's COMMITs block waiting on a flush position that never advances.
proc startReplication(conn: PgConnection; slotName: string; startLsn: Lsn = InvalidLsn; options: seq[(string, string)] = @[]; autoKeepaliveReply: bool = true; statusInterval: async_backend.Duration = ZeroDuration; callback: ReplicationCallback): Future[void] {. ...stackTrace: false, raises: [Exception, ValueError, PgStateError, PgConnectionError, PgTypeError, PgProtocolError, CancelledError, CatchableError, PgQueryError, AsyncTimeoutError], tags: [RootEffect, TimeEffect], forbids: [].}
-
Begin logical replication. Callback invoked per message. Use confirmFlushed for flush tracking; or set autoKeepaliveReply=false and use sendStandbyStatus manually.
Returns on server CopyDone or connection close. To stop from the client side, call stopReplication from the callback (or a concurrent task).
Errors poison connection. Track LSN for resume. A failing auto-reply propagates too, and the callback is not invoked for that keepalive. Option values are passed unquoted and single-quoted when building the command (keys stay identifier-validated). An empty value means a flag-only option (binary rather than binary ''). Raises PgConnectionError (closed) / PgStateError (busy) unless csReady, and ValueError for a proto_version other than 1 in options (the value must be the unquoted string "1", an empty one included): the bundled pgoutput decoder supports v1 only. Any value already wrapped in quotes raises ValueError too, whatever its key, so the verbatim-options spelling cannot silently name a publication 'my_pub' or send a thrice-quoted binary flag the plugin rejects mid-stream. An empty publication_names and a value containing a NUL byte are rejected the same way. publication_names without an explicit proto_version adds proto_version '1' to the generated command, so a server-side default bump cannot outrun that decoder.
statusInterval (ZeroDuration = off) sends a proactive Standby Status Update at least that often — receive = highest received, flush/apply = confirmFlushed — so the slot advances on a server that never requests a reply (wal_sender_timeout = 0). Honoured only with autoKeepaliveReply; under asyncdispatch it fires only while messages are flowing, so a fully idle stream sends nothing until the next message.
Synchronous standbys: the auto-reply reports receive and flush/apply separately, so a consumer in synchronous_standby_names that never calls confirmFlushed keeps wal_sender_timeout reset via the receive field yet never advances flush — the primary's COMMITs then block indefinitely.
proc stopReplication(conn: PgConnection): Future[void] {....stackTrace: false, raises: [ Exception, PgStateError, PgConnectionError, CancelledError, CatchableError, ValueError], tags: [RootEffect, TimeEffect], forbids: [].}
- Terminate replication. Flushes confirmed position before CopyDone. Raises PgStateError unless the connection is csReplicating, or PgConnectionError when the connection was lost.
proc timelineHistory(conn: PgConnection; timeline: int32; timeout: async_backend.Duration = ZeroDuration): Future[ TimelineHistory] {....stackTrace: false, raises: [Exception, ValueError, CatchableError, PgConnectionError, PgTypeError, PgError, PgProtocolError], tags: [RootEffect, TimeEffect], forbids: [].}
- Execute TIMELINE_HISTORY. Raises ValueError if timeline <=0. On timeout the connection is marked csClosed (protocol out of sync), so a caller catching PgTimeoutError must reconnect, not retry in place.
proc toString(field: TupleField): string {....raises: [PgProtocolError], tags: [], forbids: [].}
- Convert a TupleField's data to a string by copying the bytes.
Templates
template makeReplicationCallback(body: untyped): ReplicationCallback
-
Create a ReplicationCallback that works with both asyncdispatch and chronos. Inside body, the current message is available as msg: ReplicationMessage.
Kept module-local: routing this through a shared template with an untyped/typedesc param for the parameter type trips asyncdispatch's {.async.} macro ("cannot use symbol of kind 'func' as a 'param'").