1use std::fmt::{Display, Formatter};
26use std::sync::Arc;
27
28use datafusion_common::arrow::array::{Array, ArrayRef, AsArray, Int32Array};
29use datafusion_common::arrow::compute::take;
30use datafusion_common::arrow::datatypes::{
31 DataType, Int8Type, Int16Type, Int32Type, Int64Type, UInt8Type, UInt16Type, UInt32Type,
32 UInt64Type,
33};
34use datafusion_common::{DataFusionError, ScalarValue};
35use datafusion_expr::{
36 ColumnarValue, Expr, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, Volatility,
37};
38use datatypes::prelude::ConcreteDataType;
39use datatypes::value::Value;
40use serde::{Deserialize, Serialize};
41
42use crate::error::{self, Result};
43
44#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
51pub struct IcebergHash(String);
52
53impl IcebergHash {
54 pub const NAME: &str = "iceberg_hash";
55
56 pub fn new(column: impl Into<String>) -> Self {
57 Self(column.into())
58 }
59
60 pub fn column(&self) -> &str {
61 &self.0
62 }
63
64 pub fn accepts(data_type: &ConcreteDataType) -> bool {
65 data_type.is_string() || data_type.is_unsigned() || is_signed_integer(data_type)
66 }
67
68 pub fn output_type() -> ConcreteDataType {
70 ConcreteDataType::int32_datatype()
71 }
72
73 pub fn evaluate_value(value: &Value) -> Result<Value> {
74 let hash = match value {
75 Value::Null => return Ok(Value::Null),
76 Value::String(v) => murmur3_32(v.as_utf8().as_bytes()),
77 Value::Int8(v) => hash_long(*v as i64),
78 Value::Int16(v) => hash_long(*v as i64),
79 Value::Int32(v) => hash_long(*v as i64),
80 Value::Int64(v) => hash_long(*v),
81 Value::UInt8(v) => hash_long(*v as i64),
82 Value::UInt16(v) => hash_long(*v as i64),
83 Value::UInt32(v) => hash_long(*v as i64),
84 Value::UInt64(v) => hash_long(*v as i64),
85 other => {
86 return error::UnexpectedSnafu {
87 err_msg: format!("Unsupported value for {}: {other:?}", Self::NAME),
88 }
89 .fail();
90 }
91 };
92 Ok(Value::Int32(hash))
93 }
94
95 pub fn evaluate_array(array: &dyn Array) -> datafusion_common::Result<ArrayRef> {
96 iceberg_hash_array(array)
97 }
98
99 pub fn to_logical_expr(&self) -> Expr {
100 let column = Expr::Column(datafusion_common::Column::from_name(self.column()));
101 ScalarUDF::new_from_impl(IcebergHashUdf::default()).call(vec![column])
102 }
103}
104
105impl Display for IcebergHash {
106 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
107 write!(f, "{}({})", Self::NAME, self.column())
108 }
109}
110
111fn is_signed_integer(data_type: &ConcreteDataType) -> bool {
112 matches!(
113 data_type,
114 ConcreteDataType::Int8(_)
115 | ConcreteDataType::Int16(_)
116 | ConcreteDataType::Int32(_)
117 | ConcreteDataType::Int64(_)
118 )
119}
120
121fn iceberg_hash_array(array: &dyn Array) -> datafusion_common::Result<ArrayRef> {
122 let hashes: Int32Array = match array.data_type() {
123 DataType::Utf8 => array
124 .as_string::<i32>()
125 .iter()
126 .map(|v| v.map(|v| murmur3_32(v.as_bytes())))
127 .collect(),
128 DataType::LargeUtf8 => array
129 .as_string::<i64>()
130 .iter()
131 .map(|v| v.map(|v| murmur3_32(v.as_bytes())))
132 .collect(),
133 DataType::Utf8View => array
134 .as_string_view()
135 .iter()
136 .map(|v| v.map(|v| murmur3_32(v.as_bytes())))
137 .collect(),
138 DataType::Int8 => array
139 .as_primitive::<Int8Type>()
140 .unary(|v| hash_long(v as i64)),
141 DataType::Int16 => array
142 .as_primitive::<Int16Type>()
143 .unary(|v| hash_long(v as i64)),
144 DataType::Int32 => array
145 .as_primitive::<Int32Type>()
146 .unary(|v| hash_long(v as i64)),
147 DataType::Int64 => array.as_primitive::<Int64Type>().unary(hash_long),
148 DataType::UInt8 => array
149 .as_primitive::<UInt8Type>()
150 .unary(|v| hash_long(v as i64)),
151 DataType::UInt16 => array
152 .as_primitive::<UInt16Type>()
153 .unary(|v| hash_long(v as i64)),
154 DataType::UInt32 => array
155 .as_primitive::<UInt32Type>()
156 .unary(|v| hash_long(v as i64)),
157 DataType::UInt64 => array
158 .as_primitive::<UInt64Type>()
159 .unary(|v| hash_long(v as i64)),
160 DataType::Dictionary(_, _) => {
161 let dictionary = array.as_any_dictionary();
162 let values = iceberg_hash_array(dictionary.values().as_ref())?;
163 return Ok(take(values.as_ref(), dictionary.keys(), None)?);
164 }
165 other => {
166 return Err(DataFusionError::Execution(format!(
167 "{} does not support type {other}",
168 IcebergHash::NAME
169 )));
170 }
171 };
172 Ok(Arc::new(hashes))
173}
174
175fn hash_long(value: i64) -> i32 {
176 murmur3_32(&value.to_le_bytes())
177}
178
179fn murmur3_32(data: &[u8]) -> i32 {
181 const C1: u32 = 0xcc9e_2d51;
182 const C2: u32 = 0x1b87_3593;
183
184 fn mix_k(k: u32) -> u32 {
185 k.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2)
186 }
187
188 let mut h = 0u32;
189 let (blocks, tail) = data.as_chunks::<4>();
190 for block in blocks {
191 h = (h ^ mix_k(u32::from_le_bytes(*block)))
192 .rotate_left(13)
193 .wrapping_mul(5)
194 .wrapping_add(0xe654_6b64);
195 }
196 if !tail.is_empty() {
197 let k = tail
198 .iter()
199 .rev()
200 .fold(0u32, |k, byte| (k << 8) | *byte as u32);
201 h ^= mix_k(k);
202 }
203
204 h ^= data.len() as u32;
206 h ^= h >> 16;
207 h = h.wrapping_mul(0x85eb_ca6b);
208 h ^= h >> 13;
209 h = h.wrapping_mul(0xc2b2_ae35);
210 h ^= h >> 16;
211 h as i32
212}
213
214#[derive(Debug, PartialEq, Eq, Hash)]
217struct IcebergHashUdf {
218 signature: Signature,
219}
220
221impl Default for IcebergHashUdf {
222 fn default() -> Self {
223 Self {
224 signature: Signature::any(1, Volatility::Immutable),
225 }
226 }
227}
228
229impl ScalarUDFImpl for IcebergHashUdf {
230 fn name(&self) -> &str {
231 IcebergHash::NAME
232 }
233
234 fn signature(&self) -> &Signature {
235 &self.signature
236 }
237
238 fn return_type(&self, _arg_types: &[DataType]) -> datafusion_common::Result<DataType> {
239 Ok(DataType::Int32)
240 }
241
242 fn invoke_with_args(
243 &self,
244 args: ScalarFunctionArgs,
245 ) -> datafusion_common::Result<ColumnarValue> {
246 let [arg] = args.args.as_slice() else {
247 return Err(DataFusionError::Internal(format!(
248 "{} expects one argument, got {}",
249 IcebergHash::NAME,
250 args.args.len()
251 )));
252 };
253 match arg {
254 ColumnarValue::Array(array) => {
255 Ok(ColumnarValue::Array(iceberg_hash_array(array.as_ref())?))
256 }
257 ColumnarValue::Scalar(scalar) => {
258 let hashes = iceberg_hash_array(scalar.to_array()?.as_ref())?;
259 Ok(ColumnarValue::Scalar(ScalarValue::try_from_array(
260 &hashes, 0,
261 )?))
262 }
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use datafusion_common::arrow::array::{
270 Int64Array, StringArray, StringDictionaryBuilder, StringViewArray, UInt64Array,
271 };
272 use datafusion_common::arrow::datatypes::UInt16Type;
273
274 use super::*;
275
276 #[test]
277 fn test_iceberg_spec_vectors() {
278 assert_eq!(hash_long(34), 2017239379);
280 assert_eq!(hash_long(17486), -653330422);
282 assert_eq!(hash_long(81068000000), -662762989);
284 assert_eq!(murmur3_32("iceberg".as_bytes()), 1210000089);
285 assert_eq!(murmur3_32(&[0, 1, 2, 3]), -188683207);
286 }
287
288 #[test]
289 fn test_unsigned_policy() {
290 assert_eq!(
291 IcebergHash::evaluate_value(&Value::UInt64(34)).unwrap(),
292 Value::Int32(2017239379)
293 );
294 assert_eq!(
295 IcebergHash::evaluate_value(&Value::UInt64(u64::MAX)).unwrap(),
296 Value::Int32(hash_long(-1))
297 );
298 }
299
300 #[test]
301 fn test_array_matches_value() {
302 let strings = [Some("iceberg"), None, Some(""), Some("中🙂")];
303 let mut dictionary = StringDictionaryBuilder::<UInt16Type>::new();
304 for v in strings {
305 dictionary.append_option(v);
306 }
307 let string_arrays: Vec<ArrayRef> = vec![
308 Arc::new(StringArray::from(strings.to_vec())),
309 Arc::new(StringViewArray::from(strings.to_vec())),
310 Arc::new(dictionary.finish()),
311 ];
312 let string_values = strings.map(|v| v.map_or(Value::Null, Value::from));
313
314 let ints = [Some(34), None, Some(i64::MIN), Some(-1)];
315 let unsigned = [Some(34), None, Some(u64::MAX), Some(0)];
316 let cases = string_arrays
317 .into_iter()
318 .map(|array| (array, string_values.to_vec()))
319 .chain([
320 (
321 Arc::new(Int64Array::from(ints.to_vec())) as ArrayRef,
322 ints.map(|v| v.map_or(Value::Null, Value::Int64)).to_vec(),
323 ),
324 (
325 Arc::new(UInt64Array::from(unsigned.to_vec())) as ArrayRef,
326 unsigned
327 .map(|v| v.map_or(Value::Null, Value::UInt64))
328 .to_vec(),
329 ),
330 ]);
331 for (array, values) in cases {
332 let hashes = IcebergHash::evaluate_array(array.as_ref()).unwrap();
333 let hashes = hashes.as_primitive::<Int32Type>();
334 for (row, value) in values.iter().enumerate() {
335 let expected = IcebergHash::evaluate_value(value).unwrap();
336 let actual = if hashes.is_null(row) {
337 Value::Null
338 } else {
339 Value::Int32(hashes.value(row))
340 };
341 assert_eq!(actual, expected, "{:?} row {row}", array.data_type());
342 }
343 }
344 }
345}