compaction

package
v0.7.0-rc0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package compaction provides bin-pack compaction planning for Iceberg tables.

Index

Constants

View Source
const (
	// DefaultMinInputFiles is the default minimum number of files per group.
	DefaultMinInputFiles uint = 5

	// DefaultPackingLookback is the default packing lookback.
	DefaultPackingLookback uint = 128

	// DefaultDeleteRatioThreshold mirrors Java's delete-ratio default: a file
	// whose file-scoped deletes (deletion vectors and path-scoped positional
	// deletes) shadow at least 30% of its rows is rewritten.
	DefaultDeleteRatioThreshold float64 = 0.3
)

Variables

This section is empty.

Functions

func CollectDeadEqualityDeletes

func CollectDeadEqualityDeletes(
	ctx context.Context,
	fs iceio.IO,
	snap *table.Snapshot,
	rewrittenPaths map[string]struct{},
) ([]iceberg.DataFile, error)

CollectDeadEqualityDeletes walks the given snapshot's manifests and returns equality delete files that are provably dead after a rewrite removes the data files in rewrittenPaths. It preserves the original conservative API, which treats every candidate as potentially global because it has no spec metadata. New callers should use CollectDeadEqualityDeletesWithSpecs.

"Dead" means: no surviving data file in the snapshot (excluding rewrittenPaths) could apply to the eq-delete under the conservative compatibility predicate (see DecideDeadEqualityDeletes).

rewrittenPaths is the union of every old data file path being replaced by a planned rewrite (across all groups). The caller typically computes this from a compaction plan.

The returned files are safe to remove during the same commit that stages the rewrite — they cannot be resurrected by any concurrent commit because (1) any concurrent eq-delete is rejected by the rewrite-validator, and (2) any concurrent data file gets a fresh sequence number strictly greater than every preexisting eq-delete, so it cannot satisfy E.seq > D.seq for any preexisting E.

Two-pass design: the first pass walks delete manifests only to discover candidate eq-deletes; if there are none, the more expensive data-manifest walk is skipped.

func CollectDeadEqualityDeletesWithSpecs added in v0.7.0

func CollectDeadEqualityDeletesWithSpecs(
	ctx context.Context,
	fs iceio.IO,
	specs PartitionSpecLookup,
	snap *table.Snapshot,
	rewrittenPaths map[string]struct{},
) ([]iceberg.DataFile, error)

CollectDeadEqualityDeletesWithSpecs walks the given snapshot's manifests and uses partition spec metadata to identify dead equality deletes exactly.

func CollectDeadPositionDeletes added in v0.7.0

func CollectDeadPositionDeletes(
	ctx context.Context,
	fs iceio.IO,
	snap *table.Snapshot,
	rewrittenPaths map[string]struct{},
) ([]iceberg.DataFile, error)

CollectDeadPositionDeletes walks the given snapshot's manifests and returns the position-delete files made dead by a rewrite that removes rewrittenPaths. A dead position delete references only data files the rewrite is removing, so after the rewrite it applies to nothing and is safe to expunge in the same commit.

Scope resolution mirrors Java ContentFileUtil.referencedDataFile and DeleteFileIndex:

  • File-scoped — explicit referenced_data_file, or equal file_path lower/upper bounds — is dead iff its single target is in rewrittenPaths.
  • Partition-scoped — no single target — is dead iff no surviving (non-rewritten) data file in the same (specID, partition) has a sequence number <= the delete's. Such a survivor is one the delete still applies to, so the delete must be retained for a later dangling-delete pass.

Deletion vectors (Puffin position deletes) are intentionally excluded: they are 1:1 with their data file and expunged per-group via table.CompactionGroupResult.SafeDeletionVectors.

rewrittenPaths is the union of every old data file path being replaced across all rewrite groups. This is the whole-rewrite re-check that table.CollectSafePositionDeletes's caller contract requires whenever multiple groups land in one rewrite snapshot; the result feeds table.RewriteDataFilesOptions.ExtraDeleteFilesToRemove. Like CollectDeadEqualityDeletesWithSpecs, the returned files are safe to remove in the same commit that stages the rewrite: a concurrent commit cannot resurrect them, because any concurrent delete touching a rewritten file is rejected by the rewrite validator and any concurrent data file gets a sequence number strictly greater than every preexisting delete.

