Documentation
¶
Overview ¶
Package postgres implements resumable full-snapshot reads from a PostgreSQL source.
At plan time (before the first row is read) the router asks one question of Postgres and combines the answer with a few facts from the run config:
pg_stats.correlation for the leading PK column ↓ (between -1 and 1; 1 = rows are physically in key order; 0 = random) Is this a CDC run with a logical slot available? → ModeSlot Is the table declared append-only? → ModeCtidAppendOnly (no xmin, just blocks) |correlation| >= 0.8? → ModeKeyset (index scan already near-sequential) PK is mutable (rows can change their key)? → ModeCtidXmin (physical blocks + repair pass) Otherwise (random keys, immutable PK) → ModeBitmap (bitmap heap scan, same correctness as keyset)
The chosen mode is frozen into the checkpoint at plan time and reused on every resume of the same run, so the shard layout never changes mid-flight.
What each mode actually does ¶
ModeKeyset — "follow the index in order" Issues ORDER BY pk LIMIT page queries. The PK index is walked in key order, one page at a time, and the last key seen becomes the cursor for the next page. Works great when the heap is already in the same order as the key (high correlation), because each page is one contiguous disk read. Resumable to the exact row.
ModeBitmap — "collect all the addresses first, then fetch blocks in order" Splits the key space into sub-ranges (same boundaries as keyset). Each sub-range is read with a plain range predicate and NO ORDER BY, so Postgres can collect all matching index TIDs, sort them by heap block, and read those blocks in ascending order with prefetch. This beats keyset for randomly-keyed tables because it avoids the random I/O pattern that ORDER BY forces. Resume is all-or-nothing per sub-range: when a sub-range drains fully the reader sends a Drained sentinel; the pipeline counts written rows and marks the shard Done. Interrupted sub-ranges are re-read whole on resume.
ModeCtidXmin — "read the heap directly, then patch up anything that moved" Divides the table into block-range shards (ctid = physical heap address). Reads blocks in order — fast sequential I/O, no index needed. The hard part is safety across a resume boundary, handled by three guards: (1) Rewrite guard: the filenode at run-start is stamped; if VACUUM FULL / CLUSTER / TRUNCATE rewrites the heap between runs, the cursor is stale → re-read from scratch. (2) Horizon-compare reconciliation: the run-start xmin horizon H1 is stamped; before the run closes, completed block ranges are re-scanned for rows with age(xmin) <= age(H1) (i.e. rows that were written or moved after run-start). The idempotent sink absorbs any over-delivery. (3) Freeze guard: if relfrozenxid advanced past H1 (vacuum froze xids), the age() comparison is unreliable → unfiltered rescan of completed ranges.
ModeCtidAppendOnly — like ModeCtidXmin but without the xmin apparatus; rows never move or update, so the horizon-compare pass is skipped and resume just continues past the recorded tail. (Not yet wired into the router; declared append-only sources route here explicitly.)
ModeSlot — a logical replication slot pins an LSN so updated/moved rows return via WAL replay. Correct for as-of-completion semantics and CDC. The CDC extractor uses this tier through a persistent pgoutput logical replication slot.
Decision table ¶
Condition Mode ───────────────────────────────────────────────────────────────── CDC run + logical slot available ModeSlot AppendOnly declared ModeCtidAppendOnly |corr| >= 0.8 (heap ≈ key order) ModeKeyset PK mutable ModeCtidXmin Default (random keys, immutable PK) ModeBitmap
Routing code path ¶
PlanResume (keyset.go)
└─ resolveMode (bitmap.go) ← reads s.readMode config + auto probe
└─ Route (readmode.go) ← queries pg_stats.correlation
└─ chooseMode (readmode.go) ← pure decision function, unit-tested
Package postgres implements the filament.Source interface as a full-snapshot (ModeFull) reader for PostgreSQL.
Each requested resource is a table name. A table's heap is sliced into one or more shards by physical ctid block range, and each shard is read window by window: WHERE ctid >= '(lo,0)' AND ctid < '(hi,0)' over a fixed span of blocks at a time. Reading by ctid (not the primary key) means one reader handles primary-key, composite-key, and keyless tables identically. On PostgreSQL 14+ each bounded window is a TID Range Scan: sequential heap I/O, no index needed. Both bounds matter — an open-ended ctid > x re-scans to end-of-table on every page, so every window is bounded on both sides.
Large tables split into several shards so a single big table no longer pins one worker while the rest sit idle (see planShards / shard_pages). When more than one shard runs concurrently the reads share an exported snapshot (pg_export_snapshot), so every shard sees one consistent point-in-time even under concurrent writes — the same mechanism pg_dump -j uses.
Rows are read as typed columns over pgx's binary protocol and appended straight into the pipeline's row writers — see columns.go — so neither the source database nor this process ever renders a row as text.
Index ¶
- Constants
- type LSNCodec
- type ReadMode
- type RouteSignals
- type Source
- func (s *Source) AcknowledgeChanges(ctx context.Context, checkpoints map[string]filament.Checkpoint) error
- func (s *Source) BindReplicationStream(config map[string]any, stream filament.ReplicationStream) (map[string]any, error)
- func (s *Source) CleanupReplicationStream(ctx context.Context, stream filament.ReplicationStream) error
- func (s *Source) Configure(ctx context.Context, cfg filament.Config) error
- func (s *Source) CursorColumns(ctx context.Context, table string) ([]filament.CursorColumn, error)
- func (s *Source) Discover(ctx context.Context, _ filament.DiscoverOpts) (filament.DiscoverResult, error)
- func (s *Source) Extract(ctx context.Context, sink arrowbatch.Inlet, opts filament.ExtractOpts) error
- func (s *Source) ExtractChanges(ctx context.Context, sink arrowbatch.Inlet, opts filament.ChangeExtractOpts) error
- func (s *Source) ExtractFrom(ctx context.Context, sink arrowbatch.Inlet, opts filament.ExtractOpts, ...) error
- func (*Source) Lookup(name string, version int) (filament.PositionCodec, error)
- func (s *Source) OpenStream(ctx context.Context, opts filament.StreamOpenOpts) (_ filament.StreamSession, result error)
- func (s *Source) PlanIncremental(ctx context.Context, resources []string, prev map[string]filament.Checkpoint, ...) (map[string]filament.Checkpoint, error)
- func (s *Source) PlanReplicationStream(request filament.ReplicationStreamPlanningRequest) (filament.ReplicationStreamPlan, error)
- func (s *Source) PlanResume(ctx context.Context, resources []string, prev map[string]filament.Checkpoint) (map[string]filament.Checkpoint, error)
- func (s *Source) Route(ctx context.Context, table string, sig RouteSignals) (ReadMode, float64, bool)
- func (s *Source) Schema(ctx context.Context, resource string) (rowmodel.Schema, error)
- func (s *Source) Spec() filament.ConnectorSpec
- func (s *Source) Teardown(context.Context) error
- func (s *Source) TestConnection(ctx context.Context, cfg filament.Config) error
- func (s *Source) Validate(cfg filament.Config) error
Constants ¶
const PositionCodec = "postgres.wal.lsn"
PositionCodec identifies PostgreSQL transaction-end WAL positions.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type LSNCodec ¶ added in v0.31.0
type LSNCodec struct{}
LSNCodec compares WAL addresses numerically and canonicalizes PostgreSQL hex notation.
func (LSNCodec) Canonicalize ¶ added in v0.31.0
Canonicalize returns the standard hexadecimal LSN representation.
type ReadMode ¶
type ReadMode int
ReadMode is the chosen extraction strategy for a resumable resource.
const ( // ModeKeyset reads in key order via the PK index — already sequential when the leading // key correlates with heap order, and resumable from a compact key cursor. ModeKeyset ReadMode = iota // ModeBitmap reads each key sub-range with no ORDER BY so the planner is free to pick a // bitmap heap scan (ascending block order + prefetch). Default for random immutable keys. ModeBitmap // ModeCtidAppendOnly reads physical block ranges on a declared append-only table — no // xmin apparatus, since rows never move; resume continues past the recorded tail. ModeCtidAppendOnly // ModeCtidXmin is the general physical tier: block-range cursor + horizon-compare repair. // Catches arbitrary churn including PK moves. ModeCtidXmin // ModeSlot is the premium physical tier: a logical replication slot pins an LSN and moved // rows return via WAL replay. ModeSlot )
type RouteSignals ¶
type RouteSignals struct {
AppendOnly bool // source declared the table append-only (rows never move/update)
PKMutable bool // the primary key can change → needs the physical tier for completeness
SlotAvailable bool // a logical replication slot is usable (wal_level=logical + privilege)
CDC bool // this run feeds change-data-capture (slot pins the snapshot boundary)
}
RouteSignals are the inputs the correlation probe cannot derive from catalog stats: they come from the run config, the source declaration, or the environment.
type Source ¶
type Source struct {
// contains filtered or unexported fields
}
Source reads tables from a PostgreSQL database as a full snapshot. One instance is created per run: Configure opens the pool, Extract pages the tables, Teardown closes the pool.
func (*Source) AcknowledgeChanges ¶
func (s *Source) AcknowledgeChanges(ctx context.Context, checkpoints map[string]filament.Checkpoint) error
AcknowledgeChanges advances the logical slot to the oldest checkpoint the runner has verified durable after sink commit. ExtractChanges cannot do this itself: at extraction return time the sink and tracker may still fail. The standby replies sent while streaming only confirm a flush position; advancing decodes the retained WAL so restart_lsn moves and the server can recycle it.
func (*Source) BindReplicationStream ¶ added in v0.0.27
func (s *Source) BindReplicationStream(config map[string]any, stream filament.ReplicationStream) (map[string]any, error)
BindReplicationStream injects the resolved generation's slot into a run without mutating the stored connection or pipeline configuration.
func (*Source) CleanupReplicationStream ¶ added in v0.0.27
func (s *Source) CleanupReplicationStream(ctx context.Context, stream filament.ReplicationStream) error
CleanupReplicationStream drops a retired logical slot after its successor has committed. Missing slots are already clean and therefore succeed.
func (*Source) Configure ¶
Configure reads dsn/schema/page_size/shard_pages/max_conns and opens a connection pool. Set max_conns to at least the run's parallelism + 1 (the snapshot coordinator holds one connection) so concurrent shard reads each get a connection instead of queueing; unset keeps pgx's default (max(4, NumCPU)).
func (*Source) CursorColumns ¶
CursorColumns reports every table column and ranks supported conventional watermark names.
func (*Source) Discover ¶
func (s *Source) Discover(ctx context.Context, _ filament.DiscoverOpts) (filament.DiscoverResult, error)
Discover lists tables in the configured schema with their primary keys and row estimates.
func (*Source) Extract ¶
func (s *Source) Extract(ctx context.Context, sink arrowbatch.Inlet, opts filament.ExtractOpts) error
Extract plans every requested table into shards and pages them out through the sink. With Parallelism > 1 the shards are read concurrently (bounded by Parallelism) into the shared sink under one exported snapshot; paging within a shard stays sequential. The first shard error cancels the rest and is returned.
func (*Source) ExtractChanges ¶
func (s *Source) ExtractChanges(ctx context.Context, sink arrowbatch.Inlet, opts filament.ChangeExtractOpts) error
ExtractChanges replays pgoutput row changes for the selected tables. The publication is created/expanded by default; set manage_publication=false when database administration owns it instead. A slot is never dropped automatically: its stable identity is what makes a later catch-up lossless.
func (*Source) ExtractFrom ¶
func (s *Source) ExtractFrom(ctx context.Context, sink arrowbatch.Inlet, opts filament.ExtractOpts, prev map[string]filament.Checkpoint) error
ExtractFrom reads each resource from its checkpoint; the checkpoint plan's mode decides how. Incremental plans read by cursor, everything else reads via keyset shards (concurrently, under one exported snapshot, like Extract); a resource with no keyset plan (no primary key, or a checkpoint-free full read) falls back to the ctid reader and is read whole.
func (*Source) Lookup ¶ added in v0.31.0
Lookup resolves the PostgreSQL WAL codec without opening a connection.
func (*Source) OpenStream ¶ added in v0.31.0
func (s *Source) OpenStream(ctx context.Context, opts filament.StreamOpenOpts) (_ filament.StreamSession, result error)
OpenStream opens one persistent pgoutput session. An uncertified initial snapshot completes before the session returns its first candidate coverage.
func (*Source) PlanIncremental ¶
func (s *Source) PlanIncremental(ctx context.Context, resources []string, prev map[string]filament.Checkpoint, cursors map[string]filament.ResourceCursorConfig) (map[string]filament.Checkpoint, error)
PlanIncremental builds a single compound watermark per table. The source cursor is followed by the complete primary key so equal cursor values are deterministic and cannot skip rows at a batch boundary.
func (*Source) PlanReplicationStream ¶ added in v0.0.27
func (s *Source) PlanReplicationStream(request filament.ReplicationStreamPlanningRequest) (filament.ReplicationStreamPlan, error)
PlanReplicationStream gives one pipeline route its own logical replication slot while keeping the publication shared at the connection level.
func (*Source) PlanResume ¶
func (s *Source) PlanResume(ctx context.Context, resources []string, prev map[string]filament.Checkpoint) (map[string]filament.Checkpoint, error)
PlanResume builds the per-resource keyset plan: the shard layout plus any cursor carried over from prev. A resource without a primary key maps to nil (non-resumable; read whole via ctid). The engine persists this plan so a later resume reuses the same stable shard boundaries.
func (*Source) Route ¶
func (s *Source) Route(ctx context.Context, table string, sig RouteSignals) (ReadMode, float64, bool)
Route runs the correlation probe for a table's leading key and returns the chosen mode along with the observed correlation (for an observability fact). It never fails the run: a probe error degrades to "no correlation" (random) and routes on the signals alone.
func (*Source) Schema ¶
Schema returns the column schema of one table: every live column in attribute order with its Postgres type (format_type, kept verbatim in Native for a same-engine round-trip and classified into a LogicalType), plus the primary key. It implements filament.SchemaProvider so a Schematized sink can build matching typed tables.
func (*Source) Spec ¶
func (s *Source) Spec() filament.ConnectorSpec
Spec describes the source's config fields, modes, and write policies.
func (*Source) TestConnection ¶
TestConnection opens a short-lived pool and pings the database.