blob: f893f68903f4acafa6a8c86d0f8dfe38465171a2 [file]
// 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.
use std::cmp::Ordering;
use arrow_array::*;
use arrow_schema::{DataType, TimeUnit};
use crate::types;
use crate::values::Value;
use crate::varint;
#[derive(Debug, Clone)]
pub struct ColumnStats {
pub column_index: usize,
pub null_count: usize,
pub min: Option<Value>,
pub max: Option<Value>,
}
pub fn supports_stats(dt: &DataType) -> bool {
matches!(
dt,
DataType::Boolean
| DataType::Int8
| DataType::Int16
| DataType::Int32
| DataType::Int64
| DataType::Float32
| DataType::Float64
| DataType::Date32
| DataType::Time32(_)
| DataType::Decimal128(0..=18, _)
| DataType::Timestamp(_, _)
| DataType::Utf8
) || matches!(dt, DataType::Struct(fields) if types::is_timestamp_nanos_struct(fields))
}
pub fn compare_values(a: &Value, b: &Value) -> Option<Ordering> {
match (a, b) {
(Value::Boolean(x), Value::Boolean(y)) => Some(x.cmp(y)),
(Value::TinyInt(x), Value::TinyInt(y)) => Some(x.cmp(y)),
(Value::SmallInt(x), Value::SmallInt(y)) => Some(x.cmp(y)),
(Value::Integer(x), Value::Integer(y)) => Some(x.cmp(y)),
(Value::BigInt(x), Value::BigInt(y)) => Some(x.cmp(y)),
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y),
(Value::Double(x), Value::Double(y)) => x.partial_cmp(y),
(Value::Date(x), Value::Date(y)) => Some(x.cmp(y)),
(Value::Time(x), Value::Time(y)) => Some(x.cmp(y)),
(Value::DecimalCompact(x), Value::DecimalCompact(y)) => Some(x.cmp(y)),
(Value::TimestampMillis(x), Value::TimestampMillis(y)) => Some(x.cmp(y)),
(Value::TimestampMicros(x), Value::TimestampMicros(y)) => Some(x.cmp(y)),
(
Value::TimestampNanos {
millis: m1,
nanos_of_milli: n1,
},
Value::TimestampNanos {
millis: m2,
nanos_of_milli: n2,
},
) => Some(m1.cmp(m2).then(n1.cmp(n2))),
(Value::String(x), Value::String(y)) => Some(x.cmp(y)),
_ => None,
}
}
struct ColTracker {
column_index: usize,
data_type: DataType,
null_count: usize,
min: Option<Value>,
max: Option<Value>,
}
pub struct StatsCollector {
trackers: Vec<ColTracker>,
}
impl StatsCollector {
pub fn new(columns: &[(usize, DataType)]) -> Self {
let trackers = columns
.iter()
.map(|(idx, dt)| ColTracker {
column_index: *idx,
data_type: dt.clone(),
null_count: 0,
min: None,
max: None,
})
.collect();
StatsCollector { trackers }
}
pub fn is_empty(&self) -> bool {
self.trackers.is_empty()
}
pub fn update(&mut self, row: &[Value]) {
for tracker in &mut self.trackers {
let value = &row[tracker.column_index];
if value.is_null() {
tracker.null_count += 1;
continue;
}
if !supports_stats(&tracker.data_type) {
continue;
}
update_min_max(tracker, value.clone());
}
}
pub fn update_batch(&mut self, batch: &RecordBatch) {
for tracker in &mut self.trackers {
let array = batch.column(tracker.column_index);
tracker.null_count += array.null_count();
if !supports_stats(&tracker.data_type) {
continue;
}
for row in 0..array.len() {
if array.is_null(row) {
continue;
}
if let Some(val) = extract_value_for_stats(array.as_ref(), row, &tracker.data_type)
{
update_min_max(tracker, val);
}
}
}
}
pub fn finish(&mut self) -> Vec<ColumnStats> {
let stats = self
.trackers
.iter()
.map(|t| ColumnStats {
column_index: t.column_index,
null_count: t.null_count,
min: t.min.clone(),
max: t.max.clone(),
})
.collect();
self.reset();
stats
}
pub fn reset(&mut self) {
for tracker in &mut self.trackers {
tracker.null_count = 0;
tracker.min = None;
tracker.max = None;
}
}
}
fn update_min_max(tracker: &mut ColTracker, value: Value) {
let update_min = match &tracker.min {
None => true,
Some(cur) => compare_values(&value, cur) == Some(Ordering::Less),
};
if update_min {
tracker.min = Some(value.clone());
}
let update_max = match &tracker.max {
None => true,
Some(cur) => compare_values(&value, cur) == Some(Ordering::Greater),
};
if update_max {
tracker.max = Some(value);
}
}
fn extract_value_for_stats(array: &dyn Array, row: usize, dt: &DataType) -> Option<Value> {
match dt {
DataType::Boolean => {
let a = array.as_any().downcast_ref::<BooleanArray>()?;
Some(Value::Boolean(a.value(row)))
}
DataType::Int8 => {
let a = array.as_any().downcast_ref::<Int8Array>()?;
Some(Value::TinyInt(a.value(row)))
}
DataType::Int16 => {
let a = array.as_any().downcast_ref::<Int16Array>()?;
Some(Value::SmallInt(a.value(row)))
}
DataType::Int32 => {
let a = array.as_any().downcast_ref::<Int32Array>()?;
Some(Value::Integer(a.value(row)))
}
DataType::Int64 => {
let a = array.as_any().downcast_ref::<Int64Array>()?;
Some(Value::BigInt(a.value(row)))
}
DataType::Float32 => {
let a = array.as_any().downcast_ref::<Float32Array>()?;
Some(Value::Float(a.value(row)))
}
DataType::Float64 => {
let a = array.as_any().downcast_ref::<Float64Array>()?;
Some(Value::Double(a.value(row)))
}
DataType::Date32 => {
let a = array.as_any().downcast_ref::<Date32Array>()?;
Some(Value::Date(a.value(row)))
}
DataType::Time32(_) => {
let a = array.as_any().downcast_ref::<Time32MillisecondArray>()?;
Some(Value::Time(a.value(row)))
}
DataType::Utf8 => {
let a = array.as_any().downcast_ref::<StringArray>()?;
Some(Value::String(a.value(row).as_bytes().to_vec()))
}
DataType::Decimal128(p, _) => {
let a = array.as_any().downcast_ref::<Decimal128Array>()?;
let val = a.value(row);
if *p <= 18 {
Some(Value::DecimalCompact(val as i64))
} else {
None
}
}
DataType::Timestamp(TimeUnit::Millisecond, _) => {
let a = array.as_any().downcast_ref::<TimestampMillisecondArray>()?;
Some(Value::TimestampMillis(a.value(row)))
}
DataType::Timestamp(TimeUnit::Microsecond, _) => {
let a = array.as_any().downcast_ref::<TimestampMicrosecondArray>()?;
Some(Value::TimestampMicros(a.value(row)))
}
DataType::Struct(fields) if types::is_timestamp_nanos_struct(fields) => {
let s = array.as_any().downcast_ref::<StructArray>()?;
let millis = s
.column(0)
.as_any()
.downcast_ref::<Int64Array>()?
.value(row);
let nanos = s
.column(1)
.as_any()
.downcast_ref::<Int32Array>()?
.value(row);
Some(Value::TimestampNanos {
millis,
nanos_of_milli: nanos,
})
}
_ => None,
}
}
pub fn serialize_stats(
stats: &[ColumnStats],
schema_columns: &[crate::schema::ColumnMeta],
) -> Vec<u8> {
let mut buf = Vec::new();
varint::encode(&mut buf, stats.len() as u32);
for stat in stats {
varint::encode(&mut buf, stat.column_index as u32);
varint::encode(&mut buf, stat.null_count as u32);
if let (Some(min_val), Some(max_val)) = (&stat.min, &stat.max) {
let dt = &schema_columns[stat.column_index].data_type;
serialize_value(&mut buf, min_val, dt);
serialize_value(&mut buf, max_val, dt);
}
}
buf
}
pub fn deserialize_stats(
data: &[u8],
pos: &mut usize,
schema_columns: &[crate::schema::ColumnMeta],
num_rows: usize,
) -> std::io::Result<Vec<ColumnStats>> {
let num_stats = varint::decode(data, pos)? as usize;
let mut stats = Vec::with_capacity(num_stats);
for _ in 0..num_stats {
let column_index = varint::decode(data, pos)? as usize;
if column_index >= schema_columns.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"stats: column_index out of range",
));
}
let null_count = varint::decode(data, pos)? as usize;
if null_count > num_rows {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"stats: null_count exceeds num_rows",
));
}
let (min, max) = if null_count < num_rows {
let dt = &schema_columns[column_index].data_type;
let min_val = deserialize_value(data, pos, dt)?;
let max_val = deserialize_value(data, pos, dt)?;
(Some(min_val), Some(max_val))
} else {
(None, None)
};
stats.push(ColumnStats {
column_index,
null_count,
min,
max,
});
}
Ok(stats)
}
fn serialize_value(buf: &mut Vec<u8>, value: &Value, dt: &DataType) {
let w = types::fixed_width(dt);
if w > 0 {
crate::values::write_fixed(buf, value, w).expect("stats value type must match column type");
} else {
crate::values::write_variable(buf, value).expect("stats value type must match column type");
}
}
fn deserialize_value(data: &[u8], pos: &mut usize, dt: &DataType) -> std::io::Result<Value> {
let w = types::fixed_width(dt);
if w > 0 {
let end = *pos + w as usize;
if end > data.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"stats: not enough data for fixed value",
));
}
let value = read_fixed_value(data, *pos, dt, w);
*pos += w as usize;
Ok(value)
} else {
let (value, size) = read_variable_value(data, *pos, dt)?;
*pos += size;
Ok(value)
}
}
fn read_fixed_value(buf: &[u8], pos: usize, dt: &DataType, width: i32) -> Value {
match dt {
DataType::Boolean => Value::Boolean(buf[pos] != 0),
DataType::Int8 => Value::TinyInt(buf[pos] as i8),
DataType::Int16 => Value::SmallInt(i16::from_be_bytes([buf[pos], buf[pos + 1]])),
DataType::Int32 => Value::Integer(i32::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
])),
DataType::Date32 => Value::Date(i32::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
])),
DataType::Time32(_) => Value::Time(i32::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
])),
DataType::Float32 => {
let bits = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
Value::Float(f32::from_bits(bits))
}
DataType::Int64 => Value::BigInt(i64::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
buf[pos + 4],
buf[pos + 5],
buf[pos + 6],
buf[pos + 7],
])),
DataType::Float64 => {
let bits = u64::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
buf[pos + 4],
buf[pos + 5],
buf[pos + 6],
buf[pos + 7],
]);
Value::Double(f64::from_bits(bits))
}
DataType::Decimal128(_, _) => Value::DecimalCompact(i64::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
buf[pos + 4],
buf[pos + 5],
buf[pos + 6],
buf[pos + 7],
])),
DataType::Timestamp(TimeUnit::Millisecond, _) => {
Value::TimestampMillis(i64::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
buf[pos + 4],
buf[pos + 5],
buf[pos + 6],
buf[pos + 7],
]))
}
DataType::Timestamp(TimeUnit::Microsecond, _) => {
Value::TimestampMicros(i64::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
buf[pos + 4],
buf[pos + 5],
buf[pos + 6],
buf[pos + 7],
]))
}
DataType::Struct(fields) if types::is_timestamp_nanos_struct(fields) => {
debug_assert_eq!(width, 12);
let millis = i64::from_be_bytes([
buf[pos],
buf[pos + 1],
buf[pos + 2],
buf[pos + 3],
buf[pos + 4],
buf[pos + 5],
buf[pos + 6],
buf[pos + 7],
]);
let nanos =
i32::from_be_bytes([buf[pos + 8], buf[pos + 9], buf[pos + 10], buf[pos + 11]]);
Value::TimestampNanos {
millis,
nanos_of_milli: nanos,
}
}
_ => Value::Null,
}
}
fn read_variable_value(buf: &[u8], pos: usize, dt: &DataType) -> std::io::Result<(Value, usize)> {
let mut p = pos;
let len = varint::decode(buf, &mut p)? as usize;
let header_size = p - pos;
if p + len > buf.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"stats: not enough data for variable value",
));
}
let bytes = buf[p..p + len].to_vec();
let total_size = header_size + len;
let value = match dt {
DataType::Utf8 => Value::String(bytes),
_ => Value::Null,
};
Ok((value, total_size))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::ColumnMeta;
use arrow_schema::DataType;
#[test]
fn test_compare_values() {
assert_eq!(
compare_values(&Value::Integer(1), &Value::Integer(2)),
Some(Ordering::Less)
);
assert_eq!(
compare_values(&Value::Integer(2), &Value::Integer(2)),
Some(Ordering::Equal)
);
assert_eq!(
compare_values(&Value::Double(1.5), &Value::Double(2.5)),
Some(Ordering::Less)
);
assert_eq!(
compare_values(
&Value::String(b"abc".to_vec()),
&Value::String(b"abd".to_vec())
),
Some(Ordering::Less)
);
}
#[test]
fn test_stats_collector() {
let columns = vec![(0usize, DataType::Int32), (1usize, DataType::Float64)];
let mut collector = StatsCollector::new(&columns);
collector.update(&[Value::Integer(10), Value::Double(1.5)]);
collector.update(&[Value::Integer(5), Value::Double(3.0)]);
collector.update(&[Value::Integer(20), Value::Null]);
collector.update(&[Value::Null, Value::Double(0.5)]);
let stats = collector.finish();
assert_eq!(stats.len(), 2);
assert_eq!(stats[0].column_index, 0);
assert_eq!(stats[0].null_count, 1);
assert!(matches!(&stats[0].min, Some(Value::Integer(5))));
assert!(matches!(&stats[0].max, Some(Value::Integer(20))));
assert_eq!(stats[1].column_index, 1);
assert_eq!(stats[1].null_count, 1);
match &stats[1].min {
Some(Value::Double(v)) => assert!((*v - 0.5).abs() < 1e-10),
other => panic!("expected Double(0.5), got {:?}", other),
}
match &stats[1].max {
Some(Value::Double(v)) => assert!((*v - 3.0).abs() < 1e-10),
other => panic!("expected Double(3.0), got {:?}", other),
}
}
#[test]
fn test_serialize_deserialize_stats() {
let schema_columns = vec![
ColumnMeta {
name: "a".to_string(),
data_type: DataType::Int32,
nullable: true,
bucket_id: 0,
},
ColumnMeta {
name: "b".to_string(),
data_type: DataType::Utf8,
nullable: true,
bucket_id: 0,
},
];
let stats = vec![
ColumnStats {
column_index: 0,
null_count: 3,
min: Some(Value::Integer(5)),
max: Some(Value::Integer(100)),
},
ColumnStats {
column_index: 1,
null_count: 0,
min: Some(Value::String(b"abc".to_vec())),
max: Some(Value::String(b"xyz".to_vec())),
},
];
let buf = serialize_stats(&stats, &schema_columns);
let mut pos = 0;
let num_rows = 10;
let result = deserialize_stats(&buf, &mut pos, &schema_columns, num_rows).unwrap();
assert_eq!(pos, buf.len());
assert_eq!(result.len(), 2);
assert_eq!(result[0].column_index, 0);
assert_eq!(result[0].null_count, 3);
assert!(matches!(&result[0].min, Some(Value::Integer(5))));
assert!(matches!(&result[0].max, Some(Value::Integer(100))));
assert_eq!(result[1].column_index, 1);
assert_eq!(result[1].null_count, 0);
assert!(matches!(&result[1].min, Some(Value::String(b)) if b == b"abc"));
assert!(matches!(&result[1].max, Some(Value::String(b)) if b == b"xyz"));
}
#[test]
fn test_serialize_empty_stats() {
let schema_columns = vec![];
let stats: Vec<ColumnStats> = vec![];
let buf = serialize_stats(&stats, &schema_columns);
assert_eq!(buf, vec![0]);
let mut pos = 0;
let result = deserialize_stats(&buf, &mut pos, &schema_columns, 0).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_serialize_all_null_stats() {
let schema_columns = vec![ColumnMeta {
name: "x".to_string(),
data_type: DataType::Int32,
nullable: true,
bucket_id: 0,
}];
let stats = vec![ColumnStats {
column_index: 0,
null_count: 100,
min: None,
max: None,
}];
let buf = serialize_stats(&stats, &schema_columns);
let mut pos = 0;
let result = deserialize_stats(&buf, &mut pos, &schema_columns, 100).unwrap();
assert_eq!(result[0].null_count, 100);
assert!(result[0].min.is_none());
assert!(result[0].max.is_none());
}
}