diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index fa18463ae..aee7ee935 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -98,4 +98,10 @@ impl Dialect for DuckDbDialect { fn supports_select_wildcard_exclude(&self) -> bool { true } + + /// DuckDB supports `NOTNULL` as an alias for `IS NOT NULL`, + /// see DuckDB Comparisons + fn supports_notnull_operator(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c79b279df..68e237ca8 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -677,8 +677,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 && parser.in_normal_state() => { + Ok(p!(Is)) + } _ => Ok(self.prec_unknown()), }, + Token::Word(w) if w.keyword == Keyword::NOTNULL && self.supports_notnull_operator() => { + 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)), @@ -1122,6 +1128,12 @@ pub trait Dialect: Debug + Any { ) -> bool { false } + + /// Returns true if the dialect supports the `x NOTNULL` + /// operator expression. + fn supports_notnull_operator(&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 c1f025574..532082328 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -264,4 +264,10 @@ impl Dialect for PostgreSqlDialect { fn supports_alter_column_type_using(&self) -> bool { true } + + /// Postgres supports `NOTNULL` as an alias for `IS NOT NULL` + /// See: + fn supports_notnull_operator(&self) -> bool { + true + } } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 847e0d135..64a8d532f 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -110,4 +110,10 @@ impl Dialect for SQLiteDialect { fn supports_dollar_placeholder(&self) -> bool { true } + + /// SQLite supports `NOTNULL` as aliases for `IS NOT NULL` + /// See: + fn supports_notnull_operator(&self) -> bool { + true + } } diff --git a/src/keywords.rs b/src/keywords.rs index 9e689a6df..7781939bc 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 b3ceec7e2..4766db8c6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -28,16 +28,16 @@ use helpers::attached_token::AttachedToken; use log::debug; -use recursion::RecursionCounter; -use IsLateral::*; -use IsOptional::*; - use crate::ast::helpers::stmt_create_table::{CreateTableBuilder, CreateTableConfiguration}; use crate::ast::Statement::CreatePolicy; use crate::ast::*; use crate::dialect::*; use crate::keywords::{Keyword, ALL_KEYWORDS}; use crate::tokenizer::*; +use recursion::RecursionCounter; +use sqlparser::parser::ParserState::ColumnDefinition; +use IsLateral::*; +use IsOptional::*; mod alter; @@ -275,6 +275,9 @@ enum ParserState { /// PRIOR expressions while still allowing prior as an identifier name /// in other contexts. ConnectBy, + /// The state when parsing column definitions. This state prohibits + /// NOT NULL as an alias for IS NOT NULL. + ColumnDefinition, } /// A SQL Parser @@ -3570,6 +3573,11 @@ 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 = if self.in_normal_state() { + self.parse_keyword(Keyword::NULL) + } else { + false + }; if regexp || rlike { Ok(Expr::RLike { negated, @@ -3579,6 +3587,8 @@ impl<'a> Parser<'a> { ), regexp, }) + } else if 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) { @@ -3616,6 +3626,9 @@ impl<'a> Parser<'a> { self.expected("IN or BETWEEN after NOT", self.peek_token()) } } + Keyword::NOTNULL if dialect.supports_notnull_operator() => { + Ok(Expr::IsNotNull(Box::new(expr))) + } Keyword::MEMBER => { if self.parse_keyword(Keyword::OF) { self.expect_token(&Token::LParen)?; @@ -7724,6 +7737,27 @@ impl<'a> Parser<'a> { return option; } + self.with_state( + ColumnDefinition, + |parser| -> Result, ParserError> { + parser.parse_optional_column_option_inner() + }, + ) + } + + fn parse_optional_column_option_inner(&mut self) -> Result, ParserError> { + /// In some cases, we need to revert to [ParserState::Normal] when parsing nested expressions + /// In those cases we use the following macro to parse instead of calling [parse_expr] directly. + macro_rules! parse_expr_normal { + ($option:expr) => { + if matches!(self.peek_token().token, Token::LParen) { + let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_prefix())?; + Ok(Some($option(expr))) + } else { + Ok(Some($option(self.parse_expr()?))) + } + }; + } if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) { Ok(Some(ColumnOption::CharacterSet( self.parse_object_name(false)?, @@ -7739,11 +7773,11 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::NULL) { Ok(Some(ColumnOption::Null)) } else if self.parse_keyword(Keyword::DEFAULT) { - Ok(Some(ColumnOption::Default(self.parse_expr()?))) + parse_expr_normal!(ColumnOption::Default) } else if dialect_of!(self is ClickHouseDialect| GenericDialect) && self.parse_keyword(Keyword::MATERIALIZED) { - Ok(Some(ColumnOption::Materialized(self.parse_expr()?))) + parse_expr_normal!(ColumnOption::Materialized) } else if dialect_of!(self is ClickHouseDialect| GenericDialect) && self.parse_keyword(Keyword::ALIAS) { @@ -7799,7 +7833,8 @@ impl<'a> Parser<'a> { })) } else if self.parse_keyword(Keyword::CHECK) { self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; + // since `CHECK` requires parentheses, we can parse the inner expression in ParserState::Normal + let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?; self.expect_token(&Token::RParen)?; Ok(Some(ColumnOption::Check(expr))) } else if self.parse_keyword(Keyword::AUTO_INCREMENT) @@ -16514,6 +16549,10 @@ impl<'a> Parser<'a> { Ok(None) } } + + pub fn in_normal_state(&self) -> bool { + matches!(self.state, ParserState::Normal) + } } fn maybe_prefixed_expr(expr: Expr, prefix: Option) -> Expr { @@ -17249,4 +17288,15 @@ mod tests { assert!(Parser::parse_sql(&MySqlDialect {}, sql).is_err()); } + + #[test] + fn test_parse_not_null_in_column_options() { + let canonical = + "CREATE TABLE foo (abc INT DEFAULT (42 IS NOT NULL) NOT NULL, CHECK (abc IS NOT NULL))"; + all_dialects().verified_stmt(canonical); + all_dialects().one_statement_parses_to( + "CREATE TABLE foo (abc INT DEFAULT (42 NOT NULL) NOT NULL, CHECK (abc NOT NULL) )", + canonical, + ); + } } diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 9e5b6ce8a..66e2026c3 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1705,6 +1705,15 @@ fn parse_table_sample() { clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10 OFFSET 1 / 2"); } +#[test] +fn test_parse_not_null_in_column_options() { + // In addition to DEFAULT and CHECK ClickHouse also supports MATERIALIZED, all of which + // can contain `IS NOT NULL` and thus `NOT NULL` as an alias. + let canonical = "CREATE TABLE foo (abc INT DEFAULT (42 IS NOT NULL) NOT NULL, not_null BOOL MATERIALIZED (abc IS NOT NULL), CHECK (abc IS NOT NULL))"; + clickhouse().verified_stmt(canonical); + clickhouse().one_statement_parses_to("CREATE TABLE foo (abc INT DEFAULT (42 NOT NULL) NOT NULL, not_null BOOL MATERIALIZED (abc IS NOT NULL), CHECK (abc NOT NULL) )", canonical); +} + fn clickhouse() -> TestedDialects { TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ba72399f9..05e60c3af 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16015,6 +16015,30 @@ fn parse_create_procedure_with_parameter_modes() { } } +#[test] +fn parse_not_null_supported() { + let _ = all_dialects().expr_parses_to("x NOT NULL", "x IS NOT NULL"); + let _ = all_dialects().expr_parses_to("NULL NOT NULL", "NULL IS NOT NULL"); +} + +#[test] +fn test_not_null_precedence() { + assert_matches!( + all_dialects().expr_parses_to("NOT NULL NOT NULL", "NOT NULL IS NOT NULL"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); + assert_matches!( + all_dialects().expr_parses_to("NOT x NOT NULL", "NOT x IS NOT NULL"), + Expr::UnaryOp { + op: UnaryOperator::Not, + .. + } + ); +} + #[test] fn test_select_exclude() { let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude()); @@ -16147,3 +16171,39 @@ fn test_identifier_unicode_support() { ]); let _ = dialects.verified_stmt(sql); } + +#[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_notnull_operator()); + 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_notnull_operator()); + 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_notnull_operator()); + let unsupported_dialects = all_dialects_except(|d| d.supports_notnull_operator()); + + 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` + unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL"); +}