Skip to main content

keelson_psql/
function.rs

1use std::borrow::Cow;
2
3use keelson_core::clause::{HasOrderBy, OrderBy, Window};
4use keelson_core::expr::{Expr, IntoExpr, IntoExprList};
5use keelson_core::{Expression, Mod, SqlWriter};
6
7/// A PostgreSQL function call, with every decoration the grammar hangs off one.
8///
9/// From PostgreSQL 17, `4.2.7 Aggregate Expressions` and
10/// `4.2.8 Window Function Calls`:
11///
12/// ```text
13/// name ( [ DISTINCT ] expression [, ...] [ ORDER BY … ] ) [ FILTER ( WHERE … ) ]
14/// name ( expression [, ...] ) WITHIN GROUP ( ORDER BY … ) [ FILTER ( WHERE … ) ]
15/// name ( … ) [ FILTER ( WHERE … ) ] OVER ( window_definition | window_name )
16/// ```
17///
18/// plus the `FROM`-list form, where a set-returning function may name its result
19/// columns:
20///
21/// ```text
22/// function_name ( … ) [ AS ] [ alias ] ( column_definition [, ...] )
23/// ```
24///
25/// [`Expr::Func`](keelson_core::expr::Expr::Func) carries only what all three
26/// dialects share, so everything above lives here and reaches core through
27/// [`Expr::Custom`](keelson_core::expr::Expr::Custom) — which is why this is a
28/// `keelson-psql` type and not a core one.
29///
30/// ```
31/// use keelson_psql::{f, quote, window};
32///
33/// // avg("views") OVER (PARTITION BY "user_id")
34/// let e = f("avg", quote("views")).over(window::partition_by(quote("user_id")));
35/// ```
36#[derive(Debug, Clone, Default)]
37pub struct Function {
38    name: Cow<'static, str>,
39    args: Vec<Expr>,
40    distinct: bool,
41    order_by: OrderBy,
42    within_group: bool,
43    filter: Vec<Expr>,
44    over: Option<OverClause>,
45}
46
47/// What follows `OVER`.
48///
49/// The two forms are not interchangeable, and the difference is not cosmetic.
50/// `OVER window_name` **references** a window from the statement's `WINDOW` clause;
51/// `OVER ( … )` is a definition, and a definition that begins with an existing
52/// window's name *copies* it — which PostgreSQL refuses when that window has a frame
53/// clause:
54///
55/// ```text
56/// ERROR:  cannot copy window "w" because it has a frame clause
57/// HINT:   Omit the parentheses in this OVER clause.
58/// ```
59///
60/// bob only ever writes the parenthesised form, so a named window with a frame is
61/// unreachable there. [`Function::over_name`] is the other one.
62#[derive(Debug, Clone)]
63enum OverClause {
64    /// `OVER "w"`.
65    Name(Cow<'static, str>),
66    /// `OVER ( … )`.
67    Definition(Window),
68}
69
70impl Function {
71    /// A call to `name` with `args`.
72    pub fn new(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> Function {
73        Function {
74            name: name.into(),
75            args: args.into_expr_list(),
76            ..Function::default()
77        }
78    }
79
80    /// `DISTINCT`, for an aggregate that should see each distinct input once.
81    #[must_use]
82    pub fn distinct(mut self) -> Function {
83        self.distinct = true;
84        self
85    }
86
87    /// Add a sort key to the aggregate's own `ORDER BY`.
88    ///
89    /// Rendered inside the argument list — `array_agg(x ORDER BY y)` — unless
90    /// [`within_group`](Self::within_group) moved it out.
91    #[must_use]
92    pub fn order_by(mut self, order: impl IntoExpr) -> Function {
93        self.order_by.append_order(order);
94        self
95    }
96
97    /// Write the `ORDER BY` as `WITHIN GROUP (ORDER BY …)`, which is what an
98    /// ordered-set aggregate such as `percentile_cont` requires.
99    #[must_use]
100    pub fn within_group(mut self) -> Function {
101        self.within_group = true;
102        self
103    }
104
105    /// Add a condition to `FILTER (WHERE …)`. Several are `AND`-joined.
106    #[must_use]
107    pub fn filter(mut self, condition: impl IntoExpr) -> Function {
108        self.filter.push(condition.into_expr());
109        self
110    }
111
112    /// The alias of a set-returning function used as a from-item: `f() AS "t"`,
113    /// or — with [`columns`](TableFunction::columns) — `f() AS "t" ("a" int)`.
114    ///
115    /// Not the select-list alias — that is [`as_`](Self::as_). The two must not
116    /// meet: `gram.y`'s `func_alias_clause` shares one `AS` between the alias
117    /// and the column definitions, so a second alias would be a second `AS`,
118    /// which is a syntax error. That is why this returns a [`TableFunction`],
119    /// on which `as_` — and the rest of the expression-position decorations —
120    /// does not exist.
121    #[must_use]
122    pub fn as_table(self, alias: impl Into<Cow<'static, str>>) -> TableFunction {
123        TableFunction::from(self).as_table(alias)
124    }
125
126    /// Name and type the columns a set-returning function returns:
127    /// `json_to_recordset($1) AS ("a" int, "b" text)`.
128    ///
129    /// The name is quoted; the type is written verbatim, so `int`, `text[]` and
130    /// `numeric(10, 2)` all work.
131    ///
132    /// Returns a [`TableFunction`] for the same reason
133    /// [`as_table`](Self::as_table) does: the column definitions spend the one
134    /// `AS` the `func_alias_clause` production has, so the select-list
135    /// [`as_`](Self::as_) cannot be allowed to write another.
136    #[must_use]
137    pub fn columns<N, T>(self, columns: impl IntoIterator<Item = (N, T)>) -> TableFunction
138    where
139        N: Into<Cow<'static, str>>,
140        T: Into<Cow<'static, str>>,
141    {
142        TableFunction::from(self).columns(columns)
143    }
144
145    /// Attach `OVER (…)`, built from window mods — `psql::window::*` and
146    /// `psql::frame::*`.
147    ///
148    /// Ends the builder, because `OVER` is the last thing in the grammar: it is
149    /// written after `FILTER`, and nothing may follow it. `over(())` gives the legal
150    /// `OVER ()`, which means the whole partition.
151    ///
152    /// To *reference* a window declared in the statement's `WINDOW` clause, use
153    /// [`over_name`](Self::over_name) — not `over(window::based_on(..))`, which is
154    /// the copying form and cannot copy a frame.
155    #[must_use]
156    pub fn over(mut self, mods: impl Mod<Window>) -> Expr {
157        let mut w = Window::default();
158        mods.apply(&mut w);
159        self.over = Some(OverClause::Definition(w));
160        self.into_expr()
161    }
162
163    /// Attach `OVER "w"` — a reference to a window in the statement's `WINDOW`
164    /// clause.
165    ///
166    /// Unparenthesised, which is what makes it a reference rather than a copy: the
167    /// parenthesised form is refused outright when the named window has a frame.
168    #[must_use]
169    pub fn over_name(mut self, name: impl Into<Cow<'static, str>>) -> Expr {
170        self.over = Some(OverClause::Name(name.into()));
171        self.into_expr()
172    }
173
174    /// `f(…) AS "alias"` — the select-list alias.
175    ///
176    /// Ends the builder for the same reason
177    /// [`Chain::as_`](keelson_core::expr::Chain::as_) does: an alias is not an
178    /// operand.
179    #[must_use]
180    pub fn as_(self, alias: impl Into<Cow<'static, str>>) -> Expr {
181        use keelson_core::expr::Chain as _;
182        self.into_expr().as_(alias.into())
183    }
184}
185
186impl HasOrderBy for Function {
187    fn order_by_mut(&mut self) -> &mut OrderBy {
188        &mut self.order_by
189    }
190}
191
192impl Expression for Function {
193    fn write_sql(&self, w: &mut SqlWriter<'_>) {
194        if self.name.is_empty() {
195            // A call with no name is not a fragment of anything, and there is no
196            // rendering of it that parses.
197            w.record_error(keelson_core::Error::Incomplete("the name of a function"));
198            return;
199        }
200
201        w.push_str(&self.name);
202        w.push_str("(");
203        if self.distinct {
204            w.push_str("DISTINCT ");
205        }
206        w.write_slice(&self.args, "", ", ", "");
207        if !self.within_group {
208            // `array_agg(x ORDER BY y)`: the separator is only needed when there
209            // is an argument in front of it, and `f(ORDER BY x)` is not a thing.
210            w.write_if(
211                !self.order_by.is_empty() && !self.args.is_empty(),
212                " ",
213                &self.order_by,
214                "",
215            );
216        }
217        w.push_str(")");
218
219        if self.within_group {
220            w.write_if(
221                !self.order_by.is_empty(),
222                " WITHIN GROUP (",
223                &self.order_by,
224                ")",
225            );
226        }
227
228        w.write_slice(&self.filter, " FILTER (WHERE ", " AND ", ")");
229
230        match &self.over {
231            None => {}
232            Some(OverClause::Name(name)) => {
233                w.push_str(" OVER ");
234                w.push_quoted(&[name]);
235            }
236            Some(OverClause::Definition(window)) => {
237                w.push_str(" OVER (");
238                w.write_expr(window);
239                w.push_str(")");
240            }
241        }
242    }
243}
244
245impl IntoExpr for Function {
246    fn into_expr(self) -> Expr {
247        Expr::custom(self)
248    }
249}
250
251impl IntoExprList for Function {
252    fn into_expr_list(self) -> Vec<Expr> {
253        vec![self.into_expr()]
254    }
255}
256
257/// A [`Function`] committed to the from-item form: the call plus `gram.y`'s
258/// `func_alias_clause`, `[ AS ] [ alias ] ( column_definition [, ...] )`.
259///
260/// [`Function::as_table`] and [`Function::columns`] return this instead of
261/// `Function`, and the expression-position enders — [`Function::as_`],
262/// [`Function::over`], [`Function::over_name`] — do not exist here. That is the
263/// point: `func_alias_clause` has exactly one `AS` shared between the alias and
264/// the column definitions, so `f() AS ("a" int) AS "r"` — the column form plus
265/// the select-list alias — is a syntax error, and this type makes it
266/// unwritable rather than an error to render. The from-item alias *is* the
267/// [`as_table`](Self::as_table) alias.
268///
269/// [`from_functions`](crate::shared::from_functions) accepts a plain
270/// `Function` and a `TableFunction` alike, so `f(..)` and
271/// `f(..).columns(..)` both go straight in.
272#[derive(Debug, Clone)]
273pub struct TableFunction {
274    function: Function,
275    alias: Option<Cow<'static, str>>,
276    columns: Vec<ColumnDef>,
277}
278
279impl TableFunction {
280    /// The alias in front of the column definitions: `f() AS "t" ("a" int)`.
281    #[must_use]
282    pub fn as_table(mut self, alias: impl Into<Cow<'static, str>>) -> TableFunction {
283        self.alias = Some(alias.into());
284        self
285    }
286
287    /// Add column definitions. Several calls accumulate into the one list.
288    #[must_use]
289    pub fn columns<N, T>(mut self, columns: impl IntoIterator<Item = (N, T)>) -> TableFunction
290    where
291        N: Into<Cow<'static, str>>,
292        T: Into<Cow<'static, str>>,
293    {
294        self.columns.extend(
295            columns
296                .into_iter()
297                .map(|(name, ty)| ColumnDef::new(name, ty)),
298        );
299        self
300    }
301}
302
303impl From<Function> for TableFunction {
304    fn from(function: Function) -> TableFunction {
305        TableFunction {
306            function,
307            alias: None,
308            columns: Vec::new(),
309        }
310    }
311}
312
313impl Expression for TableFunction {
314    fn write_sql(&self, w: &mut SqlWriter<'_>) {
315        self.function.write_sql(w);
316
317        // `AS` introduces the column-definition list, and the alias — when there
318        // is one — sits between them: `f() AS "t" ("a" int)`.
319        if self.alias.is_some() || !self.columns.is_empty() {
320            w.push_str(" AS");
321            if let Some(alias) = &self.alias {
322                w.push_str(" ");
323                w.push_quoted(&[alias]);
324            }
325            w.write_slice(&self.columns, " (", ", ", ")");
326        }
327    }
328}
329
330impl IntoExpr for TableFunction {
331    fn into_expr(self) -> Expr {
332        Expr::custom(self)
333    }
334}
335
336impl IntoExprList for TableFunction {
337    fn into_expr_list(self) -> Vec<Expr> {
338        vec![self.into_expr()]
339    }
340}
341
342/// One entry of a set-returning function's column-definition list: `"a" int`.
343#[derive(Debug, Clone)]
344pub struct ColumnDef {
345    /// The column name, quoted on output.
346    pub name: Cow<'static, str>,
347    /// The type, written verbatim.
348    pub data_type: Cow<'static, str>,
349}
350
351impl ColumnDef {
352    /// A named, typed column.
353    pub fn new(
354        name: impl Into<Cow<'static, str>>,
355        data_type: impl Into<Cow<'static, str>>,
356    ) -> ColumnDef {
357        ColumnDef {
358            name: name.into(),
359            data_type: data_type.into(),
360        }
361    }
362}
363
364impl Expression for ColumnDef {
365    fn write_sql(&self, w: &mut SqlWriter<'_>) {
366        w.push_quoted(&[&self.name]);
367        w.push_str(" ");
368        w.push_str(&self.data_type);
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::{Psql, arg, f, frame, quote, window};
376    use keelson_core::build;
377
378    fn sql(e: impl Expression) -> String {
379        build(&Psql, &e).expect("render").0
380    }
381
382    #[test]
383    fn a_plain_call_is_just_a_call() {
384        assert_eq!(sql(f("now", ())), "now()");
385        assert_eq!(sql(f("count", "*")), "count(*)");
386    }
387
388    /// PostgreSQL 17, 4.2.7: `DISTINCT` and the aggregate's own `ORDER BY` are
389    /// both inside the argument parentheses.
390    #[test]
391    fn distinct_and_order_by_stay_inside_the_argument_list() {
392        assert_eq!(
393            sql(f("count", quote("id")).distinct()),
394            r#"count(DISTINCT "id")"#
395        );
396        assert_eq!(
397            sql(f("array_agg", quote("id")).order_by(quote("name"))),
398            r#"array_agg("id" ORDER BY "name")"#
399        );
400    }
401
402    /// PostgreSQL 17, 4.2.7: an ordered-set aggregate puts its `ORDER BY` in a
403    /// `WITHIN GROUP` of its own, after the argument list.
404    #[test]
405    fn within_group_moves_the_order_by_out() {
406        assert_eq!(
407            sql(f("percentile_cont", arg(0.5f64))
408                .within_group()
409                .order_by(quote("views"))),
410            r#"percentile_cont($1) WITHIN GROUP (ORDER BY "views")"#
411        );
412    }
413
414    #[test]
415    fn filter_conditions_are_and_joined_inside_one_where() {
416        assert_eq!(
417            sql(f("count", "*").filter(quote("a")).filter(quote("b"))),
418            r#"count(*) FILTER (WHERE "a" AND "b")"#
419        );
420    }
421
422    /// PostgreSQL 17 `sql-select.html`, `from_item`:
423    /// `function_name ( … ) [ AS ] [ alias ] ( column_definition [, ...] )`.
424    #[test]
425    fn column_definitions_follow_as_with_the_alias_between() {
426        assert_eq!(
427            sql(f("json_to_recordset", arg("[]")).columns([("a", "int"), ("b", "text")])),
428            r#"json_to_recordset($1) AS ("a" int, "b" text)"#
429        );
430        assert_eq!(
431            sql(f("json_to_recordset", arg("[]"))
432                .as_table("t")
433                .columns([("a", "int")])),
434            r#"json_to_recordset($1) AS "t" ("a" int)"#
435        );
436    }
437
438    #[test]
439    fn over_takes_a_definition_a_name_or_nothing() {
440        assert_eq!(sql(f("row_number", ()).over(())), "row_number() OVER ()");
441        // The reference form has no parentheses; the copy form does, and copying is
442        // what PostgreSQL refuses when the named window has a frame.
443        assert_eq!(
444            sql(f("avg", quote("views")).over_name("w")),
445            r#"avg("views") OVER "w""#
446        );
447        assert_eq!(
448            sql(f("avg", quote("views")).over(window::based_on("w"))),
449            r#"avg("views") OVER ("w")"#
450        );
451        assert_eq!(
452            sql(f("sum", quote("views")).over((
453                window::partition_by(quote("user_id")),
454                window::order_by(quote("id")),
455                frame::rows(),
456                frame::from_current_row(),
457                frame::to_unbounded_following(),
458            ))),
459            r#"sum("views") OVER (PARTITION BY "user_id" ORDER BY "id" ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)"#
460        );
461    }
462
463    #[test]
464    fn filter_is_written_before_over() {
465        // 4.2.8: `[ FILTER ( WHERE filter_clause ) ] OVER ( … )`.
466        assert_eq!(
467            sql(f("count", "*")
468                .filter(quote("a"))
469                .over(window::partition_by(quote("b")))),
470            r#"count(*) FILTER (WHERE "a") OVER (PARTITION BY "b")"#
471        );
472    }
473
474    #[test]
475    fn an_unnamed_call_is_a_recorded_failure() {
476        let err = build(&Psql, &Function::default()).unwrap_err();
477        // The substring names the SQL concept (a function's name), not the
478        // message wording.
479        assert!(
480            matches!(&err, keelson_core::Error::Incomplete(what) if what.contains("function")),
481            "got: {err}"
482        );
483    }
484}