use crate::ast::dialect::{
AggregateCallSyntax, CallSyntax, DoubleAmpersand, ExpressionSyntax, FeatureDelta, FeatureSet,
KeywordOperators, LexicalConflict, NumericLiteralSyntax, OperatorSyntax, ParameterSyntax,
PipeOperator, PredicateSyntax, SessionVariableSyntax, StringFuncForms, StringLiteralSyntax,
};
use crate::ast::precedence::{Assoc, BindingPower};
use crate::ast::{
ArgSyntax, ArrayExpr, ArraySpelling, BinaryOperator, BitStringRadix, CastSyntax,
CharacterTypeName, DataType, DecimalTypeName, EqualsSpelling, Expr, FieldSelector,
FilterWhereSpelling, IntegerDivideSpelling, IntegerTypeName, IntervalFields,
IsDistinctFromSpelling, IsNotDistinctFromSpelling, Literal, LiteralKind, ModuloSpelling, NoExt,
NotEqSpelling, ParameterKind, ParameterSigil, Quantifier, RegexpSpelling, Resolver as _,
SelectItem, SemiStructuredPathSegment, SessionVariableKind, SetExpr, SetQuantifier, Span,
Spanned, SpecialFunctionKeyword, Statement, StructKeySpelling, SubscriptKind, TableFactor,
TimeZone, TimestampTypeName, TruthValue, UnaryOperator, WindowFrameBound, WindowFrameExclusion,
WindowFrameUnits, WindowSpec,
};
use crate::dialect::{Ansi, DuckDb, MySql, Postgres, Sqlite};
use crate::parser::{FeatureDialect, ParseConfig, Parsed, TestDialect, parse_with};
use crate::render::Renderer;
const PG_EXPR_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet = FeatureSet::ANSI.with(
FeatureDelta::EMPTY
.expression_syntax(ExpressionSyntax::POSTGRES)
.operator_syntax(OperatorSyntax::POSTGRES)
.call_syntax(CallSyntax::POSTGRES)
.string_func_forms(StringFuncForms::POSTGRES)
.aggregate_call_syntax(AggregateCallSyntax::POSTGRES),
);
FeatureDialect {
features: &FEATURES,
}
};
const OVERLAPS_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet = FeatureSet::ANSI.with(
FeatureDelta::EMPTY
.expression_syntax(ExpressionSyntax::POSTGRES)
.predicate_syntax(PredicateSyntax::POSTGRES),
);
FeatureDialect {
features: &FEATURES,
}
};
const MYSQL_EXPR_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet = FeatureSet::ANSI.with(
FeatureDelta::EMPTY
.expression_syntax(ExpressionSyntax::MYSQL)
.operator_syntax(OperatorSyntax::MYSQL)
.call_syntax(CallSyntax::MYSQL)
.string_func_forms(StringFuncForms::MYSQL)
.aggregate_call_syntax(AggregateCallSyntax::MYSQL),
);
FeatureDialect {
features: &FEATURES,
}
};
const DUCKDB_TYPE_DIALECT: FeatureDialect = FeatureDialect {
features: &FeatureSet::DUCKDB,
};
const SEMI_STRUCTURED_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
semi_structured_access: true,
..ExpressionSyntax::ANSI
}));
FeatureDialect {
features: &FEATURES,
}
};
const SEMI_STRUCTURED_POSTFIX_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
semi_structured_access: true,
subscript: true,
typecast_operator: true,
collection_literals: true,
..ExpressionSyntax::ANSI
}));
FeatureDialect {
features: &FEATURES,
}
};
fn project_expr(parsed: &Parsed) -> &Expr<NoExt> {
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
match &select.projection[0] {
SelectItem::Expr {
expr, alias: None, ..
} => expr,
other => panic!("expected a bare expression item, got {other:?}"),
}
}
fn column_name<'a>(parsed: &'a Parsed, expr: &Expr<NoExt>) -> &'a str {
match expr {
Expr::Column { name, .. } if name.0.len() == 1 => parsed.resolver().resolve(name.0[0].sym),
other => panic!("expected an unqualified column, got {other:?}"),
}
}
fn cast_type(parsed: &Parsed) -> &DataType {
match project_expr(parsed) {
Expr::Cast { data_type, .. } => data_type,
other => panic!("expected a CAST expression, got {other:?}"),
}
}
fn selection_expr(parsed: &Parsed) -> &Expr<NoExt> {
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
select
.selection
.as_ref()
.expect("expected a WHERE expression")
}
#[test]
fn literal_keywords_parse_as_literals_in_expression_position() {
let parsed = parse_with(
"SELECT TRUE, FALSE, NULL",
crate::ParseConfig::new(TestDialect),
)
.expect("literal keywords parse");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let kinds: Vec<_> = select
.projection
.iter()
.map(|item| {
let SelectItem::Expr {
expr: Expr::Literal { literal, .. },
..
} = item
else {
panic!("expected literal projection item, got {item:?}");
};
literal.kind.clone()
})
.collect();
assert_eq!(
kinds,
vec![
LiteralKind::Boolean(true),
LiteralKind::Boolean(false),
LiteralKind::Null,
],
);
}
#[test]
fn literal_keywords_still_follow_identifier_rules_outside_expression_position() {
assert!(
parse_with("SELECT a FROM null", crate::ParseConfig::new(TestDialect)).is_err(),
"a reserved keyword is not a bare table name",
);
let parsed = parse_with(
"SELECT a FROM \"null\"",
crate::ParseConfig::new(TestDialect),
)
.expect("a quoted reserved word is a valid table name");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let TableFactor::Table { name, .. } = &select.from[0].relation else {
panic!("expected a table factor");
};
assert_eq!(parsed.resolver().resolve(name.0[0].sym), "null");
}
#[test]
fn multiplication_binds_tighter_than_addition() {
let parsed = parse_with("SELECT 1 + 2 * 3", crate::ParseConfig::new(TestDialect))
.expect("valid expression");
let Expr::BinaryOp {
left,
op: BinaryOperator::Plus,
right,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be `+`");
};
assert!(matches!(**left, Expr::Literal { .. }), "left of `+` is `1`");
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::Multiply,
..
}
),
"right of `+` is the `2 * 3` product",
);
}
#[test]
fn parentheses_override_precedence() {
let parsed = parse_with("SELECT (1 + 2) * 3", crate::ParseConfig::new(TestDialect))
.expect("valid expression");
let Expr::BinaryOp {
left,
op: BinaryOperator::Multiply,
right,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be `*`");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Plus,
..
}
),
"left of `*` is the grouped `1 + 2`",
);
assert!(
matches!(**right, Expr::Literal { .. }),
"right of `*` is `3`"
);
}
#[test]
fn and_binds_tighter_than_or() {
let parsed = parse_with("SELECT a AND b OR c", crate::ParseConfig::new(TestDialect))
.expect("valid expression");
let Expr::BinaryOp {
left,
op: BinaryOperator::Or,
right,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be `OR`");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::And,
..
}
),
"left of `OR` is the `a AND b` conjunction",
);
assert_eq!(column_name(&parsed, right), "c");
}
#[test]
fn string_concat_binds_between_additive_and_comparison() {
let parsed = parse_with("SELECT a = b || c", crate::ParseConfig::new(TestDialect))
.expect("valid expression");
let Expr::BinaryOp {
op: BinaryOperator::Eq(_),
right,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be `=`");
};
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::StringConcat,
..
}
),
"right of `=` is the `b || c` concatenation",
);
}
const HIGH_CONCAT_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet = FeatureSet::ANSI.with(FeatureDelta::EMPTY.binding_powers(
FeatureSet::ANSI.binding_powers.with_binary(
&BinaryOperator::StringConcat,
BindingPower {
left: 70,
right: 71,
assoc: Assoc::Left,
},
),
));
FeatureDialect {
features: &FEATURES,
}
};
const LEFT_ASSOC_COMPARISON_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet = FeatureSet::ANSI.with(FeatureDelta::EMPTY.binding_powers(
FeatureSet::ANSI.binding_powers.with_binary(
&BinaryOperator::Lt,
BindingPower {
left: 40,
right: 41,
assoc: Assoc::Left,
},
),
));
FeatureDialect {
features: &FEATURES,
}
};
const LOGICAL_OR_PIPE_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.pipe_operator(PipeOperator::LogicalOr));
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn pipe_operator_meaning_is_dialect_data() {
let concat = parse_with("SELECT a = b || c", crate::ParseConfig::new(TestDialect))
.expect("concat dialect parses");
let Expr::BinaryOp {
op: BinaryOperator::Eq(_),
right,
..
} = project_expr(&concat)
else {
panic!("under string-concat `||`, `=` is the root of `a = b || c`");
};
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::StringConcat,
..
}
),
"`||` concatenates `b || c` on the right of `=`",
);
let logical_or = parse_with(
"SELECT a = b || c",
crate::ParseConfig::new(LOGICAL_OR_PIPE_DIALECT),
)
.expect("OR-pipe dialect parses");
let Expr::BinaryOp {
op: BinaryOperator::Or,
left,
..
} = project_expr(&logical_or)
else {
panic!("under logical-OR `||`, `||` is the root of `a = b || c`");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Eq(_),
..
}
),
"`a = b` is the left operand of the `||`-as-OR",
);
}
const LOGICAL_AND_AMP_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.double_ampersand(DoubleAmpersand::LogicalAnd));
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn double_ampersand_meaning_is_dialect_data() {
assert!(
parse_with("SELECT a && b", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI does not accept `&&` as a scalar operator",
);
let and = parse_with(
"SELECT a && b",
crate::ParseConfig::new(LOGICAL_AND_AMP_DIALECT),
)
.expect("AND-amp dialect parses");
assert!(
matches!(
project_expr(&and),
Expr::BinaryOp {
op: BinaryOperator::And,
..
}
),
"`&&` parses to logical AND under a MySQL-like dialect",
);
let rooted = parse_with(
"SELECT a = b && c",
crate::ParseConfig::new(LOGICAL_AND_AMP_DIALECT),
)
.expect("AND-amp dialect parses");
let Expr::BinaryOp {
op: BinaryOperator::And,
left,
..
} = project_expr(&rooted)
else {
panic!("under `&&`-as-AND, `&&` is the root of `a = b && c`");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Eq(_),
..
}
),
"`a = b` is the left operand of the `&&`-as-AND",
);
}
const KEYWORD_OPERATOR_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.keyword_operators(KeywordOperators::MySql));
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn keyword_operators_bind_at_their_dialect_precedence() {
for (sql, expected) in [
(
"SELECT a + b DIV c",
BinaryOperator::IntegerDivide(IntegerDivideSpelling::Div),
),
(
"SELECT a + b MOD c",
BinaryOperator::Modulo(ModuloSpelling::Mod),
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(KEYWORD_OPERATOR_DIALECT))
.expect("multiplicative parses");
let Expr::BinaryOp {
op: BinaryOperator::Plus,
right,
..
} = project_expr(&parsed)
else {
panic!("{sql}: a multiplicative keyword operator binds tighter than `+`");
};
let Expr::BinaryOp { op, .. } = &**right else {
panic!("{sql}: `b OP c` is the right operand of `+`");
};
assert_eq!(*op, expected, "{sql}");
}
let xor_and = parse_with(
"SELECT a XOR b AND c",
crate::ParseConfig::new(KEYWORD_OPERATOR_DIALECT),
)
.expect("`XOR`/`AND` parses");
let Expr::BinaryOp {
op: BinaryOperator::Xor,
right,
..
} = project_expr(&xor_and)
else {
panic!("`AND` binds tighter than `XOR`, so `XOR` roots `a XOR b AND c`");
};
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::And,
..
}
),
"`b AND c` is the right operand of `XOR`",
);
let xor_or = parse_with(
"SELECT a XOR b OR c",
crate::ParseConfig::new(KEYWORD_OPERATOR_DIALECT),
)
.expect("`XOR`/`OR` parses");
let Expr::BinaryOp {
op: BinaryOperator::Or,
left,
..
} = project_expr(&xor_or)
else {
panic!("`OR` binds looser than `XOR`, so `OR` roots `a XOR b OR c`");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Xor,
..
}
),
"`a XOR b` is the left operand of `OR`",
);
for (sql, expected) in [
("SELECT a + b RLIKE c", RegexpSpelling::Rlike),
("SELECT a + b REGEXP c", RegexpSpelling::Regexp),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(KEYWORD_OPERATOR_DIALECT))
.expect("regex match parses");
let Expr::BinaryOp {
op: BinaryOperator::Regexp(spelling),
left,
..
} = project_expr(&parsed)
else {
panic!("{sql}: regex match is looser than `+`, so it roots the expression");
};
assert_eq!(*spelling, expected, "{sql}");
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Plus,
..
}
),
"{sql}: `a + b` is the left operand of the regex match",
);
}
}
#[test]
fn keyword_operators_round_trip_exact_spelling() {
for sql in [
"SELECT a DIV b",
"SELECT a MOD b",
"SELECT a XOR b",
"SELECT a RLIKE b",
"SELECT a REGEXP b",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(KEYWORD_OPERATOR_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(KEYWORD_OPERATOR_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn duckdb_equals_and_integer_divide_spellings_round_trip() {
for (sql, op) in [
("SELECT a == b", BinaryOperator::Eq(EqualsSpelling::Double)),
(
"SELECT a // b",
BinaryOperator::IntegerDivide(IntegerDivideSpelling::SlashSlash),
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::BinaryOp { op: parsed_op, .. } = project_expr(&parsed) else {
panic!("expected a binary operator for {sql}");
};
assert_eq!(*parsed_op, op, "operator for {sql}");
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn duckdb_integer_divide_slash_binds_multiplicative() {
let parsed = parse_with(
"SELECT a + b // c",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("`//` parses");
let Expr::BinaryOp {
op: BinaryOperator::Plus,
right,
..
} = project_expr(&parsed)
else {
panic!("expected `+` at the root");
};
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::IntegerDivide(IntegerDivideSpelling::SlashSlash),
..
}
),
"`//` is the right operand of `+`",
);
}
#[test]
fn duckdb_string_projection_alias_round_trips() {
let sql = "SELECT 1 AS 'x'";
let parsed =
parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).expect("string alias parses");
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{err}"));
assert_eq!(rendered, sql);
}
#[test]
fn duckdb_symbol_spellings_and_string_alias_reject_elsewhere() {
for sql in ["SELECT a == b", "SELECT a // b", "SELECT 1 AS 'x'"] {
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects {sql}"
);
}
for sql in ["SELECT a == b", "SELECT a // b"] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("PG accepts {sql}: {e}"));
assert!(matches!(project_expr(&parsed), Expr::NamedOperator { .. }));
}
assert!(parse_with("SELECT 1 AS 'x'", crate::ParseConfig::new(Postgres)).is_err());
}
#[test]
fn duckdb_shares_the_general_symbolic_operator_surface() {
for sql in [
"SELECT 1 <<| 2", "SELECT 1 <-> 2", "SELECT p &&&&&@ Le", "SELECT 1 ~ 2", "SELECT 1 !~ 2", "SELECT 1 ~* 2", "SELECT p `= q", "SELECT 1 ` 2", ] {
assert!(
matches!(
project_expr(
&parse_with(sql, crate::ParseConfig::new(DuckDb))
.unwrap_or_else(|e| panic!("DuckDb accepts {sql}: {e}"))
),
Expr::NamedOperator { .. }
),
"DuckDb reads {sql} as a bare named operator",
);
}
for sql in ["SELECT @ 1", "SELECT |/ 4", "SELECT ||/ 8", "SELECT !! 3"] {
assert!(
matches!(
project_expr(
&parse_with(sql, crate::ParseConfig::new(DuckDb))
.unwrap_or_else(|e| panic!("DuckDb accepts {sql}: {e}"))
),
Expr::PrefixOperator { .. }
),
"DuckDb reads {sql} as a prefix operator",
);
}
for sql in [
"SELECT 1 @#@ 2",
"SELECT 1 # 2",
"SELECT 1 ? 2",
"SELECT 1 &#& 2",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(DuckDb)).is_err(),
"DuckDb rejects {sql}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_ok(),
"Postgres keeps # / ? in its operator runs: {sql}"
);
}
}
#[test]
fn duckdb_general_operator_trees_round_trip() {
for sql in [
"SELECT 1 <-> 2",
"SELECT 1 ~ 2",
"SELECT @ 1",
"SELECT 1 ` 2",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn duckdb_postfix_symbolic_operators_parse_in_operand_absent_position() {
for sql in [
"SELECT 10!",
"SELECT 1 !",
"SELECT 1 ~",
"SELECT 1 <->",
"SELECT 1 !!",
"SELECT 1 &",
"SELECT 1 |",
"SELECT 1 <<",
"SELECT 1 <@",
"SELECT 1 @>",
"SELECT 1 ! FROM t", "SELECT (1!)", ] {
assert!(
matches!(
project_expr(
&parse_with(sql, crate::ParseConfig::new(DuckDb))
.unwrap_or_else(|e| panic!("DuckDb accepts {sql}: {e}"))
),
Expr::PostfixOperator { .. }
),
"DuckDb reads {sql} as a postfix operator",
);
}
assert!(
matches!(
project_expr(
&parse_with("SELECT 1 ! + 2", crate::ParseConfig::new(DuckDb))
.expect("infix `!` parses")
),
Expr::NamedOperator { .. }
),
"an operand after the operator keeps the infix reading",
);
for sql in ["SELECT 1 ->", "SELECT 1 ->>"] {
assert!(
parse_with(sql, crate::ParseConfig::new(DuckDb)).is_err(),
"DuckDb rejects trailing {sql}"
);
}
}
#[test]
fn duckdb_postfix_operator_precedence_and_round_trip() {
for (sql, expected) in [
("SELECT 10!", "SELECT 10 !"),
("SELECT 2 * 3!", "SELECT 2 * 3 !"),
("SELECT 1 + 2!", "SELECT 1 + 2 !"),
("SELECT 1! < 2", "SELECT 1 ! < 2"),
("SELECT 1! :: INT", "SELECT 1 !::INTEGER"),
("SELECT 1! IS NULL", "SELECT 1 ! IS NULL"),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, expected, "render for {sql}");
let reparsed = parse_with(&rendered, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("re-parse {rendered}: {err:?}"));
let again = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&reparsed)
.unwrap_or_else(|err| panic!("{rendered}: {err}"));
assert_eq!(again, rendered, "round-trip changed the tree for {sql}");
}
let parsed =
parse_with("SELECT 2 * 3!", crate::ParseConfig::new(DuckDb)).expect("`2 * 3!` parses");
let Expr::PostfixOperator {
postfix_operator, ..
} = project_expr(&parsed)
else {
panic!("`2 * 3!` is a postfix operator at the top");
};
assert!(
matches!(&postfix_operator.operand, Expr::BinaryOp { .. }),
"the postfix operand is the whole `2 * 3` product",
);
let parsed =
parse_with("SELECT 1! < 2", crate::ParseConfig::new(DuckDb)).expect("`1! < 2` parses");
let Expr::BinaryOp { left, .. } = project_expr(&parsed) else {
panic!("`1! < 2` is a comparison at the top");
};
assert!(
matches!(left.as_ref(), Expr::PostfixOperator { .. }),
"the comparison's left operand is the `1!` postfix",
);
}
#[test]
fn postfix_operators_reject_without_the_dialect() {
for dialect_rejects in [
parse_with("SELECT 10!", crate::ParseConfig::new(Postgres)).is_err(),
parse_with("SELECT 1 ~", crate::ParseConfig::new(Postgres)).is_err(),
parse_with("SELECT 10!", crate::ParseConfig::new(Ansi)).is_err(),
parse_with("SELECT 10!", crate::ParseConfig::new(MySql)).is_err(),
parse_with("SELECT 10!", crate::ParseConfig::new(Sqlite)).is_err(),
] {
assert!(
dialect_rejects,
"postfix operators reject without the dialect gate"
);
}
}
#[test]
fn duckdb_unparenthesized_in_parses_as_in_expr_and_round_trips() {
for (sql, negated) in [
("SELECT z IN y", false),
("SELECT z IN t.c", false),
("SELECT z IN f(x)", false),
("SELECT z IN y[1]", false),
("SELECT z IN [1, 2, 3]", false),
("SELECT z IN {'a': 1}", false),
("SELECT z IN ?", false),
("SELECT z IN $1", false),
("SELECT z NOT IN y", true),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::InExpr {
negated: parsed_negated,
..
} = project_expr(&parsed)
else {
panic!("expected `Expr::InExpr` for {sql}");
};
assert_eq!(*parsed_negated, negated, "negation for {sql}");
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn duckdb_unparenthesized_in_rejects_constant_and_unary_leading_rhs() {
for sql in [
"SELECT z IN 4",
"SELECT z IN 3.5",
"SELECT z IN 'abc'",
"SELECT z IN TRUE",
"SELECT z IN FALSE",
"SELECT z IN NULL",
"SELECT z IN -5",
"SELECT z IN +y",
"SELECT z IN ~y",
"SELECT z IN b'101'",
"SELECT z IN DATE '2020-01-01'",
"SELECT z IN *",
"SELECT z IN EXISTS (SELECT 1)",
"SELECT z IN COLUMNS('a')",
"SELECT z NOT IN 4",
"SELECT z NOT IN -5",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_err(),
"DuckDB rejects {sql} (constant/unary/excluded leading token)",
);
}
}
#[test]
fn duckdb_parenthesized_in_stays_the_standard_predicate() {
for (sql, is_subquery) in [
("SELECT z IN (4)", false),
("SELECT z IN (y)", false),
("SELECT z IN (a, b)", false),
("SELECT z IN (SELECT 1)", true),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let expr = project_expr(&parsed);
if is_subquery {
assert!(
matches!(expr, Expr::InSubquery { .. }),
"expected `InSubquery` for {sql}, got {expr:?}",
);
} else {
assert!(
matches!(expr, Expr::InList { .. }),
"expected `InList` for {sql}, got {expr:?}",
);
}
}
}
#[test]
fn duckdb_unparenthesized_in_binds_tighter_than_comparison_and_arithmetic() {
let parsed = parse_with(
"SELECT z = w IN y",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("parses");
let Expr::BinaryOp {
op: BinaryOperator::Eq(EqualsSpelling::Single),
right,
..
} = project_expr(&parsed)
else {
panic!("expected `=` at the root");
};
assert!(
matches!(**right, Expr::InExpr { .. }),
"the `IN` is the right operand of `=`",
);
let parsed = parse_with(
"SELECT z IN y = w",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("parses");
let Expr::BinaryOp {
op: BinaryOperator::Eq(EqualsSpelling::Single),
left,
..
} = project_expr(&parsed)
else {
panic!("expected `=` at the root");
};
assert!(
matches!(**left, Expr::InExpr { .. }),
"the `IN` is the left operand of `=`",
);
let parsed = parse_with(
"SELECT a * b IN y",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("parses");
let Expr::InExpr { expr, .. } = project_expr(&parsed) else {
panic!("expected `InExpr` at the root");
};
assert!(
matches!(
**expr,
Expr::BinaryOp {
op: BinaryOperator::Multiply,
..
}
),
"`a * b` is the left operand of the `IN`",
);
let parsed = parse_with(
"SELECT z IN y IN w",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("parses");
let Expr::InExpr { expr, .. } = project_expr(&parsed) else {
panic!("expected `InExpr` at the root");
};
assert!(
matches!(**expr, Expr::InExpr { .. }),
"`z IN y IN w` associates left as `(z IN y) IN w`",
);
}
#[test]
fn duckdb_unparenthesized_in_rhs_is_c_expr() {
let parsed = parse_with(
"SELECT z IN y[1]",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("parses");
let Expr::InExpr { rhs, .. } = project_expr(&parsed) else {
panic!("expected `InExpr` at the root");
};
assert!(
matches!(**rhs, Expr::Subscript { .. }),
"the subscript binds into the RHS",
);
let parsed = parse_with(
"SELECT z IN y::INT",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("parses");
let Expr::Cast { expr, .. } = project_expr(&parsed) else {
panic!("expected `Cast` at the root");
};
assert!(
matches!(**expr, Expr::InExpr { .. }),
"the `::` typecast wraps the whole `InExpr`",
);
}
#[test]
fn duckdb_unparenthesized_in_gated_off_in_other_dialects() {
for sql in [
"SELECT z IN y",
"SELECT z NOT IN y",
"SELECT z IN [1, 2, 3]",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects {sql}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL rejects {sql}"
);
}
assert!(
parse_with("SELECT z IN (y)", crate::ParseConfig::new(Postgres)).is_ok(),
"the parenthesized form still parses",
);
}
#[test]
fn keyword_operators_are_inert_without_the_dialect() {
for sql in [
"SELECT a DIV b",
"SELECT a MOD b",
"SELECT a XOR b",
"SELECT a RLIKE b",
"SELECT a REGEXP b",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI does not treat the keyword as an operator: {sql}",
);
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL does not treat the keyword as an operator: {sql}",
);
}
}
#[test]
fn mysql_preset_gives_keyword_operators_their_meaning() {
let cases: [(&str, BinaryOperator); 5] = [
(
"SELECT a DIV b",
BinaryOperator::IntegerDivide(IntegerDivideSpelling::Div),
),
(
"SELECT a MOD b",
BinaryOperator::Modulo(ModuloSpelling::Mod),
),
("SELECT a XOR b", BinaryOperator::Xor),
(
"SELECT a RLIKE b",
BinaryOperator::Regexp(RegexpSpelling::Rlike),
),
(
"SELECT a REGEXP b",
BinaryOperator::Regexp(RegexpSpelling::Regexp),
),
];
for (sql, expected) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::BinaryOp { op, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a binary operator expression");
};
assert_eq!(*op, expected, "{sql}");
}
}
#[test]
fn is_distinct_from_parses_to_binary_op_in_both_polarities() {
for (sql, expected) in [
(
"SELECT a IS DISTINCT FROM b",
BinaryOperator::IsDistinctFrom(IsDistinctFromSpelling::Keyword),
),
(
"SELECT a IS NOT DISTINCT FROM b",
BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::Keyword),
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::BinaryOp { op, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a binary operator expression");
};
assert_eq!(*op, expected, "{sql}");
}
}
#[test]
fn is_distinct_from_is_non_associative() {
for sql in [
"SELECT a IS DISTINCT FROM b IS DISTINCT FROM c",
"SELECT a IS NOT DISTINCT FROM b IS NOT DISTINCT FROM c",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"{sql} should reject"
);
}
}
#[test]
fn is_truth_predicate_parses_all_six_forms() {
for (sql, expected_value, expected_negated) in [
("SELECT a IS TRUE", TruthValue::True, false),
("SELECT a IS NOT TRUE", TruthValue::True, true),
("SELECT a IS FALSE", TruthValue::False, false),
("SELECT a IS NOT FALSE", TruthValue::False, true),
("SELECT a IS UNKNOWN", TruthValue::Unknown, false),
("SELECT a IS NOT UNKNOWN", TruthValue::Unknown, true),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::IsTruth { value, negated, .. } = project_expr(&parsed) else {
panic!("{sql}: expected Expr::IsTruth");
};
assert_eq!(*value, expected_value, "{sql}");
assert_eq!(*negated, expected_negated, "{sql}");
}
}
#[test]
fn is_truth_binds_tighter_than_boolean_and() {
let parsed = parse_with(
"SELECT a IS TRUE AND b IS FALSE",
crate::ParseConfig::new(Postgres),
)
.expect("parses");
let Expr::BinaryOp {
left,
op: BinaryOperator::And,
right,
..
} = project_expr(&parsed)
else {
panic!("expected a top-level boolean AND");
};
assert!(matches!(
**left,
Expr::IsTruth {
value: TruthValue::True,
negated: false,
..
}
));
assert!(matches!(
**right,
Expr::IsTruth {
value: TruthValue::False,
negated: false,
..
}
));
}
#[test]
fn is_truth_is_non_associative() {
assert!(
parse_with(
"SELECT a IS TRUE IS FALSE",
crate::ParseConfig::new(Postgres)
)
.is_err(),
"unparenthesized truth-test chain should reject"
);
let parsed = parse_with(
"SELECT (a IS TRUE) IS FALSE",
crate::ParseConfig::new(Postgres),
)
.expect("parenthesized nests");
let Expr::IsTruth {
value: TruthValue::False,
..
} = project_expr(&parsed)
else {
panic!("expected an outer IS FALSE over a parenthesized IS TRUE");
};
}
#[test]
fn is_truth_gated_off_under_sqlite_general_equality() {
let parsed = parse_with("SELECT a IS TRUE", crate::ParseConfig::new(Sqlite))
.expect("SQLite parses `a IS TRUE`");
assert!(
matches!(
project_expr(&parsed),
Expr::BinaryOp {
op: BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::Is),
..
}
),
"SQLite `a IS TRUE` is general null-safe equality, not a truth test",
);
let parsed = parse_with("SELECT a IS UNKNOWN", crate::ParseConfig::new(Sqlite))
.expect("SQLite parses `a IS UNKNOWN`");
assert!(
!matches!(project_expr(&parsed), Expr::IsTruth { .. }),
"SQLite `a IS UNKNOWN` is general equality against `unknown`, not a truth test",
);
}
const PARAMETER_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.parameters(ParameterSyntax {
positional_dollar: true,
positional_dollar_large: false,
anonymous_question: true,
named_colon: true,
named_at: true,
named_dollar: true,
numbered_question: true,
}));
FeatureDialect {
features: &FEATURES,
}
};
const SQLITE_DIALECT: FeatureDialect = FeatureDialect {
features: &FeatureSet::SQLITE,
};
#[test]
fn parameter_placeholders_parse_to_parameter_expressions() {
let positional = parse_with("SELECT $1", crate::ParseConfig::new(PARAMETER_DIALECT))
.expect("positional parameter parses");
assert!(
matches!(
project_expr(&positional),
Expr::Parameter {
kind: ParameterKind::Positional(1),
..
}
),
"`$1` parses to a positional parameter with index 1: {:?}",
project_expr(&positional),
);
let anonymous = parse_with("SELECT ?", crate::ParseConfig::new(PARAMETER_DIALECT))
.expect("anonymous parameter parses");
assert!(
matches!(
project_expr(&anonymous),
Expr::Parameter {
kind: ParameterKind::Anonymous,
..
}
),
"`?` parses to an anonymous parameter",
);
assert!(
parse_with("SELECT $1", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects `$1`",
);
assert!(
parse_with("SELECT ?", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects `?`",
);
}
#[test]
fn named_parameters_parse_and_round_trip() {
for (sql, name, want_sigil) in [
("SELECT :user_id", "user_id", ParameterSigil::Colon),
("SELECT @count", "count", ParameterSigil::At),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PARAMETER_DIALECT))
.expect("named parameter parses");
let Expr::Parameter {
kind: ParameterKind::Named { name: sym, sigil },
..
} = project_expr(&parsed)
else {
panic!(
"expected a named parameter, got {:?}",
project_expr(&parsed)
);
};
assert_eq!(*sigil, want_sigil, "sigil tag for {sql:?}");
assert_eq!(
parsed.resolver().resolve(*sym),
name,
"interned name (sigil stripped) for {sql:?}",
);
assert_eq!(
Renderer::new(PARAMETER_DIALECT)
.render_parsed(&parsed)
.expect("named parameter renders"),
sql,
"named parameter round-trips to its source",
);
}
assert!(
parse_with("SELECT :user_id", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects `:name`",
);
assert!(
parse_with("SELECT @count", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects `@name`",
);
}
#[test]
fn sqlite_numbered_parameter_parses_range_checks_and_round_trips() {
for (sql, index) in [
("SELECT ?1", 1),
("SELECT ?123", 123),
("SELECT ?32766", 32766),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(SQLITE_DIALECT))
.unwrap_or_else(|e| panic!("{sql:?}: {e}"));
assert!(
matches!(
project_expr(&parsed),
Expr::Parameter {
kind: ParameterKind::Numbered(n),
..
} if *n == index,
),
"{sql:?} parses to Numbered({index}): {:?}",
project_expr(&parsed),
);
assert_eq!(
Renderer::new(SQLITE_DIALECT)
.render_parsed(&parsed)
.expect("renders"),
sql,
"{sql:?} round-trips",
);
}
parse_with("SELECT ?1abc", crate::ParseConfig::new(SQLITE_DIALECT))
.expect("`?1abc` is `?1` aliased `abc`");
for sql in [
"SELECT ?0",
"SELECT ?32767",
"SELECT ?70000",
"SELECT ?999999999999999999999",
] {
let err = parse_with(sql, crate::ParseConfig::new(SQLITE_DIALECT))
.expect_err(&format!("{sql:?} is out of range"));
assert_eq!(
err.expected.as_str(),
"a numbered parameter index between ?1 and ?32766",
"{sql:?}",
);
}
assert!(
parse_with("SELECT ?1", crate::ParseConfig::new(Ansi)).is_err(),
"ANSI rejects `?1`"
);
}
#[test]
fn sqlite_bare_string_projection_alias_round_trips() {
let sql = "SELECT 1 'x'";
let parsed =
parse_with(sql, crate::ParseConfig::new(SQLITE_DIALECT)).expect("bare string alias parses");
let rendered = Renderer::new(SQLITE_DIALECT)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(
rendered, "SELECT 1 AS 'x'",
"bare alias canonicalises to the `AS` spelling"
);
parse_with(&rendered, crate::ParseConfig::new(SQLITE_DIALECT))
.expect("the canonical render re-parses");
assert!(
parse_with("SELECT 1 'x'", crate::ParseConfig::new(DuckDb)).is_err(),
"DuckDB rejects the bare string alias (AS-only)",
);
assert!(
parse_with("SELECT 1 'x'", crate::ParseConfig::new(Ansi)).is_err(),
"ANSI rejects the bare string alias",
);
}
#[test]
fn mysql_bare_string_alias_and_adjacent_concat_split_by_parse_order() {
for (sql, alias) in [("SELECT 1 'x'", "x"), ("SELECT 1 \"x\"", "x")] {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("{sql}: expected a query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("{sql}: expected a SELECT body");
};
assert_eq!(select.projection.len(), 1, "{sql}: one projected column");
let SelectItem::Expr {
expr,
alias: Some(ident),
..
} = &select.projection[0]
else {
panic!(
"{sql}: expected an aliased expression, got {:?}",
select.projection[0]
);
};
assert_eq!(
parsed.resolver().resolve(ident.sym),
alias,
"{sql}: alias name"
);
assert!(
matches!(expr, Expr::Literal { .. }),
"{sql}: the operand is the bare `1`, not the alias",
);
}
for (sql, value) in [
("SELECT 'a' 'b'", "ab"),
("SELECT 'a' 'b' 'c'", "abc"),
("SELECT 'a' \"b\"", "ab"),
("SELECT \"a\" \"b\"", "ab"),
("SELECT N'a' 'b'", "ab"),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("{sql}: expected a query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("{sql}: expected a SELECT body");
};
assert_eq!(select.projection.len(), 1, "{sql}: one projected column");
let SelectItem::Expr {
expr: Expr::Literal { literal, .. },
alias: None,
..
} = &select.projection[0]
else {
panic!(
"{sql}: expected an unaliased literal, got {:?}",
select.projection[0]
);
};
assert_eq!(
parsed.literal_str(literal).expect("materialises"),
value,
"{sql}: the adjacent segments concatenate",
);
}
for sql in [
"SELECT 1 'x' 'y'",
"SELECT 'a' _utf8'b'",
"SELECT _utf8'a' _utf8'b'",
"SELECT 'a' N'b'",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"{sql}: rejects"
);
}
}
#[test]
fn positional_parameter_index_overflow_is_a_clean_error() {
let err = parse_with(
"SELECT $99999999999",
crate::ParseConfig::new(PARAMETER_DIALECT),
)
.expect_err("an out-of-range positional index is rejected");
assert_eq!(
err.expected.as_str(),
"a positional parameter index within u32 range",
);
let parsed = parse_with(
"SELECT $99999999999",
crate::ParseConfig::new(crate::dialect::Postgres),
)
.expect("PostgreSQL preserves the oversized ParamRef spelling");
let Expr::Parameter {
kind: ParameterKind::PositionalLarge { digits },
..
} = project_expr(&parsed)
else {
panic!("expected an oversized positional parameter");
};
assert_eq!(parsed.resolver().resolve(*digits), "99999999999");
}
const SESSION_VARIABLE_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.session_variables(SessionVariableSyntax::MYSQL));
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn session_variables_parse_and_round_trip() {
for (sql, want_kind, name) in [
(
"SELECT @user_count",
SessionVariableKind::User,
"user_count",
),
(
"SELECT @@max_connections",
SessionVariableKind::System,
"max_connections",
),
(
"SELECT @@global.time_zone",
SessionVariableKind::SystemGlobal,
"time_zone",
),
(
"SELECT @@session.sql_mode",
SessionVariableKind::SystemSession,
"sql_mode",
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(SESSION_VARIABLE_DIALECT))
.expect("session variable parses");
let Expr::SessionVariable {
kind, name: sym, ..
} = project_expr(&parsed)
else {
panic!(
"expected a session variable, got {:?}",
project_expr(&parsed)
);
};
assert_eq!(*kind, want_kind, "kind tag for {sql:?}");
assert_eq!(
parsed.resolver().resolve(*sym),
name,
"interned name (sigil/scope stripped) for {sql:?}",
);
assert_eq!(
Renderer::new(SESSION_VARIABLE_DIALECT)
.render_parsed(&parsed)
.expect("session variable renders"),
sql,
"session variable round-trips to its source",
);
}
let bare = parse_with(
"SELECT @@global",
crate::ParseConfig::new(SESSION_VARIABLE_DIALECT),
)
.expect("bare `@@global` parses");
assert!(
matches!(
project_expr(&bare),
Expr::SessionVariable {
kind: SessionVariableKind::System,
..
}
),
"`@@global` is an implicit-scope system variable named `global`",
);
assert!(
parse_with(
"SELECT @@bogus.x",
crate::ParseConfig::new(SESSION_VARIABLE_DIALECT)
)
.is_err(),
"an unknown system-variable scope is rejected",
);
for sql in [
"SELECT @user_count",
"SELECT @@max_connections",
"SELECT @@global.time_zone",
"SELECT @@session.sql_mode",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_ok(),
"MySQL parses {sql:?}",
);
}
assert!(
parse_with("SELECT @x", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects `@x`",
);
assert!(
parse_with("SELECT @@x", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects `@@x`",
);
}
#[test]
fn parser_reads_binding_powers_from_the_dialect() {
let standard = parse_with("SELECT a || b * c", crate::ParseConfig::new(TestDialect))
.expect("standard precedence parses");
let Expr::BinaryOp {
op: BinaryOperator::StringConcat,
right,
..
} = project_expr(&standard)
else {
panic!("standard dialect root should be concatenation");
};
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::Multiply,
..
}
),
"multiply binds under concat in the standard table",
);
let custom = parse_with(
"SELECT a || b * c",
crate::ParseConfig::new(HIGH_CONCAT_DIALECT),
)
.expect("custom precedence parses");
let Expr::BinaryOp {
op: BinaryOperator::Multiply,
left,
..
} = project_expr(&custom)
else {
panic!("custom dialect root should be multiplication");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::StringConcat,
..
}
),
"concat binds under multiply in the custom table",
);
}
#[test]
fn not_parses_as_a_prefix_unary() {
let parsed =
parse_with("SELECT NOT a", crate::ParseConfig::new(TestDialect)).expect("valid expression");
let Expr::UnaryOp {
op: UnaryOperator::Not,
expr,
..
} = project_expr(&parsed)
else {
panic!("expected a unary `NOT`");
};
assert_eq!(column_name(&parsed, expr), "a");
}
#[test]
fn unary_minus_wraps_its_operand() {
let parsed =
parse_with("SELECT - 1", crate::ParseConfig::new(TestDialect)).expect("valid expression");
let Expr::UnaryOp {
op: UnaryOperator::Minus,
expr,
..
} = project_expr(&parsed)
else {
panic!("expected a unary minus");
};
assert!(
matches!(**expr, Expr::Literal { .. }),
"operand is the literal `1`"
);
}
#[test]
fn cast_parses_numeric_and_character_type_names() {
let parsed = parse_with(
"SELECT CAST(a AS INT)",
crate::ParseConfig::new(TestDialect),
)
.expect("CAST parses");
assert!(matches!(
cast_type(&parsed),
DataType::Integer {
spelling: IntegerTypeName::Int,
..
}
));
let parsed = parse_with(
"SELECT CAST(1 AS NUMERIC(10, 2))",
crate::ParseConfig::new(TestDialect),
)
.expect("NUMERIC precision and scale parse");
assert!(matches!(
cast_type(&parsed),
DataType::Decimal {
spelling: DecimalTypeName::Numeric,
precision: Some(10),
scale: Some(2),
..
}
));
let parsed = parse_with(
"SELECT CAST(a AS CHARACTER VARYING(5))",
crate::ParseConfig::new(TestDialect),
)
.expect("CHARACTER VARYING size parses");
assert!(matches!(
cast_type(&parsed),
DataType::Character {
spelling: CharacterTypeName::CharacterVarying,
size: Some(5),
..
}
));
}
#[test]
fn cast_parses_temporal_interval_and_array_type_names() {
let parsed = parse_with(
"SELECT CAST(a AS TIMESTAMP(3) WITH TIME ZONE)",
crate::ParseConfig::new(TestDialect),
)
.expect("TIMESTAMP WITH TIME ZONE parses");
assert!(matches!(
cast_type(&parsed),
DataType::Timestamp {
spelling: TimestampTypeName::Timestamp,
precision: Some(3),
time_zone: TimeZone::WithTimeZone,
..
}
));
let parsed = parse_with(
"SELECT CAST(a AS INTERVAL DAY TO SECOND(3))",
crate::ParseConfig::new(TestDialect),
)
.expect("INTERVAL DAY TO SECOND precision parses");
assert!(matches!(
cast_type(&parsed),
DataType::Interval {
fields: Some(IntervalFields::DayToSecond),
precision: Some(3),
..
}
));
let parsed = parse_with(
"SELECT CAST(a AS VARCHAR(5)[])",
crate::ParseConfig::new(TestDialect),
)
.expect("array suffix parses");
assert!(matches!(
cast_type(&parsed),
DataType::Array { element, .. }
if matches!(
&**element,
DataType::Character {
spelling: CharacterTypeName::Varchar,
size: Some(5),
..
}
)
));
}
#[test]
fn cast_parses_user_defined_qualified_type_names() {
let parsed = parse_with(
"SELECT CAST(a AS public.geometry(4326))",
crate::ParseConfig::new(TestDialect),
)
.expect("qualified user-defined type parses");
let DataType::UserDefined {
name, modifiers, ..
} = cast_type(&parsed)
else {
panic!("expected a user-defined type");
};
assert_eq!(name.0.len(), 2);
assert_eq!(parsed.resolver().resolve(name.0[0].sym), "public");
assert_eq!(parsed.resolver().resolve(name.0[1].sym), "geometry");
assert_eq!(modifiers.len(), 1);
assert_eq!(modifiers[0].kind, LiteralKind::Integer);
assert_eq!(
modifiers[0]
.as_i64(parsed.source())
.expect("integer modifier"),
4326
);
}
#[test]
fn scalar_subquery_parses_as_expression() {
let parsed = parse_with("SELECT (SELECT 1)", crate::ParseConfig::new(TestDialect))
.expect("subquery parses");
let Expr::Subquery { query, .. } = project_expr(&parsed) else {
panic!("expected a scalar subquery expression");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected the scalar subquery body to be SELECT");
};
assert_eq!(select.projection.len(), 1);
}
#[test]
fn exists_predicate_parses_as_distinct_expression() {
let parsed = parse_with(
"SELECT * FROM t WHERE EXISTS (SELECT 1)",
crate::ParseConfig::new(TestDialect),
)
.expect("EXISTS predicate parses");
let Expr::Exists { query, .. } = selection_expr(&parsed) else {
panic!("expected an EXISTS predicate");
};
assert!(matches!(query.body, SetExpr::Select { .. }));
}
#[test]
fn special_value_functions_parse_nullary_and_precision_forms() {
for (sql, expected) in [
("SELECT CURRENT_DATE", SpecialFunctionKeyword::CurrentDate),
("SELECT CURRENT_USER", SpecialFunctionKeyword::CurrentUser),
("SELECT USER", SpecialFunctionKeyword::User),
("SELECT SESSION_USER", SpecialFunctionKeyword::SessionUser),
(
"SELECT CURRENT_CATALOG",
SpecialFunctionKeyword::CurrentCatalog,
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.expect("special value function parses");
let Expr::SpecialFunction {
keyword,
precision: None,
..
} = project_expr(&parsed)
else {
panic!("expected a nullary special function for {sql}");
};
assert_eq!(*keyword, expected);
}
let parsed = parse_with("SELECT CURRENT_TIME(3)", crate::ParseConfig::new(Postgres))
.expect("precision form parses");
let Expr::SpecialFunction {
keyword: SpecialFunctionKeyword::CurrentTime,
precision: Some(3),
..
} = project_expr(&parsed)
else {
panic!("expected CURRENT_TIME(3)");
};
assert!(parse_with("SELECT CURRENT_DATE(1)", crate::ParseConfig::new(Postgres)).is_err());
assert!(matches!(
project_expr(
&parse_with("SELECT current_schema", crate::ParseConfig::new(Postgres))
.expect("bare parses")
),
Expr::SpecialFunction {
keyword: SpecialFunctionKeyword::CurrentSchema,
..
}
));
assert!(matches!(
project_expr(
&parse_with("SELECT current_schema()", crate::ParseConfig::new(Postgres))
.expect("call parses")
),
Expr::Function { .. }
));
}
#[test]
fn special_value_functions_round_trip_through_rendering() {
for sql in [
"SELECT CURRENT_DATE",
"SELECT CURRENT_TIMESTAMP",
"SELECT CURRENT_TIME(3)",
"SELECT LOCALTIMESTAMP(6)",
"SELECT USER",
"SELECT SESSION_USER",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.expect("special value function parses");
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.expect("special value function renders");
assert_eq!(rendered, sql);
}
}
#[test]
fn nullif_requires_exactly_two_arguments() {
let parsed = parse_with("SELECT nullif(a, b)", crate::ParseConfig::new(Postgres))
.expect("NULLIF(a, b) parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a NULLIF function call");
};
assert_eq!(parsed.resolver().resolve(call.name.0[0].sym), "nullif");
assert_eq!(call.args.len(), 2);
for sql in [
"SELECT nullif(1)",
"SELECT nullif(1, 2, 3)",
"SELECT nullif(*)",
"SELECT nullif(DISTINCT a, b)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"{sql} must be rejected"
);
}
let bare =
parse_with("SELECT nullif", crate::ParseConfig::new(Postgres)).expect("bare nullif parses");
assert_eq!(column_name(&bare, project_expr(&bare)), "nullif");
}
#[test]
fn bare_exists_is_a_column_reference() {
let parsed =
parse_with("SELECT exists", crate::ParseConfig::new(Postgres)).expect("bare exists parses");
assert_eq!(column_name(&parsed, project_expr(&parsed)), "exists");
assert!(matches!(
selection_expr(
&parse_with(
"SELECT * FROM t WHERE EXISTS (SELECT 1)",
crate::ParseConfig::new(Postgres)
)
.expect("EXISTS predicate parses"),
),
Expr::Exists { .. }
));
}
#[test]
fn cast_parses_special_postgres_type_productions() {
assert!(matches!(
cast_type(
&parse_with("SELECT CAST(x AS bit)", crate::ParseConfig::new(Postgres))
.expect("bit parses")
),
DataType::Bit {
varying: false,
size: None,
..
}
));
assert!(matches!(
cast_type(
&parse_with(
"SELECT CAST(x AS bit varying(3))",
crate::ParseConfig::new(Postgres)
)
.expect("bit varying parses")
),
DataType::Bit {
varying: true,
size: Some(3),
..
}
));
assert!(matches!(
cast_type(
&parse_with("SELECT CAST(x AS json)", crate::ParseConfig::new(Postgres))
.expect("json parses")
),
DataType::Json { .. }
));
assert!(matches!(
cast_type(
&parse_with("SELECT CAST(x AS uuid)", crate::ParseConfig::new(Postgres))
.expect("uuid parses")
),
DataType::Uuid { .. }
));
assert!(matches!(
cast_type(
&parse_with(
"SELECT UUID '00000000-0000-0000-0000-000000000000'",
crate::ParseConfig::new(Postgres),
)
.expect("uuid typed literal parses")
),
DataType::Uuid { .. }
));
for (sql, expected) in [
("SELECT CAST(x AS nchar)", CharacterTypeName::Nchar),
(
"SELECT CAST(x AS national character)",
CharacterTypeName::NationalCharacter,
),
(
"SELECT CAST(x AS national char)",
CharacterTypeName::NationalChar,
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.expect("national character type parses");
let DataType::Character { spelling, .. } = cast_type(&parsed) else {
panic!("expected a character type for {sql}");
};
assert_eq!(*spelling, expected);
}
assert!(matches!(
cast_type(
&parse_with(
"SELECT CAST(x AS double)",
crate::ParseConfig::new(Postgres)
)
.expect("bare double parses")
),
DataType::UserDefined { .. }
));
assert!(matches!(
cast_type(
&parse_with(
"SELECT CAST(x AS double precision)",
crate::ParseConfig::new(Postgres)
)
.expect("double precision parses")
),
DataType::Double { .. }
));
}
#[test]
fn uuid_type_name_renders_canonical_uppercase() {
for (sql, expected) in [
("SELECT CAST(x AS UUID)", "SELECT CAST(x AS UUID)"),
("SELECT CAST(x AS uuid)", "SELECT CAST(x AS UUID)"),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
assert!(
matches!(cast_type(&parsed), DataType::Uuid { .. }),
"UUID identity for {sql}",
);
let rendered = Renderer::new(PG_EXPR_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, expected, "canonical UUID render for {sql}");
}
}
#[test]
fn in_subquery_predicate_preserves_negation_and_lhs() {
let parsed = parse_with(
"SELECT * FROM t WHERE a NOT IN (SELECT b FROM u)",
crate::ParseConfig::new(TestDialect),
)
.expect("NOT IN subquery predicate parses");
let Expr::InSubquery {
expr,
subquery,
negated,
..
} = selection_expr(&parsed)
else {
panic!("expected a NOT IN subquery predicate");
};
assert!(*negated);
assert_eq!(column_name(&parsed, expr), "a");
assert!(matches!(subquery.body, SetExpr::Select { .. }));
}
#[test]
fn quantified_comparison_parses_any_all_and_some() {
for (sql, expected) in [
(
"SELECT * FROM t WHERE a = ANY (SELECT b FROM u)",
Quantifier::Any,
),
(
"SELECT * FROM t WHERE a < ALL (SELECT b FROM u)",
Quantifier::All,
),
(
"SELECT * FROM t WHERE a <> SOME (SELECT b FROM u)",
Quantifier::Some,
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(TestDialect))
.expect("quantified comparison parses");
let Expr::QuantifiedComparison {
left,
op: _,
quantifier,
subquery,
..
} = selection_expr(&parsed)
else {
panic!("expected a quantified comparison for {sql}");
};
assert_eq!(column_name(&parsed, left), "a");
assert_eq!(*quantifier, expected);
assert!(matches!(subquery.body, SetExpr::Select { .. }));
}
}
#[test]
fn subquery_predicates_bind_at_comparison_precedence() {
let parsed = parse_with(
"SELECT * FROM t WHERE a = ANY (SELECT b) AND c = d",
crate::ParseConfig::new(TestDialect),
)
.expect("quantified comparison and AND parse");
let Expr::BinaryOp {
left,
op: BinaryOperator::And,
right,
..
} = selection_expr(&parsed)
else {
panic!("expected AND at the root");
};
assert!(
matches!(**left, Expr::QuantifiedComparison { .. }),
"left side is the comparison-level quantified predicate",
);
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::Eq(_),
..
}
),
"right side is a regular comparison",
);
}
#[test]
fn quantified_list_parses_scalar_array_operand() {
for (sql, quantifier) in [
("SELECT * FROM t WHERE a = ANY (b)", Quantifier::Any),
("SELECT * FROM t WHERE a = ANY ([1, 2, 3])", Quantifier::Any),
("SELECT * FROM t WHERE a < ALL (b)", Quantifier::All),
("SELECT * FROM t WHERE a <> SOME (b)", Quantifier::Some),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::QuantifiedList {
left,
op: _,
quantifier: parsed_quantifier,
..
} = selection_expr(&parsed)
else {
panic!("expected a quantified-list comparison for {sql}");
};
assert_eq!(column_name(&parsed, left), "a");
assert_eq!(*parsed_quantifier, quantifier);
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn quantified_like_and_arbitrary_operator_parse_and_round_trip() {
for sql in [
"SELECT * FROM t WHERE a LIKE ANY (ARRAY['%a', '%o'])",
"SELECT * FROM t WHERE a NOT LIKE ALL (b)",
"SELECT * FROM t WHERE a ILIKE SOME (b)",
"SELECT * FROM t WHERE a * ANY (b) > 0",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
let like = parse_with(
"SELECT * FROM t WHERE a LIKE ANY (ARRAY['%a'])",
crate::ParseConfig::new(Postgres),
)
.expect("LIKE ANY parses");
assert!(
matches!(selection_expr(&like), Expr::QuantifiedLike { .. }),
"LIKE ANY builds the QuantifiedLike node",
);
let cast = parse_with(
"SELECT * FROM t WHERE 'foo'::text = ANY ((SELECT ARRAY['a']::text[])::text[])",
crate::ParseConfig::new(Postgres),
)
.expect("cast-of-subquery operand parses");
assert!(
matches!(selection_expr(&cast), Expr::QuantifiedList { .. }),
"a (subquery)::type operand builds the list node, not the subquery node",
);
}
#[test]
fn quantified_comparison_dispatch_splits_subquery_from_list_operand() {
let subquery = parse_with(
"SELECT * FROM t WHERE a = ANY (SELECT b FROM u)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("subquery operand parses");
assert!(
matches!(selection_expr(&subquery), Expr::QuantifiedComparison { .. }),
"a leading SELECT keeps the subquery node even where the list form is enabled",
);
let list = parse_with(
"SELECT * FROM t WHERE a = ANY (b)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("list operand parses");
assert!(
matches!(selection_expr(&list), Expr::QuantifiedList { .. }),
"a value operand builds the list node",
);
}
#[test]
fn quantified_list_operand_rejected_without_the_gate() {
assert!(
parse_with(
"SELECT * FROM t WHERE a = ANY (b)",
crate::ParseConfig::new(TestDialect)
)
.is_err(),
"ANSI rejects the list-operand quantified comparison",
);
parse_with(
"SELECT * FROM t WHERE a = ANY (SELECT b FROM u)",
crate::ParseConfig::new(TestDialect),
)
.expect("ANSI still accepts the subquery quantifier");
}
#[test]
fn subquery_predicates_do_not_chain_with_comparisons() {
let err = parse_with(
"SELECT * FROM t WHERE a IN (SELECT b) = c",
crate::ParseConfig::new(TestDialect),
)
.expect_err("IN predicate is non-associative with comparisons");
assert_eq!(err.expected.as_str(), "the end of the comparison");
}
#[test]
fn comparison_parses_as_a_binary_op() {
let parsed =
parse_with("SELECT a < b", crate::ParseConfig::new(TestDialect)).expect("valid expression");
let Expr::BinaryOp {
left,
op: BinaryOperator::Lt,
right,
..
} = project_expr(&parsed)
else {
panic!("expected the binary `a < b`");
};
assert_eq!(column_name(&parsed, left), "a");
assert_eq!(column_name(&parsed, right), "b");
}
#[test]
fn chained_comparison_is_rejected_as_non_associative() {
let err = parse_with("SELECT a < b < c", crate::ParseConfig::new(TestDialect))
.expect_err("comparison operators do not chain");
assert_eq!(err.span, Span::new(13, 14));
let err = parse_with("SELECT a < b < c", crate::ParseConfig::new(Postgres))
.expect_err("PostgreSQL comparison operators do not chain");
assert_eq!(err.span, Span::new(13, 14));
}
#[test]
fn parenthesized_comparisons_reset_non_associative_chain_detection() {
let parsed = parse_with("SELECT (a < b) < c", crate::ParseConfig::new(TestDialect))
.expect("parenthesized left comparison parses");
let Expr::BinaryOp {
left,
op: BinaryOperator::Lt,
right,
..
} = project_expr(&parsed)
else {
panic!("expected the outer comparison");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Lt,
..
}
),
"the parenthesized comparison is the left operand",
);
assert_eq!(column_name(&parsed, right), "c");
let parsed = parse_with("SELECT a < (b < c)", crate::ParseConfig::new(TestDialect))
.expect("parenthesized right comparison parses");
let Expr::BinaryOp {
left,
op: BinaryOperator::Lt,
right,
..
} = project_expr(&parsed)
else {
panic!("expected the outer comparison");
};
assert_eq!(column_name(&parsed, left), "a");
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::Lt,
..
}
),
"the parenthesized comparison is the right operand",
);
}
#[test]
fn parser_reads_comparison_associativity_from_the_dialect() {
let parsed = parse_with(
"SELECT a < b < c",
crate::ParseConfig::new(LEFT_ASSOC_COMPARISON_DIALECT),
)
.expect("left-associative comparison dialect permits chains");
let Expr::BinaryOp {
left,
op: BinaryOperator::Lt,
right,
..
} = project_expr(&parsed)
else {
panic!("expected the outer comparison");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Lt,
..
}
),
"the custom dialect left-associates the comparison chain",
);
assert_eq!(column_name(&parsed, right), "c");
}
#[test]
fn mysql_left_associates_comparison_chains() {
for (sql, op) in [
("SELECT a < b < c", BinaryOperator::Lt),
(
"SELECT a = b = c",
BinaryOperator::Eq(EqualsSpelling::Single),
),
(
"SELECT a <> b <> c",
BinaryOperator::NotEq(NotEqSpelling::AngleBracket),
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::BinaryOp {
left,
op: outer_op,
right,
..
} = project_expr(&parsed)
else {
panic!("{sql}: expected the outer comparison");
};
assert_eq!(*outer_op, op, "{sql}: outer operator");
let Expr::BinaryOp { op: inner_op, .. } = &**left else {
panic!("{sql}: left operand should itself be a binary comparison");
};
assert_eq!(
*inner_op, op,
"{sql}: left operand is the inner `a {op:?} b`"
);
assert_eq!(
column_name(&parsed, right),
"c",
"{sql}: right operand is bare `c`"
);
}
}
#[test]
fn expression_spans_are_recoverable() {
let parsed =
parse_with("SELECT 1 + 2", crate::ParseConfig::new(TestDialect)).expect("valid expression");
assert_eq!(project_expr(&parsed).span(), Span::new(7, 12));
}
#[test]
fn function_call_parses_name_and_arguments() {
let parsed = parse_with(
"SELECT coalesce(a, b, c)",
crate::ParseConfig::new(TestDialect),
)
.expect("function call parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(parsed.resolver().resolve(call.name.0[0].sym), "coalesce");
assert_eq!(call.args.len(), 3);
assert!(call.quantifier.is_none());
assert!(!call.wildcard);
assert!(call.order_by.is_empty());
assert!(call.filter.is_none());
}
#[test]
fn function_call_parses_empty_distinct_and_wildcard_forms() {
let parsed = parse_with("SELECT now()", crate::ParseConfig::new(TestDialect))
.expect("no-arg call parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(call.args.is_empty() && !call.wildcard && call.quantifier.is_none());
let parsed = parse_with("SELECT count(*)", crate::ParseConfig::new(TestDialect))
.expect("count star parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(call.wildcard && call.args.is_empty());
let parsed = parse_with(
"SELECT count(DISTINCT a)",
crate::ParseConfig::new(TestDialect),
)
.expect("distinct aggregate parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(
matches!(call.quantifier, Some(SetQuantifier::Distinct))
&& call.args.len() == 1
&& !call.wildcard
);
}
#[test]
fn function_call_parses_explicit_all_quantifier() {
let parsed = parse_with("SELECT count(ALL a)", crate::ParseConfig::new(TestDialect))
.expect("ALL aggregate parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(
matches!(call.quantifier, Some(SetQuantifier::All))
&& call.args.len() == 1
&& !call.wildcard
);
}
#[test]
fn function_call_rejects_quantifier_with_wildcard() {
assert!(parse_with("SELECT count(ALL *)", crate::ParseConfig::new(TestDialect)).is_err());
}
#[test]
fn function_call_nests_argument_expressions() {
let parsed = parse_with(
"SELECT f(a + 1, g(b))",
crate::ParseConfig::new(TestDialect),
)
.expect("nested args parse");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(matches!(call.args[0].value, Expr::BinaryOp { .. }));
assert!(matches!(call.args[1].value, Expr::Function { .. }));
}
#[test]
fn function_call_span_covers_the_whole_call() {
let parsed =
parse_with("SELECT count(a)", crate::ParseConfig::new(TestDialect)).expect("call parses");
assert_eq!(project_expr(&parsed).span(), Span::new(7, 15));
}
#[test]
fn function_call_parses_order_by_modifier() {
let parsed = parse_with(
"SELECT array_agg(a ORDER BY b DESC)",
crate::ParseConfig::new(TestDialect),
)
.expect("ordered-set aggregate parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.args.len(), 1);
assert_eq!(call.order_by.len(), 1);
assert_eq!(call.order_by[0].asc, Some(false));
assert!(call.filter.is_none());
}
#[test]
fn duckdb_standalone_argument_order_by_parses_and_round_trips() {
for (sql, keys) in [
("SELECT rank(ORDER BY b DESC) OVER w", 1usize),
("SELECT cume_dist(ORDER BY b DESC) OVER w", 1),
("SELECT row_number(ORDER BY b) OVER w", 1),
("SELECT rank(ORDER BY b DESC, c ASC) OVER w", 2),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.expect("standalone ORDER BY parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call for {sql}");
};
assert!(call.args.is_empty(), "no positional argument in {sql}");
assert_eq!(call.order_by.len(), keys, "sort-key count for {sql}");
assert!(call.over.is_some(), "OVER clause retained for {sql}");
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.expect("standalone ORDER BY renders");
assert_eq!(rendered, sql, "round-trips exactly");
}
}
#[test]
fn duckdb_standalone_argument_order_by_rejects_trailing_comma() {
for sql in [
"SELECT rank(ORDER BY b,) OVER w",
"SELECT rank(ORDER BY b DESC, c,) OVER w",
"SELECT rank(ORDER BY b,,) OVER w",
"SELECT sum(a ORDER BY b,) OVER w",
"SELECT sum(a) OVER (ORDER BY b,)",
"SELECT sum(a) OVER (PARTITION BY b,)",
"SELECT a FROM t ORDER BY a,",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_err(),
"a trailing comma in an ORDER BY list must reject: {sql}",
);
}
}
#[test]
fn standalone_argument_order_by_is_duckdb_gated() {
let sql = "SELECT rank(ORDER BY b DESC) OVER w";
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects the standalone in-argument ORDER BY",
);
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL rejects the standalone in-argument ORDER BY",
);
parse_with(
"SELECT array_agg(a ORDER BY b)",
crate::ParseConfig::new(TestDialect),
)
.expect("argument-then-ORDER BY is not gated");
}
#[test]
fn group_concat_separator_parses_and_round_trips() {
let sql = "SELECT group_concat(a ORDER BY b SEPARATOR ',')";
let parsed = parse_with(sql, crate::ParseConfig::new(MYSQL_EXPR_DIALECT))
.expect("GROUP_CONCAT SEPARATOR parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.order_by.len(), 1);
let separator = call.separator.as_ref().expect("a SEPARATOR delimiter");
assert_eq!(separator.kind, LiteralKind::String);
assert_eq!(
Renderer::new(MYSQL_EXPR_DIALECT)
.render_parsed(&parsed)
.expect("SEPARATOR renders"),
sql,
);
}
#[test]
fn ansi_and_postgres_reject_group_concat_separator() {
let sql = "SELECT group_concat(a SEPARATOR ',')";
parse_with(sql, crate::ParseConfig::new(TestDialect))
.expect_err("ANSI has no GROUP_CONCAT SEPARATOR");
parse_with(sql, crate::ParseConfig::new(Postgres))
.expect_err("PostgreSQL has no GROUP_CONCAT SEPARATOR");
}
#[test]
fn function_call_parses_filter_clause() {
let parsed = parse_with(
"SELECT count(*) FILTER (WHERE a)",
crate::ParseConfig::new(TestDialect),
)
.expect("aggregate FILTER parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(call.wildcard);
assert!(call.filter.is_some());
}
#[test]
fn duckdb_filter_omits_where_keyword() {
let parsed = parse_with(
"SELECT sum(x) FILTER (x > 1)",
crate::ParseConfig::new(DuckDb),
)
.expect("DuckDB accepts a keyword-less FILTER body");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(call.filter.is_some());
assert_eq!(call.filter_where, FilterWhereSpelling::Omitted);
let parsed = parse_with(
"SELECT sum(x) FILTER (WHERE x > 1)",
crate::ParseConfig::new(DuckDb),
)
.expect("DuckDB still accepts the standard FILTER (WHERE …)");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.filter_where, FilterWhereSpelling::Where);
for dialect_rejects in [
parse_with(
"SELECT sum(x) FILTER (x > 1)",
crate::ParseConfig::new(Postgres),
)
.is_err(),
parse_with(
"SELECT sum(x) FILTER (x > 1)",
crate::ParseConfig::new(Sqlite),
)
.is_err(),
] {
assert!(
dialect_rejects,
"non-DuckDB dialects require FILTER (WHERE …)"
);
}
for sql in [
"SELECT sum(x) FILTER (x > 1)",
"SELECT sum(x) FILTER (WHERE x > 1)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn bare_filter_after_a_call_is_not_a_bare_alias() {
assert!(
parse_with(
"SELECT count(*) filter",
crate::ParseConfig::new(TestDialect)
)
.is_err(),
"FILTER is AS_LABEL, so it cannot be a bare alias",
);
parse_with(
"SELECT count(*) filter (WHERE a)",
crate::ParseConfig::new(TestDialect),
)
.expect("FILTER (WHERE ...) is the aggregate filter clause");
let parsed = parse_with(
"SELECT count(*) AS filter",
crate::ParseConfig::new(TestDialect),
)
.expect("AS filter is a valid explicit alias");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let SelectItem::Expr {
expr: Expr::Function { call, .. },
alias: Some(alias),
..
} = &select.projection[0]
else {
panic!("expected an aliased function call");
};
assert!(call.filter.is_none());
assert_eq!(parsed.resolver().resolve(alias.sym), "filter");
}
#[test]
fn function_call_parses_within_group_ordered_set() {
let parsed = parse_with(
"SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY x DESC)",
crate::ParseConfig::new(TestDialect),
)
.expect("WITHIN GROUP ordered-set aggregate parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.args.len(), 1);
assert!(call.order_by.is_empty());
let within_group = call.within_group.as_ref().expect("WITHIN GROUP present");
assert_eq!(within_group.len(), 1);
assert_eq!(within_group[0].asc, Some(false));
}
#[test]
fn within_group_precedes_filter_and_over() {
let parsed = parse_with(
"SELECT rank(a) WITHIN GROUP (ORDER BY b) FILTER (WHERE c) OVER w",
crate::ParseConfig::new(TestDialect),
)
.expect("WITHIN GROUP composes before FILTER and OVER");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(call.within_group.is_some());
assert!(call.filter.is_some());
assert!(call.over.is_some());
}
#[test]
fn filter_before_within_group_is_rejected() {
assert!(
parse_with(
"SELECT rank(a) FILTER (WHERE c) WITHIN GROUP (ORDER BY b)",
crate::ParseConfig::new(TestDialect),
)
.is_err(),
"WITHIN GROUP must precede FILTER, matching PostgreSQL",
);
}
#[test]
fn within_group_requires_the_group_keyword() {
assert!(
parse_with(
"SELECT count(x) WITHIN (ORDER BY y)",
crate::ParseConfig::new(TestDialect)
)
.is_err(),
"a bare WITHIN without GROUP does not open the ordered-set clause",
);
}
#[test]
fn within_group_rejects_distinct_and_in_paren_order_by() {
assert!(
parse_with(
"SELECT array_agg(x ORDER BY y) WITHIN GROUP (ORDER BY z)",
crate::ParseConfig::new(TestDialect),
)
.is_err(),
"an in-parenthesis ORDER BY cannot combine with WITHIN GROUP",
);
assert!(
parse_with(
"SELECT count(DISTINCT x) WITHIN GROUP (ORDER BY y)",
crate::ParseConfig::new(TestDialect),
)
.is_err(),
"a WITHIN GROUP ordered-set aggregate cannot be DISTINCT",
);
parse_with(
"SELECT count(ALL x) WITHIN GROUP (ORDER BY y)",
crate::ParseConfig::new(TestDialect),
)
.expect("ALL composes with WITHIN GROUP");
}
#[test]
fn function_call_parses_over_inline_window() {
let parsed = parse_with(
"SELECT sum(a) OVER (PARTITION BY b, c ORDER BY d DESC)",
crate::ParseConfig::new(TestDialect),
)
.expect("window function parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let Some(WindowSpec::Inline { definition, .. }) = &call.over else {
panic!("expected an inline OVER window");
};
assert!(definition.existing.is_none());
assert_eq!(definition.partition_by.len(), 2);
assert_eq!(definition.order_by.len(), 1);
assert_eq!(definition.order_by[0].asc, Some(false));
assert!(definition.frame.is_none());
}
#[test]
fn function_call_parses_over_named_window() {
let parsed = parse_with(
"SELECT count(*) OVER w",
crate::ParseConfig::new(TestDialect),
)
.expect("named window parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let Some(WindowSpec::Named { name, .. }) = &call.over else {
panic!("expected a named OVER window");
};
assert_eq!(parsed.resolver().resolve(name.sym), "w");
}
#[test]
fn window_frame_parses_between_bounds_and_exclusion() {
let parsed = parse_with(
"SELECT avg(a) OVER (ORDER BY b ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE TIES)",
crate::ParseConfig::new(TestDialect),
)
.expect("framed window parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let Some(WindowSpec::Inline { definition, .. }) = &call.over else {
panic!("expected an inline OVER window");
};
let frame = definition.frame.as_ref().expect("a frame clause");
assert!(matches!(frame.units, WindowFrameUnits::Rows));
assert!(matches!(frame.start, WindowFrameBound::Preceding { .. }));
assert!(matches!(
frame.end,
Some(WindowFrameBound::UnboundedFollowing { .. })
));
assert!(matches!(frame.exclusion, Some(WindowFrameExclusion::Ties)));
}
#[test]
fn window_frame_parses_bare_current_row_bound() {
let sql = "SELECT avg(a) OVER (ORDER BY b RANGE CURRENT ROW)";
let parsed =
parse_with(sql, crate::ParseConfig::new(TestDialect)).expect("bare frame bound parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let Some(WindowSpec::Inline { definition, .. }) = &call.over else {
panic!("expected an inline OVER window");
};
let frame = definition.frame.as_ref().expect("a frame clause");
assert!(matches!(frame.units, WindowFrameUnits::Range));
assert!(matches!(frame.start, WindowFrameBound::CurrentRow { .. }));
let current_row_start = sql.find("CURRENT").expect("test SQL contains CURRENT") as u32;
assert_eq!(
frame.start.span(),
Span::new(
current_row_start,
current_row_start + "CURRENT ROW".len() as u32
),
);
assert!(frame.end.is_none());
assert!(frame.exclusion.is_none());
}
#[test]
fn window_frame_word_led_offset_is_a_value_expression() {
for sql in [
"SELECT sum(u) OVER (ROWS BETWEEN unbounded(1) PRECEDING AND unbounded(1) FOLLOWING) FROM t",
"SELECT sum(u) OVER (ROWS BETWEEN unbounded.x PRECEDING AND unbounded.x FOLLOWING) FROM t",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call: {sql}");
};
let Some(WindowSpec::Inline { definition, .. }) = &call.over else {
panic!("expected an inline OVER window: {sql}");
};
let frame = definition.frame.as_ref().expect("a frame clause");
assert!(
matches!(frame.start, WindowFrameBound::Preceding { .. }),
"start must be a value-offset PRECEDING, not the UNBOUNDED sentinel: {sql}",
);
assert!(
matches!(frame.end, Some(WindowFrameBound::Following { .. })),
"end must be a value-offset FOLLOWING, not the UNBOUNDED sentinel: {sql}",
);
}
let parsed = parse_with(
"SELECT sum(u) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t",
crate::ParseConfig::new(Postgres),
)
.expect("sentinel frame still parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let Some(WindowSpec::Inline { definition, .. }) = &call.over else {
panic!("expected an inline OVER window");
};
let frame = definition.frame.as_ref().expect("a frame clause");
assert!(matches!(
frame.start,
WindowFrameBound::UnboundedPreceding { .. }
));
assert!(matches!(
frame.end,
Some(WindowFrameBound::CurrentRow { .. })
));
}
#[test]
fn window_frame_rejects_impossible_bound_ordering() {
for sql in [
"SELECT count(*) OVER (ORDER BY b RANGE UNBOUNDED FOLLOWING)",
"SELECT count(*) OVER (ORDER BY b RANGE BETWEEN UNBOUNDED FOLLOWING AND UNBOUNDED FOLLOWING)",
"SELECT count(*) OVER (ORDER BY b RANGE BETWEEN UNBOUNDED FOLLOWING AND UNBOUNDED PRECEDING)",
"SELECT count(*) OVER (ORDER BY b RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED PRECEDING)",
"SELECT count(*) OVER (ORDER BY b RANGE BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING)",
"SELECT count(*) OVER (ORDER BY b ROWS BETWEEN CURRENT ROW AND 1 PRECEDING)",
"SELECT count(*) OVER (ORDER BY b ROWS BETWEEN 1 FOLLOWING AND CURRENT ROW)",
"SELECT count(*) OVER (ORDER BY b ROWS BETWEEN 1 FOLLOWING AND 1 PRECEDING)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_err(),
"an impossibly-ordered frame must reject at parse: {sql}",
);
}
for sql in [
"SELECT count(*) OVER (ORDER BY b RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)",
"SELECT count(*) OVER (ORDER BY b ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)",
"SELECT count(*) OVER (ORDER BY b ROWS BETWEEN 1 PRECEDING AND 2 PRECEDING)",
"SELECT count(*) OVER (ORDER BY b ROWS BETWEEN 2 FOLLOWING AND 1 FOLLOWING)",
"SELECT count(*) OVER (ORDER BY b RANGE UNBOUNDED PRECEDING)",
"SELECT count(*) OVER (ORDER BY b RANGE CURRENT ROW)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_ok(),
"a validly-ordered frame must still parse: {sql}",
);
}
}
#[test]
fn grouping_without_arguments_rejects_under_grouping_set_dialects() {
assert!(
parse_with("SELECT GROUPING()", crate::ParseConfig::new(Postgres)).is_err(),
"GROUPING() with no arguments must reject under a grouping-set dialect",
);
assert!(
parse_with("SELECT GROUPING()", crate::ParseConfig::new(TestDialect)).is_err(),
"GROUPING() with no arguments must reject under a grouping-set dialect",
);
assert!(
parse_with("SELECT GROUPING()", crate::ParseConfig::new(MySql)).is_ok(),
"GROUPING() is an ordinary empty call where grouping-set constructs are off",
);
assert!(
parse_with("SELECT GROUPING(a)", crate::ParseConfig::new(Postgres)).is_ok(),
"GROUPING with an argument must parse",
);
assert!(
parse_with("SELECT \"grouping\"()", crate::ParseConfig::new(Postgres)).is_ok(),
"a quoted grouping() is an ordinary call, not the GROUPING special form",
);
}
#[test]
fn select_window_clause_defines_named_windows() {
let parsed = parse_with(
"SELECT count(*) OVER w FROM t WINDOW w AS (PARTITION BY a ORDER BY b)",
crate::ParseConfig::new(TestDialect),
)
.expect("WINDOW clause parses");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
assert_eq!(select.windows.len(), 1);
assert_eq!(parsed.resolver().resolve(select.windows[0].name.sym), "w");
assert_eq!(select.windows[0].definition.partition_by.len(), 1);
assert_eq!(select.windows[0].definition.order_by.len(), 1);
}
#[test]
fn over_definition_extends_a_base_window() {
let parsed = parse_with(
"SELECT count(*) OVER (w ORDER BY b) FROM t WINDOW w AS (PARTITION BY a)",
crate::ParseConfig::new(TestDialect),
)
.expect("base-window reference parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let Some(WindowSpec::Inline { definition, .. }) = &call.over else {
panic!("expected an inline OVER window");
};
let existing = definition.existing.as_ref().expect("a base window name");
assert_eq!(parsed.resolver().resolve(existing.sym), "w");
assert_eq!(definition.order_by.len(), 1);
}
#[test]
fn frame_keywords_stay_usable_as_identifiers() {
let parsed = parse_with(
"SELECT partition, range, preceding FROM rows",
crate::ParseConfig::new(TestDialect),
)
.expect("non-reserved window keywords parse as identifiers");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query statement");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
assert_eq!(select.projection.len(), 3);
assert_eq!(select.from.len(), 1);
}
#[test]
fn non_reserved_keyword_after_paren_parses_as_column_not_values_row() {
let parsed = parse_with(
"SELECT values AS values FROM t WHERE ((values + 1) > 3)",
crate::ParseConfig::new(TestDialect),
)
.expect("a parenthesized non-reserved keyword parses as a column reference");
let Expr::BinaryOp {
left,
op: BinaryOperator::Gt,
..
} = selection_expr(&parsed)
else {
panic!("expected the `>` comparison at the WHERE root");
};
let Expr::BinaryOp {
left: inner_left,
op: BinaryOperator::Plus,
..
} = &**left
else {
panic!("expected the grouped `values + 1` on the left of `>`");
};
assert_eq!(column_name(&parsed, inner_left), "values");
}
#[test]
fn non_reserved_keyword_in_an_in_list_parses_as_column_not_values_subquery() {
let parsed = parse_with(
"SELECT a FROM t WHERE x IN (values, y)",
crate::ParseConfig::new(TestDialect),
)
.expect("a non-reserved keyword in an IN list parses as a column reference");
let Expr::InList {
list,
negated: false,
..
} = selection_expr(&parsed)
else {
panic!("expected an IN-list predicate");
};
assert_eq!(list.len(), 2);
assert_eq!(column_name(&parsed, &list[0]), "values");
assert_eq!(column_name(&parsed, &list[1]), "y");
let parsed = parse_with(
"SELECT a FROM t WHERE x IN (VALUES (1), (2))",
crate::ParseConfig::new(TestDialect),
)
.expect("a VALUES constructor still parses as an IN subquery");
assert!(matches!(selection_expr(&parsed), Expr::InSubquery { .. }));
}
#[test]
fn window_is_reserved_but_over_is_not() {
assert!(parse_with("SELECT a FROM window", crate::ParseConfig::new(TestDialect)).is_err());
parse_with("SELECT over FROM t", crate::ParseConfig::new(TestDialect))
.expect("OVER is a usable column name");
parse_with("SELECT a FROM over", crate::ParseConfig::new(TestDialect))
.expect("OVER is a usable table name");
}
#[test]
fn searched_case_parses_with_when_then_else() {
let parsed = parse_with(
"SELECT CASE WHEN a THEN b WHEN c THEN d ELSE e END",
crate::ParseConfig::new(TestDialect),
)
.expect("searched CASE parses");
let Expr::Case { case, .. } = project_expr(&parsed) else {
panic!("expected a CASE expression");
};
assert!(case.operand.is_none());
assert_eq!(case.when_clauses.len(), 2);
assert!(case.else_result.is_some());
assert_eq!(column_name(&parsed, &case.when_clauses[0].condition), "a");
assert_eq!(column_name(&parsed, &case.when_clauses[0].result), "b");
}
#[test]
fn simple_case_parses_with_operand() {
let parsed = parse_with(
"SELECT CASE a WHEN 1 THEN b END",
crate::ParseConfig::new(TestDialect),
)
.expect("simple CASE parses");
let Expr::Case { case, .. } = project_expr(&parsed) else {
panic!("expected a CASE expression");
};
let operand = case.operand.as_ref().expect("simple CASE has an operand");
assert_eq!(column_name(&parsed, operand), "a");
assert_eq!(case.when_clauses.len(), 1);
assert!(case.else_result.is_none());
}
#[test]
fn case_requires_at_least_one_when() {
let err = parse_with("SELECT CASE a END", crate::ParseConfig::new(TestDialect))
.expect_err("CASE with no WHEN is rejected");
assert_eq!(err.expected.as_str(), "`WHEN` after `CASE`");
}
#[test]
fn subscript_on_bare_case_is_rejected_but_parenthesized_is_allowed() {
let err = parse_with(
"SELECT CASE 1 WHEN 1 THEN 2 ELSE 3 END['a']",
crate::ParseConfig::new(Postgres),
)
.expect_err("a subscript on a bare CASE is rejected");
assert_eq!(
err.expected.as_str(),
"`(` around the `CASE` expression before subscripting it"
);
let parsed = parse_with(
"SELECT (CASE 1 WHEN 1 THEN 2 ELSE 3 END)['a']",
crate::ParseConfig::new(Postgres),
)
.expect("a subscript on a parenthesized CASE parses");
assert!(matches!(project_expr(&parsed), Expr::Subscript { .. }));
}
#[test]
fn extract_parses_field_and_source() {
let parsed = parse_with(
"SELECT EXTRACT(year FROM a)",
crate::ParseConfig::new(TestDialect),
)
.expect("EXTRACT parses");
let Expr::Extract { extract, .. } = project_expr(&parsed) else {
panic!("expected an EXTRACT expression");
};
assert_eq!(parsed.resolver().resolve(extract.field.sym), "year");
assert_eq!(column_name(&parsed, &extract.source), "a");
}
#[test]
fn bare_extract_stays_a_column() {
let parsed = parse_with("SELECT extract", crate::ParseConfig::new(TestDialect))
.expect("extract as column parses");
assert_eq!(column_name(&parsed, project_expr(&parsed)), "extract");
}
#[test]
fn is_null_predicate_parses_with_negation() {
let parsed = parse_with(
"SELECT a FROM t WHERE a IS NULL",
crate::ParseConfig::new(TestDialect),
)
.expect("IS NULL parses");
let Expr::IsNull { expr, negated, .. } = selection_expr(&parsed) else {
panic!("expected IS NULL");
};
assert_eq!(column_name(&parsed, expr), "a");
assert!(!negated);
let parsed = parse_with(
"SELECT a FROM t WHERE a IS NOT NULL",
crate::ParseConfig::new(TestDialect),
)
.expect("IS NOT NULL parses");
let Expr::IsNull { negated, .. } = selection_expr(&parsed) else {
panic!("expected IS NOT NULL");
};
assert!(negated);
}
#[test]
fn between_bounds_bind_above_the_separator_and() {
let parsed = parse_with(
"SELECT a FROM t WHERE a BETWEEN 1 AND 2 AND b",
crate::ParseConfig::new(TestDialect),
)
.expect("BETWEEN parses");
let Expr::BinaryOp {
left,
op: BinaryOperator::And,
right,
..
} = selection_expr(&parsed)
else {
panic!("expected a top-level boolean AND");
};
assert!(matches!(**left, Expr::Between { negated: false, .. }));
assert_eq!(column_name(&parsed, right), "b");
}
#[test]
fn not_between_parses_negated() {
let parsed = parse_with(
"SELECT a FROM t WHERE a NOT BETWEEN 1 AND 2",
crate::ParseConfig::new(TestDialect),
)
.expect("NOT BETWEEN parses");
assert!(matches!(
selection_expr(&parsed),
Expr::Between { negated: true, .. }
));
}
#[test]
fn in_value_list_is_distinct_from_in_subquery() {
let parsed = parse_with(
"SELECT a FROM t WHERE a IN (1, 2, 3)",
crate::ParseConfig::new(TestDialect),
)
.expect("IN value list parses");
let Expr::InList {
expr,
list,
negated,
..
} = selection_expr(&parsed)
else {
panic!("expected an IN value list");
};
assert_eq!(column_name(&parsed, expr), "a");
assert_eq!(list.len(), 3);
assert!(!negated);
let parsed = parse_with(
"SELECT a FROM t WHERE a IN (SELECT b FROM u)",
crate::ParseConfig::new(TestDialect),
)
.expect("IN subquery parses");
assert!(matches!(selection_expr(&parsed), Expr::InSubquery { .. }));
}
#[test]
fn not_in_value_list_parses_negated() {
let parsed = parse_with(
"SELECT a FROM t WHERE a NOT IN (1, 2)",
crate::ParseConfig::new(TestDialect),
)
.expect("NOT IN list parses");
assert!(matches!(
selection_expr(&parsed),
Expr::InList { negated: true, .. }
));
}
#[test]
fn double_colon_cast_parses_with_syntax_tag() {
let parsed = parse_with("SELECT a::int", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("`::` cast parses");
let Expr::Cast {
expr,
data_type,
syntax,
..
} = project_expr(&parsed)
else {
panic!("expected a `::` cast");
};
assert_eq!(*syntax, CastSyntax::DoubleColon);
assert_eq!(column_name(&parsed, expr), "a");
assert!(matches!(
**data_type,
DataType::Integer {
spelling: IntegerTypeName::Int,
..
}
));
assert!(
parse_with("SELECT a::int", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects the `::` cast operator",
);
}
#[test]
fn double_colon_cast_binds_tighter_than_arithmetic_and_unary() {
let parsed = parse_with(
"SELECT a::int + b",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("cast in addition parses");
let Expr::BinaryOp {
left,
op: BinaryOperator::Plus,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be `+`");
};
assert!(
matches!(
**left,
Expr::Cast {
syntax: CastSyntax::DoubleColon,
..
}
),
"the cast is the left operand of `+`",
);
let parsed = parse_with("SELECT - a::int", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("unary over cast parses");
let Expr::UnaryOp {
op: UnaryOperator::Minus,
expr,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be unary minus");
};
assert!(
matches!(
**expr,
Expr::Cast {
syntax: CastSyntax::DoubleColon,
..
}
),
"the unary minus wraps the cast",
);
let parsed = parse_with(
"SELECT a::int::text",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("chained casts parse");
let Expr::Cast { expr, .. } = project_expr(&parsed) else {
panic!("expected the outer cast");
};
assert!(
matches!(
**expr,
Expr::Cast {
syntax: CastSyntax::DoubleColon,
..
}
),
"the inner cast is the operand of the outer cast",
);
}
fn project_binary_op(parsed: &Parsed) -> BinaryOperator {
match project_expr(parsed) {
Expr::BinaryOp { op, .. } => op.clone(),
other => panic!("expected a binary expression, got {other:?}"),
}
}
#[test]
fn pg_at_family_operators_parse_to_their_binary_operators() {
for (sql, expected) in [
("SELECT a @> b", BinaryOperator::Contains),
("SELECT a <@ b", BinaryOperator::ContainedBy),
("SELECT a -> b", BinaryOperator::JsonGet),
("SELECT a ->> b", BinaryOperator::JsonGetText),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
assert_eq!(project_binary_op(&parsed), expected, "operator for {sql}");
}
}
#[test]
fn pg_jsonb_operators_parse_to_their_binary_operators() {
for (sql, expected) in [
("SELECT a ? b", BinaryOperator::JsonExists),
("SELECT a ?| b", BinaryOperator::JsonExistsAny),
("SELECT a ?& b", BinaryOperator::JsonExistsAll),
("SELECT a @? b", BinaryOperator::JsonPathExists),
("SELECT a @@ b", BinaryOperator::JsonPathMatch),
("SELECT a #> b", BinaryOperator::JsonExtractPath),
("SELECT a #>> b", BinaryOperator::JsonExtractPathText),
("SELECT a #- b", BinaryOperator::JsonDeletePath),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
assert_eq!(project_binary_op(&parsed), expected, "operator for {sql}");
}
}
#[test]
fn pg_jsonb_operators_round_trip() {
for sql in [
"SELECT a ? b",
"SELECT a ?| b",
"SELECT a ?& b",
"SELECT a @? b",
"SELECT a @@ b",
"SELECT a #> b",
"SELECT a #>> b",
"SELECT a #- b",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "round-trip for {sql}");
}
}
#[test]
fn pg_jsonb_operators_sit_at_the_any_operator_rank() {
let eq = parse_with("SELECT a #> b = c", crate::ParseConfig::new(Postgres)).expect("parses");
let Expr::BinaryOp {
op: BinaryOperator::Eq(_),
left,
..
} = project_expr(&eq)
else {
panic!("`=` should be the root of `a #> b = c`");
};
assert!(matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::JsonExtractPath,
..
}
));
let add = parse_with("SELECT a #> b + c", crate::ParseConfig::new(Postgres)).expect("parses");
let Expr::BinaryOp {
op: BinaryOperator::JsonExtractPath,
right,
..
} = project_expr(&add)
else {
panic!("`#>` should be the root of `a #> b + c`");
};
assert!(matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::Plus,
..
}
));
let chain =
parse_with("SELECT a #> b @@ c", crate::ParseConfig::new(Postgres)).expect("parses");
let Expr::BinaryOp {
op: BinaryOperator::JsonPathMatch,
left,
..
} = project_expr(&chain)
else {
panic!("`@@` should be the root of `a #> b @@ c`");
};
assert!(matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::JsonExtractPath,
..
}
));
}
#[test]
fn pg_hash_minus_munches_ahead_of_bitwise_xor() {
assert!(matches!(
project_binary_op(
&parse_with("SELECT 5#-3", crate::ParseConfig::new(Postgres)).expect("parses")
),
BinaryOperator::JsonDeletePath,
));
assert!(matches!(
project_binary_op(
&parse_with("SELECT 5 # -3", crate::ParseConfig::new(Postgres)).expect("parses")
),
BinaryOperator::BitwiseXor(_),
));
}
#[test]
fn pg_jsonb_operators_are_dialect_gated() {
use crate::dialect::Ansi;
assert!(
parse_with("SELECT a ? b", crate::ParseConfig::new(Ansi)).is_err(),
"`?` is not a jsonb op in ANSI"
);
assert!(
parse_with("SELECT a #> b", crate::ParseConfig::new(Ansi)).is_err(),
"`#>` is not a jsonb op in ANSI"
);
assert!(
parse_with("SELECT a @@ b", crate::ParseConfig::new(Ansi)).is_err(),
"`@@` is not a jsonb op in ANSI"
);
assert!(
parse_with("SELECT ?", crate::ParseConfig::new(Postgres)).is_err(),
"bare `?` rejects in PostgreSQL"
);
}
#[test]
fn pg_at_family_operators_bind_looser_than_arithmetic() {
let parsed = parse_with(
"SELECT a -> b + c",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("json arrow parses");
let Expr::BinaryOp {
op: BinaryOperator::JsonGet,
right,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be `->`");
};
assert!(
matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::Plus,
..
}
),
"`b + c` is the right operand of `->`",
);
}
#[test]
fn pg_at_family_operators_are_left_associative_at_one_level() {
let parsed = parse_with(
"SELECT a @> b <@ c",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("chain parses");
let Expr::BinaryOp {
op: BinaryOperator::ContainedBy,
left,
..
} = project_expr(&parsed)
else {
panic!("the root operator should be `<@`");
};
assert!(
matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Contains,
..
}
),
"`a @> b` is the left operand of `<@`",
);
}
#[test]
fn pg_at_family_operators_round_trip() {
for sql in [
"SELECT a @> b",
"SELECT a <@ b",
"SELECT a -> b",
"SELECT a ->> b",
"SELECT a -> b + c",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(PG_EXPR_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "round-trip for {sql}");
}
}
#[test]
fn pg_at_family_operators_are_inert_without_the_dialect() {
for sql in [
"SELECT a @> b",
"SELECT a <@ b",
"SELECT a -> b",
"SELECT a ->> b",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_err(),
"{sql} must reject without the PostgreSQL operator flags",
);
}
}
#[test]
fn duckdb_composite_type_constructors_parse() {
use crate::ast::{ArrayTypeSpelling, StructTypeSpelling};
let parsed = parse_with(
"SELECT CAST(a AS STRUCT(x INTEGER, y VARCHAR))",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("STRUCT type parses");
let DataType::Struct {
fields,
spelling: StructTypeSpelling::Struct,
..
} = cast_type(&parsed)
else {
panic!("expected a STRUCT data type, got {:?}", cast_type(&parsed));
};
assert_eq!(fields.len(), 2);
assert!(matches!(
&fields[0].ty,
DataType::Integer {
spelling: IntegerTypeName::Integer,
..
}
));
let parsed = parse_with(
"SELECT a::ROW(i BIGINT, j VARCHAR)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("ROW type parses");
assert!(matches!(
cast_type(&parsed),
DataType::Struct {
spelling: StructTypeSpelling::Row,
..
}
));
let parsed = parse_with(
"SELECT a::UNION(i SMALLINT, b VARCHAR)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("UNION type parses");
let DataType::Union { members, .. } = cast_type(&parsed) else {
panic!("expected a UNION data type, got {:?}", cast_type(&parsed));
};
assert_eq!(members.len(), 2);
let parsed = parse_with(
"SELECT NULL::MAP(VARCHAR, INTEGER)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("MAP type parses");
assert!(matches!(cast_type(&parsed), DataType::Map { .. }));
let parsed = parse_with(
"SELECT NULL::MAP(INTEGER[], STRUCT(x INTEGER[]))",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("nested composite parses");
let DataType::Map { key, value, .. } = cast_type(&parsed) else {
panic!("expected a MAP data type");
};
assert!(matches!(&**key, DataType::Array { .. }));
assert!(matches!(&**value, DataType::Struct { .. }));
let parsed = parse_with(
"SELECT CAST(a AS INTEGER[3])",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("fixed-size array parses");
assert!(matches!(
cast_type(&parsed),
DataType::Array {
size: Some(3),
spelling: ArrayTypeSpelling::Bracket,
..
}
));
let parsed = parse_with(
"SELECT CAST(a AS INTEGER ARRAY[3])",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("keyword fixed-size array parses");
assert!(matches!(
cast_type(&parsed),
DataType::Array {
size: Some(3),
spelling: ArrayTypeSpelling::Keyword,
..
}
));
let parsed = parse_with(
"SELECT CAST(a AS INTEGER[])",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("list array parses");
assert!(matches!(
cast_type(&parsed),
DataType::Array {
size: None,
spelling: ArrayTypeSpelling::Bracket,
..
}
));
}
#[test]
fn duckdb_try_cast_parses_as_cast_with_try_flag() {
let parsed = parse_with(
"SELECT TRY_CAST(a AS INTEGER)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("TRY_CAST parses");
assert!(matches!(
project_expr(&parsed),
Expr::Cast {
try_cast: true,
syntax: CastSyntax::Call,
..
}
));
let parsed = parse_with(
"SELECT CAST(a AS INTEGER)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("CAST parses");
assert!(matches!(
project_expr(&parsed),
Expr::Cast {
try_cast: false,
..
}
));
let parsed = parse_with(
"SELECT try_cast",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("bare try_cast is a name");
assert!(matches!(project_expr(&parsed), Expr::Column { .. }));
}
#[test]
fn duckdb_composite_types_and_try_cast_round_trip() {
for sql in [
"SELECT CAST(a AS STRUCT(x INTEGER, y VARCHAR))",
"SELECT a::ROW(i BIGINT, j VARCHAR)",
"SELECT a::UNION(i SMALLINT, b VARCHAR)",
"SELECT NULL::MAP(VARCHAR, INTEGER)",
"SELECT NULL::MAP(INTEGER[], STRUCT(x INTEGER[]))",
"SELECT CAST(a AS INTEGER[3])",
"SELECT CAST(a AS INTEGER ARRAY[3])",
"SELECT CAST(a AS INTEGER[])",
"SELECT CAST(a AS INTEGER ARRAY)",
"SELECT CAST(a AS STRUCT(a INTEGER)[])",
"SELECT TRY_CAST(a AS INTEGER)",
"SELECT TRY_CAST(a AS INTEGER[3])",
"SELECT (a::INTEGER[])[1]",
"SELECT (a::ROW(i BIGINT, j VARCHAR))['i']",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "byte round-trip for {sql}");
}
}
#[test]
fn duckdb_positional_column_reference_parses_and_round_trips() {
let parsed =
parse_with("SELECT #1", crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).expect("#1 parses");
let Expr::PositionalColumn { index, .. } = project_expr(&parsed) else {
panic!("expected a positional column reference");
};
assert_eq!(*index, 1);
for sql in [
"SELECT #1",
"SELECT #1 + #2",
"SELECT a, b FROM t ORDER BY #2, #1",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "byte round-trip for {sql}");
}
assert!(parse_with("SELECT #0", crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_err());
assert!(parse_with("SELECT #", crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_err());
}
#[test]
fn positional_column_reference_rejects_without_the_dialect() {
assert!(parse_with("SELECT #1", crate::ParseConfig::new(TestDialect)).is_err());
let parsed = parse_with("SELECT #1", crate::ParseConfig::new(Postgres))
.expect("`#1` parses as a prefix operator");
assert!(
matches!(project_expr(&parsed), Expr::PrefixOperator { .. }),
"`#1` under PostgreSQL is a prefix `#` operator, not a positional column",
);
}
#[test]
fn duckdb_composite_types_and_try_cast_reject_without_the_dialect() {
for sql in [
"SELECT a::STRUCT(x INTEGER)",
"SELECT a::ROW(i BIGINT)",
"SELECT a::UNION(i SMALLINT)",
"SELECT a::MAP(VARCHAR, INTEGER)",
"SELECT TRY_CAST(a AS INTEGER)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"{sql} must reject under the PostgreSQL-like dialect (composite_types / try_cast off)",
);
}
let parsed = parse_with(
"SELECT CAST(a AS structish)",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("bare name is a UDT");
assert!(matches!(cast_type(&parsed), DataType::UserDefined { .. }));
}
#[test]
fn prefix_typed_literal_parses_as_cast_with_syntax_tag() {
let parsed = parse_with(
"SELECT float8 'NaN'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("typed literal parses");
let Expr::Cast {
expr,
data_type,
syntax,
..
} = project_expr(&parsed)
else {
panic!("expected a typed-literal cast");
};
assert_eq!(*syntax, CastSyntax::PrefixTyped);
let Expr::Literal { literal, .. } = &**expr else {
panic!("the operand is a string constant");
};
assert!(matches!(literal.kind, LiteralKind::String));
assert_eq!(
literal
.as_str(parsed.source())
.expect("string materializes"),
"NaN"
);
let DataType::UserDefined { name, .. } = &**data_type else {
panic!("float8 is a user-defined type name");
};
assert_eq!(parsed.resolver().resolve(name.0[0].sym), "float8");
}
#[test]
fn prefix_typed_literal_parses_arbitrary_type_names() {
let cases: &[(&str, &str)] = &[
("SELECT int4 '42'", "42"),
("SELECT bool 'true'", "true"),
("SELECT real 'Infinity'", "Infinity"),
("SELECT double precision '1.5'", "1.5"),
("SELECT pg_catalog.float8 'NaN'", "NaN"),
];
for (sql, value) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|e| panic!("{sql} parses: {e:?}"));
let Expr::Cast { expr, syntax, .. } = project_expr(&parsed) else {
panic!("{sql} is a typed-literal cast");
};
assert_eq!(*syntax, CastSyntax::PrefixTyped, "{sql}");
let Expr::Literal { literal, .. } = &**expr else {
panic!("{sql} operand is a string constant");
};
assert_eq!(
literal.as_str(parsed.source()).expect("materializes"),
*value,
"{sql}",
);
}
let real = parse_with(
"SELECT real 'Infinity'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("real parses");
assert!(matches!(cast_type(&real), DataType::Real { .. }));
let double = parse_with(
"SELECT double precision '1.5'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("double parses");
assert!(matches!(cast_type(&double), DataType::Double { .. }));
}
#[test]
fn prefix_typed_literal_shares_one_shape_with_colon_and_call() {
let parsed = parse_with(
"SELECT float8 'NaN', 'NaN'::float8, CAST('NaN' AS float8)",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("all three spellings parse");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT");
};
let cast = |item: &SelectItem<NoExt>| match item {
SelectItem::Expr {
expr:
Expr::Cast {
expr,
data_type,
syntax,
..
},
..
} => (expr.clone(), data_type.clone(), *syntax),
other => panic!("expected a cast, got {other:?}"),
};
let (prefix_expr, prefix_type, prefix_syntax) = cast(&select.projection[0]);
let (colon_expr, colon_type, colon_syntax) = cast(&select.projection[1]);
let (call_expr, call_type, call_syntax) = cast(&select.projection[2]);
assert_eq!(prefix_expr, colon_expr);
assert_eq!(prefix_expr, call_expr);
assert_eq!(prefix_type, colon_type);
assert_eq!(prefix_type, call_type);
assert_eq!(prefix_syntax, CastSyntax::PrefixTyped);
assert_eq!(colon_syntax, CastSyntax::DoubleColon);
assert_eq!(call_syntax, CastSyntax::Call);
}
#[test]
fn prefix_typed_literal_folds_adjacent_string_continuation() {
let parsed = parse_with(
"SELECT float8 'x'\n'y'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("continuation parses");
let Expr::Cast { expr, syntax, .. } = project_expr(&parsed) else {
panic!("expected a typed-literal cast");
};
assert_eq!(*syntax, CastSyntax::PrefixTyped);
let Expr::Literal { literal, .. } = &**expr else {
panic!("operand is a string constant");
};
assert_eq!(literal.as_str(parsed.source()).expect("materializes"), "xy");
}
#[test]
fn prefix_typed_literal_disambiguation_and_rejects() {
assert!(parse_with("SELECT float8 42", crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err());
assert!(
parse_with(
"SELECT float8 'x' 'y'",
crate::ParseConfig::new(PG_EXPR_DIALECT)
)
.is_err()
);
let aliased = parse_with("SELECT a b", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("implicit alias parses");
let Statement::Query { query, .. } = &aliased.statements()[0] else {
panic!("expected a query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT");
};
assert!(matches!(
&select.projection[0],
SelectItem::Expr {
expr: Expr::Column { .. },
alias: Some(_),
..
}
));
let call = parse_with(
"SELECT count('x')",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("call parses");
assert!(matches!(project_expr(&call), Expr::Function { .. }));
let col = parse_with(
"SELECT float8 FROM t",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("bare name parses");
assert_eq!(column_name(&col, project_expr(&col)), "float8");
}
#[test]
fn prefix_typed_literal_value_must_be_an_sconst() {
for head in [
"float8",
"char(1)",
"left(1)",
"pg_catalog.float8",
"DATE",
"TIMESTAMP",
"TIME",
"INTERVAL",
] {
for value in ["B'1'", "X'ab'"] {
let sql = format!("SELECT {head} {value}");
assert!(
parse_with(&sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"{sql} must reject: the typed-literal value is not an Sconst"
);
}
let sql = format!("SELECT {head} 'x'");
assert!(
parse_with(&sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_ok(),
"{sql} must parse: a plain Sconst is a valid typed-literal value"
);
}
}
#[test]
fn parameterized_typed_literal_parses_over_modifier_list() {
let char20 = parse_with(
"SELECT char(20) 'chars'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("char(20) parses");
let Expr::Cast {
expr,
data_type,
syntax,
try_cast,
..
} = project_expr(&char20)
else {
panic!("expected a typed-literal cast");
};
assert_eq!(*syntax, CastSyntax::PrefixTyped);
assert!(!try_cast);
assert!(matches!(
&**data_type,
DataType::Character { size: Some(20), .. }
));
let Expr::Literal { literal, .. } = &**expr else {
panic!("operand is the string constant");
};
assert_eq!(
literal.as_str(char20.source()).expect("materializes"),
"chars"
);
for (sql, name) in [("SELECT foo(1) 'x'", "foo"), ("SELECT left(1) 'x'", "left")] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|e| panic!("{sql}: {e:?}"));
let Expr::Cast {
data_type, syntax, ..
} = project_expr(&parsed)
else {
panic!("{sql} is a typed-literal cast");
};
assert_eq!(*syntax, CastSyntax::PrefixTyped, "{sql}");
let DataType::UserDefined {
name: type_name,
modifiers,
..
} = &**data_type
else {
panic!("{sql} target is a user-defined type name");
};
assert_eq!(parsed.resolver().resolve(type_name.0[0].sym), name, "{sql}");
assert_eq!(modifiers.len(), 1, "{sql} carries the modifier");
assert_eq!(
modifiers[0]
.as_i64(parsed.source())
.expect("integer modifier"),
1,
"{sql} carries the modifier"
);
}
for sql in [
"SELECT char(20) 'chars'",
"SELECT numeric(10, 2) 'x'",
"SELECT bit(4) 'x'",
"SELECT foo(1) 'x'",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|e| panic!("{sql}: {e:?}"));
let rendered = Renderer::new(PG_EXPR_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|e| panic!("{sql}: {e}"));
let reparsed = parse_with(&rendered, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|e| panic!("{rendered}: {e:?}"));
assert!(
matches!(
project_expr(&reparsed),
Expr::Cast {
syntax: CastSyntax::PrefixTyped,
..
}
),
"render {rendered:?} round-trips to a prefix-typed cast",
);
}
}
#[test]
fn parameterized_typed_literal_boundary_and_gating() {
for sql in [
"SELECT substring(a) 'x'",
"SELECT coalesce(1) 'x'",
"SELECT int(4) 'x'",
"SELECT integer(4) 'x'",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"{sql} rejects"
);
}
for sql in [
"SELECT foo(1)",
"SELECT substring(a, 1)",
"SELECT left(a, 1)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|e| panic!("{sql}: {e:?}"));
assert!(
matches!(project_expr(&parsed), Expr::Function { .. }),
"{sql} stays a function call",
);
}
for sql in ["SELECT char(20) 'chars'", "SELECT foo(1) 'x'"] {
let parsed = parse_with(sql, crate::ParseConfig::new(Sqlite))
.unwrap_or_else(|e| panic!("{sql} parses under SQLite: {e:?}"));
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
let SelectItem::Expr {
expr,
alias: Some(_),
..
} = &select.projection[0]
else {
panic!(
"{sql} is a bare-aliased call, got {:?}",
select.projection[0]
);
};
assert!(
matches!(expr, Expr::Function { .. }),
"{sql} is a `Function` aliased by the string, not a typed-string literal",
);
}
}
#[test]
fn subscript_parses_index_and_slice_forms() {
let parsed = parse_with("SELECT a[1]", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("index subscript parses");
let Expr::Subscript { subscript, .. } = project_expr(&parsed) else {
panic!("expected a subscript");
};
assert_eq!(subscript.kind, SubscriptKind::Index);
assert!(subscript.lower.is_some() && subscript.upper.is_none() && subscript.step.is_none());
assert_eq!(column_name(&parsed, &subscript.base), "a");
for (sql, lower, upper) in [
("SELECT a[1:2]", true, true),
("SELECT a[1:]", true, false),
("SELECT a[:2]", false, true),
("SELECT a[:]", false, false),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("slice subscript parses");
let Expr::Subscript { subscript, .. } = project_expr(&parsed) else {
panic!("expected a slice subscript for {sql}");
};
assert_eq!(subscript.kind, SubscriptKind::Slice, "{sql} is a slice");
assert_eq!(subscript.lower.is_some(), lower, "{sql} lower bound");
assert_eq!(subscript.upper.is_some(), upper, "{sql} upper bound");
assert!(subscript.step.is_none(), "{sql} has no step");
}
assert!(parse_with("SELECT a[]", crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err());
assert!(parse_with("SELECT a[1]", crate::ParseConfig::new(TestDialect)).is_err());
assert!(parse_with("SELECT a[1:2:3]", crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err());
assert!(parse_with("SELECT a[1:-:2]", crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err());
}
#[test]
fn duckdb_three_bound_slice_parses_and_round_trips() {
for (sql, lower, upper, step) in [
("SELECT a[1:2:3]", true, true, true),
("SELECT a[:2:3]", false, true, true),
("SELECT a[1:2:]", true, true, false),
("SELECT a[:2:]", false, true, false),
("SELECT a[1:-:3]", true, false, true),
("SELECT a[:-:3]", false, false, true),
("SELECT a[1:-:]", true, false, false),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|e| panic!("{sql}: {e:?}"));
let Expr::Subscript { subscript, .. } = project_expr(&parsed) else {
panic!("expected a stepped slice for {sql}");
};
assert_eq!(subscript.kind, SubscriptKind::SliceWithStep, "{sql} kind");
assert_eq!(subscript.lower.is_some(), lower, "{sql} lower");
assert_eq!(subscript.upper.is_some(), upper, "{sql} upper");
assert_eq!(subscript.step.is_some(), step, "{sql} step");
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|e| panic!("{sql}: {e}"));
assert_eq!(rendered, sql, "round-trip for {sql}");
}
let parsed = parse_with(
"SELECT a[1:-5:2]",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("negative upper parses");
let Expr::Subscript { subscript, .. } = project_expr(&parsed) else {
panic!("expected a stepped slice");
};
assert!(
subscript.upper.is_some(),
"-5 is a bound, not the placeholder"
);
for sql in [
"SELECT a[1::2]",
"SELECT a[::2]",
"SELECT a[1::]",
"SELECT a[-:2:3]",
"SELECT a[1:2:-]",
"SELECT a[1:-]",
"SELECT a[1:2:3:4]",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_err(),
"{sql} must be rejected"
);
}
}
#[test]
fn collate_parses_with_collation_name() {
let parsed = parse_with(
"SELECT a COLLATE \"C\"",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("COLLATE parses");
let Expr::Collate { collate, .. } = project_expr(&parsed) else {
panic!("expected a COLLATE expression");
};
assert_eq!(column_name(&parsed, &collate.expr), "a");
assert_eq!(collate.collation.0.len(), 1);
assert_eq!(parsed.resolver().resolve(collate.collation.0[0].sym), "C");
assert!(
parse_with(
"SELECT a COLLATE \"C\"",
crate::ParseConfig::new(TestDialect)
)
.is_err()
);
}
#[test]
fn sqlite_collate_postfix_binds_above_comparison_across_positions() {
let parsed = parse_with("SELECT a COLLATE nocase", crate::ParseConfig::new(Sqlite))
.expect("SQLite COLLATE parses");
let Expr::Collate { collate, .. } = project_expr(&parsed) else {
panic!("expected a COLLATE expression");
};
assert_eq!(column_name(&parsed, &collate.expr), "a");
assert_eq!(
parsed.resolver().resolve(collate.collation.0[0].sym),
"nocase"
);
let parsed = parse_with(
"SELECT a = b COLLATE nocase",
crate::ParseConfig::new(Sqlite),
)
.expect("precedence parses");
let Expr::BinaryOp {
op: BinaryOperator::Eq(_),
right,
..
} = project_expr(&parsed)
else {
panic!("expected `=` at the root, with COLLATE folded into its right operand");
};
assert!(
matches!(right.as_ref(), Expr::Collate { .. }),
"COLLATE must bind above `=`",
);
parse_with(
"SELECT a FROM t ORDER BY a COLLATE nocase",
crate::ParseConfig::new(Sqlite),
)
.expect("ORDER BY COLLATE parses");
parse_with(
"CREATE INDEX i ON t(a COLLATE nocase)",
crate::ParseConfig::new(Sqlite),
)
.expect("CREATE INDEX key COLLATE parses");
}
#[test]
fn at_time_zone_parses_with_zone_operand() {
let parsed = parse_with(
"SELECT a AT TIME ZONE 'UTC'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("AT TIME ZONE parses");
let Expr::AtTimeZone { at_time_zone, .. } = project_expr(&parsed) else {
panic!("expected an AT TIME ZONE expression");
};
assert_eq!(column_name(&parsed, &at_time_zone.expr), "a");
assert!(matches!(at_time_zone.zone, Expr::Literal { .. }));
assert!(
parse_with(
"SELECT a AT TIME ZONE 'UTC'",
crate::ParseConfig::new(TestDialect)
)
.is_err()
);
}
#[test]
fn semi_structured_access_parses_path_segments_and_round_trips() {
let sql = "SELECT src:customer[0].name";
let parsed = parse_with(sql, crate::ParseConfig::new(SEMI_STRUCTURED_DIALECT))
.expect("semi-structured path parses");
let Expr::SemiStructuredAccess {
semi_structured_access,
..
} = project_expr(&parsed)
else {
panic!("expected a semi-structured access expression");
};
assert_eq!(column_name(&parsed, &semi_structured_access.base), "src");
assert_eq!(semi_structured_access.path.len(), 3);
let [first, second, third] = semi_structured_access.path.as_slice() else {
panic!("expected three path segments");
};
let SemiStructuredPathSegment::Key { key, .. } = first else {
panic!("expected the colon key segment");
};
assert_eq!(parsed.resolver().resolve(key.sym), "customer");
let SemiStructuredPathSegment::Index { index, .. } = second else {
panic!("expected the bracket index segment");
};
assert!(matches!(&**index, Expr::Literal { .. }));
let SemiStructuredPathSegment::Key { key, .. } = third else {
panic!("expected the dotted key segment");
};
assert_eq!(parsed.resolver().resolve(key.sym), "name");
let rendered = Renderer::new(SEMI_STRUCTURED_DIALECT)
.render_parsed(&parsed)
.expect("semi-structured path renders");
assert_eq!(rendered, sql);
}
#[test]
fn semi_structured_access_precedence_and_disambiguation_are_stable() {
let parsed = parse_with(
"SELECT a + src:customer",
crate::ParseConfig::new(SEMI_STRUCTURED_DIALECT),
)
.expect("path parses");
let Expr::BinaryOp {
op: BinaryOperator::Plus,
right,
..
} = project_expr(&parsed)
else {
panic!("expected addition with a path on the right");
};
assert!(matches!(&**right, Expr::SemiStructuredAccess { .. }));
let grouped = parse_with(
"SELECT (a + b):customer",
crate::ParseConfig::new(SEMI_STRUCTURED_DIALECT),
)
.expect("grouped base parses");
let rendered = Renderer::new(SEMI_STRUCTURED_DIALECT)
.render_parsed(&grouped)
.expect("grouped base renders");
assert_eq!(rendered, "SELECT (a + b):customer");
let cast = parse_with(
"SELECT src::TEXT",
crate::ParseConfig::new(SEMI_STRUCTURED_POSTFIX_DIALECT),
)
.expect("double-colon cast parses");
assert!(matches!(project_expr(&cast), Expr::Cast { .. }));
let subscript = parse_with(
"SELECT arr[1:2]",
crate::ParseConfig::new(SEMI_STRUCTURED_POSTFIX_DIALECT),
)
.expect("array slice parses");
let Expr::Subscript { subscript, .. } = project_expr(&subscript) else {
panic!("expected a subscript slice");
};
assert_eq!(subscript.kind, SubscriptKind::Slice);
let r#struct = parse_with(
"SELECT {'customer': src:customer}",
crate::ParseConfig::new(SEMI_STRUCTURED_POSTFIX_DIALECT),
)
.expect("struct key-value colon remains local to the struct parser");
assert!(matches!(project_expr(&r#struct), Expr::Struct { .. }));
}
#[test]
fn semi_structured_access_is_gated_and_conflicts_with_named_colon_parameters() {
assert!(parse_with("SELECT src:customer", crate::ParseConfig::new(TestDialect)).is_err());
assert!(
parse_with(
"SELECT :customer",
crate::ParseConfig::new(PARAMETER_DIALECT)
)
.is_ok()
);
assert_eq!(
FeatureSet::ANSI
.try_with(
FeatureDelta::EMPTY
.expression_syntax(ExpressionSyntax {
semi_structured_access: true,
..ExpressionSyntax::ANSI
})
.parameters(ParameterSyntax {
named_colon: true,
..ParameterSyntax::ANSI
}),
)
.expect_err("colon paths and colon parameters share a token trigger"),
LexicalConflict::ColonParameterVersusSliceBound,
);
}
#[test]
fn array_constructor_parses_elements_and_subquery() {
let parsed = parse_with(
"SELECT ARRAY[1, 2, 3]",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("array elements parse");
let Expr::Array { array, .. } = project_expr(&parsed) else {
panic!("expected an array constructor");
};
let ArrayExpr::Elements {
elements, spelling, ..
} = &**array
else {
panic!("expected an element-list array");
};
assert_eq!(elements.len(), 3);
assert_eq!(*spelling, ArraySpelling::Keyword);
let parsed = parse_with(
"SELECT ARRAY(SELECT 1)",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("array subquery parses");
let Expr::Array { array, .. } = project_expr(&parsed) else {
panic!("expected an array constructor");
};
assert!(matches!(&**array, ArrayExpr::Subquery { .. }));
parse_with("SELECT ARRAY[]", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("empty array parses");
assert!(parse_with("SELECT ARRAY[1]", crate::ParseConfig::new(TestDialect)).is_err());
}
#[test]
fn array_constructor_parses_multidimensional_rows() {
let parsed = parse_with(
"SELECT ARRAY[[1, 2], [3, 4]]",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("2-D array parses");
let Expr::Array { array, .. } = project_expr(&parsed) else {
panic!("expected an array constructor");
};
let ArrayExpr::Elements {
elements, spelling, ..
} = &**array
else {
panic!("expected an element-list array");
};
assert_eq!(*spelling, ArraySpelling::Keyword);
assert_eq!(elements.len(), 2);
let Expr::Array { array: row, .. } = &elements[0] else {
panic!("expected a nested row");
};
assert!(matches!(
&**row,
ArrayExpr::Elements {
spelling: ArraySpelling::Bracket,
elements,
..
} if elements.len() == 2
));
for sql in [
"SELECT ARRAY[[1, 2], [3, 4]]",
"SELECT ARRAY[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]",
"SELECT ARRAY[[1, 2], [3]]",
"SELECT ARRAY[[], []]",
"SELECT ARRAY[1, ARRAY[2, 3]]",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(PG_EXPR_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "round-trip for {sql}");
}
for sql in [
"SELECT ARRAY[[1, 2], 3]",
"SELECT ARRAY[1, [2, 3]]",
"SELECT ARRAY[[1, 2], ARRAY[3, 4]]",
"SELECT ARRAY[ARRAY[1, 2], [3, 4]]",
"SELECT [[1, 2], [3, 4]]",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"must reject {sql}"
);
}
}
const COLLECTION_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
collection_literals: true,
subscript: true,
array_constructor: true,
..ExpressionSyntax::ANSI
}));
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn list_literal_parses_with_bracket_spelling() {
let parsed = parse_with(
"SELECT [1, 2, 3]",
crate::ParseConfig::new(COLLECTION_DIALECT),
)
.expect("list parses");
let Expr::Array { array, .. } = project_expr(&parsed) else {
panic!("expected a list literal");
};
let ArrayExpr::Elements {
elements, spelling, ..
} = &**array
else {
panic!("expected an element-list array");
};
assert_eq!(elements.len(), 3);
assert_eq!(*spelling, ArraySpelling::Bracket);
let parsed = parse_with("SELECT []", crate::ParseConfig::new(COLLECTION_DIALECT))
.expect("empty list parses");
let Expr::Array { array, .. } = project_expr(&parsed) else {
panic!("expected a list literal");
};
assert!(matches!(
&**array,
ArrayExpr::Elements { elements, .. } if elements.is_empty()
));
assert!(
parse_with(
"SELECT [1, 2,]",
crate::ParseConfig::new(COLLECTION_DIALECT)
)
.is_err()
);
}
#[test]
fn duckdb_trailing_comma_is_accepted_in_list_positions() {
use crate::dialect::Ansi;
let normalized_pairs = [
("SELECT 1, 2,", "SELECT 1, 2"),
("SELECT a, b, FROM t", "SELECT a, b FROM t"),
("VALUES (1), (2),", "VALUES (1), (2)"),
("VALUES (1, 2,)", "VALUES (1, 2)"),
(
"SELECT * FROM (VALUES (1, 2,), (3, 4,)) AS t(a, b)",
"SELECT * FROM (VALUES (1, 2), (3, 4)) AS t(a, b)",
),
(
"SELECT a FROM VALUES (1), (2), AS t(a)",
"SELECT a FROM VALUES (1), (2) AS t(a)",
),
(
"INSERT INTO t VALUES (1, 2,)",
"INSERT INTO t VALUES (1, 2)",
),
("SELECT [1, 2,]", "SELECT [1, 2]"),
("SELECT ARRAY[1, 2,]", "SELECT ARRAY[1, 2]"),
("SELECT {'a': 1, 'b': 2,}", "SELECT {'a': 1, 'b': 2}"),
("SELECT MAP {1: 2,}", "SELECT MAP {1: 2}"),
("SELECT 1 IN (1, 2,)", "SELECT 1 IN (1, 2)"),
(
"SELECT a, b FROM t GROUP BY a, b,",
"SELECT a, b FROM t GROUP BY a, b",
),
(
"SELECT a FROM t GROUP BY a, ORDER BY a",
"SELECT a FROM t GROUP BY a ORDER BY a",
),
(
"SELECT a, count(*) FROM t GROUP BY a, HAVING count(*) > 0",
"SELECT a, count(*) FROM t GROUP BY a HAVING count(*) > 0",
),
(
"SELECT * FROM (SELECT a FROM t GROUP BY a,) x",
"SELECT * FROM (SELECT a FROM t GROUP BY a) x",
),
(
"SELECT a, b FROM t GROUP BY ROLLUP(a, b,)",
"SELECT a, b FROM t GROUP BY ROLLUP(a, b)",
),
(
"SELECT a, b FROM t GROUP BY CUBE(a, b,)",
"SELECT a, b FROM t GROUP BY CUBE(a, b)",
),
(
"SELECT a, b FROM t GROUP BY GROUPING SETS ((a), (b),)",
"SELECT a, b FROM t GROUP BY GROUPING SETS ((a), (b))",
),
(
"SELECT * EXCLUDE (a, b,) FROM t",
"SELECT * EXCLUDE (a, b) FROM t",
),
(
"SELECT * REPLACE (b + 1 AS b,) FROM t",
"SELECT * REPLACE (b + 1 AS b) FROM t",
),
(
"SELECT * RENAME (a AS x,) FROM t",
"SELECT * RENAME (a AS x) FROM t",
),
("SELECT coalesce(1, 2,)", "SELECT coalesce(1, 2)"),
("SELECT coalesce(1,)", "SELECT coalesce(1)"),
(
"SELECT DISTINCT ON (a,) a FROM t",
"SELECT DISTINCT ON (a) a FROM t",
),
(
"SELECT DISTINCT ON (a, b,) a FROM t",
"SELECT DISTINCT ON (a, b) a FROM t",
),
];
for (with_tc, without_tc) in normalized_pairs {
let with_rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(
&parse_with(with_tc, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{with_tc}: {err:?}")),
)
.unwrap_or_else(|err| panic!("{with_tc}: {err}"));
let without_rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(
&parse_with(without_tc, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{without_tc}: {err:?}")),
)
.unwrap_or_else(|err| panic!("{without_tc}: {err}"));
assert_eq!(
with_rendered, without_rendered,
"the trailing comma must normalize away for {with_tc:?}",
);
assert!(
parse_with(with_tc, crate::ParseConfig::new(Ansi)).is_err(),
"ANSI must reject the trailing comma in {with_tc:?}",
);
}
for sql in [
"SELECT greatest(1, 2,)",
"SELECT nullif(1, 2,)",
"SELECT \"coalesce\"(1, 2,)",
"SELECT (1, 2,)",
"SELECT a FROM t ORDER BY a,",
"SELECT a, row_number() OVER (PARTITION BY a,) FROM t",
"INSERT INTO t (a, b,) VALUES (1, 2)",
"SELECT 1, 2, ,",
"SELECT [1, 2, ,]",
"SELECT [,]",
"SELECT a, b FROM t GROUP BY a, b,,",
"SELECT * EXCLUDE (a,,) FROM t",
"SELECT coalesce(1, 2,,)",
"SELECT DISTINCT ON (a,,) a FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_err(),
"DuckDB rejects the trailing comma here, so the parser must too: {sql:?}",
);
}
}
#[test]
fn struct_literal_parses_fields_with_each_key_spelling() {
let parsed = parse_with(
"SELECT {'a': 1, b: 2, \"c d\": [3]}",
crate::ParseConfig::new(COLLECTION_DIALECT),
)
.expect("struct parses");
let Expr::Struct { r#struct, .. } = project_expr(&parsed) else {
panic!("expected a struct literal");
};
let keys: Vec<_> = r#struct
.fields
.iter()
.map(|field| (parsed.resolver().resolve(field.key), field.key_spelling))
.collect();
assert_eq!(
keys,
[
("a", StructKeySpelling::SingleQuoted),
("b", StructKeySpelling::Bare),
("c d", StructKeySpelling::DoubleQuoted),
],
);
assert!(matches!(r#struct.fields[2].value, Expr::Array { .. }));
let parsed = parse_with(
"SELECT {'it''s': 1}",
crate::ParseConfig::new(COLLECTION_DIALECT),
)
.expect("escaped key");
let Expr::Struct { r#struct, .. } = project_expr(&parsed) else {
panic!("expected a struct literal");
};
assert_eq!(parsed.resolver().resolve(r#struct.fields[0].key), "it's");
assert!(parse_with("SELECT {}", crate::ParseConfig::new(COLLECTION_DIALECT)).is_err());
assert!(
parse_with(
"SELECT {1: 'x'}",
crate::ParseConfig::new(COLLECTION_DIALECT)
)
.is_err()
);
assert!(
parse_with(
"SELECT {select: 1}",
crate::ParseConfig::new(COLLECTION_DIALECT)
)
.is_err()
);
}
#[test]
fn map_literal_parses_entries_and_bare_map_stays_a_name() {
let parsed = parse_with(
"SELECT MAP {'a': 1, [2]: 'x'}",
crate::ParseConfig::new(COLLECTION_DIALECT),
)
.expect("map literal parses");
let Expr::Map { map, .. } = project_expr(&parsed) else {
panic!("expected a map literal");
};
assert_eq!(map.entries.len(), 2);
assert!(matches!(map.entries[0].key, Expr::Literal { .. }));
assert!(matches!(map.entries[1].key, Expr::Array { .. }));
let parsed = parse_with("SELECT MAP {}", crate::ParseConfig::new(COLLECTION_DIALECT))
.expect("empty map parses");
let Expr::Map { map, .. } = project_expr(&parsed) else {
panic!("expected a map literal");
};
assert!(map.entries.is_empty());
let parsed = parse_with(
"SELECT MAP([1], [2])",
crate::ParseConfig::new(COLLECTION_DIALECT),
)
.expect("call parses");
assert!(matches!(project_expr(&parsed), Expr::Function { .. }));
let parsed = parse_with(
"SELECT map FROM t",
crate::ParseConfig::new(COLLECTION_DIALECT),
)
.expect("column parses");
assert!(matches!(project_expr(&parsed), Expr::Column { .. }));
}
#[test]
fn collection_literals_nest_and_compose_with_subscript() {
let parsed = parse_with(
"SELECT [{'a': 42}, {'b': 84}]",
crate::ParseConfig::new(COLLECTION_DIALECT),
)
.expect("nested collections parse");
let Expr::Array { array, .. } = project_expr(&parsed) else {
panic!("expected a list literal");
};
let ArrayExpr::Elements { elements, .. } = &**array else {
panic!("expected an element-list array");
};
assert!(
elements
.iter()
.all(|element| matches!(element, Expr::Struct { .. }))
);
for (sql, kind) in [
("SELECT [1, 2, 3][2]", SubscriptKind::Index),
("SELECT [1, 2][1:2]", SubscriptKind::Slice),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(COLLECTION_DIALECT))
.expect("subscripted list parses");
let Expr::Subscript { subscript, .. } = project_expr(&parsed) else {
panic!("expected a subscript over the list literal for {sql}");
};
assert_eq!(subscript.kind, kind, "{sql}");
assert!(matches!(subscript.base, Expr::Array { .. }), "{sql}");
}
}
#[test]
fn collection_literals_round_trip_exactly() {
for sql in [
"SELECT [1, 2, 3]",
"SELECT []",
"SELECT {'a': 1, b: 2, \"c d\": 3}",
"SELECT {'it''s': 1}",
"SELECT MAP {'a': 1}",
"SELECT MAP {}",
"SELECT MAP {[1, 2]: 'x'}",
"SELECT [{'a': 42}, {'b': 84}]",
"SELECT [1, 2, 3][2]",
"SELECT ARRAY[1, 2]",
] {
let parsed =
parse_with(sql, crate::ParseConfig::new(COLLECTION_DIALECT)).unwrap_or_else(|err| {
panic!("{sql}: {err:?}");
});
let rendered = Renderer::new(COLLECTION_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn collection_literals_are_dialect_gated() {
for sql in [
"SELECT [1, 2, 3]",
"SELECT [a]",
"SELECT {'a': 1}",
"SELECT MAP {'a': 1}",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects {sql}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL rejects {sql}",
);
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects {sql}"
);
}
}
#[test]
fn bracket_identifiers_do_not_bleed_into_collection_literals() {
use crate::dialect::{DuckDb, Sqlite};
let parsed = parse_with("SELECT [1, 2, 3]", crate::ParseConfig::new(Sqlite))
.expect("bracket identifier parses");
let Expr::Column { name, .. } = project_expr(&parsed) else {
panic!("expected a bracket-quoted column identifier under SQLite");
};
assert_eq!(parsed.resolver().resolve(name.0[0].sym), "1, 2, 3");
assert!(parse_with("SELECT {'a': 1}", crate::ParseConfig::new(Sqlite)).is_err());
assert!(parse_with("SELECT MAP {'a': 1}", crate::ParseConfig::new(Sqlite)).is_err());
let parsed = parse_with("SELECT [1, 2, 3]", crate::ParseConfig::new(DuckDb))
.expect("list literal parses");
assert!(matches!(project_expr(&parsed), Expr::Array { .. }));
}
#[test]
fn overlaps_period_predicate_parses_row_operands() {
let parsed = parse_with(
"SELECT (a, b) OVERLAPS (c, d)",
crate::ParseConfig::new(OVERLAPS_DIALECT),
)
.expect("OVERLAPS parses");
let Expr::BinaryOp {
left, op, right, ..
} = project_expr(&parsed)
else {
panic!("expected a binary OVERLAPS predicate");
};
assert_eq!(*op, BinaryOperator::Overlaps);
assert!(matches!(&**left, Expr::Row { row, .. } if row.fields.len() == 2));
assert!(matches!(&**right, Expr::Row { row, .. } if row.fields.len() == 2));
for sql in [
"SELECT (a, b) OVERLAPS (c, d)",
"SELECT ROW(a, b) OVERLAPS ROW(c, d)",
"SELECT (a, b) OVERLAPS ROW(c, d)",
"SELECT ROW(a, b) OVERLAPS (c, d)",
"SELECT NOT (a, b) OVERLAPS (c, d)",
"SELECT (a, b) OVERLAPS (c, d) AND (e, f) OVERLAPS (g, h)",
"SELECT (a, b) OVERLAPS (c, d) = TRUE",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(OVERLAPS_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(OVERLAPS_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "round-trip for {sql}");
}
for sql in [
"SELECT a OVERLAPS b",
"SELECT (a) OVERLAPS (b)",
"SELECT (a, b, c) OVERLAPS (d, e, f)",
"SELECT (a, b) OVERLAPS (c, d, e)",
"SELECT (a, b) NOT OVERLAPS (c, d)",
"SELECT (a, b) OVERLAPS (c, d) OVERLAPS (e, f)",
"SELECT ((a, b) OVERLAPS (c, d)) OVERLAPS (e, f)",
"SELECT ((a, b)) OVERLAPS (c, d)",
"SELECT (a, b) OVERLAPS ((c, d))",
"SELECT (a, b) OVERLAPS c",
"SELECT a OVERLAPS (c, d)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(OVERLAPS_DIALECT)).is_err(),
"must reject {sql}"
);
}
assert!(
parse_with(
"SELECT (a, b) OVERLAPS (c, d)",
crate::ParseConfig::new(PG_EXPR_DIALECT)
)
.is_err()
);
}
#[test]
fn row_constructor_parses_explicit_and_implicit_forms() {
let parsed = parse_with("SELECT ROW(1, 2)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("explicit ROW parses");
let Expr::Row { row, .. } = project_expr(&parsed) else {
panic!("expected a row constructor");
};
assert!(row.explicit);
assert_eq!(row.fields.len(), 2);
let parsed = parse_with("SELECT (a, b, c)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("implicit row parses");
let Expr::Row { row, .. } = project_expr(&parsed) else {
panic!("expected an implicit row constructor");
};
assert!(!row.explicit);
assert_eq!(row.fields.len(), 3);
let parsed = parse_with("SELECT (a)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("grouping parses");
assert!(matches!(project_expr(&parsed), Expr::Column { .. }));
assert!(parse_with("SELECT (a, b)", crate::ParseConfig::new(TestDialect)).is_err());
}
const LAMBDA_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet = FeatureSet::ANSI.with(
FeatureDelta::EMPTY
.expression_syntax(ExpressionSyntax::POSTGRES)
.operator_syntax(OperatorSyntax {
lambda_expressions: true,
..OperatorSyntax::POSTGRES
})
.call_syntax(CallSyntax::POSTGRES)
.string_func_forms(StringFuncForms::POSTGRES)
.aggregate_call_syntax(AggregateCallSyntax::POSTGRES)
.binding_powers(crate::ast::precedence::STANDARD_BINDING_POWERS.with_binary(
&BinaryOperator::JsonGet,
BindingPower {
left: 4,
right: 5,
assoc: Assoc::Left,
},
)),
);
FeatureDialect {
features: &FEATURES,
}
};
fn project_lambda<'a>(parsed: &'a Parsed, sql: &str) -> &'a crate::ast::LambdaExpr<NoExt> {
match project_expr(parsed) {
Expr::Lambda { lambda, .. } => lambda,
other => panic!("expected a lambda for {sql:?}, got {other:?}"),
}
}
#[test]
fn lambda_parses_each_param_spelling() {
use crate::ast::LambdaParamSpelling;
for (sql, names, spelling) in [
("SELECT x -> x + 1", &["x"][..], LambdaParamSpelling::Bare),
(
"SELECT (x) -> x + 1",
&["x"][..],
LambdaParamSpelling::Parenthesized,
),
(
"SELECT (x, y) -> x + y",
&["x", "y"][..],
LambdaParamSpelling::Parenthesized,
),
(
"SELECT ROW(x, y) -> x + y",
&["x", "y"][..],
LambdaParamSpelling::RowKeyword,
),
] {
let parsed =
parse_with(sql, crate::ParseConfig::new(LAMBDA_DIALECT)).unwrap_or_else(|err| {
panic!("{sql}: {err:?}");
});
let lambda = project_lambda(&parsed, sql);
let params: Vec<_> = lambda
.params
.iter()
.map(|param| parsed.resolver().resolve(param.sym))
.collect();
assert_eq!(params, names, "params for {sql:?}");
assert_eq!(lambda.spelling, spelling, "spelling for {sql:?}");
}
let parsed = parse_with(
"SELECT \"x\" -> \"x\" + 1",
crate::ParseConfig::new(LAMBDA_DIALECT),
)
.expect("quoted param parses");
let lambda = project_lambda(&parsed, "quoted");
assert_eq!(lambda.params[0].quote, crate::ast::QuoteStyle::Double);
}
#[test]
fn lambda_body_captures_the_full_right_expression() {
let parsed = parse_with(
"SELECT x -> x % 2 = 0",
crate::ParseConfig::new(LAMBDA_DIALECT),
)
.expect("lambda body parses");
let lambda = project_lambda(&parsed, "body");
assert!(
matches!(
lambda.body,
Expr::BinaryOp {
op: BinaryOperator::Eq(_),
..
}
),
"the comparison is inside the body, got {:?}",
lambda.body,
);
}
#[test]
fn non_param_shaped_arrows_stay_the_json_operator() {
for sql in [
"SELECT t.a -> 'k'",
"SELECT 1 -> 2",
"SELECT (a + 1) -> b",
"SELECT (a, b.c) -> d",
"SELECT ROW() -> 1",
] {
let parsed =
parse_with(sql, crate::ParseConfig::new(LAMBDA_DIALECT)).unwrap_or_else(|err| {
panic!("{sql}: {err:?}");
});
assert!(
matches!(
project_expr(&parsed),
Expr::BinaryOp {
op: BinaryOperator::JsonGet,
..
}
),
"expected the JSON arrow for {sql:?}, got {:?}",
project_expr(&parsed),
);
}
}
#[test]
fn chained_arrows_left_associate_with_only_the_first_a_lambda() {
let parsed = parse_with(
"SELECT x -> y -> z",
crate::ParseConfig::new(LAMBDA_DIALECT),
)
.expect("chain parses");
let Expr::BinaryOp {
op: BinaryOperator::JsonGet,
left,
..
} = project_expr(&parsed)
else {
panic!(
"expected the outer JSON fold, got {:?}",
project_expr(&parsed)
);
};
assert!(matches!(**left, Expr::Lambda { .. }));
}
#[test]
fn lambda_round_trips_exactly_and_stays_json_without_the_gate() {
for sql in [
"SELECT x -> x + 1",
"SELECT (x) -> x + 1",
"SELECT (x, y) -> x + y",
"SELECT ROW(x, y) -> x + y",
"SELECT \"x\" -> \"x\" + 1",
"SELECT t.a -> 'k'",
"SELECT x -> y -> z",
"SELECT x -> x % 2 = 0",
] {
let parsed =
parse_with(sql, crate::ParseConfig::new(LAMBDA_DIALECT)).unwrap_or_else(|err| {
panic!("{sql}: {err:?}");
});
let rendered = Renderer::new(LAMBDA_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
for sql in ["SELECT x -> x + 1", "SELECT (x, y) -> x + y"] {
let parsed =
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).unwrap_or_else(|err| {
panic!("{sql}: {err:?}");
});
assert!(matches!(
project_expr(&parsed),
Expr::BinaryOp {
op: BinaryOperator::JsonGet,
..
}
));
let rendered = Renderer::new(PG_EXPR_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn field_selection_parses_after_parenthesized_base() {
let parsed = parse_with("SELECT (a).b", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("field selection parses");
let Expr::FieldSelection {
field_selection, ..
} = project_expr(&parsed)
else {
panic!("expected a field selection");
};
assert_eq!(column_name(&parsed, &field_selection.base), "a");
let FieldSelector::Field { field, .. } = &field_selection.selector else {
panic!("expected a named field selector");
};
assert_eq!(parsed.resolver().resolve(field.sym), "b");
let parsed = parse_with("SELECT a.b", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("qualified column parses");
let Expr::Column { name, .. } = project_expr(&parsed) else {
panic!("expected a qualified column");
};
assert_eq!(name.0.len(), 2);
assert!(parse_with("SELECT (a).b", crate::ParseConfig::new(TestDialect)).is_err());
}
#[test]
fn field_wildcard_star_selection_in_value_positions() {
let parsed = parse_with("SELECT (f(x)).*", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("composite star target parses");
let Expr::FieldSelection {
field_selection, ..
} = project_expr(&parsed)
else {
panic!("expected a field selection");
};
assert!(matches!(
field_selection.selector,
FieldSelector::Star { .. }
));
assert!(matches!(field_selection.base, Expr::Function { .. }));
let parsed = parse_with("SELECT ROW(t.*)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("whole-row in ROW parses");
let Expr::Row { row, .. } = project_expr(&parsed) else {
panic!("expected a row constructor");
};
let Expr::FieldSelection {
field_selection, ..
} = &row.fields[0]
else {
panic!("expected a field-wildcard row field");
};
assert!(matches!(
field_selection.selector,
FieldSelector::Star { .. }
));
assert!(matches!(field_selection.base, Expr::Column { .. }));
let parsed = parse_with("SELECT t.*", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("qualified wildcard parses");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
assert!(matches!(
select.projection[0],
SelectItem::QualifiedWildcard { .. }
));
let parsed = parse_with("SELECT f((t).*)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("whole-row arg parses");
let rendered = Renderer::new(PG_EXPR_DIALECT)
.render_parsed(&parsed)
.expect("render");
assert_eq!(rendered, "SELECT f((t).*)");
assert!(parse_with("SELECT (f(x)).*", crate::ParseConfig::new(TestDialect)).is_err());
assert!(parse_with("SELECT ROW(t.*)", crate::ParseConfig::new(TestDialect)).is_err());
}
#[test]
fn typed_temporal_literals_parse_with_kind_and_value_text() {
let date = parse_with(
"SELECT DATE '1998-12-01'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("date literal");
let Expr::Literal { literal, .. } = project_expr(&date) else {
panic!("expected a literal");
};
assert_eq!(literal.kind, LiteralKind::Date);
assert_eq!(
literal.as_temporal_text(date.source()).expect("value text"),
"1998-12-01",
);
let ts = parse_with(
"SELECT TIMESTAMP WITH TIME ZONE '2020-01-01 00:00:00+00'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("timestamp-with-time-zone literal");
let Expr::Literal { literal, .. } = project_expr(&ts) else {
panic!("expected a literal");
};
assert_eq!(
literal.kind,
LiteralKind::Timestamp {
time_zone: TimeZone::WithTimeZone,
},
);
assert_eq!(
literal.as_temporal_text(ts.source()).expect("value text"),
"2020-01-01 00:00:00+00",
);
let time = parse_with(
"SELECT TIME '12:00:00'",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("time literal");
let Expr::Literal { literal, .. } = project_expr(&time) else {
panic!("expected a literal");
};
assert_eq!(
literal.kind,
LiteralKind::Time {
time_zone: TimeZone::Unspecified,
},
);
}
#[test]
fn interval_literal_captures_qualifier_and_precision() {
let cases: [(&str, Option<IntervalFields>, Option<u32>); 6] = [
("SELECT INTERVAL '90' DAY", Some(IntervalFields::Day), None),
(
"SELECT INTERVAL '1-2' YEAR TO MONTH",
Some(IntervalFields::YearToMonth),
None,
),
(
"SELECT INTERVAL '1' DAY TO SECOND",
Some(IntervalFields::DayToSecond),
None,
),
(
"SELECT INTERVAL '1' SECOND(3)",
Some(IntervalFields::Second),
Some(3),
),
("SELECT INTERVAL(6) '1'", None, Some(6)),
("SELECT INTERVAL '1 2:03'", None, None),
];
for (sql, fields, precision) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a literal");
};
assert_eq!(
literal.kind,
LiteralKind::Interval { fields, precision },
"{sql}",
);
}
}
#[test]
fn temporal_keyword_without_trailing_string_falls_back_to_name() {
let parsed = parse_with("SELECT date", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("bare date column");
assert_eq!(column_name(&parsed, project_expr(&parsed)), "date");
let parsed = parse_with("SELECT date(a)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("date(...) call");
assert!(matches!(project_expr(&parsed), Expr::Function { .. }));
}
#[test]
fn temporal_literals_round_trip_exact_source_spelling() {
let cases = [
"SELECT date '1998-12-01' - interval '90' day",
"SELECT TIMESTAMP WITH TIME ZONE '2020-01-01 00:00:00+00'",
"SELECT timestamp without time zone '2020-01-01 00:00:00'",
"SELECT interval '1-2' year to month",
"SELECT interval '1' second(3)",
];
for sql in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn duckdb_relaxed_interval_amount_forms_parse_with_unit_qualifier() {
let cases: [(&str, IntervalFields); 5] = [
("SELECT INTERVAL 1000 DAY", IntervalFields::Day),
("SELECT INTERVAL 3 DAYS", IntervalFields::Day),
("SELECT INTERVAL '1' hours", IntervalFields::Hour),
("SELECT INTERVAL (days) DAY", IntervalFields::Day),
("SELECT INTERVAL (a + 1) MINUTES", IntervalFields::Minute),
];
for (sql, fields) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected an interval literal");
};
assert_eq!(
literal.kind,
LiteralKind::Interval {
fields: Some(fields),
precision: None,
},
"{sql}",
);
}
}
#[test]
fn duckdb_relaxed_interval_spellings_round_trip_exact_source() {
let cases = [
"SELECT interval 1000 day",
"SELECT interval 3 days",
"SELECT interval '1' hours",
"SELECT interval (days) day",
];
for sql in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn duckdb_relaxed_interval_amount_requires_a_unit() {
for sql in ["SELECT INTERVAL 3", "SELECT INTERVAL (days)"] {
assert!(
parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_err(),
"{sql} must be rejected without a unit",
);
}
}
#[test]
fn relaxed_interval_spellings_are_gated_off_by_default() {
for sql in ["SELECT INTERVAL 3 DAYS", "SELECT INTERVAL (days) DAY"] {
assert!(
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"{sql} must be rejected without the relaxed-interval gate",
);
}
assert!(
parse_with(
"SELECT INTERVAL '3' DAY",
crate::ParseConfig::new(PG_EXPR_DIALECT)
)
.is_ok()
);
}
#[test]
fn duckdb_extended_interval_units_parse_singular_and_plural() {
let cases: [(&str, IntervalFields); 14] = [
("SELECT INTERVAL 5 WEEK", IntervalFields::Week),
("SELECT INTERVAL 5 WEEKS", IntervalFields::Week),
("SELECT INTERVAL 5 QUARTER", IntervalFields::Quarter),
("SELECT INTERVAL 5 QUARTERS", IntervalFields::Quarter),
("SELECT INTERVAL 5 DECADE", IntervalFields::Decade),
("SELECT INTERVAL 5 DECADES", IntervalFields::Decade),
("SELECT INTERVAL 5 CENTURY", IntervalFields::Century),
("SELECT INTERVAL 5 CENTURIES", IntervalFields::Century),
("SELECT INTERVAL 5 MILLENNIUM", IntervalFields::Millennium),
("SELECT INTERVAL 5 MILLENNIA", IntervalFields::Millennium),
("SELECT INTERVAL 5 MILLISECOND", IntervalFields::Millisecond),
(
"SELECT INTERVAL 5 MILLISECONDS",
IntervalFields::Millisecond,
),
("SELECT INTERVAL 5 MICROSECOND", IntervalFields::Microsecond),
(
"SELECT INTERVAL 5 MICROSECONDS",
IntervalFields::Microsecond,
),
];
for (sql, fields) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected an interval literal");
};
assert_eq!(
literal.kind,
LiteralKind::Interval {
fields: Some(fields),
precision: None,
},
"{sql}",
);
}
}
#[test]
fn duckdb_extended_interval_units_render_canonical_singular_in_type_position() {
let cases: [(&str, IntervalFields, &str); 3] = [
(
"SELECT CAST(a AS INTERVAL weeks)",
IntervalFields::Week,
"WEEK",
),
(
"SELECT CAST(a AS INTERVAL centuries)",
IntervalFields::Century,
"CENTURY",
),
(
"SELECT CAST(a AS INTERVAL microsecond)",
IntervalFields::Microsecond,
"MICROSECOND",
),
];
for (sql, fields, canonical) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
assert!(
matches!(
cast_type(&parsed),
DataType::Interval { fields: Some(f), precision: None, .. } if *f == fields
),
"{sql}: unexpected cast type",
);
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(
rendered,
format!("SELECT CAST(a AS INTERVAL {canonical})"),
"{sql}",
);
}
}
#[test]
fn duckdb_extended_interval_units_gated_off_by_default() {
for sql in [
"SELECT INTERVAL 5 WEEK",
"SELECT INTERVAL 5 WEEKS",
"SELECT INTERVAL 5 CENTURY",
"SELECT INTERVAL 5 MICROSECONDS",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"{sql} must be rejected without the relaxed-interval gate",
);
}
let parsed = parse_with(
"SELECT INTERVAL '5' CENTURY",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("quoted extended-unit interval parses under the relaxed gate");
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("expected an interval literal");
};
assert_eq!(
literal.kind,
LiteralKind::Interval {
fields: Some(IntervalFields::Century),
precision: None,
},
);
}
#[test]
fn tpch_q1_date_arithmetic_keeps_literal_structure() {
let parsed = parse_with(
"SELECT date '1998-12-01' - interval '90' day",
crate::ParseConfig::new(Postgres),
)
.expect("date arithmetic parses");
let Expr::BinaryOp {
left, op, right, ..
} = project_expr(&parsed)
else {
panic!("expected subtraction at the projection root");
};
assert_eq!(*op, BinaryOperator::Minus);
assert!(
matches!(
**left,
Expr::Literal {
literal: Literal {
kind: LiteralKind::Date,
..
},
..
},
),
"left operand is the date literal",
);
assert!(
matches!(
**right,
Expr::Literal {
literal: Literal {
kind: LiteralKind::Interval {
fields: Some(IntervalFields::Day),
precision: None,
},
..
},
..
},
),
"right operand is the `interval '90' day` literal",
);
}
#[test]
fn bit_string_literals_parse_with_radix_and_value_text() {
let cases = [
("SELECT B'1010'", BitStringRadix::Binary, "1010"),
("SELECT b'1010'", BitStringRadix::Binary, "1010"),
("SELECT X'1FF'", BitStringRadix::Hex, "1FF"),
("SELECT x'1ff'", BitStringRadix::Hex, "1ff"),
];
for (sql, radix, digits) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a literal");
};
assert_eq!(
literal.kind,
LiteralKind::BitString { radix },
"kind for {sql}"
);
assert_eq!(
literal.as_bit_text(parsed.source()).expect("digit body"),
digits,
"digits for {sql}",
);
}
}
#[test]
fn sqlite_hex_blob_literals_accept_even_hex_and_reject_malformed() {
for (sql, digits) in [
("SELECT x'53514C'", "53514C"),
("SELECT X'53514c'", "53514c"),
("SELECT x'1A'", "1A"),
("SELECT x''", ""),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Sqlite))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a literal");
};
assert_eq!(
literal.kind,
LiteralKind::BitString {
radix: BitStringRadix::Hex,
},
"kind for {sql}",
);
assert_eq!(
literal.as_bit_text(parsed.source()).expect("digit body"),
digits,
"digits for {sql}",
);
}
for sql in [
"SELECT x'ABC'",
"SELECT x'0'",
"SELECT x'XY'",
"SELECT X'1FF'",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(Sqlite)).is_err(),
"{sql} must reject as a malformed blob",
);
}
assert!(
parse_with("SELECT X'1FF'", crate::ParseConfig::new(Postgres)).is_ok(),
"PostgreSQL X'1FF' stays a deferred bit-string",
);
}
#[test]
fn number_literal_kind_classifies_radix_integers_apart_from_decimal_floats() {
for radix in ["0xBEEF", "0x1E", "0xE", "0XbeeF", "0o17", "0b1010"] {
assert_eq!(
super::number_literal_kind(radix, false),
LiteralKind::Integer,
"radix literal {radix} classifies as Integer",
);
}
for float in ["1e5", "1.5", "3.14", ".5", "0e5"] {
assert_eq!(
super::number_literal_kind(float, false),
LiteralKind::Float,
"decimal float {float} classifies as Float",
);
}
assert_eq!(
super::number_literal_kind("42", false),
LiteralKind::Integer
);
}
#[test]
fn parse_float_as_decimal_flag_reclassifies_only_floats() {
for float in ["1e5", "1.5", "3.14", ".5", "0e5"] {
assert_eq!(
super::number_literal_kind(float, true),
LiteralKind::Decimal,
"decimal float {float} classifies as Decimal when the flag is set",
);
}
for integer in ["42", "0xBEEF", "0o17", "0b1010"] {
assert_eq!(
super::number_literal_kind(integer, true),
LiteralKind::Integer,
"integer {integer} is unaffected by the flag",
);
}
assert_eq!(
super::number_literal_kind("$1234.56", true),
LiteralKind::Money,
"money is unaffected by the flag",
);
}
#[test]
fn parse_options_parse_float_as_decimal_reaches_the_ast_without_touching_render() {
let default_parsed =
parse_with("SELECT 3.14", crate::ParseConfig::new(Ansi)).expect("default parse");
let Expr::Literal { literal, .. } = project_expr(&default_parsed) else {
panic!("expected a literal");
};
assert_eq!(literal.kind, LiteralKind::Float);
assert_eq!(
Renderer::new(Ansi)
.render_parsed(&default_parsed)
.expect("render default"),
"SELECT 3.14",
);
let options = ParseConfig::default().parse_float_as_decimal(true);
let decimal_parsed =
parse_with("SELECT 3.14", options.dialect(Ansi)).expect("float-as-decimal parse");
let Expr::Literal { literal, .. } = project_expr(&decimal_parsed) else {
panic!("expected a literal");
};
assert_eq!(literal.kind, LiteralKind::Decimal);
assert_eq!(
literal
.as_decimal_text(decimal_parsed.source())
.expect("decimal literal materialises")
.as_ref(),
"3.14",
);
assert_eq!(
Renderer::new(Ansi)
.render_parsed(&decimal_parsed)
.expect("render decimal"),
"SELECT 3.14",
"render is unaffected by the classification",
);
let int_parsed =
parse_with("SELECT 42", options.dialect(Ansi)).expect("integer parse with flag");
let Expr::Literal { literal, .. } = project_expr(&int_parsed) else {
panic!("expected a literal");
};
assert_eq!(literal.kind, LiteralKind::Integer);
}
#[test]
fn radix_integer_literals_classify_and_decode_through_as_i64() {
let cases = [
("SELECT 0xBEEF", 48879_i64),
("SELECT 0x1E", 30),
("SELECT 0xE", 14),
("SELECT 0b1010", 10),
];
for (sql, value) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a literal");
};
assert_eq!(literal.kind, LiteralKind::Integer, "kind for {sql}");
assert_eq!(
literal.as_i64(parsed.source()).expect("integer value"),
value,
"value for {sql}",
);
}
}
#[test]
fn bit_string_marker_without_abutting_quote_is_an_identifier() {
let parsed = parse_with("SELECT b 'x'", crate::ParseConfig::new(Postgres))
.expect("`b 'x'` is a typed string constant");
let Expr::Cast { expr, syntax, .. } = project_expr(&parsed) else {
panic!("`b 'x'` is a typed-literal cast, not a bit string");
};
assert_eq!(*syntax, CastSyntax::PrefixTyped);
let Expr::Literal { literal, .. } = &**expr else {
panic!("the operand is a plain string constant");
};
assert!(
matches!(literal.kind, LiteralKind::String),
"the spaced marker leaves a plain string, not a bit string",
);
let parsed = parse_with("SELECT x", crate::ParseConfig::new(Postgres))
.expect("`x` is a column reference");
assert!(matches!(project_expr(&parsed), Expr::Column { .. }));
}
#[test]
fn national_and_unicode_strings_parse_as_string_literals() {
let parsed = parse_with("SELECT N'naive'", crate::ParseConfig::new(MySql))
.expect("MySQL lexes the national string");
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("SELECT N'naive': expected a literal under MySQL");
};
assert_eq!(literal.kind, LiteralKind::String);
assert_eq!(
literal.as_str(parsed.source()).expect("string value"),
"naive",
);
let cases = [
(r"SELECT U&'d\0061ta'", "data"),
("SELECT U&'d!0061ta' UESCAPE '!'", "data"),
(r"SELECT U&'a\\b''c'", "a\\b'c"),
];
for (sql, value) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a literal");
};
assert_eq!(literal.kind, LiteralKind::String, "kind for {sql}");
assert_eq!(
literal.as_str(parsed.source()).expect("string value"),
value,
"value for {sql}",
);
}
}
#[test]
fn unicode_escaped_identifier_decodes_to_its_resolved_name() {
let cases = [
(r#"SELECT U&"d\0061ta""#, "data"),
(r#"SELECT U&"d0061ta""#, "d0061ta"), (r#"SELECT U&"real\00A7_name""#, "real\u{00A7}_name"),
(r#"SELECT u&"x""#, "x"), (r#"SELECT U&"a'b""#, "a'b"), (r#"SELECT U&"""""#, "\""), (r#"SELECT U&"d*0061t\+000061" UESCAPE '*'"#, "dat\\+000061"),
(r#"SELECT U&"\ZZZZ" UESCAPE '!'"#, "\\ZZZZ"), ];
for (sql, name) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let expr = project_expr(&parsed);
assert_eq!(column_name(&parsed, expr), name, "decoded name for {sql}");
let Expr::Column { name: object, .. } = expr else {
panic!("{sql}: expected a column reference");
};
assert_eq!(
object.0[0].quote,
crate::ast::QuoteStyle::UnicodeDouble,
"a U&\"...\" identifier carries the UnicodeDouble spelling for {sql}",
);
}
}
#[test]
fn unicode_escaped_identifier_preserves_source_spelling_through_canonical_render() {
for sql in [
r#"SELECT U&"d\0061t\+000061""#,
r#"SELECT U&"d*0061t\+000061" UESCAPE '*'"#,
r#"SELECT U&"real\00A7_name""#,
r#"SELECT * FROM U&"my\0074able""#,
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
assert_eq!(
parsed.to_sql(),
sql,
"the U&\"...\" spelling must round-trip verbatim through canonical render",
);
}
}
#[test]
fn unicode_escaped_identifier_folds_uescape_in_every_identifier_position() {
for sql in [
r#"SELECT U&"real\00A7_name" FROM (select 1) AS x(real_name)"#,
r#"SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061""#,
r#"SELECT U&'d!0061t\+000061' UESCAPE '!' AS U&"d*0061t\+000061" UESCAPE '*'"#,
r#"SELECT 'tricky' AS U&"\" UESCAPE '!'"#,
r#"SELECT * FROM U&"my\0074able""#,
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: render failed: {err:?}"));
parse_with(&rendered, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: render {rendered:?} does not re-parse: {err:?}"));
}
}
#[test]
fn unicode_escaped_identifier_rejects_like_postgres() {
for sql in [
r#"SELECT U&"" FROM t"#, r#"SELECT U&"d0061" UESCAPE '+'"#, r#"SELECT U&"d0061" UESCAPE '5'"#, r#"SELECT U&"d0061" UESCAPE '!!'"#, r#"SELECT U&"\ZZZZ""#, r#"SELECT U&"\d800""#, r#"SELECT U&"\0000""#, r#"SELECT U&"x" uescape FROM t"#, ] {
parse_with(sql, crate::ParseConfig::new(Postgres))
.expect_err(&format!("{sql:?} must be rejected"));
}
parse_with(
r#"SELECT "x" UESCAPE '!'"#,
crate::ParseConfig::new(Postgres),
)
.expect_err("a non-U& identifier does not take a UESCAPE clause");
}
#[test]
fn unicode_string_with_invalid_escape_is_rejected_at_parse_time() {
for sql in [
r"SELECT U&'\0000'", r"SELECT U&'\D800'", r"SELECT U&'\+110000'", r"SELECT U&'\XYZW'", r"SELECT U&'\'", ] {
let err = parse_with(sql, crate::ParseConfig::new(Postgres))
.expect_err(&format!("{sql:?} must be rejected"));
assert_eq!(
err.found.to_string(),
"invalid escape sequence in string literal",
"for {sql:?}",
);
}
}
#[test]
fn unicode_string_invalid_escape_error_spans_the_whole_literal() {
let sql = r"SELECT U&'\0000'";
let err = parse_with(sql, crate::ParseConfig::new(Postgres))
.expect_err("a NUL-decoding escape is rejected");
let literal_start = sql.find("U&").expect("fixture contains U&") as u32;
assert_eq!(
err.span,
Span::new(literal_start, sql.len() as u32),
"the error spans the whole U&'...' literal, not just the offending escape",
);
}
#[test]
fn unicode_string_validates_against_the_resolved_uescape_character_not_default_backslash() {
let sql = r"SELECT U&'\ZZZZ' UESCAPE '!'";
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql:?} is legal under UESCAPE '!': {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("expected a literal");
};
assert_eq!(
literal.as_str(parsed.source()).expect("string value"),
r"\ZZZZ",
);
let sql = "SELECT U&'!d800' UESCAPE '!'";
let err = parse_with(sql, crate::ParseConfig::new(Postgres))
.expect_err("a lone surrogate under the custom escape character is rejected");
assert_eq!(
err.found.to_string(),
"invalid escape sequence in string literal"
);
}
#[test]
fn parsed_as_str_decodes_mysql_backslash_escapes_without_naming_the_dialect() {
for sql in [r#"SELECT "a\nb""#, r"SELECT 'a\nb'"] {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a string literal");
};
assert!(
parsed.string_literal_syntax().backslash_escapes,
"MySQL retains backslash-escape syntax on the root",
);
assert_eq!(
parsed.literal_str(literal).expect("MySQL string value"),
"a\nb",
"Parsed::literal_str value for {sql}",
);
assert_eq!(
literal.as_str(parsed.source()).expect("ANSI string value"),
r"a\nb",
"ANSI as_str value for {sql}",
);
}
}
#[test]
fn parsed_literal_str_keeps_backslashes_literal_under_postgres() {
let sql = r"SELECT 'a\nb'";
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.expect(r"`'a\nb'` parses under PostgreSQL");
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("expected a string literal");
};
assert!(
!parsed.string_literal_syntax().backslash_escapes,
"PostgreSQL leaves backslash escapes off on the root",
);
assert_eq!(
parsed.literal_str(literal).expect("PG string value"),
r"a\nb"
);
}
#[test]
fn parsed_literal_str_agrees_with_ansi_as_str_and_preserves_doubled_quotes() {
let sql = "SELECT 'it''s'";
let parsed =
parse_with(sql, crate::ParseConfig::new(Postgres)).expect("doubled-quote string parses");
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("expected a string literal");
};
assert_eq!(literal.as_str(parsed.source()).expect("ANSI value"), "it's");
assert_eq!(parsed.literal_str(literal).expect("Parsed value"), "it's");
}
#[test]
fn charset_introduced_strings_parse_as_string_literals_under_mysql() {
let cases = [
("SELECT _utf8mb4'cafe'", "utf8mb4", "cafe"),
("SELECT _latin1'x'", "latin1", "x"),
("SELECT _utf8'it''s'", "utf8", "it's"), ];
for (sql, charset, value) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a literal");
};
assert_eq!(literal.kind, LiteralKind::String, "kind for {sql}");
assert_eq!(
literal.as_str(parsed.source()).expect("string value"),
value,
"value for {sql}",
);
assert_eq!(
literal
.charset_introducer(parsed.source())
.expect("string literal"),
Some(charset),
"introducer for {sql}",
);
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn charset_introducer_is_inert_without_the_dialect_knob() {
let parsed = parse_with("SELECT _utf8'x'", crate::ParseConfig::new(Postgres))
.expect("`_utf8 'x'` is a typed literal");
assert!(
matches!(project_expr(&parsed), Expr::Cast { .. }),
"charset introducers off reads `_utf8'x'` as a typed-literal cast",
);
let parsed =
parse_with("SELECT 'x'", crate::ParseConfig::new(MySql)).expect("plain string parses");
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("expected a literal");
};
assert_eq!(
literal
.charset_introducer(parsed.source())
.expect("string literal"),
None,
"a plain string carries no charset introducer",
);
}
#[test]
fn charset_introduced_string_concatenates_across_a_newline() {
let parsed = parse_with("SELECT _utf8'foo'\n'bar'", crate::ParseConfig::new(MySql))
.expect("adjacent charset strings concatenate");
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("expected a literal");
};
assert_eq!(literal.kind, LiteralKind::String);
assert_eq!(
literal.as_str(parsed.source()).expect("concatenated value"),
"foobar",
);
assert_eq!(
literal
.charset_introducer(parsed.source())
.expect("string literal"),
Some("utf8"),
"the introducer applies to the whole continued constant",
);
}
#[test]
fn adjacent_string_literals_concatenate_across_a_newline() {
let cases = [
("SELECT 'foo'\n'bar'", "foobar"),
("SELECT 'a'\n'b'\n'c'", "abc"), ("SELECT E'a'\n'b'", "ab"), ("SELECT 'a' \r\n 'b'", "ab"), ("SELECT 'a'\n\n'b'", "ab"), ];
for (sql, value) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql:?}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql:?}: expected a literal");
};
assert_eq!(literal.kind, LiteralKind::String, "kind for {sql:?}");
assert_eq!(
literal.as_str(parsed.source()).expect("concatenated value"),
value,
"value for {sql:?}",
);
}
}
#[test]
fn bit_string_literals_concatenate_across_a_newline() {
let parsed = parse_with("SELECT B'1010'\n'0101'", crate::ParseConfig::new(Postgres))
.expect("adjacent bit strings concatenate");
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("expected a literal");
};
assert!(matches!(literal.kind, LiteralKind::BitString { .. }));
assert_eq!(
literal.as_bit_text(parsed.source()).expect("joined digits"),
"10100101",
);
}
#[test]
fn adjacent_string_literals_on_one_line_are_rejected() {
for sql in [
"SELECT 'foo' 'bar'", "SELECT 'foo'\t'bar'", "SELECT 'a'/* c */'b'", "SELECT 'a'/* \n */'b'", "SELECT 'a' -- c\n'b'", ] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"must reject {sql:?}"
);
}
}
#[test]
fn adjacent_string_concat_requires_a_plain_continuation_segment() {
for sql in [
"SELECT 'a'\nE'b'", "SELECT 'a'\nU&'b'", "SELECT $$a$$\n'b'", ] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"must reject {sql:?}"
);
}
}
#[test]
fn adjacent_string_concatenation_round_trips_exact_source() {
for sql in ["SELECT 'foo'\n'bar'", "SELECT E'a'\n'b'\n'c'"] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql:?}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql:?}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql:?}");
}
}
#[test]
fn temporal_literal_value_string_concatenates_across_a_newline() {
let cases = [(
"SELECT DATE '1998'\n'-12-01'",
LiteralKind::Date,
"1998-12-01",
)];
for (sql, kind, value) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql:?}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql:?}: expected a literal");
};
assert_eq!(literal.kind, kind, "kind for {sql:?}");
assert_eq!(
literal
.as_temporal_text(parsed.source())
.expect("value text"),
value,
"value for {sql:?}",
);
}
assert!(
parse_with(
"SELECT DATE '1998' '-12-01'",
crate::ParseConfig::new(Postgres)
)
.is_err()
);
}
#[test]
fn pg_special_literals_round_trip_exact_source_spelling() {
let cases = [
"SELECT B'1010'",
"SELECT X'1FF'",
r"SELECT U&'d\0061ta'",
"SELECT U&'d!0061ta' UESCAPE '!'",
];
for sql in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
let national = parse_with("SELECT N'naive'", crate::ParseConfig::new(NATIONAL_DIALECT))
.expect("a national-arming dialect lexes the national string");
assert_eq!(
Renderer::new(NATIONAL_DIALECT)
.render_parsed(&national)
.expect("renders under the national-arming dialect"),
"SELECT N'naive'",
"the national literal renders span-verbatim",
);
let pg = parse_with("SELECT N'naive'", crate::ParseConfig::new(Postgres))
.expect("parses under PostgreSQL");
assert_eq!(
Renderer::new(Postgres)
.render_parsed(&pg)
.expect("renders under PostgreSQL"),
"SELECT N 'naive'",
"the typed-literal reading canonicalizes the abutting prefix with a space",
);
}
#[test]
fn unicode_string_without_uescape_keyword_does_not_consume_following_tokens() {
let parsed = parse_with(r"SELECT U&'\0041', 1", crate::ParseConfig::new(Postgres))
.expect("two projection items parse");
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("expected a query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("expected a SELECT body");
};
assert_eq!(
select.projection.len(),
2,
"comma after U&'...' is the item separator"
);
}
const NATIONAL_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.string_literals(StringLiteralSyntax {
national_strings: true,
..StringLiteralSyntax::ANSI
}));
FeatureDialect {
features: &FEATURES,
}
};
const MONEY_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.numeric_literals(NumericLiteralSyntax {
money_literals: true,
..NumericLiteralSyntax::ANSI
}));
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn money_literals_parse_with_money_kind_and_value_text() {
let cases = [
("SELECT $1234.56", "1234.56"),
("SELECT $100", "100"),
("SELECT $.5", ".5"),
];
for (sql, body) in cases {
let parsed = parse_with(sql, crate::ParseConfig::new(MONEY_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let Expr::Literal { literal, .. } = project_expr(&parsed) else {
panic!("{sql}: expected a literal");
};
assert_eq!(literal.kind, LiteralKind::Money, "kind for {sql}");
assert_eq!(
literal.as_money_text(parsed.source()).expect("money body"),
body,
"body for {sql}",
);
}
}
#[test]
fn signed_money_is_a_unary_op_over_an_unsigned_money_literal() {
let parsed = parse_with("SELECT -$1000", crate::ParseConfig::new(MONEY_DIALECT))
.expect("signed money parses");
let Expr::UnaryOp {
op: UnaryOperator::Minus,
expr,
..
} = project_expr(&parsed)
else {
panic!("expected a unary minus over a money literal");
};
let Expr::Literal { literal, .. } = expr.as_ref() else {
panic!("expected a money literal operand");
};
assert_eq!(literal.kind, LiteralKind::Money);
assert_eq!(
literal.as_money_text(parsed.source()).expect("money body"),
"1000",
);
}
#[test]
fn money_literals_are_dialect_gated() {
assert!(
parse_with("SELECT $1234.56", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects money literals",
);
assert!(
parse_with("SELECT $1234.56", crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL rejects money literals",
);
}
#[test]
fn money_literals_round_trip_exact_source_spelling() {
for sql in [
"SELECT $1234.56",
"SELECT $100",
"SELECT $.5",
"SELECT -$1000",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(MONEY_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(MONEY_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn named_argument_arrow_parses() {
let parsed = parse_with("SELECT f(a => 1)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("named arg parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.args.len(), 1);
let arg = &call.args[0];
assert_eq!(arg.syntax, ArgSyntax::Arrow);
let name = arg.name.expect("a named argument carries a name");
assert_eq!(parsed.resolver().resolve(name), "a");
assert!(matches!(arg.value, Expr::Literal { .. }));
}
#[test]
fn named_argument_colon_equals_parses() {
let parsed = parse_with("SELECT f(a := 1)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("deprecated named arg parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let arg = &call.args[0];
assert_eq!(arg.syntax, ArgSyntax::ColonEquals);
assert_eq!(parsed.resolver().resolve(arg.name.expect("name")), "a");
}
#[test]
fn mixed_positional_and_named_arguments_parse() {
let parsed = parse_with(
"SELECT f(1, b => 2)",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("mixed args parse");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.args.len(), 2);
assert_eq!(call.args[0].syntax, ArgSyntax::Positional);
assert!(call.args[0].name.is_none());
assert_eq!(call.args[1].syntax, ArgSyntax::Arrow);
assert_eq!(
parsed.resolver().resolve(call.args[1].name.expect("name")),
"b"
);
}
#[test]
fn all_positional_arguments_stay_positional() {
let parsed = parse_with("SELECT f(1, 2)", crate::ParseConfig::new(PG_EXPR_DIALECT))
.expect("positional args parse");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.args.len(), 2);
assert!(
call.args
.iter()
.all(|arg| arg.name.is_none() && arg.syntax == ArgSyntax::Positional),
);
}
#[test]
fn named_arguments_are_rejected_under_ansi() {
assert!(
parse_with("SELECT f(a => 1)", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects the `=>` named-argument arrow",
);
assert!(
parse_with("SELECT f(a := 1)", crate::ParseConfig::new(TestDialect)).is_err(),
"ANSI rejects the `:=` named-argument separator",
);
}
#[test]
fn named_arguments_round_trip_each_arrow() {
for sql in [
"SELECT f(a => 1)",
"SELECT f(a := 1)",
"SELECT f(1, b => 2)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn variadic_argument_marks_the_last_positional() {
let parsed = parse_with(
"SELECT f(a, VARIADIC arr)",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("VARIADIC arg parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert_eq!(call.args.len(), 2);
assert!(!call.args[0].variadic, "the leading argument is ordinary");
assert!(call.args[1].variadic, "the final argument carries VARIADIC");
assert_eq!(call.args[1].syntax, ArgSyntax::Positional);
assert!(call.args[1].name.is_none());
}
#[test]
fn variadic_argument_combines_with_a_named_argument() {
let parsed = parse_with(
"SELECT f(VARIADIC x => arr)",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("VARIADIC named parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
let arg = &call.args[0];
assert!(arg.variadic);
assert_eq!(arg.syntax, ArgSyntax::Arrow);
assert_eq!(parsed.resolver().resolve(arg.name.expect("name")), "x");
}
#[test]
fn variadic_argument_admits_the_aggregate_order_by_tail() {
let parsed = parse_with(
"SELECT string_agg(VARIADIC arr ORDER BY b)",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("VARIADIC with ORDER BY parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(call.args[0].variadic);
assert_eq!(call.order_by.len(), 1);
}
#[test]
fn variadic_argument_rejected_when_not_last() {
for sql in [
"SELECT f(VARIADIC arr, a)",
"SELECT f(a, VARIADIC arr, b)",
"SELECT f(VARIADIC a, VARIADIC b)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"VARIADIC must be the final argument: {sql}",
);
}
}
#[test]
fn variadic_argument_rejected_with_a_quantifier() {
for sql in [
"SELECT array_agg(DISTINCT VARIADIC arr)",
"SELECT f(ALL a, VARIADIC arr)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(PG_EXPR_DIALECT)).is_err(),
"VARIADIC cannot combine with a quantifier: {sql}",
);
}
}
#[test]
fn variadic_argument_rejected_off_dialect() {
assert!(
parse_with(
"SELECT f(a, VARIADIC arr)",
crate::ParseConfig::new(TestDialect)
)
.is_err(),
"ANSI does not admit the VARIADIC argument marker",
);
}
#[test]
fn variadic_argument_parses_under_duckdb() {
let parsed = parse_with("SELECT f(a, VARIADIC arr)", crate::ParseConfig::new(DuckDb))
.expect("DuckDB admits VARIADIC");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a function call");
};
assert!(call.args[1].variadic);
}
#[test]
fn variadic_argument_round_trips() {
for sql in [
"SELECT f(VARIADIC arr)",
"SELECT f(a, VARIADIC arr)",
"SELECT f(VARIADIC x => arr)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn operator_construct_parses_unqualified() {
let parsed = parse_with(
"SELECT a OPERATOR(+) b",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("OPERATOR(...) parses");
let Expr::NamedOperator { named_operator, .. } = project_expr(&parsed) else {
panic!("expected a named-operator expression");
};
assert!(
named_operator.schema.0.is_empty(),
"an unqualified operator has no schema",
);
assert_eq!(parsed.resolver().resolve(named_operator.op), "+");
assert!(matches!(named_operator.left, Expr::Column { .. }));
assert!(matches!(named_operator.right, Expr::Column { .. }));
}
#[test]
fn operator_construct_parses_schema_qualified() {
let parsed = parse_with(
"SELECT a OPERATOR(pg_catalog.+) b",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("schema-qualified OPERATOR(...) parses");
let Expr::NamedOperator { named_operator, .. } = project_expr(&parsed) else {
panic!("expected a named-operator expression");
};
assert_eq!(named_operator.schema.0.len(), 1);
assert_eq!(
parsed.resolver().resolve(named_operator.schema.0[0].sym),
"pg_catalog",
);
assert_eq!(parsed.resolver().resolve(named_operator.op), "+");
}
#[test]
fn operator_construct_binds_at_the_any_other_operator_rank() {
let tight_right = parse_with(
"SELECT a OPERATOR(+) b * c",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("parses");
let Expr::NamedOperator { named_operator, .. } = project_expr(&tight_right) else {
panic!("expected a named operator at the top");
};
assert!(
matches!(
named_operator.right,
Expr::BinaryOp {
op: BinaryOperator::Multiply,
..
}
),
"`*` binds tighter, so the right operand is `b * c`",
);
let tight_left = parse_with(
"SELECT a + b OPERATOR(+) c",
crate::ParseConfig::new(PG_EXPR_DIALECT),
)
.expect("parses");
let Expr::NamedOperator { named_operator, .. } = project_expr(&tight_left) else {
panic!("expected a named operator at the top");
};
assert!(
matches!(
named_operator.left,
Expr::BinaryOp {
op: BinaryOperator::Plus,
..
}
),
"`+` binds tighter, so the left operand is `a + b`",
);
}
#[test]
fn operator_construct_is_rejected_under_ansi() {
assert!(
parse_with(
"SELECT a OPERATOR(+) b",
crate::ParseConfig::new(TestDialect)
)
.is_err(),
"ANSI has no explicit-operator infix form",
);
}
#[test]
fn operator_construct_round_trips() {
for sql in [
"SELECT a OPERATOR(+) b",
"SELECT a OPERATOR(pg_catalog.+) b",
"SELECT a OPERATOR(+) b * c",
"SELECT a + b OPERATOR(+) c",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn mysql_has_no_typed_interval_literal() {
for sql in [
"SELECT INTERVAL '1'",
"SELECT INTERVAL \"x\"",
"SELECT * FROM t WHERE INTERVAL \"is\" > 1",
"SELECT INTERVAL '1' HOUR TO SECOND",
"SELECT INTERVAL '1' SECOND(3)",
"SELECT INTERVAL '1-2' YEAR TO MONTH",
"SELECT '2020-01-01' - INTERVAL '1' HOUR TO SECOND",
"SELECT '2020-01-01' - INTERVAL '1' SECOND(3)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects the typed interval literal {sql:?}",
);
}
for sql in [
"SELECT '2020-01-01' + INTERVAL '1' DAY",
"SELECT SUM(x) OVER (ORDER BY b RANGE BETWEEN INTERVAL '1' DAY PRECEDING \
AND CURRENT ROW) FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_ok(),
"MySQL parses the operator-position interval {sql:?}",
);
}
for sql in ["SELECT INTERVAL '1'", "SELECT INTERVAL '1' HOUR TO SECOND"] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_ok(),
"PostgreSQL admits the interval literal {sql:?}",
);
}
}
#[test]
fn mysql_interval_operator_parses_round_trips_and_shapes() {
use crate::dialect::Lenient;
const MYSQL_RENDER: FeatureDialect = FeatureDialect {
features: &FeatureSet::MYSQL,
};
const LENIENT_RENDER: FeatureDialect = FeatureDialect {
features: &FeatureSet::LENIENT,
};
let round_trip = [
"SELECT NOW() - INTERVAL 3 DAY",
"SELECT NOW() + INTERVAL 3 DAY",
"SELECT INTERVAL 3 DAY + NOW()",
"SELECT NOW() - INTERVAL 1.5 DAY",
"SELECT NOW() - INTERVAL -3 DAY",
"SELECT NOW() - INTERVAL ? DAY",
"SELECT NOW() - INTERVAL @x DAY",
"SELECT NOW() - INTERVAL 3 + 1 DAY",
"SELECT NOW() - INTERVAL '3' DAY",
"SELECT NOW() + INTERVAL '3-2' YEAR_MONTH",
"SELECT NOW() + INTERVAL '1:2:3' HOUR_SECOND",
"SELECT NOW() + INTERVAL 1 DAY_MICROSECOND",
"SELECT NOW() + INTERVAL 1 MICROSECOND",
"SELECT NOW() + INTERVAL 1 WEEK",
"SELECT NOW() + INTERVAL 1 QUARTER",
"SELECT NOW() - INTERVAL 1 DAY - INTERVAL 1 HOUR",
"SELECT '2020-01-01' + INTERVAL n MONTH FROM t",
];
for sql in round_trip {
for dialect in ["MySql", "Lenient"] {
let parsed = match dialect {
"MySql" => parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("{sql}: {err:?}")),
_ => parse_with(sql, crate::ParseConfig::new(Lenient))
.unwrap_or_else(|err| panic!("{sql}: {err:?}")),
};
let rendered = match dialect {
"MySql" => Renderer::new(MYSQL_RENDER).render_parsed(&parsed),
_ => Renderer::new(LENIENT_RENDER).render_parsed(&parsed),
}
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql} under {dialect}");
}
}
let parsed = parse_with(
"SELECT NOW() - INTERVAL 3 DAY",
crate::ParseConfig::new(MySql),
)
.expect("parses");
let Expr::BinaryOp { right, .. } = project_expr(&parsed) else {
panic!("expected a binary subtraction");
};
let Expr::Interval { value, unit, .. } = &**right else {
panic!("expected the RHS to be an Expr::Interval, got {right:?}");
};
assert_eq!(*unit, IntervalFields::Day);
assert!(
matches!(&**value, Expr::Literal { literal, .. } if literal.kind == LiteralKind::Integer),
"the amount is the integer literal 3, got {value:?}",
);
let parsed = parse_with(
"SELECT NOW() - INTERVAL (3 + 1) DAY",
crate::ParseConfig::new(MySql),
)
.expect("parses");
let rendered = Renderer::new(MYSQL_RENDER)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(rendered, "SELECT NOW() - INTERVAL 3 + 1 DAY");
let parsed = parse_with(
"SELECT NOW() + INTERVAL 1 DAY_HOUR",
crate::ParseConfig::new(MySql),
)
.expect("parses");
let Expr::BinaryOp { right, .. } = project_expr(&parsed) else {
panic!("binary");
};
assert!(matches!(
&**right,
Expr::Interval {
unit: IntervalFields::DayToHour,
..
}
));
}
#[test]
fn mysql_interval_operator_precedence_matches_left_assoc_additive() {
let parsed = parse_with(
"SELECT a - INTERVAL 1 DAY + b FROM t",
crate::ParseConfig::new(MySql),
)
.expect("parses");
let Expr::BinaryOp {
left,
op: BinaryOperator::Plus,
..
} = project_expr(&parsed)
else {
panic!("the outermost operator must be the trailing `+`");
};
let Expr::BinaryOp {
op: BinaryOperator::Minus,
right,
..
} = &**left
else {
panic!("its left operand must be the `a - INTERVAL 1 DAY` subtraction");
};
assert!(
matches!(&**right, Expr::Interval { .. }),
"the subtraction's RHS is the interval operand",
);
let rendered = Renderer::new(FeatureDialect {
features: &FeatureSet::MYSQL,
})
.render_parsed(&parsed)
.expect("renders");
assert_eq!(rendered, "SELECT a - INTERVAL 1 DAY + b FROM t");
}
#[test]
fn mysql_interval_operator_boundary_rejects_and_fallthrough() {
for sql in ["SELECT NOW() - INTERVAL 3", "SELECT INTERVAL '1'"] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects the unit-less interval {sql:?}",
);
}
assert!(
parse_with(
"SELECT NOW() - INTERVAL 3 DAY",
crate::ParseConfig::new(Postgres)
)
.is_err(),
"the operator interval is gated off for PostgreSQL",
);
for sql in [
"SELECT INTERVAL '1' HOUR TO SECOND",
"SELECT INTERVAL '1' SECOND(3)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(crate::dialect::Lenient)).is_ok(),
"Lenient parses the ANSI interval literal {sql:?} via the literal path",
);
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects the ANSI interval literal {sql:?}",
);
}
}
#[test]
fn mysql_builtin_aggregate_rejects_an_empty_argument_list() {
for sql in [
"SELECT COUNT()",
"SELECT SUM()",
"SELECT GROUP_CONCAT()",
"SELECT JSON_ARRAYAGG()",
"SELECT BIT_AND()",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects the empty aggregate call {sql:?}",
);
}
for sql in [
"SELECT COUNT(*)",
"SELECT COUNT(a) FROM t",
"SELECT NOW()",
"SELECT my_udf()",
"SELECT db.count()",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_ok(),
"MySQL parses {sql:?}",
);
}
assert!(
parse_with("SELECT COUNT()", crate::ParseConfig::new(Postgres)).is_ok(),
"PostgreSQL admits an empty COUNT() at parse time",
);
}
#[test]
fn mysql_over_clause_requires_a_windowable_function() {
for sql in [
"SELECT PERCENTILE_CONT(x, 0.5) OVER () FROM t",
"SELECT ABS(x) OVER () FROM t",
"SELECT ANY_VALUE(x) OVER () FROM t",
"SELECT my_udf(x) OVER () FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects OVER on the non-windowable {sql:?}",
);
}
assert!(
parse_with(
"SELECT PERCENTILE_CONT(x, 0.5) FROM t",
crate::ParseConfig::new(MySql)
)
.is_ok(),
"the bare (non-windowed) call parses; only OVER is rejected",
);
for sql in [
"SELECT SUM(x) OVER () FROM t",
"SELECT COUNT(*) OVER () FROM t",
"SELECT COUNT(x) OVER () FROM t",
"SELECT GROUP_CONCAT(x) OVER () FROM t",
"SELECT BIT_XOR(x) OVER () FROM t",
"SELECT AVG(x) OVER () FROM t",
"SELECT MIN(x) OVER () FROM t",
"SELECT MAX(x) OVER () FROM t",
"SELECT STDDEV(x) OVER () FROM t",
"SELECT VARIANCE(x) OVER () FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_ok(),
"MySQL parses the windowed aggregate {sql:?}",
);
}
assert!(
parse_with(
"SELECT ABS(x) OVER () FROM t",
crate::ParseConfig::new(Postgres)
)
.is_ok(),
"PostgreSQL admits OVER on any function",
);
}
#[test]
fn mysql_window_functions_parse_as_call_heads_with_over() {
for sql in [
"SELECT ROW_NUMBER() OVER ()",
"SELECT RANK() OVER ()",
"SELECT DENSE_RANK() OVER ()",
"SELECT PERCENT_RANK() OVER ()",
"SELECT CUME_DIST() OVER ()",
"SELECT NTILE(4) OVER ()",
"SELECT LEAD(a) OVER () FROM t",
"SELECT LEAD(a, 2) OVER () FROM t",
"SELECT LEAD(a, 2, 0) OVER () FROM t",
"SELECT LAG(a, 1) OVER () FROM t",
"SELECT FIRST_VALUE(a) OVER () FROM t",
"SELECT LAST_VALUE(a) OVER () FROM t",
"SELECT NTH_VALUE(a, 2) OVER () FROM t",
"SELECT ROW_NUMBER() OVER (PARTITION BY a ORDER BY b) FROM t",
"SELECT row_number() OVER ()",
"SELECT row_number () OVER ()",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_ok(),
"MySQL parses the window function {sql:?}",
);
}
}
#[test]
fn mysql_window_functions_require_over_and_fixed_arity() {
for sql in [
"SELECT ROW_NUMBER()",
"SELECT RANK() FROM t",
"SELECT LEAD(a) FROM t",
"SELECT ROW_NUMBER(1) OVER ()",
"SELECT RANK(a) OVER () FROM t",
"SELECT NTILE() OVER ()",
"SELECT NTILE(4, 5) OVER ()",
"SELECT FIRST_VALUE(a, b) OVER () FROM t",
"SELECT NTH_VALUE(a) OVER () FROM t",
"SELECT NTH_VALUE(a, 2, 3) OVER () FROM t",
"SELECT LEAD(a, 2, 3, 4) OVER () FROM t",
"SELECT ROW_NUMBER(*) OVER ()",
"SELECT RANK(DISTINCT a) OVER () FROM t",
"SELECT NTILE(DISTINCT a) OVER () FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects the malformed window call {sql:?}",
);
}
}
#[test]
fn mysql_window_function_tail_admits_respect_nulls_and_from_first() {
for (sql, fragment) in [
(
"SELECT LEAD(a) RESPECT NULLS OVER () FROM t",
") RESPECT NULLS OVER",
),
(
"SELECT LAG(a) RESPECT NULLS OVER () FROM t",
") RESPECT NULLS OVER",
),
(
"SELECT FIRST_VALUE(a) RESPECT NULLS OVER () FROM t",
") RESPECT NULLS OVER",
),
(
"SELECT LAST_VALUE(a) RESPECT NULLS OVER () FROM t",
") RESPECT NULLS OVER",
),
(
"SELECT NTH_VALUE(a, 2) RESPECT NULLS OVER () FROM t",
") RESPECT NULLS OVER",
),
(
"SELECT NTH_VALUE(a, 2) FROM FIRST OVER () FROM t",
") FROM FIRST OVER",
),
(
"SELECT NTH_VALUE(a, 2) FROM FIRST RESPECT NULLS OVER () FROM t",
") FROM FIRST RESPECT NULLS OVER",
),
(
"SELECT LEAD(a, 1, 0) RESPECT NULLS OVER () FROM t",
") RESPECT NULLS OVER",
),
(
"SELECT nth_value(a, 2) from first over () FROM t",
") FROM FIRST OVER",
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("MySQL parses {sql:?}: {err:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert!(
rendered.contains(fragment),
"the tail renders in the post-`)` position for {sql:?}: got {rendered:?}",
);
parse_with(&rendered, crate::ParseConfig::new(MySql))
.unwrap_or_else(|err| panic!("the rendered tail {rendered:?} re-parses: {err:?}"));
}
}
#[test]
fn mysql_window_function_tail_rejects_the_unadmitted_forms() {
for sql in [
"SELECT LEAD(a) IGNORE NULLS OVER () FROM t",
"SELECT FIRST_VALUE(a) IGNORE NULLS OVER () FROM t",
"SELECT NTH_VALUE(a, 2) IGNORE NULLS OVER () FROM t",
"SELECT NTH_VALUE(a, 2) FROM LAST OVER () FROM t",
"SELECT ROW_NUMBER() RESPECT NULLS OVER ()",
"SELECT RANK() RESPECT NULLS OVER ()",
"SELECT NTILE(4) RESPECT NULLS OVER ()",
"SELECT FIRST_VALUE(a) FROM FIRST OVER () FROM t",
"SELECT LEAD(a) FROM FIRST OVER () FROM t",
"SELECT NTH_VALUE(a, 2) RESPECT NULLS FROM FIRST OVER () FROM t",
"SELECT LEAD(a RESPECT NULLS) OVER () FROM t",
"SELECT NTH_VALUE(a, 2 FROM FIRST) OVER () FROM t",
"SELECT SUM(a) RESPECT NULLS OVER () FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects the unadmitted window tail {sql:?}",
);
}
assert!(
parse_with(
"SELECT LEAD(a) RESPECT NULLS OVER () FROM t",
crate::ParseConfig::new(Postgres)
)
.is_err(),
"PostgreSQL rejects the MySQL window-function tail",
);
}
#[test]
fn mysql_window_function_names_reserved_outside_the_call_head() {
for sql in [
"SELECT ROW_NUMBER",
"SELECT ROW_NUMBER FROM t",
"SELECT rank FROM t",
"SELECT 1 AS row_number",
"SELECT a AS lead FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL keeps the window name reserved in {sql:?}",
);
}
assert!(
parse_with(
"SELECT row_number() OVER ()",
crate::ParseConfig::new(Postgres)
)
.is_ok(),
"PostgreSQL admits row_number() as an ordinary window call",
);
assert!(
parse_with("SELECT row_number()", crate::ParseConfig::new(Postgres)).is_ok(),
"PostgreSQL does not require OVER on row_number() at parse time",
);
assert!(
parse_with("SELECT rank FROM t", crate::ParseConfig::new(Postgres)).is_ok(),
"PostgreSQL admits `rank` as an ordinary column name",
);
}
#[test]
fn postgres_sqljson_constructors_reject_empty_argument_list() {
for sql in [
"SELECT JSON()",
"SELECT JSON_SCALAR()",
"SELECT JSON_SERIALIZE()",
] {
parse_with(sql, crate::ParseConfig::new(Postgres)).expect_err(&format!(
"SQL/JSON constructor requires an argument: {sql:?}"
));
}
parse_with("SELECT JSON('{}')", crate::ParseConfig::new(Postgres)).expect("JSON('{}') parses");
parse_with("SELECT JSON_SCALAR(1)", crate::ParseConfig::new(Postgres))
.expect("JSON_SCALAR(1) parses");
parse_with("SELECT \"json\"()", crate::ParseConfig::new(Postgres))
.expect("quoted \"json\"() is a general call");
parse_with("SELECT json()", crate::ParseConfig::new(MySql))
.expect("MySQL treats json() as a general call");
}
#[test]
fn postgres_merge_action_support_function() {
let parsed = parse_with("SELECT merge_action()", crate::ParseConfig::new(Postgres))
.expect("merge_action() parses");
let Expr::Function { call, .. } = project_expr(&parsed) else {
panic!("expected a merge_action function call");
};
assert_eq!(
parsed.resolver().resolve(call.name.0[0].sym),
"merge_action"
);
assert!(call.args.is_empty() && !call.wildcard && call.over.is_none());
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.expect("merge_action() renders");
assert_eq!(rendered, "SELECT merge_action()");
for sql in [
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET x = 1 RETURNING merge_action()",
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET x = 1 RETURNING merge_action() AS act",
"SELECT merge_action() FROM t",
] {
parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql:?}: {err:?}"));
}
for sql in ["SELECT merge_action(1)", "SELECT merge_action() OVER ()"] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"{sql} must be rejected"
);
}
parse_with(
"SELECT merge_action()",
crate::ParseConfig::new(crate::dialect::Lenient),
)
.expect("Lenient admits it");
assert!(
parse_with(
"SELECT merge_action()",
crate::ParseConfig::new(crate::dialect::Ansi)
)
.is_err(),
"ANSI has no merge_action() support function",
);
}
#[test]
fn postgres_collation_for_expression() {
use crate::ast::StringFunc;
let parsed = parse_with(
"SELECT COLLATION FOR ('foo')",
crate::ParseConfig::new(Postgres),
)
.expect("COLLATION FOR (expr) parses");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
assert!(matches!(
string_func.as_ref(),
StringFunc::CollationFor { .. }
));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.expect("COLLATION FOR renders");
assert_eq!(rendered, "SELECT COLLATION FOR ('foo')");
for sql in [
"SELECT COLLATION FOR (a.b)",
"SELECT COLLATION FOR (col1 || col2)",
"SELECT COLLATION FOR (x) FROM t",
] {
parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
}
for sql in [
"SELECT COLLATION FOR 'foo'",
"SELECT COLLATION FOR ()",
"SELECT COLLATION FOR ('foo', 'bar')",
"SELECT COLLATION FOR (select 1)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"{sql:?} must be rejected"
);
}
parse_with("SELECT collation('foo')", crate::ParseConfig::new(Postgres))
.expect("plain collation() call parses");
parse_with(
"SELECT COLLATION FOR ('foo')",
crate::ParseConfig::new(crate::dialect::Lenient),
)
.expect("Lenient admits COLLATION FOR");
assert!(
parse_with(
"SELECT COLLATION FOR ('foo')",
crate::ParseConfig::new(crate::dialect::Ansi)
)
.is_err(),
"ANSI has no COLLATION FOR (expr) common-subexpr",
);
}
#[test]
fn ceil_to_field_special_form() {
use crate::ast::{CeilSpelling, StringFunc};
use crate::dialect::Lenient;
let parsed = parse_with("SELECT CEIL(x TO DAY)", crate::ParseConfig::new(Lenient))
.expect("CEIL(x TO field) parses under Lenient");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
let StringFunc::CeilTo {
field, spelling, ..
} = string_func.as_ref()
else {
panic!("expected StringFunc::CeilTo");
};
assert_eq!(parsed.resolver().resolve(field.sym), "DAY");
assert_eq!(*spelling, CeilSpelling::Ceil);
assert_eq!(
Renderer::new(Lenient)
.render_parsed(&parsed)
.expect("CEIL(x TO field) renders"),
"SELECT CEIL(x TO DAY)",
);
let parsed = parse_with(
"SELECT CEILING(x TO HOUR)",
crate::ParseConfig::new(Lenient),
)
.expect("CEILING(x TO field) parses under Lenient");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
let StringFunc::CeilTo { spelling, .. } = string_func.as_ref() else {
panic!("expected StringFunc::CeilTo");
};
assert_eq!(*spelling, CeilSpelling::Ceiling);
assert_eq!(
Renderer::new(Lenient)
.render_parsed(&parsed)
.expect("CEILING(x TO field) renders"),
"SELECT CEILING(x TO HOUR)",
);
let parsed = parse_with("SELECT CEIL(x, 2)", crate::ParseConfig::new(Lenient))
.expect("CEIL(x, 2) parses as an ordinary call");
assert!(
matches!(project_expr(&parsed), Expr::Function { .. }),
"CEIL(x, 2) must stay an ordinary Expr::Function, not StringFunc::CeilTo",
);
assert!(
parse_with(
"SELECT CEIL(x TO DAY)",
crate::ParseConfig::new(crate::dialect::Ansi)
)
.is_err(),
"ANSI has no CEIL TO-field special form",
);
assert!(
parse_with("SELECT CEIL(x TO DAY)", crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL has no CEIL TO-field grammar (pg_query-verified)",
);
}
#[test]
fn floor_to_field_special_form() {
use crate::ast::StringFunc;
use crate::dialect::Lenient;
let parsed = parse_with("SELECT FLOOR(x TO DAY)", crate::ParseConfig::new(Lenient))
.expect("FLOOR(x TO field) parses under Lenient");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
let StringFunc::FloorTo { field, .. } = string_func.as_ref() else {
panic!("expected StringFunc::FloorTo");
};
assert_eq!(parsed.resolver().resolve(field.sym), "DAY");
assert_eq!(
Renderer::new(Lenient)
.render_parsed(&parsed)
.expect("FLOOR(x TO field) renders"),
"SELECT FLOOR(x TO DAY)",
);
let parsed = parse_with("SELECT FLOOR(x, 2)", crate::ParseConfig::new(Lenient))
.expect("FLOOR(x, 2) parses as an ordinary call");
assert!(
matches!(project_expr(&parsed), Expr::Function { .. }),
"FLOOR(x, 2) must stay an ordinary Expr::Function, not StringFunc::FloorTo",
);
assert!(
parse_with(
"SELECT FLOOR(x TO DAY)",
crate::ParseConfig::new(crate::dialect::Ansi)
)
.is_err(),
"ANSI has no FLOOR TO-field special form",
);
assert!(
parse_with("SELECT FLOOR(x TO DAY)", crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL has no FLOOR TO-field grammar (pg_query-verified)",
);
}
#[test]
fn mysql_convert_special_form() {
use crate::ast::{CastSyntax, StringFunc};
use crate::dialect::Lenient;
let parsed = parse_with("SELECT CONVERT(1, SIGNED)", crate::ParseConfig::new(MySql))
.expect("CONVERT(expr, type) parses");
let Expr::Cast { syntax, .. } = project_expr(&parsed) else {
panic!("expected Expr::Cast for the comma form");
};
assert_eq!(*syntax, CastSyntax::Convert);
let parsed = parse_with(
"SELECT CONVERT('x' USING utf8mb4)",
crate::ParseConfig::new(MySql),
)
.expect("CONVERT(expr USING cs) parses");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc for the USING form");
};
assert!(matches!(
string_func.as_ref(),
StringFunc::ConvertUsing { .. }
));
for sql in [
"SELECT CONVERT(1, SIGNED)",
"SELECT CONVERT('1.5', DECIMAL(10, 2))",
"SELECT CONVERT(1 + 2 USING utf8mb4)",
"SELECT CONVERT('x' USING `utf8mb4`)",
"SELECT CONVERT('x' USING 'latin1')",
"SELECT CONVERT('x' USING binary)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Lenient))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
assert_eq!(
Renderer::new(Lenient)
.render_parsed(&parsed)
.unwrap_or_else(|e| panic!("{sql:?} renders: {e:?}")),
sql,
"{sql:?} round-trips"
);
}
for sql in [
"SELECT CONVERT('x', CHAR(10))",
"SELECT CONVERT('x', CHAR(10) CHARACTER SET utf8mb4)",
"SELECT CONVERT(CONVERT('x', CHAR) USING utf8mb4)",
"SELECT CONVERT(CONVERT('x' USING utf8mb4), CHAR)",
] {
parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
}
for sql in [
"SELECT CONVERT(1, INT)",
"SELECT CONVERT('x', VARCHAR)",
"SELECT CONVERT('x', VARCHAR(5) CHARACTER SET utf8mb4)",
"SELECT CONVERT('x', NCHAR CHARACTER SET utf8mb4)",
"SELECT CONVERT('x')",
"SELECT CONVERT('x', CHAR, BINARY)",
"SELECT CONVERT('x' AS CHAR)",
"SELECT CONVERT('x' USING)",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects {sql:?}"
);
}
parse_with(
"SELECT CONVERT('x' USING utf8mb4)",
crate::ParseConfig::new(Lenient),
)
.expect("Lenient admits CONVERT USING");
parse_with("SELECT CONVERT(1, INT)", crate::ParseConfig::new(Lenient))
.expect("Lenient CONVERT admits any target");
assert!(
parse_with(
"SELECT CONVERT('x' USING utf8mb4)",
crate::ParseConfig::new(Ansi)
)
.is_err(),
"ANSI has no CONVERT USING production",
);
}
#[test]
fn mysql_match_against_fulltext() {
use crate::ast::{MatchSearchModifier, StringFunc};
use crate::dialect::Lenient;
let parsed = parse_with(
"SELECT MATCH(a, b) AGAINST('x') FROM t",
crate::ParseConfig::new(MySql),
)
.expect("MATCH ... AGAINST parses");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
let StringFunc::MatchAgainst {
columns, modifier, ..
} = string_func.as_ref()
else {
panic!("expected StringFunc::MatchAgainst");
};
assert_eq!(columns.len(), 2);
assert!(modifier.is_none());
for (sql, want) in [
("SELECT MATCH(a, b) AGAINST('x') FROM t", None),
(
"SELECT MATCH(a, b) AGAINST('x' IN NATURAL LANGUAGE MODE) FROM t",
Some(MatchSearchModifier::NaturalLanguage),
),
(
"SELECT MATCH(a, b) AGAINST('x' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION) FROM t",
Some(MatchSearchModifier::NaturalLanguageQueryExpansion),
),
(
"SELECT MATCH(a, b) AGAINST('x' IN BOOLEAN MODE) FROM t",
Some(MatchSearchModifier::Boolean),
),
(
"SELECT MATCH(a, b) AGAINST('x' WITH QUERY EXPANSION) FROM t",
Some(MatchSearchModifier::QueryExpansion),
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Lenient))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("{sql:?}: expected Expr::StringFunc");
};
let StringFunc::MatchAgainst { modifier, .. } = string_func.as_ref() else {
panic!("{sql:?}: expected StringFunc::MatchAgainst");
};
assert_eq!(*modifier, want, "{sql:?} modifier");
assert_eq!(
Renderer::new(Lenient)
.render_parsed(&parsed)
.unwrap_or_else(|e| panic!("{sql:?} renders: {e:?}")),
sql,
"{sql:?} round-trips"
);
}
for sql in [
"SELECT MATCH(t.a, t.b) AGAINST('x') FROM t",
"SELECT MATCH(a) AGAINST(1 + 2) FROM t",
"SELECT MATCH(a, b) AGAINST(concat('x', 'y')) FROM t",
"SELECT 1 FROM t WHERE MATCH(a, b) AGAINST('x') > 0.5",
"SELECT 1 FROM t ORDER BY MATCH(a, b) AGAINST('x') DESC",
] {
parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
}
for sql in [
"SELECT MATCH() AGAINST('x') FROM t",
"SELECT MATCH(a + 1) AGAINST('x') FROM t",
"SELECT MATCH('lit') AGAINST('x') FROM t",
"SELECT MATCH(a, b) AGAINST() FROM t",
"SELECT MATCH(a, b) FROM t",
"SELECT MATCH(a, b) AGAINST(1 > 2) FROM t",
"SELECT MATCH(a, b) AGAINST('x' IN BOOLEAN MODE WITH QUERY EXPANSION) FROM t",
"SELECT MATCH(a, b) AGAINST('x' IN QUERY EXPANSION) FROM t",
"SELECT MATCH(a, b) AGAINST('x' IN NATURAL LANGUAGE) FROM t",
"SELECT MATCH(a, b) AGAINST('x' WITH EXPANSION) FROM t",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(MySql)).is_err(),
"MySQL rejects {sql:?}"
);
}
parse_with("SELECT 'abc' MATCH 'a'", crate::ParseConfig::new(Sqlite))
.expect("SQLite infix MATCH operator still parses");
assert!(
parse_with(
"SELECT MATCH(a, b) AGAINST('x') FROM t",
crate::ParseConfig::new(Ansi)
)
.is_err(),
"ANSI has no MATCH ... AGAINST special form",
);
}
#[test]
fn postgres_rejects_invalid_uescape_delimiter() {
for sql in [
"SELECT U&'wrong: +0061' UESCAPE '+'",
"SELECT U&'d0061' UESCAPE '5'",
"SELECT U&'d0061' UESCAPE 'a'",
"SELECT U&'d0061' UESCAPE ''''",
"SELECT U&'d0061' UESCAPE '!!'",
] {
parse_with(sql, crate::ParseConfig::new(Postgres))
.expect_err(&format!("invalid UESCAPE delimiter: {sql:?}"));
}
for sql in [
"SELECT U&'d!0061' UESCAPE '!'",
"SELECT U&'d0061' UESCAPE '-'",
"SELECT U&'d0061' UESCAPE 'g'",
"SELECT U&'d0061' UESCAPE '\\'",
] {
parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql:?}: {err:?}"));
}
}
#[test]
fn json_value_query_exists_map_to_json_func() {
use crate::ast::{JsonFuncKind, JsonWrapperBehavior};
for (sql, kind) in [
(
"SELECT JSON_VALUE(js, '$' RETURNING int DEFAULT 0 ON EMPTY ERROR ON ERROR)",
JsonFuncKind::Value,
),
(
"SELECT JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER OMIT QUOTES)",
JsonFuncKind::Query,
),
(
"SELECT JSON_EXISTS(js, '$' PASSING 1 AS a TRUE ON ERROR)",
JsonFuncKind::Exists,
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let Expr::JsonFunc { json_func, .. } = project_expr(&parsed) else {
panic!("{sql:?}: expected Expr::JsonFunc");
};
assert_eq!(json_func.kind, kind);
if matches!(kind, JsonFuncKind::Query) {
assert_eq!(json_func.wrapper, JsonWrapperBehavior::Conditional);
} else {
assert_eq!(json_func.wrapper, JsonWrapperBehavior::Unspecified);
}
}
}
#[test]
fn json_object_standard_form_vs_legacy_function() {
for sql in [
"SELECT JSON_OBJECT('a': 1)",
"SELECT JSON_OBJECT('a' VALUE 1, 'b': 2 ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb)",
"SELECT JSON_OBJECT(RETURNING jsonb)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
assert!(
matches!(project_expr(&parsed), Expr::JsonObject { .. }),
"{sql:?}: expected Expr::JsonObject",
);
}
for sql in [
"SELECT JSON_OBJECT('{a,1}')",
"SELECT JSON_OBJECT('{a}', '{1}')",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
assert!(
matches!(project_expr(&parsed), Expr::Function { .. }),
"{sql:?}: expected the legacy Expr::Function",
);
}
}
#[test]
fn is_json_predicate_parses() {
use crate::ast::JsonItemType;
let parsed = parse_with(
"SELECT js IS NOT JSON ARRAY WITH UNIQUE KEYS",
crate::ParseConfig::new(Postgres),
)
.expect("IS JSON predicate parses");
let Expr::IsJson { is_json, .. } = project_expr(&parsed) else {
panic!("expected Expr::IsJson");
};
assert!(is_json.negated);
assert_eq!(is_json.item_type, JsonItemType::Array);
assert!(is_json.unique_keys);
}
#[test]
fn sqljson_over_accept_boundary_rejects() {
for sql in [
"SELECT JSON_OBJECT(1 + 2 VALUE 3)", "SELECT JSON('1' FORMAT JSON ENCODING foo)", "SELECT JSON('1' FORMAT JSONB)", "SELECT JSON_EXISTS(js, '$' RETURNING int)", "SELECT JSON_VALUE(js, '$' WITH WRAPPER)", "SELECT JSON_SCALAR(1 FORMAT JSON)", "SELECT JSON_QUERY(js, '$' WITH ARRAY)", "SELECT JSON_OBJECTAGG('k': v ORDER BY v)", ] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"{sql:?}: PostgreSQL rejects this at raw parse, so we must too",
);
}
}
#[test]
fn sqljson_gated_off_leaves_keywords_as_names() {
for sql in [
"SELECT JSON_VALUE(js, '$' RETURNING int)",
"SELECT JSON_QUERY(js, '$' WITH WRAPPER)",
"SELECT JSON_OBJECT('a': 1)",
] {
parse_with(sql, crate::ParseConfig::new(TestDialect))
.expect_err(&format!("{sql:?}: ANSI has no SQL/JSON forms"));
parse_with(sql, crate::ParseConfig::new(MySql))
.expect_err(&format!("{sql:?}: MySQL has no SQL/JSON standard forms"));
}
}
#[test]
fn sqljson_forms_round_trip() {
for sql in [
"SELECT JSON_VALUE(js, '$' RETURNING int DEFAULT 0 ON EMPTY ERROR ON ERROR)",
"SELECT JSON_QUERY(js, '$' RETURNING jsonb WITH WRAPPER OMIT QUOTES)",
"SELECT JSON_EXISTS(js, '$' PASSING 1 AS a UNKNOWN ON ERROR)",
"SELECT JSON_OBJECT('a': 1 ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb)",
"SELECT JSON_ARRAY(1, 2 NULL ON NULL RETURNING jsonb)",
"SELECT JSON_OBJECTAGG('k' VALUE v RETURNING jsonb) FILTER (WHERE v > 0)",
"SELECT JSON_ARRAYAGG(v ORDER BY v RETURNING jsonb) OVER ()",
"SELECT JSON('1' FORMAT JSON ENCODING UTF8 WITH UNIQUE KEYS)",
"SELECT JSON_SERIALIZE('1' RETURNING text FORMAT JSON)",
"SELECT js IS NOT JSON OBJECT WITH UNIQUE KEYS",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|e| panic!("{sql:?}: {e}"));
let reparsed = parse_with(&rendered, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("re-parse of {rendered:?}: {e:?}"));
let rerendered = Renderer::new(Postgres)
.render_parsed(&reparsed)
.unwrap_or_else(|e| panic!("{rendered:?}: {e}"));
assert_eq!(rendered, rerendered, "stable round-trip for {sql:?}");
}
}
#[test]
fn xml_functions_map_to_xml_func_variants() {
use crate::ast::{XmlFunc, XmlStandalone};
let parsed = parse_with(
"SELECT xmlelement(NAME root, xmlattributes('v' AS a, 1 + 1 AS n), 'body', xmlelement(NAME leaf))",
crate::ParseConfig::new(Postgres),
)
.expect("xmlelement parses");
let Expr::XmlFunc { xml_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::XmlFunc");
};
let XmlFunc::Element {
attributes,
content,
..
} = xml_func.as_ref()
else {
panic!("expected XmlFunc::Element");
};
assert_eq!(attributes.len(), 2);
assert!(attributes.iter().all(|attr| attr.name.is_some()));
assert_eq!(content.len(), 2);
let parsed = parse_with(
"SELECT xmlroot(x, version no value, standalone no value)",
crate::ParseConfig::new(Postgres),
)
.expect("xmlroot parses");
let Expr::XmlFunc { xml_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::XmlFunc");
};
let XmlFunc::Root {
version,
standalone,
..
} = xml_func.as_ref()
else {
panic!("expected XmlFunc::Root");
};
assert!(version.is_none(), "VERSION NO VALUE is a None version");
assert_eq!(*standalone, XmlStandalone::NoValue);
}
#[test]
fn is_document_predicate_parses() {
let parsed = parse_with(
"SELECT xml '<a/>' IS NOT DOCUMENT",
crate::ParseConfig::new(Postgres),
)
.expect("IS NOT DOCUMENT parses");
let Expr::IsDocument { negated, .. } = project_expr(&parsed) else {
panic!("expected Expr::IsDocument");
};
assert!(negated);
}
#[test]
fn xml_over_accept_boundary_rejects() {
for sql in [
"SELECT xmlelement(foo)", "SELECT xmlelement(NAME foo, 'c', xmlattributes(1 AS a))", "SELECT xmlforest()", "SELECT xmlparse(x)", "SELECT xmlparse(document x whitespace)", "SELECT xmlpi(name foo, 'a', 'b')", "SELECT xmlroot(x)", "SELECT xmlroot(x, version '1.0', standalone maybe)", "SELECT xmlserialize(x as text)", "SELECT xmlserialize(document x)", "SELECT xmlexists('//a')", "SELECT xmlexists('a' || 'b' passing x)", ] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"{sql:?}: PostgreSQL rejects this at raw parse, so we must too",
);
}
}
#[test]
fn xml_gated_off_leaves_keywords_as_names() {
for sql in [
"SELECT xmlelement(NAME root, 'body')",
"SELECT xmlserialize(DOCUMENT x AS text)",
"SELECT x IS DOCUMENT",
] {
parse_with(sql, crate::ParseConfig::new(TestDialect))
.expect_err(&format!("{sql:?}: ANSI has no SQL/XML forms"));
parse_with(sql, crate::ParseConfig::new(MySql))
.expect_err(&format!("{sql:?}: MySQL has no SQL/XML forms"));
}
}
#[test]
fn xmlagg_stays_an_ordinary_aggregate() {
let parsed = parse_with(
"SELECT xmlagg(x ORDER BY y)",
crate::ParseConfig::new(Postgres),
)
.expect("xmlagg parses as a call");
assert!(
matches!(project_expr(&parsed), Expr::Function { .. }),
"xmlagg is an ordinary aggregate call, not a special form",
);
}
#[test]
fn xml_forms_round_trip() {
for sql in [
"SELECT xmlelement(NAME root, xmlattributes('v' AS a, 1 AS n), 'body')",
"SELECT xmlelement(NAME foo)",
"SELECT xmlforest(a, b AS y)",
"SELECT xmlconcat(a, b, c)",
"SELECT xmlparse(DOCUMENT x STRIP WHITESPACE)",
"SELECT xmlparse(CONTENT x)",
"SELECT xmlpi(NAME php, 'echo')",
"SELECT xmlroot(x, VERSION '1.0', STANDALONE YES)",
"SELECT xmlroot(x, VERSION no value)",
"SELECT xmlserialize(DOCUMENT x AS text INDENT)",
"SELECT xmlserialize(CONTENT x AS text NO INDENT)",
"SELECT xmlexists('//a' PASSING BY REF doc BY REF)",
"SELECT xmlexists('//a' PASSING doc)",
"SELECT x IS DOCUMENT",
"SELECT x IS NOT DOCUMENT",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|e| panic!("{sql:?}: {e}"));
let reparsed = parse_with(&rendered, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("re-parse of {rendered:?}: {e:?}"));
let rerendered = Renderer::new(Postgres)
.render_parsed(&reparsed)
.unwrap_or_else(|e| panic!("{rendered:?}: {e}"));
assert_eq!(rendered, rerendered, "stable round-trip for {sql:?}");
}
}
#[test]
fn string_special_forms_map_to_string_func_variants() {
use crate::ast::{StringFunc, TrimSide};
for sql in [
"SELECT SUBSTRING('abcdef' FROM 2 FOR 3)",
"SELECT SUBSTRING('abcdef' FOR 3 FROM 2)",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("{sql:?}: expected Expr::StringFunc");
};
let StringFunc::Substring { start, count, .. } = string_func.as_ref() else {
panic!("{sql:?}: expected StringFunc::Substring");
};
assert!(start.is_some() && count.is_some(), "{sql:?}: both operands");
}
let parsed = parse_with(
"SELECT POSITION('b' IN 'abc')",
crate::ParseConfig::new(Postgres),
)
.expect("POSITION parses");
assert!(matches!(
project_expr(&parsed),
Expr::StringFunc { string_func, .. } if matches!(**string_func, StringFunc::Position { .. }),
));
let parsed = parse_with(
"SELECT OVERLAY('abc' PLACING 'X' FROM 2 FOR 1)",
crate::ParseConfig::new(Postgres),
)
.expect("OVERLAY parses");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
let StringFunc::Overlay { count, .. } = string_func.as_ref() else {
panic!("expected StringFunc::Overlay");
};
assert!(count.is_some());
let parsed = parse_with(
"SELECT TRIM(TRAILING ' foo ')",
crate::ParseConfig::new(Postgres),
)
.expect("TRIM parses");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
let StringFunc::Trim {
side,
trim_chars,
from,
sources,
..
} = string_func.as_ref()
else {
panic!("expected StringFunc::Trim");
};
assert_eq!(*side, Some(TrimSide::Trailing));
assert!(trim_chars.is_none());
assert!(!from);
assert_eq!(sources.len(), 1);
let parsed = parse_with(
"SELECT TRIM(BOTH 'x' FROM 'y', 'z')",
crate::ParseConfig::new(Postgres),
)
.expect("TRIM parses");
let Expr::StringFunc { string_func, .. } = project_expr(&parsed) else {
panic!("expected Expr::StringFunc");
};
let StringFunc::Trim {
side,
trim_chars,
from,
sources,
..
} = string_func.as_ref()
else {
panic!("expected StringFunc::Trim");
};
assert_eq!(*side, Some(TrimSide::Both));
assert!(trim_chars.is_some());
assert!(from);
assert_eq!(sources.len(), 2, "PostgreSQL's trim_list is an expr_list");
}
#[test]
fn string_special_plain_calls_stay_ordinary_calls() {
for sql in [
"SELECT SUBSTRING('abcdef', 2, 3)",
"SELECT SUBSTR('abcdef', 2, 3)",
"SELECT TRIM('abc')",
"SELECT TRIM('a', 'b')",
"SELECT OVERLAY('abc', 'X', 2, 1)",
"SELECT SUBSTRING()",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
assert!(
matches!(project_expr(&parsed), Expr::Function { .. }),
"{sql:?}: the plain call stays an ordinary Expr::Function",
);
}
let parsed = parse_with(
"SELECT position('b', 'abc')",
crate::ParseConfig::new(Sqlite),
)
.expect("sqlite plain call");
assert!(matches!(project_expr(&parsed), Expr::Function { .. }));
}
#[test]
fn string_special_over_accept_boundary_rejects() {
for sql in [
"SELECT TRIM()", "SELECT TRIM(LEADING TRAILING 'x' FROM 'y')", "SELECT TRIM(LEADING 'x' 'y' FROM 'z')", "SELECT SUBSTRING('a' FROM 2 FOR 3 FOR 4)", "SELECT SUBSTRING('a' FROM 2 FOR 3 FROM 4)", "SELECT SUBSTRING('a' FROM 2, 3)", "SELECT SUBSTRING('a' SIMILAR 'p')", "SELECT OVERLAY('a' PLACING 'b')", "SELECT OVERLAY('a' PLACING 'b' FOR 1)", "SELECT POSITION('b')", "SELECT POSITION()", "SELECT POSITION('b', 'abc')", "SELECT POSITION(1 IN 2 OR 3)", "SELECT POSITION('b' NOT IN 'abc')", ] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"{sql:?}: PostgreSQL rejects this at raw parse, so we must too",
);
}
}
#[test]
fn string_special_gated_off_leaves_heads_ordinary() {
for sql in [
"SELECT SUBSTRING('abcdef' FROM 2)",
"SELECT POSITION('b' IN 'abc')",
"SELECT OVERLAY('abc' PLACING 'X' FROM 2)",
"SELECT TRIM(BOTH 'x' FROM 'y')",
"SELECT TRIM(FROM 'y')",
] {
parse_with(sql, crate::ParseConfig::new(Sqlite))
.expect_err(&format!("{sql:?}: SQLite has no keyword forms"));
}
for sql in [
"SELECT SUBSTRING('abcdef' FOR 3)",
"SELECT SUBSTRING('abcdef' SIMILAR 'a' ESCAPE '#')",
"SELECT TRIM(TRAILING ' foo ')",
"SELECT TRIM(FROM 'y')",
"SELECT TRIM('a', 'b')",
"SELECT OVERLAY('abc', 'X', 2, 1)",
] {
parse_with(sql, crate::ParseConfig::new(TestDialect))
.expect_err(&format!("{sql:?}: ANSI takes the standard shapes only"));
}
parse_with(
"SELECT SUBSTRING('abcdef' FROM 2 FOR 3)",
crate::ParseConfig::new(TestDialect),
)
.expect("ANSI accepts the standard FROM/FOR form");
}
#[test]
fn string_special_mysql_flavor_matches_engine() {
for sql in [
"SELECT SUBSTRING('abcdef' FROM 2 FOR 3)",
"SELECT SUBSTR('abcdef' FROM 2 FOR 3)", "SELECT SUBSTRING('abcdef', 2, 3)",
"SELECT POSITION('b' IN 'abc')",
"SELECT POSITION(1 IN 2 OR 3)", "SELECT TRIM(LEADING 'x' FROM 'y')",
"SELECT TRIM('x' FROM 'y')",
"SELECT TRIM ('abc')", "SELECT SUBSTRING ('abcdef', 2)", ] {
parse_with(sql, crate::ParseConfig::new(MySql))
.unwrap_or_else(|e| panic!("{sql:?}: mysql accepts: {e:?}"));
}
for sql in [
"SELECT SUBSTRING('abcdef' FOR 3)", "SELECT SUBSTRING('abcdef' FOR 3 FROM 2)",
"SELECT SUBSTRING('abcdef' SIMILAR 'a' ESCAPE '#')",
"SELECT SUBSTRING('abcdef')", "SELECT SUBSTRING('a', 2, 3, 4)",
"SELECT SUBSTR('abcdef')", "SELECT POSITION('a' = 'b' IN 'c')", "SELECT POSITION('b', 'abc')",
"SELECT OVERLAY('abc' PLACING 'X' FROM 2)", "SELECT TRIM(FROM 'y')", "SELECT TRIM(TRAILING ' foo ')", "SELECT TRIM('a', 'b')", "SELECT TRIM('a' FROM 'b', 'c')", "SELECT TRIM (LEADING 'x' FROM 'y')", "SELECT SUBSTRING ('abcdef' FROM 2)", ] {
parse_with(sql, crate::ParseConfig::new(MySql))
.expect_err(&format!("{sql:?}: mysql:8.4 parse-rejects (1064)"));
}
parse_with(
"SELECT TRIM(LEADING 'x' 'y' FROM 'z')",
crate::ParseConfig::new(MySql),
)
.expect("adjacent literals concatenate inside the MySQL trim chars");
}
#[test]
fn string_special_duckdb_flavor_matches_engine() {
for sql in [
"SELECT SUBSTRING('abcdef' FOR 3 FROM 2)",
"SELECT OVERLAY('abc' PLACING 'X' FROM 2 FOR 1)",
"SELECT TRIM(BOTH FROM 'a', 'b')",
"SELECT TRIM('a', 'b')",
"SELECT POSITION('a' || 'b' IN 'abc')",
] {
parse_with(sql, crate::ParseConfig::new(DuckDb))
.unwrap_or_else(|e| panic!("{sql:?}: duckdb accepts: {e:?}"));
}
for sql in [
"SELECT SUBSTRING('abcdef' SIMILAR 'a' ESCAPE '#')",
"SELECT OVERLAY('abc', 'X', 2, 1)",
"SELECT OVERLAY('abc')",
"SELECT OVERLAY()",
] {
parse_with(sql, crate::ParseConfig::new(DuckDb))
.expect_err(&format!("{sql:?}: duckdb parse-rejects"));
}
}
#[test]
fn string_special_forms_round_trip() {
for sql in [
"SELECT SUBSTRING('abcdef' FROM 2 FOR 3)",
"SELECT SUBSTRING('abcdef' FROM 2)",
"SELECT SUBSTRING('abcdef' FOR 3)",
"SELECT SUBSTRING('abcdef' FOR 3 FROM 2)", "SELECT SUBSTRING('abcdef' SIMILAR 'a' ESCAPE '#')",
"SELECT POSITION('b' IN 'abc')",
"SELECT POSITION('a' || 'b' IN 'abc')",
"SELECT OVERLAY('abc' PLACING 'X' FROM 2 FOR 1)",
"SELECT OVERLAY('abc' PLACING 'X' FROM 2)",
"SELECT TRIM(BOTH 'x' FROM 'xxabc')",
"SELECT TRIM(LEADING FROM 'xxabc')",
"SELECT TRIM(TRAILING ' foo ')",
"SELECT TRIM('x' FROM 'xxabc')",
"SELECT TRIM(FROM 'xxabc')",
"SELECT TRIM(BOTH 'x')",
"SELECT TRIM(LEADING 'x', 'y')",
"SELECT TRIM('a' FROM 'b', 'c')",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("{sql:?}: {e:?}"));
let rendered = Renderer::new(Postgres)
.render_parsed(&parsed)
.unwrap_or_else(|e| panic!("{sql:?}: {e}"));
let reparsed = parse_with(&rendered, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("re-parse of {rendered:?}: {e:?}"));
let rerendered = Renderer::new(Postgres)
.render_parsed(&reparsed)
.unwrap_or_else(|e| panic!("{rendered:?}: {e}"));
assert_eq!(rendered, rerendered, "stable round-trip for {sql:?}");
}
let parsed = parse_with(
"SELECT SUBSTR('abcdef' FROM 2 FOR 3)",
crate::ParseConfig::new(MySql),
)
.expect("parses");
let rendered = Renderer::new(crate::dialect::Lenient)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(rendered, "SELECT SUBSTRING('abcdef' FROM 2 FOR 3)");
parse_with(&rendered, crate::ParseConfig::new(MySql)).expect("the canonical render re-parses");
}
const PG_PREDICATE_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet = FeatureSet::ANSI.with(
FeatureDelta::EMPTY
.predicate_syntax(PredicateSyntax::POSTGRES)
.operator_syntax(OperatorSyntax::POSTGRES),
);
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn between_symmetric_is_recorded_and_round_trips() {
let parsed = parse_with(
"SELECT a BETWEEN SYMMETRIC 1 AND 2",
crate::ParseConfig::new(PG_PREDICATE_DIALECT),
)
.expect("parses");
let Expr::Between { symmetric, .. } = project_expr(&parsed) else {
panic!("expected a BETWEEN, got {:?}", project_expr(&parsed));
};
assert!(*symmetric, "SYMMETRIC is recorded");
let rendered = Renderer::new(PG_PREDICATE_DIALECT)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(rendered, "SELECT a BETWEEN SYMMETRIC 1 AND 2");
let asym = parse_with(
"SELECT a BETWEEN ASYMMETRIC 1 AND 2",
crate::ParseConfig::new(PG_PREDICATE_DIALECT),
)
.expect("ASYMMETRIC parses");
let Expr::Between { symmetric, .. } = project_expr(&asym) else {
panic!("expected a BETWEEN");
};
assert!(!*symmetric, "ASYMMETRIC is the non-symmetric default");
let rendered = Renderer::new(PG_PREDICATE_DIALECT)
.render_parsed(&asym)
.expect("renders");
assert_eq!(rendered, "SELECT a BETWEEN 1 AND 2");
}
#[test]
fn between_symmetric_is_rejected_where_the_gate_is_off() {
assert!(
parse_with(
"SELECT a BETWEEN SYMMETRIC 1 AND 2",
crate::ParseConfig::new(TestDialect)
)
.is_err()
);
assert!(
parse_with(
"SELECT a BETWEEN SYMMETRIC 1 AND 2",
crate::ParseConfig::new(MySql)
)
.is_err()
);
assert!(
parse_with(
"SELECT a BETWEEN 1 AND 2",
crate::ParseConfig::new(TestDialect)
)
.is_ok()
);
}
#[test]
fn postfix_isnull_notnull_fold_onto_is_null_with_a_spelling() {
use crate::ast::NullTestSpelling;
for (sql, negated) in [("SELECT a ISNULL", false), ("SELECT a NOTNULL", true)] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_PREDICATE_DIALECT))
.unwrap_or_else(|e| panic!("{sql}: {e:?}"));
let Expr::IsNull {
negated: got,
spelling,
..
} = project_expr(&parsed)
else {
panic!("{sql}: expected IsNull, got {:?}", project_expr(&parsed));
};
assert_eq!(*got, negated, "negated flag for {sql}");
assert_eq!(
*spelling,
NullTestSpelling::Postfix,
"postfix spelling for {sql}"
);
let rendered = Renderer::new(PG_PREDICATE_DIALECT)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(rendered, sql, "postfix spelling round-trips for {sql}");
}
let is_null = parse_with(
"SELECT a IS NOT NULL",
crate::ParseConfig::new(PG_PREDICATE_DIALECT),
)
.expect("parses");
let Expr::IsNull { spelling, .. } = project_expr(&is_null) else {
panic!("expected IsNull");
};
assert_eq!(*spelling, NullTestSpelling::Is);
}
#[test]
fn postfix_isnull_notnull_are_rejected_where_the_gate_is_off() {
assert!(
parse_with(
"SELECT a FROM t WHERE c ISNULL",
crate::ParseConfig::new(TestDialect)
)
.is_err()
);
assert!(
parse_with(
"SELECT a FROM t WHERE c NOTNULL",
crate::ParseConfig::new(MySql)
)
.is_err()
);
assert!(
parse_with(
"SELECT a FROM t WHERE c ISNULL",
crate::ParseConfig::new(Sqlite)
)
.is_ok()
);
}
const SQLITE_PREDICATE_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.predicate_syntax(PredicateSyntax::SQLITE));
FeatureDialect {
features: &FEATURES,
}
};
#[test]
fn two_word_not_null_postfix_folds_onto_is_null_and_round_trips() {
use crate::ast::NullTestSpelling;
let parsed = parse_with(
"SELECT a NOT NULL",
crate::ParseConfig::new(SQLITE_PREDICATE_DIALECT),
)
.expect("parses");
let Expr::IsNull {
negated, spelling, ..
} = project_expr(&parsed)
else {
panic!("expected IsNull, got {:?}", project_expr(&parsed));
};
assert!(*negated, "two-word NOT NULL is the negated null test");
assert_eq!(*spelling, NullTestSpelling::PostfixNotNull);
let rendered = Renderer::new(SQLITE_PREDICATE_DIALECT)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(
rendered, "SELECT a NOT NULL",
"two-word spelling round-trips"
);
}
#[test]
fn two_word_not_null_postfix_round_trips_exact_under_duckdb() {
use crate::ast::NullTestSpelling;
let parsed = parse_with(
"SELECT a NOT NULL",
crate::ParseConfig::new(DUCKDB_TYPE_DIALECT),
)
.expect("parses");
let Expr::IsNull {
negated, spelling, ..
} = project_expr(&parsed)
else {
panic!("expected IsNull, got {:?}", project_expr(&parsed));
};
assert!(*negated, "two-word NOT NULL is the negated null test");
assert_eq!(*spelling, NullTestSpelling::PostfixNotNull);
let rendered = Renderer::new(DUCKDB_TYPE_DIALECT)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(
rendered, "SELECT a NOT NULL",
"two-word spelling round-trips exact under DuckDB"
);
}
#[test]
fn two_word_not_null_postfix_is_rejected_where_the_gate_is_off() {
assert!(parse_with("SELECT a NOT NULL", crate::ParseConfig::new(Postgres)).is_err());
assert!(parse_with("SELECT a NOTNULL", crate::ParseConfig::new(Postgres)).is_ok());
assert!(parse_with("SELECT a NOT NULL", crate::ParseConfig::new(Ansi)).is_err());
}
#[test]
fn two_word_not_null_does_not_disturb_the_not_led_predicate_family() {
for sql in [
"SELECT a NOT IN (1, 2)",
"SELECT a NOT LIKE b",
"SELECT a NOT BETWEEN 1 AND 2",
"SELECT a NOT GLOB b",
"SELECT NOT a",
"SELECT NOT a NOT NULL",
] {
parse_with(sql, crate::ParseConfig::new(Sqlite)).unwrap_or_else(|e| panic!("{sql}: {e:?}"));
}
let parsed =
parse_with("SELECT NOT a NOT NULL", crate::ParseConfig::new(Sqlite)).expect("parses");
let Expr::UnaryOp { expr, .. } = project_expr(&parsed) else {
panic!("expected a prefix NOT, got {:?}", project_expr(&parsed));
};
assert!(
matches!(**expr, Expr::IsNull { negated: true, .. }),
"prefix NOT wraps the postfix NOT NULL"
);
}
#[test]
fn is_normalized_records_the_form_and_round_trips() {
use crate::ast::NormalizationForm;
for (sql, want_form, want_neg) in [
("SELECT a IS NORMALIZED", None, false),
(
"SELECT a IS NFC NORMALIZED",
Some(NormalizationForm::Nfc),
false,
),
(
"SELECT a IS NOT NFKD NORMALIZED",
Some(NormalizationForm::Nfkd),
true,
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(PG_PREDICATE_DIALECT))
.unwrap_or_else(|e| panic!("{sql}: {e:?}"));
let Expr::IsNormalized { form, negated, .. } = project_expr(&parsed) else {
panic!(
"{sql}: expected IsNormalized, got {:?}",
project_expr(&parsed)
);
};
assert_eq!(*form, want_form, "form for {sql}");
assert_eq!(*negated, want_neg, "negated for {sql}");
let rendered = Renderer::new(PG_PREDICATE_DIALECT)
.render_parsed(&parsed)
.expect("renders");
assert_eq!(rendered, sql, "round-trip for {sql}");
}
}
#[test]
fn is_normalized_is_rejected_where_the_gate_is_off() {
assert!(
parse_with(
"SELECT a IS NORMALIZED",
crate::ParseConfig::new(TestDialect)
)
.is_err()
);
assert!(parse_with("SELECT a IS NFC NORMALIZED", crate::ParseConfig::new(MySql)).is_err());
assert!(
parse_with(
"SELECT normalized FROM t",
crate::ParseConfig::new(TestDialect)
)
.is_ok()
);
}
#[test]
fn pg_range_predicates_bind_tighter_than_comparison() {
let pg = Renderer::new(Postgres);
for sql in [
"SELECT true <> -1 BETWEEN 1 AND 1",
"SELECT false <= -1 BETWEEN 1 AND 1",
"SELECT false >= -1 BETWEEN 1 AND 1",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("PG accepts {sql}: {e}"));
let Expr::BinaryOp { op, right, .. } = project_expr(&parsed) else {
panic!("{sql}: root is the comparison operator");
};
assert!(
matches!(
op,
BinaryOperator::NotEq(_) | BinaryOperator::LtEq | BinaryOperator::GtEq
),
"{sql}: root is a comparison",
);
assert!(
matches!(**right, Expr::Between { .. }),
"{sql}: the BETWEEN is the comparison's right operand",
);
let rendered = pg.render_parsed(&parsed).expect("PG renders");
assert_eq!(rendered, sql, "{sql}: minimal paren-free round-trip");
}
for (sql, is_range) in [
(
"SELECT a = b IN (c)",
(|e: &Expr<NoExt>| matches!(e, Expr::InList { .. })) as fn(&Expr<NoExt>) -> bool,
),
(
"SELECT a = b LIKE 'x'",
(|e: &Expr<NoExt>| matches!(e, Expr::Like { .. })) as fn(&Expr<NoExt>) -> bool,
),
] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("PG accepts {sql}: {e}"));
let Expr::BinaryOp {
op: BinaryOperator::Eq(_),
right,
..
} = project_expr(&parsed)
else {
panic!("{sql}: root is `=`");
};
assert!(
is_range(right),
"{sql}: the range predicate is `=`'s right operand"
);
assert_eq!(
pg.render_parsed(&parsed).expect("renders"),
sql,
"{sql}: round-trip"
);
}
let sql = "SELECT (a = b) BETWEEN c AND d";
let parsed =
parse_with(sql, crate::ParseConfig::new(Postgres)).expect("PG accepts the grouped form");
assert!(matches!(project_expr(&parsed), Expr::Between { .. }));
assert_eq!(
pg.render_parsed(&parsed).expect("renders"),
sql,
"grouped principal keeps parens"
);
}
#[test]
fn range_predicate_precedence_above_comparison_is_dialect_data() {
for sql in [
"SELECT a = b BETWEEN c AND d",
"SELECT true <> -1 BETWEEN 1 AND 1",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_ok(),
"PostgreSQL accepts {sql}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(Ansi)).is_err(),
"ANSI (default rank) rejects {sql}"
);
}
}
#[test]
fn pg_range_predicate_chains_reject_like_comparisons() {
for sql in [
"SELECT a BETWEEN b AND c BETWEEN d AND e",
"SELECT a NOT BETWEEN b AND c BETWEEN d AND e",
"SELECT a LIKE 'x' LIKE 'y'",
"SELECT a = b = c",
] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_err(),
"PostgreSQL rejects the chain {sql}"
);
}
let parsed = parse_with(
"SELECT a BETWEEN b AND c = d",
crate::ParseConfig::new(Postgres),
)
.expect("comparison after BETWEEN");
let Expr::BinaryOp {
op: BinaryOperator::Eq(_),
left,
..
} = project_expr(&parsed)
else {
panic!("root is `=`");
};
assert!(
matches!(**left, Expr::Between { .. }),
"the BETWEEN is `=`'s left operand"
);
}
#[test]
fn pg_and_duckdb_is_family_ranks_below_comparison() {
let pg = Renderer::new(Postgres);
let duck = Renderer::new(DUCKDB_TYPE_DIALECT);
for sql in [
"SELECT a <> b IS NULL",
"SELECT a = b IS NULL",
"SELECT a < b IS NOT NULL",
] {
let pg_parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|e| panic!("PG accepts {sql}: {e}"));
let duck_parsed = parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT))
.unwrap_or_else(|e| panic!("DuckDB accepts {sql}: {e}"));
assert!(
matches!(project_expr(&pg_parsed), Expr::IsNull { .. }),
"PG {sql}: root is the null test",
);
assert!(
matches!(project_expr(&duck_parsed), Expr::IsNull { .. }),
"DuckDB {sql}: root is the null test",
);
assert_eq!(
pg.render_parsed(&pg_parsed).expect("renders"),
sql,
"PG {sql}: minimal paren-free round-trip",
);
assert_eq!(
duck.render_parsed(&duck_parsed).expect("renders"),
sql,
"DuckDB {sql}: minimal paren-free round-trip",
);
}
let parsed = parse_with("SELECT a IS NULL = b", crate::ParseConfig::new(Postgres))
.expect("PG accepts IS NULL = b");
let Expr::BinaryOp {
op: BinaryOperator::Eq(_),
left,
..
} = project_expr(&parsed)
else {
panic!("root is `=`");
};
assert!(
matches!(**left, Expr::IsNull { .. }),
"IS NULL is `=`'s left operand"
);
assert_eq!(
pg.render_parsed(&parsed).expect("renders"),
"SELECT (a IS NULL) = b",
"round-trip",
);
let parsed = parse_with(
"SELECT a = b IS DISTINCT FROM c",
crate::ParseConfig::new(Postgres),
)
.expect("parses");
let Expr::BinaryOp {
op: BinaryOperator::IsDistinctFrom(_),
left,
..
} = project_expr(&parsed)
else {
panic!("root is IS DISTINCT FROM");
};
assert!(matches!(
**left,
Expr::BinaryOp {
op: BinaryOperator::Eq(_),
..
}
));
assert_eq!(
pg.render_parsed(&parsed).expect("renders"),
"SELECT a = b IS DISTINCT FROM c"
);
let parsed = parse_with(
"SELECT a IS DISTINCT FROM b = c",
crate::ParseConfig::new(Postgres),
)
.expect("parses");
let Expr::BinaryOp {
op: BinaryOperator::IsDistinctFrom(_),
right,
..
} = project_expr(&parsed)
else {
panic!("root is IS DISTINCT FROM");
};
assert!(matches!(
**right,
Expr::BinaryOp {
op: BinaryOperator::Eq(_),
..
}
));
assert_eq!(
pg.render_parsed(&parsed).expect("renders"),
"SELECT a IS DISTINCT FROM b = c"
);
}
#[test]
fn is_family_below_comparison_is_dialect_data() {
for sql in ["SELECT a <> b IS NULL", "SELECT a = b IS DISTINCT FROM c"] {
assert!(
parse_with(sql, crate::ParseConfig::new(Postgres)).is_ok(),
"PostgreSQL accepts {sql}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(DUCKDB_TYPE_DIALECT)).is_ok(),
"DuckDB accepts {sql}"
);
assert!(
parse_with(sql, crate::ParseConfig::new(Ansi)).is_err(),
"ANSI (default rank) rejects {sql}"
);
}
assert!(
parse_with("SELECT a <> b IS NULL", crate::ParseConfig::new(MySql)).is_ok(),
"MySQL accepts via left-associative comparison",
);
}
const STRUCT_CONSTRUCTOR_DIALECT: FeatureDialect = {
const FEATURES: FeatureSet =
FeatureSet::ANSI.with(FeatureDelta::EMPTY.expression_syntax(ExpressionSyntax {
struct_constructor: true,
..ExpressionSyntax::ANSI
}));
FeatureDialect {
features: &FEATURES,
}
};
fn project_struct_constructor<'a>(
parsed: &'a Parsed,
sql: &str,
) -> &'a crate::ast::StructConstructorExpr<NoExt> {
match project_expr(parsed) {
Expr::StructConstructor { constructor, .. } => constructor,
other => panic!("expected a struct constructor for {sql:?}, got {other:?}"),
}
}
#[test]
fn struct_constructor_parses_typeless_forms() {
let sql = "SELECT STRUCT(1, 2)";
let parsed = parse_with(sql, crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT))
.expect("positional STRUCT parses");
let ctor = project_struct_constructor(&parsed, sql);
assert!(ctor.fields.is_empty(), "typeless form carries no fields");
assert_eq!(ctor.args.len(), 2);
assert!(ctor.args.iter().all(|arg| arg.alias.is_none()));
let sql = "SELECT STRUCT(x AS a, y AS b)";
let parsed = parse_with(sql, crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT))
.expect("named STRUCT parses");
let ctor = project_struct_constructor(&parsed, sql);
assert!(ctor.fields.is_empty());
let aliases: Vec<_> = ctor
.args
.iter()
.map(|arg| {
parsed
.resolver()
.resolve(arg.alias.as_ref().expect("alias").sym)
})
.collect();
assert_eq!(aliases, ["a", "b"]);
let parsed = parse_with(
"SELECT STRUCT()",
crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT),
)
.expect("empty STRUCT parses");
let ctor = project_struct_constructor(&parsed, "SELECT STRUCT()");
assert!(ctor.fields.is_empty() && ctor.args.is_empty());
}
#[test]
fn struct_constructor_parses_the_typed_form() {
let sql = "SELECT STRUCT<a INT64, b STRING>(1, 'x')";
let parsed = parse_with(sql, crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT))
.expect("typed STRUCT parses");
let ctor = project_struct_constructor(&parsed, sql);
assert_eq!(ctor.fields.len(), 2);
assert_eq!(ctor.args.len(), 2);
let names: Vec<_> = ctor
.fields
.iter()
.map(|field| {
parsed
.resolver()
.resolve(field.name.as_ref().expect("field name").sym)
})
.collect();
assert_eq!(names, ["a", "b"]);
assert!(
ctor.fields
.iter()
.all(|field| matches!(field.ty, DataType::UserDefined { .. })),
"INT64/STRING resolve through the user-defined type path",
);
let sql = "SELECT STRUCT<INT64>(5)";
let parsed = parse_with(sql, crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT))
.expect("anonymous typed field");
let ctor = project_struct_constructor(&parsed, sql);
assert_eq!(ctor.fields.len(), 1);
assert!(ctor.fields[0].name.is_none());
let sql = "SELECT STRUCT<NUMERIC(10, 2)>(x)";
let parsed = parse_with(sql, crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT))
.expect("parameterized typed field");
let ctor = project_struct_constructor(&parsed, sql);
assert!(ctor.fields[0].name.is_none());
let parsed = parse_with(
"SELECT STRUCT<>(1)",
crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT),
)
.expect("`<>` comparison");
assert!(matches!(
project_expr(&parsed),
Expr::BinaryOp {
op: BinaryOperator::NotEq(_),
..
}
));
assert!(
parse_with(
"SELECT STRUCT<a INT64(1)",
crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT)
)
.is_err()
);
}
#[test]
fn struct_constructor_round_trips() {
for sql in [
"SELECT STRUCT(1, 2)",
"SELECT STRUCT(x AS a, y AS b)",
"SELECT STRUCT<a INT64, b STRING>(1, 'x')",
"SELECT STRUCT<INT64>(5)",
"SELECT STRUCT()",
] {
let parsed = parse_with(sql, crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
let rendered = Renderer::new(STRUCT_CONSTRUCTOR_DIALECT)
.render_parsed(&parsed)
.unwrap_or_else(|err| panic!("{sql}: {err}"));
assert_eq!(rendered, sql, "exact round-trip for {sql}");
}
}
#[test]
fn struct_constructor_dispatch_is_bounded_to_the_paren_or_angle_lead() {
let parsed = parse_with(
"SELECT struct",
crate::ParseConfig::new(STRUCT_CONSTRUCTOR_DIALECT),
)
.expect("bare struct is a column");
assert_eq!(column_name(&parsed, project_expr(&parsed)), "struct");
}
#[test]
fn struct_call_stays_an_ordinary_function_without_the_gate() {
for sql in ["SELECT struct(1, 2)", "SELECT STRUCT(x, y)"] {
let parsed = parse_with(sql, crate::ParseConfig::new(Postgres))
.unwrap_or_else(|err| panic!("{sql}: {err:?}"));
assert!(
matches!(project_expr(&parsed), Expr::Function { .. }),
"{sql} must stay an ordinary Expr::Function under PostgreSQL",
);
}
let parsed = parse_with("SELECT struct(1)", crate::ParseConfig::new(TestDialect))
.expect("ANSI call parses");
assert!(matches!(project_expr(&parsed), Expr::Function { .. }));
assert!(
parse_with(
"SELECT STRUCT<a INT64>(1)",
crate::ParseConfig::new(Postgres)
)
.is_err()
);
let parsed = parse_with("SELECT struct < x", crate::ParseConfig::new(Postgres))
.expect("comparison parses");
assert!(matches!(
project_expr(&parsed),
Expr::BinaryOp {
op: BinaryOperator::Lt,
..
}
));
}
#[test]
fn struct_constructor_is_on_for_the_bigquery_preset() {
use crate::dialect::BigQuery;
let sql = "SELECT STRUCT(1 AS a)";
let parsed = parse_with(sql, crate::ParseConfig::new(BigQuery))
.expect("the BigQuery preset admits STRUCT(...)");
let ctor = project_struct_constructor(&parsed, sql);
assert_eq!(ctor.args.len(), 1);
let typed = "SELECT STRUCT<a INT64>(1)";
let parsed = parse_with(typed, crate::ParseConfig::new(BigQuery))
.expect("the BigQuery preset admits STRUCT<...>()");
assert_eq!(project_struct_constructor(&parsed, typed).fields.len(), 1);
}
#[test]
fn list_comprehension_single_and_multi_var_parse_under_duckdb() {
use crate::ast::{ArrayExpr, Expr};
use crate::dialect::{Ansi, DuckDb};
use crate::render::Renderer;
let sql = "SELECT [x * 2 FOR x IN [1, 2, 3]]";
let parsed =
parse_with(sql, crate::ParseConfig::new(DuckDb)).expect("single-var comprehension");
let multi = "SELECT [x + i FOR x, i IN [10, 9, 8]]";
let parsed_multi =
parse_with(multi, crate::ParseConfig::new(DuckDb)).expect("multi-var comprehension");
fn first_array(parsed: &crate::parser::Parsed) -> &ArrayExpr {
use crate::ast::{SelectItem, SetExpr, Statement};
let Statement::Query { query, .. } = &parsed.statements()[0] else {
panic!("query");
};
let SetExpr::Select { select, .. } = &query.body else {
panic!("select");
};
let SelectItem::Expr { expr, .. } = &select.projection[0] else {
panic!("proj {:?}", select.projection[0]);
};
let Expr::Array { array, .. } = expr else {
panic!("array {:?}", expr);
};
array.as_ref()
}
match first_array(&parsed) {
ArrayExpr::Comprehension { comprehension, .. } => {
assert_eq!(comprehension.vars.len(), 1);
}
other => panic!("expected comprehension, got {other:?}"),
}
match first_array(&parsed_multi) {
ArrayExpr::Comprehension { comprehension, .. } => {
assert_eq!(comprehension.vars.len(), 2, "for x, i must yield two vars");
}
other => panic!("expected multi-var comprehension, got {other:?}"),
}
let rendered = Renderer::new(crate::parser::FeatureDialect {
features: &crate::ast::dialect::FeatureSet::DUCKDB,
})
.render_parsed(&parsed_multi)
.expect("render");
parse_with(&rendered, crate::ParseConfig::new(DuckDb))
.expect("reparse rendered multi-var comprehension");
assert!(
rendered.eq_ignore_ascii_case(multi),
"rendered {rendered:?} should match {multi:?} ignoring keyword case"
);
parse_with(multi, crate::ParseConfig::new(Ansi)).expect_err("ANSI rejects list comprehensions");
parse_with(
"SELECT [x FOR x IN [1, 2, 3] IF x > 1]",
crate::ParseConfig::new(DuckDb),
)
.expect("filter form");
parse_with(
"WITH base AS (SELECT [4, 5, 6] AS l) SELECT [x FOR x, i IN l IF i != 2] AS filtered FROM base",
crate::ParseConfig::new(DuckDb),
)
.expect("corpus multi-var with filter");
}