Skip to content

Commit d6f51a1

Browse files
committed
DuckDB, Postgres, SQLite: NOT NULL and NOTNULL expressions
1 parent ed8757f commit d6f51a1

File tree

9 files changed

+192
-0
lines changed

9 files changed

+192
-0
lines changed

src/ast/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,12 @@ pub enum Expr {
756756
IsNull(Box<Expr>),
757757
/// `IS NOT NULL` operator
758758
IsNotNull(Box<Expr>),
759+
/// `NOTNULL` or `NOT NULL` operator
760+
NotNull {
761+
expr: Box<Expr>,
762+
/// true if `NOTNULL`, false if `NOT NULL`
763+
one_word: bool,
764+
},
759765
/// `IS UNKNOWN` operator
760766
IsUnknown(Box<Expr>),
761767
/// `IS NOT UNKNOWN` operator
@@ -1430,6 +1436,12 @@ impl fmt::Display for Expr {
14301436
Expr::IsNotFalse(ast) => write!(f, "{ast} IS NOT FALSE"),
14311437
Expr::IsNull(ast) => write!(f, "{ast} IS NULL"),
14321438
Expr::IsNotNull(ast) => write!(f, "{ast} IS NOT NULL"),
1439+
Expr::NotNull { expr, one_word } => write!(
1440+
f,
1441+
"{} {}",
1442+
expr,
1443+
if *one_word { "NOTNULL" } else { "NOT NULL" }
1444+
),
14331445
Expr::IsUnknown(ast) => write!(f, "{ast} IS UNKNOWN"),
14341446
Expr::IsNotUnknown(ast) => write!(f, "{ast} IS NOT UNKNOWN"),
14351447
Expr::InList {

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,7 @@ impl Spanned for Expr {
14371437
Expr::IsNotTrue(expr) => expr.span(),
14381438
Expr::IsNull(expr) => expr.span(),
14391439
Expr::IsNotNull(expr) => expr.span(),
1440+
Expr::NotNull { expr, .. } => expr.span(),
14401441
Expr::IsUnknown(expr) => expr.span(),
14411442
Expr::IsNotUnknown(expr) => expr.span(),
14421443
Expr::IsDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()),

src/dialect/duckdb.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,12 @@ impl Dialect for DuckDbDialect {
9494
fn supports_order_by_all(&self) -> bool {
9595
true
9696
}
97+
98+
fn supports_not_null(&self) -> bool {
99+
true
100+
}
101+
102+
fn supports_notnull(&self) -> bool {
103+
true
104+
}
97105
}

src/dialect/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,14 @@ pub trait Dialect: Debug + Any {
650650
Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)),
651651
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)),
652652
Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)),
653+
Token::Word(w) if w.keyword == Keyword::NULL && self.supports_not_null() => {
654+
Ok(p!(Is))
655+
}
653656
_ => Ok(self.prec_unknown()),
654657
},
658+
Token::Word(w) if w.keyword == Keyword::NOTNULL && self.supports_notnull() => {
659+
Ok(p!(Is))
660+
}
655661
Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)),
656662
Token::Word(w) if w.keyword == Keyword::IN => Ok(p!(Between)),
657663
Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(p!(Between)),
@@ -1076,6 +1082,16 @@ pub trait Dialect: Debug + Any {
10761082
fn supports_comma_separated_drop_column_list(&self) -> bool {
10771083
false
10781084
}
1085+
1086+
/// Returns true if the dialect supports `NOTNULL` in expressions.
1087+
fn supports_notnull(&self) -> bool {
1088+
false
1089+
}
1090+
1091+
/// Returns true if the dialect supports `NOT NULL` in expressions.
1092+
fn supports_not_null(&self) -> bool {
1093+
false
1094+
}
10791095
}
10801096

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

src/dialect/postgresql.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,8 @@ impl Dialect for PostgreSqlDialect {
262262
fn supports_alter_column_type_using(&self) -> bool {
263263
true
264264
}
265+
266+
fn supports_notnull(&self) -> bool {
267+
true
268+
}
265269
}

