Skip to content

DuckDB, Postgres, SQLite: NOT NULL and NOTNULL expressions #1927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/dialect/duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://duckdb.org/docs/stable/sql/expressions/comparison_operators#between-and-is-not-null>
fn supports_notnull_operator(&self) -> bool {
true
}
}
14 changes: 14 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,8 +677,16 @@ 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_column_definition_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)),
Expand Down Expand Up @@ -1122,6 +1130,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
Expand Down
6 changes: 6 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,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: <https://www.postgresql.org/docs/17/functions-comparison.html>
fn supports_notnull_operator(&self) -> bool {
true
}
}
6 changes: 6 additions & 0 deletions src/dialect/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://sqlite.org/syntax/expr.html>
fn supports_notnull_operator(&self) -> bool {
true
}
}
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ define_keywords!(
NOT,
NOTHING,
NOTIFY,
NOTNULL,
NOWAIT,
NO_WRITE_TO_BINLOG,
NTH_VALUE,
Expand Down
104 changes: 96 additions & 8 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use crate::ast::*;
use crate::dialect::*;
use crate::keywords::{Keyword, ALL_KEYWORDS};
use crate::tokenizer::*;
use sqlparser::parser::ParserState::ColumnDefinition;

mod alter;

Expand Down Expand Up @@ -275,6 +276,12 @@ 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. For example:
/// ```sql
/// CREATE TABLE foo (abc BIGINT NOT NULL);
/// ```
ColumnDefinition,
}

