-
Notifications
You must be signed in to change notification settings - Fork 623
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
base: main
Are you sure you want to change the base?
Changes from all commits
3b5cdce
fe51680
ab5c571
75ba821
4009fa7
ab6607b
375fe8c
40abd90
45fff12
bf25f42
bc454c6
b7d4e1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -608,6 +608,7 @@ define_keywords!( | |
NOT, | ||
NOTHING, | ||
NOTIFY, | ||
NOTNULL, | ||
NOWAIT, | ||
NO_WRITE_TO_BINLOG, | ||
NTH_VALUE, | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we include a sql example to illustrate this? I'm wondering that folks won't be able to fully get the picture without reading the code otherwise |
||||||
} | ||||||
|
||||||
/// 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<Option<ColumnOption>, ParserError> { | ||||||
parser.parse_optional_column_option_inner() | ||||||
}, | ||||||
) | ||||||
} | ||||||
|
||||||
fn parse_optional_column_option_inner(&mut self) -> Result<Option<ColumnOption>, 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use a regular function for this vs a macro? In terms of fn signature, would this work just well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documentation wise, I'm thinking it could be helpful to illustrate why we need this function and with an example like below. And that we also callout that any column option that parses an arbitrary expression likely wants to call this variant instead of CREATE TABLE foo (abc (42 NOT NULL) NOT NULL);
vs
CREATE TABLE foo (abc 42 NOT NULL); |
||||||
($option:expr) => { | ||||||
if matches!(self.peek_token().token, Token::LParen) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
matches!(self.state, ParserState::Normal) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering should this check be |
||||||
} | ||||||
} | ||||||
|
||||||
fn maybe_prefixed_expr(expr: Expr, prefix: Option<Ident>) -> 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, | ||||||
); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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))"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the longer strings, in order to break them up into lines could we use this concat pattern? |
||||||
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); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think this would be the intention? |
||||||
} | ||||||
|
||||||
fn clickhouse() -> TestedDialects { | ||||||
TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) | ||||||
} | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was the change intentional or autofmt it looks like only one import is being added, if the latter could we revert to a minimal diff?