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}