blob: 82a3ba036677537a18e0092580b9a3c902e20e73 [file] [view]
# FileScannerV2 Code Review Guide
This guide contains the detailed checklists referenced by
`be/src/format_v2/AGENTS.md`. Read the common checklist for every FileReader review, then apply the
format-specific checklist when reviewing Parquet or ORC.
## Common FileReader: Indexes and Predicate Filtering
- Inventory the reader's actual pruning capabilities before evaluating a change: metadata or
statistics, dictionary information, Bloom filters, page/stripe/row indexes, partition/Split
ranges, and format-specific encodings. Record the granularity, supported predicate/type set,
exactness, I/O cost, and conservative fallback for each capability.
- A FileReader consumes only predicates already localized by `TableColumnMapper` in
`FileScanRequest`. It may translate those predicates into format-native indexes or SDK filters,
but it must not reinterpret table-schema identity, defaults, partitions, or table-format
semantics.
- Every index may discard a candidate only when it proves that the candidate cannot match. Missing,
malformed, stale, truncated, unsupported, writer-incompatible, or unsafe metadata must retain the
candidate or return the format's explicit correctness-preserving error.
- Check logical-to-physical identity at every index boundary: file-local root and nested column IDs,
physical leaf IDs, row-group/stripe/page ordinals, byte ranges, file-global row offsets, and
selected row ranges. Index results for one column or unit must never be applied to another.
- Verify metadata semantics for NULL/all-NULL, empty units, NaN, signedness, truncated bounds,
decimal precision/scale, date/timestamp/timezone, string/binary ordering, CHAR padding, and
external-writer differences before trusting min/max or membership information.
- Preserve a cheap-to-expensive pruning order. Do not read or parse a finer index for a file,
row group, stripe, or page already eliminated by a cheaper layer. Measure index read/parse/build
cost as well as the I/O, decompression, decoding, and materialization it avoids.
- Trace each predicate through index pruning, exact format-native filtering, Doris residual VExpr,
delete predicates, and final materialization. A predicate not exactly covered by an earlier layer
must remain in the residual path.
- Preserve SQL three-valued logic and error behavior across AND/OR/NOT, comparisons, IN/NOT IN,
IS NULL, null-safe equality, casts, functions, stateful expressions, and exception-sensitive
operations. Splitting or reordering predicates requires proof of equivalence.
- Predicate columns and lazily read non-predicate columns must refer to the same original rows after
all skips and filters. Skipping must advance every physical reader consistently, including nested
definition/repetition state, offsets, row positions, and subsequent batches.
- Keep row-level deletes, equality deletes, position deletes, table filters, and query predicates in
their specified order. An index optimization must not bypass a delete or use post-filter row
numbering where file-global numbering is required.
- Readers without a native index or lazy-read capability must declare that boundary and preserve
correctness through residual evaluation. Do not add an imitation index in a generic layer merely
to make formats appear uniform.
- Require differential tests that compare exact results and errors with each index/filter
optimization enabled and disabled. Cover missing/invalid indexes, all/none/partially filtered
units, multiple files/Splits/batches, NULL and type boundaries, nested data, deletes, and
external-writer fixtures.
## Common FileReader: Data and Condition Caches
- Distinguish the cache layers and their value semantics: remote `FileCache` stores file bytes,
format metadata/page caches store format-specific serialized ranges or parsed metadata,
`ConditionCache` stores predicate survivor granules, and table-format caches may store deletion
vectors or decoded objects. Never reuse an entry as a different representation.
- A cache key must include every input that can change the value: filesystem and canonical path,
stable object/file version, size or mtime where reliable, byte/Split range, format/encoding
context, and predicate digest for filter results. Disable the cache when a stable identity cannot
be established; never trade stale rows for a hit.
- Validate hit, miss, partial coverage, overlapping/subrange reads, eviction, concurrent access,
cancellation, and error paths. A partial cache hit must read or conservatively retain uncovered
data rather than treating it as absent.
- `ConditionCache` can skip only file-global granules explicitly known to contain no surviving row.
Disable or expand the key when Runtime Filters, delete files/vectors, table snapshots, or other
changing semantics are not represented. Publish a miss result only after the physical reader
reaches EOF successfully so unvisited granules cannot become false negatives.
- Cache admission, prefetch, and range merging must follow pruning and lazy materialization. Do not
prefetch output columns or pruned units merely to improve hit rate, and account for read
amplification, request count, memory ownership, and cache pollution.
- Preserve resource accounting and source attribution across local, peer, and remote hits. Require
counters for hit/miss/write/eviction, bytes by source, wait/download time, requests, and avoided
reads so performance claims are diagnosable.
- Require warm/cold, enabled/disabled, overwrite/version-change, partial-range, concurrent, and
cancellation tests. Cached and uncached execution must return identical rows and errors.
## Common FileReader: Virtual Columns
- Keep file-coordinate virtual columns distinct from table-format virtual columns. FileReaders may
synthesize reserved file-local `ROW_POSITION` and `GLOBAL_ROWID`; `TableReader` and
`TableColumnMapper` own table semantics such as Iceberg `_row_id`,
`_last_updated_sequence_number`, and Doris Iceberg row locators.
- `ROW_POSITION` is the absolute zero-based physical row in the file, not an output, batch,
selected-row, row-group, stripe, or Split-local ordinal. It must advance across pruned units,
skipped pages/granules, rejected batches, lazy filters, and deletes without renumbering survivors.
- `GLOBAL_ROWID` must be stable and unique for its documented context. Review context version,
backend/file identity, serialization, physical row position, cross-file collisions, and retries;
filtering and batching must not change the generated ID for the same source row.
- Generate virtual values only when requested as output or needed by a predicate/delete. Support
virtual-only scans with no physical projected column, predicate-only virtual columns, selected-row
materialization, and EOF without forcing unrelated file I/O.
- Preserve declared type, nullability, nested shape, and `LocalColumnId`/`LocalIndex` mapping. Do not
let reserved negative IDs collide with invalid IDs, physical columns, table IDs, or block
positions.
- Require tests across multiple files, Splits, row groups/stripes/pages, batches, all rows filtered,
no rows filtered, index/cache skips, lazy materialization, deletes, and virtual-only projection.
Compare virtual values with the same scan when pruning, caching, and lazy reads are disabled.
## Common FileReader: Performance and Observability
- Keep index construction, predicate translation, cache lookup, and virtual-column setup out of
per-row and repeated batch paths unless the work is inherently row-local. Avoid repeated schema
traversal, expression cloning, metadata parsing, allocation, and conversion.
- Keep Profile counters in the visible `FileScannerV2 -> TableReader -> FileReader -> IO` hierarchy.
Format-specific readers, such as `ParquetReader`, belong below `FileReader`; lifecycle, metadata,
index, predicate, decode, materialization, and physical I/O paths must all have timers at the layer
that owns the work. Flush recursively aggregated child-reader statistics at every batch boundary,
including empty-selection and error exits, so a slow in-progress scan is diagnosable before close.
- Require format readers to populate the common `ReaderStatistics` accurately where applicable:
filtered/read row groups, Bloom and min/max pruning, filtered group/page/lazy rows, read rows and
bytes, metadata/footer/cache timing, page-index work, predicate time, dictionary rewrite, and
Bloom read time.
- Reject dead or ambiguous counters. In particular, `FilteredBytes` counts compressed bytes of
projected physical chunks avoided by pruning, not every child in the Row Group; footer read,
footer parse, lazy page-index materialization, and page-index predicate evaluation need distinct
timers. Raw I/O counters stay under `IO` even when a format reader initiates the request.
- Evaluate performance with representative format versions, writers, data ordering, predicate
selectivity, nested width, remote storage, batch sizes, and warm/cold caches. Report both the
optimization overhead and the avoided work; a low pruning ratio alone is not a defect.
## Parquet Native Decode Boundary
- V2 must instantiate only readers and decoders under `be/src/format_v2/parquet/`; calls into the
v1 `ParquetColumnReader` or edits under `be/src/format/parquet/` are review blockers.
- Footer parsing, schema-ID assignment, and the cached native metadata tree must also be v2-owned.
Reusing a stable base file identity is allowed, but require a v2 cache type discriminator so
v1/v2 metadata objects can never be cross-cast. Production planning must not retain or rebuild an
Arrow `FileMetaData` tree from the serialized footer.
- Trace the hot path as `ColumnReader -> Decoder span/cursor API -> DataTypeSerDe -> Doris Column`.
Decoder must not accept a Doris column or target type, and the path must not create Arrow arrays,
builders, `DecodedColumnView`, or another decoded leaf batch.
- Verify physical/logical metadata is immutable per leaf reader and complete for signed integers,
decimal precision/scale, date/time/timestamp units and UTC adjustment, INT96, UUID, FLOAT16, and
fixed-width binary. Unsupported combinations return explicit errors before plausible output.
- Treat legacy Parquet `TIMESTAMP_MILLIS` and `TIMESTAMP_MICROS` converted types as UTC-adjusted.
Do not give them the local/unspecified semantics of an unannotated INT64 timestamp; data decode,
statistics conversion, and min/max pruning must use the same timezone rule.
- Route plain, dictionary, and decoded timestamp inputs through one checked conversion contract.
Validate INT96 nanos-of-day before widened Julian-day arithmetic, reject unit scaling overflow,
and enforce Doris year 0001-9999 before materialization. Conversion failures must follow the same
strict/non-strict and dictionary-ID propagation rules as other direct types.
- Verify schema-change routing separately from physical decode. The reader must emit the projected
file type, while `ColumnMapper`/`TableReader` perform file-to-table casts after file predicates.
Any different requested type at the native reader boundary must fail as an invariant violation;
do not add a reader-local conversion column or decoder-facing conversion ABI.
- Dictionary review must separate dictionary-entry IDs from logical rows and non-null payload
ordinals. Materialize the typed dictionary once per generation through the same SerDe, validate
every index before access, and invalidate cached dictionary state at Row Group/file/type changes.
Dictionary-entry predicate evaluation and later row-value flattening must reuse that same typed
generation rather than serializing, parsing, or converting the dictionary twice.
- Check direct materialization for PLAIN, RLE/dictionary, DELTA_BINARY_PACKED,
DELTA_LENGTH_BYTE_ARRAY, DELTA_BYTE_ARRAY, and BYTE_STREAM_SPLIT. Filtering must advance encoded
values without allocating output; null runs must append defaults without advancing payload.
- For predicate-only fixed-width PLAIN primitives, allow direct comparison only after proving the
whole Column Chunk uses compatible PLAIN value pages and every Expr advertises raw fixed-value
evaluation with identical Doris comparison semantics, including NaN ordering. The fallback
decision must precede definition-level consumption. Disable the direct path when a residual or
delete conjunct still references the hidden slot, because it needs the materialized payload.
Verify sparse input selection, interleaved NULLs, reversed literal comparisons, multiple ANDed
comparisons, mixed-encoding fallback, residual slot reuse, and a stable row-shaped placeholder.
- For a filtered scalar page fragment, require one SerDe entry and one batch-level selected-decode
dispatch. Nullable selections must first map logical rows to selected non-NULL physical ranges,
decode those ranges once, and restore NULL slots in place; falling back per NULL run is a review
blocker for a scalar destination that supports in-place expansion. Selection ranges belong to
persistent reader scratch; per-range virtual SerDe/decoder calls in the hot path are a review
blocker. Fixed PLAIN should
bulk-gather spans, BYTE_ARRAY PLAIN should scan lengths once, dictionary decode should validate
every ID before gathering selected IDs, and stateful encodings should batch-decode/reconstruct and
compact. Any remaining NULL-interleaving fallback must preserve logical output order without a
decoded intermediate column and be counted by `HybridSelectionNullFallbackBatches`.
- Review complex types as a level/shape problem around scalar leaf materialization. Parent offsets,
null maps, sibling alignment, page-spanning rows, and child payload counts must remain correct
without materializing an intermediate complex column.
- For MAP, the non-null key leaf owns the outer entry shape. Validate the materialized key/value
entry counts, but do not compare their raw repetition vectors: a nested value legitimately has
deeper levels. For STRUCT, compare each sibling only at the current parent boundary and ignore
repetition owned by a deeper child collection.
- Require a bounded high-water policy for persistent definition/repetition, null, selection,
conversion, dictionary-index, and decoder-owned scratch. Distinguish active bytes from retained
capacity: never release an oversized buffer while the current batch still needs it, and require
three ordinary/idle batches before releasing capacity above the high-water limit. Test both
outlier release and steady-state reuse so the policy does not create allocation thrash.
- Review all decoder read and skip paths as equally exposed corruption boundaries. Check requested
counts against remaining/declared values before pointer arithmetic or narrowing; use checked
addition/multiplication for byte extents; bound BYTE_ARRAY dictionary entry counts and IDs before
allocation/indexing; and require DELTA_BYTE_ARRAY prefixes to fit the previous reconstructed
value. BOOLEAN RLE and DELTA skip paths must consume bounded chunks and fail on short streams.
- Validate every decoded definition/repetition level against the schema maximum in batch, run, and
single-value cursor APIs. Page value counts must not drive eager nested scratch allocation;
reserve from the requested parent-row frontier and cover tiny-payload huge-count Page V1/V2 files.
- Before Snappy decompression, inspect the encoded uncompressed length and validate destination
capacity. Page V1, Page V2, and dictionary pages must produce exactly the declared decoded size;
an UNCOMPRESSED dictionary page must also declare equal compressed and uncompressed sizes.
- For a STRUCT whose projected children are all missing after schema evolution, require a
levels-only physical reference leaf. It must advance and validate encoded payload cursors while
deriving the synthetic child count, without constructing a discarded string/complex column.
- `CountColumnReader` must use the native levels-only reader and must not decode payload or call
Arrow `ReadRecords`. Require profiles that distinguish page I/O, decompression, level decode,
value decode, SerDe materialization, hybrid selection batches/ranges/NULL fallback,
filtered-value skips, and page fragmentation.
- Validate every signed Column Chunk offset/length before converting it to `size_t`. The dictionary
offset is usable only when it is non-negative and precedes the data offset; the complete range
must fit the file. Apply the PARQUET-816 tail padding only to affected parquet-mr versions, cap it
at 100 bytes, and keep it inside the file. Scalar and levels-only COUNT readers share this helper.
- Treat OffsetIndex as one optional, all-or-nothing navigation structure. Require first row zero,
the first physical location to equal the owning ColumnMetaData `data_page_offset`, strictly
increasing row ordinals and physical offsets, positive sizes, non-overlapping page ranges, and
containment in the owning Column Chunk. Discard a malformed index before selecting the indexed
reader.
- Page iteration skips `INDEX_PAGE` and unknown auxiliary pages before initializing a data decoder.
Dictionary pages retain their special first-page handling; a later dictionary page is corrupt.
- Derive writer workarounds once from `created_by` and pass them through scalar, nested, page-cache,
and COUNT paths. Pre-Arrow-3 parquet-cpp Data Page V2 payloads remain compressed despite the
historical `is_compressed=false` flag.
- Preserve nullable conversion semantics in direct native materialization. Numeric, DATE,
DATETIME, TIME, and DECIMAL failures insert a default nested value and mark the corresponding NULL
only in non-strict mode; strict or non-nullable reads return the error. Dictionary failures follow
the selected dictionary IDs to output rows.
- Keep Parquet decimals in a source-width or wider intermediate until exact scaling, target
precision, and overflow checks succeed. Scale-down with a non-zero remainder is a conversion
failure; plain and dictionary integer/binary paths must narrow only afterward.
- For cold small-file tests, separate footer I/O/Thrift parse from native schema and index planning.
V2 must not retain serialized footer bytes after the native metadata tree is initialized; v1 opens
remain independent.
- For HTTP Parquet objects at or below `in_memory_file_size`, v2 stages the complete object from
byte zero before native page access. Verify both cold and footer-cache-hit scans: some Range
servers accept the capability probe but return HTTP 200 for a near-EOF overlong range, so warm
scans must not depend on the footer read having populated an incidental transport buffer.
- Identical fixed-width POD values append with one bulk copy. FIXED_LEN_BYTE_ARRAY strings copy the
dense byte span once and synthesize offsets; validate this execution contract without flaky
wall-clock assertions.
## Parquet Multi-Level Filtering
- Use [FileScannerV2 Parquet Scan Design](file-scanner-v2-parquet-scan-design.md) as the detailed
architecture reference. Trace each affected predicate through localization, Row Group planning,
Page ranges, row-level residual evaluation, and final selected-column materialization.
- At Row Group level, check Split ownership and file-global row offsets, then verify Statistics,
Dictionary, and Bloom pruning independently. Dictionary pruning requires complete compatible
encoding. Bloom may prove absence only; a hit is never a matching row.
- Preserve the cost order from cheap to expensive. Footer Statistics should reduce candidates before
Dictionary/Bloom I/O, and ColumnIndex/OffsetIndex should be read only for surviving Row Groups.
- At Page level, require compatible ColumnIndex and OffsetIndex semantics. Check page-to-row mapping,
first/last row boundaries, empty or all-null pages, multi-column range intersection, and conversion
from logical `selected_ranges` to each leaf reader's physical `page_skip_plan`.
- Page skipping must keep every column reader aligned. Skipping values or pages must advance value,
definition, and repetition state consistently, especially for nested/repeated columns whose Page
boundaries do not align across leaves.
- At Row/Batch level, keep SelectionVector positions aligned with original Row Group rows across
dictionary-ID filters, incremental predicates, residual expressions, deletes, and output
materialization. Physical row positions must not be renumbered after pruning.
- Verify lazy materialization avoids reading and decoding non-predicate columns for rejected rows
while advancing all readers correctly. Predicate columns should be read/prefetched first; output
prefetch should wait for survivors when filtering is active.
- For safe staged single-column predicates, review the observed cost/rejection ordering and its
cold-start behavior. Reordering is allowed only after every candidate has a sample; prefetch may
stop at a low-probability reach prefix, while output-column prefetch may start early only after a
learned high survival ratio. Static conjunct schedules and position maps belong to the scanner
lifecycle; after eight warm-up samples, per-predicate clocks should be sampled only periodically.
Cache the batch SelectionVector's dense bitmap by generation so a wide lazy projection does not
rebuild the same O(batch) filter for every column.
- Single-column predicate rounds may keep previously read columns in their original row mappings.
Require one alignment compaction before multi-column, delete, or output boundaries, and expose
its time, bytes, and count instead of charging repeated movement invisibly to predicate time.
Reused compaction masks must be fully cleared when coordinate-space sizes shrink.
- PLAIN BYTE_ARRAY must not parse lengths in both decoder and destination column. Review the direct
payload-offset/cumulative-offset contract, uint32 overflow checks, surviving-span coverage, and
the legacy consumer fallback for non-string logical types.
- Production PageIndex planning must consume native Compact Thrift ColumnIndex/OffsetIndex objects.
Coalesce adjacent serialized index ranges and transfer validated OffsetIndexes into execution so
the Row Group does not read them twice. Arrow PageIndexReader is a test oracle; a production Arrow
metadata adapter or rebuilt `FileMetaData` tree is a review blocker.
- Fixed-width conversion fast paths may remove row branches only when the source domain provably
fits the target domain. Narrowing, timestamp, decimal scaling, strict rollback, and non-strict
NULL marking must retain corrupt-value tests.
- Recursive reader-profile publication and retained-scratch inspection should be amortized, but Row
Group reset/EOF/close must force the final profile delta before reader destruction.
- Accumulated lazy-column skips must not allocate one dense byte per rejected prefix row. Require a
fixed chunk bound (including multiple lazy columns) or a level-only/all-filtered cursor contract.
- Register Parquet Page Cache ranges only for surviving projected Column Chunks, require a stable
file-version key, and assess FileCache, MergeRange, prefetch, requests, and read amplification
together.
- Require counters for Statistics/Dictionary/Bloom pruning, Page Index selected ranges and skipped
rows/pages, raw and filtered rows, dictionary-row filtering, PLAIN direct-predicate batches/rows,
lazy-read savings, cache sources, and remote I/O.
- Differential tests must cover absent/invalid statistics, missing or partial Page Index, mixed
dictionary/plain encoding, Bloom false positives, NULL/NaN/type conversion, cross-Page batches,
nested/repeated columns, multiple Row Groups/Splits, and all/none filtered.
## ORC SARG and Index Filtering
- Trace every pushed predicate from localized `FileScanRequest` through
`build_orc_search_argument()`, ORC `SearchArgumentBuilder`, Stripe selection, SDK RowReader index
pruning, lazy callback filtering, and residual Doris VExpr.
- SARG conversion must be equivalent to the original Doris predicate for every value, including
NULL. Preserve AND/OR/NOT grouping, literal-on-left comparison direction, comparison/IN/NULL
semantics, and wrappers for Runtime Filter, direct-IN, and TopN predicates.
- Verify ORC predicate-domain and literal conversion for integer, floating-point, boolean, string,
binary, varchar, date, decimal, timestamp, and timestamp-instant, including overflow, non-finite
values, signed boundaries, precision/scale, CHAR/VARCHAR, timezone, and NULL.
- Treat schema-evolution casts as SARGable only when truth is preserved in the ORC domain. Review
numeric exactness, decimal widening, date-to-datetime boundary normalization, timestamp precision,
and string/binary casts. Lossy or timezone-changing casts must remain residual.
- For nested predicates, verify struct field name/ordinal traversal and the final ORC type ID.
Unsupported array/map/repeated/missing paths must not target another primitive child.
- Intersect the Split byte window with Stripe ownership before SARG selection, then let ORC RowReader
use row indexes and Bloom filters inside surviving Stripes. SARG must not reintroduce an
out-of-Split Stripe.
- Validate non-adjacent Stripe ranges, all-pruned/no-Stripe cases, file-global row positions,
deletes, and Condition Cache granules after every skipped Stripe or row group.
- Keep SDK filtering and Doris lazy materialization aligned: include the correct filter columns,
preserve selected-row indexes, and decode non-predicate columns only for survivors without
desynchronizing nested vectors or later batches.
- Review SARG cost for large IN lists, deep trees, many Runtime Filters, repeated literal conversion,
Stripe-statistics reads, and SDK index initialization. Build once per reader/Split setup and keep
expensive work out of batch loops.
- Require counters for evaluated/selected groups or Stripes, filtered rows/bytes, groups read,
lazy-filtered rows, I/O, decompression, and decoding. Explain pruning benefit and SARG/index cost.
- Differential tests must cover NULL truth tables, literal-on-left, nested AND/OR/NOT, IN/NOT IN
with NULL, casts, all literal domains, nested structs, unsupported arrays/maps, non-adjacent
Stripes, Split boundaries, row-index strides, Bloom present/absent, and all/none filtered.