Files
2026-07-13 12:32:25 +08:00

846 lines
27 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package logrepl
import (
"context"
"fmt"
"log"
"math"
"os"
"strings"
"sync"
"time"
"github.com/cockroachdb/errors"
"github.com/jackc/pglogrepl"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgproto3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/lib/pq/oid"
"github.com/dolthub/doltgresql/postgres/parser/uuid"
)
const outputPlugin = "pgoutput"
type rcvMsg struct {
msg pgproto3.BackendMessage
err error
}
type LogicalReplicator struct {
primaryDns string
replicationDns string
walFilePath string
running bool
messageReceived bool
stop chan struct{}
mu *sync.Mutex
}
// NewLogicalReplicator creates a new logical replicator instance which connects to the primary and replication
// databases using the connection strings provided. The connection to the replica is established immediately, and the
// connection to the primary is established when StartReplication is called.
func NewLogicalReplicator(walFilePath string, primaryDns string, replicationDns string) (*LogicalReplicator, error) {
return &LogicalReplicator{
primaryDns: primaryDns,
replicationDns: replicationDns,
walFilePath: walFilePath,
mu: &sync.Mutex{},
}, nil
}
// PrimaryDns returns the DNS for the primary database. Not suitable for RPCs used in replication e.g.
// StartReplication. See ReplicationDns.
func (r *LogicalReplicator) PrimaryDns() string {
return r.primaryDns
}
// ReplicationDns returns the DNS for the primary database with the replication query parameter appended. Not suitable
// for normal query RPCs.
func (r *LogicalReplicator) ReplicationDns() string {
if strings.Contains(r.primaryDns, "?") {
return fmt.Sprintf("%s&replication=database", r.primaryDns)
}
return fmt.Sprintf("%s?replication=database", r.primaryDns)
}
// CaughtUp returns true if the replication slot is caught up to the primary, and false otherwise. This only works if
// there is only a single replication slot on the primary, so it's only suitable for testing. This method uses a
// threshold value to determine if the primary considers us caught up. This corresponds to the maximum number of bytes
// that the primary is ahead of the replica's last flush position. This rarely is zero when caught up, since the
// primary often sends additional WAL records after the last WAL location that was flushed to the replica. These
// additional WAL locations cannot be recorded as flushed since they don't result in writes to the replica, and could
// result in the primary not sending us necessary records after a shutdown and restart.
func (r *LogicalReplicator) CaughtUp(threshold int) (bool, error) {
r.mu.Lock()
if !r.messageReceived {
r.mu.Unlock()
// We can't query the replication state until after receiving our first message
return false, nil
}
r.mu.Unlock()
conn, err := pgx.Connect(context.Background(), r.PrimaryDns())
if err != nil {
return false, err
}
defer conn.Close(context.Background())
result, err := conn.Query(context.Background(), "SELECT pg_wal_lsn_diff(write_lsn, sent_lsn) AS replication_lag FROM pg_stat_replication")
if err != nil {
return false, err
}
defer result.Close()
for result.Next() {
rows, err := result.Values()
if err != nil {
return false, err
}
row := rows[0]
lag, ok := row.(pgtype.Numeric)
if ok && lag.Valid {
log.Printf("Current replication lag: %v", row)
return int(math.Abs(float64(lag.Int.Int64()))) < threshold, nil
} else {
log.Printf("Replication lag unknown: %v", row)
}
}
if result.Err() != nil {
return false, result.Err()
}
// If we didn't get any rows, that usually means that replication has stopped and we're caught up
return true, nil
}
// maxConsecutiveFailures is the maximum number of consecutive RPC errors that can occur before we stop
// the replication thread
const maxConsecutiveFailures = 10
var errShutdownRequested = errors.New("shutdown requested")
type replicationState struct {
// replicaConn is the current connection to the replica database, which can be re-established if it fails
replicaConn *pgx.Conn
// lastWrittenLSN is the LSN of the commit record of the last transaction that was successfully replicated to the
// database.
lastWrittenLSN pglogrepl.LSN
// lastReceivedLSN is the last WAL position we have received from the server, which we send back to the server via
// SendStandbyStatusUpdate after every message we get.
lastReceivedLSN pglogrepl.LSN
// currentTransactionLSN is the LSN of the current transaction we are processing. This becomes the lastWrittenLSN
// when we get a CommitMessage
currentTransactionLSN pglogrepl.LSN
// inStream tracks the state of the replication stream. When we receive a StreamStartMessage, we set inStream to
// true, and then back to false when we receive a StreamStopMessage.
inStream bool
// We selectively ignore messages that are from before our last flush, which can be resent by postgres in certain
// crash scenarios. Postgres sends messages in batches based on changes in a transaction, beginning with a Begin
// message that records the last WAL position of the transaction. The individual INSERT, UPDATE, DELETE messages are
// sent, each tagged with the WAL position of that tuple write. This WAL position can be before the last flush LSN
// in some cases. Whether we ignore them or not has nothing to do with the WAL position of any individual write, but
// the final LSN of the transaction, as recorded in the Begin message. So for every Begin, we decide whether to
// process or ignore all messages until a corresponding Commit message.
processMessages bool
relations map[uint32]*pglogrepl.RelationMessageV2
typeMap *pgtype.Map
}
// StartReplication starts the replication process for the given slot name. This function blocks until replication is
// stopped via the Stop method, or an error occurs.
func (r *LogicalReplicator) StartReplication(slotName string) error {
standbyMessageTimeout := 10 * time.Second
nextStandbyMessageDeadline := time.Now().Add(standbyMessageTimeout)
lastWrittenLsn, err := r.readWALPosition()
if err != nil {
return err
}
// TODO: we need to be able to re-establish this connection if it goes bad
replicationConn, err := pgx.Connect(context.Background(), r.replicationDns)
if err != nil {
return err
}
state := &replicationState{
lastWrittenLSN: lastWrittenLsn,
replicaConn: replicationConn,
relations: map[uint32]*pglogrepl.RelationMessageV2{},
typeMap: pgtype.NewMap(),
}
var primaryConn *pgconn.PgConn
defer func() {
if primaryConn != nil {
_ = primaryConn.Close(context.Background())
}
if state.replicaConn != nil {
_ = state.replicaConn.Close(context.Background())
}
// We always shut down here and only here, so we do the cleanup on thread exit in exactly one place
r.shutdown()
}()
connErrCnt := 0
handleErrWithRetry := func(err error, incrementErrorCount bool) error {
if err != nil {
if incrementErrorCount {
connErrCnt++
}
if connErrCnt < maxConsecutiveFailures {
log.Printf("Error: %v. Retrying", err)
if primaryConn != nil {
_ = primaryConn.Close(context.Background())
}
primaryConn = nil
return nil
}
} else {
connErrCnt = 0
}
return err
}
sendStandbyStatusUpdate := func(state *replicationState) error {
// The StatusUpdate message wants us to respond with the current position in the WAL + 1:
// https://www.postgresql.org/docs/current/protocol-replication.html
standbyMessage := pglogrepl.StandbyStatusUpdate{
WALWritePosition: state.lastWrittenLSN + 1,
WALFlushPosition: state.lastWrittenLSN + 1,
WALApplyPosition: state.lastReceivedLSN + 1,
}
err := pglogrepl.SendStandbyStatusUpdate(context.Background(), primaryConn, standbyMessage)
if err != nil {
return handleErrWithRetry(err, false)
}
log.Printf("Sent standby message %v\n", standbyMessage)
nextStandbyMessageDeadline = time.Now().Add(standbyMessageTimeout)
return nil
}
log.Println("Starting replicator")
r.mu.Lock()
r.running = true
r.messageReceived = false
r.stop = make(chan struct{})
r.mu.Unlock()
for {
err := func() error {
// Shutdown if requested
select {
case <-r.stop:
return errShutdownRequested
default:
// continue below
}
if primaryConn == nil {
var err error
primaryConn, err = r.beginReplication(slotName, state.lastWrittenLSN)
if err != nil {
// unlike other error cases, back off a little here, since we're likely to just get the same error again
// on initial replication establishment
time.Sleep(3 * time.Second)
return handleErrWithRetry(err, true)
}
}
if time.Now().After(nextStandbyMessageDeadline) && state.lastReceivedLSN > 0 {
err := sendStandbyStatusUpdate(state)
if err != nil {
return err
}
if primaryConn == nil {
// if we've lost the connection, we'll re-establish it on the next pass through the loop
return nil
}
}
ctx, cancel := context.WithDeadline(context.Background(), nextStandbyMessageDeadline)
receiveMsgChan := make(chan rcvMsg)
go func() {
rawMsg, err := primaryConn.ReceiveMessage(ctx)
receiveMsgChan <- rcvMsg{msg: rawMsg, err: err}
}()
var msgAndErr rcvMsg
select {
case <-r.stop:
cancel()
return errShutdownRequested
case <-ctx.Done():
cancel()
return nil
case msgAndErr = <-receiveMsgChan:
cancel()
}
if msgAndErr.err != nil {
if pgconn.Timeout(msgAndErr.err) {
return nil
} else {
return handleErrWithRetry(msgAndErr.err, true)
}
}
r.mu.Lock()
r.messageReceived = true
r.mu.Unlock()
rawMsg := msgAndErr.msg
if errMsg, ok := rawMsg.(*pgproto3.ErrorResponse); ok {
return errors.Errorf("received Postgres WAL error: %+v", errMsg)
}
msg, ok := rawMsg.(*pgproto3.CopyData)
if !ok {
log.Printf("Received unexpected message: %T\n", rawMsg)
return nil
}
switch msg.Data[0] {
case pglogrepl.PrimaryKeepaliveMessageByteID:
pkm, err := pglogrepl.ParsePrimaryKeepaliveMessage(msg.Data[1:])
if err != nil {
log.Fatalln("ParsePrimaryKeepaliveMessage failed:", err)
}
log.Println("Primary Keepalive Message =>", "ServerWALEnd:", pkm.ServerWALEnd, "ServerTime:", pkm.ServerTime, "ReplyRequested:", pkm.ReplyRequested)
state.lastReceivedLSN = pkm.ServerWALEnd
if pkm.ReplyRequested {
// Send our reply the next time through the loop
nextStandbyMessageDeadline = time.Time{}
}
case pglogrepl.XLogDataByteID:
xld, err := pglogrepl.ParseXLogData(msg.Data[1:])
if err != nil {
return err
}
committed, err := r.processMessage(xld, state)
if err != nil {
// TODO: do we need more than one handler, one for each connection?
return handleErrWithRetry(err, true)
}
// TODO: we have a two-phase commit race here: if the WAL file update doesn't happen before the process crashes,
// we will receive a duplicate LSN the next time we start replication. A better solution would be to write the
// LSN directly into the DoltCommit message, and then parsing this message back out when we begin replication
// next.
if committed {
state.lastWrittenLSN = state.currentTransactionLSN
log.Printf("Writing LSN %s to file\n", state.lastWrittenLSN.String())
err := r.writeWALPosition(state.lastWrittenLSN)
if err != nil {
return err
}
}
return sendStandbyStatusUpdate(state)
default:
log.Printf("Received unexpected message: %T\n", rawMsg)
}
return nil
}()
if err != nil {
if errors.Is(err, errShutdownRequested) {
return nil
}
log.Println("Error during replication:", err)
return err
}
}
}
func (r *LogicalReplicator) shutdown() {
r.mu.Lock()
defer r.mu.Unlock()
log.Print("shutting down replicator")
r.running = false
close(r.stop)
}
// Running returns whether replication is currently running
func (r *LogicalReplicator) Running() bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.running
}
// Stop stops the replication process and blocks until clean shutdown occurs.
func (r *LogicalReplicator) Stop() {
r.mu.Lock()
if !r.running {
r.mu.Unlock()
return
}
r.mu.Unlock()
log.Print("stopping replication...")
r.stop <- struct{}{}
// wait for the channel to be closed, acknowledging that the replicator has stopped
<-r.stop
}
// replicateQuery executes the query provided on the replica connection
func (r *LogicalReplicator) replicateQuery(replicationConn *pgx.Conn, query string) error {
log.Printf("replicating query: %s", query)
_, err := replicationConn.Exec(context.Background(), query)
return err
}
// beginReplication starts a new replication connection to the primary server and returns it. The LSN provided is the
// last one we have confirmed that we flushed to disk.
func (r *LogicalReplicator) beginReplication(slotName string, lastFlushLsn pglogrepl.LSN) (*pgconn.PgConn, error) {
conn, err := pgconn.Connect(context.Background(), r.ReplicationDns())
if err != nil {
return nil, err
}
// streaming of large transactions is available since PG 14 (protocol version 2)
// we also need to set 'streaming' to 'true'
pluginArguments := []string{
"proto_version '2'",
fmt.Sprintf("publication_names '%s'", slotName),
"messages 'true'",
"streaming 'true'",
}
// The LSN is the position in the WAL where we want to start replication, but it can only be used to skip entries,
// not rewind to previous entries that we've already confirmed to the primary that we flushed. We still pass an LSN
// for the edge case where we have flushed an entry to disk, but crashed before the primary received confirmation.
// In that edge case, we want to "skip" entries (from the primary's perspective) that we have already flushed to disk.
log.Printf("Starting logical replication on slot %s at WAL location %s", slotName, lastFlushLsn+1)
err = pglogrepl.StartReplication(context.Background(), conn, slotName, lastFlushLsn+1, pglogrepl.StartReplicationOptions{
PluginArgs: pluginArguments,
})
if err != nil {
return nil, err
}
log.Println("Logical replication started on slot", slotName)
return conn, nil
}
// DropPublication drops the publication with the given name if it exists. Mostly useful for testing.
func DropPublication(primaryDns, slotName string) error {
conn, err := pgconn.Connect(context.Background(), primaryDns)
if err != nil {
return err
}
defer conn.Close(context.Background())
result := conn.Exec(context.Background(), fmt.Sprintf("DROP PUBLICATION IF EXISTS %s;", slotName))
_, err = result.ReadAll()
return err
}
// CreatePublication creates a publication with the given name if it does not already exist. Mostly useful for testing.
// Customers should run the CREATE PUBLICATION command on their primary server manually, specifying whichever tables
// they want to replicate.
func CreatePublication(primaryDns, slotName string) error {
conn, err := pgconn.Connect(context.Background(), primaryDns)
if err != nil {
return err
}
defer conn.Close(context.Background())
result := conn.Exec(context.Background(), fmt.Sprintf("CREATE PUBLICATION %s FOR ALL TABLES;", slotName))
_, err = result.ReadAll()
return err
}
// DropReplicationSlot drops the replication slot with the given name. Any error from the slot not existing is ignored.
func (r *LogicalReplicator) DropReplicationSlot(slotName string) error {
conn, err := pgconn.Connect(context.Background(), r.ReplicationDns())
if err != nil {
return err
}
_ = pglogrepl.DropReplicationSlot(context.Background(), conn, slotName, pglogrepl.DropReplicationSlotOptions{})
return nil
}
// CreateReplicationSlotIfNecessary creates the replication slot named if it doesn't already exist.
func (r *LogicalReplicator) CreateReplicationSlotIfNecessary(slotName string) error {
conn, err := pgx.Connect(context.Background(), r.PrimaryDns())
if err != nil {
return err
}
rows, err := conn.Query(context.Background(), "select * from pg_replication_slots where slot_name = $1", slotName)
if err != nil {
return err
}
slotExists := false
defer rows.Close()
for rows.Next() {
_, err := rows.Values()
if err != nil {
return err
}
slotExists = true
}
if rows.Err() != nil {
return rows.Err()
}
// We need a different connection to create the replication slot
conn, err = pgx.Connect(context.Background(), r.ReplicationDns())
if err != nil {
return err
}
if !slotExists {
_, err = pglogrepl.CreateReplicationSlot(context.Background(), conn.PgConn(), slotName, outputPlugin, pglogrepl.CreateReplicationSlotOptions{})
if err != nil {
pgErr, ok := err.(*pgconn.PgError)
if ok && pgErr.Code == "42710" {
// replication slot already exists, we can ignore this error
} else {
return err
}
}
log.Println("Created replication slot:", slotName)
}
return nil
}
// processMessage processes a logical replication message as appropriate. A couple important aspects:
// 1. Relation messages describe tables being replicated and are used to build a type map for decoding tuples
// 2. INSERT/UPDATE/DELETE messages describe changes to rows that must be applied to the replica.
// These describe a row in the form of a tuple, and are used to construct a query to apply the change to the replica.
//
// Returns a boolean true if the message was a commit that should be acknowledged, and an error if one occurred.
func (r *LogicalReplicator) processMessage(
xld pglogrepl.XLogData,
state *replicationState,
) (bool, error) {
walData := xld.WALData
logicalMsg, err := pglogrepl.ParseV2(walData, state.inStream)
if err != nil {
return false, err
}
log.Printf("XLogData (%T) => WALStart %s ServerWALEnd %s ServerTime %s", logicalMsg, xld.WALStart, xld.ServerWALEnd, xld.ServerTime)
state.lastReceivedLSN = xld.ServerWALEnd
switch logicalMsg := logicalMsg.(type) {
case *pglogrepl.RelationMessageV2:
state.relations[logicalMsg.RelationID] = logicalMsg
case *pglogrepl.BeginMessage:
// Indicates the beginning of a group of changes in a transaction.
// This is only sent for committed transactions. We won't get any events from rolled back transactions.
if state.lastWrittenLSN > logicalMsg.FinalLSN {
log.Printf("Received stale message, ignoring. Last written LSN: %s Message LSN: %s", state.lastWrittenLSN, logicalMsg.FinalLSN)
state.processMessages = false
return false, nil
}
state.processMessages = true
state.currentTransactionLSN = logicalMsg.FinalLSN
log.Printf("BeginMessage: %v", logicalMsg)
err = r.replicateQuery(state.replicaConn, "START TRANSACTION")
if err != nil {
return false, err
}
case *pglogrepl.CommitMessage:
log.Printf("CommitMessage: %v", logicalMsg)
err = r.replicateQuery(state.replicaConn, "COMMIT")
if err != nil {
return false, err
}
state.processMessages = false
return true, nil
case *pglogrepl.InsertMessageV2:
if !state.processMessages {
log.Printf("Received stale message, ignoring. Last written LSN: %s Message LSN: %s", state.lastWrittenLSN, xld.ServerWALEnd)
return false, nil
}
rel, ok := state.relations[logicalMsg.RelationID]
if !ok {
log.Fatalf("unknown relation ID %d", logicalMsg.RelationID)
}
columnStr := strings.Builder{}
valuesStr := strings.Builder{}
for idx, col := range logicalMsg.Tuple.Columns {
if idx > 0 {
columnStr.WriteString(", ")
valuesStr.WriteString(", ")
}
colName := rel.Columns[idx].Name
columnStr.WriteString(colName)
switch col.DataType {
case 'n': // null
valuesStr.WriteString("NULL")
case 't': // text
// We have to round-trip the data through the encodings to get an accurate text rep back
val, err := decodeTextColumnData(state.typeMap, col.Data, rel.Columns[idx].DataType)
if err != nil {
log.Fatalln("error decoding column data:", err)
}
colData, err := encodeColumnData(state.typeMap, val, rel.Columns[idx].DataType)
if err != nil {
return false, err
}
valuesStr.WriteString(colData)
default:
log.Printf("unknown column data type: %c", col.DataType)
}
}
err = r.replicateQuery(state.replicaConn, fmt.Sprintf("INSERT INTO %s.%s (%s) VALUES (%s)", rel.Namespace, rel.RelationName, columnStr.String(), valuesStr.String()))
if err != nil {
return false, err
}
case *pglogrepl.UpdateMessageV2:
if !state.processMessages {
log.Printf("Received stale message, ignoring. Last written LSN: %s Message LSN: %s", state.lastWrittenLSN, xld.ServerWALEnd)
return false, nil
}
// TODO: this won't handle primary key changes correctly
// TODO: this probably doesn't work for unkeyed tables
rel, ok := state.relations[logicalMsg.RelationID]
if !ok {
log.Fatalf("unknown relation ID %d", logicalMsg.RelationID)
}
updateStr := strings.Builder{}
whereStr := strings.Builder{}
for idx, col := range logicalMsg.NewTuple.Columns {
colName := rel.Columns[idx].Name
colFlags := rel.Columns[idx].Flags
var stringVal string
switch col.DataType {
case 'n': // null
stringVal = "NULL"
case 'u': // unchanged toast
case 't': // text
val, err := decodeTextColumnData(state.typeMap, col.Data, rel.Columns[idx].DataType)
if err != nil {
log.Fatalln("error decoding column data:", err)
}
stringVal, err = encodeColumnData(state.typeMap, val, rel.Columns[idx].DataType)
if err != nil {
return false, err
}
default:
log.Printf("unknown column data type: %c", col.DataType)
}
// TODO: quote column names?
if colFlags == 0 {
if updateStr.Len() > 0 {
updateStr.WriteString(", ")
}
updateStr.WriteString(fmt.Sprintf("%s = %v", colName, stringVal))
} else {
if whereStr.Len() > 0 {
updateStr.WriteString(", ")
}
whereStr.WriteString(fmt.Sprintf("%s = %v", colName, stringVal))
}
}
err = r.replicateQuery(state.replicaConn, fmt.Sprintf("UPDATE %s.%s SET %s%s", rel.Namespace, rel.RelationName, updateStr.String(), whereClause(whereStr)))
if err != nil {
return false, err
}
case *pglogrepl.DeleteMessageV2:
if !state.processMessages {
log.Printf("Received stale message, ignoring. Last written LSN: %s Message LSN: %s", state.lastWrittenLSN, xld.ServerWALEnd)
return false, nil
}
// TODO: this probably doesn't work for unkeyed tables
rel, ok := state.relations[logicalMsg.RelationID]
if !ok {
log.Fatalf("unknown relation ID %d", logicalMsg.RelationID)
}
whereStr := strings.Builder{}
for idx, col := range logicalMsg.OldTuple.Columns {
colName := rel.Columns[idx].Name
colFlags := rel.Columns[idx].Flags
var stringVal string
switch col.DataType {
case 'n': // null
stringVal = "NULL"
case 'u': // unchanged toast
case 't': // text
val, err := decodeTextColumnData(state.typeMap, col.Data, rel.Columns[idx].DataType)
if err != nil {
log.Fatalln("error decoding column data:", err)
}
stringVal, err = encodeColumnData(state.typeMap, val, rel.Columns[idx].DataType)
if err != nil {
return false, err
}
default:
log.Printf("unknown column data type: %c", col.DataType)
}
if colFlags == 0 {
// nothing to do
} else {
if whereStr.Len() > 0 {
whereStr.WriteString(", ")
}
whereStr.WriteString(fmt.Sprintf("%s = %v", colName, stringVal))
}
}
err = r.replicateQuery(state.replicaConn, fmt.Sprintf("DELETE FROM %s.%s WHERE %s", rel.Namespace, rel.RelationName, whereStr.String()))
if err != nil {
return false, err
}
case *pglogrepl.TruncateMessageV2:
log.Printf("truncate for xid %d\n", logicalMsg.Xid)
case *pglogrepl.TypeMessageV2:
log.Printf("typeMessage for xid %d\n", logicalMsg.Xid)
case *pglogrepl.OriginMessage:
log.Printf("originMessage for xid %s\n", logicalMsg.Name)
case *pglogrepl.LogicalDecodingMessageV2:
log.Printf("Logical decoding message: %q, %q, %d", logicalMsg.Prefix, logicalMsg.Content, logicalMsg.Xid)
case *pglogrepl.StreamStartMessageV2:
state.inStream = true
log.Printf("Stream start message: xid %d, first segment? %d", logicalMsg.Xid, logicalMsg.FirstSegment)
case *pglogrepl.StreamStopMessageV2:
state.inStream = false
log.Printf("Stream stop message")
case *pglogrepl.StreamCommitMessageV2:
log.Printf("Stream commit message: xid %d", logicalMsg.Xid)
case *pglogrepl.StreamAbortMessageV2:
log.Printf("Stream abort message: xid %d", logicalMsg.Xid)
default:
log.Printf("Unknown message type in pgoutput stream: %T", logicalMsg)
}
return false, nil
}
// readWALPosition reads the recorded WAL position from the WAL position file
func (r *LogicalReplicator) readWALPosition() (pglogrepl.LSN, error) {
walFileContents, err := os.ReadFile(r.walFilePath)
if err != nil {
// if the file doesn't exist, consider this a cold start and return 0
if os.IsNotExist(err) {
return pglogrepl.LSN(0), nil
}
return 0, err
}
return pglogrepl.ParseLSN(string(walFileContents))
}
// writeWALPosition writes the recorded WAL position to the WAL position file
func (r *LogicalReplicator) writeWALPosition(lsn pglogrepl.LSN) error {
return os.WriteFile(r.walFilePath, []byte(lsn.String()), 0644)
}
// whereClause returns a WHERE clause string with the contents of the builder if it's non-empty, or the empty
// string otherwise
func whereClause(str strings.Builder) string {
if str.Len() > 0 {
return " WHERE " + str.String()
}
return ""
}
// decodeTextColumnData decodes the given data using the given data type OID and returns the result as a golang value
func decodeTextColumnData(mi *pgtype.Map, data []byte, dataType uint32) (interface{}, error) {
switch oid.Oid(dataType) {
case oid.T_date, oid.T_time, oid.T_timestamp, oid.T_timestamptz, oid.T__date, oid.T__time, oid.T__timestamp, oid.T__timestamptz:
// The codec converts time values into a format that breaks our assumptions later on, which is unnecessary as
// the server sends the correctly-formatted time anyway.
return string(data), nil
default:
if dt, ok := mi.TypeForOID(dataType); ok {
return dt.Codec.DecodeValue(mi, dataType, pgtype.TextFormatCode, data)
}
return string(data), nil
}
}
// encodeColumnData encodes the given data using the given data type OID and returns the result as a string to be
// used in an INSERT or other DML query.
func encodeColumnData(mi *pgtype.Map, data interface{}, dataType uint32) (string, error) {
var value string
if dt, ok := mi.TypeForOID(dataType); ok {
e := dt.Codec.PlanEncode(mi, dataType, pgtype.TextFormatCode, data)
if e != nil {
encoded, err := e.Encode(data, nil)
if err != nil {
return "", err
}
value = string(encoded)
} else {
// no encoder for this type, use the string representation
value = fmt.Sprintf("%v", data)
}
} else {
value = fmt.Sprintf("%v", data)
}
// Some types need additional quoting after encoding
switch data := data.(type) {
case string, time.Time, pgtype.Time, bool:
return fmt.Sprintf("'%s'", value), nil
case [16]byte:
// TODO: should we actually register an encoder for this type?
uid := uuid.UUID(data)
return fmt.Sprintf("'%s'", uid.String()), nil
default:
return value, nil
}
}