diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 425e1fb6..273196e3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -756,6 +756,12 @@ pub enum Expr { IsNull(Box), /// `IS NOT NULL` operator IsNotNull(Box), + /// `NOTNULL` or `NOT NULL` operator + NotNull { + expr: Box, + /// true if `NOTNULL`, false if `NOT NULL` + one_word: bool, + }, /// `IS UNKNOWN` operator IsUnknown(Box), /// `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 { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 144de592..bb0cd77c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -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()), diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 3366c670..15b914a4 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -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 + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bc3c5555..13667687 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -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 diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index b2d4014c..ba9de584 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -262,4 +262,8 @@ impl Dialect for PostgreSqlDialect { fn supports_alter_column_type_using(&self) -> bool { true } + + fn supports_notnull(&self) -> bool { + true + } } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 847e0d13..6db34289 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -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 + } } diff --git a/src/keywords.rs b/src/keywords.rs index 73865150..028f0f46 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -608,6 +608,7 @@ define_keywords!( NOT, NOTHING, NOTIFY, + NOTNULL, NOWAIT, NO_WRITE_TO_BINLOG, NTH_VALUE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 839d3645..61418a94 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ed9bb704..1a5d61e7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15974,3 +15974,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!(), + } +}