scan

package
v0.18.17 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package scan provides table scanning with 3-level predicate pushdown.

Index

Constants

View Source
const (
	// DefaultDecodeAheadWorkers is deliberately modest: decode workers
	// multiply with the per-column errgroup inside ReadRowGroupNative
	// (min(#cols, GOMAXPROCS) each), and with concurrent fragments per
	// worker process. cpuToken integration (memo S3) replaces this with
	// budget-aware width.
	DefaultDecodeAheadWorkers = 4

	// DefaultDecodeAheadWindowBytes matches the scanPrefetch byte-window
	// order of magnitude; the morsel dispenser's budget bounds the next
	// stage downstream.
	DefaultDecodeAheadWindowBytes int64 = 256 << 20
)
View Source
const (
	OpLike    = "like"
	OpNotLike = "not_like"
)

RowPred.Op values for pattern predicates.

Variables

View Source
var DictPrune = optswitch.Register("dict-prune", "WADJET_DICT_PRUNE",
	"dictionary-probe row-group pruning for equality predicates on pure-dictionary chunks")

DictPrune gates dictionary-probe row-group pruning. The planner checks it when collecting equality conjuncts into EqProbes (plan.go), so with the switch off no probe is ever built. Kill switch: WADJET_DICT_PRUNE=0.

View Source
var LengthsOnlyColumnDecodes atomic.Int64

LengthsOnlyColumnDecodes counts column chunks decoded as lengths. Tests assert engagement with it — a suite that never takes the path proves nothing about it.

View Source
var RLERunPreds = optswitch.Register("rle-run-preds", "WADJET_RLE_RUN_PREDS",
	"run-granularity predicate evaluation over RLE dictionary-index pages in the scan filter")

RLERunPreds gates run-granularity predicate evaluation over dictionary-index pages. With it off, dictionary pages expand their index stream and the mask is applied per row, exactly as before. Kill switch: WADJET_RLE_RUN_PREDS=0.

View Source
var StatsPrune = optswitch.Register("stats-prune", "WADJET_STATS_PRUNE",
	"min/max zonemap row-group pruning from static scan predicates")

StatsPrune gates static-predicate min/max (zonemap) row-group pruning at every consumption site: the planner's rgUnit build, readBatchDirect, and the Scanner decode path. Dynamic-filter pruning (join-build ranges, blooms) is separately gated. Kill switch: WADJET_STATS_PRUNE=0.

Functions

func ApplyDeleteMarkers added in v0.18.2

func ApplyDeleteMarkers(b *batch.RecordBatch, rowOffset int64, del *DeleteSet) bool

ApplyDeleteMarkers narrows b's selection to the rows the set does not delete, where rowOffset is the FILE-ABSOLUTE index of b's row 0. Returns false when nothing survives — the caller drops the batch rather than passing an empty selection downstream.

An existing selection is intersected, never overwritten: a scan-level filter that already marked rows must not have them resurrected here (the same rule the single-process rgWorker follows).

func BackingReuseEnabled

func BackingReuseEnabled() bool

BackingReuseEnabled reports whether the scan-backing-reuse optimization is on. Exported so a source's armBackingReuse (internal/worker) can skip building a pool at all when the switch is off, rather than building one that Get/Recycle would immediately no-op through — the toggle is package- private here so the invariance oracle stays the single place that flips it.

func CanBloomPruneRowGroup

func CanBloomPruneRowGroup(bf *exec.BloomScanFilter, stats pqt.RowGroupStats) bool

CanBloomPruneRowGroup returns true when every integer value in the row group's min..max range is absent from the bloom — i.e., the row group cannot contain any rows matching the build side. Only applicable for single-column integer keys with a small (≤1024) value range; larger ranges return false (no pruning) to keep the check O(small).

func CanDictPruneRowGroup

func CanDictPruneRowGroup(fr *pqt.FileReader, rgIdx int, probes []EqProbe) bool

CanDictPruneRowGroup reports whether ANY of the equality conjuncts is provably unsatisfiable in this row group via a pure-dictionary probe. A true return means the row group cannot produce a matching row.

func CanPruneRowGroup

func CanPruneRowGroup(pred StatsPredicate, stats pqt.RowGroupStats) bool

CanPruneRowGroup returns true if the row group can be skipped based on min/max stats.

func CanRangePruneRowGroup

func CanRangePruneRowGroup(ranges []exec.DynamicRange, stats pqt.RowGroupStats) bool

CanRangePruneRowGroup returns true when a row group can be skipped based on dynamic min/max ranges supplied by an upstream hash-join build (or a distributed dynamic filter). A row group prunes when its column range has no overlap with ANY of the supplied range filters.

Behavior matches the in-process planner's canRangePruneRowGroup (planner/physical/util.go) — moved here so the worker fragment-runner scan path can apply the same logic without a circular import.

func CompareValues

func CompareValues(a, b any) int

CompareValues compares two typed values (int32, int64, float32, float64, string). Returns -1, 0, or 1. Exported for use by dynamic filter row-group pruning.

func DictPruneStatsSnapshot

func DictPruneStatsSnapshot() (int64, int64)

DictPruneStatsSnapshot returns (pruned row groups, non-pruning probes).

func EncodeDeleteRuns added in v0.18.2

func EncodeDeleteRuns(rows []int64) []byte

EncodeDeleteRuns serializes a set of file-absolute row indices as varint (gap, length) pairs over the coalesced runs — the form a task spec carries per input file (distributed.DeleteSpec.Runs).

gap is the distance from the previous run's END to this run's START, so the first pair's gap is the first deleted row itself and every later gap is >= 1 (runs are coalesced, hence never adjacent). A contiguous DELETE of any size is 2 varints; scattered deletes cost ~2 bytes each, against ~8 for the same index as a JSON decimal in the manifest that produced it. See the spec-size table in docs/internals/native-dag-execution.md.

func HasUnsupportedColumnarTypes

func HasUnsupportedColumnarTypes(schema []pqt.Column) bool

