Skip to content

Commit 0dd47a1

Browse files
ryanschneideriffyio
authored andcommitted
Add support for NOT NULL and NOTNULL expressions (apache#1927)
Co-authored-by: Ifeanyi Ubah <ify1992@yahoo.com>
1 parent 8e4b909 commit 0dd47a1

File tree

8 files changed

+200
-8
lines changed

8 files changed

+200
-8
lines changed

src/dialect/duckdb.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,10 @@ impl Dialect for DuckDbDialect {
9898
fn supports_select_wildcard_exclude(&self) -> bool {
9999
true
100100
}
101+
102+
/// DuckDB supports `NOTNULL` as an alias for `IS NOT NULL`,
103+
/// see DuckDB Comparisons <https://duckdb.org/docs/stable/sql/expressions/comparison_operators#between-and-is-not-null>
104+
fn supports_notnull_operator(&self) -> bool {
105+
true
106+
}
101107
}

src/dialect/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,8 +677,16 @@ pub trait Dialect: Debug + Any {
677677
Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)),
678678
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)),
679679
Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)),
680+
Token::Word(w)
681+
if w.keyword == Keyword::NULL && !parser.in_column_definition_state() =>
682+
{
683+
Ok(p!(Is))
684+
}
680685
_ => Ok(self.prec_unknown()),
681686
},
687+
Token::Word(w) if w.keyword == Keyword::NOTNULL && self.supports_notnull_operator() => {
688+
Ok(p!(Is))
689+
}
682690
Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)),
683691
Token::Word(w) if w.keyword == Keyword::IN => Ok(p!(Between)),
684692
Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(p!(Between)),
@@ -1127,6 +1135,12 @@ pub trait Dialect: Debug + Any {
11271135
) -> bool {
11281136
false
11291137
}
1138+
1139+
/// Returns true if the dialect supports the `x NOTNULL`
1140+
/// operator expression.
1141+
fn supports_notnull_operator(&self) -> bool {
1142+
false
1143+
}
11301144
}
11311145

11321146
/// This represents the operators for which precedence must be defined

src/dialect/postgresql.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,10 @@ impl Dialect for PostgreSqlDialect {
263263
fn supports_alter_column_type_using(&self) -> bool {
264264
true
265265
}
266+
267+
/// Postgres supports `NOTNULL` as an alias for `IS NOT NULL`
268+
/// See: <https://www.postgresql.org/docs/17/functions-comparison.html>
269+
fn supports_notnull_operator(&self) -> bool {
270+
true
271+
}
266272
}