/// A SQL Parser
Expand Down Expand Up @@ -3578,6 +3585,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_column_definition_state() {
self.parse_keyword(Keyword::NULL)
} else {
false
};
if regexp || rlike {
Ok(Expr::RLike {
negated,
Expand All @@ -3587,6 +3599,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) {
Expand Down Expand Up @@ -3624,6 +3638,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)?;
Expand Down Expand Up @@ -7734,6 +7751,15 @@ impl<'a> Parser<'a> {
return option;
}

self.with_state(
ColumnDefinition,
|parser| -> Result<Option<ColumnOption>, ParserError> {
parser.parse_optional_column_option_inner()
},
)
}

fn parse_optional_column_option_inner(&mut self) -> Result<Option<ColumnOption>, ParserError> {
if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) {
Ok(Some(ColumnOption::CharacterSet(
self.parse_object_name(false)?,
Expand All @@ -7749,15 +7775,19 @@ 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()?)))
Ok(Some(ColumnOption::Default(
self.parse_column_option_expr()?,
)))
} else if dialect_of!(self is ClickHouseDialect| GenericDialect)
&& self.parse_keyword(Keyword::MATERIALIZED)
{
Ok(Some(ColumnOption::Materialized(self.parse_expr()?)))
Ok(Some(ColumnOption::Materialized(
self.parse_column_option_expr()?,
)))
} else if dialect_of!(self is ClickHouseDialect| GenericDialect)
&& self.parse_keyword(Keyword::ALIAS)
{
Ok(Some(ColumnOption::Alias(self.parse_expr()?)))
Ok(Some(ColumnOption::Alias(self.parse_column_option_expr()?)))
} else if dialect_of!(self is ClickHouseDialect| GenericDialect)
&& self.parse_keyword(Keyword::EPHEMERAL)
{
Expand All @@ -7766,7 +7796,9 @@ impl<'a> Parser<'a> {
if matches!(self.peek_token().token, Token::Comma | Token::RParen) {
Ok(Some(ColumnOption::Ephemeral(None)))
} else {
Ok(Some(ColumnOption::Ephemeral(Some(self.parse_expr()?))))
Ok(Some(ColumnOption::Ephemeral(Some(
self.parse_column_option_expr()?,
))))
}
} else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) {
let characteristics = self.parse_constraint_characteristics()?;
Expand Down Expand Up @@ -7809,7 +7841,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)
Expand Down Expand Up @@ -7843,7 +7876,7 @@ impl<'a> Parser<'a> {
} else if self.parse_keywords(&[Keyword::ON, Keyword::UPDATE])
&& dialect_of!(self is MySqlDialect | GenericDialect)
{
let expr = self.parse_expr()?;
let expr = self.parse_column_option_expr()?;
Ok(Some(ColumnOption::OnUpdate(expr)))
} else if self.parse_keyword(Keyword::GENERATED) {
self.parse_optional_column_option_generated()
Expand All @@ -7861,7 +7894,9 @@ impl<'a> Parser<'a> {
} else if self.parse_keyword(Keyword::SRID)
&& dialect_of!(self is MySqlDialect | GenericDialect)
{
Ok(Some(ColumnOption::Srid(Box::new(self.parse_expr()?))))
Ok(Some(ColumnOption::Srid(Box::new(
self.parse_column_option_expr()?,
))))
} else if self.parse_keyword(Keyword::IDENTITY)
&& dialect_of!(self is MsSqlDialect | GenericDialect)
{
Expand Down Expand Up @@ -7901,6 +7936,31 @@ impl<'a> Parser<'a> {
}
}

/// When parsing some column option expressions we need to revert to [ParserState::Normal] since
/// `NOT NULL` is allowed as an alias for `IS NOT NULL`.
/// In those cases we use this helper instead of calling [Parser::parse_expr] directly.
///
/// For example, consider these `CREATE TABLE` statements:
/// ```sql
/// CREATE TABLE foo (abc BOOL DEFAULT (42 NOT NULL) NOT NULL);
/// ```
/// vs
/// ```sql
/// CREATE TABLE foo (abc BOOL NOT NULL);
/// ```
///
/// In the first we should parse the inner portion of `(42 NOT NULL)` as [Expr::IsNotNull],
/// whereas is both statements that trailing `NOT NULL` should only be parsed as a
/// [ColumnOption::NotNull].
fn parse_column_option_expr(&mut self) -> Result<Expr, ParserError> {
if self.peek_token_ref().token == Token::LParen {
let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_prefix())?;
Ok(expr)
} else {
Ok(self.parse_expr()?)
}
}

pub(crate) fn parse_tag(&mut self) -> Result<Tag, ParserError> {
let name = self.parse_object_name(false)?;
self.expect_token(&Token::Eq)?;
Expand Down Expand Up @@ -7945,7 +8005,7 @@ impl<'a> Parser<'a> {
}))
} else if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS]) {
if self.expect_token(&Token::LParen).is_ok() {
let expr = self.parse_expr()?;
let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?;
self.expect_token(&Token::RParen)?;
let (gen_as, expr_mode) = if self.parse_keywords(&[Keyword::STORED]) {
Ok((
Expand Down Expand Up @@ -16524,6 +16584,10 @@ impl<'a> Parser<'a> {
Ok(None)
}
}

pub(crate) fn in_column_definition_state(&self) -> bool {
matches!(self.state, ColumnDefinition)
}
}

fn maybe_prefixed_expr(expr: Expr, prefix: Option<Ident>) -> Expr {
Expand Down Expand Up @@ -17259,4 +17323,28 @@ mod tests {

assert!(Parser::parse_sql(&MySqlDialect {}, sql).is_err());
}

#[test]
fn test_parse_not_null_in_column_options() {
let canonical = concat!(
"CREATE TABLE foo (",
"abc INT DEFAULT (42 IS NOT NULL) NOT NULL,",
" def INT,",
" def_null BOOL GENERATED ALWAYS AS (def IS NOT NULL) STORED,",
" CHECK (abc IS NOT NULL)",
")"
);
all_dialects().verified_stmt(canonical);
all_dialects().one_statement_parses_to(
concat!(
"CREATE TABLE foo (",
"abc INT DEFAULT (42 NOT NULL) NOT NULL,",
" def INT,",
" def_null BOOL GENERATED ALWAYS AS (def NOT NULL) STORED,",
" CHECK (abc NOT NULL)",
")"
),
canonical,
);
}
}
24 changes: 24 additions & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,30 @@ 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 = concat!(
"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(
concat!(
"CREATE TABLE foo (",
"abc INT DEFAULT (42 NOT NULL) NOT NULL,",
" not_null BOOL MATERIALIZED (abc NOT NULL),",
" CHECK (abc NOT NULL)",
")",
),
canonical,
);
}

fn clickhouse() -> TestedDialects {
TestedDialects::new(vec![Box::new(ClickHouseDialect {})])
}
Expand Down
47 changes: 47 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16031,6 +16031,27 @@ fn parse_create_procedure_with_parameter_modes() {
}
}

#[test]
fn parse_not_null() {
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");

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());
Expand Down Expand Up @@ -16174,3 +16195,29 @@ fn test_identifier_unicode_start() {
]);
let _ = dialects.verified_stmt(sql);
}

#[test]
fn parse_notnull() {
// Some dialects support `x NOTNULL` as an expression while others consider
// `x NOTNULL` like `x AS NOTNULL` and thus consider `NOTNULL` an alias for x.
let notnull_unsupported_dialects = all_dialects_except(|d| d.supports_notnull_operator());
let _ = notnull_unsupported_dialects
.verified_only_select_with_canonical("SELECT NULL NOTNULL", "SELECT NULL AS NOTNULL");

// Supported dialects consider `x NOTNULL` as an alias for `x IS NOT NULL`
let notnull_supported_dialects = all_dialects_where(|d| d.supports_notnull_operator());
let _ = notnull_supported_dialects.expr_parses_to("x NOTNULL", "x IS NOT NULL");

// For dialects which support it, `NOT NULL NOTNULL` should
// parse as `(NOT (NULL IS NOT NULL))`
assert_matches!(
notnull_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`
notnull_unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL");
}