HasUnsupportedColumnarTypes returns true if any column uses a type the native columnar reader cannot handle (Array, Map). TypeDecimal is supported by the native reader.

TypeRow is supported only when every field is a DIRECT PRIMITIVE LEAF. readRowGroupNative's leafByPath is keyed on a leaf's FULL path, and the ROW arm can only ever build a two-element one — {column, field}. That is the whole of what it can address: a field that is itself a ROW, ARRAY or MAP is a GROUP in the file, its leaves live one or more levels below that path, and the lookup missed — taking the "column absent from the file → all nulls" branch and answering `{a:"x", inner:{x:7}}` as `inner:<nil>` with no error (#448, the same silent-wrong-answer class as #425 one level down).

Refusing the shape here is the fix rather than teaching the ROW arm to recurse, because the recursion is not one level of lookup: assembling a container needs the def/rep level walk the row reader's record assembler already does (nested_assembly.go), and the native reader has no level machinery at all — which is why ARRAY and MAP are refused outright. A ROW of ROW alone could be addressed by path, but ROW of ARRAY and ROW of MAP could not, so the predicate would still have to exist and the reader would gain a second partial assembler to keep in agreement with the first (ADR-0018 §3). The row reader has been correct for these shapes since the #409 nested-assembly rewrite; this routes to it.

Nothing loses a read path by this. The planner decides between the eager native scan and the row fallback on parquet.Schema.HasNestedColumns, which refuses a table with ANY ROW column one layer earlier (plan.go Init, buildRGUnits, readBatchDirect), and the top-N late-materialization rewrite refuses one on the full table schema (topn_late_mat.go) — so ReadRowGroupNativeShaped, the late-mat decode, never sees a ROW at all. The worker's cachedFileStreamSource is the path that does, and it tests this predicate before opening a row-group iterator and falls back to ReadFileBatchesShard, which reads through the row reader.

func LengthsOnlyDecodeOn

func LengthsOnlyDecodeOn() bool

LengthsOnlyDecodeOn reports whether lengths-only column decode is enabled.

func ReadFileBatches

func ReadFileBatches(reader *pqt.Reader, schema []pqt.Column, selectedCols []string) ([]*batch.RecordBatch, error)

ReadFileBatches reads all row groups from a Parquet file into separate RecordBatches (one per row group). Supports column projection via selectedCols. Falls back to row-based reading for schemas containing Array or Map types.

func ReadFileBatchesNative

func ReadFileBatchesNative(fr *pqt.FileReader, schema []pqt.Column, selectedCols []string) ([]*batch.RecordBatch, error)

ReadFileBatchesNative reads all row groups from a Parquet file using our custom FileReader (no parquet-go dependency). Returns one RecordBatch per row group for schemas without unsupported types.

func ReadFileBatchesNativeShard

func ReadFileBatchesNativeShard(fr *pqt.FileReader, schema []pqt.Column, selectedCols []string, shardIdx, shardCount int) ([]*batch.RecordBatch, error)

ReadFileBatchesNativeShard reads only the row-group slice assigned to one shard. With shardCount=1 the behavior matches ReadFileBatchesNative.

Row-group ownership: shardIdx i reads row groups in [i*total/count, (i+1)*total/count). When total < count, the early shards each read one row group and later shards read nothing — a degenerate but correct split.

func ReadFileBatchesShard

func ReadFileBatchesShard(reader *pqt.Reader, schema []pqt.Column, selectedCols []string, shardIdx, shardCount int) ([]*batch.RecordBatch, error)

ReadFileBatchesShard is like ReadFileBatches but reads only the row-group slice assigned to one shard of a multi-task scan. With shardCount=1 the behavior is identical to ReadFileBatches (whole file). With shardCount>1 the file's row groups are split evenly into shardCount disjoint ranges and shardIdx selects which range to read; the union over all shards equals the whole file.

This is the primitive that lets a single compacted parquet file (e.g. SF10 partsupp = 691 MB single file) fan out across N tasks without requiring the file to be physically chunked. The downstream broadcast-join chain then inherits parallelism through probe-split (which checks `len(probeFiles) >= 2`) because each shard task emits its own output file.

func ReadFileColumnar

func ReadFileColumnar(reader *pqt.Reader, schema []pqt.Column) (*batch.RecordBatch, error)

ReadFileColumnar reads all row groups from a Parquet reader into a single RecordBatch. Used by the DML executor to read entire files for DELETE/UPDATE operations.

A schema with an Array/Map column, or a ROW whose field is itself a container, is refused by ReadRowGroupNative (HasUnsupportedColumnarTypes, #448/#449) — routing here instead of erroring would either fail every DELETE/UPDATE against such a table or, before that guard existed, silently null out the nested column on readback. The row reader has no such restriction, and readFileBatchesViaRows(reader, schema, nil) reads the whole file into exactly one batch in file order, so the row indices the DML callers compute against it (delete markers, scalar UPDATE rewrites) line up the same way ReadRowGroupNative's batches would have.

func ReadRowGroupNative

func ReadRowGroupNative(fr *pqt.FileReader, rgIdx int, schema []pqt.Column, pool *batch.BatchPool) (*batch.RecordBatch, error)

ReadRowGroupNative reads a row group using our custom page reader, bypassing parquet-go entirely for the data path.

func ReadRowGroupNativeBacked

func ReadRowGroupNativeBacked(fr *pqt.FileReader, rgIdx int, schema []pqt.Column, cache *DecodedChunkCache, backing *BackingPool) (*batch.RecordBatch, error)

ReadRowGroupNativeBacked is ReadRowGroupNativeCached with the scan source's row-group backing pool: the decode writes into a backing a previous row group used, when the consumer released it and nobody claimed it. A nil backing (or the reuse kill switch off) is byte-identical to ReadRowGroupNativeCached. See BackingPool's ownership rule and docs/design/scan-output-backing-reuse.md.

func ReadRowGroupNativeCached

func ReadRowGroupNativeCached(fr *pqt.FileReader, rgIdx int, schema []pqt.Column, pool *batch.BatchPool, cache *DecodedChunkCache) (*batch.RecordBatch, error)

ReadRowGroupNativeCached is ReadRowGroupNative with an optional decoded chunk cache: plain leaf columns whose (identity, row group, column, type) is cached skip decompress+decode and copy from the cache; fresh decodes are offered back for admission. A nil cache (or a reader without a CacheIdentity) is byte-identical to ReadRowGroupNative.

func ReadRowGroupNativeSel

func ReadRowGroupNativeSel(fr *pqt.FileReader, rgIdx int, schema []pqt.Column, pool *batch.BatchPool, sel []uint32) (*batch.RecordBatch, error)

ReadRowGroupNativeSel is ReadRowGroupNative under a partial scan-filter selection: eligible byte-array columns materialize only the rows in sel (ascending row indices; see sel_decode.go). A nil sel — or the sel-decode kill switch off — is identical to ReadRowGroupNative.

Selectivity gate (metal-validated 2026-08-17): the sel path copies per selected value, the full path bulk-copies the page. At sparse selections the skipped values dominate (ClickBench Q22 −30% hot at ~0.1%); past ~25% selected the per-value loop loses to the single memcpy (Q28 +8s, Q29 +1.9s on `Referer <> ”`, which selects most rows) — those decode in full.

func ReadRowGroupNativeShaped

func ReadRowGroupNativeShaped(fr *pqt.FileReader, rgIdx int, schema []pqt.Column, pool *batch.BatchPool, sel []uint32, shapeOnly map[string]bool) (*batch.RecordBatch, error)

ReadRowGroupNativeShaped is ReadRowGroupNativeSel plus the set of columns (lowercased names) the planner proved are consumed for their SHAPE only. Those decode to per-row lengths with no value bytes materialized at all (lengths_decode.go). A nil/empty set — or the lengths-only kill switch off — is identical to ReadRowGroupNativeSel.

func RunPathStatsSnapshot

func RunPathStatsSnapshot() (int64, int64)

RunPathStatsSnapshot returns (pages, rows) evaluated run-wise.

func ScanFilterStatsSnapshot

func ScanFilterStatsSnapshot() (int64, int64)

ScanFilterStatsSnapshot returns (evaluated, skipped) row-group counts.

func SelDecodeOn

func SelDecodeOn() bool

SelDecodeOn reports whether sel-aware materialization is enabled; the physical planner keys the LIKE selected-column pushdown gate off it (pattern columns in SELECT only pay a mask-eval + selected-only copy when this path is live).

func SetLengthsOnlyDecodeForTest

func SetLengthsOnlyDecodeForTest(on bool) bool

SetLengthsOnlyDecodeForTest flips the kill switch and returns its previous value. Test-only: production reads the env var once at Register time.

func StorageClass

func StorageClass(t pqt.TypeID) pqt.TypeID

StorageClass exposes storageClass to other packages. It is the file-vs- catalog compatibility relation the columnar decoder uses: when the file's type and the catalog's type share a storage class the page values are copied into the vector VERBATIM, and only a mismatch routes through copyNativeCoerced* (which converts values). Callers that want to reason about a parquet value without reading it — the planner's footer-statistics MIN/MAX path — need exactly this test.

Types

type BackingPool

type BackingPool struct {
	// contains filtered or unexported fields
}

BackingPool is one scan source's free list of decoded row-group backings.

Ownership rule

A backing may be handed to a later decode only when BOTH hold:

  1. RELEASED — the consumer side said it is finished. That is the morsel dispenser's retire callback for the parent batch (fired once every zero-copy view minted over it has retired, which is after the whole op chain AND the sink consume), or the serial fragment path's return from driver.push. This is what a claim check alone cannot supply: the decode-ahead ring (ADR-0015) decodes group N+1 while group N is being consumed, k morsel consumers read one parent concurrently, and HashJoinProbe.emitViewOutput's view columns read our columns through Vector.Base after Execute returned — none of those readers claim.

  2. UNCLAIMED — nobody kept it: neither RecordBatch.Detach on the batch we emitted nor Vector.Claim on any column, including a claim that arrived transitively through a derived batch (ColumnPrune, set-op emit, selView) or through Vector.Base from a view minted downstream over one of our columns. This is what a release signal alone cannot supply: Sort, Window, the hash-join build, SortMergeJoin and the spillable collector all retain past retire, and all of them Detach (ADR-0016).

The release is the liveness signal; the claim is the retention veto. The backing is surrendered whole or not at all — one claimed column surrenders the batch, permanently, because every column is reachable from the batch a consumer kept.

Identity, without a reference

The pool only ever takes back a batch it minted, at the generation it minted it: a WSHF shuffle chunk, a row-based fallback batch, a batch from another pool or a batch already recycled is ignored, so no second owner can be created for storage someone else recycles.

That identity is a batch.MintStamp the pool writes ON the batch, never a registry of outstanding pointers. A registry is a strong reference: the batches this pool mints are whole decoded row groups (~280 MB each at SF100 lineitem widths), and most of the sources that own a pool have consumers with NO release edge at all (the shuffle task's plain Next loop, the hash-join build source, planner.StreamingSources) — every one of those would pin its entire live set for the source's lifetime, invisibly to the memory ledger, and any cap on the registry's size would silently turn reuse off rather than bound the damage. The stamp inverts the direction: the pool holds only its own idle list, a dropped batch (a bloom-filtered-to-nothing row group, a ring discard at a cross-file boundary) is plain garbage the moment the pipeline lets go of it, and a source whose consumer never releases costs exactly one stamp per decode.

A pool is created only where a RELEASE EDGE exists — see the caller side in internal/worker (batchRecyclerOf arms it). A pool without one could never take a backing back, so it would be pure overhead.

See docs/design/scan-output-backing-reuse.md for the full statement and the preconditions it rests on.

func NewBackingPool

func NewBackingPool(opts BackingPoolOpts) *BackingPool

NewBackingPool returns a pool for one scan source. It is safe for concurrent use: decode workers call get, consumers call Recycle.

func (*BackingPool) Drop

func (p *BackingPool) Drop()

Drop releases the idle set. Call when the owning source closes: retained row-group storage must not outlive the source that owns it.

func (*BackingPool) Recycle

func (p *BackingPool) Recycle(b *batch.RecordBatch, mint batch.MintStamp)

Recycle is the release edge: the consumer side is done with the batch it was handed as b under the stamp mint. It is a no-op for a batch this pool did not mint, and for a stamp that is not the CURRENT one for that storage — a stale release from a previous generation, which must never re-admit a live backing. See the ownership rule on BackingPool: the claim check below is the retention veto.

The caller captures mint when it takes delivery of the batch, not at release time: reading the stamp back off the batch would defeat the generation check exactly when it matters, since by then the batch may already carry the next generation's stamp.

func (*BackingPool) Stats

func (p *BackingPool) Stats() BackingPoolStats

Stats snapshots the counters.

type BackingPoolOpts

type BackingPoolOpts struct {
	// MaxIdle bounds the idle free list by count.
	MaxIdle int
	// MaxIdleBytes bounds the idle free list by MemBytes sum. One backing is
	// always keepable regardless of size, mirroring the decode ring's
	// "the delivery-cursor group is always admitted" escape: without it a
	// single row group larger than the cap disables the mechanism on exactly
	// the table that motivates it (SF100 lineitem, ~280 MB per group).
	MaxIdleBytes int64
}

BackingPoolOpts sizes a BackingPool. Zero fields take the defaults.

type BackingPoolStats

type BackingPoolStats struct {
	Hits      int64 // decodes that reused a backing
	Misses    int64 // decodes that had to mint one
	Claimed   int64 // releases refused because a consumer had claimed the batch
	IdleBytes int64
	// Held is the number of batches this pool holds a Go reference to. It is
	// exactly the idle free list: the pool never references a backing it has
	// handed out, so a source whose consumer has no release edge holds nothing
	// here however many row groups it decodes.
	Held int
}

BackingPoolStats is one pool's engagement counters.

type DecodeAdmission

type DecodeAdmission interface {
	DecodeStallBegin()
	DecodeStallEnd()
}

DecodeAdmission is the optional half of TokenPool: a pool that implements it is told when a decode worker parks on a failed acquisition and when it stops waiting, so the pool can hold a floor of tokens back from its own consumer queue instead of granting every release around a decoder that is the only thing able to refill that queue. Without it decode is a non-blocking second-class caller whose demand is invisible — the closed loop the worker pool's admission policy documents (internal/worker/cpu_tokens.go). Begin/End are always paired and must be safe for concurrent use; they are called with the window lock HELD, so an implementation must never call back into the iterator.

type DecodeAheadIter

type DecodeAheadIter struct {
	// contains filtered or unexported fields
}

DecodeAheadIter is the parallel sibling of RowGroupIter (docs/design/scan-decode-pipelining.md): k decode workers pull row-group indices in file order, each runs ReadRowGroupNative for its group, and results deliver to the consumer strictly in source order. The consumer-facing contract is identical to RowGroupIter — same batches in the same order, same error surfaced at the same position, same prune behavior — only the decode of group N+1..N+w overlaps the consumption of group N instead of waiting for it.

Memory is bounded by WindowBytes of decoded-but-undelivered batches, estimated per group from the projected columns' TotalUncompressedSize metadata (estimation error is bounded by one group per worker). The group at the delivery cursor is always admitted regardless of the window so a single oversized row group cannot deadlock the pipeline.

Concurrency safety rests on ReadRowGroupNative's documented contract: FileReader is read-only after construction and every ColumnPages call allocates a fresh ColumnPageReader, so concurrent decodes of distinct row groups never share mutable state (columnar_native.go:124-126).

Lifecycle: workers start on the FIRST Next() call, not at Open — filters attach after Open on the worker scan path (and may keep arriving mid-scan; assignments re-read them per group). Close() stops assignment and JOINS in-flight decodes before returning: the caller munmaps the file bytes right after Close, so no decode may touch the underlying slice once Close returns.

func OpenDecodeAheadIter

func OpenDecodeAheadIter(reader *pqt.Reader, schema []pqt.Column, selectedCols []string, shardIdx, shardCount int, opts DecodeAheadOpts) (*DecodeAheadIter, error)

OpenDecodeAheadIter constructs a decode-ahead iterator over the row-group range assigned to (shardIdx, shardCount), mirroring OpenRowGroupIter's contract (empty-shard sentinel, Array/Map rejection, projection via selectedCols).

func (*DecodeAheadIter) AssignmentDrained

func (it *DecodeAheadIter) AssignmentDrained() bool

AssignmentDrained reports whether every row group has been assigned to a decode worker (not necessarily delivered) — the cross-file continuation's pre-open trigger: once true, idle workers exist or soon will, and the next file's head is the only work left to feed them.

func (*DecodeAheadIter) Close

func (it *DecodeAheadIter) Close() error

Close stops assignment, wakes everything, and joins in-flight decodes. The caller may munmap the file bytes as soon as Close returns — workers never touch the FileReader after the join. Undelivered decoded batches are dropped (GC-eligible). Idempotent.

func (*DecodeAheadIter) DecodeSpans

func (it *DecodeAheadIter) DecodeSpans() (ns, bytes int64)

DecodeSpans returns the total wall time (ns) spent inside ReadRowGroupNative and the projected compressed bytes those decodes covered. Unlike StallDurations — which only measures parked waiters — this captures inline mmap fault time hidden inside token-holding decode spans: ns/byte materially above a page-cache-hot run's ratio means decode workers are faulting synchronously despite I/O-ahead.

func (*DecodeAheadIter) Next

func (it *DecodeAheadIter) Next() (*batch.RecordBatch, error)

Next returns the next decoded row group in source order, or (nil, nil) when exhausted. Contract identical to RowGroupIter.Next.

func (*DecodeAheadIter) PruneStats

func (it *DecodeAheadIter) PruneStats() (bloom, rangeP, read int)

PruneStats mirrors RowGroupIter.PruneStats.

func (*DecodeAheadIter) RowOffset added in v0.18.2

func (it *DecodeAheadIter) RowOffset() int64

RowOffset is the FILE-ABSOLUTE index of the first row of the batch Next most recently returned. Same contract as RowGroupIter.RowOffset: valid immediately after a non-nil Next, 0 before the first delivery.

func (*DecodeAheadIter) SetDynamicFilters

func (it *DecodeAheadIter) SetDynamicFilters(ranges []exec.DynamicRange, blooms []*exec.BloomScanFilter)

SetDynamicFilters attaches dynamic-filter pushdowns. Safe mid-scan: decode workers re-read the filter set under the window lock at every group assignment, so a set that lands while the scan is running prunes every group not yet assigned (attach-on-arrival delivery). Groups already assigned or advised before the call decode/advise unfiltered — drop-only semantics, results identical. Multiple calls overwrite; callers pass the accumulated union. Mirrors RowGroupIter.SetDynamicFilters.

func (*DecodeAheadIter) StallDurations

func (it *DecodeAheadIter) StallDurations() (windowFullNs, pressureNs, tokenNs, ledgerNs int64)

StallDurations returns the total blocked time (ns) behind each Stats counter, in the same order (window-full, pressure, token, ledger). Counts say how often a gate closed; these say how much wall it cost.

func (*DecodeAheadIter) Start

func (it *DecodeAheadIter) Start()

Start spawns the decode workers immediately instead of on the first Next — the cross-file continuation pre-opens the next file's iterator and wants its head decoding while the current file's tail delivers. Dispatch-time filters should already be attached; late (attach-on- arrival) filters may still land afterward via SetDynamicFilters. Idempotent.

func (*DecodeAheadIter) Stats

func (it *DecodeAheadIter) Stats() (groupsRead, windowFullStalls, pressureStalls, tokenStalls, ledgerStalls int64)

Stats returns the decode-ahead engagement counters (memo §5/§9 markers): row groups decoded, worker stalls on a full window, admissions refused under memory pressure, admissions deferred for lack of a cpu token, and admissions denied by the memory ledger (the group still decodes later — serially at worst, in every case).

type DecodeAheadOpts

type DecodeAheadOpts struct {
	// Workers is the decode worker count. <= 0 selects
	// DefaultDecodeAheadWorkers capped at GOMAXPROCS.
	Workers int
	// WindowBytes bounds decoded-but-undelivered batch bytes. <= 0
	// selects DefaultDecodeAheadWindowBytes. Ignored when Window is set.
	WindowBytes int64
	// Window shares an existing byte window (and its lock) with another
	// iterator — the cross-file continuation shape: the tail of file F
	// and the head of file F+1 draw from one budget.
	Window *DecodeWindow
	// Pressure, when set, is consulted before admitting any group beyond
	// the delivery cursor — the memory-pressure collapse hook. Must be
	// safe to call from multiple goroutines.
	//
	// While it reports true, admission is OCCUPANCY-FLOORED by default:
	// one non-cursor group may be in flight or parked (a 2-deep
	// pipeline), further admission waits. The 2026-07-18 SF100 sensor
	// A/B (memo §9.5) split the pressure regime in two: when decode
	// outruns the consumer the window fills and its held bytes displace
	// page cache (collapse is right — Q06 +46 % without it), but on
	// producer-bound repartition stages the window sits EMPTY and
	// cursor-only collapse is pure serialization with nothing to shed
	// (Q05 −12.7 % when spared). One group ahead holds ~one group-est of
	// bytes — nothing to displace with — while keeping the producer
	// pipelined.
	Pressure func() bool
	// PressureStrict restores cursor-only collapse under Pressure (no
	// group ahead at all). Set on edge-class envelopes (< 2 GiB
	// GOMEMLIMIT), where the capped repro measured even one extra
	// in-flight group as harmful (32 MiB window arm +37 % vs cursor-only
	// winning outright).
	PressureStrict bool
	// Tokens, when set, budgets decode CPU per ROW GROUP: a worker
	// acquires one token at admission and releases it when the decoded
	// group parks, so a worker stalled on the window (or between groups)
	// holds nothing. The delivery-cursor group is token-exempt — serial
	// progress is always allowed, mirroring the morsel "first consumer is
	// free" rule. The 2026-07-16 SF100 pair convicted the previous
	// source-lifetime acquisition: decode workers sat window-stalled
	// holding tokens (~35k stalls/40k groups), starving concurrent join
	// fragments' morsel width (Q20 +54%, Q05 +17%) while utilization
	// stayed flat.
	Tokens TokenPool
	// Advise, when set, receives the file-relative byte range of each
	// projected column chunk shortly BEFORE its row group is decoded —
	// the I/O-ahead seam (docs/design/rowgroup-readahead.md). The worker
	// wires an madvise(MADV_WILLNEED) closure over the scan mmap so a
	// steady-state re-read faults asynchronously via kernel readahead
	// instead of synchronously under a held CPU token. Ranges for group
	// N+workers are issued as group N is assigned, so the advice leads
	// decode by roughly one full assignment wave. Must be safe for
	// concurrent use; calls stop before Close returns (decode workers
	// are joined), so an mmap-backed closure never outlives its munmap.
	Advise func(off, n int64)
	// Cache, when set, consults/feeds the worker's decoded-chunk cache
	// inside ReadRowGroupNativeCached (docs/design/decoded-rowgroup-cache.md).
	// Inert unless the reader carries a CacheIdentity. nil = uncached.
	Cache *DecodedChunkCache
	// Backing, when set, is the SCAN SOURCE's row-group backing pool: a
	// decode writes into storage a previous group used once the consumer
	// released it and nobody claimed it (BackingPool's ownership rule,
	// docs/design/scan-output-backing-reuse.md). It belongs to the source,
	// not the iterator, so it survives the cross-file continuation — a
	// backing delivered from file F may be released after the source has
	// moved to F+1. nil = every group allocates fresh.
	Backing *BackingPool
}

DecodeAheadOpts sizes a DecodeAheadIter.

type DecodeWindow

type DecodeWindow struct {
	// contains filtered or unexported fields
}

DecodeWindow is a byte budget shared by one or more DecodeAheadIters. Its mutex doubles as the owning iterators' state lock.

func NewDecodeWindow

func NewDecodeWindow(bytes int64) *DecodeWindow

NewDecodeWindow returns a window bounding decoded-but-undelivered bytes across every iterator opened with it. bytes <= 0 selects DefaultDecodeAheadWindowBytes.

func NewDecodeWindowWithLedger

func NewDecodeWindowWithLedger(bytes int64, ledger WindowLedger) *DecodeWindow

NewDecodeWindowWithLedger is NewDecodeWindow with a memory ledger attached: beyond the fixed byte ceiling, non-cursor admission must also clear ledger.Reserve, and every inflight byte is charged to the ledger for its parked lifetime. A nil ledger yields the fixed-window behavior. Callers passing a concrete pointer type must nil-check it themselves — a nil *T wrapped in the interface reads as non-nil here.

type DecodedChunkCache

type DecodedChunkCache struct {
	// contains filtered or unexported fields
}

DecodedChunkCache is a worker-lifetime cache of decoded parquet column chunks: one entry per (object identity, row group, leaf column, catalog type) holding a cache-owned *batch.Vector clone. It attacks the zstd decompress + decode-kernel bill (~24% + ~7% of SF100 worker CPU) for re-reads of the same immutable base-table bytes — cross-query within a run and across benchmark runs. See docs/design/decoded-rowgroup-cache.md.

Correctness model (v1, copy discipline): consumers NEVER share storage with the cache. A hit copies the cached vector into the caller's batch slot; an admit clones the freshly decoded vector into cache-owned storage. Entry vectors are immutable once inserted, so Get may return the entry pointer and the caller copies outside the cache lock (an eviction during the copy is safe — the GC keeps the clone alive).

Ledger model (ADR-0006): the cache OWNS its bytes once, surfaced through Size for a hard system reservoir (memory.NewReservoirFunc). Consumers' copies are ordinary scan output charged exactly as today. The cache also implements memory.AccountedOperator so RequestRelief can shed it — eviction is the cheapest relief in the process — before any operator pays a real spill.

Eviction is segmented LRU (probation/protected) with second-touch ghost admission: a key's first decode registers a ghost; the clone is stored on the second decode; hits promote probation entries to protected. Sequential scan floods (a cold first pass over a table) fill probation without displacing the protected hot set.

func NewDecodedChunkCache

func NewDecodedChunkCache(capBytes int64) *DecodedChunkCache

NewDecodedChunkCache returns a cache bounded to capBytes. capBytes <= 0 returns nil — a nil *DecodedChunkCache is valid and inert on every method.

func (*DecodedChunkCache) CapBytes

func (c *DecodedChunkCache) CapBytes() int64

CapBytes returns the configured budget. Nil-safe.

func (*DecodedChunkCache) EstimateRelief

func (c *DecodedChunkCache) EstimateRelief(target int64) int64

EstimateRelief implements memory.AccountedOperator (pure read).

func (*DecodedChunkCache) Inspect

Inspect implements memory.AccountedOperator.

func (*DecodedChunkCache) Offer

func (c *DecodedChunkCache) Offer(key decodedChunkKey, vec *batch.Vector, numRows int)

Offer presents a freshly decoded chunk vector for admission. First touch registers a ghost; later touches admit only when there is free budget or the candidate's frequency STRICTLY beats the eviction victim's — the churn gate from the 2026-08-12 SF100 pair (doc §9.2): under a uniform flood ties go to the incumbent, so the resident set stabilizes and the wasted-clone admission storms cannot form. The clone runs outside the cache lock, and only after the admission decision — a rejected offer costs a map touch, not a memmove.

func (*DecodedChunkCache) SetPressureFunc

func (c *DecodedChunkCache) SetPressureFunc(f func() bool)

SetPressureFunc wires the admission-pause pressure signal (worker: the heap-backpressure gauge OR the page-cache refault sensor). Call before the cache is shared with readers; nil leaves admission ungated. Nil-safe.

func (*DecodedChunkCache) ShedUnderPressure

func (c *DecodedChunkCache) ShedUnderPressure(lowWater int64) int64

ShedUnderPressure evicts down to lowWater bytes and returns bytes freed. The pressure-yield valve (doc §9.3): the worker stats loop calls this while the heap-backpressure gauge or the page-cache refault sensor is active, because resident cache heap is exactly what those channels see as displacement — a cache must be the first thing to yield, before decode-ahead collapses and producers pause on its behalf. Evicted entries re-ghost with their frequency, so the hot set re-admits through the normal gate once pressure clears. Nil-safe.

func (*DecodedChunkCache) Size

func (c *DecodedChunkCache) Size() int64

Size returns the cached bytes (the reservoir's live accessor). Nil-safe.

func (*DecodedChunkCache) SpillSome

func (c *DecodedChunkCache) SpillSome(target int64) (int64, error)

SpillSome implements memory.AccountedOperator: "spilling" a cache is eviction — entries re-decode from local compressed bytes on next miss.

func (*DecodedChunkCache) Stats

Stats returns a point-in-time counter snapshot. Nil-safe.

type DecodedChunkCacheStats

type DecodedChunkCacheStats struct {
	Hits, Misses, HitBytes       int64
	Admitted, GhostRegistered    int64
	Evictions, ReliefBytes       int64
	RejectedTooLarge, CloneSkips int64
	FreqRejected, PressurePaused int64
	SizeBytes, CapBytes          int64
	Entries                      int
}

DecodedChunkCacheStats is the counter snapshot for the worker stats ticker.

type DeleteSet added in v0.18.2

type DeleteSet struct {
	// contains filtered or unexported fields
}

Merge-on-read delete markers, and the one representation every read path shares.

A DELETE does not rewrite parquet. It records, per data file, the FILE-ABSOLUTE 0-based row indices of the rows it removed (catalog.DeleteMarker), and every scan of that file must skip them until compaction folds them in. "File-absolute" means counted over the file's row groups in order from row 0 — NOT relative to a row group, a shard, or whatever slice of the file a particular reader happens to be assigned. The single-process scanner tracks that with a per-row-group prefix sum (physical.rgUnit.rgRowOffset); the distributed scan source gets the same number from its row-group iterator (RowGroupIter.RowOffset).

DeleteSet is the runtime form: the sorted row indices coalesced into disjoint runs, which is both the compact wire encoding (EncodeDeleteRuns) and a fast membership test. A map[int64]bool costs ~50 B/row and is the wrong shape for a DELETE that removed a contiguous range — the common case, since a WHERE over a clustered column marks runs, not confetti.

func DecodeDeleteSet added in v0.18.2

func DecodeDeleteSet(b []byte) (*DeleteSet, error)

DecodeDeleteSet reverses Encode. A malformed payload is an error, never a panic and never a silently short set: a scan that cannot read its delete markers must fail the task, because the alternative is answering with the deleted rows still in it.

func NewDeleteSet added in v0.18.2

func NewDeleteSet(rows []int64) *DeleteSet

NewDeleteSet builds a DeleteSet from unsorted, possibly duplicated file-absolute row indices. Negative indices are dropped — a marker that names one is corrupt, and skipping it can only ever fail to delete a row that no reader would have matched to it anyway. Returns nil for an empty set so every consumer's nil check is the fast path.

func (*DeleteSet) Contains added in v0.18.2

func (d *DeleteSet) Contains(row int64) bool

Contains reports whether the given file-absolute row index is deleted. Nil-safe.

func (*DeleteSet) Empty added in v0.18.2

func (d *DeleteSet) Empty() bool

Empty reports whether the set deletes nothing. Nil-safe.

func (*DeleteSet) Encode added in v0.18.2

func (d *DeleteSet) Encode() []byte

Encode is EncodeDeleteRuns for an already-built set. Nil-safe; returns nil for an empty set so the wire field stays absent under omitempty.

func (*DeleteSet) Overlaps added in v0.18.2

func (d *DeleteSet) Overlaps(offset, count int64) bool

Overlaps reports whether any deleted row falls in [offset, offset+count). The cheap reject that keeps a table with a handful of markers from paying a per-row test on every row group of every file. Nil-safe.

func (*DeleteSet) Rows added in v0.18.2

func (d *DeleteSet) Rows() int64

Rows is the number of deleted row indices the set holds. Nil-safe.

func (*DeleteSet) Runs added in v0.18.2

func (d *DeleteSet) Runs() int

Runs is the number of disjoint runs. Nil-safe; used by tests and by the spec-size accounting.

type EqProbe

type EqProbe struct {
	ColName string
	Value   any
}

EqProbe is one equality conjunct to test against a row group.

type FilterDecision

type FilterDecision int

FilterDecision summarizes a row group evaluation.

const (
	FilterNone    FilterDecision = iota // no row matches: skip the row group
	FilterAll                           // every row matches: no selection needed
	FilterPartial                       // some rows match: apply Sel
)

func EvalRowGroupPreds

func EvalRowGroupPreds(fr *pqt.FileReader, rgIdx int, preds []RowPred, numRows int) ([]uint32, FilterDecision, error)

EvalRowGroupPreds evaluates the AND of preds over one row group and returns the matching selection. sel is only meaningful for FilterPartial and holds row indices in ascending order.

type PartitionFilter

type PartitionFilter map[string]string

PartitionFilter filters partitions based on partition key values.

type RowGroupIter

type RowGroupIter struct {
	// contains filtered or unexported fields
}

RowGroupIter yields one RecordBatch per row group on demand, without pre-decoding the rest of the file. Use this in long-running scan pipelines (worker fragment runners) where holding every row group of a file in memory would blow the per-task working set.

At SF100 each lineitem file has ~10 row groups × ~28 MB decoded each, so eager decode (ReadFileBatchesShard) costs ~280 MB live per file, times 2–4 files prefetched, times 3–4 concurrent tasks = multi-GB transient that the GC can't reclaim until the consumer (HashAggregate) drains. Streaming one row group at a time bounds the scan-side live memory to one decoded RG per scan source plus whatever is in flight downstream — typically <300 MB instead of multi-GB.

Lifecycle:

it, err := OpenRowGroupIter(reader, schema, selectedCols, shardIdx, shardCount)
if err != nil { ... }
defer it.Close()
for {
    b, err := it.Next(ctx)
    if err != nil || b == nil { break }
    // consume b, then b.Release() when done
}

The iterator does NOT support schemas containing Array/Map types — the existing row-based fallback in readFileBatchesViaRows decodes the whole file in one shot and predates row-group sharding. Callers must check HasUnsupportedColumnarTypes(schema) and use ReadFileBatchesShard for those types. The slice path stays available; this iterator is a parallel fast lane for the common case.

func OpenRowGroupIter

func OpenRowGroupIter(reader *pqt.Reader, schema []pqt.Column, selectedCols []string, shardIdx, shardCount int) (*RowGroupIter, error)

OpenRowGroupIter constructs a streaming iterator over the row-group range assigned to (shardIdx, shardCount) of the given reader. With shardCount=1 the iterator covers the whole file. Returns ErrUnsupportedColumnar for schemas with Array/Map types (callers must use ReadFileBatchesShard).

func (*RowGroupIter) Close

func (it *RowGroupIter) Close() error

Close marks the iterator as exhausted. Idempotent. No file handles are owned by the iterator (the caller's *pqt.Reader owns them), so Close is mostly a cancellation signal — subsequent Next calls return (nil, nil).

func (*RowGroupIter) Next

func (it *RowGroupIter) Next() (*batch.RecordBatch, error)

Next returns the next decoded row group as a RecordBatch, or (nil, nil) when exhausted. The returned batch's lifetime is the caller's; Release() to a pool when done. Subsequent calls after exhaustion or Close return (nil, nil).

func (*RowGroupIter) PruneStats

func (it *RowGroupIter) PruneStats() (bloom, rangeP, read int)

PruneStats returns counters for diagnostic logging: row groups skipped via bloom, via range, and actually read. Snapshot at any point.

func (*RowGroupIter) RowOffset added in v0.18.2

func (it *RowGroupIter) RowOffset() int64

RowOffset is the FILE-ABSOLUTE index of the first row of the batch Next most recently returned. Valid only immediately after a non-nil Next; 0 before the first delivery and for the empty-shard sentinel. Consumers use it to place a batch within its file, which is the frame merge-on-read delete markers are expressed in.

func (*RowGroupIter) SetBackingPool

func (it *RowGroupIter) SetBackingPool(p *BackingPool)

SetBackingPool attaches the scan source's row-group backing pool. Call before the first Next (the field is read without synchronization). nil = every group allocates fresh. See BackingPool's ownership rule.

func (*RowGroupIter) SetDecodedCache

func (it *RowGroupIter) SetDecodedCache(c *DecodedChunkCache)

SetDecodedCache attaches the worker's decoded-chunk cache. Call before the first Next (the field is read without synchronization). nil = uncached.

func (*RowGroupIter) SetDynamicFilters

func (it *RowGroupIter) SetDynamicFilters(ranges []exec.DynamicRange, blooms []*exec.BloomScanFilter)

SetDynamicFilters attaches dynamic-filter pushdowns to the iterator. Safe mid-scan from the goroutine calling Next: filters are consulted per group, so a set attached after some groups were read prunes every remaining group (attach-on-arrival delivery; drop-only semantics). Empty slices clear any existing filters. Multiple calls overwrite prior state — callers pass the accumulated union.

type RowPred

type RowPred struct {
	Col   string
	Op    string // =, !=, <, <=, >, >=
	Value any    // int64, float64, or string (planner-normalized)
}

RowPred is one pushed conjunct.

type ScanStats

type ScanStats struct {
	TotalPartitions  int
	PrunedPartitions int
	TotalFiles       int
	PrunedFiles      int
	TotalRowGroups   int
	PrunedRowGroups  int
	RowsScanned      int64
}

ScanStats tracks scan statistics.

type Scanner

type Scanner struct {
	// contains filtered or unexported fields
}

Scanner performs table scans with 3-level predicate pushdown.

func NewScanner

func NewScanner(cat *catalog.Catalog, tableName string) *Scanner

NewScanner creates a new scanner for the given table.

func (*Scanner) Close

func (s *Scanner) Close() error

func (*Scanner) Init

func (s *Scanner) Init(ctx context.Context) error

func (*Scanner) Next

func (s *Scanner) Next(ctx context.Context) (*batch.RecordBatch, error)

func (*Scanner) Stats

func (s *Scanner) Stats() ScanStats

Stats returns scan statistics after scanning.

func (*Scanner) WithColumns

func (s *Scanner) WithColumns(cols []string) *Scanner

WithColumns sets the columns to select (projection pushdown).

func (*Scanner) WithPartitionFilter

func (s *Scanner) WithPartitionFilter(filter PartitionFilter) *Scanner

WithPartitionFilter sets the partition filter (Level 1: partition pruning).

func (*Scanner) WithRowFilter

func (s *Scanner) WithRowFilter(pred exec.Predicate) *Scanner

WithRowFilter sets the row-level filter predicate (Level 3: row-level evaluation).

func (*Scanner) WithStatsPredicates

func (s *Scanner) WithStatsPredicates(preds []StatsPredicate) *Scanner

WithStatsPredicates sets predicates for Level 2 row-group pruning.

type StatsPredicate

type StatsPredicate struct {
	Column string
	Op     exec.CompareOp
	Value  any
}

StatsPredicate evaluates whether a row group can be skipped based on column stats.

Value must already be in the STATS domain — the representation parquet.RowGroupStats decodes the footer's bounds into, which for several types is not the representation a SQL literal arrives in. This layer compares two `any` values by their Go kind and cannot tell an unscaled DECIMAL bound from a scaled literal, or sixteen raw address bytes from an address in text; both pairs land in the same kind and get compared as if they agreed, which is #442. kernel.StatsDomainValue is the conversion, and the producer WITHHOLDS a predicate it cannot convert.

type TokenPool

type TokenPool interface {
	TryAcquire(n int) int
	Release(n int)
}

TokenPool is the compute-budget seam shared with the caller's pool (the worker's cpuTokens). Both methods must be safe for concurrent use; TryAcquire must not block.

type WindowLedger

type WindowLedger interface {
	Reserve(n int64) error
	ForceReserve(n int64)
	Release(n int64)
}

WindowLedger is the memory-budget seam shared with the caller's task ledger (the worker's shared pool tracker — *memory.Tracker satisfies it directly). Decoded-but-undelivered window bytes are charged here so they are visible to spill decisions and so admission collapses toward the cursor-only serial floor as budget headroom vanishes — the memo §9 fix: under memory pressure the window's held batches displace page cache and GC headroom that the Go-heap pressure hook cannot see, so the byte cost must ride the same ledger as every other operator.

Reserve returns a non-nil error to deny (nothing retained on denial); ForceReserve charges unconditionally — used for the delivery-cursor group, which is always admitted but whose bytes are still real. All methods must be safe for concurrent use and must not block.

Jump to

Keyboard shortcuts

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