| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| //! [`ArrowBytesMap`] and [`ArrowBytesSet`] for storing maps/sets of values from |
| //! StringArray / LargeStringArray / BinaryArray / LargeBinaryArray. |
| |
| use ahash::RandomState; |
| use arrow::array::{ |
| cast::AsArray, |
| types::{ByteArrayType, GenericBinaryType, GenericStringType}, |
| Array, ArrayRef, BufferBuilder, GenericBinaryArray, GenericStringArray, |
| NullBufferBuilder, OffsetSizeTrait, |
| }; |
| use arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer}; |
| use arrow::datatypes::DataType; |
| use datafusion_common::hash_utils::create_hashes; |
| use datafusion_common::utils::proxy::{HashTableAllocExt, VecAllocExt}; |
| use std::any::type_name; |
| use std::fmt::Debug; |
| use std::mem::{size_of, swap}; |
| use std::ops::Range; |
| use std::sync::Arc; |
| |
| /// Should the output be a String or Binary? |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub enum OutputType { |
| /// `StringArray` or `LargeStringArray` |
| Utf8, |
| /// `StringViewArray` |
| Utf8View, |
| /// `BinaryArray` or `LargeBinaryArray` |
| Binary, |
| /// `BinaryViewArray` |
| BinaryView, |
| } |
| |
| /// HashSet optimized for storing string or binary values that can produce that |
| /// the final set as a GenericStringArray with minimal copies. |
| #[derive(Debug)] |
| pub struct ArrowBytesSet<O: OffsetSizeTrait>(ArrowBytesMap<O, ()>); |
| |
| impl<O: OffsetSizeTrait> ArrowBytesSet<O> { |
| pub fn new(output_type: OutputType) -> Self { |
| Self(ArrowBytesMap::new(output_type)) |
| } |
| |
| /// Return the contents of this set and replace it with a new empty |
| /// set with the same output type |
| pub fn take(&mut self) -> Self { |
| Self(self.0.take()) |
| } |
| |
| /// Inserts each value from `values` into the set |
| pub fn insert(&mut self, values: &ArrayRef) { |
| fn make_payload_fn(_value: Option<&[u8]>) {} |
| fn observe_payload_fn(_payload: ()) {} |
| self.0 |
| .insert_if_new(values, make_payload_fn, observe_payload_fn); |
| } |
| |
| /// Converts this set into a `StringArray`/`LargeStringArray` or |
| /// `BinaryArray`/`LargeBinaryArray` containing each distinct value that |
| /// was interned. This is done without copying the values. |
| pub fn into_state(self) -> ArrayRef { |
| self.0.into_state() |
| } |
| |
| /// Returns the total number of distinct values (including nulls) seen so far |
| pub fn len(&self) -> usize { |
| self.0.len() |
| } |
| |
| pub fn is_empty(&self) -> bool { |
| self.0.is_empty() |
| } |
| |
| /// returns the total number of distinct values (not including nulls) seen so far |
| pub fn non_null_len(&self) -> usize { |
| self.0.non_null_len() |
| } |
| |
| /// Return the total size, in bytes, of memory used to store the data in |
| /// this set, not including `self` |
| pub fn size(&self) -> usize { |
| self.0.size() |
| } |
| } |
| |
| /// Optimized map for storing Arrow "bytes" types (`String`, `LargeString`, |
| /// `Binary`, and `LargeBinary`) values that can produce the set of keys on |
| /// output as `GenericBinaryArray` without copies. |
| /// |
| /// Equivalent to `HashSet<String, V>` but with better performance if you need |
| /// to emit the keys as an Arrow `StringArray` / `BinaryArray`. For other |
| /// purposes it is the same as a `HashMap<String, V>` |
| /// |
| /// # Generic Arguments |
| /// |
| /// * `O`: OffsetSize (String/LargeString) |
| /// * `V`: payload type |
| /// |
| /// # Description |
| /// |
| /// This is a specialized HashMap with the following properties: |
| /// |
| /// 1. Optimized for storing and emitting Arrow byte types (e.g. |
| /// `StringArray` / `BinaryArray`) very efficiently by minimizing copying of |
| /// the string values themselves, both when inserting and when emitting the |
| /// final array. |
| /// |
| /// |
| /// 2. Retains the insertion order of entries in the final array. The values are |
| /// in the same order as they were inserted. |
| /// |
| /// Note this structure can be used as a `HashSet` by specifying the value type |
| /// as `()`, as is done by [`ArrowBytesSet`]. |
| /// |
| /// This map is used by the special `COUNT DISTINCT` aggregate function to |
| /// store the distinct values, and by the `GROUP BY` operator to store |
| /// group values when they are a single string array. |
| /// |
| /// # Example |
| /// |
| /// The following diagram shows how the map would store the four strings |
| /// "Foo", NULL, "Bar", "TheQuickBrownFox": |
| /// |
| /// * `hashtable` stores entries for each distinct string that has been |
| /// inserted. The entries contain the payload as well as information about the |
| /// value (either an offset or the actual bytes, see `Entry` docs for more |
| /// details) |
| /// |
| /// * `offsets` stores offsets into `buffer` for each distinct string value, |
| /// following the same convention as the offsets in a `StringArray` or |
| /// `LargeStringArray`. |
| /// |
| /// * `buffer` stores the actual byte data |
| /// |
| /// * `null`: stores the index and payload of the null value, in this case the |
| /// second value (index 1) |
| /// |
| /// ```text |
| /// ┌───────────────────────────────────┐ ┌─────┐ ┌────┐ |
| /// │ ... │ │ 0 │ │FooB│ |
| /// │ ┌──────────────────────────────┐ │ │ 0 │ │arTh│ |
| /// │ │ <Entry for "Bar"> │ │ │ 3 │ │eQui│ |
| /// │ │ len: 3 │ │ │ 3 │ │ckBr│ |
| /// │ │ offset_or_inline: "Bar" │ │ │ 6 │ │ownF│ |
| /// │ │ payload:... │ │ │ │ │ox │ |
| /// │ └──────────────────────────────┘ │ │ │ │ │ |
| /// │ ... │ └─────┘ └────┘ |
| /// │ ┌──────────────────────────────┐ │ |
| /// │ │<Entry for "TheQuickBrownFox">│ │ offsets buffer |
| /// │ │ len: 16 │ │ |
| /// │ │ offset_or_inline: 6 │ │ ┌───────────────┐ |
| /// │ │ payload: ... │ │ │ Some(1) │ |
| /// │ └──────────────────────────────┘ │ │ payload: ... │ |
| /// │ ... │ └───────────────┘ |
| /// └───────────────────────────────────┘ |
| /// null |
| /// HashTable |
| /// ``` |
| /// |
| /// # Entry Format |
| /// |
| /// Entries stored in a [`ArrowBytesMap`] represents a value that is either |
| /// stored inline or in the buffer |
| /// |
| /// This helps the case where there are many short (less than 8 bytes) strings |
| /// that are the same (e.g. "MA", "CA", "NY", "TX", etc) |
| /// |
| /// ```text |
| /// ┌──────────────────┐ |
| /// ─ ─ ─ ─ ─ ─ ─▶│... │ |
| /// │ │TheQuickBrownFox │ |
| /// │... │ |
| /// │ │ │ |
| /// └──────────────────┘ |
| /// │ buffer of u8 |
| /// |
| /// │ |
| /// ┌────────────────┬───────────────┬───────────────┐ |
| /// Storing │ │ starting byte │ length, in │ |
| /// "TheQuickBrownFox" │ hash value │ offset in │ bytes (not │ |
| /// (long string) │ │ buffer │ characters) │ |
| /// └────────────────┴───────────────┴───────────────┘ |
| /// 8 bytes 8 bytes 4 or 8 |
| /// |
| /// |
| /// ┌───────────────┬─┬─┬─┬─┬─┬─┬─┬─┬───────────────┐ |
| /// Storing "foobar" │ │ │ │ │ │ │ │ │ │ length, in │ |
| /// (short string) │ hash value │?│?│f│o│o│b│a│r│ bytes (not │ |
| /// │ │ │ │ │ │ │ │ │ │ characters) │ |
| /// └───────────────┴─┴─┴─┴─┴─┴─┴─┴─┴───────────────┘ |
| /// 8 bytes 8 bytes 4 or 8 |
| /// ``` |
| pub struct ArrowBytesMap<O, V> |
| where |
| O: OffsetSizeTrait, |
| V: Debug + PartialEq + Eq + Clone + Copy + Default, |
| { |
| /// Should the output be String or Binary? |
| output_type: OutputType, |
| /// Underlying hash set for each distinct value |
| map: hashbrown::hash_table::HashTable<Entry<O, V>>, |
| /// Total size of the map in bytes |
| map_size: usize, |
| /// In progress arrow `Buffer` containing all values |
| buffer: BufferBuilder<u8>, |
| /// Offsets into `buffer` for each distinct value. These offsets as used |
| /// directly to create the final `GenericBinaryArray`. The `i`th string is |
| /// stored in the range `offsets[i]..offsets[i+1]` in `buffer`. Null values |
| /// are stored as a zero length string. |
| offsets: Vec<O>, |
| /// random state used to generate hashes |
| random_state: RandomState, |
| /// buffer that stores hash values (reused across batches to save allocations) |
| hashes_buffer: Vec<u64>, |
| /// `(payload, null_index)` for the 'null' value, if any |
| /// NOTE null_index is the logical index in the final array, not the index |
| /// in the buffer |
| null: Option<(V, usize)>, |
| } |
| |
| /// The size, in number of entries, of the initial hash table |
| const INITIAL_MAP_CAPACITY: usize = 128; |
| /// The initial size, in bytes, of the string data |
| pub const INITIAL_BUFFER_CAPACITY: usize = 8 * 1024; |
| impl<O: OffsetSizeTrait, V> ArrowBytesMap<O, V> |
| where |
| V: Debug + PartialEq + Eq + Clone + Copy + Default, |
| { |
| pub fn new(output_type: OutputType) -> Self { |
| Self { |
| output_type, |
| map: hashbrown::hash_table::HashTable::with_capacity(INITIAL_MAP_CAPACITY), |
| map_size: 0, |
| buffer: BufferBuilder::new(INITIAL_BUFFER_CAPACITY), |
| offsets: vec![O::default()], // first offset is always 0 |
| random_state: RandomState::new(), |
| hashes_buffer: vec![], |
| null: None, |
| } |
| } |
| |
| /// Return the contents of this map and replace it with a new empty map with |
| /// the same output type |
| pub fn take(&mut self) -> Self { |
| let mut new_self = Self::new(self.output_type); |
| swap(self, &mut new_self); |
| new_self |
| } |
| |
| /// Inserts each value from `values` into the map, invoking `payload_fn` for |
| /// each value if *not* already present, deferring the allocation of the |
| /// payload until it is needed. |
| /// |
| /// Note that this is different than a normal map that would replace the |
| /// existing entry |
| /// |
| /// # Arguments: |
| /// |
| /// `values`: array whose values are inserted |
| /// |
| /// `make_payload_fn`: invoked for each value that is not already present |
| /// to create the payload, in order of the values in `values` |
| /// |
| /// `observe_payload_fn`: invoked once, for each value in `values`, that was |
| /// already present in the map, with corresponding payload value. |
| /// |
| /// # Returns |
| /// |
| /// The payload value for the entry, either the existing value or |
| /// the newly inserted value |
| /// |
| /// # Safety: |
| /// |
| /// Note that `make_payload_fn` and `observe_payload_fn` are only invoked |
| /// with valid values from `values`, not for the `NULL` value. |
| pub fn insert_if_new<MP, OP>( |
| &mut self, |
| values: &ArrayRef, |
| make_payload_fn: MP, |
| observe_payload_fn: OP, |
| ) where |
| MP: FnMut(Option<&[u8]>) -> V, |
| OP: FnMut(V), |
| { |
| // Sanity array type |
| match self.output_type { |
| OutputType::Binary => { |
| assert!(matches!( |
| values.data_type(), |
| DataType::Binary | DataType::LargeBinary |
| )); |
| self.insert_if_new_inner::<MP, OP, GenericBinaryType<O>>( |
| values, |
| make_payload_fn, |
| observe_payload_fn, |
| ) |
| } |
| OutputType::Utf8 => { |
| assert!(matches!( |
| values.data_type(), |
| DataType::Utf8 | DataType::LargeUtf8 |
| )); |
| self.insert_if_new_inner::<MP, OP, GenericStringType<O>>( |
| values, |
| make_payload_fn, |
| observe_payload_fn, |
| ) |
| } |
| _ => unreachable!("View types should use `ArrowBytesViewMap`"), |
| }; |
| } |
| |
| /// Generic version of [`Self::insert_if_new`] that handles `ByteArrayType` |
| /// (both String and Binary) |
| /// |
| /// Note this is the only function that is generic on [`ByteArrayType`], which |
| /// avoids having to template the entire structure, making the code |
| /// simpler and understand and reducing code bloat due to duplication. |
| /// |
| /// See comments on `insert_if_new` for more details |
| fn insert_if_new_inner<MP, OP, B>( |
| &mut self, |
| values: &ArrayRef, |
| mut make_payload_fn: MP, |
| mut observe_payload_fn: OP, |
| ) where |
| MP: FnMut(Option<&[u8]>) -> V, |
| OP: FnMut(V), |
| B: ByteArrayType, |
| { |
| // step 1: compute hashes |
| let batch_hashes = &mut self.hashes_buffer; |
| batch_hashes.clear(); |
| batch_hashes.resize(values.len(), 0); |
| create_hashes([values], &self.random_state, batch_hashes) |
| // hash is supported for all types and create_hashes only |
| // returns errors for unsupported types |
| .unwrap(); |
| |
| // step 2: insert each value into the set, if not already present |
| let values = values.as_bytes::<B>(); |
| |
| // Ensure lengths are equivalent |
| assert_eq!(values.len(), batch_hashes.len()); |
| |
| for (value, &hash) in values.iter().zip(batch_hashes.iter()) { |
| // handle null value |
| let Some(value) = value else { |
| let payload = if let Some(&(payload, _offset)) = self.null.as_ref() { |
| payload |
| } else { |
| let payload = make_payload_fn(None); |
| let null_index = self.offsets.len() - 1; |
| // nulls need a zero length in the offset buffer |
| let offset = self.buffer.len(); |
| self.offsets.push(O::usize_as(offset)); |
| self.null = Some((payload, null_index)); |
| payload |
| }; |
| observe_payload_fn(payload); |
| continue; |
| }; |
| |
| // get the value as bytes |
| let value: &[u8] = value.as_ref(); |
| let value_len = O::usize_as(value.len()); |
| |
| // value is "small" |
| let payload = if value.len() <= SHORT_VALUE_LEN { |
| let inline = value.iter().fold(0usize, |acc, &x| (acc << 8) | x as usize); |
| |
| // is value is already present in the set? |
| let entry = self.map.find_mut(hash, |header| { |
| // compare value if hashes match |
| if header.len != value_len { |
| return false; |
| } |
| // value is stored inline so no need to consult buffer |
| // (this is the "small string optimization") |
| inline == header.offset_or_inline |
| }); |
| |
| if let Some(entry) = entry { |
| entry.payload |
| } |
| // if no existing entry, make a new one |
| else { |
| // Put the small values into buffer and offsets so it appears |
| // the output array, but store the actual bytes inline for |
| // comparison |
| self.buffer.append_slice(value); |
| self.offsets.push(O::usize_as(self.buffer.len())); |
| let payload = make_payload_fn(Some(value)); |
| let new_header = Entry { |
| hash, |
| len: value_len, |
| offset_or_inline: inline, |
| payload, |
| }; |
| self.map.insert_accounted( |
| new_header, |
| |header| header.hash, |
| &mut self.map_size, |
| ); |
| payload |
| } |
| } |
| // value is not "small" |
| else { |
| // Check if the value is already present in the set |
| let entry = self.map.find_mut(hash, |header| { |
| // compare value if hashes match |
| if header.len != value_len { |
| return false; |
| } |
| // Need to compare the bytes in the buffer |
| // SAFETY: buffer is only appended to, and we correctly inserted values and offsets |
| let existing_value = |
| unsafe { self.buffer.as_slice().get_unchecked(header.range()) }; |
| value == existing_value |
| }); |
| |
| if let Some(entry) = entry { |
| entry.payload |
| } |
| // if no existing entry, make a new one |
| else { |
| // Put the small values into buffer and offsets so it |
| // appears the output array, and store that offset |
| // so the bytes can be compared if needed |
| let offset = self.buffer.len(); // offset of start for data |
| self.buffer.append_slice(value); |
| self.offsets.push(O::usize_as(self.buffer.len())); |
| |
| let payload = make_payload_fn(Some(value)); |
| let new_header = Entry { |
| hash, |
| len: value_len, |
| offset_or_inline: offset, |
| payload, |
| }; |
| self.map.insert_accounted( |
| new_header, |
| |header| header.hash, |
| &mut self.map_size, |
| ); |
| payload |
| } |
| }; |
| observe_payload_fn(payload); |
| } |
| // Check for overflow in offsets (if more data was sent than can be represented) |
| if O::from_usize(self.buffer.len()).is_none() { |
| panic!( |
| "Put {} bytes in buffer, more than can be represented by a {}", |
| self.buffer.len(), |
| type_name::<O>() |
| ); |
| } |
| } |
| |
| /// Converts this set into a `StringArray`, `LargeStringArray`, |
| /// `BinaryArray`, or `LargeBinaryArray` containing each distinct value |
| /// that was inserted. This is done without copying the values. |
| /// |
| /// The values are guaranteed to be returned in the same order in which |
| /// they were first seen. |
| pub fn into_state(self) -> ArrayRef { |
| let Self { |
| output_type, |
| map: _, |
| map_size: _, |
| offsets, |
| mut buffer, |
| random_state: _, |
| hashes_buffer: _, |
| null, |
| } = self; |
| |
| // Only make a `NullBuffer` if there was a null value |
| let nulls = null.map(|(_payload, null_index)| { |
| let num_values = offsets.len() - 1; |
| single_null_buffer(num_values, null_index) |
| }); |
| // SAFETY: the offsets were constructed correctly in `insert_if_new` -- |
| // monotonically increasing, overflows were checked. |
| let offsets = unsafe { OffsetBuffer::new_unchecked(ScalarBuffer::from(offsets)) }; |
| let values = buffer.finish(); |
| |
| match output_type { |
| OutputType::Binary => { |
| // SAFETY: the offsets were constructed correctly |
| Arc::new(unsafe { |
| GenericBinaryArray::new_unchecked(offsets, values, nulls) |
| }) |
| } |
| OutputType::Utf8 => { |
| // SAFETY: |
| // 1. the offsets were constructed safely |
| // |
| // 2. we asserted the input arrays were all the correct type and |
| // thus since all the values that went in were valid (e.g. utf8) |
| // so are all the values that come out |
| Arc::new(unsafe { |
| GenericStringArray::new_unchecked(offsets, values, nulls) |
| }) |
| } |
| _ => unreachable!("View types should use `ArrowBytesViewMap`"), |
| } |
| } |
| |
| /// Total number of entries (including null, if present) |
| pub fn len(&self) -> usize { |
| self.non_null_len() + self.null.map(|_| 1).unwrap_or(0) |
| } |
| |
| /// Is the set empty? |
| pub fn is_empty(&self) -> bool { |
| self.map.is_empty() && self.null.is_none() |
| } |
| |
| /// Number of non null entries |
| pub fn non_null_len(&self) -> usize { |
| self.map.len() |
| } |
| |
| /// Return the total size, in bytes, of memory used to store the data in |
| /// this set, not including `self` |
| pub fn size(&self) -> usize { |
| self.map_size |
| + self.buffer.capacity() * size_of::<u8>() |
| + self.offsets.allocated_size() |
| + self.hashes_buffer.allocated_size() |
| } |
| } |
| |
| /// Returns a `NullBuffer` with a single null value at the given index |
| fn single_null_buffer(num_values: usize, null_index: usize) -> NullBuffer { |
| let mut null_builder = NullBufferBuilder::new(num_values); |
| null_builder.append_n_non_nulls(null_index); |
| null_builder.append_null(); |
| null_builder.append_n_non_nulls(num_values - null_index - 1); |
| // SAFETY: inner builder must be constructed |
| null_builder.finish().unwrap() |
| } |
| |
| impl<O: OffsetSizeTrait, V> Debug for ArrowBytesMap<O, V> |
| where |
| V: Debug + PartialEq + Eq + Clone + Copy + Default, |
| { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_struct("ArrowBytesMap") |
| .field("map", &"<map>") |
| .field("map_size", &self.map_size) |
| .field("buffer", &self.buffer) |
| .field("random_state", &self.random_state) |
| .field("hashes_buffer", &self.hashes_buffer) |
| .finish() |
| } |
| } |
| |
| /// Maximum size of a value that can be inlined in the hash table |
| const SHORT_VALUE_LEN: usize = size_of::<usize>(); |
| |
| /// Entry in the hash table -- see [`ArrowBytesMap`] for more details |
| #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] |
| struct Entry<O, V> |
| where |
| O: OffsetSizeTrait, |
| V: Debug + PartialEq + Eq + Clone + Copy + Default, |
| { |
| /// hash of the value (stored to avoid recomputing it in hash table check) |
| hash: u64, |
| /// if len =< [`SHORT_VALUE_LEN`]: the data inlined |
| /// if len > [`SHORT_VALUE_LEN`], the offset of where the data starts |
| offset_or_inline: usize, |
| /// length of the value, in bytes (use O here so we use only i32 for |
| /// strings, rather 64 bit usize) |
| len: O, |
| /// value stored by the entry |
| payload: V, |
| } |
| |
| impl<O, V> Entry<O, V> |
| where |
| O: OffsetSizeTrait, |
| V: Debug + PartialEq + Eq + Clone + Copy + Default, |
| { |
| /// returns self.offset..self.offset + self.len |
| #[inline(always)] |
| fn range(&self) -> Range<usize> { |
| self.offset_or_inline..self.offset_or_inline + self.len.as_usize() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use arrow::array::{BinaryArray, LargeBinaryArray, StringArray}; |
| use std::collections::HashMap; |
| |
| #[test] |
| fn string_set_empty() { |
| let mut set = ArrowBytesSet::<i32>::new(OutputType::Utf8); |
| let array: ArrayRef = Arc::new(StringArray::new_null(0)); |
| set.insert(&array); |
| assert_eq!(set.len(), 0); |
| assert_eq!(set.non_null_len(), 0); |
| assert_set(set, &[]); |
| } |
| |
| #[test] |
| fn string_set_one_null() { |
| let mut set = ArrowBytesSet::<i32>::new(OutputType::Utf8); |
| let array: ArrayRef = Arc::new(StringArray::new_null(1)); |
| set.insert(&array); |
| assert_eq!(set.len(), 1); |
| assert_eq!(set.non_null_len(), 0); |
| assert_set(set, &[None]); |
| } |
| |
| #[test] |
| fn string_set_many_null() { |
| let mut set = ArrowBytesSet::<i32>::new(OutputType::Utf8); |
| let array: ArrayRef = Arc::new(StringArray::new_null(11)); |
| set.insert(&array); |
| assert_eq!(set.len(), 1); |
| assert_eq!(set.non_null_len(), 0); |
| assert_set(set, &[None]); |
| } |
| |
| #[test] |
| fn string_set_basic_i32() { |
| test_string_set_basic::<i32>(); |
| } |
| |
| #[test] |
| fn string_set_basic_i64() { |
| test_string_set_basic::<i64>(); |
| } |
| |
| fn test_string_set_basic<O: OffsetSizeTrait>() { |
| // basic test for mixed small and large string values |
| let values = GenericStringArray::<O>::from(vec![ |
| Some("a"), |
| Some("b"), |
| Some("CXCCCCCCCC"), // 10 bytes |
| Some(""), |
| Some("cbcxx"), // 5 bytes |
| None, |
| Some("AAAAAAAA"), // 8 bytes |
| Some("BBBBBQBBB"), // 9 bytes |
| Some("a"), |
| Some("cbcxx"), |
| Some("b"), |
| Some("cbcxx"), |
| Some(""), |
| None, |
| Some("BBBBBQBBB"), |
| Some("BBBBBQBBB"), |
| Some("AAAAAAAA"), |
| Some("CXCCCCCCCC"), |
| ]); |
| |
| let mut set = ArrowBytesSet::<O>::new(OutputType::Utf8); |
| let array: ArrayRef = Arc::new(values); |
| set.insert(&array); |
| // values mut appear be in the order they were inserted |
| assert_set( |
| set, |
| &[ |
| Some("a"), |
| Some("b"), |
| Some("CXCCCCCCCC"), |
| Some(""), |
| Some("cbcxx"), |
| None, |
| Some("AAAAAAAA"), |
| Some("BBBBBQBBB"), |
| ], |
| ); |
| } |
| |
| #[test] |
| fn string_set_non_utf8_32() { |
| test_string_set_non_utf8::<i32>(); |
| } |
| |
| #[test] |
| fn string_set_non_utf8_64() { |
| test_string_set_non_utf8::<i64>(); |
| } |
| |
| fn test_string_set_non_utf8<O: OffsetSizeTrait>() { |
| // basic test for mixed small and large string values |
| let values = GenericStringArray::<O>::from(vec![ |
| Some("a"), |
| Some("✨🔥"), |
| Some("🔥"), |
| Some("✨✨✨"), |
| Some("foobarbaz"), |
| Some("🔥"), |
| Some("✨🔥"), |
| ]); |
| |
| let mut set = ArrowBytesSet::<O>::new(OutputType::Utf8); |
| let array: ArrayRef = Arc::new(values); |
| set.insert(&array); |
| // strings mut appear be in the order they were inserted |
| assert_set( |
| set, |
| &[ |
| Some("a"), |
| Some("✨🔥"), |
| Some("🔥"), |
| Some("✨✨✨"), |
| Some("foobarbaz"), |
| ], |
| ); |
| } |
| |
| // asserts that the set contains the expected strings, in the same order |
| fn assert_set<O: OffsetSizeTrait>(set: ArrowBytesSet<O>, expected: &[Option<&str>]) { |
| let strings = set.into_state(); |
| let strings = strings.as_string::<O>(); |
| let state = strings.into_iter().collect::<Vec<_>>(); |
| assert_eq!(state, expected); |
| } |
| |
| // Test use of binary output type |
| #[test] |
| fn test_binary_set() { |
| let values: ArrayRef = Arc::new(BinaryArray::from_opt_vec(vec![ |
| Some(b"a"), |
| Some(b"CXCCCCCCCC"), |
| None, |
| Some(b"CXCCCCCCCC"), |
| ])); |
| |
| let expected: ArrayRef = Arc::new(BinaryArray::from_opt_vec(vec![ |
| Some(b"a"), |
| Some(b"CXCCCCCCCC"), |
| None, |
| ])); |
| |
| let mut set = ArrowBytesSet::<i32>::new(OutputType::Binary); |
| set.insert(&values); |
| assert_eq!(&set.into_state(), &expected); |
| } |
| |
| // Test use of binary output type |
| #[test] |
| fn test_large_binary_set() { |
| let values: ArrayRef = Arc::new(LargeBinaryArray::from_opt_vec(vec![ |
| Some(b"a"), |
| Some(b"CXCCCCCCCC"), |
| None, |
| Some(b"CXCCCCCCCC"), |
| ])); |
| |
| let expected: ArrayRef = Arc::new(LargeBinaryArray::from_opt_vec(vec![ |
| Some(b"a"), |
| Some(b"CXCCCCCCCC"), |
| None, |
| ])); |
| |
| let mut set = ArrowBytesSet::<i64>::new(OutputType::Binary); |
| set.insert(&values); |
| assert_eq!(&set.into_state(), &expected); |
| } |
| |
| #[test] |
| #[should_panic( |
| expected = "matches!(values.data_type(), DataType::Utf8 | DataType::LargeUtf8)" |
| )] |
| fn test_mismatched_types() { |
| // inserting binary into a set that expects strings should panic |
| let values: ArrayRef = Arc::new(LargeBinaryArray::from_opt_vec(vec![Some(b"a")])); |
| |
| let mut set = ArrowBytesSet::<i64>::new(OutputType::Utf8); |
| set.insert(&values); |
| } |
| |
| #[test] |
| #[should_panic] |
| fn test_mismatched_sizes() { |
| // inserting large strings into a set that expects small should panic |
| let values: ArrayRef = Arc::new(LargeBinaryArray::from_opt_vec(vec![Some(b"a")])); |
| |
| let mut set = ArrowBytesSet::<i32>::new(OutputType::Binary); |
| set.insert(&values); |
| } |
| |
| // put more than 2GB in a string set and expect it to panic |
| #[test] |
| #[should_panic( |
| expected = "Put 2147483648 bytes in buffer, more than can be represented by a i32" |
| )] |
| fn test_string_overflow() { |
| let mut set = ArrowBytesSet::<i32>::new(OutputType::Utf8); |
| for value in ["a", "b", "c"] { |
| // 1GB strings, so 3rd is over 2GB and should panic |
| let arr: ArrayRef = |
| Arc::new(StringArray::from_iter_values([value.repeat(1 << 30)])); |
| set.insert(&arr); |
| } |
| } |
| |
| // inserting strings into the set does not increase reported memory |
| #[test] |
| fn test_string_set_memory_usage() { |
| let strings1 = GenericStringArray::<i32>::from(vec![ |
| Some("a"), |
| Some("b"), |
| Some("CXCCCCCCCC"), // 10 bytes |
| Some("AAAAAAAA"), // 8 bytes |
| Some("BBBBBQBBB"), // 9 bytes |
| ]); |
| let total_strings1_len = strings1 |
| .iter() |
| .map(|s| s.map(|s| s.len()).unwrap_or(0)) |
| .sum::<usize>(); |
| let values1: ArrayRef = Arc::new(GenericStringArray::<i32>::from(strings1)); |
| |
| // Much larger strings in strings2 |
| let strings2 = GenericStringArray::<i32>::from(vec![ |
| "FOO".repeat(1000), |
| "BAR".repeat(2000), |
| "BAZ".repeat(3000), |
| ]); |
| let total_strings2_len = strings2 |
| .iter() |
| .map(|s| s.map(|s| s.len()).unwrap_or(0)) |
| .sum::<usize>(); |
| let values2: ArrayRef = Arc::new(GenericStringArray::<i32>::from(strings2)); |
| |
| let mut set = ArrowBytesSet::<i32>::new(OutputType::Utf8); |
| let size_empty = set.size(); |
| |
| set.insert(&values1); |
| let size_after_values1 = set.size(); |
| assert!(size_empty < size_after_values1); |
| assert!( |
| size_after_values1 > total_strings1_len, |
| "expect {size_after_values1} to be more than {total_strings1_len}" |
| ); |
| assert!(size_after_values1 < total_strings1_len + total_strings2_len); |
| |
| // inserting the same strings should not affect the size |
| set.insert(&values1); |
| assert_eq!(set.size(), size_after_values1); |
| |
| // inserting the large strings should increase the reported size |
| set.insert(&values2); |
| let size_after_values2 = set.size(); |
| assert!(size_after_values2 > size_after_values1); |
| assert!(size_after_values2 > total_strings1_len + total_strings2_len); |
| } |
| |
| #[test] |
| fn test_map() { |
| let input = vec![ |
| // Note mix of short/long strings |
| Some("A"), |
| Some("bcdefghijklmnop"), |
| Some("X"), |
| Some("Y"), |
| None, |
| Some("qrstuvqxyzhjwya"), |
| Some("✨🔥"), |
| Some("🔥"), |
| Some("🔥🔥🔥🔥🔥🔥"), |
| ]; |
| |
| let mut test_map = TestMap::new(); |
| test_map.insert(&input); |
| test_map.insert(&input); // put it in twice |
| let expected_output: ArrayRef = Arc::new(StringArray::from(input)); |
| assert_eq!(&test_map.into_array(), &expected_output); |
| } |
| |
| #[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] |
| struct TestPayload { |
| // store the string value to check against input |
| index: usize, // store the index of the string (each new string gets the next sequential input) |
| } |
| |
| /// Wraps an [`ArrowBytesMap`], validating its invariants |
| struct TestMap { |
| map: ArrowBytesMap<i32, TestPayload>, |
| // stores distinct strings seen, in order |
| strings: Vec<Option<String>>, |
| // map strings to index in strings |
| indexes: HashMap<Option<String>, usize>, |
| } |
| |
| impl Debug for TestMap { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_struct("TestMap") |
| .field("map", &"...") |
| .field("strings", &self.strings) |
| .field("indexes", &self.indexes) |
| .finish() |
| } |
| } |
| |
| impl TestMap { |
| /// creates a map with TestPayloads for the given strings and then |
| /// validates the payloads |
| fn new() -> Self { |
| Self { |
| map: ArrowBytesMap::new(OutputType::Utf8), |
| strings: vec![], |
| indexes: HashMap::new(), |
| } |
| } |
| |
| /// Inserts strings into the map |
| fn insert(&mut self, strings: &[Option<&str>]) { |
| let string_array = StringArray::from(strings.to_vec()); |
| let arr: ArrayRef = Arc::new(string_array); |
| |
| let mut next_index = self.indexes.len(); |
| let mut actual_new_strings = vec![]; |
| let mut actual_seen_indexes = vec![]; |
| // update self with new values, keeping track of newly added values |
| for str in strings { |
| let str = str.map(|s| s.to_string()); |
| let index = self.indexes.get(&str).cloned().unwrap_or_else(|| { |
| actual_new_strings.push(str.clone()); |
| let index = self.strings.len(); |
| self.strings.push(str.clone()); |
| self.indexes.insert(str, index); |
| index |
| }); |
| actual_seen_indexes.push(index); |
| } |
| |
| // insert the values into the map, recording what we did |
| let mut seen_new_strings = vec![]; |
| let mut seen_indexes = vec![]; |
| self.map.insert_if_new( |
| &arr, |
| |s| { |
| let value = s |
| .map(|s| String::from_utf8(s.to_vec()).expect("Non utf8 string")); |
| let index = next_index; |
| next_index += 1; |
| seen_new_strings.push(value); |
| TestPayload { index } |
| }, |
| |payload| { |
| seen_indexes.push(payload.index); |
| }, |
| ); |
| |
| assert_eq!(actual_seen_indexes, seen_indexes); |
| assert_eq!(actual_new_strings, seen_new_strings); |
| } |
| |
| /// Call `self.map.into_array()` validating that the strings are in the same |
| /// order as they were inserted |
| fn into_array(self) -> ArrayRef { |
| let Self { |
| map, |
| strings, |
| indexes: _, |
| } = self; |
| |
| let arr = map.into_state(); |
| let expected: ArrayRef = Arc::new(StringArray::from(strings)); |
| assert_eq!(&arr, &expected); |
| arr |
| } |
| } |
| } |