Skip to content

Commit bd513ec

Browse files
committed
Address PR feedback.
1 parent dbb8f3b commit bd513ec

4 files changed

Lines changed: 79 additions & 135 deletions

File tree

‎datafusion/datasource-parquet/src/opener.rs‎

Lines changed: 62 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ use datafusion_common::encryption::FileDecryptionProperties;
4444
use datafusion_common::stats::Precision;
4545
use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
4646
use datafusion_common::{
47-
ColumnStatistics, DataFusionError, Result, ScalarValue, Statistics, exec_err,
48-
not_impl_err,
47+
ColumnStatistics, DataFusionError, HashSet, Result, ScalarValue, Statistics,
48+
exec_err, not_impl_err,
4949
};
5050
use datafusion_datasource::{PartitionedFile, TableSchema};
5151
use datafusion_physical_expr::expressions::Column;
@@ -1411,7 +1411,7 @@ fn append_fields(base: &SchemaRef, extra: &[FieldRef]) -> SchemaRef {
14111411
/// stringly-typed allowlist here.
14121412
fn validate_supported_virtual_columns(virtual_columns: &[FieldRef]) -> Result<()> {
14131413
for field in virtual_columns {
1414-
ParquetVirtualColumn::try_from(Arc::clone(field))?;
1414+
ParquetVirtualColumn::try_from(field)?;
14151415
}
14161416
Ok(())
14171417
}
@@ -1425,27 +1425,24 @@ fn validate_supported_virtual_columns(virtual_columns: &[FieldRef]) -> Result<()
14251425
/// inside a row filter. Silently dropping such a predicate would produce wrong
14261426
/// results, so we fail loudly here.
14271427
///
1428-
/// Callers constructing a `ParquetSource` should rely on
1429-
/// `FileSource::try_pushdown_filters` (invoked by the `FilterPushdown` physical
1430-
/// optimizer rule) to classify filters correctly: filters referencing virtual
1431-
/// columns are reported as `PushedDown::No` and stay in the enclosing
1432-
/// `FilterExec`, while the scan emits the virtual columns for the filter to
1433-
/// consume. Callers building plans manually must keep the `FilterExec` above
1434-
/// the `DataSourceExec` themselves rather than setting the predicate on
1435-
/// `ParquetSource` with pushdown enabled.
1428+
/// `ParquetSource::try_pushdown_filters` already classifies virtual-column
1429+
/// filters as `PushedDown::No` so the `FilterPushdown` optimizer leaves them
1430+
/// above the scan; this check is defense-in-depth for callers that build plans
1431+
/// manually and set `with_pushdown_filters(true)` alongside a predicate
1432+
/// referencing virtual columns.
14361433
fn validate_predicate_does_not_reference_virtual_columns(
14371434
predicate: &Arc<dyn PhysicalExpr>,
14381435
virtual_columns: &[FieldRef],
14391436
) -> Result<()> {
14401437
if virtual_columns.is_empty() {
14411438
return Ok(());
14421439
}
1440+
let virtual_names: HashSet<&str> =
1441+
virtual_columns.iter().map(|f| f.name().as_str()).collect();
14431442
let mut offender: Option<String> = None;
14441443
predicate.apply(|node: &Arc<dyn PhysicalExpr>| {
14451444
if let Some(column) = node.downcast_ref::<Column>()
1446-
&& virtual_columns
1447-
.iter()
1448-
.any(|f| f.name().as_str() == column.name())
1445+
&& virtual_names.contains(column.name())
14491446
{
14501447
offender = Some(column.name().to_string());
14511448
return Ok(TreeNodeRecursion::Stop);
@@ -1455,14 +1452,9 @@ fn validate_predicate_does_not_reference_virtual_columns(
14551452
if let Some(name) = offender {
14561453
return not_impl_err!(
14571454
"Predicate references virtual column '{name}' while \
1458-
pushdown_filters=true. DataFusion cannot push predicates on \
1459-
virtual columns into the Parquet reader: arrow-rs's RowFilter \
1460-
operates on a ProjectionMask over file leaves, and virtual \
1461-
columns (e.g. row_number) are synthesized by the reader after \
1462-
filter evaluation. Either leave the filter in a FilterExec above \
1463-
the scan (this is what `ParquetSource::try_pushdown_filters` / \
1464-
the FilterPushdown optimizer rule will do automatically), or \
1465-
disable pushdown via `with_pushdown_filters(false)`."
1455+
pushdown_filters=true; predicates on virtual columns must be \
1456+
evaluated above the scan. Disable filter pushdown or leave the \
1457+
filter above the scan."
14661458
);
14671459
}
14681460
Ok(())
@@ -3218,119 +3210,90 @@ mod test {
32183210
);
32193211
}
32203212

3221-
#[tokio::test]
3222-
async fn test_row_index_predicate_pushdown_mixed_or_errors() {
3223-
// Mixed `row_number = 2 OR value = 4` with pushdown_filters=true.
3224-
// Silently dropping this in the scan would return all 5 rows; we
3225-
// instead surface a guidance-rich error so callers realize the
3226-
// predicate needs to stay above the scan.
3227-
let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
3228-
let (file_schema, data_size) =
3229-
write_grouped_file(&store, "pushdown_mixed.parquet", 1, 5).await;
3230-
3213+
/// Build a morselizer + file for a 5-row single-row-group parquet at
3214+
/// `path`, with a single `row_number` virtual column and the given
3215+
/// physical predicate applied to
3216+
/// `table_schema = [value(0), row_number(1)]`.
3217+
async fn build_pushdown_morselizer(
3218+
store: &Arc<dyn ObjectStore>,
3219+
path: &str,
3220+
predicate_expr: datafusion_expr::Expr,
3221+
pushdown_filters: bool,
3222+
) -> (ParquetMorselizer, PartitionedFile) {
3223+
let (file_schema, data_size) = write_grouped_file(store, path, 1, 5).await;
32313224
let rn_field = row_number_field("row_number", false);
32323225
let table_schema = TableSchema::from_file_schema(Arc::clone(&file_schema))
32333226
.with_virtual_columns(vec![Arc::clone(&rn_field)]);
32343227
let projection =
32353228
ProjectionExprs::from_indices(&[0, 1], table_schema.table_schema());
3236-
3237-
let expr = col("row_number")
3238-
.eq(lit(2i64))
3239-
.or(col("value").eq(lit(4i64)));
3240-
let predicate = logical2physical(&expr, table_schema.table_schema());
3229+
let predicate =
3230+
logical2physical(&predicate_expr, table_schema.table_schema());
32413231

32423232
let morselizer = ParquetMorselizerBuilder::new()
3243-
.with_store(Arc::clone(&store))
3233+
.with_store(Arc::clone(store))
32443234
.with_table_schema(table_schema)
32453235
.with_projection(projection)
32463236
.with_predicate(predicate)
3247-
.with_pushdown_filters(true)
3237+
.with_pushdown_filters(pushdown_filters)
32483238
.build();
32493239

3250-
let file = PartitionedFile::new(
3251-
"pushdown_mixed.parquet".to_string(),
3252-
u64::try_from(data_size).unwrap(),
3253-
);
3240+
let file =
3241+
PartitionedFile::new(path.to_string(), u64::try_from(data_size).unwrap());
3242+
(morselizer, file)
3243+
}
3244+
3245+
#[tokio::test]
3246+
async fn test_row_index_predicate_pushdown_mixed_or_errors() {
3247+
// Silent drop in the scan would return all 5 rows; we want a loud
3248+
// error instead.
3249+
let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
3250+
let expr = col("row_number")
3251+
.eq(lit(2i64))
3252+
.or(col("value").eq(lit(4i64)));
3253+
let (morselizer, file) =
3254+
build_pushdown_morselizer(&store, "pushdown_mixed.parquet", expr, true)
3255+
.await;
3256+
32543257
let err = morselizer.plan_file(file).unwrap_err();
32553258
let msg = err.to_string();
32563259
assert!(
32573260
msg.contains("row_number"),
32583261
"error should name the offending virtual column, got: {msg}"
32593262
);
32603263
assert!(
3261-
msg.contains("arrow-rs") && msg.contains("RowFilter"),
3262-
"error should explain the arrow-rs limitation, got: {msg}"
3263-
);
3264-
assert!(
3265-
msg.contains("FilterExec")
3266-
|| msg.contains("FilterPushdown")
3267-
|| msg.contains("try_pushdown_filters"),
3268-
"error should point at the correct API path, got: {msg}"
3264+
msg.contains("virtual column") && msg.contains("pushdown"),
3265+
"error should explain the virtual-column + pushdown context, \
3266+
got: {msg}"
32693267
);
32703268
}
32713269

32723270
#[tokio::test]
32733271
async fn test_row_index_predicate_pushdown_virtual_only_errors() {
3274-
// Predicate referencing only a virtual column with pushdown_filters=true.
32753272
let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
3276-
let (file_schema, data_size) =
3277-
write_grouped_file(&store, "pushdown_virtual_only.parquet", 1, 5).await;
3278-
3279-
let rn_field = row_number_field("row_number", false);
3280-
let table_schema = TableSchema::from_file_schema(Arc::clone(&file_schema))
3281-
.with_virtual_columns(vec![Arc::clone(&rn_field)]);
3282-
let projection =
3283-
ProjectionExprs::from_indices(&[0, 1], table_schema.table_schema());
3284-
32853273
let expr = col("row_number").eq(lit(2i64));
3286-
let predicate = logical2physical(&expr, table_schema.table_schema());
3287-
3288-
let morselizer = ParquetMorselizerBuilder::new()
3289-
.with_store(Arc::clone(&store))
3290-
.with_table_schema(table_schema)
3291-
.with_projection(projection)
3292-
.with_predicate(predicate)
3293-
.with_pushdown_filters(true)
3294-
.build();
3274+
let (morselizer, file) = build_pushdown_morselizer(
3275+
&store,
3276+
"pushdown_virtual_only.parquet",
3277+
expr,
3278+
true,
3279+
)
3280+
.await;
32953281

3296-
let file = PartitionedFile::new(
3297-
"pushdown_virtual_only.parquet".to_string(),
3298-
u64::try_from(data_size).unwrap(),
3299-
);
33003282
let err = morselizer.plan_file(file).unwrap_err();
33013283
assert!(err.to_string().contains("row_number"));
33023284
}
33033285

33043286
#[tokio::test]
33053287
async fn test_row_index_predicate_allowed_when_pushdown_disabled() {
3306-
// Same predicate, but pushdown_filters=false. The predicate is only
3307-
// used for stats pruning (which is a no-op for row_number) and must
3308-
// NOT error.
3288+
// Guards the `pushdown_filters=false` path: the predicate is only
3289+
// used for stats pruning (a no-op for row_number) and must not
3290+
// trip the pushdown-guard error.
33093291
let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
3310-
let (file_schema, data_size) =
3311-
write_grouped_file(&store, "pushdown_off.parquet", 1, 5).await;
3312-
3313-
let rn_field = row_number_field("row_number", false);
3314-
let table_schema = TableSchema::from_file_schema(Arc::clone(&file_schema))
3315-
.with_virtual_columns(vec![Arc::clone(&rn_field)]);
3316-
let projection =
3317-
ProjectionExprs::from_indices(&[0, 1], table_schema.table_schema());
3318-
33193292
let expr = col("row_number").eq(lit(2i64));
3320-
let predicate = logical2physical(&expr, table_schema.table_schema());
3321-
3322-
let morselizer = ParquetMorselizerBuilder::new()
3323-
.with_store(Arc::clone(&store))
3324-
.with_table_schema(table_schema)
3325-
.with_projection(projection)
3326-
.with_predicate(predicate)
3327-
.build(); // pushdown_filters defaults to false
3293+
let (morselizer, file) =
3294+
build_pushdown_morselizer(&store, "pushdown_off.parquet", expr, false)
3295+
.await;
33283296

3329-
let file = PartitionedFile::new(
3330-
"pushdown_off.parquet".to_string(),
3331-
u64::try_from(data_size).unwrap(),
3332-
);
3333-
// Should open cleanly and return all rows (no row filter applied).
33343297
let stream = open_file(&morselizer, file).await.unwrap();
33353298
let (_batches, rows) = count_batches_and_rows(stream).await;
33363299
assert_eq!(rows, 5);

‎datafusion/datasource-parquet/src/source.rs‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -981,8 +981,6 @@ mod tests {
981981

982982
let source = ParquetSource::new(table_schema).with_pushdown_filters(true);
983983

984-
// Three filters: pure file-col (pushable), pure virtual (not pushable),
985-
// and a mixed OR conjunct (not pushable).
986984
let full_schema = source.table_schema.table_schema();
987985

988986
let pushable = logical2physical(&col("value").eq(logical_lit(1i64)), full_schema);

‎datafusion/datasource-parquet/src/virtual_column.rs‎

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,21 @@
1717

1818
//! Typed wrapper for parquet virtual columns.
1919
//!
20-
//! arrow-rs identifies virtual columns (produced by the reader, not read from
21-
//! the file) via arrow extension types carried on the `FieldRef`. This module
22-
//! lifts that contract into the type system: [`ParquetVirtualColumn`] is the
23-
//! set of virtual columns DataFusion currently supports, and its
24-
//! [`TryFrom<FieldRef>`] impl validates the extension type at the boundary so
25-
//! downstream code can pattern-match on variants rather than string-compare.
20+
//! arrow-rs identifies virtual columns via arrow extension types carried on
21+
//! the `FieldRef`. [`ParquetVirtualColumn`] lifts that contract into the type
22+
//! system so callers validate at the boundary (via `TryFrom<&FieldRef>`)
23+
//! rather than string-comparing extension-type names deep inside the reader.
2624
2725
use arrow::datatypes::FieldRef;
2826
use arrow_schema::extension::ExtensionType;
2927
use datafusion_common::{DataFusionError, Result, not_impl_err};
3028
use parquet::arrow::RowNumber;
29+
use std::sync::Arc;
3130

3231
/// A parquet virtual column validated to have a supported arrow extension
3332
/// type.
3433
///
35-
/// Virtual columns are synthesized by the parquet reader (e.g.
36-
/// [`RowNumber`] produces the absolute row number within the file) rather
37-
/// than being read from column data. Because arrow-rs identifies them via
38-
/// extension types on `FieldRef`, it would be easy to pass an unsupported or
39-
/// misspelled extension type and have the error surface deep in the reader
40-
/// pipeline; this enum forces the check to the construction boundary.
41-
///
42-
/// Construct via [`TryFrom<FieldRef>`]; add a new variant (and update the
34+
/// Construct via [`TryFrom<&FieldRef>`]; add a new variant (and update the
4335
/// `TryFrom` impl) when DataFusion gains support for another arrow-rs virtual
4436
/// extension type.
4537
#[derive(Debug, Clone)]
@@ -50,9 +42,6 @@ pub enum ParquetVirtualColumn {
5042
}
5143

5244
impl ParquetVirtualColumn {
53-
/// The underlying arrow field, with its extension type and metadata
54-
/// preserved. Suitable for passing to
55-
/// `ArrowReaderOptions::with_virtual_columns`.
5645
pub fn field(&self) -> &FieldRef {
5746
match self {
5847
Self::RowNumber(field) => field,
@@ -68,10 +57,10 @@ impl From<ParquetVirtualColumn> for FieldRef {
6857
}
6958
}
7059

71-
impl TryFrom<FieldRef> for ParquetVirtualColumn {
60+
impl TryFrom<&FieldRef> for ParquetVirtualColumn {
7261
type Error = DataFusionError;
7362

74-
fn try_from(field: FieldRef) -> Result<Self> {
63+
fn try_from(field: &FieldRef) -> Result<Self> {
7564
let Some(name) = field.extension_type_name() else {
7665
return not_impl_err!(
7766
"Virtual column '{}' is missing an Arrow extension type; \
@@ -81,7 +70,7 @@ impl TryFrom<FieldRef> for ParquetVirtualColumn {
8170
);
8271
};
8372
match name {
84-
n if n == RowNumber::NAME => Ok(Self::RowNumber(field)),
73+
n if n == RowNumber::NAME => Ok(Self::RowNumber(Arc::clone(field))),
8574
other => not_impl_err!(
8675
"Virtual column '{}' uses unsupported Arrow extension type '{}'; \
8776
supported types: [{}]. Add a ParquetVirtualColumn variant and \
@@ -98,28 +87,25 @@ impl TryFrom<FieldRef> for ParquetVirtualColumn {
9887
mod tests {
9988
use super::*;
10089
use arrow::datatypes::{DataType, Field};
101-
use std::sync::Arc;
10290

10391
#[test]
10492
fn row_number_field_converts() {
10593
let field: FieldRef = Arc::new(
10694
Field::new("row_number", DataType::Int64, false)
10795
.with_extension_type(RowNumber),
10896
);
109-
let col =
110-
ParquetVirtualColumn::try_from(Arc::clone(&field)).expect("valid row_number");
97+
let col = ParquetVirtualColumn::try_from(&field).expect("valid row_number");
11198
assert!(matches!(col, ParquetVirtualColumn::RowNumber(_)));
11299
assert_eq!(col.field().name(), "row_number");
113100
}
114101

115102
#[test]
116103
fn missing_extension_type_rejected() {
117104
let field: FieldRef = Arc::new(Field::new("plain", DataType::Int64, false));
118-
let err = ParquetVirtualColumn::try_from(field).unwrap_err();
119-
let msg = err.to_string();
105+
let err = ParquetVirtualColumn::try_from(&field).unwrap_err();
120106
assert!(
121-
msg.contains("missing an Arrow extension type"),
122-
"got: {msg}"
107+
err.to_string().contains("missing an Arrow extension type"),
108+
"got: {err}"
123109
);
124110
}
125111

@@ -130,11 +116,10 @@ mod tests {
130116
Field::new("row_group_index", DataType::Int64, false)
131117
.with_extension_type(parquet::arrow::RowGroupIndex),
132118
);
133-
let err = ParquetVirtualColumn::try_from(field).unwrap_err();
134-
let msg = err.to_string();
119+
let err = ParquetVirtualColumn::try_from(&field).unwrap_err();
135120
assert!(
136-
msg.contains("parquet.virtual.row_group_index"),
137-
"error should name the offending extension type, got: {msg}"
121+
err.to_string().contains("parquet.virtual.row_group_index"),
122+
"error should name the offending extension type, got: {err}"
138123
);
139124
}
140125
}

‎datafusion/datasource/src/table_schema.rs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,7 @@ impl TableSchema {
259259
if self.virtual_columns.is_empty() {
260260
return Arc::clone(&self.table_schema);
261261
}
262-
let mut builder = SchemaBuilder::from(self.file_schema.as_ref());
263-
builder.extend(self.table_partition_cols.iter().cloned());
264-
Arc::new(builder.finish())
262+
build_table_schema(&self.file_schema, &self.table_partition_cols, &[])
265263
}
266264
}
267265

0 commit comments

Comments
 (0)