radixdb_executor/pipeline/
paging.rs1use 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}