Skip to main content

keelson_core/clause/
fetch.rs

1use super::offset::RowsKeyword;
2use crate::expr::{Expr, IntoExpr};
3use crate::writer::{Expression, SqlWriter};
4
5/// `FETCH { FIRST | NEXT } n { ROW | ROWS } { ONLY | WITH TIES }` — the
6/// SQL-standard spelling of `LIMIT`, and the only one that can ask for ties.
7///
8/// From PostgreSQL 17:
9///
10/// ```text
11/// [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } { ONLY | WITH TIES } ]
12/// ```
13///
14/// `FIRST`/`NEXT` and `ROW`/`ROWS` are pure synonyms in the grammar, but not on
15/// the page: which one a query says is visible whenever generated SQL is read or
16/// diffed against a hand-written statement, so every spelling is representable.
17/// The defaults write `FETCH NEXT n ROWS`. `WITH TIES` requires an `ORDER BY`,
18/// which is the statement's business rather than this clause's.
19#[derive(Debug, Clone, Default)]
20pub struct Fetch {
21    /// How many rows.
22    pub count: Option<Expr>,
23    /// `FETCH FIRST` rather than `FETCH NEXT`.
24    pub first_or_next: FirstOrNext,
25    /// `ROW` or `ROWS`, either legal whatever the count.
26    pub rows: RowsKeyword,
27    /// `WITH TIES` rather than `ONLY`: also return rows that tie with
28    /// the last one under the `ORDER BY`.
29    pub with_ties: bool,
30}
31
32/// The `FIRST`/`NEXT` synonym pair of a `FETCH` — `gram.y` calls the production
33/// `first_or_next`. Synonyms in the grammar, distinct on the page, exactly as
34/// [`RowsKeyword`].
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub enum FirstOrNext {
37    /// `FETCH FIRST`.
38    First,
39    /// `FETCH NEXT`.
40    #[default]
41    Next,
42}
43
44impl FirstOrNext {
45    /// The keyword, as written after `FETCH`.
46    pub fn as_str(self) -> &'static str {
47        match self {
48            FirstOrNext::First => "FIRST",
49            FirstOrNext::Next => "NEXT",
50        }
51    }
52}
53
54impl Fetch {
55    /// Fetch `count` rows, without ties, in the default spelling.
56    pub fn new(count: impl IntoExpr) -> Self {
57        Fetch {
58            count: Some(count.into_expr()),
59            ..Fetch::default()
60        }
61    }
62
63    /// Set the count.
64    pub fn set_fetch(&mut self, count: impl IntoExpr) {
65        self.count = Some(count.into_expr());
66    }
67
68    /// Whether the clause is absent.
69    pub fn is_empty(&self) -> bool {
70        self.count.is_none()
71    }
72}
73
74impl Expression for Fetch {
75    fn write_sql(&self, w: &mut SqlWriter<'_>) {
76        // The spelling choices on their own say nothing, so the count gates the
77        // clause. The count is formally optional in the grammar — `FETCH NEXT
78        // ROWS ONLY` means one row — but that form is a trap, and asking for it
79        // explicitly with `Fetch::new(1)` is clearer than making the field's
80        // absence mean two different things.
81        let Some(count) = &self.count else {
82            return;
83        };
84
85        w.push_str("FETCH ");
86        w.push_str(self.first_or_next.as_str());
87        w.push_str(" ");
88        w.write_expr(count);
89        w.push_str(" ");
90        w.push_str(self.rows.as_str());
91        w.push_str(if self.with_ties {
92            " WITH TIES"
93        } else {
94            " ONLY"
95        });
96    }
97}
98
99/// A statement with a `FETCH` clause.
100pub trait HasFetch {
101    /// The `FETCH` clause to modify.
102    fn fetch_mut(&mut self) -> &mut Fetch;
103}
104
105impl HasFetch for Fetch {
106    fn fetch_mut(&mut self) -> &mut Fetch {
107        self
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use keelson_sqlcheck::testing::assert_frag_sql;
114
115    use super::*;
116    use crate::dialect::testing::Numbered;
117    use crate::expr::arg;
118    use crate::value::Value;
119    use crate::writer::build;
120
121    /// The frame carries an `ORDER BY` because PostgreSQL refuses `WITH TIES`
122    /// without one — the ties are ties *in the sort order*.
123    const FRAME: &str = r#"SELECT "id" FROM users ORDER BY "id" {}"#;
124
125    fn sql(f: &Fetch) -> String {
126        build(&Numbered, f).expect("render").0
127    }
128
129    #[test]
130    fn an_unset_fetch_writes_nothing_even_with_spellings_asked_for() {
131        let f = Fetch {
132            count: None,
133            first_or_next: FirstOrNext::First,
134            rows: RowsKeyword::Row,
135            with_ties: true,
136        };
137        assert_frag_sql(FRAME, &sql(&f), "");
138        assert!(Fetch::default().is_empty());
139    }
140
141    #[test]
142    fn the_suffix_switches_on_with_ties() {
143        let mut f = Fetch::new(3i64);
144        assert_frag_sql(FRAME, &sql(&f), "FETCH NEXT 3 ROWS ONLY");
145
146        f.with_ties = true;
147        assert_frag_sql(FRAME, &sql(&f), "FETCH NEXT 3 ROWS WITH TIES");
148    }
149
150    /// PostgreSQL 17, `sql-select.html`: `FETCH { FIRST | NEXT } [ count ]
151    /// { ROW | ROWS } …` — the spellings vary independently, and either number
152    /// is legal whatever the count.
153    #[test]
154    fn first_and_row_are_the_other_spellings() {
155        let mut f = Fetch::new(3i64);
156        f.first_or_next = FirstOrNext::First;
157        assert_frag_sql(FRAME, &sql(&f), "FETCH FIRST 3 ROWS ONLY");
158
159        f.rows = RowsKeyword::Row;
160        assert_frag_sql(FRAME, &sql(&f), "FETCH FIRST 3 ROW ONLY");
161
162        f.first_or_next = FirstOrNext::Next;
163        f.set_fetch(1i64);
164        assert_frag_sql(FRAME, &sql(&f), "FETCH NEXT 1 ROW ONLY");
165    }
166
167    #[test]
168    fn the_count_may_be_bound() {
169        let mut f = Fetch::default();
170        f.set_fetch(arg(3i64));
171        let (rendered, args) = build(&Numbered, &f).unwrap();
172        assert_frag_sql(FRAME, &rendered, "FETCH NEXT $1 ROWS ONLY");
173        assert_eq!(args, vec![Value::I64(3)]);
174    }
175
176    /// The spellings compose with a bound count and with ties: `FIRST` is a
177    /// synonym, not a different clause.
178    #[test]
179    fn first_composes_with_a_bound_count_and_ties() {
180        let mut f = Fetch::default();
181        f.set_fetch(arg(3i64));
182        f.first_or_next = FirstOrNext::First;
183        f.with_ties = true;
184        let (rendered, args) = build(&Numbered, &f).unwrap();
185        assert_frag_sql(FRAME, &rendered, "FETCH FIRST $1 ROWS WITH TIES");
186        assert_eq!(args, vec![Value::I64(3)]);
187    }
188}