patrickswedish commented on code in PR #24394:
URL: https://github.com/apache/datafusion/pull/24394#discussion_r3824595366


##########
datafusion/physical-plan/src/aggregates/mod.rs:
##########
@@ -876,6 +877,45 @@ pub struct AggregateExec {
     dynamic_filter: Option<Arc<AggrDynFilter>>,
 }
 
+/// A stream wrapper that ensures every yielded batch matches the declared 
input schema.

Review Comment:
   Hi @alamb,
   
   Thank you for this key architectural insight! You are completely right: 
fixing this at the \AggregateExec\ boundary was treating a symptom rather than 
addressing the producer contract.
   
   ### Root Cause Analysis
   In DataFusion, in-memory table sources like \MemTable::try_new\ accept 
batches whose schemas are stricter than the declared table schema using 
\Schema::contains(&batches_schema)\ (e.g. nullable nested fields in the 
declared table schema vs non-nullable nested fields in the input batches).
   
   When \MemTable::scan\ creates \MemorySourceConfig\ / \MemoryExec\, 
\MemoryStream\ was constructed with the declared schema, but its \poll_next\ 
emitted the underlying stricter \RecordBatch\s without adapting them. 
Downstream operators (like \AggregateExec\) received batches that did not 
conform to the stream's advertised output schema.
   
   ### Architectural Solution
   1. **Reverted AggregateExec Changes**: Completely removed 
\AdaptedInputRecordBatchStream\ and \AggregateExec::execute_input\, restoring 
all aggregate physical plans to clean upstream state.
   2. **Fixed Producer Invariant in \MemoryStream\ 
(\physical-plan/src/memory.rs\)**:
      - \MemoryStream::poll_next\ now normalizes emitted batches using 
\dapt_batch_to_schema(batch, &self.schema)\ whenever a batch differs from 
\self.schema\ and \self.schema.contains(batch.schema())\.
      - Every \RecordBatch\ emitted by \MemoryStream\ is guaranteed to conform 
to \stream.schema()\.
   3. **Retained Narrow Schema Conformance in \
   ested_struct\ (\common/src/nested_struct.rs\)**:
      - \dapt_batch_to_schema\ supports Structs, Lists, and Unions 
(Dense/Sparse) without changing general DataFusion CAST behavior (\ 
equires_nested_struct_cast\ remains unchanged).
   4. **Unit and Integration Regressions**:
      - Added unit tests in \physical-plan/src/memory.rs\ directly verifying 
that \MemoryStream\ emits batches matching \self.schema\ (with and without 
projection).
      - Retained end-to-end SQL aggregation regressions in \
   ested_nullability.rs\ covering standard, distinct, and spilling aggregations.



##########
datafusion/common/src/nested_struct.rs:
##########
@@ -1703,3 +1833,525 @@ mod tests {
         ));
     }
 }
+
+/// Adapts a [`RecordBatch`] to a target [`SchemaRef`].
+///
+/// If `batch` already has the target schema, it is returned immediately.
+///
+/// If `batch` has columns whose data types differ from `target_schema` (e.g. 
stricter
+/// nested struct or list nullabilities), this function verifies that each 
target data
+/// type contains the incoming column data type (as verified by 
[`arrow::datatypes::DataType::contains`])
+/// and transforms the metadata/types of differing columns to match 
`target_schema`
+/// without copying primitive buffer data.
+///
+/// If `batch` has an incompatible column count or incompatible column data 
types,
+/// an error is returned.
+pub fn adapt_batch_to_schema(
+    batch: RecordBatch,
+    target_schema: &SchemaRef,
+) -> Result<RecordBatch> {
+    if Arc::ptr_eq(batch.schema_ref(), target_schema)
+        || batch.schema().as_ref() == target_schema.as_ref()
+    {
+        return Ok(batch);
+    }
+
+    if batch.num_columns() != target_schema.fields().len() {
+        return _plan_err!(
+            "Batch schema does not conform to expected schema (column count 
mismatch). Expected: {target_schema}, got: {}",
+            batch.schema()
+        );
+    }
+
+    let mut columns = Vec::with_capacity(batch.num_columns());
+    let mut needs_column_adaptation = false;
+    let cast_options = CastOptions::default();
+
+    for (target_field, col) in 
target_schema.fields().iter().zip(batch.columns()) {
+        if target_field.data_type() != col.data_type() {
+            // If data types differ, verify that target_field's data type 
contains
+            // the column's data type (e.g. stricter nested struct / list 
field nullability).
+            if !target_field.data_type().contains(col.data_type()) {
+                return _plan_err!(
+                    "Batch column '{}' with type {} cannot be adapted to 
expected type {}",
+                    target_field.name(),
+                    col.data_type(),
+                    target_field.data_type()
+                );
+            }
+            needs_column_adaptation = true;
+            let adapted_col = cast_column(col, target_field.data_type(), 
&cast_options)?;
+            columns.push(adapted_col);
+        } else {
+            columns.push(Arc::clone(col));
+        }
+    }
+
+    if needs_column_adaptation {
+        Ok(RecordBatch::try_new(Arc::clone(target_schema), columns)?)
+    } else {
+        // Schema differs only in top-level metadata or field nullability, 
while
+        // column data types match exactly. Replace the schema on the batch.
+        Ok(RecordBatch::try_new(
+            Arc::clone(target_schema),
+            batch.columns().to_vec(),
+        )?)
+    }
+}
+
+#[cfg(test)]
+mod adapt_schema_tests {

Review Comment:
   Hi @alamb,
   
   Thank you for pointing us to the PR review test coverage guide!
   
   We refactored the test suite in \datafusion/common/src/nested_struct.rs\ and 
\datafusion/core/tests/sql/aggregates/nested_nullability.rs\:
   - Extracted shared test field builders (\    est_two_field_union\, etc.) to 
eliminate repeated boilerplate across test cases.
   - Streamlined unit tests to focus on distinct semantic cases: Sparse 
adaptation, Dense adaptation, non-contiguous/reordered type-ID mappings, nested 
Structs, mode mismatches, and field-set mismatches.
   - Retained exact row-level unpacked value assertions (\10\, \\b\\, \30\), 
active type IDs, and dense offsets without test repetition.
   - Removed oversized duplicate tests from \
   ested_nullability.rs\ to keep integration tests concise and focused on the 
end-to-end bug report reproducer.



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