peter-toth opened a new pull request, #59715:
URL: https://github.com/apache/spark/pull/59715

   ### What changes were proposed in this pull request?
   
   A follow-up to SPARK-59620, which lets the V1 Parquet reader apply a join's 
runtime bloom filter while it reads. This PR gives data source V2 scans the 
same. A connector such as Iceberg can then read the bloom's key columns first, 
and skip the other columns of the rows the bloom rejects.
   
   The API, all `@Evolving`:
   - `SupportsStorageFilters`, a `Scan` mix-in. `storageFilterAttributes()` 
names the columns the scan's readers can evaluate a storage filter on.
   - `StorageFilter`, an opaque predicate that Spark implements. It has 
`references()`, `eval(InternalRow)` and `int select(ColumnarBatch, int[] 
selected)`, which writes the positions of the rows it keeps. The default 
`select` loops over `eval`.
   - `StorageFilterNotAppliedException`, error condition 
`STORAGE_FILTER_NOT_APPLIED`. `eval` and `select` throw it when the filter 
cannot be applied to a row.
   - `StorageFilterRequest`, with `filter()` and `appendsCheckedColumn()`.
   - `PartitionReaderFactory.createReader(partition, request)` and 
`createColumnarReader(partition, request)`. The factory of a scan that declares 
storage-filter columns must implement the one for its read mode.
   - `Scan.toBatch` now says it may be called more than once for a scan offered 
a storage filter, as for runtime filtering.
   
   Planning and execution:
   - `DataSourceV2Strategy` offers a top-level runtime bloom over declared 
columns, under the new `spark.sql.sources.v2.storageFilterPushdown.enabled`. 
The per-conjunct rules are shared with `FileSourceStrategy` through the new 
`StorageFilterPlanning`.
   - The bloom stays in the post-scan `Filter`. Where whole-stage codegen is on 
and the scan's output still fits it with the column, the scan gets a boolean 
checked column last, and the `Filter` evaluates the bloom as `IF(checked, true, 
bloom)`. A `Project` drops the column.
   - `BatchScanExec` ANDs the storage filters into one `StorageFilter`, and 
`DataSourceRDD` hands each task's readers a `StorageFilterRequest`. Each task 
deserializes its own filter.
   
   The design decisions:
   - **Applying the filter is optional per unit,** such as a file or a row 
group. Spark keeps the predicate above the scan. A reader that cannot use it on 
a unit reads that unit as a plain read does, and marks its rows unchecked. When 
the filter throws `StorageFilterNotAppliedException`, the reader reads the rest 
of the unit that way. Spark's filter throws it for an error evaluating it, but 
not for a task kill or a fatal error.
   - **The checked column says which rows the reader filtered.** It is 
requested in the task, through the request, since the scan's read schema is 
fixed before `InjectRuntimeFilter` adds the bloom. It is requested only where 
dropping it copies no extra row. A columnar scan out of whole-stage codegen 
would copy every row in the `Project` that drops it. A row-based V2 scan has 
that `Project` anyway, for the UnsafeRow conversion.
   - **The filter is delivered in the task, not at planning.** The reader 
factory does not carry it. The same API can later hand each partition its own 
bloom, for storage-partitioned joins.
   - **One combined filter per scan, opaque to the connector.** Spark 
implements it, so a connector does not reproduce Spark's bloom format or hash. 
Only runtime blooms are offered for now. Offering more is a planner change.
   - **`select` returns positions, not a bitmap.** Iceberg's Spark reader 
already wraps a filtered batch as `ColumnVectorWithFilter(delegate, int[] 
rowIdMapping)`.
   
   `BatchScanExec` gets a `storageFilters` field, so the positional patterns 
over it in eight test files gain one `_`.
   
   ### Why are the changes needed?
   
   SPARK-59620 saves IO only in Spark's own Parquet reader. A table read 
through a V2 connector, such as Iceberg, still reads every projected column of 
the rows a runtime bloom then drops. With this API a connector can do what the 
V1 reader does. The Iceberg side is a follow-up in that project.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes:
   - a new public connector API, listed above;
   - with `spark.sql.sources.v2.storageFilterPushdown.enabled` on, which is off 
by default:
     - a V2 scan that implements it lists the offered filters under 
`StorageFilters` in its description and in `EXPLAIN FORMATTED`;
     - its output gains the checked column, which a `Project` drops;
     - the post-scan `Filter` skips the bloom on the rows the reader marks 
checked.
   
   Nothing changes for a scan that does not implement `SupportsStorageFilters`.
   
   ### How was this patch tested?
   
   New `StorageFilterPushDownSuite`, over a V2 test source whose reader applies 
the filter row by row or through `select`:
   - a row-based and a columnar scan, with AQE on and off, each get the bloom 
and the checked column;
   - the `Filter` skips the bloom on the rows a reader marks checked, even rows 
it did not filter;
   - a reader that gives the filter up returns its rows unchecked, and the 
`Filter` applies the bloom to them;
   - a filter that throws `StorageFilterNotAppliedException` is given up, and 
its rows come back unchecked;
   - a scan too wide for whole-stage codegen with the column, or without 
whole-stage codegen, gets the bloom without it;
   - nothing is offered with the conf off, to a scan that declares no column or 
only another one, or to a scan with a column named like the checked column;
   - a non-deterministic conjunct is not offered;
   - the filters show in the scan description and in `EXPLAIN FORMATTED`, and 
keep a plain scan's canonicalized plan apart, while a twin with fresh exprIds 
keeps the same result.
   
   New `StorageFilterImplSuite`: the positions `select` writes, the not-applied 
answer, that a task kill propagates, and the internal error for a column 
outside the scan output. `ParquetStorageFilterSuite` passes over the shared 
planning rules.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code with Claude Opus 5.5
   


-- 
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