Skip to main content

keelson_core/clause/
offset.rs

1use crate::expr::{Expr, IntoExpr};
2use crate::writer::{Expression, SqlWriter};
3
4/// The `ROW`/`ROWS` synonym pair, spelled the way the human reading the SQL
5/// expects.
6///
7/// The two spellings mean the same thing everywhere the grammar offers them —
8/// `OFFSET n { ROW | ROWS }` and `FETCH { FIRST | NEXT } n { ROW | ROWS }` — but
9/// unlike a grammar *default* such as `SELECT ALL`, writing one is not writing
10/// nothing: the keyword shows up whenever generated SQL is read or diffed against
11/// a hand-written query, so both spellings are representable.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum RowsKeyword {
14    /// `ROW`, the singular.
15    Row,
16    /// `ROWS`.
17    #[default]
18    Rows,
19}
20
21impl RowsKeyword {
22    /// The keyword itself.
23    pub fn as_str(self) -> &'static str {
24        match self {
25            RowsKeyword::Row => "ROW",
26            RowsKeyword::Rows => "ROWS",
27        }
28    }
29}
30
31/// `OFFSET n [ ROW | ROWS ]`
32///
33/// Like [`Limit`](super::Limit) the count is an expression: SQLite accepts one, and
34/// every dialect accepts a bound argument.
35///
36/// The trailing keyword is the SQL-standard spelling and PostgreSQL accepts it;
37/// `None` writes the bare historical `OFFSET n`, which every dialect takes.
38#[derive(Debug, Clone, Default)]
39pub struct Offset {
40    /// How many rows to skip.
41    pub count: Option<Expr>,
42    /// The optional trailing `ROW`/`ROWS`. `None` writes neither.
43    pub rows: Option<RowsKeyword>,
44}
45
46impl Offset {
47    /// Set the count.
48    pub fn set_offset(&mut self, count: impl IntoExpr) {
49        self.count = Some(count.into_expr());
50    }
51
52    /// Whether the clause is absent. The keyword rides on the count, so it does
53    /// not make an otherwise-empty clause present.
54    pub fn is_empty(&self) -> bool {
55        self.count.is_none()
56    }
57}
58
59impl Expression for Offset {
60    fn write_sql(&self, w: &mut SqlWriter<'_>) {
61        let Some(count) = &self.count else {
62            return;
63        };
64
65        w.push_str("OFFSET ");
66        w.write_expr(count);
67        if let Some(rows) = self.rows {
68            w.push_str(" ");
69            w.push_str(rows.as_str());
70        }
71    }
72}
73
74/// A statement with an `OFFSET`.
75pub trait HasOffset {
76    /// The `OFFSET` clause to modify.
77    fn offset_mut(&mut self) -> &mut Offset;
78}
79
80impl HasOffset for Offset {
81    fn offset_mut(&mut self) -> &mut Offset {
82        self
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use keelson_sqlcheck::testing::assert_frag_sql;
89
90    use super::*;
91    use crate::dialect::testing::Numbered;
92    use crate::expr::arg;
93    use crate::value::Value;
94    use crate::writer::build;
95
96    const FRAME: &str = r#"SELECT "id" FROM users {}"#;
97
98    #[test]
99    fn an_unset_offset_writes_nothing() {
100        assert_frag_sql(FRAME, &build(&Numbered, &Offset::default()).unwrap().0, "");
101        assert!(Offset::default().is_empty());
102    }
103
104    #[test]
105    fn the_count_is_written_after_the_keyword() {
106        let mut o = Offset::default();
107        o.set_offset(5i64);
108        assert_frag_sql(FRAME, &build(&Numbered, &o).unwrap().0, "OFFSET 5");
109
110        o.set_offset(arg(5i64));
111        let (sql, args) = build(&Numbered, &o).unwrap();
112        assert_frag_sql(FRAME, &sql, "OFFSET $1");
113        assert_eq!(args, vec![Value::I64(5)]);
114    }
115
116    /// PostgreSQL 17, `sql-select.html`: `[ OFFSET start [ ROW | ROWS ] ]` — the
117    /// keyword follows the count and either number is legal whatever the count.
118    #[test]
119    fn the_rows_keyword_follows_the_count_in_either_number() {
120        let mut o = Offset::default();
121        o.set_offset(5i64);
122        o.rows = Some(RowsKeyword::Rows);
123        assert_frag_sql(FRAME, &build(&Numbered, &o).unwrap().0, "OFFSET 5 ROWS");
124
125        o.rows = Some(RowsKeyword::Row);
126        assert_frag_sql(FRAME, &build(&Numbered, &o).unwrap().0, "OFFSET 5 ROW");
127    }
128
129    /// With the keyword, the count is `gram.y`'s `select_fetch_first_value` — a
130    /// `c_expr` — and a placeholder is one, so binding still works.
131    #[test]
132    fn the_count_may_be_bound_with_the_keyword_present() {
133        let mut o = Offset::default();
134        o.set_offset(arg(5i64));
135        o.rows = Some(RowsKeyword::Rows);
136        let (sql, args) = build(&Numbered, &o).unwrap();
137        assert_frag_sql(FRAME, &sql, "OFFSET $1 ROWS");
138        assert_eq!(args, vec![Value::I64(5)]);
139    }
140
141    /// The keyword rides on the count: without one there is nothing for it to
142    /// follow, so it does not write a dangling `OFFSET ROWS`.
143    #[test]
144    fn the_keyword_alone_writes_nothing() {
145        let o = Offset {
146            count: None,
147            rows: Some(RowsKeyword::Rows),
148        };
149        assert_frag_sql(FRAME, &build(&Numbered, &o).unwrap().0, "");
150        assert!(o.is_empty());
151    }
152}