Skip to main content

keelson_psql/
extras.rs

1use std::borrow::Cow;
2
3use keelson_core::clause::{GroupingSet, GroupingSetKind};
4use keelson_core::expr::{Expr, IntoExprList};
5use keelson_core::{Expression, Query, SqlWriter};
6
7/// `SELECT` **`DISTINCT`** or `SELECT` **`DISTINCT ON (a, b)`**.
8///
9/// From PostgreSQL 17: `SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ]`.
10/// `ALL` is the default and adds nothing, so it is not representable; the absence
11/// of a `Distinct` is what `ALL` means.
12///
13/// A `SelectQuery` stores this as an `Option`, because `DISTINCT` with an empty
14/// `ON` list is a different statement from no `DISTINCT` at all — which is exactly
15/// the distinction bob loses by keying off `On != nil`.
16#[derive(Debug, Clone, Default)]
17pub struct Distinct {
18    /// The `ON` expressions. Empty is a plain `DISTINCT`.
19    pub on: Vec<Expr>,
20}
21
22impl Expression for Distinct {
23    fn write_sql(&self, w: &mut SqlWriter<'_>) {
24        w.push_str("DISTINCT");
25        w.write_slice(&self.on, " ON (", ", ", ")");
26    }
27}
28
29/// `OVERRIDING { SYSTEM | USER } VALUE`, an `INSERT`'s treatment of an identity
30/// column.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Overriding {
33    /// `OVERRIDING SYSTEM VALUE` — write the supplied value into a
34    /// `GENERATED ALWAYS` identity column.
35    System,
36    /// `OVERRIDING USER VALUE` — ignore the supplied value and use the sequence,
37    /// for a `GENERATED BY DEFAULT` column.
38    User,
39}
40
41impl Overriding {
42    /// The keyword, as written between `OVERRIDING` and `VALUE`.
43    pub fn as_str(self) -> &'static str {
44        match self {
45            Overriding::System => "SYSTEM",
46            Overriding::User => "USER",
47        }
48    }
49}
50
51/// `TABLESAMPLE method ( args ) [ REPEATABLE ( seed ) ]`.
52#[derive(Debug, Clone)]
53pub(crate) struct Sample {
54    pub(crate) method: Cow<'static, str>,
55    pub(crate) args: Vec<Expr>,
56    pub(crate) repeatable: Option<Expr>,
57}
58
59/// A from-item with a `TABLESAMPLE` clause, alias and all.
60///
61/// PostgreSQL's `gram.y` puts the sampling clause *after* the alias:
62///
63/// ```text
64/// table_ref: relation_expr opt_alias_clause tablesample_clause
65/// ```
66///
67/// [`TableRef`](keelson_core::clause::TableRef) has no slot in that position — it
68/// writes the alias last of the pre-join decorations — so the alias, the column
69/// aliases and the sampling clause are folded into one expression here and put in
70/// `TableRef::expression`. `ONLY` and `LATERAL` stay on the `TableRef`, because
71/// they precede the table name, and so do the joins, because they follow
72/// everything.
73#[derive(Debug)]
74pub(crate) struct SampledTable {
75    pub(crate) table: Expr,
76    pub(crate) alias: Option<Cow<'static, str>>,
77    pub(crate) columns: Vec<Cow<'static, str>>,
78    pub(crate) sample: Sample,
79}
80
81impl Expression for SampledTable {
82    fn write_sql(&self, w: &mut SqlWriter<'_>) {
83        w.write_expr(&self.table);
84
85        if let Some(alias) = &self.alias {
86            w.push_str(" AS ");
87            w.push_quoted(&[alias]);
88        }
89        if !self.columns.is_empty() {
90            w.push_str(" (");
91            for (i, column) in self.columns.iter().enumerate() {
92                if i > 0 {
93                    w.push_str(", ");
94                }
95                w.push_quoted(&[column]);
96            }
97            w.push_str(")");
98        }
99
100        w.push_str(" TABLESAMPLE ");
101        w.push_str(&self.sample.method);
102        // Unconditional parentheses: every sampling method takes at least the
103        // percentage, so an empty list is a caller error rather than a shape.
104        w.push_str(" (");
105        w.write_slice(&self.sample.args, "", ", ", "");
106        w.push_str(")");
107
108        if let Some(seed) = &self.sample.repeatable {
109            w.push_str(" REPEATABLE (");
110            w.write_expr(seed);
111            w.push_str(")");
112        }
113    }
114}
115
116/// A whole query standing in an expression slot, rendered in **its own** dialect.
117///
118/// bob's `BaseQuery.WriteSQL` ignores the dialect it is handed and uses the one it
119/// was built with; [`SqlWriter::write_with_dialect`] is how that is done here, and
120/// it keeps one shared argument list and placeholder counter, so a sub-query
121/// re-indexes into its container for free.
122#[derive(Debug)]
123struct QueryExpr<Q>(Q);
124
125impl<Q: Query> Expression for QueryExpr<Q> {
126    fn write_sql(&self, w: &mut SqlWriter<'_>) {
127        w.write_with_dialect(self.0.dialect(), &self.0);
128    }
129}
130
131/// A query as an expression, **not** parenthesised.
132///
133/// This is the form for slots that supply their own parentheses — a `WITH` body,
134/// a set-operation operand, `IN (…)`, `INSERT … SELECT`. Use [`subquery`] where
135/// the parentheses are part of the sub-query itself, as in a `FROM` item.
136pub fn query(q: impl Query + 'static) -> Expr {
137    Expr::custom(QueryExpr(q))
138}
139
140/// A parenthesised sub-query: `(SELECT …)`.
141///
142/// What a `FROM` item or a scalar sub-expression needs. PostgreSQL additionally
143/// requires an alias on a `FROM` sub-query, which is
144/// [`select::from(..).as_(..)`](mod@crate::select).
145pub fn subquery(q: impl Query + 'static) -> Expr {
146    Expr::group(query(q))
147}
148
149/// `EXCLUDED."col"` — the proposed row inside `ON CONFLICT DO UPDATE`.
150pub fn excluded(column: impl Into<Cow<'static, str>>) -> Expr {
151    Expr::join_with("", (Expr::raw("EXCLUDED."), Expr::ident(column.into())))
152}
153
154/// A fragment that cannot be rendered, and says so instead of writing nothing.
155///
156/// The pattern this exists for: a helper is handed an empty list, and the clause
157/// that will hold it has *already* committed its keyword by the time the fragment
158/// renders. `GroupBy` writes `GROUP BY ` as soon as it has one grouping element, so
159/// an element that writes nothing leaves the keyword dangling and
160/// [`build`](keelson_core::Query::build) hands back SQL that cannot parse — with no
161/// error at all, which is the worst of the available outcomes. Every other
162/// unfillable construct in the clause layer records
163/// [`Error::Incomplete`](keelson_core::Error::Incomplete); so does this.
164#[derive(Debug)]
165pub(crate) struct Incomplete(pub(crate) &'static str);
166
167impl Expression for Incomplete {
168    fn write_sql(&self, w: &mut SqlWriter<'_>) {
169        w.record_error(keelson_core::Error::Incomplete(self.0));
170    }
171}
172
173/// A from-item that was marked `LATERAL` but is a bare table or CTE name.
174///
175/// PostgreSQL's grammar puts `LATERAL` only in front of a sub-query or a
176/// function item — `JOIN LATERAL "posts"` is a syntax error, and there is
177/// nothing for the keyword to mean on a name anyway (a table cannot reference
178/// the items before it). The chain methods swap this in when `.lateral()` is
179/// called on such an item, so the mistake is caught where it is made; the item
180/// still renders, keeping the debug print honest, while `build()` refuses.
181///
182/// Only [`Expr::Ident`] items are judged. A raw fragment could be anything —
183/// progressive enhancement means hand-written SQL is trusted — and sub-queries
184/// and function calls arrive as other variants.
185#[derive(Debug)]
186pub(crate) struct LateralBareName(pub(crate) Expr);
187
188impl Expression for LateralBareName {
189    fn write_sql(&self, w: &mut SqlWriter<'_>) {
190        w.record_error(keelson_core::Error::other(
191            "LATERAL is set on a bare table or CTE name, but LATERAL can precede only a sub-query or a function item",
192        ));
193        w.write_expr(&self.0);
194    }
195}
196
197/// Wrap a grouping element, refusing an empty one.
198fn grouping_element(kind: GroupingSetKind, groups: impl IntoExprList) -> Expr {
199    let set = GroupingSet::new(kind, groups);
200    if set.is_empty() {
201        // `ROLLUP` with no list is a syntax error, and so is the `GROUP BY ` that
202        // would be left in front of it. See `Incomplete`.
203        return Expr::custom(Incomplete("the columns of a grouping element"));
204    }
205    Expr::custom(set)
206}
207
208/// `ROLLUP (a, b)` — a grouping element covering every prefix of the list.
209pub fn rollup(groups: impl IntoExprList) -> Expr {
210    grouping_element(GroupingSetKind::Rollup, groups)
211}
212
213/// `CUBE (a, b)` — a grouping element covering every subset of the list.
214pub fn cube(groups: impl IntoExprList) -> Expr {
215    grouping_element(GroupingSetKind::Cube, groups)
216}
217
218/// `GROUPING SETS ((a), (b), ())` — the sets listed explicitly.
219///
220/// Each element is normally a [`group`](crate::group); the empty set is written
221/// [`raw("()")`](crate::raw), because an empty
222/// [`Expr::Group`](keelson_core::expr::Expr::Group) renders `(NULL)` — a row of one
223/// null, which is a different thing.
224pub fn grouping_sets(sets: impl IntoExprList) -> Expr {
225    grouping_element(GroupingSetKind::GroupingSets, sets)
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::{Psql, group, quote, raw};
232    use keelson_core::build;
233
234    fn sql(e: impl Expression) -> String {
235        build(&Psql, &e).expect("render").0
236    }
237
238    #[test]
239    fn distinct_renders_with_and_without_an_on_list() {
240        assert_eq!(sql(Distinct::default()), "DISTINCT");
241        assert_eq!(
242            sql(Distinct {
243                on: vec![quote("a"), quote("b")]
244            }),
245            r#"DISTINCT ON ("a", "b")"#
246        );
247    }
248
249    #[test]
250    fn excluded_qualifies_the_column_with_the_pseudo_table() {
251        assert_eq!(sql(excluded("email")), r#"EXCLUDED."email""#);
252    }
253
254    /// PostgreSQL 17 `sql-select.html`, `grouping_element`.
255    #[test]
256    fn the_grouping_elements_use_their_own_keywords() {
257        assert_eq!(
258            sql(rollup((quote("a"), quote("b")))),
259            r#"ROLLUP ("a", "b")"#
260        );
261        assert_eq!(sql(cube((quote("a"), quote("b")))), r#"CUBE ("a", "b")"#);
262        assert_eq!(
263            sql(grouping_sets((group(quote("a")), raw("()")))),
264            r#"GROUPING SETS (("a"), ())"#
265        );
266    }
267}