comphead commented on code in PR #7569:
URL: https://github.com/apache/datafusion-comet/pull/7569#discussion_r4532981099


##########
native/spark-expr/src/hash_funcs/murmur3.rs:
##########
@@ -1148,4 +1148,202 @@ mod tests {
             "a dictionary child must keep the per-element path"
         );
     }
+
+    /// `create_hashes_internal!` specialises maps of (Utf8, Int32), (Int32, 
Utf8), (Utf8, Utf8)
+    /// and (Int32, Int32), each with a loop for input without nulls and a 
null-aware loop. Covers
+    /// both loops of every pair for both hashes. The null shape has a null 
map that still owns an
+    /// entry, which must not be hashed, and a null value, which is skipped 
while its key is not.
+    /// Expected values are Spark 4.1.3's `hash` and `xxhash64` (seed 42) on 
the same rows.
+    #[test]
+    fn test_specialised_map_pairs_match_spark() {
+        use crate::hash_funcs::create_xxhash64_hashes;
+        use arrow::array::{Array, MapArray, StructArray};
+        use arrow::buffer::{NullBuffer, OffsetBuffer};
+        use arrow::datatypes::{DataType, Field, Fields};
+
+        const STR_KEYS: [&str; 7] = ["a", "b", "", "日本", "x", "yz", "hidden"];
+        const INT_KEYS: [i32; 7] = [1, -1, 0, i32::MAX, i32::MIN, 7, 99];
+        const STR_VALUES: [&str; 7] = ["v1", "", "é", "w", "long value", "z", 
"hidden"];
+        const INT_VALUES: [i32; 7] = [10, -20, 0, i32::MIN, i32::MAX, 3, 99];
+
+        // The first `len` entries of a pool, with entry `null_at` null.
+        fn column(
+            utf8: bool,
+            strs: &[&str],
+            ints: &[i32],
+            len: usize,
+            null_at: Option<usize>,
+        ) -> ArrayRef {
+            let valid = |i: usize| Some(i) != null_at;
+            if utf8 {
+                Arc::new(
+                    (0..len)
+                        .map(|i| valid(i).then(|| strs[i]))
+                        .collect::<StringArray>(),
+                )
+            } else {
+                Arc::new(
+                    (0..len)
+                        .map(|i| valid(i).then(|| ints[i]))
+                        .collect::<Int32Array>(),
+                )
+            }
+        }
+
+        fn map(key_utf8: bool, value_utf8: bool, shape: Shape) -> ArrayRef {
+            let (lengths, valid, null_value) = shape;
+            let len = lengths.iter().sum();
+            let keys = column(key_utf8, &STR_KEYS, &INT_KEYS, len, None);
+            let values = column(value_utf8, &STR_VALUES, &INT_VALUES, len, 
null_value);
+            let entry_fields = Fields::from(vec![
+                Field::new("key", keys.data_type().clone(), false),
+                Field::new("value", values.data_type().clone(), true),
+            ]);
+            let entries = StructArray::new(entry_fields.clone(), vec![keys, 
values], None);
+            Arc::new(MapArray::new(
+                Arc::new(Field::new("entries", DataType::Struct(entry_fields), 
false)),
+                OffsetBuffer::from_lengths(lengths.iter().copied()),
+                entries,
+                valid
+                    .contains(&false)
+                    .then(|| NullBuffer::from(valid.to_vec())),
+                false,
+            ))
+        }
+
+        // Entries per row, map validity per row, and the entry whose value is 
null.
+        type Shape = (&'static [usize], &'static [bool], Option<usize>);
+        const NO_NULLS: Shape = (&[2, 0, 1, 3], &[true; 4], None);
+        const WITH_NULLS: Shape = (&[2, 0, 1, 1, 3], &[true, true, false, 
true, true], Some(3));
+
+        // (pair, key is Utf8, value is Utf8, [murmur3, xxhash64] without 
nulls, the same with nulls)
+        #[allow(clippy::type_complexity)]
+        let cases: [(&str, bool, bool, (Vec<u32>, Vec<u64>), (Vec<u32>, 
Vec<u64>)); 4] = [

Review Comment:
   This adds about 100 lines of hardcoded murmur3 and xxhash64 constants. Most 
of these shapes (all four key/value pairs, no nulls, and a null value whose key 
is still hashed) can be built in SQL, and `expressions/hash/hash.sql` already 
sets `spark.sql.legacy.allowHashOnMapType=true` and compares `hash` and 
`xxhash64` with Spark at run time. Could they go there instead, so no constants 
need to be kept in sync? Only the null map row that still owns an entry cannot 
come from SQL, and that case can be checked here without constants by comparing 
with the same rows with that map row removed. I have not checked that a Parquet 
map column reaches the null-free fast loop, so that is worth confirming.



##########
native/spark-expr/src/hash_funcs/murmur3.rs:
##########
@@ -1148,4 +1148,202 @@ mod tests {
             "a dictionary child must keep the per-element path"
         );
     }
