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


##########
datafusion/physical-plan/src/joins/piecewise_merge_join/classic_join.rs:
##########
@@ -633,18 +595,121 @@ fn build_matched_indices_and_mark_buffered(
     )?)
 }
 
-// Creates a record batch from the unmatched indices on the streamed side
-fn create_unmatched_batch(
-    streamed_indices: &mut PrimitiveBuilder<UInt32Type>,
-    stream_batch: &SortedStreamBatch,
+// The last non-null key of the sorted buffered side, or `None` when it is 
empty or every key
+// is NULL. NULLs sort last for `<`/`<=`, so this is not simply the last key. 
Logical, as a
+// run-end encoded or `Null`-typed NULL has no physical null buffer.
+fn buffered_extreme(
+    values: &ArrayRef,
+    sort_options: SortOptions,
+) -> Result<Option<ColumnarValue>> {
+    let (start, end) = non_null_range(values.as_ref(), sort_options);
+    if start == end {
+        return Ok(None);
+    }
+    let last = values.slice(end - 1, 1);
+    // `apply_cmp` normalizes `-0.0` only in flat float scalars, not inside a
+    // `ScalarValue::Dictionary`, so normalize the key before taking it.
+    let extreme = normalize_float_zero(&last);
+    Ok(Some(ColumnarValue::Scalar(ScalarValue::try_from_array(
+        &extreme, 0,
+    )?)))
+}
+
+// Which rows of `stream_values` can match at least one buffered row.
+//
+// Every match set is a suffix `[k, non_null_end)` of the sorted buffered 
side's non-null keys,
+// so a streamed row matches anything at all iff it matches the last of them, 
`buffered_extreme`
+// (`None` when no buffered key is non-null, so nothing can match). A NULL on 
either side
+// compares to NULL, which is no match.
+//
+// This must agree exactly with the scan's `JoinKeyComparator`, or it would 
change which rows
+// match. One vectorized `apply_cmp` does, nested keys included: it normalizes 
`-0.0` to
+// `+0.0` as the comparator does, and orders NULL elements inside a nested key 
the same way.
+fn matchable_rows(
+    stream_values: &ArrayRef,
+    buffered_values: &ArrayRef,
+    buffered_extreme: Option<&ColumnarValue>,
+    operator: Operator,
+    sort_options: SortOptions,
+) -> Result<BooleanArray> {
+    let num_rows = stream_values.len();
+    let Some(extreme) = buffered_extreme else {
+        return Ok(BooleanArray::new(BooleanBuffer::new_unset(num_rows), None));
+    };
+
+    // Run-end encoded keys are decided with the scan's own comparator: 
arrow's run-end
+    // comparison kernel overflows on an empty batch sliced past its first 
run, which the
+    // scan never reaches.
+    if matches!(stream_values.data_type(), DataType::RunEndEncoded(_, _)) {
+        let match_on_equal = matches_on_equal(operator)?;
+        let cmp = JoinKeyComparator::new(
+            &[Arc::clone(stream_values)],
+            &[Arc::clone(buffered_values)],
+            &[sort_options],
+            NullEquality::NullEqualsNothing,
+        )?;
+        let last = non_null_range(buffered_values.as_ref(), sort_options).1 - 
1;
+        // Logical NULLs: a run-end encoded NULL has no physical null buffer.
+        let nulls = stream_values.logical_nulls();
+        let matchable = BooleanBuffer::collect_bool(num_rows, |row| {
+            nulls.as_ref().is_none_or(|nulls| nulls.is_valid(row))
+                && is_match(cmp.compare(row, last), match_on_equal)
+        });
+        return Ok(BooleanArray::new(matchable, None));
+    }
+
+    // The predicate is oriented `buffered OP streamed`.
+    let result = apply_cmp(
+        operator,
+        extreme,
+        &ColumnarValue::Array(Arc::clone(stream_values)),
+    )?
+    .into_array(num_rows)?;
+    let result = result.as_boolean();

Review Comment:
   `apply_cmp_for_nested` masks only physical nulls (`l.nulls()` / 
`r.nulls()`). For `Dictionary<_, List>` those are the keys' nulls, so a valid 
key pointing at a NULL list passes as matchable and then hits the scan guard: 
`Internal error: PiecewiseMergeJoin: streamed NULL keys must be settled before 
the scan.` (Right join, `>`). The previous head sent `is_nested()` types, 
dictionary-of-nested included, through the comparator path, which used 
`logical_nulls()`.
   
   Repro: add this to `matchable_rows_agrees_with_scan`. It fails on this head 
with `dictionary_list_null_values/full >: streamed row 1`:
   ```rs
   (
       // A valid key pointing at a NULL list: logically NULL, physically valid.
       "dictionary_list_null_values",
       Arc::new(DictionaryArray::<Int32Type>::new(
           Int32Array::from(vec![0, 1, 2]),
           Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
               Some(vec![Some(5)]),
               None,
               Some(vec![Some(1)]),
           ])),
       )),
       Arc::new(DictionaryArray::<Int32Type>::new(
           Int32Array::from(vec![Some(0), Some(1), Some(2), Some(3), None]),
           Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
               Some(vec![Some(2)]),
               None,
               Some(vec![Some(6)]),
               Some(vec![Some(0)]),
           ])),
       )),
   ),
   ```
   
   Fix (passes the case above and the end-to-end join):
   ```diff
        let result = result.as_boolean();
   -    Ok(if result.null_count() > 0 {
   +    let matchable = if result.null_count() > 0 {
            prep_null_mask_filter(result)
        } else {
            result.clone()
   -    })
   +    };
   +    // The nested `apply_cmp` masks only physical NULLs, so a dictionary 
key pointing at a
   +    // NULL nested value would pass.
   +    Ok(match stream_values.logical_nulls() {
   +        Some(nulls) => BooleanArray::new(matchable.values() & 
nulls.inner(), None),
   +        None => matchable,
   +    })
   ```



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