Two-pass design: the first pass walks delete manifests to resolve candidates, deciding file-scoped ones immediately; the more expensive data-manifest walk runs only when a partition-scoped candidate needs a survivor check.

func DecideDeadEqualityDeletes

func DecideDeadEqualityDeletes(survey *SurvivorSurvey, candidates []iceberg.ManifestEntry) []iceberg.DataFile

DecideDeadEqualityDeletes conservatively returns equality-delete files that are dead without partition spec metadata. Because a non-empty partition tuple may belong to a void-only spec whose deletes apply globally, this compatibility API assumes every candidate could be global. It may preserve deletes that exact spec-aware matching can prove dead. New callers with table metadata should use DecideDeadEqualityDeletesWithSpecs.

func DecideDeadEqualityDeletesWithSpecs added in v0.7.0

func DecideDeadEqualityDeletesWithSpecs(
	survey *SurvivorSurvey,
	candidates []iceberg.ManifestEntry,
	specs PartitionSpecLookup,
) ([]iceberg.DataFile, error)

DecideDeadEqualityDeletesWithSpecs is the exact spec-aware predicate for equality-delete cleanup during compaction. Given a survey of surviving data file sequences populated with SurvivorSurvey.AddSurvivorWithSpec, it returns the equality-delete files that no surviving data file could apply to.

The cleanup rule is:

E applies to D iff E.seq > D.seq AND (
    E.spec.isUnpartitioned() ||
    (E.specID == D.specID && E.partition == D.partition)
)

E is dead iff no applicable surviving D has D.seq < E.seq — equivalently, the applicable min-seq is >= E.seq.

Defensive: candidates with sequence number < 0 (sentinel for unset) are skipped — preserved rather than risk dropping an unidentifiable file.

Dedup by file path: the same eq-delete file may appear in multiple manifest entries after manifest merging.

Types

type Config

type Config struct {
	// TargetFileSizeBytes is the desired output file size.
	// Default: table.WriteTargetFileSizeBytesDefault (512 MB).
	TargetFileSizeBytes int64

	// MinFileSizeBytes is the lower bound for a file to be considered "optimal".
	// Files smaller than this are candidates for compaction.
	// Default: 75% of TargetFileSizeBytes.
	MinFileSizeBytes int64

	// MaxFileSizeBytes is the upper bound. Files larger than this are never rewritten
	// (unless they exceed DeleteFileThreshold).
	// Default: 180% of TargetFileSizeBytes.
	MaxFileSizeBytes int64

	// MinInputFiles is the minimum number of files in a group to justify rewriting.
	// Groups with fewer files are dropped from the plan.
	// Default: DefaultMinInputFiles.
	MinInputFiles uint

	// DeleteFileThreshold is the minimum number of delete files associated with
	// a data file to force it into compaction regardless of file size.
	// Default: 5.
	DeleteFileThreshold int

	// DeleteRatioThreshold forces a data file into compaction when the fraction
	// of its rows shadowed by file-scoped deletes (deletion vectors and
	// path-scoped positional deletes) reaches this value, regardless of file
	// size. File-scoped deletes apply to exactly one data file and carry an
	// exact deleted-row count, so size-based selection alone never reclaims a
	// right-sized file's dead space; this rule does. Range [0,1]; 0 disables
	// it. Default: DefaultDeleteRatioThreshold.
	DeleteRatioThreshold float64

	// PackingLookback controls how many open bins the packer considers
	// before evicting. Higher values produce better packing at the cost
	// of memory. Default: DefaultPackingLookback.
	PackingLookback uint

	// PreserveDeadEqualityDeletes, when true, retains equality delete
	// files that are provably dead after the rewrite. The cleanup
	// predicate follows scan planning's partition-spec applicability rule
	// (see DecideDeadEqualityDeletesWithSpecs) and never removes one that could still
	// affect a surviving data file.
	//
	// Zero value (false) is the recommended default: dead eq-deletes are
	// expunged during the rewrite commit, which keeps manifest fanout
	// bounded under sustained CDC workloads where eq-deletes accumulate
	// one-per-snapshot. Set to true only if a downstream consumer
	// depends on the historical eq-delete files surviving in the live
	// snapshot's manifests (rare).
	//
	// Honored only in atomic mode (RewriteDataFilesOptions.PartialProgress=false).
	// The CLI / library caller is responsible for translating this flag
	// into the snapshot walk + RewriteDataFilesOptions.ExtraDeleteFilesToRemove
	// — see CollectDeadEqualityDeletesWithSpecs.
	PreserveDeadEqualityDeletes bool
}

