use crate::ast::{
ArgSyntax, Expr, FilterWhereSpelling, FromFirstLast, FunctionArg, FunctionCall, Keyword,
Literal, NullTreatment, ObjectName, OrderByExpr, QuoteStyle, SetQuantifier, Span, Spanned,
WindowFunctionTail,
};
use crate::error::ParseResult;
use crate::parser::Dialect;
use crate::parser::engine::Parser;
use crate::tokenizer::{Operator, Punctuation, TokenKind};
use thin_vec::ThinVec;
type ParsedAggregateFilter<X> = (Option<Box<Expr<X>>>, FilterWhereSpelling);
pub(crate) const MYSQL_AGGREGATE_FUNCTIONS: &[&str] = &[
"COUNT",
"SUM",
"AVG",
"MIN",
"MAX",
"BIT_AND",
"BIT_OR",
"BIT_XOR",
"STD",
"STDDEV",
"STDDEV_POP",
"STDDEV_SAMP",
"VAR_POP",
"VAR_SAMP",
"VARIANCE",
"GROUP_CONCAT",
"JSON_ARRAYAGG",
"JSON_OBJECTAGG",
];
pub(crate) const MYSQL_WINDOW_FUNCTIONS: &[&str] = &[
"ROW_NUMBER",
"RANK",
"DENSE_RANK",
"PERCENT_RANK",
"CUME_DIST",
"NTILE",
"LEAD",
"LAG",
"FIRST_VALUE",
"LAST_VALUE",
"NTH_VALUE",
];
const DUCKDB_COALESCE_SPECIAL_FORM: &[&str] = &["coalesce"];
const GROUPING_SPECIAL_FORM: &[&str] = &["grouping"];
const MYSQL_WINDOW_FUNCTION_ARITY: &[(&str, usize, usize)] = &[
("ROW_NUMBER", 0, 0),
("RANK", 0, 0),
("DENSE_RANK", 0, 0),
("PERCENT_RANK", 0, 0),
("CUME_DIST", 0, 0),
("NTILE", 1, 1),
("FIRST_VALUE", 1, 1),
("LAST_VALUE", 1, 1),
("LEAD", 1, 3),
("LAG", 1, 3),
("NTH_VALUE", 2, 2),
];
const MYSQL_NULL_TREATMENT_WINDOW_FUNCTIONS: &[&str] =
&["FIRST_VALUE", "LAST_VALUE", "LEAD", "LAG", "NTH_VALUE"];
const PG_SQLJSON_EMPTY_REJECTING_CONSTRUCTORS: &[&str] = &["JSON", "JSON_SCALAR", "JSON_SERIALIZE"];
impl<'a, D: Dialect> Parser<'a, D> {
pub(in crate::parser) fn parse_function_call(
&mut self,
name: ObjectName,
start: Span,
) -> ParseResult<FunctionCall<D::Ext>> {
let name_adjacent_paren = name.span().end() == self.current_span()?.start();
self.advance()?; let quantifier = self.parse_aggregate_quantifier()?;
let (wildcard, args) = if quantifier.is_none()
&& self.peek_is_op(Operator::Star)?
&& !self.peek_is_columns_unpack_prefix()?
{
self.advance()?; (true, ThinVec::new())
} else if self.peek_is_punct(Punctuation::RParen)? {
(false, ThinVec::new())
} else if self
.features()
.aggregate_call_syntax
.standalone_argument_order_by
&& self.peek_is_keyword(Keyword::Order)?
&& self.peek_nth_is_keyword(1, Keyword::By)?
{
(false, ThinVec::new())
} else {
let trailing_comma = self.features().select_syntax.trailing_comma
&& self.function_name_in_set(&name, DUCKDB_COALESCE_SPECIAL_FORM);
(false, self.parse_function_args(trailing_comma)?)
};
if let Some(arg) = args
.iter()
.enumerate()
.find_map(|(idx, a)| (a.variadic && idx + 1 != args.len()).then_some(a))
{
let bad = arg.span();
return Err(self.error_at(
bad,
"VARIADIC only on the final argument: the array-spread marker must prefix the \
last argument of the call",
self.span_text(bad).to_owned(),
));
}
if quantifier.is_some() {
if let Some(arg) = args.iter().find(|a| a.variadic) {
let bad = arg.span();
return Err(self.error_at(
bad,
"no ALL/DISTINCT quantifier: a VARIADIC argument cannot combine with an \
aggregate ALL/DISTINCT quantifier",
self.span_text(bad).to_owned(),
));
}
}
let order_by = self.parse_aggregate_order_by()?;
let separator = self.parse_group_concat_separator()?;
let null_treatment = self.parse_null_treatment()?;
self.expect_punct(Punctuation::RParen, "`)` to close the function call")?;
let within_group = self.parse_within_group()?;
let (filter, filter_where) = self.parse_aggregate_filter()?;
let window_tail = self.parse_window_function_tail(&name)?;
let over = self.parse_over_clause()?;
let span = start.union(self.preceding_span());
let window_arity = self.mysql_window_function_arity(&name);
if within_group.is_some() {
if !order_by.is_empty() {
return Err(self.error_at(
span,
"a single ORDER BY: an in-parenthesis ORDER BY cannot combine with WITHIN GROUP",
self.span_text(span).to_owned(),
));
}
if matches!(quantifier, Some(SetQuantifier::Distinct)) {
return Err(self.error_at(
span,
"no DISTINCT: a WITHIN GROUP ordered-set aggregate cannot be DISTINCT",
self.span_text(span).to_owned(),
));
}
}
if self
.features()
.aggregate_call_syntax
.aggregate_args_require_adjacent_paren
&& !name_adjacent_paren
&& window_arity.is_none()
&& (wildcard
|| quantifier.is_some()
|| !order_by.is_empty()
|| separator.is_some()
|| over.is_some())
{
return Err(self.error_at(
span,
"the function name adjacent to `(`: a built-in aggregate/window argument or \
tail form (`*`, `DISTINCT`/`ALL`, `ORDER BY`, `SEPARATOR`, or `OVER`) \
requires no space before the parentheses",
self.span_text(span).to_owned(),
));
}
if self
.features()
.aggregate_call_syntax
.aggregate_calls_reject_empty_arguments
&& !wildcard
&& args.is_empty()
&& self.function_name_in_set(&name, MYSQL_AGGREGATE_FUNCTIONS)
{
return Err(self.error_at(
span,
"at least one argument: a MySQL built-in aggregate rejects an empty \
argument list (use `COUNT(*)` for the row count)",
self.span_text(span).to_owned(),
));
}
if self.features().grouping_syntax.grouping_sets
&& !wildcard
&& args.is_empty()
&& self.function_name_in_set(&name, GROUPING_SPECIAL_FORM)
{
return Err(self.error_at(
span,
"at least one argument: GROUPING requires a non-empty argument list",
self.span_text(span).to_owned(),
));
}
if self
.features()
.call_syntax
.sqljson_constructors_require_argument
&& !wildcard
&& args.is_empty()
&& self.function_name_in_set(&name, PG_SQLJSON_EMPTY_REJECTING_CONSTRUCTORS)
{
return Err(self.error_at(
span,
"the SQL/JSON context-item argument: `JSON`/`JSON_SCALAR`/`JSON_SERIALIZE` \
reject an empty argument list",
self.span_text(span).to_owned(),
));
}
if over.is_some()
&& self
.features()
.aggregate_call_syntax
.over_requires_windowable_function
&& !self.function_name_in_set(&name, MYSQL_AGGREGATE_FUNCTIONS)
&& !self.function_name_in_set(&name, MYSQL_WINDOW_FUNCTIONS)
{
return Err(self.error_at(
span,
"a windowable function: MySQL admits `OVER` only on an aggregate or window \
function, not on an ordinary scalar or user function",
self.span_text(span).to_owned(),
));
}
if let Some((min_args, max_args)) = window_arity.filter(|_| {
self.features()
.aggregate_call_syntax
.over_requires_windowable_function
}) {
if over.is_none() {
return Err(self.error_at(
span,
"an OVER clause: a MySQL window function (ROW_NUMBER, RANK, LEAD, …) \
requires a windowing OVER clause",
self.span_text(span).to_owned(),
));
}
if wildcard || quantifier.is_some() || !order_by.is_empty() || separator.is_some() {
return Err(self.error_at(
span,
"a plain argument list: a MySQL window function rejects the aggregate-only \
forms (`*`, a `DISTINCT`/`ALL` quantifier, an in-parenthesis `ORDER BY`, \
and `SEPARATOR`)",
self.span_text(span).to_owned(),
));
}
if args.len() < min_args || args.len() > max_args {
let expected = if min_args == max_args {
format!(
"exactly {min_args} argument(s): this MySQL window function takes a fixed argument count"
)
} else {
format!(
"{min_args} to {max_args} arguments: this MySQL window function takes a fixed argument count"
)
};
return Err(self.error_at(span, expected, self.span_text(span).to_owned()));
}
}
let call_meta = self.make_meta(span);
Ok(FunctionCall {
name,
quantifier,
args,
wildcard,
order_by,
separator,
within_group,
filter,
filter_where,
over,
null_treatment,
window_tail,
meta: call_meta,
})
}
fn function_name_in_set(&self, name: &ObjectName, set: &[&str]) -> bool {
let [part] = name.0.as_slice() else {
return false;
};
if part.quote != QuoteStyle::None {
return false;
}
let text = self.span_text(part.meta.span);
set.iter()
.any(|candidate| text.eq_ignore_ascii_case(candidate))
}
fn mysql_window_function_arity(&self, name: &ObjectName) -> Option<(usize, usize)> {
let [part] = name.0.as_slice() else {
return None;
};
if part.quote != QuoteStyle::None {
return None;
}
let text = self.span_text(part.meta.span);
MYSQL_WINDOW_FUNCTION_ARITY
.iter()
.find(|(candidate, _, _)| text.eq_ignore_ascii_case(candidate))
.map(|&(_, min_args, max_args)| (min_args, max_args))
}
fn parse_null_treatment(&mut self) -> ParseResult<Option<NullTreatment>> {
if !self.features().aggregate_call_syntax.null_treatment {
return Ok(None);
}
let treatment = if self.peek_is_keyword(Keyword::Ignore)?
&& self.peek_nth_is_keyword(1, Keyword::Nulls)?
{
NullTreatment::IgnoreNulls
} else if self.peek_is_keyword(Keyword::Respect)?
&& self.peek_nth_is_keyword(1, Keyword::Nulls)?
{
NullTreatment::RespectNulls
} else {
return Ok(None);
};
self.advance()?; self.advance()?; Ok(Some(treatment))
}
fn parse_window_function_tail(
&mut self,
name: &ObjectName,
) -> ParseResult<Option<WindowFunctionTail>> {
if !self.features().aggregate_call_syntax.window_function_tail {
return Ok(None);
}
let from_first_last = if self.function_name_in_set(name, &["NTH_VALUE"])
&& self.peek_is_keyword(Keyword::From)?
&& self.peek_nth_is_keyword(1, Keyword::First)?
{
self.advance()?; self.advance()?; Some(FromFirstLast::First)
} else {
None
};
let null_treatment = if self
.function_name_in_set(name, MYSQL_NULL_TREATMENT_WINDOW_FUNCTIONS)
&& self.peek_is_keyword(Keyword::Respect)?
&& self.peek_nth_is_keyword(1, Keyword::Nulls)?
{
self.advance()?; self.advance()?; Some(NullTreatment::RespectNulls)
} else {
None
};
Ok(if from_first_last.is_none() && null_treatment.is_none() {
None
} else {
Some(WindowFunctionTail {
from_first_last,
null_treatment,
})
})
}
fn parse_group_concat_separator(&mut self) -> ParseResult<Option<Literal>> {
if !(self.features().aggregate_call_syntax.group_concat_separator
&& self.peek_is_contextual_keyword("SEPARATOR")?)
{
return Ok(None);
}
self.expect_contextual_keyword("SEPARATOR")?;
let Some(token) = self.peek()? else {
return Err(self.unexpected("a string literal after `SEPARATOR`"));
};
if token.kind != TokenKind::String {
return Err(self.unexpected("a string literal after `SEPARATOR`"));
}
let Expr::Literal { literal, .. } = self.parse_string_literal(token)? else {
unreachable!("parse_string_literal always yields Expr::Literal");
};
Ok(Some(literal))
}
fn parse_function_args(
&mut self,
trailing_comma: bool,
) -> ParseResult<ThinVec<FunctionArg<D::Ext>>> {
if trailing_comma {
self.parse_comma_separated_trailing(Self::parse_function_arg, |p| {
p.trailing_comma_at(Punctuation::RParen)
})
} else {
self.parse_comma_separated(Self::parse_function_arg)
}
}
fn parse_function_arg(&mut self) -> ParseResult<FunctionArg<D::Ext>> {
let arg_start = self.current_span()?;
let variadic =
self.features().call_syntax.variadic_argument && self.eat_keyword(Keyword::Variadic)?;
if let Some(syntax) = self.peek_named_arg_separator()? {
let name = self.parse_ident()?.sym;
self.advance()?; let value = self.parse_expr()?;
let span = arg_start.union(value.span());
let meta = self.make_meta(span);
return Ok(FunctionArg {
name: Some(name),
variadic,
syntax,
value,
meta,
});
}
let value = self.parse_expr()?;
let span = arg_start.union(value.span());
let meta = self.make_meta(span);
Ok(FunctionArg {
name: None,
variadic,
syntax: ArgSyntax::Positional,
value,
meta,
})
}
fn peek_named_arg_separator(&mut self) -> ParseResult<Option<ArgSyntax>> {
if !self.features().call_syntax.named_argument {
return Ok(None);
}
let Some(name) = self.peek()? else {
return Ok(None);
};
if !self.token_can_be_column_name(name) {
return Ok(None);
}
Ok(match self.peek_nth(1)? {
Some(token) if token.kind == TokenKind::Operator(Operator::Arrow) => {
Some(ArgSyntax::Arrow)
}
Some(token) if token.kind == TokenKind::Operator(Operator::ColonEquals) => {
Some(ArgSyntax::ColonEquals)
}
_ => None,
})
}
fn parse_aggregate_quantifier(&mut self) -> ParseResult<Option<SetQuantifier>> {
if self.eat_keyword(Keyword::All)? {
Ok(Some(SetQuantifier::All))
} else if self.eat_keyword(Keyword::Distinct)? {
Ok(Some(SetQuantifier::Distinct))
} else {
Ok(None)
}
}
pub(super) fn parse_aggregate_filter(&mut self) -> ParseResult<ParsedAggregateFilter<D::Ext>> {
if !(self.features().aggregate_call_syntax.aggregate_filter
&& self.peek_is_keyword(Keyword::Filter)?
&& self.peek_nth_is_punct(1, Punctuation::LParen)?)
{
return Ok((None, FilterWhereSpelling::Where));
}
self.advance()?; self.expect_punct(Punctuation::LParen, "`(` after `FILTER`")?;
let where_spelling = if self.features().aggregate_call_syntax.filter_optional_where {
if self.eat_keyword(Keyword::Where)? {
FilterWhereSpelling::Where
} else {
FilterWhereSpelling::Omitted
}
} else {
self.expect_keyword(Keyword::Where)?;
FilterWhereSpelling::Where
};
let predicate = self.parse_expr()?;
self.expect_punct(Punctuation::RParen, "`)` to close the `FILTER` clause")?;
Ok((Some(Box::new(predicate)), where_spelling))
}
fn parse_within_group(&mut self) -> ParseResult<Option<ThinVec<OrderByExpr<D::Ext>>>> {
if !self.features().aggregate_call_syntax.within_group {
return Ok(None);
}
if !(self.peek_is_keyword(Keyword::Within)?
&& self.peek_nth_is_keyword(1, Keyword::Group)?)
{
return Ok(None);
}
self.advance()?; self.advance()?; self.expect_punct(Punctuation::LParen, "`(` after `WITHIN GROUP`")?;
let order_by = self.parse_aggregate_order_by()?;
if order_by.is_empty() {
return Err(self.unexpected("`ORDER BY` inside `WITHIN GROUP (…)`"));
}
self.expect_punct(
Punctuation::RParen,
"`)` to close the `WITHIN GROUP` clause",
)?;
Ok(Some(order_by))
}
}
#[cfg(test)]
mod tests {
use super::{MYSQL_WINDOW_FUNCTION_ARITY, MYSQL_WINDOW_FUNCTIONS};
#[test]
fn window_function_arity_table_covers_exactly_the_window_functions() {
for &(name, min_args, max_args) in MYSQL_WINDOW_FUNCTION_ARITY {
assert!(
MYSQL_WINDOW_FUNCTIONS.contains(&name),
"{name} has an arity entry but is not a window function",
);
assert!(min_args <= max_args, "{name} has an inverted arity bound");
}
for &name in MYSQL_WINDOW_FUNCTIONS {
assert!(
MYSQL_WINDOW_FUNCTION_ARITY
.iter()
.any(|&(candidate, _, _)| candidate == name),
"{name} is a window function with no arity entry",
);
}
}
}