diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 3366c6705..ea069bc67 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use crate::dialect::Dialect; +use crate::dialect::{Dialect, IsNotNullAlias}; /// A [`Dialect`] for [DuckDB](https://duckdb.org/) #[derive(Debug, Default)] @@ -94,4 +94,13 @@ impl Dialect for DuckDbDialect { fn supports_order_by_all(&self) -> bool { true } + + /// DuckDB supports `NOT NULL` and `NOTNULL` as aliases + /// for `IS NOT NULL`, see DuckDB Comparisons + fn supports_is_not_null_alias(&self, alias: IsNotNullAlias) -> bool { + match alias { + IsNotNullAlias::NotNull => true, + IsNotNullAlias::NotSpaceNull => true, + } + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index bc3c55554..a40b18905 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -55,6 +55,7 @@ use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; +use crate::dialect::IsNotNullAlias::{NotNull, NotSpaceNull}; #[cfg(not(feature = "std"))] use alloc::boxed::Box; @@ -650,8 +651,19 @@ 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_is_not_null_alias(NotSpaceNull) => + { + Ok(p!(Is)) + } _ => Ok(self.prec_unknown()), }, + Token::Word(w) + if w.keyword == Keyword::NOTNULL && self.supports_is_not_null_alias(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 +1088,15 @@ pub trait Dialect: Debug + Any { fn supports_comma_separated_drop_column_list(&self) -> bool { false } + + /// Returns true if the dialect supports the passed in alias. + /// See [IsNotNullAlias]. + fn supports_is_not_null_alias(&self, alias: IsNotNullAlias) -> bool { + match alias { + NotNull => false, + NotSpaceNull => false, + } + } } /// This represents the operators for which precedence must be defined @@ -1102,6 +1123,13 @@ pub enum Precedence { Or, } +/// Possible aliases for `IS NOT NULL` supported +/// by some non-standard dialects. +pub enum IsNotNullAlias { + NotNull, + NotSpaceNull, +} + impl dyn Dialect { #[inline] pub fn is(&self) -> bool { diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index b2d4014cb..b3d2c0e2b 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -28,7 +28,7 @@ // limitations under the License. use log::debug; -use crate::dialect::{Dialect, Precedence}; +use crate::dialect::{Dialect, IsNotNullAlias, Precedence}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; @@ -262,4 +262,13 @@ impl Dialect for PostgreSqlDialect { fn supports_alter_column_type_using(&self) -> bool { true } + + /// Postgres supports `NOTNULL` as an alias for `IS NOT NULL` + /// but does not support `NOT NULL`. See: + fn supports_is_not_null_alias(&self, alias: IsNotNullAlias) -> bool { + match alias { + IsNotNullAlias::NotNull => true, + IsNotNullAlias::NotSpaceNull => false, + } + } } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 847e0d135..b03f7be02 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -20,7 +20,7 @@ use alloc::boxed::Box; use crate::ast::BinaryOperator; use crate::ast::{Expr, Statement}; -use crate::dialect::Dialect; +use crate::dialect::{Dialect, IsNotNullAlias}; use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; @@ -110,4 +110,13 @@ impl Dialect for SQLiteDialect { fn supports_dollar_placeholder(&self) -> bool { true } + + /// SQLite supports ``NOT NULL` and `NOTNULL` as + /// aliases for `IS NOT NULL`, see: + fn supports_is_not_null_alias(&self, alias: IsNotNullAlias) -> bool { + match alias { + IsNotNullAlias::NotNull => true, + IsNotNullAlias::NotSpaceNull => true, + } + } } diff --git a/src/keywords.rs b/src/keywords.rs index 738651504..028f0f46d 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 839d36459..f9d08f3e8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -35,6 +35,7 @@ use IsOptional::*; use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, CreateTableConfiguration}; use crate::ast::Statement::CreatePolicy; use crate::ast::*; +use crate::dialect::IsNotNullAlias::{NotNull, NotSpaceNull}; use crate::dialect::*; use crate::keywords::{Keyword, ALL_KEYWORDS}; use crate::tokenizer::*; @@ -3562,6 +3563,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 +3573,8 @@ impl<'a> Parser<'a> { ), regexp, }) + } else if dialect.supports_is_not_null_alias(NotSpaceNull) && negated && null { + Ok(Expr::IsNotNull(Box::new(expr))) } else if self.parse_keyword(Keyword::IN) { self.parse_in(expr, negated) } else if self.parse_keyword(Keyword::BETWEEN) { @@ -3608,6 +3612,9 @@ impl<'a> Parser<'a> { self.expected("IN or BETWEEN after NOT", self.peek_token()) } } + Keyword::NOTNULL if dialect.supports_is_not_null_alias(NotNull) => { + Ok(Expr::IsNotNull(Box::new(expr))) + } 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 ed9bb704d..86372bdca 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -32,8 +32,8 @@ use sqlparser::ast::TableFactor::{Pivot, Unpivot}; use sqlparser::ast::*; use sqlparser::dialect::{ AnsiDialect, BigQueryDialect, ClickHouseDialect, DatabricksDialect, Dialect, DuckDbDialect, - GenericDialect, HiveDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, RedshiftSqlDialect, - SQLiteDialect, SnowflakeDialect, + GenericDialect, HiveDialect, IsNotNullAlias, MsSqlDialect, MySqlDialect, PostgreSqlDialect, + RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect, }; use sqlparser::keywords::{Keyword, ALL_KEYWORDS}; use sqlparser::parser::{Parser, ParserError, ParserOptions}; @@ -15974,3 +15974,79 @@ 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 the `NOT NULL` portion + let dialects = + all_dialects_except(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotSpaceNull)); + let _ = dialects.expr_parses_to("x NOT NULL", "x"); +} + +#[test] +fn parse_not_null_supported() { + // DuckDB and SQLite support `x NOT NULL` as an alias for `x IS NOT NULL` + let dialects = + all_dialects_where(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotSpaceNull)); + let _ = dialects.expr_parses_to("x NOT NULL", "x IS NOT NULL"); +} + +#[test] +fn test_not_null_precedence() { + // For dialects which support it, `NOT NULL NOT NULL` should + // parse as `(NOT (NULL IS NOT NULL))` + let supported_dialects = + all_dialects_where(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotSpaceNull)); + let unsuported_dialects = + all_dialects_except(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotSpaceNull)); + + assert_matches!( + supported_dialects.expr_parses_to("NOT NULL NOT NULL", "NOT NULL IS NOT NULL"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); + + // for unsupported dialects, parsing should stop at `NOT NULL` + unsuported_dialects.expr_parses_to("NOT NULL NOT NULL", "NOT NULL"); +} + +#[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 dialects = all_dialects_except(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotNull)); + let _ = dialects + .verified_only_select_with_canonical("SELECT NULL NOTNULL", "SELECT NULL AS NOTNULL"); +} + +#[test] +fn parse_notnull_supported() { + // Postgres, DuckDB and SQLite support `x NOTNULL` as an alias for `x IS NOT NULL` + let dialects = all_dialects_where(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotNull)); + let _ = dialects.expr_parses_to("x NOTNULL", "x IS NOT NULL"); +} + +#[test] +fn test_notnull_precedence() { + // For dialects which support it, `NOT NULL NOTNULL` should + // parse as `(NOT (NULL IS NOT NULL))` + let supported_dialects = + all_dialects_where(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotNull)); + let unsuported_dialects = + all_dialects_except(|d| d.supports_is_not_null_alias(IsNotNullAlias::NotNull)); + + assert_matches!( + supported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL IS NOT NULL"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); + + // for unsupported dialects, parsing should stop at `NOT NULL` + unsuported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL"); +}