Config holds tunable thresholds for bin-pack compaction.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with production defaults.

func (Config) PlanCompaction

func (cfg Config) PlanCompaction(tasks []table.FileScanTask) (Plan, error)

PlanCompaction analyzes the given scan tasks and produces a Plan. Tasks are grouped by partition, classified as candidates or skipped, and candidates are bin-packed into groups targeting TargetFileSizeBytes.

func (Config) Validate

func (cfg Config) Validate() error

Validate checks that the config values are sensible.

type Group

type Group struct {
	// PartitionKey is an opaque grouping key derived from partition values.
	// Empty string for unpartitioned tables.
	PartitionKey string

	// Tasks are the input FileScanTasks that should be compacted together.
	Tasks []table.FileScanTask

	// TotalSizeBytes is the sum of data file sizes in this group.
	TotalSizeBytes int64

	// DeleteFileCount is the total number of delete files across all tasks.
	DeleteFileCount int
}

Group represents a set of files in the same partition that should be compacted together.

type PartitionSpecLookup added in v0.7.0

type PartitionSpecLookup interface {
	PartitionSpecByID(int) *iceberg.PartitionSpec
}

PartitionSpecLookup resolves the partition spec used to write a data file.

type Plan

type Plan struct {
	// Groups are the sets of files to compact, each within a single partition.
	Groups []Group

	// SkippedFiles is the count of files that are already optimal or oversized.
	SkippedFiles int

	// TotalInputFiles is the total number of files scanned.
	TotalInputFiles int

	// TotalInputBytes is the total size of all scanned files.
	TotalInputBytes int64

	// EstOutputFiles is the estimated number of output files after compaction.
	EstOutputFiles int

	// EstOutputBytes is the estimated total size of output files.
	// This is an upper-bound estimate: actual output may be smaller because
	// deleted rows are removed and Parquet compression/encoding is more
	// effective on larger files with better column statistics.
	EstOutputBytes int64
}

Plan is the output of analyzing a table for compaction.

func Analyze

func Analyze(ctx context.Context, tbl *table.Table, cfg Config) (Plan, error)

Analyze scans the table and produces a compaction Plan without modifying anything. This is the dry-run / planning entry point.

type SurvivorSurvey

type SurvivorSurvey struct {
	EmptyPartMinSeq int64
	PartMinSeq      map[string]int64
	// contains filtered or unexported fields
}

SurvivorSurvey describes the surviving data files in a snapshot after a planned rewrite has logically removed its rewrite set. It is the input to DecideDeadEqualityDeletes and DecideDeadEqualityDeletesWithSpecs.

EmptyPartMinSeq and PartMinSeq retain the original survey layout used by DecideDeadEqualityDeletes. Spec-aware callers should populate the survey with SurvivorSurvey.AddSurvivorWithSpec.

Sentinel "no survivor in this bucket" is math.MaxInt64.

func NewSurvivorSurvey

func NewSurvivorSurvey() *SurvivorSurvey

NewSurvivorSurvey returns a survey initialized with the no-survivor sentinel for the empty-partition bucket and an empty per-partition map. Callers populate via AddSurvivor.

func (*SurvivorSurvey) AddSurvivor

func (s *SurvivorSurvey) AddSurvivor(partition map[int]any, seq int64)

AddSurvivor records a surviving data file's partition and sequence number using the conservative compatibility rule.

Defensive: if seq < 0 (sentinel for "unset"), the file is recorded with seq=0 (smallest real value), which keeps it permanently alive against every eq-delete. Better to preserve uncertain state than to drop deletes that may still apply.

func (*SurvivorSurvey) AddSurvivorWithSpec added in v0.7.0

func (s *SurvivorSurvey) AddSurvivorWithSpec(specID int32, partition map[int]any, seq int64)

AddSurvivorWithSpec records a surviving data file's spec ID, partition, and sequence number for exact equality-delete matching. It also maintains the conservative survey used by DecideDeadEqualityDeletes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL