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}