use std::{ops::Bound, sync::Arc};
use arrow_schema::{DataType, Field};
use async_recursion::async_recursion;
use async_trait::async_trait;
use datafusion_common::ScalarValue;
use datafusion_expr::{
Between, BinaryExpr, Expr, Operator, ReturnFieldArgs, ScalarUDF,
expr::{InList, Like, ScalarFunction},
};
use tokio::try_join;
use super::{
AnyQuery, BloomFilterQuery, LabelListQuery, MetricsCollector, SargableQuery, ScalarIndex,
SearchResult, TextQuery, TokenQuery,
};
#[cfg(feature = "geo")]
use super::{GeoQuery, RelationQuery};
use lance_core::{Error, Result};
use lance_datafusion::{expr::safe_coerce_scalar, planner::Planner};
use lance_select::{IndexExprResult, NullableIndexExprResult, NullableRowAddrMask};
use roaring::RoaringBitmap;
use tracing::instrument;
const MAX_DEPTH: usize = 500;
#[derive(Debug, PartialEq)]
pub struct IndexedExpression {
pub scalar_query: Option<ScalarIndexExpr>,
pub refine_expr: Option<Expr>,
}
pub trait ScalarQueryParser: std::fmt::Debug + Send + Sync {
fn visit_between(
&self,
column: &str,
low: &Bound<ScalarValue>,
high: &Bound<ScalarValue>,
) -> Option<IndexedExpression>;
fn visit_in_list(&self, column: &str, in_list: &[ScalarValue]) -> Option<IndexedExpression>;
fn visit_is_bool(&self, column: &str, value: bool) -> Option<IndexedExpression>;
fn visit_is_null(&self, column: &str) -> Option<IndexedExpression>;
fn visit_comparison(
&self,
column: &str,
value: &ScalarValue,
op: &Operator,
) -> Option<IndexedExpression>;
fn visit_scalar_function(
&self,
column: &str,
data_type: &DataType,
func: &ScalarUDF,
args: &[Expr],
) -> Option<IndexedExpression>;
fn visit_like(
&self,
_column: &str,
_like: &Like,
_pattern: &ScalarValue,
) -> Option<IndexedExpression> {
None
}
fn is_valid_reference(&self, func: &Expr, data_type: &DataType) -> Option<DataType> {
match func {
Expr::Column(_) => Some(data_type.clone()),
_ => None,
}
}
}
#[derive(Debug)]
pub struct MultiQueryParser {
parsers: Vec<Box<dyn ScalarQueryParser>>,
}
impl MultiQueryParser {
pub fn single(parser: Box<dyn ScalarQueryParser>) -> Self {
Self {
parsers: vec![parser],
}
}
pub fn add(&mut self, other: Box<dyn ScalarQueryParser>) {
self.parsers.push(other);
}
}
impl ScalarQueryParser for MultiQueryParser {
fn visit_between(
&self,
column: &str,
low: &Bound<ScalarValue>,
high: &Bound<ScalarValue>,
) -> Option<IndexedExpression> {
self.parsers
.iter()
.find_map(|parser| parser.visit_between(column, low, high))
}
fn visit_in_list(&self, column: &str, in_list: &[ScalarValue]) -> Option<IndexedExpression> {
self.parsers
.iter()
.find_map(|parser| parser.visit_in_list(column, in_list))
}
fn visit_is_bool(&self, column: &str, value: bool) -> Option<IndexedExpression> {
self.parsers
.iter()
.find_map(|parser| parser.visit_is_bool(column, value))
}
fn visit_is_null(&self, column: &str) -> Option<IndexedExpression> {
self.parsers
.iter()
.find_map(|parser| parser.visit_is_null(column))
}
fn visit_comparison(
&self,
column: &str,
value: &ScalarValue,
op: &Operator,
) -> Option<IndexedExpression> {
self.parsers
.iter()
.find_map(|parser| parser.visit_comparison(column, value, op))
}
fn visit_scalar_function(
&self,
column: &str,
data_type: &DataType,
func: &ScalarUDF,
args: &[Expr],
) -> Option<IndexedExpression> {
self.parsers
.iter()
.find_map(|parser| parser.visit_scalar_function(column, data_type, func, args))
}
fn visit_like(
&self,
column: &str,
like: &Like,
pattern: &ScalarValue,
) -> Option<IndexedExpression> {
self.parsers
.iter()
.find_map(|parser| parser.visit_like(column, like, pattern))
}
fn is_valid_reference(&self, func: &Expr, data_type: &DataType) -> Option<DataType> {
self.parsers
.iter()
.find_map(|parser| parser.is_valid_reference(func, data_type))
}
}
#[derive(Debug)]
pub struct SargableQueryParser {
index_name: String,
index_type: String,
needs_recheck: bool,
}
impl SargableQueryParser {
pub fn new(index_name: String, index_type: String, needs_recheck: bool) -> Self {
Self {
index_name,
index_type,
needs_recheck,
}
}
}
impl ScalarQueryParser for SargableQueryParser {
fn is_valid_reference(&self, func: &Expr, data_type: &DataType) -> Option<DataType> {
match func {
Expr::Column(_) => Some(data_type.clone()),
Expr::ScalarFunction(udf) if udf.name() == "get_field" => Some(data_type.clone()),
_ => None,
}
}
fn visit_between(
&self,
column: &str,
low: &Bound<ScalarValue>,
high: &Bound<ScalarValue>,
) -> Option<IndexedExpression> {
if let Bound::Included(val) | Bound::Excluded(val) = low
&& val.is_null()
{
return None;
}
if let Bound::Included(val) | Bound::Excluded(val) = high
&& val.is_null()
{
return None;
}
let query = SargableQuery::Range(low.clone(), high.clone());
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
self.needs_recheck,
))
}
fn visit_in_list(&self, column: &str, in_list: &[ScalarValue]) -> Option<IndexedExpression> {
if in_list.iter().any(|val| val.is_null()) {
return None;
}
let query = SargableQuery::IsIn(in_list.to_vec());
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
self.needs_recheck,
))
}
fn visit_is_bool(&self, column: &str, value: bool) -> Option<IndexedExpression> {
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(SargableQuery::Equals(ScalarValue::Boolean(Some(value)))),
self.needs_recheck,
))
}
fn visit_is_null(&self, column: &str) -> Option<IndexedExpression> {
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(SargableQuery::IsNull()),
self.needs_recheck,
))
}
fn visit_comparison(
&self,
column: &str,
value: &ScalarValue,
op: &Operator,
) -> Option<IndexedExpression> {
if value.is_null() {
return None;
}
let query = match op {
Operator::Lt => SargableQuery::Range(Bound::Unbounded, Bound::Excluded(value.clone())),
Operator::LtEq => {
SargableQuery::Range(Bound::Unbounded, Bound::Included(value.clone()))
}
Operator::Gt => SargableQuery::Range(Bound::Excluded(value.clone()), Bound::Unbounded),
Operator::GtEq => {
SargableQuery::Range(Bound::Included(value.clone()), Bound::Unbounded)
}
Operator::Eq => SargableQuery::Equals(value.clone()),
Operator::NotEq => SargableQuery::Equals(value.clone()),
_ => unreachable!(),
};
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
self.needs_recheck,
))
}
fn visit_scalar_function(
&self,
column: &str,
_data_type: &DataType,
func: &ScalarUDF,
args: &[Expr],
) -> Option<IndexedExpression> {
if func.name() == "starts_with" && args.len() == 2 {
let prefix = match &args[1] {
Expr::Literal(ScalarValue::Utf8(Some(s)), _) => ScalarValue::Utf8(Some(s.clone())),
Expr::Literal(ScalarValue::LargeUtf8(Some(s)), _) => {
ScalarValue::LargeUtf8(Some(s.clone()))
}
_ => return None,
};
let query = SargableQuery::LikePrefix(prefix);
return Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
self.needs_recheck,
));
}
None
}
fn visit_like(
&self,
column: &str,
like: &Like,
pattern: &ScalarValue,
) -> Option<IndexedExpression> {
if like.case_insensitive {
return None;
}
let pattern_str = match pattern {
ScalarValue::Utf8(Some(s)) => s.as_str(),
ScalarValue::LargeUtf8(Some(s)) => s.as_str(),
_ => return None,
};
let (prefix, needs_refine) = extract_like_leading_prefix(pattern_str, like.escape_char)?;
let prefix_value = match pattern {
ScalarValue::Utf8(_) => ScalarValue::Utf8(Some(prefix)),
ScalarValue::LargeUtf8(_) => ScalarValue::LargeUtf8(Some(prefix)),
_ => return None,
};
let query = SargableQuery::LikePrefix(prefix_value);
let scalar_query = Some(ScalarIndexExpr::Query(ScalarIndexSearch {
column: column.to_string(),
index_name: self.index_name.clone(),
index_type: self.index_type.clone(),
query: Arc::new(query),
needs_recheck: self.needs_recheck,
fragment_bitmap: None,
}));
let refine_expr = if needs_refine {
Some(Expr::Like(like.clone()))
} else {
None
};
Some(IndexedExpression {
scalar_query,
refine_expr,
})
}
}
fn extract_like_leading_prefix(pattern: &str, escape_char: Option<char>) -> Option<(String, bool)> {
let chars: Vec<char> = pattern.chars().collect();
let len = chars.len();
if len == 0 {
return None;
}
let effective_escape_char = escape_char.or(Some('\\'));
let is_escaped = |i: usize| -> bool {
if let Some(esc) = effective_escape_char {
if i > 0 && chars[i - 1] == esc {
if i >= 2 && chars[i - 2] == esc {
false } else {
true }
} else {
false
}
} else {
false
}
};
let has_wildcard = chars.iter().enumerate().any(|(i, &c)| {
if c != '%' && c != '_' {
return false;
}
!is_escaped(i)
});
if !has_wildcard {
return None; }
if chars[0] == '%' || chars[0] == '_' {
return None; }
let mut prefix = String::new();
let mut i = 0;
let mut found_wildcard = false;
while i < len {
let c = chars[i];
if let Some(esc) = effective_escape_char
&& c == esc
&& i + 1 < len
{
let next = chars[i + 1];
if next == '%' || next == '_' || next == esc {
prefix.push(next);
i += 2;
continue;
}
}
if c == '%' || c == '_' {
found_wildcard = true;
break;
}
prefix.push(c);
i += 1;
}
if prefix.is_empty() {
return None;
}
let needs_refine = if found_wildcard && i < len {
if chars[i] == '%' && i + 1 == len {
false
} else {
true
}
} else {
false
};
Some((prefix, needs_refine))
}
#[derive(Debug)]
pub struct BloomFilterQueryParser {
index_name: String,
index_type: String,
needs_recheck: bool,
}
impl BloomFilterQueryParser {
pub fn new(index_name: String, index_type: String, needs_recheck: bool) -> Self {
Self {
index_name,
index_type,
needs_recheck,
}
}
}
impl ScalarQueryParser for BloomFilterQueryParser {
fn visit_between(
&self,
_: &str,
_: &Bound<ScalarValue>,
_: &Bound<ScalarValue>,
) -> Option<IndexedExpression> {
None
}
fn visit_in_list(&self, column: &str, in_list: &[ScalarValue]) -> Option<IndexedExpression> {
let query = BloomFilterQuery::IsIn(in_list.to_vec());
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
self.needs_recheck,
))
}
fn visit_is_bool(&self, column: &str, value: bool) -> Option<IndexedExpression> {
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(BloomFilterQuery::Equals(ScalarValue::Boolean(Some(value)))),
self.needs_recheck,
))
}
fn visit_is_null(&self, column: &str) -> Option<IndexedExpression> {
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(BloomFilterQuery::IsNull()),
self.needs_recheck,
))
}
fn visit_comparison(
&self,
column: &str,
value: &ScalarValue,
op: &Operator,
) -> Option<IndexedExpression> {
let query = match op {
Operator::Eq => BloomFilterQuery::Equals(value.clone()),
Operator::NotEq => BloomFilterQuery::Equals(value.clone()),
_ => return None,
};
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
self.needs_recheck,
))
}
fn visit_scalar_function(
&self,
_: &str,
_: &DataType,
_: &ScalarUDF,
_: &[Expr],
) -> Option<IndexedExpression> {
None
}
}
#[derive(Debug)]
pub struct LabelListQueryParser {
index_name: String,
index_type: String,
}
impl LabelListQueryParser {
pub fn new(index_name: String, index_type: String) -> Self {
Self {
index_name,
index_type,
}
}
}
impl ScalarQueryParser for LabelListQueryParser {
fn visit_between(
&self,
_: &str,
_: &Bound<ScalarValue>,
_: &Bound<ScalarValue>,
) -> Option<IndexedExpression> {
None
}
fn visit_in_list(&self, _: &str, _: &[ScalarValue]) -> Option<IndexedExpression> {
None
}
fn visit_is_bool(&self, _: &str, _: bool) -> Option<IndexedExpression> {
None
}
fn visit_is_null(&self, _: &str) -> Option<IndexedExpression> {
None
}
fn visit_comparison(
&self,
_: &str,
_: &ScalarValue,
_: &Operator,
) -> Option<IndexedExpression> {
None
}
fn visit_scalar_function(
&self,
column: &str,
data_type: &DataType,
func: &ScalarUDF,
args: &[Expr],
) -> Option<IndexedExpression> {
if args.len() != 2 {
return None;
}
if func.name() == "array_has" {
let inner_type = match data_type {
DataType::List(field) | DataType::LargeList(field) => field.data_type(),
_ => return None,
};
let scalar = maybe_scalar(&args[1], inner_type)?;
if scalar.is_null() {
return None;
}
let query = LabelListQuery::HasAnyLabel(vec![scalar]);
return Some(IndexedExpression::index_query(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
));
}
let label_list = maybe_scalar(&args[1], data_type)?;
if let ScalarValue::List(list_arr) = label_list {
let list_values = list_arr.values();
if list_values.is_empty() {
return None;
}
let mut scalars = Vec::with_capacity(list_values.len());
for idx in 0..list_values.len() {
scalars.push(ScalarValue::try_from_array(list_values.as_ref(), idx).ok()?);
}
if func.name() == "array_has_all" {
let query = LabelListQuery::HasAllLabels(scalars);
Some(IndexedExpression::index_query(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
))
} else if func.name() == "array_has_any" {
let query = LabelListQuery::HasAnyLabel(scalars);
Some(IndexedExpression::index_query(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
))
} else {
None
}
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct TextQueryParser {
index_name: String,
index_type: String,
needs_recheck: bool,
supports_regex: bool,
}
impl TextQueryParser {
pub fn new(
index_name: String,
index_type: String,
needs_recheck: bool,
supports_regex: bool,
) -> Self {
Self {
index_name,
index_type,
needs_recheck,
supports_regex,
}
}
}
impl ScalarQueryParser for TextQueryParser {
fn visit_between(
&self,
_: &str,
_: &Bound<ScalarValue>,
_: &Bound<ScalarValue>,
) -> Option<IndexedExpression> {
None
}
fn visit_in_list(&self, _: &str, _: &[ScalarValue]) -> Option<IndexedExpression> {
None
}
fn visit_is_bool(&self, _: &str, _: bool) -> Option<IndexedExpression> {
None
}
fn visit_is_null(&self, _: &str) -> Option<IndexedExpression> {
None
}
fn visit_comparison(
&self,
_: &str,
_: &ScalarValue,
_: &Operator,
) -> Option<IndexedExpression> {
None
}
fn visit_scalar_function(
&self,
column: &str,
data_type: &DataType,
func: &ScalarUDF,
args: &[Expr],
) -> Option<IndexedExpression> {
if args.len() < 2 {
return None;
}
let (ScalarValue::Utf8(Some(pattern)) | ScalarValue::LargeUtf8(Some(pattern))) =
maybe_scalar(&args[1], data_type)?
else {
return None;
};
let query = match func.name() {
"contains" if args.len() == 2 => TextQuery::StringContains(pattern),
"regexp_like" | "regexp_match" if self.supports_regex => {
let pattern = match args.get(2) {
Some(flags_expr) => apply_regex_flags(&pattern, flags_expr)?,
None => pattern,
};
if !crate::scalar::ngram::regex_can_use_index(&pattern) {
return None;
}
TextQuery::Regex(pattern)
}
_ => return None,
};
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
self.needs_recheck,
))
}
fn visit_like(
&self,
column: &str,
like: &Like,
pattern: &ScalarValue,
) -> Option<IndexedExpression> {
if !self.supports_regex || like.case_insensitive {
return None;
}
let pattern_str = match pattern {
ScalarValue::Utf8(Some(s)) | ScalarValue::LargeUtf8(Some(s)) => s.as_str(),
_ => return None,
};
let regex = like_to_regex(pattern_str, like.escape_char)?;
if !crate::scalar::ngram::regex_can_use_index(®ex) {
return None;
}
Some(IndexedExpression {
scalar_query: Some(ScalarIndexExpr::Query(ScalarIndexSearch {
column: column.to_string(),
index_name: self.index_name.clone(),
index_type: self.index_type.clone(),
query: Arc::new(TextQuery::Regex(regex)),
needs_recheck: self.needs_recheck,
fragment_bitmap: None,
})),
refine_expr: Some(Expr::Like(like.clone())),
})
}
}
fn like_to_regex(pattern: &str, escape: Option<char>) -> Option<String> {
let mut regex = String::new();
let mut run = 0usize;
let mut longest_run = 0usize;
let mut chars = pattern.chars();
while let Some(c) = chars.next() {
let literal = if Some(c) == escape {
chars.next()
} else {
match c {
'%' => {
regex.push_str(".*");
run = 0;
None
}
'_' => {
regex.push('.');
run = 0;
None
}
other => Some(other),
}
};
if let Some(lit) = literal {
if regex_syntax::is_meta_character(lit) {
regex.push('\\');
}
regex.push(lit);
if lit.is_alphanumeric() {
run += 1;
longest_run = longest_run.max(run);
} else {
run = 0;
}
}
}
(longest_run >= 3).then_some(regex)
}
fn apply_regex_flags(pattern: &str, flags_expr: &Expr) -> Option<String> {
let (Expr::Literal(ScalarValue::Utf8(Some(flags)), _)
| Expr::Literal(ScalarValue::LargeUtf8(Some(flags)), _)) = flags_expr
else {
return None;
};
let mut inline = String::new();
for flag in flags.chars() {
if ['i', 's', 'm', 'x'].contains(&flag) {
inline.push(flag);
} else {
return None;
}
}
if inline.is_empty() {
Some(pattern.to_string())
} else {
Some(format!("(?{inline}){pattern}"))
}
}
#[derive(Debug, Clone)]
pub struct FtsQueryParser {
index_name: String,
index_type: String,
}
impl FtsQueryParser {
pub fn new(name: String, index_type: String) -> Self {
Self {
index_name: name,
index_type,
}
}
}
impl ScalarQueryParser for FtsQueryParser {
fn visit_between(
&self,
_: &str,
_: &Bound<ScalarValue>,
_: &Bound<ScalarValue>,
) -> Option<IndexedExpression> {
None
}
fn visit_in_list(&self, _: &str, _: &[ScalarValue]) -> Option<IndexedExpression> {
None
}
fn visit_is_bool(&self, _: &str, _: bool) -> Option<IndexedExpression> {
None
}
fn visit_is_null(&self, _: &str) -> Option<IndexedExpression> {
None
}
fn visit_comparison(
&self,
_: &str,
_: &ScalarValue,
_: &Operator,
) -> Option<IndexedExpression> {
None
}
fn visit_scalar_function(
&self,
column: &str,
data_type: &DataType,
func: &ScalarUDF,
args: &[Expr],
) -> Option<IndexedExpression> {
if args.len() != 2 {
return None;
}
let scalar = maybe_scalar(&args[1], data_type)?;
if let ScalarValue::Utf8(Some(scalar_str)) = scalar
&& func.name() == "contains_tokens"
{
let query = TokenQuery::TokensContains(scalar_str);
return Some(IndexedExpression::index_query(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
));
}
None
}
}
#[cfg(feature = "geo")]
#[derive(Debug, Clone)]
pub struct GeoQueryParser {
index_name: String,
index_type: String,
}
#[cfg(feature = "geo")]
impl GeoQueryParser {
pub fn new(index_name: String, index_type: String) -> Self {
Self {
index_name,
index_type,
}
}
}
#[cfg(feature = "geo")]
impl ScalarQueryParser for GeoQueryParser {
fn visit_between(
&self,
_: &str,
_: &Bound<ScalarValue>,
_: &Bound<ScalarValue>,
) -> Option<IndexedExpression> {
None
}
fn visit_in_list(&self, _: &str, _: &[ScalarValue]) -> Option<IndexedExpression> {
None
}
fn visit_is_bool(&self, _: &str, _: bool) -> Option<IndexedExpression> {
None
}
fn visit_is_null(&self, column: &str) -> Option<IndexedExpression> {
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(GeoQuery::IsNull),
true,
))
}
fn visit_comparison(
&self,
_: &str,
_: &ScalarValue,
_: &Operator,
) -> Option<IndexedExpression> {
None
}
fn visit_scalar_function(
&self,
column: &str,
_data_type: &DataType,
func: &ScalarUDF,
args: &[Expr],
) -> Option<IndexedExpression> {
if (func.name() == "st_intersects"
|| func.name() == "st_contains"
|| func.name() == "st_within"
|| func.name() == "st_touches"
|| func.name() == "st_crosses"
|| func.name() == "st_overlaps"
|| func.name() == "st_covers"
|| func.name() == "st_coveredby")
&& args.len() == 2
{
let left_arg = &args[0];
let right_arg = &args[1];
return match (left_arg, right_arg) {
(Expr::Literal(left_value, metadata), Expr::Column(_)) => {
let mut field = Field::new("_geo", left_value.data_type(), false);
if let Some(metadata) = metadata {
field = field.with_metadata(metadata.to_hashmap());
}
let query = GeoQuery::IntersectQuery(RelationQuery {
value: left_value.clone(),
field,
});
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
true,
))
}
(Expr::Column(_), Expr::Literal(right_value, metadata)) => {
let mut field = Field::new("_geo", right_value.data_type(), false);
if let Some(metadata) = metadata {
field = field.with_metadata(metadata.to_hashmap());
}
let query = GeoQuery::IntersectQuery(RelationQuery {
value: right_value.clone(),
field,
});
Some(IndexedExpression::index_query_with_recheck(
column.to_string(),
self.index_name.clone(),
self.index_type.clone(),
Arc::new(query),
true,
))
}
_ => None,
};
}
None
}
}
impl IndexedExpression {
fn refine_only(refine_expr: Expr) -> Self {
Self {
scalar_query: None,
refine_expr: Some(refine_expr),
}
}
fn index_query(
column: String,
index_name: String,
index_type: String,
query: Arc<dyn AnyQuery>,
) -> Self {
Self {
scalar_query: Some(ScalarIndexExpr::Query(ScalarIndexSearch {
column,
index_name,
index_type,
query,
needs_recheck: false, fragment_bitmap: None, })),
refine_expr: None,
}
}
fn index_query_with_recheck(
column: String,
index_name: String,
index_type: String,
query: Arc<dyn AnyQuery>,
needs_recheck: bool,
) -> Self {
Self {
scalar_query: Some(ScalarIndexExpr::Query(ScalarIndexSearch {
column,
index_name,
index_type,
query,
needs_recheck,
fragment_bitmap: None, })),
refine_expr: None,
}
}
fn maybe_not(self) -> Option<Self> {
match (self.scalar_query, self.refine_expr) {
(Some(_), Some(_)) => None,
(Some(scalar_query), None) => {
if scalar_query.needs_recheck() {
return None;
}
Some(Self {
scalar_query: Some(ScalarIndexExpr::Not(Box::new(scalar_query))),
refine_expr: None,
})
}
(None, Some(refine_expr)) => Some(Self {
scalar_query: None,
refine_expr: Some(Expr::Not(Box::new(refine_expr))),
}),
(None, None) => panic!("Empty node should not occur"),
}
}
fn and(self, other: Self) -> Self {
let scalar_query = match (self.scalar_query, other.scalar_query) {
(Some(scalar_query), Some(other_scalar_query)) => Some(ScalarIndexExpr::And(
Box::new(scalar_query),
Box::new(other_scalar_query),
)),
(Some(scalar_query), None) => Some(scalar_query),
(None, Some(scalar_query)) => Some(scalar_query),
(None, None) => None,
};
let refine_expr = match (self.refine_expr, other.refine_expr) {
(Some(refine_expr), Some(other_refine_expr)) => {
Some(refine_expr.and(other_refine_expr))
}
(Some(refine_expr), None) => Some(refine_expr),
(None, Some(refine_expr)) => Some(refine_expr),
(None, None) => None,
};
Self {
scalar_query,
refine_expr,
}
}
fn maybe_or(self, other: Self) -> Option<Self> {
let scalar_query = self.scalar_query?;
let other_scalar_query = other.scalar_query?;
let scalar_query = Some(ScalarIndexExpr::Or(
Box::new(scalar_query),
Box::new(other_scalar_query),
));
let refine_expr = match (self.refine_expr, other.refine_expr) {
(Some(_), Some(_)) => {
return None;
}
(Some(_), None) => {
return None;
}
(None, Some(_)) => {
return None;
}
(None, None) => None,
};
Some(Self {
scalar_query,
refine_expr,
})
}
fn refine(self, expr: Expr) -> Self {
match self.refine_expr {
Some(refine_expr) => Self {
scalar_query: self.scalar_query,
refine_expr: Some(refine_expr.and(expr)),
},
None => Self {
scalar_query: self.scalar_query,
refine_expr: Some(expr),
},
}
}
}
#[async_trait]
pub trait ScalarIndexLoader: Send + Sync {
async fn load_index(
&self,
column: &str,
index_name: &str,
metrics: &dyn MetricsCollector,
) -> Result<Arc<dyn ScalarIndex>>;
}
#[derive(Debug, Clone)]
pub struct ScalarIndexSearch {
pub column: String,
pub index_name: String,
pub index_type: String,
pub query: Arc<dyn AnyQuery>,
pub needs_recheck: bool,
pub fragment_bitmap: Option<RoaringBitmap>,
}
impl PartialEq for ScalarIndexSearch {
fn eq(&self, other: &Self) -> bool {
self.column == other.column
&& self.index_name == other.index_name
&& self.query.as_ref().eq(other.query.as_ref())
}
}
#[derive(Debug, Clone)]
pub enum ScalarIndexExpr {
Not(Box<Self>),
And(Box<Self>, Box<Self>),
Or(Box<Self>, Box<Self>),
Query(ScalarIndexSearch),
}
impl PartialEq for ScalarIndexExpr {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Not(l0), Self::Not(r0)) => l0 == r0,
(Self::And(l0, l1), Self::And(r0, r1)) => l0 == r0 && l1 == r1,
(Self::Or(l0, l1), Self::Or(r0, r1)) => l0 == r0 && l1 == r1,
(Self::Query(l_search), Self::Query(r_search)) => l_search == r_search,
_ => false,
}
}
}
impl std::fmt::Display for ScalarIndexExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Not(inner) => write!(f, "NOT({})", inner),
Self::And(lhs, rhs) => write!(f, "AND({},{})", lhs, rhs),
Self::Or(lhs, rhs) => write!(f, "OR({},{})", lhs, rhs),
Self::Query(search) => write!(
f,
"[{}]@{}({})",
search.query.format(&search.column),
search.index_name,
search.index_type
),
}
}
}
impl From<SearchResult> for NullableIndexExprResult {
fn from(result: SearchResult) -> Self {
match result {
SearchResult::Exact(mask) => Self::exact(NullableRowAddrMask::AllowList(mask)),
SearchResult::AtMost(mask) => Self::at_most(NullableRowAddrMask::AllowList(mask)),
SearchResult::AtLeast(mask) => Self::at_least(NullableRowAddrMask::AllowList(mask)),
}
}
}
impl ScalarIndexExpr {
#[async_recursion]
pub async fn evaluate_nullable(
&self,
index_loader: &dyn ScalarIndexLoader,
metrics: &dyn MetricsCollector,
) -> Result<NullableIndexExprResult> {
match self {
Self::Not(inner) => {
let result = inner.evaluate_nullable(index_loader, metrics).await?;
Ok(!result)
}
Self::And(lhs, rhs) => {
let lhs_result = lhs.evaluate_nullable(index_loader, metrics);
let rhs_result = rhs.evaluate_nullable(index_loader, metrics);
let (lhs_result, rhs_result) = try_join!(lhs_result, rhs_result)?;
Ok(lhs_result & rhs_result)
}
Self::Or(lhs, rhs) => {
let lhs_result = lhs.evaluate_nullable(index_loader, metrics);
let rhs_result = rhs.evaluate_nullable(index_loader, metrics);
let (lhs_result, rhs_result) = try_join!(lhs_result, rhs_result)?;
Ok(lhs_result | rhs_result)
}
Self::Query(search) => {
let index = index_loader
.load_index(&search.column, &search.index_name, metrics)
.await?;
let search_result = index.search(search.query.as_ref(), metrics).await?;
Ok(search_result.into())
}
}
}
#[instrument(level = "debug", skip_all)]
pub async fn evaluate(
&self,
index_loader: &dyn ScalarIndexLoader,
metrics: &dyn MetricsCollector,
) -> Result<IndexExprResult> {
Ok(self
.evaluate_nullable(index_loader, metrics)
.await?
.drop_nulls())
}
pub fn to_expr(&self) -> Expr {
match self {
Self::Not(inner) => Expr::Not(inner.to_expr().into()),
Self::And(lhs, rhs) => {
let lhs = lhs.to_expr();
let rhs = rhs.to_expr();
lhs.and(rhs)
}
Self::Or(lhs, rhs) => {
let lhs = lhs.to_expr();
let rhs = rhs.to_expr();
lhs.or(rhs)
}
Self::Query(search) => search.query.to_expr(search.column.clone()),
}
}
pub fn needs_recheck(&self) -> bool {
match self {
Self::Not(inner) => inner.needs_recheck(),
Self::And(lhs, rhs) | Self::Or(lhs, rhs) => lhs.needs_recheck() || rhs.needs_recheck(),
Self::Query(search) => search.needs_recheck,
}
}
}
fn maybe_column(expr: &Expr) -> Option<&str> {
match expr {
Expr::Column(col) => Some(&col.name),
_ => None,
}
}
fn extract_nested_column_path(expr: &Expr) -> Option<String> {
let mut current_expr = expr;
let mut parts = Vec::new();
loop {
match current_expr {
Expr::ScalarFunction(udf) if udf.name() == "get_field" => {
if udf.args.len() != 2 {
return None;
}
if let Expr::Literal(ScalarValue::Utf8(Some(field_name)), _) = &udf.args[1] {
parts.push(field_name.clone());
} else {
return None;
}
current_expr = &udf.args[0];
}
Expr::Column(col) => {
parts.push(col.name.clone());
break;
}
_ => {
return None;
}
}
}
parts.reverse();
let field_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
Some(lance_core::datatypes::format_field_path(&field_refs))
}
fn maybe_indexed_column<'b>(
expr: &Expr,
index_info: &'b dyn IndexInformationProvider,
) -> Option<(String, DataType, &'b dyn ScalarQueryParser)> {
if let Some(nested_path) = extract_nested_column_path(expr)
&& let Some((data_type, parser)) = index_info.get_index(&nested_path)
&& let Some(data_type) = parser.is_valid_reference(expr, data_type)
{
return Some((nested_path, data_type, parser));
}
match expr {
Expr::Column(col) => {
let col = col.name.as_str();
let (data_type, parser) = index_info.get_index(col)?;
if let Some(data_type) = parser.is_valid_reference(expr, data_type) {
Some((col.to_string(), data_type, parser))
} else {
None
}
}
Expr::ScalarFunction(udf) => {
if udf.args.is_empty() {
return None;
}
let col = maybe_column(&udf.args[0])?;
let (data_type, parser) = index_info.get_index(col)?;
if let Some(data_type) = parser.is_valid_reference(expr, data_type) {
Some((col.to_string(), data_type, parser))
} else {
None
}
}
_ => None,
}
}
fn maybe_scalar(expr: &Expr, expected_type: &DataType) -> Option<ScalarValue> {
match expr {
Expr::Literal(value, _) => safe_coerce_scalar(value, expected_type),
Expr::Cast(cast) => match cast.expr.as_ref() {
Expr::Literal(value, _) => {
let casted = value.cast_to(&cast.data_type).ok()?;
safe_coerce_scalar(&casted, expected_type)
}
_ => None,
},
Expr::ScalarFunction(scalar_function) => {
if scalar_function.name() == "arrow_cast" {
if scalar_function.args.len() != 2 {
return None;
}
match (&scalar_function.args[0], &scalar_function.args[1]) {
(Expr::Literal(value, _), Expr::Literal(cast_type, _)) => {
let target_type = scalar_function
.func
.return_field_from_args(ReturnFieldArgs {
arg_fields: &[
Arc::new(Field::new("expression", value.data_type(), false)),
Arc::new(Field::new("datatype", cast_type.data_type(), false)),
],
scalar_arguments: &[Some(value), Some(cast_type)],
})
.ok()?;
let casted = value.cast_to(target_type.data_type()).ok()?;
safe_coerce_scalar(&casted, expected_type)
}
_ => None,
}
} else {
None
}
}
_ => None,
}
}
fn maybe_scalar_list(exprs: &Vec<Expr>, expected_type: &DataType) -> Option<Vec<ScalarValue>> {
let mut scalar_values = Vec::with_capacity(exprs.len());
for expr in exprs {
match maybe_scalar(expr, expected_type) {
Some(scalar_val) => {
scalar_values.push(scalar_val);
}
None => {
return None;
}
}
}
Some(scalar_values)
}
fn visit_between(
between: &Between,
index_info: &dyn IndexInformationProvider,
) -> Option<IndexedExpression> {
let (column, col_type, query_parser) = maybe_indexed_column(&between.expr, index_info)?;
let low = maybe_scalar(&between.low, &col_type)?;
let high = maybe_scalar(&between.high, &col_type)?;
let indexed_expr =
query_parser.visit_between(&column, &Bound::Included(low), &Bound::Included(high))?;
if between.negated {
indexed_expr.maybe_not()
} else {
Some(indexed_expr)
}
}
fn visit_in_list(
in_list: &InList,
index_info: &dyn IndexInformationProvider,
) -> Option<IndexedExpression> {
let (column, col_type, query_parser) = maybe_indexed_column(&in_list.expr, index_info)?;
let values = maybe_scalar_list(&in_list.list, &col_type)?;
let indexed_expr = query_parser.visit_in_list(&column, &values)?;
if in_list.negated {
indexed_expr.maybe_not()
} else {
Some(indexed_expr)
}
}
fn visit_is_bool(
expr: &Expr,
index_info: &dyn IndexInformationProvider,
value: bool,
) -> Option<IndexedExpression> {
let (column, col_type, query_parser) = maybe_indexed_column(expr, index_info)?;
if col_type != DataType::Boolean {
None
} else {
query_parser.visit_is_bool(&column, value)
}
}
fn visit_column(
col: &Expr,
index_info: &dyn IndexInformationProvider,
) -> Option<IndexedExpression> {
let (column, col_type, query_parser) = maybe_indexed_column(col, index_info)?;
if col_type != DataType::Boolean {
None
} else {
query_parser.visit_is_bool(&column, true)
}
}
fn visit_is_null(
expr: &Expr,
index_info: &dyn IndexInformationProvider,
negated: bool,
) -> Option<IndexedExpression> {
let (column, _, query_parser) = maybe_indexed_column(expr, index_info)?;
let indexed_expr = query_parser.visit_is_null(&column)?;
if negated {
indexed_expr.maybe_not()
} else {
Some(indexed_expr)
}
}
fn visit_not(
expr: &Expr,
index_info: &dyn IndexInformationProvider,
depth: usize,
) -> Result<Option<IndexedExpression>> {
let node = visit_node(expr, index_info, depth + 1)?;
Ok(node.and_then(|node| node.maybe_not()))
}
fn visit_comparison(
expr: &BinaryExpr,
index_info: &dyn IndexInformationProvider,
) -> Option<IndexedExpression> {
let left_col = maybe_indexed_column(&expr.left, index_info);
if let Some((column, col_type, query_parser)) = left_col {
let scalar = maybe_scalar(&expr.right, &col_type)?;
query_parser.visit_comparison(&column, &scalar, &expr.op)
} else {
None
}
}
fn maybe_range(
expr: &BinaryExpr,
index_info: &dyn IndexInformationProvider,
) -> Option<IndexedExpression> {
let left_expr = match expr.left.as_ref() {
Expr::BinaryExpr(binary_expr) => Some(binary_expr),
_ => None,
}?;
let right_expr = match expr.right.as_ref() {
Expr::BinaryExpr(binary_expr) => Some(binary_expr),
_ => None,
}?;
let (left_col, dt, parser) = maybe_indexed_column(&left_expr.left, index_info)?;
let right_col = maybe_column(&right_expr.left)?;
if left_col != right_col {
return None;
}
let left_value = maybe_scalar(&left_expr.right, &dt)?;
let right_value = maybe_scalar(&right_expr.right, &dt)?;
let (low, high) = match (left_expr.op, right_expr.op) {
(Operator::GtEq, Operator::LtEq) => {
(Bound::Included(left_value), Bound::Included(right_value))
}
(Operator::GtEq, Operator::Lt) => {
(Bound::Included(left_value), Bound::Excluded(right_value))
}
(Operator::Gt, Operator::LtEq) => {
(Bound::Excluded(left_value), Bound::Included(right_value))
}
(Operator::Gt, Operator::Lt) => (Bound::Excluded(left_value), Bound::Excluded(right_value)),
(Operator::LtEq, Operator::GtEq) => {
(Bound::Included(right_value), Bound::Included(left_value))
}
(Operator::LtEq, Operator::Gt) => {
(Bound::Excluded(right_value), Bound::Included(left_value))
}
(Operator::Lt, Operator::GtEq) => {
(Bound::Included(right_value), Bound::Excluded(left_value))
}
(Operator::Lt, Operator::Gt) => (Bound::Excluded(right_value), Bound::Excluded(left_value)),
_ => return None,
};
parser.visit_between(&left_col, &low, &high)
}
fn visit_and(
expr: &BinaryExpr,
index_info: &dyn IndexInformationProvider,
depth: usize,
) -> Result<Option<IndexedExpression>> {
if let Some(range_expr) = maybe_range(expr, index_info) {
return Ok(Some(range_expr));
}
let left = visit_node(&expr.left, index_info, depth + 1)?;
let right = visit_node(&expr.right, index_info, depth + 1)?;
Ok(match (left, right) {
(Some(left), Some(right)) => Some(left.and(right)),
(Some(left), None) => Some(left.refine((*expr.right).clone())),
(None, Some(right)) => Some(right.refine((*expr.left).clone())),
(None, None) => None,
})
}
fn visit_or(
expr: &BinaryExpr,
index_info: &dyn IndexInformationProvider,
depth: usize,
) -> Result<Option<IndexedExpression>> {
let left = visit_node(&expr.left, index_info, depth + 1)?;
let right = visit_node(&expr.right, index_info, depth + 1)?;
Ok(match (left, right) {
(Some(left), Some(right)) => left.maybe_or(right),
(Some(_), None) => None,
(None, Some(_)) => None,
(None, None) => None,
})
}
fn visit_binary_expr(
expr: &BinaryExpr,
index_info: &dyn IndexInformationProvider,
depth: usize,
) -> Result<Option<IndexedExpression>> {
match &expr.op {
Operator::Lt | Operator::LtEq | Operator::Gt | Operator::GtEq | Operator::Eq => {
Ok(visit_comparison(expr, index_info))
}
Operator::NotEq => Ok(visit_comparison(expr, index_info).and_then(|node| node.maybe_not())),
Operator::And => visit_and(expr, index_info, depth),
Operator::Or => visit_or(expr, index_info, depth),
_ => Ok(None),
}
}
fn visit_scalar_fn(
scalar_fn: &ScalarFunction,
index_info: &dyn IndexInformationProvider,
) -> Option<IndexedExpression> {
if scalar_fn.args.is_empty() {
return None;
}
let (col, data_type, query_parser) = maybe_indexed_column(&scalar_fn.args[0], index_info)?;
query_parser.visit_scalar_function(&col, &data_type, &scalar_fn.func, &scalar_fn.args)
}
fn visit_like_expr(
like: &Like,
index_info: &dyn IndexInformationProvider,
) -> Option<IndexedExpression> {
let (column, _, query_parser) = maybe_indexed_column(&like.expr, index_info)?;
let pattern = match like.pattern.as_ref() {
Expr::Literal(scalar, _) => scalar.clone(),
_ => return None,
};
query_parser.visit_like(&column, like, &pattern)
}
fn visit_node(
expr: &Expr,
index_info: &dyn IndexInformationProvider,
depth: usize,
) -> Result<Option<IndexedExpression>> {
if depth >= MAX_DEPTH {
return Err(Error::invalid_input(format!(
"the filter expression is too long, lance limit the max number of conditions to {}",
MAX_DEPTH
)));
}
match expr {
Expr::Between(between) => Ok(visit_between(between, index_info)),
Expr::Alias(alias) => visit_node(alias.expr.as_ref(), index_info, depth),
Expr::Column(_) => Ok(visit_column(expr, index_info)),
Expr::InList(in_list) => Ok(visit_in_list(in_list, index_info)),
Expr::IsFalse(expr) => Ok(visit_is_bool(expr.as_ref(), index_info, false)),
Expr::IsTrue(expr) => Ok(visit_is_bool(expr.as_ref(), index_info, true)),
Expr::IsNull(expr) => Ok(visit_is_null(expr.as_ref(), index_info, false)),
Expr::IsNotNull(expr) => {
if let Expr::ScalarFunction(scalar_fn) = expr.as_ref()
&& scalar_fn.func.name() == "regexp_match"
{
return Ok(visit_scalar_fn(scalar_fn, index_info));
}
Ok(visit_is_null(expr.as_ref(), index_info, true))
}
Expr::Not(expr) => visit_not(expr.as_ref(), index_info, depth),
Expr::BinaryExpr(binary_expr) => visit_binary_expr(binary_expr, index_info, depth),
Expr::ScalarFunction(scalar_fn) => Ok(visit_scalar_fn(scalar_fn, index_info)),
Expr::Like(like) => {
if like.negated {
Ok(None)
} else {
Ok(visit_like_expr(like, index_info))
}
}
_ => Ok(None),
}
}
pub trait IndexInformationProvider {
fn get_index(&self, col: &str) -> Option<(&DataType, &dyn ScalarQueryParser)>;
fn fragment_bitmap(&self, _column: &str, _index_name: &str) -> Option<RoaringBitmap> {
None
}
}
pub fn apply_scalar_indices(
expr: Expr,
index_info: &dyn IndexInformationProvider,
) -> Result<IndexedExpression> {
let mut result =
visit_node(&expr, index_info, 0)?.unwrap_or(IndexedExpression::refine_only(expr));
if let Some(query) = result.scalar_query.as_mut() {
populate_fragment_bitmaps(query, index_info);
}
Ok(result)
}
fn populate_fragment_bitmaps(
expr: &mut ScalarIndexExpr,
index_info: &dyn IndexInformationProvider,
) {
match expr {
ScalarIndexExpr::Not(inner) => populate_fragment_bitmaps(inner, index_info),
ScalarIndexExpr::And(lhs, rhs) | ScalarIndexExpr::Or(lhs, rhs) => {
populate_fragment_bitmaps(lhs, index_info);
populate_fragment_bitmaps(rhs, index_info);
}
ScalarIndexExpr::Query(search) => {
search.fragment_bitmap = index_info.fragment_bitmap(&search.column, &search.index_name);
}
}
}
#[derive(Clone, Default, Debug)]
pub struct FilterPlan {
pub index_query: Option<ScalarIndexExpr>,
pub skip_recheck: bool,
pub refine_expr: Option<Expr>,
pub full_expr: Option<Expr>,
}
impl FilterPlan {
pub fn empty() -> Self {
Self {
index_query: None,
skip_recheck: true,
refine_expr: None,
full_expr: None,
}
}
pub fn new_refine_only(expr: Expr) -> Self {
Self {
index_query: None,
skip_recheck: true,
refine_expr: Some(expr.clone()),
full_expr: Some(expr),
}
}
pub fn is_empty(&self) -> bool {
self.refine_expr.is_none() && self.index_query.is_none()
}
pub fn all_columns(&self) -> Vec<String> {
self.full_expr
.as_ref()
.map(Planner::column_names_in_expr)
.unwrap_or_default()
}
pub fn refine_columns(&self) -> Vec<String> {
self.refine_expr
.as_ref()
.map(Planner::column_names_in_expr)
.unwrap_or_default()
}
pub fn has_refine(&self) -> bool {
self.refine_expr.is_some()
}
pub fn has_index_query(&self) -> bool {
self.index_query.is_some()
}
pub fn has_any_filter(&self) -> bool {
self.refine_expr.is_some() || self.index_query.is_some()
}
pub fn make_refine_only(&mut self) {
self.index_query = None;
self.refine_expr = self.full_expr.clone();
}
pub fn is_exact_index_search(&self) -> bool {
self.index_query.is_some() && self.refine_expr.is_none() && self.skip_recheck
}
}
pub trait PlannerIndexExt {
fn create_filter_plan(
&self,
filter: Expr,
index_info: &dyn IndexInformationProvider,
use_scalar_index: bool,
) -> Result<FilterPlan>;
}
impl PlannerIndexExt for Planner {
fn create_filter_plan(
&self,
filter: Expr,
index_info: &dyn IndexInformationProvider,
use_scalar_index: bool,
) -> Result<FilterPlan> {
let logical_expr = self.optimize_expr(filter)?;
if use_scalar_index {
let indexed_expr = apply_scalar_indices(logical_expr.clone(), index_info)?;
let mut skip_recheck = false;
if let Some(scalar_query) = indexed_expr.scalar_query.as_ref() {
skip_recheck = !scalar_query.needs_recheck();
}
Ok(FilterPlan {
index_query: indexed_expr.scalar_query,
refine_expr: indexed_expr.refine_expr,
full_expr: Some(logical_expr),
skip_recheck,
})
} else {
Ok(FilterPlan {
index_query: None,
skip_recheck: true,
refine_expr: Some(logical_expr.clone()),
full_expr: Some(logical_expr),
})
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use arrow_array::Array;
use arrow_schema::{Field, Schema};
use chrono::Utc;
use datafusion_common::{Column, DFSchema};
use datafusion_expr::simplify::SimplifyContext;
use lance_datafusion::exec::{LanceExecutionOptions, get_session_context};
use lance_select::result::IndexExprResultWireFormat;
use roaring::RoaringBitmap;
use crate::scalar::json::{JsonQuery, JsonQueryParser};
use super::*;
struct ColInfo {
data_type: DataType,
parser: Box<dyn ScalarQueryParser>,
}
impl ColInfo {
fn new(data_type: DataType, parser: Box<dyn ScalarQueryParser>) -> Self {
Self { data_type, parser }
}
}
struct MockIndexInfoProvider {
indexed_columns: HashMap<String, ColInfo>,
}
impl MockIndexInfoProvider {
fn new(indexed_columns: Vec<(&str, ColInfo)>) -> Self {
Self {
indexed_columns: HashMap::from_iter(
indexed_columns
.into_iter()
.map(|(s, ty)| (s.to_string(), ty)),
),
}
}
}
impl IndexInformationProvider for MockIndexInfoProvider {
fn get_index(&self, col: &str) -> Option<(&DataType, &dyn ScalarQueryParser)> {
self.indexed_columns
.get(col)
.map(|col_info| (&col_info.data_type, col_info.parser.as_ref()))
}
}
fn check(
index_info: &dyn IndexInformationProvider,
expr: &str,
expected: Option<IndexedExpression>,
optimize: bool,
) {
let schema = Schema::new(vec![
Field::new("color", DataType::Utf8, false),
Field::new("size", DataType::Float32, false),
Field::new("aisle", DataType::UInt32, false),
Field::new("on_sale", DataType::Boolean, false),
Field::new("price", DataType::Float32, false),
Field::new("json", DataType::LargeBinary, false),
]);
let df_schema: DFSchema = schema.try_into().unwrap();
let ctx = get_session_context(&LanceExecutionOptions::default());
let state = ctx.state();
let mut expr = state.create_logical_expr(expr, &df_schema).unwrap();
if optimize {
let simplify_context = SimplifyContext::default()
.with_schema(Arc::new(df_schema))
.with_query_execution_start_time(Some(Utc::now()));
let simplifier =
datafusion::optimizer::simplify_expressions::ExprSimplifier::new(simplify_context);
expr = simplifier.simplify(expr).unwrap();
}
let actual = apply_scalar_indices(expr.clone(), index_info).unwrap();
if let Some(expected) = expected {
assert_eq!(actual, expected);
} else {
assert!(actual.scalar_query.is_none());
assert_eq!(actual.refine_expr.unwrap(), expr);
}
}
fn check_no_index(index_info: &dyn IndexInformationProvider, expr: &str) {
check(index_info, expr, None, false)
}
fn check_simple(
index_info: &dyn IndexInformationProvider,
expr: &str,
col: &str,
query: impl AnyQuery,
) {
check(
index_info,
expr,
Some(IndexedExpression::index_query(
col.to_string(),
format!("{}_idx", col),
"BTree".to_string(),
Arc::new(query),
)),
false,
)
}
fn check_range(
index_info: &dyn IndexInformationProvider,
expr: &str,
col: &str,
query: SargableQuery,
) {
check(
index_info,
expr,
Some(IndexedExpression::index_query(
col.to_string(),
format!("{}_idx", col),
"BTree".to_string(),
Arc::new(query),
)),
true,
)
}
fn check_simple_negated(
index_info: &dyn IndexInformationProvider,
expr: &str,
col: &str,
query: SargableQuery,
) {
check(
index_info,
expr,
Some(
IndexedExpression::index_query(
col.to_string(),
format!("{}_idx", col),
"BTree".to_string(),
Arc::new(query),
)
.maybe_not()
.unwrap(),
),
false,
)
}
#[test]
fn test_expressions() {
let index_info = MockIndexInfoProvider::new(vec![
(
"color",
ColInfo::new(
DataType::Utf8,
Box::new(SargableQueryParser::new(
"color_idx".to_string(),
"BTree".to_string(),
false,
)),
),
),
(
"aisle",
ColInfo::new(
DataType::UInt32,
Box::new(SargableQueryParser::new(
"aisle_idx".to_string(),
"BTree".to_string(),
false,
)),
),
),
(
"on_sale",
ColInfo::new(
DataType::Boolean,
Box::new(SargableQueryParser::new(
"on_sale_idx".to_string(),
"BTree".to_string(),
false,
)),
),
),
(
"price",
ColInfo::new(
DataType::Float32,
Box::new(SargableQueryParser::new(
"price_idx".to_string(),
"BTree".to_string(),
false,
)),
),
),
(
"json",
ColInfo::new(
DataType::LargeBinary,
Box::new(JsonQueryParser::new(
"$.name".to_string(),
Box::new(SargableQueryParser::new(
"json_idx".to_string(),
"BTree".to_string(),
false,
)),
)),
),
),
]);
check_simple(
&index_info,
"json_extract(json, '$.name') = 'foo'",
"json",
JsonQuery::new(
Arc::new(SargableQuery::Equals(ScalarValue::Utf8(Some(
"foo".to_string(),
)))),
"$.name".to_string(),
),
);
check_no_index(&index_info, "size BETWEEN 5 AND 10");
check_simple(
&index_info,
"aisle = arrow_cast(5, 'Int16')",
"aisle",
SargableQuery::Equals(ScalarValue::UInt32(Some(5))),
);
check_range(
&index_info,
"aisle BETWEEN 5 AND 10",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(5))),
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_range(
&index_info,
"aisle >= 5 AND aisle <= 10",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(5))),
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_range(
&index_info,
"aisle <= 10 AND aisle >= 5",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(5))),
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_range(
&index_info,
"5 <= aisle AND 10 >= aisle",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(5))),
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_range(
&index_info,
"10 >= aisle AND 5 <= aisle",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(5))),
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_range(
&index_info,
"aisle <= 10 AND aisle > 5",
"aisle",
SargableQuery::Range(
Bound::Excluded(ScalarValue::UInt32(Some(5))),
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_range(
&index_info,
"aisle < 10 AND aisle >= 5",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(5))),
Bound::Excluded(ScalarValue::UInt32(Some(10))),
),
);
check_simple(
&index_info,
"on_sale IS TRUE",
"on_sale",
SargableQuery::Equals(ScalarValue::Boolean(Some(true))),
);
check_simple(
&index_info,
"on_sale",
"on_sale",
SargableQuery::Equals(ScalarValue::Boolean(Some(true))),
);
check_simple_negated(
&index_info,
"NOT on_sale",
"on_sale",
SargableQuery::Equals(ScalarValue::Boolean(Some(true))),
);
check_simple(
&index_info,
"on_sale IS FALSE",
"on_sale",
SargableQuery::Equals(ScalarValue::Boolean(Some(false))),
);
check_simple_negated(
&index_info,
"aisle NOT BETWEEN 5 AND 10",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(5))),
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_simple(
&index_info,
"aisle IN (5, 6, 7)",
"aisle",
SargableQuery::IsIn(vec![
ScalarValue::UInt32(Some(5)),
ScalarValue::UInt32(Some(6)),
ScalarValue::UInt32(Some(7)),
]),
);
check_simple_negated(
&index_info,
"NOT aisle IN (5, 6, 7)",
"aisle",
SargableQuery::IsIn(vec![
ScalarValue::UInt32(Some(5)),
ScalarValue::UInt32(Some(6)),
ScalarValue::UInt32(Some(7)),
]),
);
check_simple_negated(
&index_info,
"aisle NOT IN (5, 6, 7)",
"aisle",
SargableQuery::IsIn(vec![
ScalarValue::UInt32(Some(5)),
ScalarValue::UInt32(Some(6)),
ScalarValue::UInt32(Some(7)),
]),
);
check_simple(
&index_info,
"aisle IN (5, 6, 7, 8, 9)",
"aisle",
SargableQuery::IsIn(vec![
ScalarValue::UInt32(Some(5)),
ScalarValue::UInt32(Some(6)),
ScalarValue::UInt32(Some(7)),
ScalarValue::UInt32(Some(8)),
ScalarValue::UInt32(Some(9)),
]),
);
check_simple_negated(
&index_info,
"NOT aisle IN (5, 6, 7, 8, 9)",
"aisle",
SargableQuery::IsIn(vec![
ScalarValue::UInt32(Some(5)),
ScalarValue::UInt32(Some(6)),
ScalarValue::UInt32(Some(7)),
ScalarValue::UInt32(Some(8)),
ScalarValue::UInt32(Some(9)),
]),
);
check_simple_negated(
&index_info,
"aisle NOT IN (5, 6, 7, 8, 9)",
"aisle",
SargableQuery::IsIn(vec![
ScalarValue::UInt32(Some(5)),
ScalarValue::UInt32(Some(6)),
ScalarValue::UInt32(Some(7)),
ScalarValue::UInt32(Some(8)),
ScalarValue::UInt32(Some(9)),
]),
);
check_simple(
&index_info,
"on_sale is false",
"on_sale",
SargableQuery::Equals(ScalarValue::Boolean(Some(false))),
);
check_simple(
&index_info,
"on_sale is true",
"on_sale",
SargableQuery::Equals(ScalarValue::Boolean(Some(true))),
);
check_simple(
&index_info,
"aisle < 10",
"aisle",
SargableQuery::Range(
Bound::Unbounded,
Bound::Excluded(ScalarValue::UInt32(Some(10))),
),
);
check_simple(
&index_info,
"aisle <= 10",
"aisle",
SargableQuery::Range(
Bound::Unbounded,
Bound::Included(ScalarValue::UInt32(Some(10))),
),
);
check_simple(
&index_info,
"aisle > 10",
"aisle",
SargableQuery::Range(
Bound::Excluded(ScalarValue::UInt32(Some(10))),
Bound::Unbounded,
),
);
check_no_index(&index_info, "10 > aisle");
check_simple(
&index_info,
"aisle >= 10",
"aisle",
SargableQuery::Range(
Bound::Included(ScalarValue::UInt32(Some(10))),
Bound::Unbounded,
),
);
check_simple(
&index_info,
"aisle = 10",
"aisle",
SargableQuery::Equals(ScalarValue::UInt32(Some(10))),
);
check_simple_negated(
&index_info,
"aisle <> 10",
"aisle",
SargableQuery::Equals(ScalarValue::UInt32(Some(10))),
);
let left = Box::new(ScalarIndexExpr::Query(ScalarIndexSearch {
column: "aisle".to_string(),
index_name: "aisle_idx".to_string(),
index_type: "BTree".to_string(),
query: Arc::new(SargableQuery::Equals(ScalarValue::UInt32(Some(10)))),
needs_recheck: false,
fragment_bitmap: None,
}));
let right = Box::new(ScalarIndexExpr::Query(ScalarIndexSearch {
column: "color".to_string(),
index_name: "color_idx".to_string(),
index_type: "BTree".to_string(),
query: Arc::new(SargableQuery::Equals(ScalarValue::Utf8(Some(
"blue".to_string(),
)))),
needs_recheck: false,
fragment_bitmap: None,
}));
check(
&index_info,
"aisle = 10 AND color = 'blue'",
Some(IndexedExpression {
scalar_query: Some(ScalarIndexExpr::And(left.clone(), right.clone())),
refine_expr: None,
}),
false,
);
let refine = Expr::Column(Column::new_unqualified("size")).gt(datafusion_expr::lit(30_i64));
check(
&index_info,
"aisle = 10 AND color = 'blue' AND size > 30",
Some(IndexedExpression {
scalar_query: Some(ScalarIndexExpr::And(left.clone(), right.clone())),
refine_expr: Some(refine.clone()),
}),
false,
);
check(
&index_info,
"aisle = 10 OR color = 'blue'",
Some(IndexedExpression {
scalar_query: Some(ScalarIndexExpr::Or(left.clone(), right.clone())),
refine_expr: None,
}),
false,
);
check_no_index(&index_info, "aisle = 10 OR color = 'blue' OR size > 30");
check(
&index_info,
"(aisle = 10 OR color = 'blue') AND size > 30",
Some(IndexedExpression {
scalar_query: Some(ScalarIndexExpr::Or(left, right)),
refine_expr: Some(refine),
}),
false,
);
check_no_index(
&index_info,
"(aisle = 10 AND size > 30) OR (color = 'blue' AND size > 20)",
);
check_no_index(&index_info, "aisle + 3 < 10");
check_no_index(&index_info, "aisle IN (5, 6, NULL)");
check_no_index(&index_info, "aisle = 5 OR aisle = 6 OR NULL");
check_no_index(&index_info, "aisle IN (5, 6, 7, 8, NULL)");
check_no_index(&index_info, "aisle = NULL");
check_no_index(&index_info, "aisle BETWEEN 5 AND NULL");
check_no_index(&index_info, "aisle BETWEEN NULL AND 10");
}
#[tokio::test]
async fn test_not_flips_certainty() {
use lance_select::{NullableRowAddrSet, RowAddrTreeMap};
let at_most = NullableIndexExprResult::at_most(NullableRowAddrMask::AllowList(
NullableRowAddrSet::new(RowAddrTreeMap::from_iter(&[1, 2]), RowAddrTreeMap::new()),
));
assert!((!at_most).is_at_least());
let at_least = NullableIndexExprResult::at_least(NullableRowAddrMask::AllowList(
NullableRowAddrSet::new(RowAddrTreeMap::from_iter(&[1, 2]), RowAddrTreeMap::new()),
));
assert!((!at_least).is_at_most());
let exact = NullableIndexExprResult::exact(NullableRowAddrMask::AllowList(
NullableRowAddrSet::new(RowAddrTreeMap::from_iter(&[1, 2]), RowAddrTreeMap::new()),
));
assert!((!exact).is_exact());
}
#[tokio::test]
async fn test_and_or_preserve_certainty() {
use lance_select::{NullableRowAddrSet, RowAddrTreeMap};
let make_at_most = || {
NullableIndexExprResult::at_most(NullableRowAddrMask::AllowList(
NullableRowAddrSet::new(
RowAddrTreeMap::from_iter(&[1, 2, 3]),
RowAddrTreeMap::new(),
),
))
};
let make_at_least = || {
NullableIndexExprResult::at_least(NullableRowAddrMask::AllowList(
NullableRowAddrSet::new(
RowAddrTreeMap::from_iter(&[2, 3, 4]),
RowAddrTreeMap::new(),
),
))
};
let make_exact = || {
NullableIndexExprResult::exact(NullableRowAddrMask::AllowList(NullableRowAddrSet::new(
RowAddrTreeMap::from_iter(&[1, 2]),
RowAddrTreeMap::new(),
)))
};
assert!((make_at_most() & make_at_most()).is_at_most());
assert!((make_at_least() & make_at_least()).is_at_least());
assert!((make_at_most() & make_at_least()).is_at_most());
assert!((make_at_most() | make_at_most()).is_at_most());
assert!((make_at_least() | make_at_least()).is_at_least());
assert!((make_at_most() | make_at_least()).is_at_least());
assert!((make_exact() & make_at_most()).is_at_most());
assert!((make_exact() | make_at_least()).is_at_least());
}
#[tokio::test]
async fn test_refined_result_constructed_through_algebra() {
use lance_select::{NullableRowAddrSet, RowAddrTreeMap};
let allow_set = |rows: &[u64]| {
NullableRowAddrMask::AllowList(NullableRowAddrSet::new(
RowAddrTreeMap::from_iter(rows),
RowAddrTreeMap::new(),
))
};
let at_least_12 = NullableIndexExprResult::at_least(allow_set(&[1, 2]));
let exact_123 = NullableIndexExprResult::exact(allow_set(&[1, 2, 3]));
let refined = at_least_12 & exact_123;
assert!(
!refined.is_exact(),
"Refined must not be classified as Exact"
);
assert!(
!refined.is_at_most(),
"Refined must not be classified as AtMost"
);
assert!(
!refined.is_at_least(),
"Refined must not be classified as AtLeast"
);
assert_eq!(refined.lower, allow_set(&[1, 2]));
assert_eq!(refined.upper, allow_set(&[1, 2, 3]));
let negated = !refined;
assert!(!negated.is_exact());
assert!(!negated.is_at_most());
assert!(!negated.is_at_least());
assert!(matches!(negated.lower, NullableRowAddrMask::BlockList(_)));
assert!(matches!(negated.upper, NullableRowAddrMask::BlockList(_)));
}
#[test]
fn test_like_to_regex() {
assert_eq!(like_to_regex("%foo%", None).as_deref(), Some(".*foo.*"));
assert_eq!(like_to_regex("foo%bar", None).as_deref(), Some("foo.*bar"));
assert_eq!(like_to_regex("foo_bar", None).as_deref(), Some("foo.bar"));
assert_eq!(like_to_regex("foobar", None).as_deref(), Some("foobar"));
assert_eq!(
like_to_regex("%a.bcd%", None).as_deref(),
Some(".*a\\.bcd.*")
);
assert_eq!(like_to_regex("%ab%", None), None);
assert_eq!(like_to_regex("%a%b%c%", None), None);
assert_eq!(like_to_regex("%", None), None);
assert_eq!(
like_to_regex(r"%foo\%bar%", Some('\\')).as_deref(),
Some(".*foo%bar.*")
);
}
#[test]
fn test_apply_regex_flags() {
fn flags(s: &str) -> Expr {
Expr::Literal(ScalarValue::Utf8(Some(s.to_string())), None)
}
assert_eq!(apply_regex_flags("foo", &flags("")).as_deref(), Some("foo"));
assert_eq!(
apply_regex_flags("foo", &flags("i")).as_deref(),
Some("(?i)foo")
);
assert_eq!(
apply_regex_flags("foo", &flags("is")).as_deref(),
Some("(?is)foo")
);
assert_eq!(apply_regex_flags("foo", &flags("g")), None);
assert_eq!(
apply_regex_flags("foo", &Expr::Literal(ScalarValue::Int32(Some(1)), None)),
None
);
}
#[test]
fn test_extract_like_leading_prefix() {
assert_eq!(
extract_like_leading_prefix("foo%", None),
Some(("foo".to_string(), false))
);
assert_eq!(
extract_like_leading_prefix("abc%", None),
Some(("abc".to_string(), false))
);
assert_eq!(
extract_like_leading_prefix("foo%bar%", None),
Some(("foo".to_string(), true))
);
assert_eq!(
extract_like_leading_prefix("foo_bar%", None),
Some(("foo".to_string(), true))
);
assert_eq!(
extract_like_leading_prefix("foo%bar", None),
Some(("foo".to_string(), true))
);
assert_eq!(
extract_like_leading_prefix("foo_", None),
Some(("foo".to_string(), true))
);
assert_eq!(extract_like_leading_prefix("%foo", None), None);
assert_eq!(extract_like_leading_prefix("_foo%", None), None);
assert_eq!(extract_like_leading_prefix("%", None), None);
assert_eq!(extract_like_leading_prefix("foo", None), None);
assert_eq!(
extract_like_leading_prefix(r"foo\%bar%", Some('\\')),
Some(("foo%bar".to_string(), false))
);
assert_eq!(
extract_like_leading_prefix(r"foo\_bar%", Some('\\')),
Some(("foo_bar".to_string(), false))
);
assert_eq!(
extract_like_leading_prefix(r"foo\\bar%", Some('\\')),
Some(("foo\\bar".to_string(), false))
);
assert_eq!(extract_like_leading_prefix(r"foo\%", Some('\\')), None);
assert_eq!(extract_like_leading_prefix(r"foo\%", None), None);
assert_eq!(
extract_like_leading_prefix(r"foo\bar%", None),
Some(("foo\\bar".to_string(), false))
);
assert_eq!(extract_like_leading_prefix("", None), None);
assert_eq!(
extract_like_leading_prefix(r"foo\%bar%baz%", Some('\\')),
Some(("foo%bar".to_string(), true))
);
}
#[test]
fn test_like_expression_parsing() {
let index_info = MockIndexInfoProvider::new(vec![(
"color",
ColInfo::new(
DataType::Utf8,
Box::new(SargableQueryParser::new(
"color_idx".to_string(),
"BTree".to_string(),
false,
)),
),
)]);
let schema = Schema::new(vec![Field::new("color", DataType::Utf8, false)]);
let df_schema: DFSchema = schema.try_into().unwrap();
let ctx = get_session_context(&LanceExecutionOptions::default());
let state = ctx.state();
let expr = state
.create_logical_expr("color LIKE 'foo%'", &df_schema)
.unwrap();
let result = apply_scalar_indices(expr, &index_info).unwrap();
assert!(result.scalar_query.is_some(), "Should have scalar_query");
assert!(
result.refine_expr.is_none(),
"Simple prefix should not need refine_expr"
);
if let Some(ScalarIndexExpr::Query(search)) = &result.scalar_query {
let query = search.query.as_any().downcast_ref::<SargableQuery>();
assert!(query.is_some(), "Query should be SargableQuery");
match query.unwrap() {
SargableQuery::LikePrefix(prefix) => {
assert_eq!(prefix, &ScalarValue::Utf8(Some("foo".to_string())));
}
_ => panic!("Expected LikePrefix query"),
}
} else {
panic!("Expected Query variant");
}
let expr = state
.create_logical_expr("color LIKE 'foo%bar%'", &df_schema)
.unwrap();
let result = apply_scalar_indices(expr, &index_info).unwrap();
assert!(result.scalar_query.is_some(), "Should have scalar_query");
assert!(
result.refine_expr.is_some(),
"Complex pattern should have refine_expr"
);
if let Some(ScalarIndexExpr::Query(search)) = &result.scalar_query {
let query = search.query.as_any().downcast_ref::<SargableQuery>();
assert!(query.is_some(), "Query should be SargableQuery");
match query.unwrap() {
SargableQuery::LikePrefix(prefix) => {
assert_eq!(prefix, &ScalarValue::Utf8(Some("foo".to_string())));
}
_ => panic!("Expected LikePrefix query"),
}
}
let refine = result.refine_expr.unwrap();
match refine {
Expr::Like(like) => {
assert!(!like.negated);
assert!(!like.case_insensitive);
if let Expr::Literal(ScalarValue::Utf8(Some(pattern)), _) = like.pattern.as_ref() {
assert_eq!(pattern, "foo%bar%");
} else {
panic!("Expected Utf8 literal pattern");
}
}
_ => panic!("Expected Like expression in refine_expr"),
}
let expr = state
.create_logical_expr("color LIKE '%foo'", &df_schema)
.unwrap();
let result = apply_scalar_indices(expr, &index_info).unwrap();
assert!(
result.scalar_query.is_none(),
"Pattern starting with wildcard should not use index"
);
assert!(result.refine_expr.is_some(), "Should fall back to refine");
}
#[test]
fn test_starts_with_with_underscore_after_optimization() {
let index_info = MockIndexInfoProvider::new(vec![(
"object_id",
ColInfo::new(
DataType::Utf8,
Box::new(SargableQueryParser::new(
"object_id_idx".to_string(),
"BTree".to_string(),
false,
)),
),
)]);
let schema = Schema::new(vec![Field::new("object_id", DataType::Utf8, false)]);
let df_schema: DFSchema = schema.try_into().unwrap();
let ctx = get_session_context(&LanceExecutionOptions::default());
let state = ctx.state();
let expr = state
.create_logical_expr("starts_with(object_id, 'test_ns$')", &df_schema)
.unwrap();
let simplify_context = SimplifyContext::default()
.with_schema(Arc::new(df_schema))
.with_query_execution_start_time(Some(Utc::now()));
let simplifier =
datafusion::optimizer::simplify_expressions::ExprSimplifier::new(simplify_context);
let simplified_expr = simplifier.simplify(expr).unwrap();
let result = apply_scalar_indices(simplified_expr, &index_info).unwrap();
if let Some(ScalarIndexExpr::Query(search)) = &result.scalar_query {
let query = search
.query
.as_any()
.downcast_ref::<SargableQuery>()
.unwrap();
match query {
SargableQuery::LikePrefix(prefix) => {
let prefix_str = match prefix {
ScalarValue::Utf8(Some(s)) => s.clone(),
_ => panic!("Expected Utf8 prefix"),
};
assert_eq!(
prefix_str, "test_ns$",
"Prefix should be 'test_ns$', not 'test' (underscore should not be a wildcard)"
);
}
_ => panic!("Expected LikePrefix query"),
}
} else {
panic!("Expected scalar_query to be present");
}
}
#[test]
fn test_starts_with_to_like_conversion() {
let index_info = MockIndexInfoProvider::new(vec![(
"color",
ColInfo::new(
DataType::Utf8,
Box::new(SargableQueryParser::new(
"color_idx".to_string(),
"BTree".to_string(),
false,
)),
),
)]);
let schema = Schema::new(vec![Field::new("color", DataType::Utf8, false)]);
let df_schema: DFSchema = schema.try_into().unwrap();
let ctx = get_session_context(&LanceExecutionOptions::default());
let state = ctx.state();
let expr = state
.create_logical_expr("starts_with(color, 'foo')", &df_schema)
.unwrap();
let result = apply_scalar_indices(expr, &index_info).unwrap();
assert!(
result.scalar_query.is_some(),
"starts_with should use index"
);
assert!(
result.refine_expr.is_none(),
"Pure prefix starts_with should not need refine_expr"
);
if let Some(ScalarIndexExpr::Query(search)) = &result.scalar_query {
let query = search.query.as_any().downcast_ref::<SargableQuery>();
assert!(query.is_some(), "Query should be SargableQuery");
match query.unwrap() {
SargableQuery::LikePrefix(prefix) => {
assert_eq!(prefix, &ScalarValue::Utf8(Some("foo".to_string())));
}
_ => panic!("Expected LikePrefix query"),
}
} else {
panic!("Expected Query variant");
}
let like_expr = state
.create_logical_expr("color LIKE 'foo%'", &df_schema)
.unwrap();
let like_result = apply_scalar_indices(like_expr, &index_info).unwrap();
if let (
Some(ScalarIndexExpr::Query(starts_with_search)),
Some(ScalarIndexExpr::Query(like_search)),
) = (&result.scalar_query, &like_result.scalar_query)
{
let sw_query = starts_with_search
.query
.as_any()
.downcast_ref::<SargableQuery>()
.unwrap();
let like_query = like_search
.query
.as_any()
.downcast_ref::<SargableQuery>()
.unwrap();
assert_eq!(
sw_query, like_query,
"starts_with and LIKE 'prefix%' should produce identical queries"
);
}
}
#[test]
fn test_serialize_index_expr_result_round_trip() {
use lance_select::{RowAddrMask, RowAddrTreeMap};
for format in [
IndexExprResultWireFormat::TwoMask,
IndexExprResultWireFormat::ThreeVariant,
] {
let mut addrs = RowAddrTreeMap::new();
addrs.insert_range(0..5);
addrs.insert_range(100..103);
let mut fragments_covered = RoaringBitmap::new();
fragments_covered.insert(0);
fragments_covered.insert(7);
let cases = [
(
"exact",
IndexExprResult::exact(RowAddrMask::from_allowed(addrs.clone())),
),
(
"at_most",
IndexExprResult::at_most(RowAddrMask::from_allowed(addrs.clone())),
),
(
"at_least",
IndexExprResult::at_least(RowAddrMask::from_allowed(addrs)),
),
];
for (label, original) in cases {
let batch = original.serialize(&fragments_covered, format).unwrap();
assert_eq!(
batch.schema(),
*format.schema(),
"format {format:?}, case {label}"
);
assert_eq!(batch.num_rows(), 2, "format {format:?}, case {label}");
let (round_tripped, round_tripped_frags) =
IndexExprResult::deserialize(&batch).unwrap();
assert_eq!(
round_tripped.lower, original.lower,
"format {format:?}, case {label}: lower"
);
assert_eq!(
round_tripped.upper, original.upper,
"format {format:?}, case {label}: upper"
);
assert_eq!(
round_tripped_frags, fragments_covered,
"format {format:?}, case {label}: frags"
);
assert_eq!(
round_tripped.is_exact(),
original.is_exact(),
"format {format:?}, case {label}"
);
assert_eq!(
round_tripped.is_at_most(),
original.is_at_most(),
"format {format:?}, case {label}"
);
assert_eq!(
round_tripped.is_at_least(),
original.is_at_least(),
"format {format:?}, case {label}"
);
}
}
}
#[test]
fn test_serialize_omits_upper_when_exact() {
use lance_select::{RowAddrMask, RowAddrTreeMap};
let mask = RowAddrMask::from_allowed(RowAddrTreeMap::from_iter(0u64..5));
let fragments_covered = RoaringBitmap::from_iter([0u32]);
use arrow::array::AsArray;
let exact_batch = IndexExprResult::exact(mask.clone())
.serialize(&fragments_covered, IndexExprResultWireFormat::TwoMask)
.unwrap();
let exact_upper = exact_batch.column(1).as_binary::<i32>();
assert!(exact_upper.is_null(0) && exact_upper.is_null(1));
let at_most_batch = IndexExprResult::at_most(mask.clone())
.serialize(&fragments_covered, IndexExprResultWireFormat::TwoMask)
.unwrap();
let at_most_upper = at_most_batch.column(1).as_binary::<i32>();
assert!(!(at_most_upper.is_null(0) && at_most_upper.is_null(1)));
let at_least_batch = IndexExprResult::at_least(mask)
.serialize(&fragments_covered, IndexExprResultWireFormat::TwoMask)
.unwrap();
let at_least_upper = at_least_batch.column(1).as_binary::<i32>();
assert!(!at_least_upper.is_null(0));
let (round_tripped, _) = IndexExprResult::deserialize(&at_least_batch).unwrap();
assert!(round_tripped.is_at_least());
assert!(!round_tripped.is_exact());
}
#[test]
fn test_three_variant_serialize_refined_degrades_to_at_most() {
use lance_select::{RowAddrMask, RowAddrTreeMap};
let lower_addrs = RowAddrTreeMap::from_iter(0u64..3);
let upper_addrs = RowAddrTreeMap::from_iter(0u64..10);
let refined = IndexExprResult::new(
RowAddrMask::from_allowed(lower_addrs),
RowAddrMask::from_allowed(upper_addrs.clone()),
);
assert!(!refined.is_exact() && !refined.is_at_most() && !refined.is_at_least());
let fragments_covered = RoaringBitmap::from_iter([0u32, 1]);
let batch = refined
.serialize(&fragments_covered, IndexExprResultWireFormat::ThreeVariant)
.unwrap();
assert_eq!(
batch.schema(),
*IndexExprResultWireFormat::ThreeVariant.schema()
);
let (round_tripped, round_tripped_frags) = IndexExprResult::deserialize(&batch).unwrap();
assert!(round_tripped.is_at_most());
assert_eq!(round_tripped.upper, RowAddrMask::from_allowed(upper_addrs));
assert_eq!(round_tripped_frags, fragments_covered);
}
}