Documentation
¶
Overview ¶
Package source.postgres implements the PostgreSQL replication source: a single logical-decoding reader (pgoutput) that decodes row changes into rowchange.Change, positions them at their commit LSN, and exposes the synced and confirmed positions for the DBLog watermark logic. The snapshot path reuses the source-agnostic DBLog orchestrator.
Index ¶
- func AdvanceSlot(ctx context.Context, db *sql.DB, slotName string, target position.LSN) error
- func CanonicalSchema(tbl *TableState) (core.Schema, error)
- func ConfirmedLSN(ctx context.Context, db *sql.DB, slotName string) (*position.LSN, error)
- func EnsureSetup(ctx context.Context, db *sql.DB, slotName string, tables []source.TableRef, ...) error
- func ValidateSlotState(ctx context.Context, db *sql.DB, slotName string, resume *position.LSN, ...) (*position.LSN, error)
- type Chunker
- type ChunkerOption
- type Column
- type Config
- type ConnConfig
- type Projection
- type Reader
- func (r *Reader) ClearWindow()
- func (r *Reader) Close()
- func (r *Reader) Master(ctx context.Context) (position.Position, error)
- func (r *Reader) OpenWindow(ctx context.Context, chunkID uint32)
- func (r *Reader) SetConfirmed(f func() position.Position)
- func (r *Reader) StartFromLSN(ctx context.Context, at *position.LSN) error
- func (r *Reader) Synced() position.Position
- type Source
- func (a Source) CloseQuery() error
- func (a Source) Discover(ctx context.Context, schemas []string) ([]source.TableRef, error)
- func (a Source) Incremental(ctx context.Context, t source.TableRef, cursor, after string) (string, []map[string]any, error)
- func (a Source) InitialPosition(ctx context.Context) (position.Position, error)
- func (a Source) Introspect(ctx context.Context, t spec.Table) (core.TableRef, core.Schema, []core.Warning, error)
- func (a Source) NewChunker(source, pk string, chunkSize int) (source.ChunkSource, error)
- func (a Source) Open(ctx context.Context, refs []source.TableRef) (source.Reader, error)
- func (a Source) ParsePosition(s string) (position.Position, error)
- type TableState
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AdvanceSlot ¶ added in v0.3.0
AdvanceSlot advances the logical slot's confirmed_flush_lsn to target, letting the server recycle WAL the sink has already committed (#156). It is idempotent and must run while the slot is inactive — before START_REPLICATION attaches the walsender.
func CanonicalSchema ¶
func CanonicalSchema(tbl *TableState) (core.Schema, error)
CanonicalSchema derives the canonical core.Schema from the introspected table. Mappable columns map to their canonical kind; unmappable ones are carried as KindUnknown so a declared cast can land them explicitly — never silent coercion. The source knows nothing about any sink.
func ConfirmedLSN ¶
ConfirmedLSN reads the slot's confirmed_flush_lsn: the stream start for a first boot (no committed resume position).
func EnsureSetup ¶
func EnsureSetup(ctx context.Context, db *sql.DB, slotName string, tables []source.TableRef, plugin string) error
EnsureSetup makes the server side ready, idempotently:
- REPLICA IDENTITY FULL on every replicated table, so updates and deletes carry the full old row (parity with MySQL's row_image=FULL);
- for pgoutput, the logical publication listing exactly the pipeline's tables;
- the logical decoding slot with the selected plugin.
The slot is the consistency anchor: created before the snapshot starts, it guarantees no transaction between slot creation and the stream start is ever lost.
func ValidateSlotState ¶ added in v0.3.0
func ValidateSlotState(ctx context.Context, db *sql.DB, slotName string, resume *position.LSN, logger *slog.Logger) (*position.LSN, error)
ValidateSlotState reconciles a stored resume LSN with the slot's confirmed_flush_lsn and returns the LSN the stream should start from (#156):
- resume == nil (first boot): start from the slot's confirmed point.
- resume == confirmed: no drift.
- resume < confirmed: the slot is ahead of the sink. The sink's stored point is behind what the server already confirmed (and may have recycled), so the slot's point is authoritative — warn and use it.
- resume > confirmed: the sink durably committed past the slot's last confirmed_flush_lsn (the server learns the applied position from a later standby-status update, so a crash can leave the slot behind). This is recoverable: the WAL from resume onward is still retained because the slot never confirmed past it, so advance the slot to the sink's point rather than forcing a re-snapshot.
Types ¶
type Chunker ¶
type Chunker struct {
// contains filtered or unexported fields
}
Chunker splits a table into snapshot chunks. The strategy is CTID by default; a configured chunk column switches to a key-based strategy (batch-size for integer/float, next-query otherwise). A partitioned table (workers > 1) always uses a key-based strategy, because the chunk range must stay routable to the same worker as the live stream.
func NewChunker ¶
func NewChunker(ctx context.Context, db *sql.DB, source, pk string, chunkSize int, opts ...ChunkerOption) (*Chunker, error)
NewChunker builds a chunker for one source table.
func (*Chunker) Bounds ¶
Bounds returns the ordered chunk boundary keys for the resolved strategy. The whole computation is retried on a transient error.
func (*Chunker) Partitions ¶ added in v0.3.0
Partitions divides this table's single-column primary key into n contiguous, ordered ranges for worker partitioning — the same ranges govern the DBLog snapshot and the live-stream routing, so a key can never switch partition ownership between the two phases (no hash is involved, so the two phases cannot disagree).
It always returns exactly n disjoint ranges (an empty or sparse table is padded with degenerate or type-domain ranges), and marks this chunker key-partitioned: CTID ranges are physical and cannot be clipped to a PK range, so a partitioned table must chunk by key.
func (*Chunker) Scan ¶
func (c *Chunker) Scan(ctx context.Context, ch source.Chunk, fn func(row map[string]any) error) error
Scan executes the chunk SELECT and calls fn for every row, keyed by column name. Values decode through the same scalar mapping the pgoutput reader uses, so snapshot rows and stream rows land in Iceberg identically.
The whole scan is retried on a transient error, covering a failure during rows.Next/Scan and not just the initial QueryContext. Rows are buffered until the scan completes, so a retry never re-delivers rows to fn.
Within one scan, rows are normalized by a bounded pool of `workers` goroutines, but fn is always invoked from a single goroutine, so it need not be thread-safe. Row order is not preserved (snapshot rows are keyed inserts, order-independent).
type ChunkerOption ¶ added in v0.3.0
type ChunkerOption func(*Chunker)
ChunkerOption tunes a Chunker.
func WithChunkColumn ¶ added in v0.3.0
func WithChunkColumn(col string) ChunkerOption
WithChunkColumn selects the chunk column (#151). Empty keeps the CTID default.
func WithColumns ¶ added in v0.3.0
func WithColumns(cols []string) ChunkerOption
WithColumns sets the column projection (#162). Empty means all columns.
func WithFilter ¶ added in v0.3.0
func WithFilter(f sq.Sqlizer) ChunkerOption
WithFilter sets the compiled structured filter (#163). Nil means none.
func WithRetries ¶ added in v0.3.0
func WithRetries(n int) ChunkerOption
WithRetries sets the transient-error retry budget for snapshot queries.
func WithTargetBytes ¶ added in v0.3.0
func WithTargetBytes(n int64) ChunkerOption
WithTargetBytes sets the CTID strategy's target bytes per chunk (from sink.defaults.targetFileSize). Non-positive keeps the default.
func WithWorkers ¶ added in v0.3.0
func WithWorkers(n int) ChunkerOption
WithWorkers sets the concurrent normalization pool size. n < 1 is clamped to 1 (sequential).
type Column ¶
type Column struct {
Name string
DataType string // pg_type native name ("int8", "text", "timestamptz", …)
NotNull bool // attnotnull: the column cannot hold NULL
}
Column is one introspected source column.
type Config ¶
type Config struct {
URI string
ConnCfg *ConnConfig // resolved from nested config (nil when using URI)
DB *sql.DB
SlotName string
Tables []source.TableRef
Logger *slog.Logger
// RetryCount is the transient-error reconnect budget for the
// replication stream (#166). 0 disables reconnect.
RetryCount int
// InitialWait bounds how long the reader waits for the first WAL message
// before failing with a non-retryable error (#154). Zero uses the
// default (300s).
InitialWait time.Duration
// Plugin is the logical decoding plugin: "pgoutput" (default) or
// "wal2json".
Plugin string
// Filters and Columns are the per-source read projection (#162/#163),
// keyed by "schema.table". The reader compiles the filter with the
// introspected column types, so numeric columns compare numerically.
Filters map[string]*spec.Filter
Columns map[string][]string
}
Config dials one PostgreSQL instance. One replication connection per source — the invariant the whole design stands on. DB is the query connection used for introspection and position reads.
type ConnConfig ¶ added in v0.3.0
type ConnConfig struct {
// QueryURI is the libpq DSN rendered from the config. It is what travels
// to a distributed worker (the worker only receives kind + dsn).
QueryURI string
// ConnConfig is the native pgx config for replication and query
// connections. It carries TLSConfig (built by pgx from the DSN) and
// DialFunc (SSH) so both connection paths share one transport.
ConnConfig *pgx.ConnConfig
// MaxOpenConns is the resolved maxThreads value.
MaxOpenConns int
// RetryCount is the resolved number of transient-connection retries.
RetryCount int
// InitialWaitTime is the resolved CDC initial-WAL-message wait.
InitialWaitTime time.Duration
// Plugin is the resolved logical decoding plugin ("pgoutput"|"wal2json").
Plugin string
// contains filtered or unexported fields
}
ConnConfig holds the resolved connection parameters, built from either a URI or the nested PostgresSource config.
func BuildConnConfig ¶ added in v0.3.0
func BuildConnConfig(uri string) (*ConnConfig, error)
BuildConnConfig constructs a ConnConfig from a URI string.
func BuildConnConfigFromPostgres ¶ added in v0.3.0
func BuildConnConfigFromPostgres(pg *spec.PostgresSource) (*ConnConfig, error)
BuildConnConfigFromPostgres constructs a ConnConfig from the nested PostgresSource config. When present, source.uri is ignored for connection building.
The config is created by pgx.ParseConfig on the rendered DSN — never by hand. pgx's ConnectConfig panics on a config it did not create, and it also needs RuntimeParams initialized (Reader.New writes "replication" into it). Parsing the DSN also gets pgx's native TLS handling for free (require, verify-ca and verify-full all follow libpq semantics, including loading the CA and client cert/key). Only the SSH DialFunc is layered on afterwards.
func (*ConnConfig) Close ¶ added in v0.3.0
func (c *ConnConfig) Close() error
Close tears down the SSH tunnel, if one was established. Safe to call multiple times.
type Projection ¶ added in v0.3.0
type Projection struct {
Columns []string
// contains filtered or unexported fields
}
Projection is a table's source-side read projection: the columns to emit and the compiled filter a row must satisfy. Both are applied on the decoded map, before the Arrow hot-path.
type Reader ¶
type Reader struct {
// contains filtered or unexported fields
}
Reader wraps one logical-decoding connection and decodes pgoutput row changes into rowchange.Change, positioned at their commit LSN.
func New ¶
New introspects the tables, performs the server-side setup (replica identity, publication, slot), and opens the replication connection — but does not start streaming.
func (*Reader) ClearWindow ¶
func (r *Reader) ClearWindow()
ClearWindow closes the DBLog window opened by OpenWindow.
func (*Reader) Close ¶
func (r *Reader) Close()
Close stops the reader and its replication connection. It cancels the replication loop first and waits for it to exit pgx, so the conn close never races an in-flight receive.
func (*Reader) Master ¶
Master reports the server's current WAL write position (the caught-up target for the DBLog window).
func (*Reader) OpenWindow ¶
OpenWindow opens the DBLog window for chunkID. The reader captures its source watermark — the server's current WAL position — and, from now on, tags decoded events whose commit LSN is strictly past that watermark InWindow for the chunk, until ClearWindow. Transactions at or before the watermark are already reflected in the chunk SELECT (or an earlier chunk) and must not be tagged. The tag is applied synchronously at decode — no event can escape the window by racing a channel pull. The watermark is an LSN (64-bit, monotonic), never the 32-bit transaction xid — the pgoutput Begin message only carries the raw xid, which wraps every ~4 billion transactions; the LSN does not.
func (*Reader) SetConfirmed ¶
SetConfirmed installs the confirmed-position callback.
func (*Reader) StartFromLSN ¶
StartFromLSN begins streaming from the given LSN, blocking until the stream ends or ctx is cancelled. Call in a goroutine. An LSN of 0/0 starts at the slot's confirmed point.
A transient stream failure (network blip, 08xxx/53xxx, admin shutdown) is retried: the replication connection is re-dialed and the stream resumes from the last position committed to the sink, up to r.retries times (#166). A permanent failure is returned immediately.
type Source ¶
type Source struct {
// contains filtered or unexported fields
}
Source adapts the Postgres pgoutput reader to the source contract. It is the self-contained driver entry point; init() registers the kind.
func (Source) CloseQuery ¶
CloseQuery releases the query connection and tears down the SSH tunnel.
func (Source) Discover ¶ added in v0.3.0
Discover implements source.Discoverer (#152): every table the connected user may SELECT, limited to schemas when non-empty. It returns base tables
- matviews ('m') and foreign tables ('f') are excluded because EnsureSetup runs REPLICA IDENTITY FULL and CREATE PUBLICATION on every discovered table, and both fail on those relkinds;
- leaf partitions are excluded (relispartition) because the parent already covers their rows — replicating parent and leaves doubles every row, and syncPublication's pg_publication_rel model expects the parent.
func (Source) Incremental ¶ added in v0.3.0
func (a Source) Incremental(ctx context.Context, t source.TableRef, cursor, after string) (string, []map[string]any, error)
Incremental implements source.IncrementalSource (#157): one cursor pass, SELECT <projection> FROM <table> WHERE [filter AND] <cursor> > $1 ORDER BY <cursor>. It returns the new cursor — the last row's cursor value, or "" when no rows — and the decoded rows. No slot, no publication.
func (Source) InitialPosition ¶
InitialPosition returns the slot's confirmed LSN: a first boot starts streaming from the slot's consistency point — everything after it flows through the stream, everything before it belongs to the snapshot.
func (Source) Introspect ¶
func (a Source) Introspect(ctx context.Context, t spec.Table) (core.TableRef, core.Schema, []core.Warning, error)
Introspect resolves one spec table into its ref and canonical schema.
func (Source) NewChunker ¶
NewChunker builds the chunk SELECT source for one table. The concurrent normalization pool (#161) and the snapshot-query retry budget (#166) come from the resolved connection config; the column projection (#162), the filter (#163), and the chunk column / CTID target bytes (#151) come from the spec.
type TableState ¶
TableState is the introspection result for one source table: the ordered column list and the primary key column indexes, so row decoding and Iceberg schema derivation share one path.
func QueryTable ¶
QueryTable introspects one table via pg_catalog, producing the column order the pgoutput relation messages follow.
func (*TableState) FindColumn ¶
func (t *TableState) FindColumn(name string) int
FindColumn returns the column index by name, or -1.