comphead commented on code in PR #25840:
URL: https://github.com/apache/datafusion/pull/25840#discussion_r4157589052


##########
datafusion/physical-plan/src/joins/piecewise_merge_join/classic_join.rs:
##########
@@ -1185,6 +1227,313 @@ mod tests {
         Ok(())
     }
 
+    /// The pre-sort check that settles never-matching streamed rows must 
compare keys
+    /// the way the scan's `JoinKeyComparator` does, which normalizes `-0.0` 
to `+0.0`.
+    /// Under IEEE 754 total order (arrow's raw kernels, and the sort) `-0.0 < 
+0.0`
+    /// holds, so a check built on that would let the `+0.0` row through as a 
candidate
+    /// that the scan then rejects; with SQL semantics both rows are unmatched.
+    #[tokio::test]
+    async fn join_right_less_than_signed_zero_prefilter_agrees_with_scan() -> 
Result<()> {

Review Comment:
   This overlaps the `float64` case of `matchable_rows_agrees_with_scan` and 
the `f64` pass of `fuzz_pwmj_matches_nested_loop`. A classic `RIGHT`/`FULL` 
case in `piecewise_merge_join_matrix.slt` Part 1 would replace it and be 
checked against `NestedLoopJoin` at batch sizes 1, 2, 100 and 8192, instead of 
a hard-coded snapshot. The matrix only has `-0.0` for existence joins today. 
The same file could take one unsorted streamed batch mixing matching, 
non-matching and NULL keys to cover the `positions` remap. I tried both cases 
across the matrix locally and they pass on this head.



##########
datafusion/physical-plan/src/joins/piecewise_merge_join/classic_join.rs:
##########
@@ -456,126 +494,49 @@ fn resolve_classic_join(
     )?;
 
     let mut buffer_idx = batch_process_state.start_buffer_idx;
-    let mut stream_idx = batch_process_state.start_stream_idx;
+    let stream_idx = batch_process_state.start_stream_idx;
 
+    // NULL keys never match and sort first, so the scan starts past the 
buffered ones.
+    // Streamed NULL keys never get here: `matchable_rows` settled them before 
the sort.
+    debug_assert_eq!(stream_values[0].null_count(), 0);
     if !batch_process_state.processed_null_count {

Review Comment:
   With streamed NULLs gone from the scan, `processed_null_count` only seeds 
`buffer_idx` with the buffered NULL count once per stream batch. Seeding 
`start_buffer_idx` right after `batch_process_state.reset()` in 
`fetch_stream_batch` removes the flag, its reset and this first-call branch. I 
tried it locally and the PWMJ unit tests and the fuzz still pass.



##########
datafusion/physical-plan/src/joins/piecewise_merge_join/classic_join.rs:
##########
@@ -1185,6 +1227,313 @@ mod tests {
         Ok(())
     }
 
+    /// The pre-sort check that settles never-matching streamed rows must 
compare keys
+    /// the way the scan's `JoinKeyComparator` does, which normalizes `-0.0` 
to `+0.0`.
+    /// Under IEEE 754 total order (arrow's raw kernels, and the sort) `-0.0 < 
+0.0`
+    /// holds, so a check built on that would let the `+0.0` row through as a 
candidate
+    /// that the scan then rejects; with SQL semantics both rows are unmatched.
+    #[tokio::test]
+    async fn join_right_less_than_signed_zero_prefilter_agrees_with_scan() -> 
Result<()> {
+        let float_exec =
+            |a: &str, b: &str, keys: Vec<f64>| -> Result<Arc<dyn 
ExecutionPlan>> {
+                let schema = Arc::new(Schema::new(vec![
+                    Field::new(a, DataType::Int32, false),
+                    Field::new(b, DataType::Float64, false),
+                ]));
+                let ids = (0..keys.len() as i32).collect::<Vec<_>>();
+                let batch = RecordBatch::try_new(
+                    Arc::clone(&schema),
+                    vec![
+                        Arc::new(arrow::array::Int32Array::from(ids)),
+                        Arc::new(arrow::array::Float64Array::from(keys)),
+                    ],
+                )?;
+                Ok(TestMemoryExec::try_new_exec(&[vec![batch]], schema, None)?)
+            };
+        let left = float_exec("a1", "b1", vec![-0.0])?;
+        let right = float_exec("a2", "b2", vec![0.0, -1.0])?;
+        let on = (
+            Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
+            Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
+        );
+
+        let (_, batches, _) =
+            join_collect(left, right, on, Operator::Lt, 
JoinType::Right).await?;
+
+        assert_snapshot!(batches_to_string(&batches), @r"
+        +----+----+----+------+
+        | a1 | b1 | a2 | b2   |
+        +----+----+----+------+
+        |    |    | 0  | 0.0  |
+        |    |    | 1  | -1.0 |
+        +----+----+----+------+
+        ");
+        Ok(())
+    }
+
+    /// `matchable_rows` against the scan's own definition of a match -- some 
non-NULL
+    /// buffered row `idx` with `is_match(cmp.compare(row, idx))` under the 
scan's
+    /// `JoinKeyComparator` -- for every operator and every kind of key it 
dispatches on:
+    /// flat keys through `apply_cmp` (incl. signed zeros, NaN, strings, 
dictionaries) and
+    /// nested keys with NULL elements through the comparator. Each is also 
checked against
+    /// an all-NULL and an empty buffered side.
+    #[test]
+    fn matchable_rows_agrees_with_scan() -> Result<()> {
+        use arrow::array::{
+            BinaryArray, BooleanArray, Decimal128Array, DictionaryArray, 
Float64Array,
+            Int32Array, ListArray, StringArray, StringViewArray,
+            TimestampMicrosecondArray,
+        };
+        use arrow::datatypes::Int32Type;
+
+        let strings =
+            |v: &[Option<&str>]| Arc::new(StringArray::from(v.to_vec())) as 
ArrayRef;
+        let cases: Vec<(&str, ArrayRef, ArrayRef)> = vec![
+            (
+                "int32",
+                Arc::new(Int32Array::from(vec![
+                    Some(5),
+                    None,
+                    Some(1),
+                    Some(9),
+                    Some(3),
+                ])),
+                Arc::new(Int32Array::from(vec![
+                    Some(0),
+                    Some(1),
+                    Some(2),
+                    Some(3),
+                    Some(5),
+                    Some(9),
+                    Some(10),
+                    None,
+                ])),
+            ),
+            (
+                "float64",
+                Arc::new(Float64Array::from(vec![
+                    Some(0.0),
+                    Some(-0.0),
+                    Some(1.5),
+                    Some(f64::NAN),
+                    None,
+                ])),
+                Arc::new(Float64Array::from(vec![
+                    Some(-0.0),
+                    Some(0.0),
+                    Some(1.5),
+                    Some(f64::NAN),
+                    Some(-1.0),
+                    Some(2.0),
+                    Some(f64::INFINITY),
+                    Some(f64::NEG_INFINITY),
+                    None,
+                ])),
+            ),
+            (
+                "utf8",
+                strings(&[Some("b"), Some(""), Some("d"), None]),
+                strings(&[
+                    Some(""),
+                    Some("a"),
+                    Some("b"),
+                    Some("c"),
+                    Some("d"),
+                    Some("e"),
+                    None,
+                ]),
+            ),
+            (
+                "utf8_view",
+                Arc::new(StringViewArray::from(vec![
+                    Some("b"),
+                    Some(""),
+                    Some("d"),
+                    None,
+                ])),
+                Arc::new(StringViewArray::from(vec![
+                    Some(""),
+                    Some("a"),
+                    Some("b"),
+                    Some("d"),
+                    Some("e"),
+                    None,
+                ])),
+            ),
+            (
+                "binary",
+                Arc::new(BinaryArray::from(vec![Some(&b"b"[..]), Some(b""), 
None])),
+                Arc::new(BinaryArray::from(vec![
+                    Some(&b""[..]),
+                    Some(b"a"),
+                    Some(b"b"),
+                    Some(b"c"),
+                    None,
+                ])),
+            ),
+            (
+                "dictionary",
+                Arc::new(
+                    vec![Some("b"), None, Some("d"), Some("b")]
+                        .into_iter()
+                        .collect::<DictionaryArray<Int32Type>>(),
+                ),
+                Arc::new(
+                    vec![Some("a"), Some("b"), Some("c"), Some("d"), 
Some("e"), None]
+                        .into_iter()
+                        .collect::<DictionaryArray<Int32Type>>(),
+                ),
+            ),
+            (
+                "decimal128",
+                Arc::new(
+                    Decimal128Array::from(vec![Some(150), None, Some(-25)])
+                        .with_precision_and_scale(10, 2)?,
+                ),
+                Arc::new(
+                    Decimal128Array::from(vec![
+                        Some(-26),
+                        Some(-25),
+                        Some(0),
+                        Some(150),
+                        Some(151),
+                        None,
+                    ])
+                    .with_precision_and_scale(10, 2)?,
+                ),
+            ),
+            (
+                "date32",
+                Arc::new(Date32Array::from(vec![Some(10), None, Some(20)])),
+                Arc::new(Date32Array::from(vec![
+                    Some(9),
+                    Some(10),
+                    Some(15),
+                    Some(20),
+                    Some(21),
+                    None,
+                ])),
+            ),
+            (
+                "timestamp_us",
+                Arc::new(TimestampMicrosecondArray::from(vec![
+                    Some(-5),
+                    Some(7),
+                    None,
+                ])),
+                Arc::new(TimestampMicrosecondArray::from(vec![
+                    Some(-6),
+                    Some(-5),
+                    Some(7),
+                    Some(8),
+                    None,
+                ])),
+            ),
+            (
+                "boolean",
+                Arc::new(BooleanArray::from(vec![Some(true), None])),
+                Arc::new(BooleanArray::from(vec![Some(false), Some(true), 
None])),
+            ),
+            (
+                "list_with_null_elements",
+                Arc::new(ListArray::from_iter_primitive::<Int32Type, _, 
_>(vec![
+                    Some(vec![Some(5)]),
+                    Some(vec![None]),
+                    Some(vec![Some(1), Some(2)]),
+                    Some(vec![]),
+                    None,
+                ])),
+                Arc::new(ListArray::from_iter_primitive::<Int32Type, _, 
_>(vec![
+                    Some(vec![None]),
+                    Some(vec![Some(7)]),
+                    Some(vec![Some(1)]),
+                    Some(vec![Some(5)]),
+                    Some(vec![]),
+                    Some(vec![None, Some(1)]),
+                    Some(vec![Some(5), None]),
+                    None,
+                ])),
+            ),
+            (
+                // No empty list, so the extreme key has a first element to 
compare
+                // against the streamed NULL elements.
+                "list_null_elements_vs_extreme",
+                Arc::new(ListArray::from_iter_primitive::<Int32Type, _, 
_>(vec![
+                    Some(vec![Some(5)]),
+                    Some(vec![Some(3)]),
+                    None,
+                ])),
+                Arc::new(ListArray::from_iter_primitive::<Int32Type, _, 
_>(vec![
+                    Some(vec![None]),
+                    Some(vec![Some(3), None]),
+                    Some(vec![Some(4)]),
+                    Some(vec![Some(7)]),
+                    Some(vec![Some(1)]),
+                    None,
+                ])),
+            ),
+        ];
+
+        for (name, buffered, streamed) in cases {
+            let all_null = new_null_array(buffered.data_type(), 3);
+            let empty = buffered.slice(0, 0);
+            for (side, buffered) in
+                [("full", buffered), ("all_null", all_null), ("empty", empty)]
+            {
+                for operator in
+                    [Operator::Lt, Operator::LtEq, Operator::Gt, 
Operator::GtEq]
+                {
+                    // As `PiecewiseMergeJoinExec::try_new` sorts both sides.
+                    let sort_options = match operator {
+                        Operator::Lt | Operator::LtEq => 
SortOptions::new(true, true),
+                        _ => SortOptions::new(false, true),
+                    };
+                    let sorted = take(
+                        buffered.as_ref(),
+                        &sort_to_indices(buffered.as_ref(), 
Some(sort_options), None)?,
+                        None,
+                    )?;
+                    let extreme = match sorted.len().checked_sub(1) {

Review Comment:
   This copies the extreme computation from `collect_buffered_side`, so the 
test would not notice a bug in the production version. A small shared function, 
for example `fn buffered_extreme(values: &ArrayRef) -> 
Result<Option<ColumnarValue>>`, would remove the duplicate and make the test 
exercise the real code.



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