src/dialect/sqlite.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,10 @@ impl Dialect for SQLiteDialect {
110110
fn supports_dollar_placeholder(&self) -> bool {
111111
true
112112
}
113+
114+
/// SQLite supports `NOTNULL` as aliases for `IS NOT NULL`
115+
/// See: <https://sqlite.org/syntax/expr.html>
116+
fn supports_notnull_operator(&self) -> bool {
117+
true
118+
}
113119
}

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ define_keywords!(
608608
NOT,
609609
NOTHING,
610610
NOTIFY,
611+
NOTNULL,
611612
NOWAIT,
612613
NO_WRITE_TO_BINLOG,
613614
NTH_VALUE,

src/parser/mod.rs

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use crate::ast::*;
3838
use crate::dialect::*;
3939
use crate::keywords::{Keyword, ALL_KEYWORDS};
4040
use crate::tokenizer::*;
41+
use sqlparser::parser::ParserState::ColumnDefinition;
4142

4243
mod alter;
4344

@@ -275,6 +276,12 @@ enum ParserState {
275276
/// PRIOR expressions while still allowing prior as an identifier name
276277
/// in other contexts.
277278
ConnectBy,
279+
/// The state when parsing column definitions. This state prohibits
280+
/// NOT NULL as an alias for IS NOT NULL. For example:
281+
/// ```sql
282+
/// CREATE TABLE foo (abc BIGINT NOT NULL);
283+
/// ```
284+
ColumnDefinition,
278285
}
279286

280287
/// A SQL Parser
@@ -3578,6 +3585,11 @@ impl<'a> Parser<'a> {
35783585
let negated = self.parse_keyword(Keyword::NOT);
35793586
let regexp = self.parse_keyword(Keyword::REGEXP);
35803587
let rlike = self.parse_keyword(Keyword::RLIKE);
3588+
let null = if !self.in_column_definition_state() {
3589+
self.parse_keyword(Keyword::NULL)
3590+
} else {
3591+
false
3592+
};
35813593
if regexp || rlike {
35823594
Ok(Expr::RLike {
35833595
negated,
@@ -3587,6 +3599,8 @@ impl<'a> Parser<'a> {
35873599
),
35883600
regexp,
35893601
})
3602+
} else if negated && null {
3603+
Ok(Expr::IsNotNull(Box::new(expr)))
35903604
} else if self.parse_keyword(Keyword::IN) {
35913605
self.parse_in(expr, negated)
35923606
} else if self.parse_keyword(Keyword::BETWEEN) {
@@ -3624,6 +3638,9 @@ impl<'a> Parser<'a> {
36243638
self.expected("IN or BETWEEN after NOT", self.peek_token())
36253639
}
36263640
}
3641+
Keyword::NOTNULL if dialect.supports_notnull_operator() => {
3642+
Ok(Expr::IsNotNull(Box::new(expr)))
3643+
}
36273644
Keyword::MEMBER => {
36283645
if self.parse_keyword(Keyword::OF) {
36293646
self.expect_token(&Token::LParen)?;
@@ -7773,6 +7790,15 @@ impl<'a> Parser<'a> {
77737790
return option;
77747791
}
77757792

7793+
self.with_state(
7794+
ColumnDefinition,
7795+
|parser| -> Result<Option<ColumnOption>, ParserError> {
7796+
parser.parse_optional_column_option_inner()
7797+
},
7798+
)
7799+
}
7800+
7801+
fn parse_optional_column_option_inner(&mut self) -> Result<Option<ColumnOption>, ParserError> {
77767802
if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) {
77777803
Ok(Some(ColumnOption::CharacterSet(
77787804
self.parse_object_name(false)?,
@@ -7788,15 +7814,19 @@ impl<'a> Parser<'a> {
77887814
} else if self.parse_keyword(Keyword::NULL) {
77897815
Ok(Some(ColumnOption::Null))
77907816
} else if self.parse_keyword(Keyword::DEFAULT) {
7791-
Ok(Some(ColumnOption::Default(self.parse_expr()?)))
7817+
Ok(Some(ColumnOption::Default(
7818+
self.parse_column_option_expr()?,
7819+
)))
77927820
} else if dialect_of!(self is ClickHouseDialect| GenericDialect)
77937821
&& self.parse_keyword(Keyword::MATERIALIZED)
77947822
{
7795-
Ok(Some(ColumnOption::Materialized(self.parse_expr()?)))
7823+
Ok(Some(ColumnOption::Materialized(
7824+
self.parse_column_option_expr()?,
7825+
)))
77967826
} else if dialect_of!(self is ClickHouseDialect| GenericDialect)
77977827
&& self.parse_keyword(Keyword::ALIAS)
77987828
{
7799-
Ok(Some(ColumnOption::Alias(self.parse_expr()?)))
7829+
Ok(Some(ColumnOption::Alias(self.parse_column_option_expr()?)))
78007830
} else if dialect_of!(self is ClickHouseDialect| GenericDialect)
78017831
&& self.parse_keyword(Keyword::EPHEMERAL)
78027832
{
@@ -7805,7 +7835,9 @@ impl<'a> Parser<'a> {
78057835
if matches!(self.peek_token().token, Token::Comma | Token::RParen) {
78067836
Ok(Some(ColumnOption::Ephemeral(None)))
78077837
} else {
7808-
Ok(Some(ColumnOption::Ephemeral(Some(self.parse_expr()?))))
7838+
Ok(Some(ColumnOption::Ephemeral(Some(
7839+
self.parse_column_option_expr()?,
7840+
))))
78097841
}
78107842
} else if self.parse_keywords(&[Keyword::PRIMARY, Keyword::KEY]) {
78117843
let characteristics = self.parse_constraint_characteristics()?;
@@ -7848,7 +7880,8 @@ impl<'a> Parser<'a> {
78487880
}))
78497881
} else if self.parse_keyword(Keyword::CHECK) {
78507882
self.expect_token(&Token::LParen)?;
7851-
let expr = self.parse_expr()?;
7883+
// since `CHECK` requires parentheses, we can parse the inner expression in ParserState::Normal
7884+
let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?;
78527885
self.expect_token(&Token::RParen)?;
78537886
Ok(Some(ColumnOption::Check(expr)))
78547887
} else if self.parse_keyword(Keyword::AUTO_INCREMENT)
@@ -7882,7 +7915,7 @@ impl<'a> Parser<'a> {
78827915
} else if self.parse_keywords(&[Keyword::ON, Keyword::UPDATE])
78837916
&& dialect_of!(self is MySqlDialect | GenericDialect)
78847917
{
7885-
let expr = self.parse_expr()?;
7918+
let expr = self.parse_column_option_expr()?;
78867919
Ok(Some(ColumnOption::OnUpdate(expr)))
78877920
} else if self.parse_keyword(Keyword::GENERATED) {
78887921
self.parse_optional_column_option_generated()
@@ -7900,7 +7933,9 @@ impl<'a> Parser<'a> {
79007933
} else if self.parse_keyword(Keyword::SRID)
79017934
&& dialect_of!(self is MySqlDialect | GenericDialect)
79027935
{
7903-
Ok(Some(ColumnOption::Srid(Box::new(self.parse_expr()?))))
7936+
Ok(Some(ColumnOption::Srid(Box::new(
7937+
self.parse_column_option_expr()?,
7938+
))))
79047939
} else if self.parse_keyword(Keyword::IDENTITY)
79057940
&& dialect_of!(self is MsSqlDialect | GenericDialect)
79067941
{
@@ -7940,6 +7975,31 @@ impl<'a> Parser<'a> {
79407975
}
79417976
}
79427977

7978+
/// When parsing some column option expressions we need to revert to [ParserState::Normal] since
7979+
/// `NOT NULL` is allowed as an alias for `IS NOT NULL`.
7980+
/// In those cases we use this helper instead of calling [Parser::parse_expr] directly.
7981+
///
7982+
/// For example, consider these `CREATE TABLE` statements:
7983+
/// ```sql
7984+
/// CREATE TABLE foo (abc BOOL DEFAULT (42 NOT NULL) NOT NULL);
7985+
/// ```
7986+
/// vs
7987+
/// ```sql
7988+
/// CREATE TABLE foo (abc BOOL NOT NULL);
7989+
/// ```
7990+
///
7991+
/// In the first we should parse the inner portion of `(42 NOT NULL)` as [Expr::IsNotNull],
7992+
/// whereas is both statements that trailing `NOT NULL` should only be parsed as a
7993+
/// [ColumnOption::NotNull].
7994+
fn parse_column_option_expr(&mut self) -> Result<Expr, ParserError> {
7995+
if self.peek_token_ref().token == Token::LParen {
7996+
let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_prefix())?;
7997+
Ok(expr)
7998+
} else {
7999+
Ok(self.parse_expr()?)
8000+
}
8001+
}
8002+
79438003
pub(crate) fn parse_tag(&mut self) -> Result<Tag, ParserError> {
79448004
let name = self.parse_object_name(false)?;
79458005
self.expect_token(&Token::Eq)?;
@@ -7984,7 +8044,7 @@ impl<'a> Parser<'a> {
79848044
}))
79858045
} else if self.parse_keywords(&[Keyword::ALWAYS, Keyword::AS]) {
79868046
if self.expect_token(&Token::LParen).is_ok() {
7987-
let expr = self.parse_expr()?;
8047+
let expr: Expr = self.with_state(ParserState::Normal, |p| p.parse_expr())?;
79888048
self.expect_token(&Token::RParen)?;
79898049
let (gen_as, expr_mode) = if self.parse_keywords(&[Keyword::STORED]) {
79908050
Ok((
@@ -16564,6 +16624,10 @@ impl<'a> Parser<'a> {
1656416624
Ok(None)
1656516625
}
1656616626
}
16627+
16628+
pub(crate) fn in_column_definition_state(&self) -> bool {
16629+
matches!(self.state, ColumnDefinition)
16630+
}
1656716631
}
1656816632

1656916633
fn maybe_prefixed_expr(expr: Expr, prefix: Option<Ident>) -> Expr {
@@ -17299,4 +17363,28 @@ mod tests {
1729917363

1730017364
assert!(Parser::parse_sql(&MySqlDialect {}, sql).is_err());
1730117365
}
17366+
17367+
#[test]
17368+
fn test_parse_not_null_in_column_options() {
17369+
let canonical = concat!(
17370+
"CREATE TABLE foo (",
17371+
"abc INT DEFAULT (42 IS NOT NULL) NOT NULL,",
17372+
" def INT,",
17373+
" def_null BOOL GENERATED ALWAYS AS (def IS NOT NULL) STORED,",
17374+
" CHECK (abc IS NOT NULL)",
17375+
")"
17376+
);
17377+
all_dialects().verified_stmt(canonical);
17378+
all_dialects().one_statement_parses_to(
17379+
concat!(
17380+
"CREATE TABLE foo (",
17381+
"abc INT DEFAULT (42 NOT NULL) NOT NULL,",
17382+
" def INT,",
17383+
" def_null BOOL GENERATED ALWAYS AS (def NOT NULL) STORED,",
17384+
" CHECK (abc NOT NULL)",
17385+
")"
17386+
),
17387+
canonical,
17388+
);
17389+
}
1730217390
}

tests/sqlparser_clickhouse.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,6 +1705,30 @@ fn parse_table_sample() {
17051705
clickhouse().verified_stmt("SELECT * FROM tbl SAMPLE 1 / 10 OFFSET 1 / 2");
17061706
}
17071707

1708+
#[test]
1709+
fn test_parse_not_null_in_column_options() {
1710+
// In addition to DEFAULT and CHECK ClickHouse also supports MATERIALIZED, all of which
1711+
// can contain `IS NOT NULL` and thus `NOT NULL` as an alias.
1712+
let canonical = concat!(
1713+
"CREATE TABLE foo (",
1714+
"abc INT DEFAULT (42 IS NOT NULL) NOT NULL,",
1715+
" not_null BOOL MATERIALIZED (abc IS NOT NULL),",
1716+
" CHECK (abc IS NOT NULL)",
1717+
")",
1718+
);
1719+
clickhouse().verified_stmt(canonical);
1720+
clickhouse().one_statement_parses_to(
1721+
concat!(
1722+
"CREATE TABLE foo (",
1723+
"abc INT DEFAULT (42 NOT NULL) NOT NULL,",
1724+
" not_null BOOL MATERIALIZED (abc NOT NULL),",
1725+
" CHECK (abc NOT NULL)",
1726+
")",
1727+
),
1728+
canonical,
1729+
);
1730+
}
1731+
17081732
fn clickhouse() -> TestedDialects {
17091733
TestedDialects::new(vec![Box::new(ClickHouseDialect {})])
17101734
}

tests/sqlparser_common.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16066,6 +16066,27 @@ fn parse_create_procedure_with_parameter_modes() {
1606616066
}
1606716067
}
1606816068

16069+
#[test]
16070+
fn parse_not_null() {
16071+
let _ = all_dialects().expr_parses_to("x NOT NULL", "x IS NOT NULL");
16072+
let _ = all_dialects().expr_parses_to("NULL NOT NULL", "NULL IS NOT NULL");
16073+
16074+
assert_matches!(
16075+
all_dialects().expr_parses_to("NOT NULL NOT NULL", "NOT NULL IS NOT NULL"),
16076+
Expr::UnaryOp {
16077+
op: UnaryOperator::Not,
16078+
..
16079+
}
16080+
);
16081+
assert_matches!(
16082+
all_dialects().expr_parses_to("NOT x NOT NULL", "NOT x IS NOT NULL"),
16083+
Expr::UnaryOp {
16084+
op: UnaryOperator::Not,
16085+
..
16086+
}
16087+
);
16088+
}
16089+
1606916090
#[test]
1607016091
fn test_select_exclude() {
1607116092
let dialects = all_dialects_where(|d| d.supports_select_wildcard_exclude());
@@ -16209,3 +16230,29 @@ fn test_identifier_unicode_start() {
1620916230
]);
1621016231
let _ = dialects.verified_stmt(sql);
1621116232
}
16233+
16234+
#[test]
16235+
fn parse_notnull() {
16236+
// Some dialects support `x NOTNULL` as an expression while others consider
16237+
// `x NOTNULL` like `x AS NOTNULL` and thus consider `NOTNULL` an alias for x.
16238+
let notnull_unsupported_dialects = all_dialects_except(|d| d.supports_notnull_operator());
16239+
let _ = notnull_unsupported_dialects
16240+
.verified_only_select_with_canonical("SELECT NULL NOTNULL", "SELECT NULL AS NOTNULL");
16241+
16242+
// Supported dialects consider `x NOTNULL` as an alias for `x IS NOT NULL`
16243+
let notnull_supported_dialects = all_dialects_where(|d| d.supports_notnull_operator());
16244+
let _ = notnull_supported_dialects.expr_parses_to("x NOTNULL", "x IS NOT NULL");
16245+
16246+
// For dialects which support it, `NOT NULL NOTNULL` should
16247+
// parse as `(NOT (NULL IS NOT NULL))`
16248+
assert_matches!(
16249+
notnull_supported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL IS NOT NULL"),
16250+
Expr::UnaryOp {
16251+
op: UnaryOperator::Not,
16252+
..
16253+
}
16254+
);
16255+
16256+
// for unsupported dialects, parsing should stop at `NOT NULL`
16257+
notnull_unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL");
16258+
}

0 commit comments

Comments
 (0)