Skip to content

Commit 3b5cdce

Browse files
committed
DuckDB, Postgres, SQLite: NOT NULL and NOTNULL expressions
1 parent fd4934e commit 3b5cdce

9 files changed

Lines changed: 192 additions & 0 deletions

File tree

src/ast/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,12 @@ pub enum Expr {
778778
IsNull(Box<Expr>),
779779
/// `IS NOT NULL` operator
780780
IsNotNull(Box<Expr>),
781+
/// `NOTNULL` or `NOT NULL` operator
782+
NotNull {
783+
expr: Box<Expr>,
784+
/// true if `NOTNULL`, false if `NOT NULL`
785+
one_word: bool,
786+
},
781787
/// `IS UNKNOWN` operator
782788
IsUnknown(Box<Expr>),
783789
/// `IS NOT UNKNOWN` operator
@@ -1452,6 +1458,12 @@ impl fmt::Display for Expr {
14521458
Expr::IsNotFalse(ast) => write!(f, "{ast} IS NOT FALSE"),
14531459
Expr::IsNull(ast) => write!(f, "{ast} IS NULL"),
14541460
Expr::IsNotNull(ast) => write!(f, "{ast} IS NOT NULL"),
1461+
Expr::NotNull { expr, one_word } => write!(
1462+
f,
1463+
"{} {}",
1464+
expr,
1465+
if *one_word { "NOTNULL" } else { "NOT NULL" }
1466+
),
14551467
Expr::IsUnknown(ast) => write!(f, "{ast} IS UNKNOWN"),
14561468
Expr::IsNotUnknown(ast) => write!(f, "{ast} IS NOT UNKNOWN"),
14571469
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)),
@@ -1089,6 +1095,16 @@ pub trait Dialect: Debug + Any {
10891095
) -> bool {
10901096
false
10911097
}
1098+
1099+
/// Returns true if the dialect supports `NOTNULL` in expressions.
1100+
fn supports_notnull(&self) -> bool {
1101+
false
1102+
}
1103+
1104+
/// Returns true if the dialect supports `NOT NULL` in expressions.
1105+
fn supports_not_null(&self) -> bool {
1106+
false
1107+
}
10921108
}
10931109

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

0 commit comments

Comments
 (0)