NoahKusaba commented on code in PR #14:
URL: https://github.com/apache/datafusion-iceberg/pull/14#discussion_r4093962795
##########
crates/datafusion/src/physical_plan/project.rs:
##########
@@ -91,38 +87,72 @@ pub fn project_with_partition(
projection_exprs.push((column_expr, field.name().clone()));
}
- let partition_expr = Arc::new(PartitionExpr::new(calculator,
partition_spec.clone()));
+ let partition_expr = Arc::new(PartitionExpr::try_new(
+ partition_spec.clone(),
+ table_schema.clone(),
+ )?);
projection_exprs.push((partition_expr,
PROJECTED_PARTITION_VALUE_COLUMN.to_string()));
let projection = ProjectionExec::try_new(projection_exprs, input)?;
Ok(Arc::new(projection))
}
/// PhysicalExpr implementation for partition value calculation
+///
+/// The [`PartitionValueCalculator`] cannot be serialized, so the spec and
schema it
+/// was built from are retained for [`Self::try_new`] to rebuild from.
#[derive(Debug, Clone)]
-struct PartitionExpr {
+pub struct PartitionExpr {
calculator: Arc<PartitionValueCalculator>,
partition_spec: Arc<PartitionSpec>,
+ table_schema: IcebergSchemaRef,
}
impl PartitionExpr {
- fn new(
- calculator: PartitionValueCalculator,
+ /// Builds the expression from the spec and schema that define it.
+ ///
+ /// The calculator is built here rather than passed in, so it cannot drift
from
+ /// the retained inputs.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the spec cannot be bound to the schema.
Review Comment:
Done in `fff3f51` — took your suggested line verbatim. I wrote it by hand
rather than through the suggestion UI, so GitHub may still show it as
unapplied; the file matches it character for character.
Added `test_try_new_error_paths` covering all three cases. Your predicted
errors were exact: `DataInvalid => Cannot create partition calculator for
unpartitioned table`, `Unexpected, context: { field_id: 2 } => Field not
found`, and `DataInvalid => boolean is not a valid input type of bucket
transform`. I folded them into one table-driven test since all three exercise
the same forwarding call; each row asserts on the distinguishing phrase so an
upstream rewording won't break them.
##########
crates/datafusion/src/physical_plan/project.rs:
##########
@@ -91,38 +87,72 @@ pub fn project_with_partition(
projection_exprs.push((column_expr, field.name().clone()));
}
- let partition_expr = Arc::new(PartitionExpr::new(calculator,
partition_spec.clone()));
+ let partition_expr = Arc::new(PartitionExpr::try_new(
+ partition_spec.clone(),
+ table_schema.clone(),
+ )?);
projection_exprs.push((partition_expr,
PROJECTED_PARTITION_VALUE_COLUMN.to_string()));
let projection = ProjectionExec::try_new(projection_exprs, input)?;
Ok(Arc::new(projection))
}
/// PhysicalExpr implementation for partition value calculation
+///
+/// The [`PartitionValueCalculator`] cannot be serialized, so the spec and
schema it
+/// was built from are retained for [`Self::try_new`] to rebuild from.
#[derive(Debug, Clone)]
-struct PartitionExpr {
+pub struct PartitionExpr {
calculator: Arc<PartitionValueCalculator>,
partition_spec: Arc<PartitionSpec>,
+ table_schema: IcebergSchemaRef,
}
impl PartitionExpr {
- fn new(
- calculator: PartitionValueCalculator,
+ /// Builds the expression from the spec and schema that define it.
+ ///
+ /// The calculator is built here rather than passed in, so it cannot drift
from
+ /// the retained inputs.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the spec cannot be bound to the schema.
+ pub fn try_new(
partition_spec: Arc<PartitionSpec>,
- ) -> Self {
- Self {
+ table_schema: IcebergSchemaRef,
+ ) -> DFResult<Self> {
+ let calculator = PartitionValueCalculator::try_new(
+ partition_spec.as_ref(),
+ table_schema.as_ref(),
+ )
+ .map_err(to_datafusion_error)?;
+ Ok(Self {
calculator: Arc::new(calculator),
partition_spec,
- }
+ table_schema,
+ })
+ }
+
+ /// The partition spec this expression computes values for. With
+ /// [`Self::table_schema`], all [`Self::try_new`] needs to rebuild an
equal one.
+ pub fn partition_spec(&self) -> &Arc<PartitionSpec> {
+ &self.partition_spec
+ }
+
+ /// The table schema the spec is bound to. Needed to rebuild: the spec
refers to
+ /// columns by `source_id`, and only the schema resolves those.
+ pub fn table_schema(&self) -> &IcebergSchemaRef {
+ &self.table_schema
}
}
-// Manual PartialEq/Eq implementations for pointer-based equality
-// (two PartitionExpr are equal if they share the same calculator and
partition_spec instances)
+// Equal when they compute the same partition values, which the spec and schema
+// decide entirely. The calculator is derived from those two, so it takes no
part:
+// comparing it by pointer would make an expression unequal to its own rebuild.
Review Comment:
You're right, and the wording wasn't just loose, it was wrong. Two tables'
specs with identical fields but different `spec_id`s do compute the same values
and compare unequal, so "equal when they compute the same partition values"
described something stricter than the code actually does. Took your suggestion
as-is in `fff3f51`.
##########
crates/datafusion/src/physical_plan/project.rs:
##########
@@ -339,6 +380,95 @@ mod tests {
}
}
+ #[test]
+ fn test_partition_expr_rebuilds_from_its_retained_parts() {
+ let table_schema = Arc::new(
+ Schema::builder()
+ .with_schema_id(1)
+ .with_fields(vec![
+ NestedField::required(1, "id",
Type::Primitive(PrimitiveType::Int))
+ .into(),
+ ])
+ .build()
+ .unwrap(),
+ );
+ let partition_spec = Arc::new(
+ PartitionSpec::builder(table_schema.clone())
+ .add_partition_field("id", "id_partition", Transform::Identity)
+ .unwrap()
+ .build()
+ .unwrap(),
+ );
+ let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
+ "id",
+ DataType::Int32,
+ false,
+ )]));
+ let batch = RecordBatch::try_new(
+ arrow_schema,
+ vec![Arc::new(Int32Array::from(vec![10, 20, 30]))],
+ )
+ .unwrap();
+
+ let expr = PartitionExpr::try_new(partition_spec,
table_schema).unwrap();
+
+ // Rebuild from deep copies, as a codec would; cloning the Arcs instead
+ // would let a pointer-based impl pass.
+ let rebuilt = PartitionExpr::try_new(
+ Arc::new(expr.partition_spec().as_ref().clone()),
+ Arc::new(expr.table_schema().as_ref().clone()),
+ )
+ .unwrap();
+
+ let eval = |e: &PartitionExpr| match e.evaluate(&batch).unwrap() {
+ ColumnarValue::Array(array) => array,
+ _ => panic!("Expected array result"),
+ };
+ assert_eq!(&eval(&rebuilt), &eval(&expr));
+
+ // Same values is not enough: it must also compare and hash as the same
+ // expression, or plan-level equality and dedup treat the two as
unrelated.
+ assert_eq!(rebuilt, expr);
+ assert_eq!(hash_of(&rebuilt), hash_of(&expr));
+ }
+
+ #[test]
+ fn test_partition_expr_equality_is_value_based() {
+ let table_schema = Arc::new(
+ Schema::builder()
+ .with_schema_id(1)
+ .with_fields(vec![
+ NestedField::required(1, "id",
Type::Primitive(PrimitiveType::Int))
+ .into(),
+ NestedField::required(2, "part",
Type::Primitive(PrimitiveType::Int))
+ .into(),
+ ])
+ .build()
+ .unwrap(),
+ );
+ let spec = |spec_id: i32, source: &str| {
+ Arc::new(
+ PartitionSpec::builder(table_schema.clone())
+ .with_spec_id(spec_id)
+ .add_partition_field(source, "p", Transform::Identity)
+ .unwrap()
+ .build()
+ .unwrap(),
+ )
+ };
+ let expr = |spec| PartitionExpr::try_new(spec,
table_schema.clone()).unwrap();
+
+ // The same inputs describe the same expression.
+ assert_eq!(expr(spec(1, "id")), expr(spec(1, "id")));
+
+ // Genuinely different specs stay apart.
+ assert_ne!(expr(spec(1, "id")), expr(spec(2, "part")));
+
+ // Same spec_id, different partition column: comparing only the ids
that
+ // Hash uses would wrongly call these equal.
+ assert_ne!(expr(spec(7, "id")), expr(spec(7, "part")));
+ }
Review Comment:
Added as `test_partition_expr_differs_on_schema_with_shared_id` in
`fff3f51`, with `part` as `Int` vs `Long` under a shared `schema_id`. It
asserts the two `schema_id`s are equal and the expressions are not, so the ids
`Hash` uses cannot be what separates them.
--
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]