use super::fields::{derive_field_name, idiom_to_field_name};
use crate::err::Error;
use crate::expr::field::{Field, Fields};
use crate::expr::{Expr, Literal};
pub(crate) fn order_is_scan_compatible(order: Option<&crate::expr::order::Ordering>) -> bool {
use crate::expr::order::Ordering;
match order {
None => true,
Some(Ordering::Random) => false,
Some(Ordering::Order(list)) => {
list.0.len() == 1 && list.0[0].value.is_id() && !list.0[0].collate && !list.0[0].numeric
}
}
}
pub(crate) fn index_covers_ordering(
index_ref: &crate::exec::index::access_path::IndexRef,
access: &crate::exec::index::access_path::BTreeAccess,
direction: crate::idx::planner::ScanDirection,
order: &crate::expr::order::Ordering,
) -> bool {
use crate::exec::index::access_path::BTreeAccess;
use crate::exec::operators::SortDirection;
use crate::exec::ordering::{OutputOrdering, SortProperty};
use crate::expr::order::Ordering;
let Ordering::Order(order_list) = order else {
return false; };
let required: Vec<SortProperty> = order_list
.iter()
.filter_map(|field| {
crate::exec::field_path::FieldPath::try_from(&field.value).ok().map(|path| {
let direction = if field.direction {
SortDirection::Asc
} else {
SortDirection::Desc
};
SortProperty {
path,
direction,
collate: field.collate,
numeric: field.numeric,
}
})
})
.collect();
if required.len() != order_list.len() {
return false;
}
let ix_def = index_ref.definition();
let (skip_cols, equality_field_paths) = match access {
BTreeAccess::Compound {
prefix,
..
} => {
let paths: Vec<_> = ix_def
.cols
.iter()
.take(prefix.len())
.filter_map(|idiom| crate::exec::field_path::FieldPath::try_from(idiom).ok())
.collect();
(prefix.len(), paths)
}
BTreeAccess::Equality(_) => {
let paths: Vec<_> = ix_def
.cols
.iter()
.filter_map(|idiom| crate::exec::field_path::FieldPath::try_from(idiom).ok())
.collect();
(ix_def.cols.len(), paths)
}
_ => (0, vec![]),
};
let required: Vec<SortProperty> =
required.into_iter().skip_while(|prop| equality_field_paths.contains(&prop.path)).collect();
let dir = match direction {
crate::idx::planner::ScanDirection::Forward => SortDirection::Asc,
crate::idx::planner::ScanDirection::Backward => SortDirection::Desc,
};
let mut cols: Vec<SortProperty> = ix_def
.cols
.iter()
.skip(skip_cols)
.filter_map(|idiom| {
crate::exec::field_path::FieldPath::try_from(idiom).ok().map(|path| SortProperty {
path,
direction: dir,
collate: false,
numeric: false,
})
})
.collect();
if !index_ref.is_unique() && !ix_def.cols.is_empty() {
cols.push(SortProperty {
path: crate::exec::field_path::FieldPath::field("id"),
direction: dir,
collate: false,
numeric: false,
});
}
if required.is_empty() {
return true;
}
if cols.is_empty() {
return false;
}
OutputOrdering::Sorted(cols).satisfies(&required)
}
pub(crate) fn get_effective_limit_literal(
start: &Option<crate::expr::start::Start>,
limit: &Option<crate::expr::limit::Limit>,
) -> Option<usize> {
let limit_val = limit_expr_as_usize(limit.as_ref().map(|l| &l.0))?;
let start_val = start.as_ref().map(|s| limit_expr_as_usize(Some(&s.0))).unwrap_or(Some(0))?;
start_val.checked_add(limit_val)
}
fn limit_expr_as_usize(expr: Option<&Expr>) -> Option<usize> {
match expr? {
Expr::Literal(Literal::Integer(n)) if *n >= 0 => Some(*n as usize),
Expr::Literal(Literal::Float(n)) if *n >= 0.0 => Some(*n as usize),
_ => None,
}
}
pub(crate) fn is_bounded_topk_downstream(
order: Option<&crate::expr::order::Ordering>,
start: &Option<crate::expr::start::Start>,
limit: &Option<crate::expr::limit::Limit>,
tempfiles: bool,
threshold: usize,
) -> bool {
use crate::expr::order::Ordering;
if tempfiles {
return false;
}
match order {
Some(Ordering::Order(_)) => match get_effective_limit_literal(start, limit) {
Some(n) => n <= threshold,
None => false,
},
_ => false,
}
}
pub(crate) async fn extract_version(
version_expr: Expr,
planner: &super::super::Planner<'_>,
) -> Result<Option<std::sync::Arc<dyn crate::exec::PhysicalExpr>>, Error> {
match version_expr {
Expr::Literal(Literal::None) => Ok(None),
_ => {
let expr = planner.physical_expr(version_expr).await?;
Ok(Some(expr))
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn is_indexed_count_eligible(
fields: &Fields,
group: &Option<crate::expr::group::Groups>,
cond: &Option<crate::expr::cond::Cond>,
split: &Option<crate::expr::split::Splits>,
order: &Option<crate::expr::order::Ordering>,
fetch: &Option<crate::expr::fetch::Fetchs>,
omit: &[Expr],
what: &[Expr],
) -> bool {
if !fields.is_count_all_only() {
return false;
}
let Some(groups) = group else {
return false;
};
if !groups.is_group_all_only() {
return false;
}
if cond.is_none() {
return false;
}
if split.is_some() || order.is_some() || fetch.is_some() || !omit.is_empty() {
return false;
}
if what.len() != 1 {
return false;
}
matches!(&what[0], Expr::Table(_) | Expr::Param(_))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn is_count_all_eligible(
fields: &Fields,
group: &Option<crate::expr::group::Groups>,
cond: &Option<crate::expr::cond::Cond>,
split: &Option<crate::expr::split::Splits>,
order: &Option<crate::expr::order::Ordering>,
fetch: &Option<crate::expr::fetch::Fetchs>,
omit: &[Expr],
what: &[Expr],
) -> bool {
if !fields.is_count_all_only() {
return false;
}
let Some(groups) = group else {
return false;
};
if !groups.is_group_all_only() {
return false;
}
if cond.is_some() {
return false;
}
if split.is_some() || order.is_some() || fetch.is_some() || !omit.is_empty() {
return false;
}
if what.len() != 1 {
return false;
}
matches!(
&what[0],
Expr::Table(_)
| Expr::Literal(crate::expr::literal::Literal::RecordId(_))
| Expr::Param(_)
| Expr::Postfix { .. }
)
}
pub(crate) fn extract_count_field_names(fields: &Fields) -> Vec<String> {
match fields {
Fields::Value(selector) => {
if let Some(alias) = &selector.alias {
vec![idiom_to_field_name(alias)]
} else {
vec![derive_field_name(&selector.expr)]
}
}
Fields::Select(field_list) => field_list
.iter()
.filter_map(|f| match f {
Field::Single(selector) => {
if let Some(alias) = &selector.alias {
Some(idiom_to_field_name(alias))
} else {
Some(derive_field_name(&selector.expr))
}
}
_ => None,
})
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::limit::Limit;
use crate::expr::order::{Order, OrderList, Ordering};
use crate::expr::start::Start;
use crate::expr::{Idiom, Part};
fn int_lit(n: i64) -> Expr {
Expr::Literal(Literal::Integer(n))
}
fn order_by_created_at_desc() -> Ordering {
Ordering::Order(OrderList(vec![Order {
value: Idiom(vec![Part::Field(crate::val::Strand::new("created_at"))]),
collate: false,
numeric: false,
direction: false,
}]))
}
#[test]
fn effective_limit_handles_literals_and_rejects_params() {
assert_eq!(get_effective_limit_literal(&None, &Some(Limit(int_lit(100)))), Some(100));
assert_eq!(
get_effective_limit_literal(&Some(Start(int_lit(20))), &Some(Limit(int_lit(100)))),
Some(120)
);
assert_eq!(get_effective_limit_literal(&None, &Some(Limit(int_lit(-1)))), None);
let param = Expr::Param(crate::expr::param::Param::default());
assert_eq!(get_effective_limit_literal(&None, &Some(Limit(param))), None);
}
#[test]
fn topk_downstream_predicate_matches_design_intent() {
let order = order_by_created_at_desc();
assert!(is_bounded_topk_downstream(
Some(&order),
&None,
&Some(Limit(int_lit(1000))),
false,
1000,
));
assert!(is_bounded_topk_downstream(
Some(&order),
&Some(Start(int_lit(500))),
&Some(Limit(int_lit(500))),
false,
1000,
));
assert!(!is_bounded_topk_downstream(
Some(&order),
&None,
&Some(Limit(int_lit(2000))),
false,
1000,
));
assert!(!is_bounded_topk_downstream(None, &None, &Some(Limit(int_lit(10))), false, 1000));
assert!(!is_bounded_topk_downstream(
Some(&Ordering::Random),
&None,
&Some(Limit(int_lit(10))),
false,
1000,
));
assert!(!is_bounded_topk_downstream(Some(&order), &None, &None, false, 1000));
assert!(!is_bounded_topk_downstream(
Some(&order),
&None,
&Some(Limit(int_lit(10))),
true,
1000,
));
}
}