fix(promql): restore exact filtering for nullable label matchers (#9418)

* fix(promql): restore exact filtering for nullable label matchers

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* test: verify compound filter semantics and last-row selection

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* test: consolidate label filter regression coverage

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* refactor: separate filter evaluation and timestamp adaptation

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* refactor: inline predicate and timestamp tests

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* refactor: keep direct IN predicates outside simple filters

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* fix: coerce partition bounds before simple filtering

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* test: cover partition bound overflow after timestamp widening

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
This commit is contained in:
dennis zhuang
2026-10-08 07:43:25 +00:00
committed by GitHub
parent 90f117b957
commit 526dd48bda
15 changed files with 2064 additions and 1130 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,944 @@
// Copyright 2023 Greptime Team
//
// Licensed 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.
//! Predicate recognition, execution, and precompiled regular expressions.
use datafusion::logical_expr::{Expr, Operator};
use datafusion_common::ScalarValue;
use datafusion_common::arrow::array::{ArrayRef, Datum, Scalar};
use datafusion_common::arrow::buffer::BooleanBuffer;
use datafusion_common::arrow::compute::kernels::cmp;
use datafusion_common::cast::as_string_array;
use datatypes::arrow::array::{
Array, ArrayAccessor, ArrayData, BooleanArray, BooleanBufferBuilder, DictionaryArray,
StringArrayType,
};
use datatypes::arrow::datatypes::{DataType, UInt32Type};
use datatypes::arrow::error::ArrowError;
use datatypes::compute::or_kleene;
use regex::Regex;
use snafu::ResultExt;
use crate::error::{ArrowComputeSnafu, Result, UnsupportedOperationSnafu};
#[derive(Debug, Clone)]
pub(crate) enum SimplePredicate {
Comparison {
literal: Scalar<ArrayRef>,
op: Operator,
regex: Option<Regex>,
regex_negative: bool,
},
OrEqChain {
literals: Vec<Scalar<ArrayRef>>,
},
IsNull {
negated: bool,
},
Constant(Option<bool>),
And(Box<Self>, Box<Self>),
Or(Box<Self>, Box<Self>),
}
impl SimplePredicate {
pub(crate) fn try_new(expr: &Expr, column_type: Option<&DataType>) -> Option<Self> {
match expr {
Expr::BinaryExpr(binary) if matches!(binary.op, Operator::And | Operator::Or) => {
let left = Self::try_new(&binary.left, column_type)?;
let right = Self::try_new(&binary.right, column_type)?;
if binary.op == Operator::And {
return Some(Self::And(Box::new(left), Box::new(right)));
}
// Preserve the encoded-key IN-list fast path for pure equality disjunctions.
if let (Some(mut lhs), Some(rhs)) =
(left.equality_literals(), right.equality_literals())
{
lhs.extend(rhs);
return Some(Self::OrEqChain { literals: lhs });
}
Some(Self::Or(Box::new(left), Box::new(right)))
}
Expr::BinaryExpr(binary) => {
let mut op = binary.op;
if !matches!(
op,
Operator::Eq
| Operator::NotEq
| Operator::Lt
| Operator::LtEq
| Operator::Gt
| Operator::GtEq
| Operator::RegexMatch
| Operator::RegexIMatch
| Operator::RegexNotMatch
| Operator::RegexNotIMatch
) {
return None;
}
let literal = if let Expr::Literal(literal, _) = &*binary.right {
Self::check_column(&binary.left, column_type)?;
literal
} else if let Expr::Literal(literal, _) = &*binary.left {
Self::check_column(&binary.right, column_type)?;
op = op.swap()?;
literal
} else {
return None;
};
let (regex, regex_negative) = Self::maybe_build_regex(op, literal).ok()?;
Some(Self::Comparison {
literal: literal.to_scalar().ok()?,
op,
regex,
regex_negative,
})
}
Expr::IsNull(column) | Expr::IsNotNull(column) => {
Self::check_column(column, column_type)?;
Some(Self::IsNull {
negated: matches!(expr, Expr::IsNotNull(_)),
})
}
Expr::Literal(ScalarValue::Boolean(value), _) => Some(Self::Constant(*value)),
_ => None,
}
}
fn check_column(expr: &Expr, column_type: Option<&DataType>) -> Option<()> {
match expr {
Expr::Column(_) => Some(()),
Expr::Cast(cast) if cast.field.data_type() == &DataType::Utf8 => {
let Expr::Column(_) = &*cast.expr else {
return None;
};
match column_type? {
DataType::Utf8 => Some(()),
DataType::Dictionary(key, value)
if **key == DataType::UInt32 && **value == DataType::Utf8 =>
{
Some(())
}
_ => None,
}
}
_ => None,
}
}
fn equality_literals(&self) -> Option<Vec<Scalar<ArrayRef>>> {
match self {
Self::Comparison {
literal,
op: Operator::Eq,
..
} => Some(vec![literal.clone()]),
Self::OrEqChain { literals } => Some(literals.clone()),
_ => None,
}
}
pub(crate) fn evaluate(&self, input: &impl Datum, len: usize) -> Result<BooleanArray> {
match self {
Self::Comparison {
literal,
op,
regex,
regex_negative,
} => match op {
Operator::Eq => cmp::eq(input, literal),
Operator::NotEq => cmp::neq(input, literal),
Operator::Lt => cmp::lt(input, literal),
Operator::LtEq => cmp::lt_eq(input, literal),
Operator::Gt => cmp::gt(input, literal),
Operator::GtEq => cmp::gt_eq(input, literal),
Operator::RegexMatch
| Operator::RegexIMatch
| Operator::RegexNotMatch
| Operator::RegexNotIMatch => {
Self::regex_match(input, regex.as_ref(), *regex_negative)
}
_ => {
return UnsupportedOperationSnafu {
reason: format!("{op:?}"),
}
.fail();
}
}
.context(ArrowComputeSnafu),
Self::OrEqChain { literals } => {
let mut result = BooleanArray::from(vec![false; len]);
for literal in literals {
let rhs = cmp::eq(input, literal).context(ArrowComputeSnafu)?;
result = or_kleene(&result, &rhs).context(ArrowComputeSnafu)?;
}
Ok(result)
}
Self::IsNull { negated } => {
let values = match input.get().0.logical_nulls() {
Some(nulls) if *negated => nulls.inner().clone(),
Some(nulls) => !nulls.inner(),
None if *negated => BooleanBuffer::new_set(len),
None => BooleanBuffer::new_unset(len),
};
Ok(BooleanArray::new(values, None))
}
Self::Constant(value) => Ok(BooleanArray::from(vec![*value; len])),
Self::And(left, right) => datatypes::compute::and_kleene(
&left.evaluate(input, len)?,
&right.evaluate(input, len)?,
)
.context(ArrowComputeSnafu),
Self::Or(left, right) => {
or_kleene(&left.evaluate(input, len)?, &right.evaluate(input, len)?)
.context(ArrowComputeSnafu)
}
}
}
/// Builds a regex pattern from a scalar value and operator.
/// Returns the `(regex, negative)` and if successful.
///
/// Returns `Err` if
/// - the value is not a string
/// - the regex pattern is invalid
///
/// The regex is `None` if
/// - the operator is not a regex operator
/// - the pattern is empty
fn maybe_build_regex(
operator: Operator,
value: &ScalarValue,
) -> Result<(Option<Regex>, bool), ArrowError> {
let (ignore_case, negative) = match operator {
Operator::RegexMatch => (false, false),
Operator::RegexIMatch => (true, false),
Operator::RegexNotMatch => (false, true),
Operator::RegexNotIMatch => (true, true),
_ => return Ok((None, false)),
};
let flag = if ignore_case { Some("i") } else { None };
let regex = value
.try_as_str()
.ok_or_else(|| ArrowError::CastError(format!("Cannot cast {:?} to str", value)))?
.ok_or_else(|| ArrowError::CastError("Regex should not be null".to_string()))?;
let pattern = match flag {
Some(flag) => format!("(?{flag}){regex}"),
None => regex.to_string(),
};
if pattern.is_empty() {
Ok((None, negative))
} else {
Regex::new(pattern.as_str())
.map_err(|e| {
ArrowError::ComputeError(format!("Regular expression did not compile: {e:?}"))
})
.map(|regex| (Some(regex), negative))
}
}
fn regex_match(
input: &impl Datum,
regex: Option<&Regex>,
negative: bool,
) -> std::result::Result<BooleanArray, ArrowError> {
let array = input.get().0;
// Try to cast to StringArray first
if let Ok(string_array) = as_string_array(array) {
let mut result = regexp_is_match_scalar(string_array, regex)?;
if negative {
result = datatypes::compute::not(&result)?;
}
return Ok(result);
}
// Try to cast to StringDictionaryArray
if let Some(dict_array) = array.as_any().downcast_ref::<DictionaryArray<UInt32Type>>() {
let mut result = regexp_is_match_dictionary(dict_array, regex)?;
if negative {
result = datatypes::compute::not(&result)?;
}
return Ok(result);
}
Err(ArrowError::CastError(format!(
"Cannot cast {:?} to StringArray or StringDictionaryArray",
array.data_type()
)))
}
}
/// The same as arrow [regexp_is_match_scalar()](datatypes::compute::kernels::regexp::regexp_is_match_scalar())
/// with pre-compiled regex.
/// See <https://github.com/apache/arrow-rs/blob/54.2.0/arrow-string/src/regexp.rs#L204-L246> for the implementation details.
pub fn regexp_is_match_scalar<'a, S>(
array: &'a S,
regex: Option<&Regex>,
) -> Result<BooleanArray, ArrowError>
where
&'a S: StringArrayType<'a>,
{
let null_bit_buffer = array.nulls().map(|x| x.inner().sliced());
let mut result = BooleanBufferBuilder::new(array.len());
if let Some(re) = regex {
for i in 0..array.len() {
let value = array.value(i);
result.append(re.is_match(value));
}
} else {
result.append_n(array.len(), true);
}
let buffer = result.into();
let data = unsafe {
ArrayData::new_unchecked(
DataType::Boolean,
array.len(),
None,
null_bit_buffer,
0,
vec![buffer],
vec![],
)
};
Ok(BooleanArray::from(data))
}
/// Similar to [regexp_is_match_scalar] but for StringDictionaryArray.
/// Iterates through dictionary keys to get string values and applies regex matching.
pub fn regexp_is_match_dictionary(
dict_array: &DictionaryArray<UInt32Type>,
regex: Option<&Regex>,
) -> Result<BooleanArray, ArrowError> {
// Get the string values from the dictionary
let string_values = dict_array
.values()
.as_any()
.downcast_ref::<datatypes::arrow::array::StringArray>()
.ok_or_else(|| {
ArrowError::CastError("Dictionary values must be StringArray".to_string())
})?;
// Dictionary logical nulls include both null keys and keys whose dictionary value is null.
let logical_nulls = dict_array.logical_nulls();
let null_bit_buffer = logical_nulls.as_ref().map(|x| x.inner().sliced());
let mut result = BooleanBufferBuilder::new(dict_array.len());
if let Some(re) = regex {
let keys = dict_array.keys().values();
for i in 0..dict_array.len() {
if logical_nulls.as_ref().is_some_and(|nulls| nulls.is_null(i)) {
result.append(false);
} else {
let key = keys[i] as usize;
let string_value = string_values.value(key);
result.append(re.is_match(string_value));
}
}
} else {
result.append_n(dict_array.len(), true);
}
let buffer = result.into();
let data = unsafe {
ArrayData::new_unchecked(
DataType::Boolean,
dict_array.len(),
None,
null_bit_buffer,
0,
vec![buffer],
vec![],
)
};
Ok(BooleanArray::from(data))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use datafusion::execution::context::ExecutionProps;
use datafusion::logical_expr::physical_planning_context::PhysicalPlanningContext;
use datafusion::logical_expr::{BinaryExpr, Expr, Literal, Operator, col, lit};
use datafusion::physical_expr::create_physical_expr;
use datafusion_common::arrow::array::ArrayRef;
use datafusion_common::arrow::buffer::BooleanBuffer;
use datafusion_common::{Column, DFSchema, ScalarValue};
use datatypes::arrow::array::{Array, DictionaryArray, RecordBatch};
use datatypes::arrow::datatypes::{DataType, Field, Schema, TimeUnit, UInt32Type};
use datatypes::value::Value;
use regex::Regex;
use crate::filter::predicate::{SimplePredicate, regexp_is_match_dictionary};
use crate::filter::{SimpleFilterEvaluator, batch_filter};
#[test]
fn unsupported_filter_op() {
// `+` is not supported
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Column(Column::from_name("foo"))),
op: Operator::Plus,
right: Box::new(1.lit()),
});
assert!(SimpleFilterEvaluator::try_new(&expr).is_none());
// two literal is not supported
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(1.lit()),
op: Operator::Eq,
right: Box::new(1.lit()),
});
assert!(SimpleFilterEvaluator::try_new(&expr).is_none());
// two column is not supported
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Column(Column::from_name("foo"))),
op: Operator::Eq,
right: Box::new(Expr::Column(Column::from_name("bar"))),
});
assert!(SimpleFilterEvaluator::try_new(&expr).is_none());
// compound expr is not supported
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Column(Column::from_name("foo"))),
op: Operator::Eq,
right: Box::new(1.lit()),
})),
op: Operator::Eq,
right: Box::new(1.lit()),
});
assert!(SimpleFilterEvaluator::try_new(&expr).is_none());
for negated in [false, true] {
let membership = col("label").in_list(vec![lit(""), lit("lo")], negated);
assert!(SimpleFilterEvaluator::try_new(&membership).is_none());
assert!(
SimpleFilterEvaluator::try_new(&col("label").is_null().or(membership)).is_none()
);
}
for expr in [
col("other").is_null().or(col("label").not_eq(lit("lo"))),
col("a.label")
.is_null()
.or(col("b.label").not_eq(lit("lo"))),
col("label").is_null().or(col("label").eq(col("other"))),
col("label")
.is_null()
.or(Expr::Cast(datafusion::logical_expr::Cast::new(
Box::new(col("label")),
DataType::Int64,
))
.eq(lit("lo"))),
] {
assert!(SimpleFilterEvaluator::try_new(&expr).is_none(), "{expr}");
}
}
#[test]
fn string_cast_requires_matching_column_type() {
let cast = Expr::Cast(datafusion::logical_expr::Cast::new(
Box::new(col("label")),
DataType::Utf8,
));
let expr = col("label").is_null().or(cast.not_eq(lit("lo")));
for data_type in [
DataType::Int64,
DataType::Timestamp(TimeUnit::Millisecond, None),
DataType::Binary,
] {
assert!(
SimpleFilterEvaluator::try_new_with_column_type(&expr, &|_| Some(
data_type.clone()
))
.is_none()
);
}
assert!(SimpleFilterEvaluator::try_new(&expr).is_none());
}
#[tokio::test]
async fn single_column_predicates_match_datafusion() {
use datafusion::prelude::SessionContext;
use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
use datatypes::arrow::array::{Int32Array, StringArray, UInt32Array};
let values = vec![
None,
Some(""),
Some("lo"),
Some("eth0"),
Some("a\nb"),
Some("服务"),
None,
];
let strings: ArrayRef = Arc::new(StringArray::from(values));
let dictionary: ArrayRef = Arc::new(DictionaryArray::<UInt32Type>::new(
UInt32Array::from(vec![
None,
Some(0),
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
]),
Arc::new(StringArray::from(vec![
Some(""),
Some("lo"),
Some("eth0"),
Some("a\nb"),
Some("服务"),
None,
])),
));
let null = || lit(ScalarValue::Utf8(None));
let mut predicates = vec![
col("label").is_null(),
col("label").is_not_null(),
col("label")
.is_null()
.or(col("label").eq(lit("")))
.and(col("label").is_not_null()),
col("label")
.not_eq(lit("lo"))
.and(col("label").not_eq(lit("eth0"))),
col("label")
.eq(lit("lo"))
.or(col("label").eq(lit("eth0")))
.or(col("label").eq(lit(""))),
col("label").eq(null()).or(col("label").eq(lit("eth0"))),
col("label").gt(lit("a")).and(col("label").lt(lit("z"))),
lit("lo").not_eq(col("label")),
];
for (op, literal) in [
(Operator::Eq, ""),
(Operator::NotEq, "lo"),
(Operator::RegexNotMatch, "^(?:lo|vfat)$"),
(Operator::RegexMatch, "^(?:|eth0)$"),
] {
let comparison = Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("label")),
op,
Box::new(lit(literal)),
));
predicates.extend([
col("label").is_null().or(comparison.clone()),
comparison.or(col("label").is_null()),
]);
}
for input in [strings, dictionary] {
let ctx = SessionContext::new();
let schema = Arc::new(Schema::new(vec![
Field::new("label", input.data_type().clone(), true),
Field::new("id", DataType::Int32, false),
]));
ctx.register_batch(
"labels",
RecordBatch::try_new(
schema,
vec![
input.clone(),
Arc::new(Int32Array::from_iter_values(0..input.len() as i32)),
],
)
.unwrap(),
)
.unwrap();
for expr in &predicates {
let df = ctx
.table("labels")
.await
.unwrap()
.filter(expr.clone())
.unwrap();
let plan = df.clone().into_optimized_plan().unwrap();
let expected = df
.collect()
.await
.unwrap()
.iter()
.flat_map(|batch| {
batch
.column_by_name("id")
.unwrap()
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.values()
.to_vec()
})
.collect::<Vec<_>>();
let evaluate = |expr: &Expr| {
let evaluator =
SimpleFilterEvaluator::try_new_with_column_type(expr, &|name| {
(name == "label").then(|| input.data_type().clone())
})
.unwrap_or_else(|| panic!("unsupported: {expr}"));
let mask = evaluator.evaluate_array(&input).unwrap();
for (i, matched) in mask.iter().enumerate() {
let scalar = ScalarValue::try_from_array(&input, i).unwrap();
assert_eq!(
evaluator.evaluate_scalar(&scalar).unwrap(),
matched,
"{expr}, row={i}"
);
}
mask
};
let selected = |mask: BooleanBuffer| {
mask.iter()
.enumerate()
.filter_map(|(i, matched)| matched.then_some(i as i32))
.collect::<Vec<_>>()
};
assert_eq!(selected(evaluate(expr)), expected, "{expr}");
let mut filters = Vec::new();
plan.apply(|plan| {
if let datafusion::logical_expr::LogicalPlan::Filter(filter) = plan {
filters.push(filter.predicate.clone());
}
Ok(TreeNodeRecursion::Continue)
})
.unwrap();
if !filters.is_empty() {
let mut mask = BooleanBuffer::new_set(input.len());
for filter in filters {
mask = &mask & &evaluate(&filter);
}
assert_eq!(selected(mask), expected, "optimized: {plan}");
}
}
}
}
#[test]
fn supported_filter_op() {
// equal
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Column(Column::from_name("foo"))),
op: Operator::Eq,
right: Box::new(1.lit()),
});
let _ = SimpleFilterEvaluator::try_new(&expr).unwrap();
// swap operands
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(1.lit()),
op: Operator::Lt,
right: Box::new(Expr::Column(Column::from_name("foo"))),
});
let evaluator = SimpleFilterEvaluator::try_new(&expr).unwrap();
assert!(evaluator.is_gt());
assert_eq!(evaluator.column_name, "foo".to_string());
}
#[test]
fn run_on_array() {
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Column(Column::from_name("foo"))),
op: Operator::Eq,
right: Box::new(1i64.lit()),
});
let evaluator = SimpleFilterEvaluator::try_new(&expr).unwrap();
let input_1 = Arc::new(datatypes::arrow::array::Int64Array::from(vec![1, 2, 3])) as _;
let result = evaluator.evaluate_array(&input_1).unwrap();
assert_eq!(result, BooleanBuffer::from(vec![true, false, false]));
let input_2 = Arc::new(datatypes::arrow::array::Int64Array::from(vec![1, 1, 1])) as _;
let result = evaluator.evaluate_array(&input_2).unwrap();
assert_eq!(result, BooleanBuffer::from(vec![true, true, true]));
let input_3 = Arc::new(datatypes::arrow::array::Int64Array::new_null(0)) as _;
let result = evaluator.evaluate_array(&input_3).unwrap();
assert_eq!(result, BooleanBuffer::from(vec![]));
}
#[test]
fn run_on_scalar() {
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Column(Column::from_name("foo"))),
op: Operator::Lt,
right: Box::new(1i64.lit()),
});
let evaluator = SimpleFilterEvaluator::try_new(&expr).unwrap();
let input_1 = ScalarValue::Int64(Some(1));
let result = evaluator.evaluate_scalar(&input_1).unwrap();
assert!(!result);
let input_2 = ScalarValue::Int64(Some(0));
let result = evaluator.evaluate_scalar(&input_2).unwrap();
assert!(result);
let input_3 = ScalarValue::Int64(None);
let result = evaluator.evaluate_scalar(&input_3).unwrap();
assert!(!result);
}
#[test]
fn test_complex_filter_expression() {
// Create an expression tree for: col = 'B' OR col = 'C' OR col = 'D'
let col_eq_b = col("col").eq(lit("B"));
let col_eq_c = col("col").eq(lit("C"));
let col_eq_d = col("col").eq(lit("D"));
// Build the OR chain
let col_or_expr = col_eq_b.or(col_eq_c).or(col_eq_d);
// Check that SimpleFilterEvaluator can handle OR chain
let or_evaluator = SimpleFilterEvaluator::try_new(&col_or_expr).unwrap();
assert_eq!(or_evaluator.column_name, "col");
assert!(or_evaluator.is_or_eq_chain());
assert_eq!(
or_evaluator.literal_list_values().unwrap(),
vec![Value::from("B"), Value::from("C"), Value::from("D")]
);
// Create a schema and batch for testing
let schema = Schema::new(vec![Field::new("col", DataType::Utf8, false)]);
let df_schema = DFSchema::try_from(schema.clone()).unwrap();
let props = ExecutionProps::new();
let physical_expr = create_physical_expr(
&col_or_expr,
&df_schema,
&props,
&PhysicalPlanningContext::default(),
)
.unwrap();
// Create test data
let col_data = Arc::new(datatypes::arrow::array::StringArray::from(vec![
"B", "C", "E", "B", "C", "D", "F",
]));
let batch = RecordBatch::try_new(Arc::new(schema), vec![col_data]).unwrap();
let expected = datatypes::arrow::array::StringArray::from(vec!["B", "C", "B", "C", "D"]);
// Filter the batch
let filtered_batch = batch_filter(&batch, &physical_expr).unwrap();
// Expected: rows with col in ("B", "C", "D")
// That would be rows 0, 1, 3, 4, 5
assert_eq!(filtered_batch.num_rows(), 5);
let col_filtered = filtered_batch
.column(0)
.as_any()
.downcast_ref::<datatypes::arrow::array::StringArray>()
.unwrap();
assert_eq!(col_filtered, &expected);
}
#[test]
fn test_maybe_build_regex() {
// Test case for RegexMatch (case sensitive, non-negative)
let (regex, negative) = SimplePredicate::maybe_build_regex(
Operator::RegexMatch,
&ScalarValue::Utf8(Some("a.*b".to_string())),
)
.unwrap();
assert!(regex.is_some());
assert!(!negative);
assert!(regex.unwrap().is_match("axxb"));
// Test case for RegexIMatch (case insensitive, non-negative)
let (regex, negative) = SimplePredicate::maybe_build_regex(
Operator::RegexIMatch,
&ScalarValue::Utf8(Some("a.*b".to_string())),
)
.unwrap();
assert!(regex.is_some());
assert!(!negative);
assert!(regex.unwrap().is_match("AxxB"));
// Test case for RegexNotMatch (case sensitive, negative)
let (regex, negative) = SimplePredicate::maybe_build_regex(
Operator::RegexNotMatch,
&ScalarValue::Utf8(Some("a.*b".to_string())),
)
.unwrap();
assert!(regex.is_some());
assert!(negative);
// Test case for RegexNotIMatch (case insensitive, negative)
let (regex, negative) = SimplePredicate::maybe_build_regex(
Operator::RegexNotIMatch,
&ScalarValue::Utf8(Some("a.*b".to_string())),
)
.unwrap();
assert!(regex.is_some());
assert!(negative);
// Test with empty regex pattern
let (regex, negative) = SimplePredicate::maybe_build_regex(
Operator::RegexMatch,
&ScalarValue::Utf8(Some("".to_string())),
)
.unwrap();
assert!(regex.is_none());
assert!(!negative);
// Test with non-regex operator
let (regex, negative) = SimplePredicate::maybe_build_regex(
Operator::Eq,
&ScalarValue::Utf8(Some("a.*b".to_string())),
)
.unwrap();
assert!(regex.is_none());
assert!(!negative);
// Test with invalid regex pattern
let result = SimplePredicate::maybe_build_regex(
Operator::RegexMatch,
&ScalarValue::Utf8(Some("a(b".to_string())),
);
assert!(result.is_err());
// Test with non-string value
let result = SimplePredicate::maybe_build_regex(
Operator::RegexMatch,
&ScalarValue::Int64(Some(123)),
);
assert!(result.is_err());
// Test with null value
let result =
SimplePredicate::maybe_build_regex(Operator::RegexMatch, &ScalarValue::Utf8(None));
assert!(result.is_err());
}
#[test]
fn test_regex_match_dictionary_array() {
use datatypes::arrow::array::StringDictionaryBuilder;
// Create a StringDictionaryArray
let mut builder = StringDictionaryBuilder::<UInt32Type>::new();
builder.append("apple").unwrap();
builder.append("banana").unwrap();
builder.append("apple").unwrap();
builder.append("cherry").unwrap();
let dict_array = builder.finish();
// Test regex that matches "apple"
let regex = regex::Regex::new(r"app.*").unwrap();
let result = regexp_is_match_dictionary(&dict_array, Some(&regex)).unwrap();
// Should match indices 0 and 2 (both "apple")
assert_eq!(result.len(), 4);
assert!(result.value(0)); // "apple"
assert!(!result.value(1)); // "banana"
assert!(result.value(2)); // "apple"
assert!(!result.value(3)); // "cherry"
// Test regex that matches "banana"
let regex2 = regex::Regex::new(r"ban.*").unwrap();
let result2 = regexp_is_match_dictionary(&dict_array, Some(&regex2)).unwrap();
assert!(!result2.value(0)); // "apple"
assert!(result2.value(1)); // "banana"
assert!(!result2.value(2)); // "apple"
assert!(!result2.value(3)); // "cherry"
// Test with no regex (should match all)
let result3 = regexp_is_match_dictionary(&dict_array, None).unwrap();
assert!(result3.value(0));
assert!(result3.value(1));
assert!(result3.value(2));
assert!(result3.value(3));
}
#[test]
fn test_regex_scan_masks_preserve_sql_null_semantics() {
let plain = Arc::new(datatypes::arrow::array::StringArray::from(vec![
Some("api"),
Some("API"),
Some("db"),
None,
])) as ArrayRef;
assert_regex_scan_masks(
&plain,
[
vec![true, false, false, false],
vec![true, true, false, false],
vec![false, true, true, false],
vec![false, false, true, false],
],
);
let dictionary = DictionaryArray::new(
datatypes::arrow::array::UInt32Array::from(vec![
Some(0),
Some(1),
Some(2),
None,
Some(3),
]),
Arc::new(datatypes::arrow::array::StringArray::from(vec![
Some("api"),
Some("API"),
Some("db"),
None,
])),
);
let raw =
regexp_is_match_dictionary(&dictionary, Some(&Regex::new("^api$").unwrap())).unwrap();
assert!(raw.is_null(3)); // null dictionary key
assert!(raw.is_null(4)); // non-null key referencing a null dictionary value
let dictionary = Arc::new(dictionary) as ArrayRef;
assert_regex_scan_masks(
&dictionary,
[
vec![true, false, false, false, false],
vec![true, true, false, false, false],
vec![false, true, true, false, false],
vec![false, false, true, false, false],
],
);
let negative = regex_evaluator(Operator::RegexNotMatch);
assert!(!negative.evaluate_scalar(&ScalarValue::Utf8(None)).unwrap());
}
fn assert_regex_scan_masks(input: &ArrayRef, expected: [Vec<bool>; 4]) {
for (op, expected) in [
Operator::RegexMatch,
Operator::RegexIMatch,
Operator::RegexNotMatch,
Operator::RegexNotIMatch,
]
.into_iter()
.zip(expected)
{
assert_eq!(
BooleanBuffer::from(expected),
regex_evaluator(op).evaluate_array(input).unwrap(),
"{op:?}"
);
}
}
fn regex_evaluator(op: Operator) -> SimpleFilterEvaluator {
let expr = Expr::BinaryExpr(BinaryExpr {
left: Box::new(Expr::Column(Column::from_name("host"))),
op,
right: Box::new("^api$".lit()),
});
SimpleFilterEvaluator::try_new(&expr).unwrap()
}
}
@@ -0,0 +1,72 @@
// Copyright 2023 Greptime Team
//
// Licensed 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::sync::Arc;
use datafusion::execution::context::ExecutionProps;
use datafusion::logical_expr::physical_planning_context::PhysicalPlanningContext;
use datafusion::logical_expr::{col, lit};
use datafusion::physical_expr::create_physical_expr;
use datafusion_common::DFSchema;
use datatypes::arrow::datatypes::{DataType, Field, Schema};
use datatypes::arrow::array::{BooleanArray, RecordBatch};
use crate::filter::{batch_filter, boolean_array_to_scan_mask};
#[test]
fn batch_filter_test() {
let expr = col("ts").gt(lit(123456u64));
let schema = Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("ts", DataType::UInt64, false),
]);
let df_schema = DFSchema::try_from(schema.clone()).unwrap();
let props = ExecutionProps::new();
let physical_expr = create_physical_expr(
&expr,
&df_schema,
&props,
&PhysicalPlanningContext::default(),
)
.unwrap();
let batch = RecordBatch::try_new(
Arc::new(schema),
vec![
Arc::new(datatypes::arrow::array::Int32Array::from(vec![4, 5, 6])),
Arc::new(datatypes::arrow::array::UInt64Array::from(vec![
123456, 123457, 123458,
])),
],
)
.unwrap();
let new_batch = batch_filter(&batch, &physical_expr).unwrap();
assert_eq!(new_batch.num_rows(), 2);
let first_column_values = new_batch
.column(0)
.as_any()
.downcast_ref::<datatypes::arrow::array::Int32Array>()
.unwrap();
let expected = datatypes::arrow::array::Int32Array::from(vec![5, 6]);
assert_eq!(first_column_values, &expected);
}
#[test]
fn test_nullable_boolean_predicate_becomes_scan_mask() {
let predicate = BooleanArray::from(vec![Some(true), None, Some(false)]);
assert_eq!(
BooleanArray::from(vec![true, false, false]),
boolean_array_to_scan_mask(&predicate)
);
}
@@ -0,0 +1,439 @@
// Copyright 2023 Greptime Team
//
// Licensed 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.
//! Timestamp-unit adaptation for predicates evaluated against historical SSTs.
use common_time::timestamp::div_mod_units;
use datafusion::logical_expr::Operator;
use datafusion_common::ScalarValue;
use datafusion_common::arrow::array::{ArrayRef, Datum, Scalar};
use datatypes::arrow::datatypes::TimeUnit;
use crate::filter::predicate::SimplePredicate;
/// Builds a tz-naive timestamp literal for `value` in `unit`.
pub fn timestamp_scalar_value(value: i64, unit: TimeUnit) -> ScalarValue {
match unit {
TimeUnit::Second => ScalarValue::TimestampSecond(Some(value), None),
TimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(value), None),
TimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(value), None),
TimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(value), None),
}
}
impl SimplePredicate {
pub(crate) fn cast_timestamp_unit(&self, target_unit: TimeUnit) -> Option<Self> {
// Constants represent scan outcomes here. As in the public Matched/Pruned
// result, UNKNOWN can be collapsed to false under these AND/OR-only trees.
match self {
Self::And(left, right) | Self::Or(left, right) => {
let left = left.cast_timestamp_unit(target_unit)?;
let right = right.cast_timestamp_unit(target_unit)?;
let and = matches!(self, Self::And(..));
Some(match (left, right) {
(Self::Constant(Some(false)), _) | (_, Self::Constant(Some(false))) if and => {
Self::Constant(Some(false))
}
(Self::Constant(Some(true)), _) | (_, Self::Constant(Some(true))) if !and => {
Self::Constant(Some(true))
}
(Self::Constant(Some(true)), other) | (other, Self::Constant(Some(true)))
if and =>
{
other
}
(Self::Constant(Some(false)), other) | (other, Self::Constant(Some(false)))
if !and =>
{
other
}
(left, right) => {
if and {
Self::And(Box::new(left), Box::new(right))
} else {
Self::Or(Box::new(left), Box::new(right))
}
}
})
}
Self::IsNull { .. } => Some(self.clone()),
Self::Constant(value) => Some(Self::Constant(Some(value.unwrap_or(false)))),
Self::OrEqChain { literals } => {
let mut converted = Vec::with_capacity(literals.len());
for literal in literals {
let scalar = ScalarValue::try_from_array(literal.get().0, 0).ok()?;
let (value, unit) = timestamp_scalar_parts(&scalar)?;
let Some(value) = value else {
continue;
};
let cast = div_mod_units(value, unit.into(), target_unit.into())?;
if cast.remainder == 0 {
converted.push(timestamp_scalar(cast.quotient, target_unit)?);
}
}
if converted.is_empty() {
return Some(Self::Constant(Some(false)));
}
Some(Self::OrEqChain {
literals: converted,
})
}
Self::Comparison { literal, op, .. } => {
let scalar = ScalarValue::try_from_array(literal.get().0, 0).ok()?;
let (value, unit) = timestamp_scalar_parts(&scalar)?;
let Some(value) = value else {
return Some(Self::Constant(Some(false)));
};
if unit == target_unit {
return Some(self.clone());
}
let cast = div_mod_units(value, unit.into(), target_unit.into())?;
let divisible = cast.remainder == 0;
let literal = timestamp_scalar(cast.quotient, target_unit)?;
let filter = |op| Self::Comparison {
literal: literal.clone(),
op,
regex: None,
regex_negative: false,
};
Some(match op {
Operator::Eq if divisible => filter(Operator::Eq),
Operator::Eq => Self::Constant(Some(false)),
Operator::NotEq if divisible => filter(Operator::NotEq),
Operator::NotEq => Self::Constant(Some(true)),
// The quotient is floor(L), including negative timestamps.
// v > L is v > quotient whether or not L is representable.
Operator::Gt => filter(Operator::Gt),
Operator::GtEq if divisible => filter(Operator::GtEq),
// Between target values, v >= L is v > quotient.
Operator::GtEq => filter(Operator::Gt),
Operator::Lt if divisible => filter(Operator::Lt),
// Between target values, v < L is v <= quotient.
Operator::Lt => filter(Operator::LtEq),
Operator::LtEq => filter(Operator::LtEq),
_ => return None,
})
}
}
}
}
/// Extracts the value and unit from a tz-naive timestamp scalar.
fn timestamp_scalar_parts(scalar: &ScalarValue) -> Option<(Option<i64>, TimeUnit)> {
let (value, unit, timezone) = match scalar {
ScalarValue::TimestampSecond(v, tz) => (*v, TimeUnit::Second, tz),
ScalarValue::TimestampMillisecond(v, tz) => (*v, TimeUnit::Millisecond, tz),
ScalarValue::TimestampMicrosecond(v, tz) => (*v, TimeUnit::Microsecond, tz),
ScalarValue::TimestampNanosecond(v, tz) => (*v, TimeUnit::Nanosecond, tz),
_ => return None,
};
// A timezone-aware literal doesn't compare against a tz-naive column;
// leave it to the caller instead of guessing the intended semantics.
(timezone.is_none()).then_some((value, unit))
}
/// Builds a one-element scalar array holding `value` in `unit`.
fn timestamp_scalar(value: i64, unit: TimeUnit) -> Option<Scalar<ArrayRef>> {
timestamp_scalar_value(value, unit).to_scalar().ok()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use datafusion::execution::context::ExecutionProps;
use datafusion::logical_expr::physical_planning_context::PhysicalPlanningContext;
use datafusion::logical_expr::{Expr, col, lit};
use datafusion::physical_expr::create_physical_expr;
use datafusion_common::arrow::array::ArrayRef;
use datafusion_common::arrow::buffer::BooleanBuffer;
use datafusion_common::cast::as_boolean_array;
use datafusion_common::{DFSchema, ScalarValue};
use datatypes::arrow::array::{
RecordBatch, TimestampMillisecondArray, TimestampNanosecondArray,
};
use datatypes::arrow::datatypes::{Field, Schema};
use datatypes::data_type::ConcreteDataType;
use crate::filter::{SimpleFilterEvaluator, TimestampUnitCast, boolean_array_to_scan_mask};
#[test]
fn compound_timestamp_filters_preserve_unit_conversion() {
use datatypes::arrow::array::TimestampMicrosecondArray;
let null = || lit(ScalarValue::TimestampMicrosecond(None, None));
let predicates = [
col("ts")
.gt(lit(ts_us(-1500)))
.and(col("ts").lt(lit(ts_us(500)))),
col("ts")
.eq(lit(ts_us(500)))
.or(col("ts").gt_eq(lit(ts_us(1500)))),
col("ts")
.is_not_null()
.and(col("ts").not_eq(lit(ts_us(500)))),
col("ts").eq(null()).or(col("ts").eq(lit(ts_us(-1000)))),
col("ts")
.eq(lit(ts_us(500)))
.and(col("ts").gt(lit(ts_us(0)))),
col("ts")
.not_eq(lit(ts_us(500)))
.or(col("ts").lt(lit(ts_us(0)))),
];
let native: ArrayRef = Arc::new(TimestampMicrosecondArray::from(vec![
-2000, -1000, 0, 1000, 2000,
]));
let old: ArrayRef = Arc::new(TimestampMillisecondArray::from(vec![-2, -1, 0, 1, 2]));
let schema = Arc::new(Schema::new(vec![Field::new(
"ts",
native.data_type().clone(),
false,
)]));
let batch = RecordBatch::try_new(schema.clone(), vec![native]).unwrap();
for expr in predicates {
let physical = create_physical_expr(
&expr,
&DFSchema::try_from(schema.as_ref().clone()).unwrap(),
&ExecutionProps::new(),
&PhysicalPlanningContext::default(),
)
.unwrap();
let result = physical
.evaluate(&batch)
.unwrap()
.into_array(batch.num_rows())
.unwrap();
let expected = boolean_array_to_scan_mask(as_boolean_array(&result).unwrap())
.values()
.clone();
let actual = match cast_to_ms(&expr).unwrap() {
TimestampUnitCast::Matched => BooleanBuffer::new_set(old.len()),
TimestampUnitCast::Pruned => BooleanBuffer::new_unset(old.len()),
TimestampUnitCast::Filter(filter) => filter.evaluate_array(&old).unwrap(),
};
assert_eq!(actual, expected, "{expr}");
}
}
fn ts_us(v: i64) -> ScalarValue {
ScalarValue::TimestampMicrosecond(Some(v), None)
}
fn cast_to_ms(expr: &Expr) -> Option<TimestampUnitCast> {
SimpleFilterEvaluator::try_new(expr)
.unwrap()
.cast_timestamp_unit(&ConcreteDataType::timestamp_millisecond_datatype())
}
/// Evaluates a cast `Filter` outcome against millisecond values and
/// returns the mask.
fn eval_ms_mask(cast: Option<TimestampUnitCast>, values: &[i64]) -> Vec<bool> {
let filter = match cast.expect("cast must apply") {
TimestampUnitCast::Filter(filter) => filter,
other => panic!("expected Filter outcome, got {other:?}"),
};
let array = Arc::new(TimestampMillisecondArray::from(values.to_vec())) as ArrayRef;
filter.evaluate_array(&array).unwrap().iter().collect()
}
#[test]
fn cast_timestamp_unit_converts_representable_literal() {
// ts = 7_000_000us evaluated against a ms column becomes ts = 7000ms.
assert_eq!(
vec![false, true, false],
eval_ms_mask(
cast_to_ms(&col("ts").eq(lit(ts_us(7_000_000)))),
&[6_999, 7_000, 7_001]
)
);
// != complements =.
assert_eq!(
vec![true, false, true],
eval_ms_mask(
cast_to_ms(&col("ts").not_eq(lit(ts_us(7_000_000)))),
&[6_999, 7_000, 7_001]
)
);
}
#[test]
fn cast_timestamp_unit_prunes_non_representable_equality() {
// 7_000_500us is not a whole millisecond: no ms row can equal it,
// and every ms row satisfies `!=`.
assert!(matches!(
cast_to_ms(&col("ts").eq(lit(ts_us(7_000_500)))),
Some(TimestampUnitCast::Pruned)
));
assert!(matches!(
cast_to_ms(&col("ts").not_eq(lit(ts_us(7_000_500)))),
Some(TimestampUnitCast::Matched)
));
}
#[test]
fn cast_timestamp_unit_strengthens_inequalities() {
// 2_500_500us is strictly between 2500ms and 2501ms: the cast must
// not round the literal to a boundary the original predicate
// excludes (>= 2500ms would wrongly match 2500ms).
let values = vec![2_499, 2_500, 2_501];
assert_eq!(
vec![false, false, true],
eval_ms_mask(cast_to_ms(&col("ts").gt(lit(ts_us(2_500_500)))), &values)
);
assert_eq!(
vec![false, false, true],
eval_ms_mask(cast_to_ms(&col("ts").gt_eq(lit(ts_us(2_500_500)))), &values)
);
assert_eq!(
vec![true, true, false],
eval_ms_mask(cast_to_ms(&col("ts").lt(lit(ts_us(2_500_500)))), &values)
);
assert_eq!(
vec![true, true, false],
eval_ms_mask(cast_to_ms(&col("ts").lt_eq(lit(ts_us(2_500_500)))), &values)
);
// Representable boundary keeps the original operator.
let values = vec![2_499, 2_500, 2_501];
assert_eq!(
vec![false, false, true],
eval_ms_mask(cast_to_ms(&col("ts").gt(lit(ts_us(2_500_000)))), &values)
);
assert_eq!(
vec![false, true, true],
eval_ms_mask(cast_to_ms(&col("ts").gt_eq(lit(ts_us(2_500_000)))), &values)
);
assert_eq!(
vec![true, false, false],
eval_ms_mask(cast_to_ms(&col("ts").lt(lit(ts_us(2_500_000)))), &values)
);
assert_eq!(
vec![true, true, false],
eval_ms_mask(cast_to_ms(&col("ts").lt_eq(lit(ts_us(2_500_000)))), &values)
);
}
#[test]
fn cast_timestamp_unit_handles_negative_instants() {
// -2_500_500us floors to -2501ms with remainder 500us: only rows
// with instant >= -2.5005ms may match >= / >.
let values = vec![-2_502, -2_501, -2_500];
assert_eq!(
vec![false, false, true],
eval_ms_mask(
cast_to_ms(&col("ts").gt_eq(lit(ts_us(-2_500_500)))),
&values
)
);
assert_eq!(
vec![false, false, true],
eval_ms_mask(cast_to_ms(&col("ts").gt(lit(ts_us(-2_500_500)))), &values)
);
assert_eq!(
vec![true, true, false],
eval_ms_mask(
cast_to_ms(&col("ts").lt_eq(lit(ts_us(-2_500_500)))),
&values
)
);
// Exactly representable negative literal: -2_500_000us == -2500ms.
assert_eq!(
vec![false, false, true],
eval_ms_mask(cast_to_ms(&col("ts").eq(lit(ts_us(-2_500_000)))), &values)
);
}
#[test]
fn cast_timestamp_unit_or_chain_drops_unrepresentable_literals() {
// Only 7_000_000us is a whole millisecond; 7_000_500us cannot match.
let expr = col("ts")
.eq(lit(ts_us(7_000_500)))
.or(col("ts").eq(lit(ts_us(7_000_000))));
assert_eq!(
vec![false, true],
eval_ms_mask(cast_to_ms(&expr), &[6_999, 7_000])
);
// A chain where no literal is representable matches nothing.
let expr = col("ts")
.eq(lit(ts_us(7_000_500)))
.or(col("ts").eq(lit(ts_us(6_000_500))));
assert!(matches!(cast_to_ms(&expr), Some(TimestampUnitCast::Pruned)));
}
#[test]
fn cast_timestamp_unit_null_literal_prunes() {
// A NULL literal never compares true.
assert!(matches!(
cast_to_ms(&col("ts").eq(lit(ScalarValue::TimestampMicrosecond(None, None)))),
Some(TimestampUnitCast::Pruned)
));
}
#[test]
fn cast_timestamp_unit_same_unit_returns_filter_directly() {
// A literal already in the target unit is returned unchanged.
let expr = col("ts").eq(lit(ts_us(7_000_000)));
let filter = SimpleFilterEvaluator::try_new(&expr).unwrap();
let cast = filter
.cast_timestamp_unit(&ConcreteDataType::timestamp_microsecond_datatype())
.unwrap();
let TimestampUnitCast::Filter(f) = cast else {
panic!("expected Filter, got {cast:?}")
};
assert!(filter.is_eq() && f.is_eq());
assert_eq!(filter.column_name(), f.column_name());
assert_eq!(filter.literal_value(), f.literal_value());
}
#[test]
fn cast_timestamp_unit_rejects_non_timestamps() {
// Non-timestamp target type.
assert!(
SimpleFilterEvaluator::try_new(&col("ts").gt(lit(ts_us(1))))
.unwrap()
.cast_timestamp_unit(&ConcreteDataType::int64_datatype())
.is_none()
);
// Non-timestamp literal against a timestamp target.
assert!(cast_to_ms(&col("ts").gt(lit(42_i64))).is_none());
}
#[test]
fn cast_timestamp_unit_to_finer_unit() {
// 5 seconds against a nanosecond column becomes 5_000_000_000ns.
let filter = SimpleFilterEvaluator::try_new(
&col("ts").eq(lit(ScalarValue::TimestampSecond(Some(5), None))),
)
.unwrap()
.cast_timestamp_unit(&ConcreteDataType::timestamp_nanosecond_datatype())
.and_then(|cast| match cast {
TimestampUnitCast::Filter(filter) => Some(filter),
_ => None,
})
.unwrap();
let array = Arc::new(TimestampNanosecondArray::from(vec![
4_999_999_999,
5_000_000_000,
])) as ArrayRef;
assert_eq!(
vec![false, true],
filter
.evaluate_array(&array)
.unwrap()
.iter()
.collect::<Vec<_>>()
);
}
}