dwsmith1983 commented on code in PR #5365:
URL: https://github.com/apache/datafusion-comet/pull/5365#discussion_r4006974272


##########
native/core/src/execution/delta_dv.rs:
##########
@@ -0,0 +1,2096 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Delta Lake deletion-vector decoding and translation into DataFusion
+//! [`ParquetAccessPlan`]s (feature = "delta").
+//!
+//! Wire formats implemented here (from delta-spark's `DeletionVectorStore` /
+//! `RoaringBitmapArray`, v3.3.2):
+//! - On-disk DV file: 1 version byte at the start of the file; at
+//!   `descriptor.offset`: `[i32 BE size][data: size bytes][i32 BE 
CRC32(data)]`.
+//! - `data`: `[i32 LE magic]` then either
+//!   - magic 1681511376 ("native"): `[i32 LE count]`, then per bitmap
+//!     `[i32 LE size][standard 32-bit RoaringBitmap]`, keys implicit (index);
+//!   - magic 1681511377 ("portable", the spec's 64-bit extension): `[i64 LE
+//!     count]`, then per bitmap `[i32 LE key][standard 32-bit RoaringBitmap]`
+//!     with keys ascending -- exactly [`RoaringTreemap`]'s serialized form.
+
+use std::mem::size_of;
+use std::sync::Arc;
+
+use datafusion::datasource::listing::PartitionedFile;
+use 
datafusion::datasource::physical_plan::parquet::metadata::DFParquetMetadata;
+use datafusion::datasource::physical_plan::parquet::{ParquetAccessPlan, 
RowGroupAccess};
+use datafusion::execution::memory_pool::{MemoryConsumer, MemoryReservation};
+use datafusion::execution::runtime_env::RuntimeEnv;
+use futures::{StreamExt, TryStreamExt};
+use object_store::path::Path;
+use object_store::{ObjectStore, ObjectStoreExt};
+use parquet::arrow::arrow_reader::{RowSelection, RowSelector};
+use parquet::file::metadata::{PageIndexPolicy, ParquetMetaData};
+use roaring::{RoaringBitmap, RoaringTreemap};
+
+use crate::execution::operators::ExecutionError;
+use crate::execution::operators::ExecutionError::GeneralError;
+use datafusion_comet_proto::spark_operator::DeltaSparkDvDescriptor;
+
+const NATIVE_MAGIC: i32 = 1681511376;
+const PORTABLE_MAGIC: i32 = 1681511377;
+
+/// Unframe a DV blob read from `descriptor.offset` of a DV file:
+/// `[i32 BE size][data][i32 BE crc]`. Verifies both the size against the
+/// descriptor's `size_in_bytes` and the CRC32 checksum.
+pub fn unframe_dv_blob(blob: &[u8], expected_size: usize) -> Result<&[u8], 
ExecutionError> {
+    if blob.len() < 8 {
+        return Err(GeneralError(format!(
+            "Deletion vector blob too short: {} bytes",
+            blob.len()
+        )));
+    }
+    let size = i32::from_be_bytes(blob[0..4].try_into().unwrap());
+    if size < 0 || size as usize != expected_size {
+        return Err(GeneralError(format!(
+            "Deletion vector size mismatch: file says {size}, descriptor says 
{expected_size}"
+        )));
+    }
+    let end = 4 + size as usize;
+    if blob.len() < end + 4 {
+        return Err(GeneralError(format!(
+            "Deletion vector blob truncated: need {} bytes, have {}",
+            end + 4,
+            blob.len()
+        )));
+    }
+    let data = &blob[4..end];
+    let expected_crc = i32::from_be_bytes(blob[end..end + 
4].try_into().unwrap());
+    let actual_crc = crc32fast::hash(data) as i32;
+    if expected_crc != actual_crc {
+        return Err(GeneralError(
+            "Deletion vector checksum mismatch".to_string(),
+        ));
+    }
+    Ok(data)
+}
+
+/// Deserialize the magic-prefixed RoaringBitmapArray into a 64-bit treemap of
+/// deleted row indexes.
+pub fn deserialize_dv_bitmap(data: &[u8]) -> Result<RoaringTreemap, 
ExecutionError> {
+    if data.len() < 4 {
+        return Err(GeneralError(
+            "Deletion vector bitmap too short for magic number".to_string(),
+        ));
+    }
+    let magic = i32::from_le_bytes(data[0..4].try_into().unwrap());
+    let rest = &data[4..];
+    match magic {
+        PORTABLE_MAGIC => RoaringTreemap::deserialize_from(rest)
+            .map_err(|e| GeneralError(format!("Invalid portable deletion 
vector bitmap: {e}"))),
+        NATIVE_MAGIC => {
+            if rest.len() < 4 {
+                return Err(GeneralError(
+                    "Native deletion vector bitmap missing count".to_string(),
+                ));
+            }
+            let count = i32::from_le_bytes(rest[0..4].try_into().unwrap());
+            if count < 0 {
+                return Err(GeneralError(format!(
+                    "Invalid RoaringBitmapArray length ({count} < 0)"
+                )));
+            }
+            let mut pos = 4usize;
+            let mut treemap = RoaringTreemap::new();
+            for key in 0..count as u64 {
+                if rest.len() < pos + 4 {
+                    return Err(GeneralError(
+                        "Native deletion vector bitmap truncated".to_string(),
+                    ));
+                }
+                let size = i32::from_le_bytes(rest[pos..pos + 
4].try_into().unwrap());
+                pos += 4;
+                if size < 0 || rest.len() < pos + size as usize {
+                    return Err(GeneralError(
+                        "Native deletion vector bitmap truncated".to_string(),
+                    ));
+                }
+                let bitmap = RoaringBitmap::deserialize_from(&rest[pos..pos + 
size as usize])
+                    .map_err(|e| {
+                        GeneralError(format!("Invalid deletion vector 
sub-bitmap: {e}"))
+                    })?;
+                pos += size as usize;
+                for value in bitmap {
+                    treemap.insert((key << 32) | value as u64);
+                }
+            }
+            Ok(treemap)
+        }
+        other => Err(GeneralError(format!(
+            "Unexpected RoaringBitmapArray magic number {other}"
+        ))),
+    }
+}
+
+/// Translate deleted row indexes into a [`ParquetAccessPlan`]: fully-deleted
+/// row groups become `Skip`, untouched groups stay `Scan`, and partially
+/// deleted groups get a `RowSelection` selecting the complement of the deleted
+/// rows. Page-index pruning later INTERSECTS with these selections, so DV
+/// skips and page skips compose.
+pub fn build_access_plan(
+    row_group_row_counts: &[i64],
+    deleted: &RoaringTreemap,
+) -> Result<ParquetAccessPlan, ExecutionError> {
+    let mut plan = ParquetAccessPlan::new_all(row_group_row_counts.len());
+    // Single sweep over the (sorted) deleted row indexes, bucketing by row 
group.
+    let mut deleted_iter = deleted.iter().peekable();
+    let mut group_start = 0u64;
+    for (idx, &num_rows) in row_group_row_counts.iter().enumerate() {
+        // A corrupt footer can report a negative row count. `num_rows as u64` 
would otherwise
+        // wrap it into a huge positive value, silently corrupting every 
row-group boundary
+        // computed from `group_start`/`group_end` below (and therefore which 
deleted row indexes
+        // land in which row group) instead of failing loudly.
+        if num_rows < 0 {
+            return Err(GeneralError(format!(
+                "Parquet footer reports a negative row count ({num_rows}) for 
row group {idx}"
+            )));
+        }
+        let num_rows = num_rows as u64;
+        let group_end = group_start + num_rows;
+        let mut selectors: Vec<RowSelector> = Vec::new();
+        let mut cursor = group_start;
+        let mut deleted_in_group = 0u64;
+        while let Some(&row) = deleted_iter.peek() {
+            if row >= group_end {
+                break;
+            }
+            deleted_iter.next();
+            deleted_in_group += 1;
+            if row > cursor {
+                selectors.push(RowSelector::select((row - cursor) as usize));
+            }
+            // Merge runs of consecutive deleted rows into one skip.
+            match selectors.last_mut() {
+                Some(last) if last.skip => last.row_count += 1,
+                _ => selectors.push(RowSelector::skip(1)),
+            }
+            cursor = row + 1;
+        }
+        if deleted_in_group == num_rows && num_rows > 0 {
+            plan.skip(idx);
+        } else if deleted_in_group > 0 {
+            if group_end > cursor {
+                selectors.push(RowSelector::select((group_end - cursor) as 
usize));
+            }
+            plan.scan_selection(idx, RowSelection::from(selectors));
+        }
+        group_start = group_end;
+    }
+    // A deleted index beyond the file's total row count means the DV does not
+    // belong to this file (stale or corrupted metadata); silently dropping it
+    // would under-apply deletions.
+    if let Some(&row) = deleted_iter.peek() {
+        return Err(GeneralError(format!(
+            "Deletion vector marks row {row} but the file only has 
{group_start} rows"
+        )));
+    }
+    Ok(plan)
+}
+
+/// Verify a decoded deletion vector's row count matches the descriptor's
+/// declared `cardinality`, mirroring Delta's JVM reader
+/// (`StoredBitmap.validateCardinality`). The CRC and framing checks catch
+/// corruption but not a stale, otherwise well-formed bitmap whose row count
+/// no longer matches the descriptor -- that would silently under- or
+/// over-delete rows.
+fn validate_cardinality(
+    file_path: &str,
+    expected: i64,
+    deleted: &RoaringTreemap,
+) -> Result<(), ExecutionError> {
+    let actual = deleted.len();
+    if actual != expected as u64 {
+        return Err(GeneralError(format!(
+            "Deletion vector for {file_path} has cardinality mismatch: 
descriptor says {expected}, decoded bitmap has {actual} deleted rows"
+        )));
+    }
+    Ok(())
+}
+
+/// One data file plus everything needed to apply its deletion vector. The
+/// file's size comes from `file.object_meta.size` (built by the planner from
+/// the proto's `file_size`).
+///
+/// `data_store` and `dv_store` are resolved by the caller *before* entering
+/// the async `attach_access_plans` runtime (see its doc comment): building an
+/// object store is sync I/O that, for a cold S3 authority, internally issues
+/// its own `Handle::block_on` calls, which panics if nested inside another
+/// `block_on`. Resolving up front means this module never constructs a
+/// store itself.
+pub struct DvScanFile {
+    pub file: PartitionedFile,
+    /// Full URL of the data file (proto `file_path`).
+    pub file_path: String,
+    pub dv: Option<DeltaSparkDvDescriptor>,
+    /// Object store for `file_path`, pre-resolved by the caller. Only read
+    /// when `dv` is `Some` (files without a deletion vector never open their
+    /// footer here), but every file carries one so the struct's shape
+    /// doesn't depend on whether a deletion vector is present.
+    pub data_store: Arc<dyn ObjectStore>,
+    /// Store and within-store path for an on-disk deletion vector's absolute
+    /// path, pre-resolved by the caller. `None` when the file has no
+    /// deletion vector or the deletion vector is stored inline.
+    pub dv_store: Option<(Arc<dyn ObjectStore>, Path)>,
+}
+
+/// Execution-memory-pool reservation covering one file's expanded DV row 
selectors across
+/// their *entire* lifetime attached to a scan -- from `build_access_plan`'s 
construction
+/// through DataFusion 54.1's reader normalizing the attached 
[`ParquetAccessPlan`]
+/// (`create_initial_plan`'s deep clone plus `into_overall_row_selection`'s 
combined
+/// `RowSelection`; see [`reader_peak_bytes`]) -- attached to the file's 
[`PartitionedFile`]
+/// extensions alongside its [`ParquetAccessPlan`]. The reservation's lifetime 
is tied to the
+/// `PartitionedFile` it is attached to, so it is released back to the pool 
exactly when the
+/// plan is dropped (query completion or an early-terminated scan), never held 
open longer.
+/// Newtype-wrapped so it occupies its own slot in the multi-slot, type-keyed 
`extensions` map
+/// (`datafusion_common::extensions::Extensions`) alongside the plan, rather 
than a bare
+/// `MemoryReservation` colliding with one some other extension might attach.
+pub struct DvAccessPlanReservation(pub MemoryReservation);
+
+/// Total number of [`RowSelector`]s materialized across `plan`'s per-row-group
+/// selections (`RowGroupAccess::Selection`); `Scan`/`Skip` row groups
+/// contribute none. An alternating deleted/retained bitmap produces one
+/// non-coalescing selector per row (see [`reader_peak_bytes`]'s doc comment
+/// for the worst-case accounting), so this count -- not the deletion
+/// vector's cardinality -- is the thing that must be bounded and reserved
+/// against the execution memory pool.
+fn total_selectors(plan: &ParquetAccessPlan) -> usize {
+    plan.inner()
+        .iter()
+        .map(|access| match access {
+            RowGroupAccess::Selection(selection) => selection.iter().count(),
+            _ => 0,
+        })
+        .sum()
+}
+
+/// Multiplier bounding the peak allocation live *during construction* of one
+/// file's [`RowSelection`]s, relative to the conservative selector-count
+/// bound `S = 2 * cardinality + num_row_groups` (one non-coalescing selector
+/// per deleted row in the worst-case alternating pattern, doubled, plus up to
+/// one extra boundary selector per row group). Split `S` into `r`, the
+/// selectors already retained from row groups `build_access_plan` has
+/// finished, and `c`, the selectors accumulated so far in the current row
+/// group's source `Vec`; `r` and `c` partition the selectors counted toward
+/// `S`, so `r + c <= S` always. While the current group is being built, the
+/// `Vec`'s doubling growth strategy can leave its backing allocation at up to
+/// `2 * c` (the next power-of-two capacity above `c`). Once the group
+/// finishes, `RowSelection::from(Vec)` (parquet's `FromIterator` impl,
+/// `with_capacity` + copy) builds a second, separate `Vec` of size `c` from
+/// that source while the source is still alive, so at the moment the copy
+/// begins, the retained selectors, the current group's doubled source `Vec`,
+/// and the copy are all live simultaneously: `r + 2c + c = r + 3c`. Since
+/// `r >= 0`, `r + 3c <= 3r + 3c = 3(r + c) <= 3S`. 3x covers that peak.
+const CONSTRUCTION_PEAK_FACTOR: usize = 3;
+
+/// Upper bound on how much larger a `Vec`'s backing allocation can be than 
its element count
+/// after being built by repeated pushes: `std`'s doubling growth strategy 
never leaves a `Vec`
+/// of `n` elements with a backing allocation larger than the next power of 
two above `n`, which
+/// is at most `2 * n` for any `n >= 1`.
+const VEC_GROWTH_CAPACITY_FACTOR: usize = 2;
+
+/// `RawVec`'s minimum non-zero capacity for element sizes `<= 1024` bytes 
([`RowSelector`] is
+/// 16 bytes on 64-bit platforms: a `usize` row count plus a padded `bool`). 
Applied once per
+/// row group (or per contiguous run of row groups) a fresh 
`from_fn`/`FlatMap`-driven `Vec`
+/// gets built for (see [`reader_peak_bytes`]), so even a group or run whose 
true selector count
+/// is tiny still pays this floor.
+const MIN_VEC_CAPACITY_SELECTORS: usize = 4;
+
+/// Conservative upper bound, in bytes, on the peak allocation live while 
DataFusion 54.1's
+/// reader normalizes one file's attached [`ParquetAccessPlan`] -- the 
allocation this module's
+/// steady-state reservation must cover, not merely the plan's own retained 
selector bytes.
+/// THREE allocations can be live simultaneously by the time 
`into_overall_row_selection`
+/// returns, not two -- the clone is only exact when page-index pruning never 
touches it:
+///
+/// 1. **Attached original** (`selectors`, exact): `create_initial_plan` 
deep-clones the
+///    attached plan while the original remains reachable from the file's 
`extensions` until
+///    the scan consumes it. The ORIGINAL's own selector `Vec`s are exact -- a 
coalesced
+///    [`RowSelection`] built via `RowSelection::from(Vec<RowSelector>)` (what
+///    `build_access_plan` uses) has no excess capacity, because that 
conversion is a plain
+///    `with_capacity(len)` copy, not a `size_hint`-blind fold.
+/// 2. **The clone, possibly capacity-inflated** (`<= 
VEC_GROWTH_CAPACITY_FACTOR * selectors +
+///    MIN_VEC_CAPACITY_SELECTORS * num_row_groups`): if page-index pruning 
fires
+///    (`PagePruningAccessPlanFilter`; `access_plan.rs`'s `scan_selection` on 
a row group that
+///    already carries a `RowGroupAccess::Selection` calls 
`existing.intersection(&page_derived)`
+///    -- `RowSelection::intersection` -> `intersect_row_selections`), it 
replaces the CLONE's
+///    per-row-group selection with that intersection's output. 
`intersect_row_selections` is
+///    ANOTHER `from_fn` generator with `size_hint() == (0, None)`, so each 
intersected row
+///    group's backing `Vec` starts at `with_capacity(0)` and doubles as it 
grows, independent
+///    of whatever capacity the pre-intersection selection had. This inflated 
clone is still
+///    live when `into_overall_row_selection` later moves its buffer. Term 1's 
exactness
+///    guarantee holds for the ORIGINAL always, and for the clone only when 
page-index pruning
+///    never fires against it -- once it does, the clone must be charged at 
the SAME
+///    growth-capped bound as a fresh combined-selection `Vec` (term 3), 
summed once per row
+///    group rather than once per run, since each row group's `Selection` is 
intersected
+///    independently.
+/// 3. **Per-run combined-selection allocation** (`<= 
VEC_GROWTH_CAPACITY_FACTOR * (selectors +
+///    num_row_groups) + MIN_VEC_CAPACITY_SELECTORS * num_row_groups`): 
`into_overall_row_selection`
+///    collects each contiguous run of row groups' selectors into a *new* 
`RowSelection` via a
+///    `FlatMap` whose `size_hint().0 == 0`, so that run's `Vec` starts at 
`with_capacity(0)`
+///    and doubles as it grows -- capping its backing allocation at
+///    `max(MIN_VEC_CAPACITY_SELECTORS, next_power_of_two(len))`, which is at 
most
+///    `MIN_VEC_CAPACITY_SELECTORS + VEC_GROWTH_CAPACITY_FACTOR * len` for a 
run of `len`
+///    selectors. `len` is at most that run's share of `selectors` plus one 
boundary selector
+///    per `RowGroupAccess::Scan` row group in the run (`Scan` always 
contributes exactly one
+///    `RowSelector::select(num_rows)`; see `access_plan.rs`'s 
`into_overall_row_selection`).
+///    Summing across at most `num_row_groups` runs (each spans >= 1 row 
group) bounds the total
+///    at `VEC_GROWTH_CAPACITY_FACTOR * selectors + 
(MIN_VEC_CAPACITY_SELECTORS +
+///    VEC_GROWTH_CAPACITY_FACTOR) * num_row_groups`.
+///
+/// Summing all three terms and converting to bytes: `((1 + 2 * 
VEC_GROWTH_CAPACITY_FACTOR) *
+/// selectors + (2 * MIN_VEC_CAPACITY_SELECTORS + VEC_GROWTH_CAPACITY_FACTOR) 
* num_row_groups)
+/// * size_of::<RowSelector>()` -- with the constants above, `(5 * selectors + 
10 *
+///   num_row_groups) * size_of::<RowSelector>()`. Checked against two 
measured worst cases:
+///
+/// - No page-index pruning (the original P2 report; term 2 stays exact): one 
2,000,000-row
+///   group, 1,000,000 alternating deletions, `selectors = 2,000,000`. 
Measured allocator peak
+///   97,554,457 B; the byte-for-byte accounting for the attached original 
plus the (here,
+///   exact) clone plus the inflated combined selection explains 97,554,432 B 
of that, a 25 B
+///   residue we did not attribute. This bound gives 160,000,160 B -- much 
looser here because
+///   it must also cover the next case, where the clone is NOT exact.
+/// - Page-index pruning fires against the clone: one 1,048,577-row group, 
`selectors =
+///   1,048,577`. Measured peak 83,886,096 B; this bound gives 83,886,320 B (a 
224 B, <1%
+///   margin -- deliberately tight, since this is the case that drives the 
bound).
+///
+/// Uses checked arithmetic throughout: a selector or row-group count large 
enough to overflow
+/// `usize` indicates a corrupted or malicious input, reported as a clean 
error rather than
+/// panicking.
+fn reader_peak_bytes(selectors: usize, num_row_groups: usize) -> Result<usize, 
ExecutionError> {
+    let overflow = || {
+        GeneralError(format!(
+            "Deletion vector reader-peak bound overflowed for {selectors} 
selectors and \
+             {num_row_groups} row groups"
+        ))
+    };
+    // Term 1: the attached original -- exact, untouched by page-index pruning 
(only the clone
+    // is ever intersected; see the doc comment above).
+    let attached_term = selectors;
+    // Term 2: the clone, bounded as if page-index pruning DID fire against 
every row group
+    // (safe even when it doesn't: term 2's bound is always >= `selectors`, so 
it never
+    // undershoots the exact case either).
+    let clone_growth = selectors
+        .checked_mul(VEC_GROWTH_CAPACITY_FACTOR)
+        .ok_or_else(overflow)?;
+    let clone_floor = num_row_groups
+        .checked_mul(MIN_VEC_CAPACITY_SELECTORS)
+        .ok_or_else(overflow)?;
+    let clone_term = 
clone_growth.checked_add(clone_floor).ok_or_else(overflow)?;
+    // Term 3: into_overall_row_selection's per-run combined-selection 
allocation.
+    let combined_growth = selectors
+        .checked_mul(VEC_GROWTH_CAPACITY_FACTOR)
+        .ok_or_else(overflow)?;
+    let combined_floor = num_row_groups
+        .checked_mul(MIN_VEC_CAPACITY_SELECTORS + VEC_GROWTH_CAPACITY_FACTOR)
+        .ok_or_else(overflow)?;
+    let combined_term = combined_growth
+        .checked_add(combined_floor)
+        .ok_or_else(overflow)?;
+
+    let selector_bound = attached_term
+        .checked_add(clone_term)
+        .and_then(|sum| sum.checked_add(combined_term))
+        .ok_or_else(overflow)?;
+    selector_bound
+        .checked_mul(size_of::<RowSelector>())
+        .ok_or_else(overflow)
+}
+
+/// Upper bound, in [`RowSelector`]s, on how many extra selectors the parquet 
reader's
+/// page-index pruning can add on top of the deletion vector's own selection 
when normalizing
+/// one file, from that file's already-fetched [`ParquetMetaData`].
+///
+/// `intersect_row_selections` (parquet's `selection.rs`), which combines a 
page-pruning
+/// selection with the deletion vector's selection, is a `from_fn` generator 
whose
+/// `size_hint()` is `(0, None)`: for inputs of length `a` and `b`, its output 
can have up to
+/// `a + b` selectors -- longer than either input. Bounding the page-pruning 
side of that sum
+/// requires knowing how many selectors a page-index-derived selection could 
produce: at most
+/// two per data page (one skip, one select, in the worst case of alternating 
page-level
+/// pruning decisions), summed over every column of every row group.
+///
+/// Returns `0` when `metadata` carries no offset index 
(`metadata.offset_index()` is `None`).
+/// This is provably safe, not merely a convenient default: page-index pruning 
cannot produce a
+/// page-level selection without the offset index to locate pages by, so there 
are no
+/// page-pruning selectors to bound. The offset index is fetched with
+/// `PageIndexPolicy::Optional` from the same `FileMetadataCache` entry the 
scan's reader later
+/// reopens (see [`attach_access_plan`]'s footer-fetch comment), so this 
function observes
+/// exactly what the reader will see.
+///
+/// Uses checked arithmetic throughout for the same reason as 
[`admission_bound_bytes`].
+fn page_selection_bound_selectors(metadata: &ParquetMetaData) -> Result<usize, 
ExecutionError> {
+    let Some(offset_index) = metadata.offset_index() else {
+        return Ok(0);
+    };
+    let overflow = || {
+        GeneralError(
+            "Deletion vector page-selection bound overflowed while summing 
offset-index page \
+             locations"
+                .to_string(),
+        )
+    };
+    let mut total_page_locations = 0usize;
+    for row_group in offset_index {
+        for column in row_group {
+            total_page_locations = total_page_locations
+                .checked_add(column.page_locations().len())
+                .ok_or_else(overflow)?;
+        }
+    }
+    total_page_locations.checked_mul(2).ok_or_else(overflow)
+}
+
+/// Execution-memory-pool admission bound, in bytes, for one file's 
deletion-vector access
+/// plan -- reserved *before* calling `build_access_plan` (see 
[`attach_access_plan`]'s
+/// pre-reserve call site) to cover the larger of two peaks live at different 
points in the
+/// plan's lifetime. In practice the reader-normalization peak below dominates 
the construction
+/// peak unconditionally for any non-trivial input (`reader_peak_bytes(S, G) = 
(5S + 10G) *
+/// size_of::<RowSelector>()` always exceeds `CONSTRUCTION_PEAK_FACTOR * S *
+/// size_of::<RowSelector>() = 3S * size_of::<RowSelector>()` once `S >= 1`, 
since the `5S` term
+/// alone already exceeds `3S`); the construction term is retained as a 
documented floor rather
+/// than dropped, since it is cheap to compute and keeps this bound correct 
even if the reader's
+/// growth factors ever shrink below construction's.
+///
+/// - **Construction peak** (`CONSTRUCTION_PEAK_FACTOR * S`, see that 
constant's doc comment):
+///   live while `build_access_plan` builds the plan's `RowSelection`s. 
Construction's
+///   transient allocations fully unwind before `build_access_plan` returns, 
so this peak never
+///   overlaps the reader-normalization peak below.
+/// - **Reader-normalization peak** (`reader_peak_bytes(S + 
page_bound_selectors,
+///   num_row_groups)`, see that function): live later, once DataFusion's 
reader normalizes the
+///   attached plan. `S = 2 * cardinality + num_row_groups` is the same 
conservative bound on
+///   the plan's final retained selector count used for the construction peak 
-- it provably
+///   bounds `R = total_selectors(&plan)` (`R <= S`, from `build_access_plan`'s
+///   one-non-coalescing-selector-per-deleted-row worst case plus one boundary 
selector per row
+///   group), so `S + page_bound_selectors` bounds `R` after page-index 
inflation the same way
+///   `S` bounds `R` before it.
+///
+/// These two peaks never overlap in time, so `max` -- not `sum` -- is the 
correct combinator:
+/// reserving their sum would over-reserve for no safety benefit.
+///
+/// Deliberately not clamped by the file's total row count here, unlike the 
reader-peak target
+/// `attach_access_plan` resizes down to after construction (see that call 
site): `S`'s
+/// `+ num_row_groups` boundary term is a worst-case padding margin that can 
legitimately exceed
+/// the total row count for a small, heavily-deleted file, and admission 
sizing has no actual
+/// retained-selector count yet to clamp against -- only after construction, 
once `R` is known,
+/// is clamping to the total row count both meaningful and strictly tighter. 
Leaving this bound
+/// unclamped only ever makes admission more conservative, never less safe.
+///
+/// Uses checked arithmetic throughout: a cardinality, row-group count, or 
page bound large
+/// enough to overflow `usize` while computing this bound indicates a 
corrupted or malicious
+/// descriptor, reported as a clean error rather than panicking.
+fn admission_bound_bytes(
+    cardinality: i64,
+    num_row_groups: usize,
+    page_bound_selectors: usize,
+) -> Result<usize, ExecutionError> {
+    let overflow = || {
+        GeneralError(format!(
+            "Deletion vector admission bound overflowed for cardinality 
{cardinality}, \
+             {num_row_groups} row groups, and page bound 
{page_bound_selectors} selectors"
+        ))
+    };
+    let cardinality_usize = usize::try_from(cardinality).map_err(|_| 
overflow())?;
+    // S: the conservative bound on the plan's final *retained* selector count 
(what
+    // `total_selectors(&plan)` cannot exceed) -- unchanged from the 
pre-existing
+    // construction-only bound this function replaces.
+    let s = cardinality_usize
+        .checked_mul(2)
+        .and_then(|doubled| doubled.checked_add(num_row_groups))
+        .ok_or_else(overflow)?;
+
+    let construction_bytes = s
+        .checked_mul(size_of::<RowSelector>())
+        .and_then(|bytes| bytes.checked_mul(CONSTRUCTION_PEAK_FACTOR))
+        .ok_or_else(overflow)?;
+
+    let s_plus_page = 
s.checked_add(page_bound_selectors).ok_or_else(overflow)?;
+    let reader_bytes = reader_peak_bytes(s_plus_page, num_row_groups)?;
+
+    Ok(construction_bytes.max(reader_bytes))
+}
+
+/// Upper bound on concurrent DV-blob and footer fetches per partition. Both
+/// are small ranged reads, so a modest fan-out hides object-store latency
+/// without flooding the store client.
+const DV_FETCH_CONCURRENCY: usize = 8;
+
+/// Called via `block_on` at plan-creation time on the executor task: DV blobs
+/// are small ranged reads and footers are needed to learn row-group
+/// boundaries. Files are fetched concurrently (bounded by
+/// [`DV_FETCH_CONCURRENCY`]) with input order preserved. Footer fetches go
+/// through the scan's shared FileMetadataCache, so the scan's subsequent open
+/// of the same file is served from cache. That reuse relies on each input
+/// [`PartitionedFile`] being returned as-is (only `with_extension` applied),
+/// never rebuilt: the cache entry is keyed by this exact `object_meta` and the
+/// scan later looks it up through the same struct.
+///
+/// Deliberately takes no object-store options map and imports no
+/// store-construction helper: every [`DvScanFile`] arrives with its stores
+/// already resolved by the caller (see its doc comment), so this async path
+/// structurally cannot build an object store -- only `runtime_env` is still
+/// threaded through, for the shared `FileMetadataCache` and (per file) the
+/// execution `MemoryPool` each expanded access plan's row selectors are
+/// reserved against -- see [`DvAccessPlanReservation`].
+pub async fn attach_access_plans(
+    runtime_env: Arc<RuntimeEnv>,
+    files: Vec<DvScanFile>,
+) -> Result<Vec<PartitionedFile>, ExecutionError> {
+    futures::stream::iter(files)
+        .map(|scan_file| attach_access_plan(Arc::clone(&runtime_env), 
scan_file))
+        .buffered(DV_FETCH_CONCURRENCY)
+        .try_collect()
+        .await
+}
+
+/// Resolve one file's deletion vector into an attached [`ParquetAccessPlan`];
+/// files without a DV pass through untouched.
+async fn attach_access_plan(
+    runtime_env: Arc<RuntimeEnv>,
+    scan_file: DvScanFile,
+) -> Result<PartitionedFile, ExecutionError> {
+    let DvScanFile {
+        file,
+        file_path,
+        dv,
+        data_store,
+        dv_store,
+    } = scan_file;
+    let dv = match dv {
+        Some(dv) => dv,
+        None => return Ok(file),
+    };
+    // Delta's canonical `DeletionVectorDescriptor.EMPTY`: inline storage, 
empty
+    // payload, size 0, cardinality 0. Spark's reader returns all rows for it;
+    // decoding would fail (the empty payload is too short for a magic
+    // number), so pass the file through unchanged before attempting to read 
it.
+    if dv.cardinality == 0 && dv.size_in_bytes == 0 {
+        return Ok(file);
+    }
+    if dv.size_in_bytes < 0 {
+        return Err(GeneralError(format!(
+            "Deletion vector for {file_path} has negative size {}",
+            dv.size_in_bytes
+        )));
+    }
+    if dv.cardinality < 0 {
+        return Err(GeneralError(format!(
+            "Deletion vector for {file_path} has negative cardinality {}",
+            dv.cardinality
+        )));
+    }
+
+    let data: Vec<u8> = if let Some(inline) = dv.inline_data {

Review Comment:
   > Could it compare the length before `deserialize_dv_bitmap`?
   
   Added `check_inline_payload_size`, called before decoding; the error names 
the payload length and the descriptor size. A test covers the mismatch and the 
matching case.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to