src/dialect/sqlite.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,12 @@ impl Dialect for SQLiteDialect {
110110
fn supports_dollar_placeholder(&self) -> bool {
111111
true
112112
}
113+
114+
fn supports_not_null(&self) -> bool {
115+
true
116+
}
117+
118+
fn supports_notnull(&self) -> bool {
119+
true
120+
}
113121
}

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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3562,6 +3562,7 @@ impl<'a> Parser<'a> {
35623562
let negated = self.parse_keyword(Keyword::NOT);
35633563
let regexp = self.parse_keyword(Keyword::REGEXP);
35643564
let rlike = self.parse_keyword(Keyword::RLIKE);
3565+
let null = self.parse_keyword(Keyword::NULL);
35653566
if regexp || rlike {
35663567
Ok(Expr::RLike {
35673568
negated,
@@ -3571,6 +3572,11 @@ impl<'a> Parser<'a> {
35713572
),
35723573
regexp,
35733574
})
3575+
} else if dialect.supports_not_null() && negated && null {
3576+
Ok(Expr::NotNull {
3577+
expr: Box::new(expr),
3578+
one_word: false,
3579+
})
35743580
} else if self.parse_keyword(Keyword::IN) {
35753581
self.parse_in(expr, negated)
35763582
} else if self.parse_keyword(Keyword::BETWEEN) {
@@ -3608,6 +3614,10 @@ impl<'a> Parser<'a> {
36083614
self.expected("IN or BETWEEN after NOT", self.peek_token())
36093615
}
36103616
}
3617+
Keyword::NOTNULL if dialect.supports_notnull() => Ok(Expr::NotNull {
3618+
expr: Box::new(expr),
3619+
one_word: true,
3620+
}),
36113621
Keyword::MEMBER => {
36123622
if self.parse_keyword(Keyword::OF) {
36133623
self.expect_token(&Token::LParen)?;

tests/sqlparser_common.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15974,3 +15974,135 @@ fn parse_create_procedure_with_parameter_modes() {
1597415974
_ => unreachable!(),
1597515975
}
1597615976
}
15977+
15978+
#[test]
15979+
fn parse_not_null_unsupported() {
15980+
// Only DuckDB and SQLite support `x NOT NULL` as an expression
15981+
// All other dialects fail to parse.
15982+
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOT NULL FROM t"#;
15983+
let dialects = all_dialects_except(|d| d.supports_not_null());
15984+
let res = dialects.parse_sql_statements(sql);
15985+
assert_eq!(
15986+
ParserError::ParserError("Expected: end of statement, found: NULL".to_string()),
15987+
res.unwrap_err()
15988+
);
15989+
}
15990+
15991+
#[test]
15992+
fn parse_not_null_supported() {
15993+
// DuckDB and SQLite support `x NOT NULL` as an expression
15994+
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOT NULL FROM t"#;
15995+
let dialects = all_dialects_where(|d| d.supports_not_null());
15996+
let stmt = dialects.one_statement_parses_to(sql, sql);
15997+
match stmt {
15998+
Statement::Query(qry) => match *qry.body {
15999+
SetExpr::Select(select) => {
16000+
assert_eq!(select.projection.len(), 1);
16001+
match select.projection.first().unwrap() {
16002+
UnnamedExpr(expr) => {
16003+
let fake_span = Span {
16004+
start: Location { line: 0, column: 0 },
16005+
end: Location { line: 0, column: 0 },
16006+
};
16007+
assert_eq!(
16008+
*expr,
16009+
Expr::NotNull {
16010+
expr: Box::new(Identifier(Ident {
16011+
value: "x".to_string(),
16012+
quote_style: None,
16013+
span: fake_span,
16014+
})),
16015+
one_word: false,
16016+
},
16017+
);
16018+
}
16019+
_ => unreachable!(),
16020+
}
16021+
}
16022+
_ => unreachable!(),
16023+
},
16024+
_ => unreachable!(),
16025+
}
16026+
}
16027+
16028+
#[test]
16029+
fn parse_notnull_unsupported() {
16030+
// Only Postgres, DuckDB, and SQLite support `x NOTNULL` as an expression
16031+
// All other dialects consider `x NOTNULL` like `x AS NOTNULL` and thus
16032+
// consider `NOTNULL` an alias for x.
16033+
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOTNULL FROM t"#;
16034+
let canonical = r#"WITH t AS (SELECT NULL AS x) SELECT x AS NOTNULL FROM t"#;
16035+
let dialects = all_dialects_except(|d| d.supports_notnull());
16036+
let stmt = dialects.one_statement_parses_to(sql, canonical);
16037+
match stmt {
16038+
Statement::Query(qry) => match *qry.body {
16039+
SetExpr::Select(select) => {
16040+
assert_eq!(select.projection.len(), 1);
16041+
match select.projection.first().unwrap() {
16042+
SelectItem::ExprWithAlias { expr, alias } => {
16043+
let fake_span = Span {
16044+
start: Location { line: 0, column: 0 },
16045+
end: Location { line: 0, column: 0 },
16046+
};
16047+
assert_eq!(
16048+
*expr,
16049+
Identifier(Ident {
16050+
value: "x".to_string(),
16051+
quote_style: None,
16052+
span: fake_span,
16053+
})
16054+
);
16055+
assert_eq!(
16056+
*alias,
16057+
Ident {
16058+
value: "NOTNULL".to_string(),
16059+
quote_style: None,
16060+
span: fake_span,
16061+
}
16062+
);
16063+
}
16064+
_ => unreachable!(),
16065+
}
16066+
}
16067+
_ => unreachable!(),
16068+
},
16069+
_ => unreachable!(),
16070+
}
16071+
}
16072+
16073+
#[test]
16074+
fn parse_notnull_supported() {
16075+
// DuckDB and SQLite support `x NOT NULL` as an expression
16076+
let sql = r#"WITH t AS (SELECT NULL AS x) SELECT x NOTNULL FROM t"#;
16077+
let dialects = all_dialects_where(|d| d.supports_notnull());
16078+
let stmt = dialects.one_statement_parses_to(sql, "");
16079+
match stmt {
16080+
Statement::Query(qry) => match *qry.body {
16081+
SetExpr::Select(select) => {
16082+
assert_eq!(select.projection.len(), 1);
16083+
match select.projection.first().unwrap() {
16084+
UnnamedExpr(expr) => {
16085+
let fake_span = Span {
16086+
start: Location { line: 0, column: 0 },
16087+
end: Location { line: 0, column: 0 },
16088+
};
16089+
assert_eq!(
16090+
*expr,
16091+
Expr::NotNull {
16092+
expr: Box::new(Identifier(Ident {
16093+
value: "x".to_string(),
16094+
quote_style: None,
16095+
span: fake_span,
16096+
})),
16097+
one_word: true,
16098+
},
16099+
);
16100+
}
16101+
_ => unreachable!(),
16102+
}
16103+
}
16104+
_ => unreachable!(),
16105+
},
16106+
_ => unreachable!(),
16107+
}
16108+
}

0 commit comments

Comments
 (0)