Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,32 @@ pub trait Dialect: Debug + Any {
/// ```sql
/// SELECT transform(array(1, 2, 3), x -> x + 1); -- returns [2,3,4]
/// ```
///
/// This enables both the `->` spelling above and the `LAMBDA` keyword
/// spelling gated by [`Self::supports_lambda_keyword_syntax`]. A dialect
/// that uses `->` as a binary operator should override only the latter.
fn supports_lambda_functions(&self) -> bool {
false
}

/// Returns true if the dialect supports the `LAMBDA` keyword spelling of
/// lambda functions, for example:
///
/// ```sql
/// SELECT list_transform([1, 2, 3], lambda x : x + 1); -- returns [2, 3, 4]
/// ```
///
/// This spelling does not claim the `->` token, so it can be enabled by
/// dialects that already give `->` a different meaning, such as PostgreSQL
/// and its derivatives, where `->` is JSON member access. Defaults to
/// [`Self::supports_lambda_functions`], so dialects supporting the `->`
/// spelling accept the `LAMBDA` spelling too unless they say otherwise.
///
/// See <https://duckdb.org/docs/stable/sql/functions/lambda>
fn supports_lambda_keyword_syntax(&self) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn supports_lambda_keyword_syntax(&self) -> bool {
///
/// See <https://duckdb.org/docs/stable/sql/functions/lambda>
fn supports_lambda_keyword_syntax(&self) -> bool {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, applied!

self.supports_lambda_functions()
}
Comment on lines +560 to +562

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could argue for adding fn supports_lambda_arrow_syntax() as well, but I'd hold off until there is a concrete use case for enabling arrow syntax but not lambda syntax.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow-only is already expressible by overriding supports_lambda_keyword_syntax to false, so I believe there is no need.


/// Returns true if the dialect supports multiple variable assignment
/// using parentheses in a `SET` variable declaration.
///
Expand Down
2 changes: 1 addition & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1649,7 +1649,7 @@ impl<'a> Parser<'a> {
Keyword::MAP if *self.peek_token_ref() == Token::LBrace && self.dialect.support_map_literal_syntax() => {
Ok(Some(self.parse_duckdb_map_literal()?))
}
Keyword::LAMBDA if self.dialect.supports_lambda_functions() => {
Keyword::LAMBDA if self.dialect.supports_lambda_keyword_syntax() => {
Ok(Some(self.parse_lambda_expr()?))
}
_ if self.dialect.supports_geometric_types() => match w.keyword {
Expand Down
117 changes: 117 additions & 0 deletions tests/sqlparser_custom_dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use sqlparser::{
dialect::Dialect,
keywords::Keyword,
parser::{Parser, ParserError},
test_utils::{expr_from_projection, only},
tokenizer::Token,
};

Expand Down Expand Up @@ -167,3 +168,119 @@ fn is_identifier_part(ch: char) -> bool {
|| ch == '$'
|| ch == '_'
}

#[test]
fn custom_dialect_lambda_keyword_syntax_without_arrow() {
// A dialect that gives `->` its own meaning can still support lambdas
// through the `LAMBDA` keyword spelling.
#[derive(Debug)]
struct MyDialect {}

impl Dialect for MyDialect {
fn is_identifier_start(&self, ch: char) -> bool {
is_identifier_start(ch)
}

fn is_identifier_part(&self, ch: char) -> bool {
is_identifier_part(ch)
}

fn supports_lambda_keyword_syntax(&self) -> bool {
true
}
}

let dialect = MyDialect {};

// The `LAMBDA` spelling parses.
let sql = "SELECT transform(xs, lambda x : x + 1)";
assert_eq!(
sql,
&format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
);

// `->` keeps whatever meaning the dialect gives it, rather than
// introducing a lambda parameter.
let sql = "SELECT a -> 'b'";
let ast = Parser::parse_sql(&dialect, sql).unwrap();
match &ast[0] {
Statement::Query(query) => {
let Expr::BinaryOp { op, .. } =
expr_from_projection(only(&query.body.as_select().unwrap().projection))
else {
panic!("expected `->` to stay a binary operator");
};
assert_eq!(&BinaryOperator::Arrow, op);
}
stmt => panic!("unexpected statement {stmt}"),
}
}

#[test]
fn custom_dialect_lambda_keyword_defaults_to_arrow_support() {
// Dialects that opt into the `->` spelling get the `LAMBDA` spelling too,
// so the new capability does not change any existing dialect.
#[derive(Debug)]
struct MyDialect {}

impl Dialect for MyDialect {
fn is_identifier_start(&self, ch: char) -> bool {
is_identifier_start(ch)
}

fn is_identifier_part(&self, ch: char) -> bool {
is_identifier_part(ch)
}

fn supports_lambda_functions(&self) -> bool {
true
}
}

let dialect = MyDialect {};
assert!(dialect.supports_lambda_keyword_syntax());
for sql in [
"SELECT transform(xs, lambda x : x + 1)",
"SELECT transform(xs, x -> x + 1)",
] {
assert_eq!(
sql,
&format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
);
}
}

#[test]
fn custom_dialect_lambda_arrow_syntax_without_keyword() {
// Arrow lambdas stay on while the `LAMBDA` keyword spelling is off,
// as in engines like Spark and Snowflake.
#[derive(Debug)]
struct MyDialect {}

impl Dialect for MyDialect {
fn is_identifier_start(&self, ch: char) -> bool {
is_identifier_start(ch)
}

fn is_identifier_part(&self, ch: char) -> bool {
is_identifier_part(ch)
}

fn supports_lambda_functions(&self) -> bool {
true
}

fn supports_lambda_keyword_syntax(&self) -> bool {
false
}
}

let dialect = MyDialect {};

let sql = "SELECT transform(xs, x -> x + 1)";
assert_eq!(
sql,
&format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
);
assert!(Parser::parse_sql(&dialect, "SELECT transform(xs, lambda x : x + 1)").is_err());
}
66 changes: 66 additions & 0 deletions tests/sqlparser_derive_dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@

//! Tests for the `derive_dialect!` macro.

use sqlparser::ast::{
BinaryOperator, Expr, FunctionArg, FunctionArgExpr, FunctionArguments, LambdaSyntax, Statement,
};
use sqlparser::derive_dialect;
use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect};
use sqlparser::parser::Parser;
use sqlparser::test_utils::{expr_from_projection, only};

#[test]
fn test_method_overrides() {
Expand Down Expand Up @@ -121,3 +125,65 @@ fn test_identifier_quote_style_overrides() {
None
);
}

#[test]
fn test_lambda_keyword_syntax_on_postgres_derivative() {
Comment on lines +129 to +130

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm split between this (the real regression test I want) and another test using a MyDialect in sqlparser_custom_dialect.rs that enables the two flags. Open to input.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both seem worth keeping.

// A PostgreSQL derivative can opt into the `LAMBDA` keyword spelling of
// lambda functions without giving up `->` as JSON member access. The two
// meet in a single expression below: a lambda whose body is a JSON access.
derive_dialect!(
LambdaPostgreSqlDialect,
PostgreSqlDialect,
overrides = { supports_lambda_keyword_syntax = true }
);
let dialect = LambdaPostgreSqlDialect::new();

// Only the keyword spelling is enabled; the arrow spelling stays off.
assert!(dialect.supports_lambda_keyword_syntax());
assert!(!dialect.supports_lambda_functions());

let sql = "SELECT transform(xs, lambda x : (x -> 'a')::INT + 1)";
let ast = Parser::parse_sql(&dialect, sql).unwrap();
assert_eq!(sql, ast[0].to_string());

// Round-tripping alone would not distinguish a JSON access from a nested
// lambda, since both print as `x -> 'a'`, so check the parsed shape.
let Statement::Query(query) = &ast[0] else {
panic!("unexpected statement {}", ast[0]);
};
let Expr::Function(func) =
expr_from_projection(only(&query.body.as_select().unwrap().projection))
else {
panic!("expected a function call");
};
let FunctionArguments::List(args) = &func.args else {
panic!("expected an argument list");
};
let [_, FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Lambda(lambda)))] = &args.args[..]
else {
panic!("expected the second argument to be a lambda");
};

// The lambda came from the `LAMBDA` keyword, not from `->`.
assert_eq!(LambdaSyntax::LambdaKeyword, lambda.syntax);

// And the `->` in its body is still JSON member access.
let Expr::BinaryOp {
left,
op: BinaryOperator::Plus,
..
} = lambda.body.as_ref()
else {
panic!("expected the lambda body to be an addition");
};
let Expr::Cast { expr, .. } = left.as_ref() else {
panic!("expected the left operand to be a cast");
};
let Expr::Nested(json_access) = expr.as_ref() else {
panic!("expected the cast operand to be parenthesized");
};
let Expr::BinaryOp { op, .. } = json_access.as_ref() else {
panic!("expected `->` to stay a binary operator");
};
assert_eq!(&BinaryOperator::Arrow, op);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we should add a test for the Spark/Snowflake case (they only accept ->):

Suggested change
}
}
#[test]
fn custom_dialect_lambda_arrow_syntax_without_keyword() {
// Arrow lambdas stay on while the `LAMBDA` keyword spelling is off,
// as in engines like Spark and Snowflake.
#[derive(Debug)]
struct MyDialect {}
impl Dialect for MyDialect {
fn is_identifier_start(&self, ch: char) -> bool {
is_identifier_start(ch)
}
fn is_identifier_part(&self, ch: char) -> bool {
is_identifier_part(ch)
}
fn supports_lambda_functions(&self) -> bool {
true
}
fn supports_lambda_keyword_syntax(&self) -> bool {
false
}
}
let dialect = MyDialect {};
let sql = "SELECT transform(xs, x -> x + 1)";
assert_eq!(
sql,
&format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
);
assert!(Parser::parse_sql(&dialect, "SELECT transform(xs, lambda x : x + 1)").is_err());
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added but in sqlparser_custom_dialect.rs

Loading