Skip to main content

radixdb_executor/pipeline/
paging.rs

1//! LIMIT/OFFSET admission and application.
2
3use radixdb_core::{Error, Result, Row, Value};
4use radixdb_sql::ast::{Expression, SelectStatement};
5use radixdb_storage::traits::QueryResult;
6
7use crate::context::ExecutionContext;
8use crate::expression::ExpressionEval;
9use crate::result::LimitedResult;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct PageWindow {
13    pub limit: Option<usize>,
14    pub offset: usize,
15}
16
17impl PageWindow {
18    pub fn evaluate(stmt: &SelectStatement, ctx: &ExecutionContext) -> Result<Self> {
19        Ok(Self {
20            limit: stmt
21                .limit
22                .as_deref()
23                .map(|expr| evaluate_page_expression(expr, ctx, "LIMIT"))
24                .transpose()?,
25            offset: stmt
26                .offset
27                .as_deref()
28                .map(|expr| evaluate_page_expression(expr, ctx, "OFFSET"))
29                .transpose()?
30                .unwrap_or(0),
31        })
32    }
33
34    pub fn retained_top_n(self) -> Option<usize> {
35        self.limit.map(|limit| limit.saturating_add(self.offset))
36    }
37
38    pub fn apply(
39        self,
40        result: Box<dyn QueryResult>,
41        already_applied: bool,
42    ) -> Box<dyn QueryResult> {
43        if !already_applied && (self.limit.is_some() || self.offset > 0) {
44            Box::new(LimitedResult::new(result, self.limit, self.offset))
45        } else {
46            result
47        }
48    }
49}
50
51pub fn evaluate_page_expression(
52    expression: &Expression,
53    ctx: &ExecutionContext,
54    clause: &str,
55) -> Result<usize> {
56    let value = ExpressionEval::compile(expression, &[])?
57        .with_context(ctx)
58        .eval_slice(&Row::new())?;
59    let Value::Integer(value) = value else {
60        return Err(Error::Parse(format!(
61            "{clause} must be an integer, got {value:?}"
62        )));
63    };
64    if value < 0 {
65        return Err(Error::Parse(format!(
66            "{clause} must be non-negative, got {value}"
67        )));
68    }
69    usize::try_from(value).map_err(|_| {
70        Error::Parse(format!(
71            "{clause} value {value} is too large for this platform"
72        ))
73    })
74}
75
76pub fn validate_order_ordinals(stmt: &SelectStatement, output_width: usize) -> Result<()> {
77    for order_by in &stmt.order_by {
78        if let Expression::IntegerLiteral(literal) = &order_by.expression {
79            if literal.value < 1 || literal.value as u128 > output_width as u128 {
80                return Err(Error::InvalidArgument(format!(
81                    "ORDER BY position {} is outside the SELECT list of {} columns",
82                    literal.value, output_width
83                )));
84            }
85        }
86    }
87    Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use radixdb_sql::parse_sql;
94
95    fn select(sql: &str) -> SelectStatement {
96        let mut statements = parse_sql(sql).unwrap();
97        match statements.remove(0) {
98            radixdb_sql::ast::Statement::Select(select) => select,
99            _ => panic!("expected SELECT"),
100        }
101    }
102
103    #[test]
104    fn order_ordinal_is_checked_against_public_shape() {
105        assert!(validate_order_ordinals(&select("SELECT a ORDER BY 1"), 1).is_ok());
106        assert!(validate_order_ordinals(&select("SELECT a ORDER BY 2"), 1).is_err());
107    }
108}