Skip to content

Commit a03000d

Browse files
authored
feat: support transform function (#42)
* support transform function and add related test: 1. identity 2. void 3. temporal * 1. remove unsupport type in iceberg 2. refactor error handle * add license * clean code * 1. use arrow-* instead of arrow 2. refine test * support bucket and truncate transform function * make code more clear * fix check * fix to truncate Unicode correctly and add related test * fix cargo-sort --------- Co-authored-by: ZENOTME <st810918843@gmail.com>
1 parent 13281d3 commit a03000d

8 files changed

Lines changed: 989 additions & 0 deletions

File tree

crates/iceberg/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ keywords = ["iceberg"]
2929
[dependencies]
3030
anyhow = "1.0.72"
3131
apache-avro = "0.15"
32+
arrow-arith = { version = ">=46" }
33+
arrow-array = { version = ">=46" }
34+
arrow-schema = { version = ">=46" }
3235
async-trait = "0.1"
3336
bimap = "0.6"
3437
bitvec = "1.0.1"
@@ -38,6 +41,7 @@ either = "1"
3841
futures = "0.3"
3942
itertools = "0.11"
4043
lazy_static = "1"
44+
murmur3 = "0.5.2"
4145
once_cell = "1"
4246
opendal = "0.39"
4347
ordered-float = "3.7.0"

crates/iceberg/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ pub mod table;
3737
mod avro;
3838
pub mod io;
3939
pub mod spec;
40+
41+
pub mod transform;
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::sync::Arc;
19+
20+
use arrow_array::ArrayRef;
21+
use arrow_schema::{DataType, TimeUnit};
22+
23+
use super::TransformFunction;
24+
25+
pub struct Bucket {
26+
mod_n: u32,
27+
}
28+
29+
impl Bucket {
30+
pub fn new(mod_n: u32) -> Self {
31+
Self { mod_n }
32+
}
33+
}
34+
35+
impl Bucket {
36+
/// When switch the hash function, we only need to change this function.
37+
fn hash_bytes(mut v: &[u8]) -> i32 {
38+
murmur3::murmur3_32(&mut v, 0).unwrap() as i32
39+
}
40+
41+
fn hash_int(v: i32) -> i32 {
42+
Self::hash_long(v as i64)
43+
}
44+
45+
fn hash_long(v: i64) -> i32 {
46+
Self::hash_bytes(v.to_le_bytes().as_slice())
47+
}
48+
49+
/// v is days from unix epoch
50+
fn hash_date(v: i32) -> i32 {
51+
Self::hash_int(v)
52+
}
53+
54+
/// v is microseconds from midnight
55+
fn hash_time(v: i64) -> i32 {
56+
Self::hash_long(v)
57+
}
58+
59+
/// v is microseconds from unix epoch
60+
fn hash_timestamp(v: i64) -> i32 {
61+
Self::hash_long(v)
62+
}
63+
64+
fn hash_str(s: &str) -> i32 {
65+
Self::hash_bytes(s.as_bytes())
66+
}
67+
68+
/// Decimal values are hashed using the minimum number of bytes required to hold the unscaled value as a two’s complement big-endian
69+
/// ref: https://iceberg.apache.org/spec/#appendix-b-32-bit-hash-requirements
70+
fn hash_decimal(v: i128) -> i32 {
71+
let bytes = v.to_be_bytes();
72+
if let Some(start) = bytes.iter().position(|&x| x != 0) {
73+
Self::hash_bytes(&bytes[start..])
74+
} else {
75+
Self::hash_bytes(&[0])
76+
}
77+
}
78+
79+
/// def bucket_N(x) = (murmur3_x86_32_hash(x) & Integer.MAX_VALUE) % N
80+
/// ref: https://iceberg.apache.org/spec/#partitioning
81+
fn bucket_n(&self, v: i32) -> i32 {
82+
(v & i32::MAX) % (self.mod_n as i32)
83+
}
84+
}
85+
86+
impl TransformFunction for Bucket {
87+
fn transform(&self, input: ArrayRef) -> crate::Result<ArrayRef> {
88+
let res: arrow_array::Int32Array = match input.data_type() {
89+
DataType::Int32 => input
90+
.as_any()
91+
.downcast_ref::<arrow_array::Int32Array>()
92+
.unwrap()
93+
.unary(|v| self.bucket_n(Self::hash_int(v))),
94+
DataType::Int64 => input
95+
.as_any()
96+
.downcast_ref::<arrow_array::Int64Array>()
97+
.unwrap()
98+
.unary(|v| self.bucket_n(Self::hash_long(v))),
99+
DataType::Decimal128(_, _) => input
100+
.as_any()
101+
.downcast_ref::<arrow_array::Decimal128Array>()
102+
.unwrap()
103+
.unary(|v| self.bucket_n(Self::hash_decimal(v))),
104+
DataType::Date32 => input
105+
.as_any()
106+
.downcast_ref::<arrow_array::Date32Array>()
107+
.unwrap()
108+
.unary(|v| self.bucket_n(Self::hash_date(v))),
109+
DataType::Time64(TimeUnit::Microsecond) => input
110+
.as_any()
111+
.downcast_ref::<arrow_array::Time64MicrosecondArray>()
112+
.unwrap()
113+
.unary(|v| self.bucket_n(Self::hash_time(v))),
114+
DataType::Timestamp(TimeUnit::Microsecond, _) => input
115+
.as_any()
116+
.downcast_ref::<arrow_array::TimestampMicrosecondArray>()
117+
.unwrap()
118+
.unary(|v| self.bucket_n(Self::hash_timestamp(v))),
119+
DataType::Utf8 => arrow_array::Int32Array::from_iter(
120+
input
121+
.as_any()
122+
.downcast_ref::<arrow_array::StringArray>()
123+
.unwrap()
124+
.iter()
125+
.map(|v| self.bucket_n(Self::hash_str(v.unwrap()))),
126+
),
127+
DataType::LargeUtf8 => arrow_array::Int32Array::from_iter(
128+
input
129+
.as_any()
130+
.downcast_ref::<arrow_array::LargeStringArray>()
131+
.unwrap()
132+
.iter()
133+
.map(|v| self.bucket_n(Self::hash_str(v.unwrap()))),
134+
),
135+
DataType::Binary => arrow_array::Int32Array::from_iter(
136+
input
137+
.as_any()
138+
.downcast_ref::<arrow_array::BinaryArray>()
139+
.unwrap()
140+
.iter()
141+
.map(|v| self.bucket_n(Self::hash_bytes(v.unwrap()))),
142+
),
143+
DataType::LargeBinary => arrow_array::Int32Array::from_iter(
144+
input
145+
.as_any()
146+
.downcast_ref::<arrow_array::LargeBinaryArray>()
147+
.unwrap()
148+
.iter()
149+
.map(|v| self.bucket_n(Self::hash_bytes(v.unwrap()))),
150+
),
151+
DataType::FixedSizeBinary(_) => arrow_array::Int32Array::from_iter(
152+
input
153+
.as_any()
154+
.downcast_ref::<arrow_array::FixedSizeBinaryArray>()
155+
.unwrap()
156+
.iter()
157+
.map(|v| self.bucket_n(Self::hash_bytes(v.unwrap()))),
158+
),
159+
_ => unreachable!("Unsupported data type: {:?}", input.data_type()),
160+
};
161+
Ok(Arc::new(res))
162+
}
163+
}
164+
165+
#[cfg(test)]
166+
mod test {
167+
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
168+
169+
use super::Bucket;
170+
#[test]
171+
fn test_hash() {
172+
// test int
173+
assert_eq!(Bucket::hash_int(34), 2017239379);
174+
// test long
175+
assert_eq!(Bucket::hash_long(34), 2017239379);
176+
// test decimal
177+
assert_eq!(Bucket::hash_decimal(1420), -500754589);
178+
// test date
179+
let date = NaiveDate::from_ymd_opt(2017, 11, 16).unwrap();
180+
assert_eq!(
181+
Bucket::hash_date(
182+
date.signed_duration_since(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap())
183+
.num_days() as i32
184+
),
185+
-653330422
186+
);
187+
// test time
188+
let time = NaiveTime::from_hms_opt(22, 31, 8).unwrap();
189+
assert_eq!(
190+
Bucket::hash_time(
191+
time.signed_duration_since(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
192+
.num_microseconds()
193+
.unwrap()
194+
),
195+
-662762989
196+
);
197+
// test timestamp
198+
let timestamp =
199+
NaiveDateTime::parse_from_str("2017-11-16 22:31:08", "%Y-%m-%d %H:%M:%S").unwrap();
200+
assert_eq!(
201+
Bucket::hash_timestamp(
202+
timestamp
203+
.signed_duration_since(
204+
NaiveDateTime::parse_from_str("1970-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
205+
.unwrap()
206+
)
207+
.num_microseconds()
208+
.unwrap()
209+
),
210+
-2047944441
211+
);
212+
// test timestamp with tz
213+
let timestamp = DateTime::parse_from_rfc3339("2017-11-16T14:31:08-08:00").unwrap();
214+
assert_eq!(
215+
Bucket::hash_timestamp(
216+
timestamp
217+
.signed_duration_since(
218+
DateTime::parse_from_rfc3339("1970-01-01T00:00:00-00:00").unwrap()
219+
)
220+
.num_microseconds()
221+
.unwrap()
222+
),
223+
-2047944441
224+
);
225+
// test str
226+
assert_eq!(Bucket::hash_str("iceberg"), 1210000089);
227+
// test uuid
228+
assert_eq!(
229+
Bucket::hash_bytes(
230+
[
231+
0xF7, 0x9C, 0x3E, 0x09, 0x67, 0x7C, 0x4B, 0xBD, 0xA4, 0x79, 0x3F, 0x34, 0x9C,
232+
0xB7, 0x85, 0xE7
233+
]
234+
.as_ref()
235+
),
236+
1488055340
237+
);
238+
// test fixed and binary
239+
assert_eq!(
240+
Bucket::hash_bytes([0x00, 0x01, 0x02, 0x03].as_ref()),
241+
-188683207
242+
);
243+
}
244+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use crate::Result;
19+
use arrow_array::ArrayRef;
20+
21+
use super::TransformFunction;
22+
23+
/// Return identity array.
24+
pub struct Identity {}
25+
26+
impl TransformFunction for Identity {
27+
fn transform(&self, input: ArrayRef) -> Result<ArrayRef> {
28+
Ok(input)
29+
}
30+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! Transform function used to compute partition values.
19+
use crate::{spec::Transform, Result};
20+
use arrow_array::ArrayRef;
21+
22+
mod bucket;
23+
mod identity;
24+
mod temporal;
25+
mod truncate;
26+
mod void;
27+
28+
/// TransformFunction is a trait that defines the interface for all transform functions.
29+
pub trait TransformFunction: Send {
30+
/// transform will take an input array and transform it into a new array.
31+
/// The implementation of this function will need to check and downcast the input to specific
32+
/// type.
33+
fn transform(&self, input: ArrayRef) -> Result<ArrayRef>;
34+
}
35+
36+
/// BoxedTransformFunction is a boxed trait object of TransformFunction.
37+
pub type BoxedTransformFunction = Box<dyn TransformFunction>;
38+
39+
/// create_transform_function creates a boxed trait object of TransformFunction from a Transform.
40+
pub fn create_transform_function(transform: &Transform) -> Result<BoxedTransformFunction> {
41+
match transform {
42+
Transform::Identity => Ok(Box::new(identity::Identity {})),
43+
Transform::Void => Ok(Box::new(void::Void {})),
44+
Transform::Year => Ok(Box::new(temporal::Year {})),
45+
Transform::Month => Ok(Box::new(temporal::Month {})),
46+
Transform::Day => Ok(Box::new(temporal::Day {})),
47+
Transform::Hour => Ok(Box::new(temporal::Hour {})),
48+
Transform::Bucket(mod_n) => Ok(Box::new(bucket::Bucket::new(*mod_n))),
49+
Transform::Truncate(width) => Ok(Box::new(truncate::Truncate::new(*width))),
50+
Transform::Unknown => Err(crate::error::Error::new(
51+
crate::ErrorKind::FeatureUnsupported,
52+
"Transform Unknown is not implemented",
53+
)),
54+
}
55+
}

0 commit comments

Comments
 (0)