This commit is contained in:
Ryan Schneider 2025-07-07 08:24:39 -07:00 committed by GitHub
commit bd6594327a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 192 additions and 0 deletions

View file

@ -756,6 +756,12 @@ pub enum Expr {
IsNull(Box<Expr>),
/// `IS NOT NULL` operator
IsNotNull(Box<Expr>),
/// `NOTNULL` or `NOT NULL` operator
NotNull {
expr: Box<Expr>,
/// true if `NOTNULL`, false if `NOT NULL`
one_word: bool,
},
/// `IS UNKNOWN` operator
IsUnknown(Box<Expr>),
/// `IS NOT UNKNOWN` operator
@ -1430,6 +1436,12 @@ impl fmt::Display for Expr {
Expr::IsNotFalse(ast) => write!(f, "{ast} IS NOT FALSE"),
Expr::IsNull(ast) => write!(f, "{ast} IS NULL"),
Expr::IsNotNull(ast) => write!(f, "{ast} IS NOT NULL"),
Expr::NotNull { expr, one_word } => write!(
f,
"{} {}",
expr,
if *one_word { "NOTNULL" } else { "NOT NULL" }
),
Expr::IsUnknown(ast) => write!(f, "{ast} IS UNKNOWN"),
Expr::IsNotUnknown(ast) => write!(f, "{ast} IS NOT UNKNOWN"),
Expr::InList {

View file

@ -1437,6 +1437,7 @@ impl Spanned for Expr {
Expr::IsNotTrue(expr) => expr.span(),
Expr::IsNull(expr) => expr.span(),
Expr::IsNotNull(expr) => expr.span(),
Expr::NotNull { expr, .. } => expr.span(),
Expr::IsUnknown(expr) => expr.span(),
Expr::IsNotUnknown(expr) => expr.span(),
Expr::IsDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()),

View file

@ -94,4 +94,12 @@ impl Dialect for DuckDbDialect {
fn supports_order_by_all(&self) -> bool {
true
}
fn supports_not_null(&self) -> bool {
true
}
fn supports_notnull(&self) -> bool {
true
}
}

View file

@ -650,8 +650,14 @@ pub trait Dialect: Debug + Any {
Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::NULL && self.supports_not_null() => {
Ok(p!(Is))
}
_ => Ok(self.prec_unknown()),
},
Token::Word(w) if w.keyword == Keyword::NOTNULL && self.supports_notnull() => {
Ok(p!(Is))
}
Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)),
Token::Word(w) if w.keyword == Keyword::IN => Ok(p!(Between)),
Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(p!(Between)),
@ -1076,6 +1082,16 @@ pub trait Dialect: Debug + Any {
fn supports_comma_separated_drop_column_list(&self) -> bool {
false
}
/// Returns true if the dialect supports `NOTNULL` in expressions.
fn supports_notnull(&self) -> bool {
false
}
/// Returns true if the dialect supports `NOT NULL` in expressions.
fn supports_not_null(&self) -> bool {
false
}
}
/// This represents the operators for which precedence must be defined

View file

@ -262,4 +262,8 @@ impl Dialect for PostgreSqlDialect {
fn supports_alter_column_type_using(&self) -> bool {
true
}
fn supports_notnull(&self) -> bool {
true
}
}

View file

@ -110,4 +110,12 @@ impl Dialect for SQLiteDialect {
fn supports_dollar_placeholder(&self) -> bool {
true
}
fn supports_not_null(&self) -> bool {
true
}
fn supports_notnull(&self) -> bool {
true
}
}

View file

@ -608,6 +608,7 @@ define_keywords!(
NOT,
NOTHING,
NOTIFY,
NOTNULL,
NOWAIT,
NO_WRITE_TO_BINLOG,
NTH_VALUE,

View file

@ -3562,6 +3562,7 @@ impl<'a> Parser<'a> {
let negated = self.parse_keyword(Keyword::NOT);
let regexp = self.parse_keyword(Keyword::REGEXP);
let rlike = self.parse_keyword(Keyword::RLIKE);
let null = self.parse_keyword(Keyword::NULL);
if regexp || rlike {
Ok(Expr::RLike {
negated,
@ -3571,6 +3572,11 @@ impl<'a> Parser<'a> {
),
regexp,
})
} else if dialect.supports_not_null() && negated && null {
Ok(Expr::NotNull {
expr: Box::new(expr),
one_word: false,
})
} else if self.parse_keyword(Keyword::IN) {
self.parse_in(expr, negated)
} else if self.parse_keyword(Keyword::BETWEEN) {
@ -3608,6 +3614,10 @@ impl<'a> Parser<'a> {
self.expected("IN or BETWEEN after NOT", self.peek_token())
}
}
Keyword::NOTNULL if dialect.supports_notnull() => Ok(Expr::NotNull {
expr: Box::new(expr),
one_word: true,
}),
Keyword::MEMBER => {
if self.parse_keyword(Keyword::OF) {
self.expect_token(&Token::LParen)?;

View file

@ -15982,3 +15982,135 @@ fn parse_create_procedure_with_parameter_modes() {
_ => unreachable!(),
}
}
#[test]
fn parse_not_null_unsupported() {
// Only DuckDB and SQLite support `x NOT NULL` as an expression
// All other dialects fail to parse.
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOT NULL FROM t"#;
let dialects = all_dialects_except(|d| d.supports_not_null());
let res = dialects.parse_sql_statements(sql);
assert_eq!(
ParserError::ParserError("Expected: end of statement, found: NULL".to_string()),
res.unwrap_err()
);
}
#[test]
fn parse_not_null_supported() {
// DuckDB and SQLite support `x NOT NULL` as an expression
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOT NULL FROM t"#;
let dialects = all_dialects_where(|d| d.supports_not_null());
let stmt = dialects.one_statement_parses_to(sql, sql);
match stmt {
Statement::Query(qry) => match *qry.body {
SetExpr::Select(select) => {
assert_eq!(select.projection.len(), 1);
match select.projection.first().unwrap() {
UnnamedExpr(expr) => {
let fake_span = Span {
start: Location { line: 0, column: 0 },
end: Location { line: 0, column: 0 },
};
assert_eq!(
*expr,
Expr::NotNull {
expr: Box::new(Identifier(Ident {
value: "x".to_string(),
quote_style: None,
span: fake_span,
})),
one_word: false,
},
);
}
_ => unreachable!(),
}
}
_ => unreachable!(),
},
_ => unreachable!(),
}
}
#[test]
fn parse_notnull_unsupported() {
// Only Postgres, DuckDB, and SQLite support `x NOTNULL` as an expression
// All other dialects consider `x NOTNULL` like `x AS NOTNULL` and thus
// consider `NOTNULL` an alias for x.
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOTNULL FROM t"#;
let canonical = r#"WITH t AS (SELECT NULL AS x) SELECT x AS NOTNULL FROM t"#;
let dialects = all_dialects_except(|d| d.supports_notnull());
let stmt = dialects.one_statement_parses_to(sql, canonical);
match stmt {
Statement::Query(qry) => match *qry.body {
SetExpr::Select(select) => {
assert_eq!(select.projection.len(), 1);
match select.projection.first().unwrap() {
SelectItem::ExprWithAlias { expr, alias } => {
let fake_span = Span {
start: Location { line: 0, column: 0 },
end: Location { line: 0, column: 0 },
};
assert_eq!(
*expr,
Identifier(Ident {
value: "x".to_string(),
quote_style: None,
span: fake_span,
})
);
assert_eq!(
*alias,
Ident {
value: "NOTNULL".to_string(),
quote_style: None,
span: fake_span,
}
);
}
_ => unreachable!(),
}
}
_ => unreachable!(),
},
_ => unreachable!(),
}
}
#[test]
fn parse_notnull_supported() {
// DuckDB and SQLite support `x NOT NULL` as an expression
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOTNULL FROM t"#;
let dialects = all_dialects_where(|d| d.supports_notnull());
let stmt = dialects.one_statement_parses_to(sql, "");
match stmt {
Statement::Query(qry) => match *qry.body {
SetExpr::Select(select) => {
assert_eq!(select.projection.len(), 1);
match select.projection.first().unwrap() {
UnnamedExpr(expr) => {
let fake_span = Span {
start: Location { line: 0, column: 0 },
end: Location { line: 0, column: 0 },
};
assert_eq!(
*expr,
Expr::NotNull {
expr: Box::new(Identifier(Ident {
value: "x".to_string(),
quote_style: None,
span: fake_span,
})),
one_word: true,
},
);
}
_ => unreachable!(),
}
}
_ => unreachable!(),
},
_ => unreachable!(),
}
}