+
+    /// `create_hashes_internal!` specialises maps of (Utf8, Int32), (Int32, 
Utf8), (Utf8, Utf8)
+    /// and (Int32, Int32), each with a loop for input without nulls and a 
null-aware loop. Covers
+    /// both loops of every pair for both hashes. The null shape has a null 
map that still owns an
+    /// entry, which must not be hashed, and a null value, which is skipped 
while its key is not.
+    /// Expected values are Spark 4.1.3's `hash` and `xxhash64` (seed 42) on 
the same rows.
+    #[test]
+    fn test_specialised_map_pairs_match_spark() {
+        use crate::hash_funcs::create_xxhash64_hashes;
+        use arrow::array::{Array, MapArray, StructArray};
+        use arrow::buffer::{NullBuffer, OffsetBuffer};
+        use arrow::datatypes::{DataType, Field, Fields};
+
+        const STR_KEYS: [&str; 7] = ["a", "b", "", "日本", "x", "yz", "hidden"];
+        const INT_KEYS: [i32; 7] = [1, -1, 0, i32::MAX, i32::MIN, 7, 99];
+        const STR_VALUES: [&str; 7] = ["v1", "", "é", "w", "long value", "z", 
"hidden"];
+        const INT_VALUES: [i32; 7] = [10, -20, 0, i32::MIN, i32::MAX, 3, 99];
+
+        // The first `len` entries of a pool, with entry `null_at` null.
+        fn column(
+            utf8: bool,
+            strs: &[&str],
+            ints: &[i32],
+            len: usize,
+            null_at: Option<usize>,
+        ) -> ArrayRef {
+            let valid = |i: usize| Some(i) != null_at;
+            if utf8 {
+                Arc::new(
+                    (0..len)
+                        .map(|i| valid(i).then(|| strs[i]))
+                        .collect::<StringArray>(),
+                )
+            } else {
+                Arc::new(
+                    (0..len)
+                        .map(|i| valid(i).then(|| ints[i]))
+                        .collect::<Int32Array>(),
+                )
+            }
+        }
+
+        fn map(key_utf8: bool, value_utf8: bool, shape: Shape) -> ArrayRef {
+            let (lengths, valid, null_value) = shape;
+            let len = lengths.iter().sum();
+            let keys = column(key_utf8, &STR_KEYS, &INT_KEYS, len, None);
+            let values = column(value_utf8, &STR_VALUES, &INT_VALUES, len, 
null_value);
+            let entry_fields = Fields::from(vec![
+                Field::new("key", keys.data_type().clone(), false),
+                Field::new("value", values.data_type().clone(), true),
+            ]);
+            let entries = StructArray::new(entry_fields.clone(), vec![keys, 
values], None);
+            Arc::new(MapArray::new(
+                Arc::new(Field::new("entries", DataType::Struct(entry_fields), 
false)),
+                OffsetBuffer::from_lengths(lengths.iter().copied()),
+                entries,
+                valid
+                    .contains(&false)
+                    .then(|| NullBuffer::from(valid.to_vec())),
+                false,
+            ))
+        }
+
+        // Entries per row, map validity per row, and the entry whose value is 
null.
+        type Shape = (&'static [usize], &'static [bool], Option<usize>);
+        const NO_NULLS: Shape = (&[2, 0, 1, 3], &[true; 4], None);
+        const WITH_NULLS: Shape = (&[2, 0, 1, 1, 3], &[true, true, false, 
true, true], Some(3));

Review Comment:
   Could the test also hash a sliced map, for example `array.slice(1, n)`, and 
compare with the tail of the expected vector? A sliced `MapArray` has offsets 
that do not start at 0 while `keys()` and `values()` stay unsliced, which is 
exactly what the macro's `$offsets[row_idx]` indexing depends on, and no map 
hash test covers it today.



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