Documentation
¶
Index ¶
- Constants
- Variables
- func BuildBasePath(prefix string, schemaID int16, minRowID, maxRowID string) string
- func BuildBaseTempPath(prefix string, schemaID int16, fileUUID string) string
- func BuildDeltaPath(prefix string, schemaID int16, fileUUID string) string
- func BuildMergedBasePath(prefix string, schemaID int16, fileUUID string) string
- func BuildPGDSN(p PGDSNParams) string
- func BuildTempPath(prefix string, schemaID int16, fileUUID string) string
- func CopyTmpToFinal(ctx context.Context, client S3ObjectClient, bucket, tmpKey, finalKey string, ...) error
- func DeleteObjectKey(ctx context.Context, client S3ObjectClient, bucket, key string) error
- func GetChangeLogStats(ctx context.Context, db *sql.DB, table string, schemaID int16) (int64, int64, error)
- func HeadObjectSize(ctx context.Context, client S3ObjectClient, bucket, key string) (int64, error)
- func MarkFlushedVersions(ctx context.Context, db *sql.DB, table string, schemaID int16, ...) ([]uuid.UUID, error)
- func ObjectSHA256(ctx context.Context, client S3GetClient, bucket, key string) (string, error)
- func ResolveStaticS3Credentials(cfg CDCConfig) (accessKeyID, secretAccessKey, sessionToken string)
- func RunOnce(ctx context.Context, cfg CDCConfig, s3Client S3ObjectClient, dryRun bool, ...) error
- func SelectBatchRowIDs(ctx context.Context, db *sql.DB, table string, schemaID int16, batchSize int) ([]uuid.UUID, map[uuid.UUID]int64, int64, error)
- func TrySchemaLock(ctx context.Context, db *sql.DB, schemaID int16) (bool, func(), error)
- type CDCConfig
- type CompactionConfig
- type DuckExporter
- type InitOptions
- type InitSummary
- type Manifest
- type ManifestConfig
- type ManifestEntry
- type PGDSNParams
- type Runner
- type S3FullClient
- type S3GetClient
- type S3ObjectClient
Constants ¶
const ( DefaultParquetCompression = "zstd" DefaultParquetCompressionLevel = 3 DefaultMinRecords = 20000 DefaultMaxAgeMs = 3600000 // 1 hour DefaultEstimatedRowBytes = 1024 DefaultMaxBatchBytes = int64(50 * 1024 * 1024) DefaultBatchSize = 10000 DefaultTargetBaseSizeMB = 256 DefaultTargetFileSizeMB = 256 DefaultMaxBatchSize = 10000000 // 10M rows max DefaultMaxDeltaSizeMB = 50 DefaultDirtyRatioPct = 5 DefaultMaxRetries = 5 DefaultBaseBackoffMs = 100 DefaultMaxBackoffMs = 10000 DefaultPGSSLMode = "require" )
Default constants
const ChecksumSHA256Prefix = "sha256:"
ChecksumSHA256Prefix tags manifest FileEntry.Checksum values produced by ObjectSHA256. Empty Checksum means the entry predates stamping (#347); field presence is the format version signal, matching the Columns rule.
Variables ¶
ErrSchemaAttrCacheUnavailable marks a CDC export that cannot proceed because a schema's attribute metadata cache could not be resolved — the registry lookup failed or the cache is empty. CDC export needs it to produce parquet the federated reader can consume; without it the reader fails fast with ErrSchemaMetadataCacheRequired (#193). This is an operator-visible configuration error, not write-path validation, so it is NOT wrapped in forma.ErrInvalidInput.
var ErrSchemaLockContended = errors.New("schema advisory lock contended")
ErrSchemaLockContended reports that another holder (flusher, cdc-init, or manifest-reconcile) owns the per-schema advisory lock.
Functions ¶
func BuildBasePath ¶
BuildBasePath returns the canonical base file path for a schema. Base files use min/max row_id naming to indicate the row range covered.
func BuildBaseTempPath ¶
BuildBaseTempPath returns the temp path for a base file during init.
func BuildDeltaPath ¶
BuildDeltaPath returns the canonical delta file path for a schema.
func BuildMergedBasePath ¶ added in v0.2.0
BuildMergedBasePath returns the path of a compaction-rewritten base file. Merged bases are UUID-named, never {min}_{max}: repeated rewrites over the same row range would otherwise reuse a key and overwrite an object still listed in the manifest under concurrent readers (#188). The row-ID range lives in the manifest FileEntry instead.
func BuildPGDSN ¶ added in v0.2.0
func BuildPGDSN(p PGDSNParams) string
BuildPGDSN renders a libpq keyword/value DSN with every string value quoted (single-quote wrapped, backslash and single-quote escaped) so passwords with spaces or quotes survive parsing by pgx and DuckDB's postgres scanner.
The implementation moved to internal/pgdsn so internal/federated can quote the postgres_scan DSN the same way (#301). Behaviour is unchanged; the tests in pgdsn_test.go and redact_test.go remain the contract.
func BuildTempPath ¶
BuildTempPath returns the temp path for a delta file.
func CopyTmpToFinal ¶
func CopyTmpToFinal(ctx context.Context, client S3ObjectClient, bucket, tmpKey, finalKey string, logger *zap.Logger) error
CopyTmpToFinal copies a parquet file from tmp key to final key and deletes tmp.
func DeleteObjectKey ¶ added in v0.2.0
func DeleteObjectKey(ctx context.Context, client S3ObjectClient, bucket, key string) error
DeleteObjectKey deletes one S3 object.
func GetChangeLogStats ¶
func GetChangeLogStats(ctx context.Context, db *sql.DB, table string, schemaID int16) (int64, int64, error)
GetChangeLogStats returns count and oldest changed_at for unflushed rows.
func HeadObjectSize ¶ added in v0.2.0
HeadObjectSize returns the byte size of an S3 object. Callers use it to populate manifest FileEntry.SizeBytes after a tmp->final copy; the size feeds compaction's promotion heuristic only, so callers should treat a failure as best-effort (log and keep 0) rather than failing the pipeline.
func MarkFlushedVersions ¶ added in v0.2.0
func MarkFlushedVersions(ctx context.Context, db *sql.DB, table string, schemaID int16, rowIDs []uuid.UUID, versions map[uuid.UUID]int64, flushedAt int64) ([]uuid.UUID, error)
MarkFlushedVersions updates flushed_at for the given rows only where the slot-0 changed_at still EQUALS the version LISTED for that row at batch selection. Exact equality is what keeps export and mark consistent without a global wall-clock cutoff: any slot whose version differs from its listing was concurrently rewritten — advanced by an update/delete, or replaced by a delete→recreate — and its exported copy (if any) no longer matches slot-0, so the row must stay dirty for the next run. (`<=` is NOT safe here: a recreate overwrites the slot, and before #274's create ordering it could even land BELOW a clock-ahead tombstone's listing — a `<=` mark would clear the dirty barrier for a payload no parquet holds.) A clock-ahead listed version matches its own listing and marks normally (review round 2 P1). Returns the row_ids actually marked flushed.
func ObjectSHA256 ¶ added in v0.2.0
ObjectSHA256 streams an object and returns "sha256:<hex>" over its bytes. The hash is of what the store returns, not what the writer sent: it blesses the published state, so it detects later mutation, not upload mangling.
func ResolveStaticS3Credentials ¶ added in v0.2.0
ResolveStaticS3Credentials resolves the static S3 credential triple shared by every CDC credential site — the SDK S3 client and the DuckDB httpfs plumbing (#326). Explicit config wins as-is. Otherwise the environment pair applies only when BOTH halves are non-empty: a lone AWS_ACCESS_KEY_ID would build an empty-secret static provider whose only observable behavior is an opaque signing failure (#302). With no fully-set pair it returns empty strings — SDK callers then leave the default chain in place, and DuckDB inherits its own environment chain.
The session token rides the source that supplied the pair and never crosses sources (#329): a config pair carries only the config token, an environment pair carries only AWS_SESSION_TOKEN. Pairing a long-lived key with a foreign temporary token yields a combination the storage endpoint can only report as an opaque signing failure. A token with no accompanying pair is not a credential source at all and is discarded with the rest.
func RunOnce ¶
func RunOnce(ctx context.Context, cfg CDCConfig, s3Client S3ObjectClient, dryRun bool, logger *zap.Logger, schemaRegistry forma.SchemaRegistry) error
RunOnce performs one full pass over schemas and attempts flush where needed. Caller may provide an S3ObjectClient; when nil, AWS config will be loaded from environment (still respecting cfg.S3Region). Optional schemaRegistry enables schema-aware projections in DuckDB export.
func SelectBatchRowIDs ¶
func SelectBatchRowIDs(ctx context.Context, db *sql.DB, table string, schemaID int16, batchSize int) ([]uuid.UUID, map[uuid.UUID]int64, int64, error)
SelectBatchRowIDs picks up to batchSize row_ids for flushing, recording each listed row's changed_at version, and returns a snapshot cutoff (ms) for exporting. The snapshot is the MAX of the wall clock and the listed versions: per-row versions are strictly monotonic and may run ahead of the wall clock (#274 GREATEST ordering), so a wall-clock-only cutoff would exclude a clock-ahead row from both export and mark — shipping an empty delta and leaving the row dirty until the clock caught up, indefinitely under sustained lead (review round 2 P1). The versions feed MarkFlushedVersions, which marks exactly what was listed.
func TrySchemaLock ¶ added in v0.2.0
TrySchemaLock pins one physical connection and takes pg_try_advisory_lock(schemaID, schemaID). The lock is session-scoped: on a pool, acquire and release could land on different connections, and a lock released on the wrong session silently fails — so unlock runs on the same pinned conn and closes it to end the session. unlock is non-nil iff locked; it uses a background context so the lock is released even after ctx cancel.
Types ¶
type CDCConfig ¶
type CDCConfig struct {
// Table names
ChangeLogTable string
EntityMainTable string
EAVDataTable string
// Thresholds
MinRecords int // flush when unflushed rows >= MinRecords
MaxAgeMs int64 // flush when oldest unflushed row age >= MaxAgeMs
BatchSize int // maximum rows per snapshot
// Postgres connection
PGHost string
PGPort int
PGUser string
PGPassword string
PGDB string
PGUseIAM bool
PGSSLMode string
// DuckDB export options
DuckDBPath string // optional on-disk duckdb; empty for :memory:
DuckThreads int // PRAGMA threads
DuckMemLimit string // e.g. "4GB"
QueryTimeout time.Duration // timeout for duckdb export
ParquetCompression string // e.g. "zstd"
ParquetCompressionLevel int // codec level if supported
EstimatedRowBytes int // rough row size estimate for batch sizing
MaxBatchBytes int64 // optional cap to limit batch size by bytes
// Init-specific options (cdc-init base file export)
TargetFileSizeMB int // target parquet file size in MB (0 = use BatchSize)
MaxBatchSize int // maximum rows per batch to cap memory usage
// S3
S3Bucket string
S3Prefix string // prefix inside bucket for delta files
S3Endpoint string
S3Region string
S3UseSSL bool
S3UsePath bool // path style addressing
S3AccessKeyID string // AWS access key ID; overrides environment variable AWS_ACCESS_KEY_ID
S3SecretAccessKey string // AWS secret access key; overrides environment variable AWS_SECRET_ACCESS_KEY
S3SessionToken string // AWS session token for temporary credentials (STS/roles); effective only with same-source S3AccessKeyID/S3SecretAccessKey, see ResolveStaticS3Credentials (#329)
// Manifest (optional - when set, flush updates manifest after export)
ManifestPrefix string // root prefix for manifests in S3
ManifestTemplate string // path template, e.g. "manifest/{{.SchemaID}}.json"
// BeforeExportHook, when non-nil, runs per batch after dirty-ID selection
// (the snapshot is already captured) and before the DuckDB export. Test
// seam for driving mutations inside the selection->export race window
// (#182); always nil in production. A hook error aborts the batch before
// any side effect.
BeforeExportHook func(ctx context.Context, schemaID int16, batchIDs []uuid.UUID, snapshot int64) error
}
CDCConfig controls change_log flushing and export behavior. S3 client is injected separately via RunOnce parameter to allow callers to provide either AWS or MinIO implementations.
func (CDCConfig) WithDefaults ¶
WithDefaults returns a copy of CDCConfig with missing fields set to defaults.
type CompactionConfig ¶
type CompactionConfig struct {
SchemaID int16 // optional filter; 0 means all
ManifestPath string // s3 path or fs path to manifest JSON for the schema
TargetBaseSizeMB int // default 256
// TargetBaseSizeBytes is the byte-precise promotion threshold. Zero
// derives it from TargetBaseSizeMB; the compactor compares delta-tier
// bytes against it directly, so sub-MB tiers are not truncated to 0 MB.
TargetBaseSizeBytes int64
MaxDeltaSizeMB int // default 50
DirtyRatioPct int // default 5 (rewrite when updated rows/base rows > 5%)
RunInterval time.Duration // scheduler interval when used in a loop
TempPrefix string // temp path prefix for rewrites
MaxParallelFiles int // optional parallelism for rewrites
// SkipInputChecksumVerify opts out of the pre-merge verification of
// rewrite inputs (#347). The zero value verifies: a rewrite merges its
// sources and then deletes them, so the gate is the last moment silent
// corruption is both detectable and attributable to a named object, and
// that has to hold for deployments that never set this struct field.
// Setting it true trades that detection away to save one GET per stamped
// source.
SkipInputChecksumVerify bool
// Backoff parameters for S3/manifest operations
MaxRetries int // default 5
BaseBackoff time.Duration // default 100ms
MaxBackoff time.Duration // default 10s
}
CompactionConfig controls Base/Delta maintenance.
func (CompactionConfig) WithDefaults ¶
func (c CompactionConfig) WithDefaults() CompactionConfig
WithDefaults returns a copy of CompactionConfig with missing fields set to defaults.
type DuckExporter ¶
DuckExporter handles DuckDB interactions for exporting snapshots to S3 temp path.
func NewDuckExporter ¶
func NewDuckExporter(ctx context.Context, cfg CDCConfig, s3AccessKey, s3Secret, s3SessionToken string, logger *zap.Logger) (*DuckExporter, error)
NewDuckExporter opens a DuckDB pool whose every physical connection self-configures (pragmas, extensions, S3 session settings) via a connector init hook — session-scoped statements issued through the pool reach only one arbitrary connection (#285, same class as #245). Init statement failures are logged and skipped, never blocking the connection; construction fails only on credential validation or ping.
The caller passes the whole key/secret/token triple because a session token is only valid with the pair it was issued alongside; resolving it here would let a token from one source pair with a key from another (#329, see ResolveStaticS3Credentials).
func (*DuckExporter) ExportBaseFileToTmp ¶
func (e *DuckExporter) ExportBaseFileToTmp(ctx context.Context, cfg CDCConfig, pgConnStr string, s3TmpPath string, schemaID int16, rowIDs []uuid.UUID, attrCache forma.SchemaAttributeCache) error
ExportBaseFileToTmp exports existing entity_main + eav_data rows directly to a base parquet file. Unlike delta export, this does NOT use change_log - it reads directly from entity_main. s3TmpPath is the destination like 's3://bucket/base/<schema_id>/_tmp/<tmp_uuid>.parquet'
func (*DuckExporter) ExportSnapshotToTmp ¶
func (e *DuckExporter) ExportSnapshotToTmp(ctx context.Context, cfg CDCConfig, pgConnStr string, s3TmpPath string, schemaID int16, snapshotTS int64, rowIDs []uuid.UUID, attrCache forma.SchemaAttributeCache) error
ExportSnapshotToTmp builds an export SQL and runs COPY to the provided s3TmpPath. s3TmpPath is the destination like 's3://bucket/prefix/<schema_id>/_tmp/<tmp_uuid>.parquet'
type InitOptions ¶ added in v0.2.0
type InitOptions struct {
Config CDCConfig
S3Client *s3.Client
SchemaRegistryTable string
SchemaIDFilter int
DryRun bool
AutoEstimateRowBytes bool
Logger *zap.Logger
SchemaRegistry forma.SchemaRegistry
}
InitOptions configures a base-file initialization export run. It mirrors the cdc-init CLI options; the CLI in cmd/tools delegates here.
type InitSummary ¶ added in v0.2.0
InitSummary reports totals across all schemas processed by RunInit.
func RunInit ¶ added in v0.2.0
func RunInit(ctx context.Context, opts InitOptions) (InitSummary, error)
RunInit performs the initialization export for all or a specific schema: existing non-deleted entity_main rows are batched into base parquet files on S3 and (when a manifest template is configured) recorded in the schema manifest. Extracted from cmd/tools/cdc_init.go (mechanical move, #173).
type Manifest ¶
type Manifest struct {
SchemaID int16 `json:"schema_id"`
Base []ManifestEntry `json:"base,omitempty"`
Delta []ManifestEntry `json:"delta,omitempty"`
}
Manifest captures the files for a schema and their stats.
type ManifestConfig ¶
type ManifestConfig struct {
Bucket string
Prefix string // root prefix for manifests (e.g., manifest/<project>/)
PathTemplate string // optional template per schema, e.g., "manifest/{{.SchemaID}}.json"
}
ManifestConfig describes where manifests are stored and how to resolve parquet paths.
type ManifestEntry ¶
type ManifestEntry struct {
Path string `json:"path"`
MinRowID string `json:"min_row_id,omitempty"`
MaxRowID string `json:"max_row_id,omitempty"`
MinTimeSlot int64 `json:"min_time_slot,omitempty"`
MaxTimeSlot int64 `json:"max_time_slot,omitempty"`
SizeBytes int64 `json:"size_bytes,omitempty"`
Level string `json:"level,omitempty"` // "base" or "delta"
}
ManifestEntry describes a parquet file tracked for a schema. It can represent either a base or delta file.
type PGDSNParams ¶ added in v0.2.0
PGDSNParams are the inputs for a libpq keyword/value connection string.
type Runner ¶ added in v0.0.27
type Runner struct {
// contains filtered or unexported fields
}
Runner caches per-config S3 runtimes and DuckDB exporters across RunOnce calls. Each cache holds one entry per non-credential config group; a credential rotation (#329) replaces the group's entry, and a superseded DuckDB exporter is closed once no in-flight RunOnce holds it (#331) — so a long-lived Runner under STS rotation keeps exactly one exporter per group, not one per token. Two configs sharing a group but alternating *different* static credential triples therefore rebuild on every alternation: a rebuild cost, accepted in #331, never a correctness issue.
type S3FullClient ¶
type S3FullClient interface {
S3ObjectClient
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
}
S3FullClient extends S3ObjectClient with GetObject and PutObject for manifest operations.
type S3GetClient ¶ added in v0.2.0
type S3GetClient interface {
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
}
S3GetClient is the minimal byte-read surface content hashing needs.
type S3ObjectClient ¶
type S3ObjectClient interface {
CopyObject(ctx context.Context, params *s3.CopyObjectInput, optFns ...func(*s3.Options)) (*s3.CopyObjectOutput, error)
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
}
S3ObjectClient is a minimal interface for copy + delete + stat used by the CDC flusher.