use super::post_process::post_process;
use super::query_tail::QueryTail;
use crate::error::{Result, SqlError};
use crate::types::SqlPlan;
use crate::types::{
ArraySlicePlan, CtePlan, DocumentIndexLookupPlan, HybridSearchPlan, HybridSearchTriplePlan,
TextSearchPlan, TextSearchShape, TimeseriesScanPlan,
};
const DEFAULT_EF_SEARCH_MULTIPLIER: usize = 2;
pub(in crate::planner::select) fn apply_limit(
mut plan: SqlPlan,
tail: &QueryTail<'_>,
) -> Result<SqlPlan> {
let (limit_val, offset_val) = tail.limit_offset()?;
if let SqlPlan::Cte(CtePlan { definitions, outer }) = plan {
return Ok(SqlPlan::Cte(CtePlan {
definitions,
outer: Box::new(apply_limit(*outer, tail)?),
}));
}
let absorbs_offset = matches!(
plan,
SqlPlan::Scan { .. } | SqlPlan::DocumentIndexLookup { .. } | SqlPlan::Subquery { .. }
);
if offset_val != 0 && !absorbs_offset {
return post_process(plan, Vec::new(), limit_val, offset_val);
}
if matches!(
plan,
SqlPlan::ConstantResult { .. } | SqlPlan::PointGet { .. }
) {
if limit_val == Some(0) {
return post_process(plan, Vec::new(), Some(0), 0);
}
return Ok(plan);
}
match plan {
SqlPlan::Scan {
ref mut limit,
ref mut offset,
..
} => {
*limit = limit_val;
*offset = offset_val;
}
SqlPlan::DocumentIndexLookup(DocumentIndexLookupPlan {
ref mut limit,
ref mut offset,
..
}) => {
*limit = limit_val;
*offset = offset_val;
}
SqlPlan::Subquery {
ref mut limit,
ref mut offset,
..
} => {
*limit = limit_val;
*offset = offset_val;
}
SqlPlan::TimeseriesScan(TimeseriesScanPlan {
limit: ref mut l, ..
}) => {
if let Some(lv) = limit_val {
*l = lv;
}
}
SqlPlan::SpatialScan {
limit: ref mut l, ..
} => {
if let Some(lv) = limit_val {
*l = lv;
}
}
SqlPlan::ArraySlice(ArraySlicePlan {
limit: ref mut l, ..
}) => {
if let Some(lv) = limit_val {
*l = u32::try_from(lv).map_err(|_| SqlError::Unsupported {
detail: format!("LIMIT {lv} exceeds the array slice row bound"),
})?;
}
}
SqlPlan::TextSearch(TextSearchPlan {
shape: TextSearchShape::Match {
top_k: ref mut k, ..
},
..
}) => {
*k = limit_val;
}
SqlPlan::TextSearch(TextSearchPlan {
shape: TextSearchShape::ScoreScan,
..
}) => {
if limit_val.is_some() {
return post_process(plan, Vec::new(), limit_val, 0);
}
}
SqlPlan::MultiVectorSearch {
top_k: ref mut k, ..
}
| SqlPlan::HybridSearch(HybridSearchPlan {
top_k: ref mut k, ..
})
| SqlPlan::HybridSearchTriple(HybridSearchTriplePlan {
top_k: ref mut k, ..
}) => {
if let Some(lv) = limit_val {
*k = lv;
}
}
SqlPlan::Aggregate {
limit: ref mut l, ..
} => {
if let Some(lv) = limit_val {
*l = lv;
}
}
SqlPlan::VectorSearch {
top_k: ref mut k,
ef_search: ref mut ef,
ann_options: ref opts,
..
} => {
if let Some(lv) = limit_val {
*k = lv;
*ef = opts
.ef_search_override
.unwrap_or(lv * DEFAULT_EF_SEARCH_MULTIPLIER);
}
}
SqlPlan::Join {
limit: ref mut l, ..
} => {
*l = limit_val;
}
SqlPlan::SparseSearch {
top_k: ref mut k, ..
} => {
if let Some(lv) = limit_val {
*k = lv;
}
}
ref other => {
if limit_val.is_some() {
return post_process(other.clone(), Vec::new(), limit_val, offset_val);
}
}
}
Ok(plan)
}
#[cfg(test)]
mod tests {
use sqlparser::ast;
use super::*;
use crate::temporal::TemporalScope;
use crate::types::LateralLoopPlan;
use crate::types::query::{EngineType, JoinType};
fn minimal_scan() -> SqlPlan {
SqlPlan::Scan {
collection: "t".into(),
alias: None,
engine: EngineType::DocumentSchemaless,
filters: vec![],
projection: vec![],
sort_keys: vec![],
limit: None,
offset: 0,
distinct: false,
window_functions: vec![],
temporal: TemporalScope::default(),
}
}
fn join_plan_with_limit(limit: Option<usize>) -> SqlPlan {
SqlPlan::Join {
left: Box::new(minimal_scan()),
right: Box::new(minimal_scan()),
on: vec![],
join_type: JoinType::Inner,
condition: None,
limit,
projection: vec![],
filters: vec![],
}
}
fn limit_clause(n: usize) -> Option<ast::LimitClause> {
Some(ast::LimitClause::LimitOffset {
limit: Some(ast::Expr::Value(
ast::Value::Number(n.to_string(), false).into(),
)),
offset: None,
limit_by: vec![],
})
}
fn tail(limit_clause: &Option<ast::LimitClause>) -> QueryTail<'_> {
QueryTail {
order_by: None,
limit_clause,
fetch: None,
}
}
#[test]
fn apply_limit_sets_join_limit() {
let plan = join_plan_with_limit(None);
let clause = limit_clause(5);
let result = apply_limit(plan, &tail(&clause)).expect("join accepts a LIMIT");
match result {
SqlPlan::Join { limit, .. } => assert_eq!(limit, Some(5)),
other => panic!("expected SqlPlan::Join, got {other:?}"),
}
}
#[test]
fn apply_limit_none_leaves_join_limit_none() {
let plan = join_plan_with_limit(None);
let result = apply_limit(plan, &tail(&None)).expect("no LIMIT is a no-op");
match result {
SqlPlan::Join { limit, .. } => assert_eq!(limit, None),
other => panic!("expected SqlPlan::Join, got {other:?}"),
}
}
fn lateral_loop_plan() -> SqlPlan {
SqlPlan::LateralLoop(LateralLoopPlan {
outer: Box::new(minimal_scan()),
outer_alias: None,
inner: Box::new(minimal_scan()),
correlation_predicates: vec![],
lateral_alias: "x".into(),
projection: vec![],
outer_row_cap: 10,
left_join: false,
})
}
#[test]
fn apply_limit_post_processes_a_plan_that_cannot_hold_it() {
let clause = limit_clause(5);
let result = apply_limit(lateral_loop_plan(), &tail(&clause)).expect("LIMIT is applied");
match result {
SqlPlan::Subquery { limit, offset, .. } => {
assert_eq!(limit, Some(5));
assert_eq!(offset, 0);
}
other => panic!("expected SqlPlan::Subquery, got {other:?}"),
}
}
#[test]
fn apply_limit_post_processes_offset_a_plan_cannot_hold() {
let clause = Some(ast::LimitClause::LimitOffset {
limit: Some(ast::Expr::Value(
ast::Value::Number("5".into(), false).into(),
)),
offset: Some(ast::Offset {
value: ast::Expr::Value(ast::Value::Number("3".into(), false).into()),
rows: ast::OffsetRows::None,
}),
limit_by: vec![],
});
let result = apply_limit(join_plan_with_limit(None), &tail(&clause))
.expect("OFFSET is applied in the tail");
match result {
SqlPlan::Subquery {
input,
limit,
offset,
..
} => {
assert_eq!(limit, Some(5));
assert_eq!(offset, 3);
assert!(
matches!(*input, SqlPlan::Join { limit: None, .. }),
"the body keeps no row bound of its own"
);
}
other => panic!("expected SqlPlan::Subquery, got {other:?}"),
}
}
}