Repository navigation
SQLite: Support multi-word type names #2556
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
4bcaa9b
c69ac55
1837075
15fd27e
a79280a
cf2e132
68b01b4
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 |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| CREATE TABLE t (a UNSIGNED BIG INT, b NATIVE CHARACTER(70), c VARYING CHARACTER(10), d 'text', e foo bar NOT NULL DEFAULT 0, f foo GENERATED, g foo GENERATED ALWAYS AS (1), h foo PATH bar); | ||
| SELECT CAST(a AS foo bar), CAST(b AS VARYING CHARACTER) FROM t; | ||
| ALTER TABLE t ADD i foo bar; | ||
| CREATE TABLE u (a NULL, b INT FOO, c TEXT FOO PRIMARY KEY, d ENUM FOO, e TIMESTAMP WITH FOO); | ||
| SELECT CAST(1 AS INT FOO), CAST(1 AS ENUM FOO), CAST(1 AS TIMESTAMP WITH FOO); | ||
| ALTER TABLE u ADD f NULL; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -566,6 +566,7 @@ define_keywords!( | |
| INVOKER, | ||
| IO, | ||
| IS, | ||
| ISNULL, | ||
| ISODOW, | ||
| ISOLATION, | ||
| ISOWEEK, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1854,7 +1854,11 @@ impl<'a> Parser<'a> { | |
| _ => None, | ||
| }; | ||
| let opt_expr = self.maybe_parse(|parser| { | ||
| match parser.parse_data_type()? { | ||
| let (data_type, trailing_bracket) = parser.parse_data_type_inner()?; | ||
| if trailing_bracket.0 { | ||
| return parser_err!("dummy", loc); | ||
| } | ||
| match data_type { | ||
| DataType::Interval { .. } => parser.parse_interval(), | ||
| // Custom names need dialect consent, else `NOT 'a'` would read as a typed string. | ||
| DataType::Custom(name, modifiers) | ||
|
|
@@ -1870,7 +1874,7 @@ impl<'a> Parser<'a> { | |
| uses_odbc_syntax: false, | ||
| })) | ||
| } | ||
| DataType::Custom(..) => parser_err!("dummy", loc), | ||
| DataType::Custom(..) | DataType::CustomMultiWord(..) => parser_err!("dummy", loc), | ||
| // MySQL supports using the `BINARY` keyword as a cast to binary type. | ||
| DataType::Binary(..) if self.dialect.supports_binary_kw_as_cast() => { | ||
| Ok(Expr::Cast { | ||
|
|
@@ -9986,6 +9990,7 @@ impl<'a> Parser<'a> { | |
| Keyword::CONSTRAINT | ||
| | Keyword::PRIMARY | ||
| | Keyword::NOT | ||
| | Keyword::NULL | ||
| | Keyword::UNIQUE | ||
| | Keyword::CHECK | ||
| | Keyword::DEFAULT | ||
|
|
@@ -9994,7 +9999,9 @@ impl<'a> Parser<'a> { | |
| | Keyword::GENERATED | ||
| | Keyword::AS | ||
| ), | ||
| _ => true, // e.g. comma immediately after column name | ||
| // A single-quoted string is a valid type name in SQLite's ids grammar. | ||
| Token::SingleQuotedString(_) => false, | ||
| _ => true, | ||
| } | ||
| } else { | ||
| false | ||
|
|
@@ -13222,10 +13229,46 @@ impl<'a> Parser<'a> { | |
| Ok(ty) | ||
| } | ||
|
|
||
| #[cfg_attr(feature = "recursive-protection", recursive::recursive)] | ||
| fn parse_data_type_helper( | ||
| &mut self, | ||
| ) -> Result<(DataType, MatchedTrailingBracket), ParserError> { | ||
| if !self.dialect.supports_multiword_type_names() { | ||
| return self.parse_data_type_inner(); | ||
| } | ||
|
|
||
| let index = self.index; | ||
| let dedicated_error = match self.try_parse(|parser| parser.parse_data_type_inner()) { | ||
| Ok(data_type) => { | ||
| if self.peek_multiword_type_name_part().is_none() { | ||
| return Ok(data_type); | ||
| } | ||
| self.index = index; | ||
| None | ||
| } | ||
| Err(ParserError::RecursionLimitExceeded) => { | ||
| return Err(ParserError::RecursionLimitExceeded); | ||
| } | ||
| Err(error) => Some(error), | ||
| }; | ||
|
|
||
| self.advance_token(); | ||
| let token = self.get_current_token(); | ||
| let first = match &token.token { | ||
| Token::Word(word) => word.clone().into_ident(token.span), | ||
| Token::SingleQuotedString(value) => Ident::with_quote('\'', value.clone()), | ||
| _ => return self.expected_ref("a data type name", token), | ||
| }; | ||
| if self.peek_multiword_type_name_part().is_none() { | ||
| if let Some(error) = dedicated_error { | ||
| return Err(error); | ||
| } | ||
| } | ||
| self.parse_multiword_type_name(first) | ||
| .map_err(|error| dedicated_error.unwrap_or(error)) | ||
| } | ||
|
|
||
| #[cfg_attr(feature = "recursive-protection", recursive::recursive)] | ||
| fn parse_data_type_inner(&mut self) -> Result<(DataType, MatchedTrailingBracket), ParserError> { | ||
| let _guard = self.recursion_counter.try_decrease()?; | ||
|
|
||
| let dialect = self.dialect; | ||
|
|
@@ -13679,13 +13722,21 @@ impl<'a> Parser<'a> { | |
| } | ||
| } | ||
| Keyword::SIGNED => { | ||
| if self.dialect.supports_multiword_type_names() { | ||
| let first = w.clone().into_ident(next_token.span); | ||
| return self.parse_multiword_type_name(first); | ||
| } | ||
| if self.parse_keyword(Keyword::INTEGER) { | ||
| Ok(DataType::SignedInteger) | ||
| } else { | ||
| Ok(DataType::Signed) | ||
| } | ||
| } | ||
| Keyword::UNSIGNED => { | ||
| if self.dialect.supports_multiword_type_names() { | ||
| let first = w.clone().into_ident(next_token.span); | ||
| return self.parse_multiword_type_name(first); | ||
| } | ||
| if self.parse_keyword(Keyword::INTEGER) { | ||
| Ok(DataType::UnsignedInteger) | ||
| } else { | ||
|
|
@@ -13699,6 +13750,10 @@ impl<'a> Parser<'a> { | |
| Ok(DataType::TsQuery) | ||
| } | ||
| _ => { | ||
| if self.dialect.supports_multiword_type_names() { | ||
|
Contributor
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. Types with a dedicated arm never reach this path, so
Contributor
Author
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. Built-in-prefixed type names are handled in 947dab70. The fallback covers both remaining name words and errors inside dedicated parsers, including |
||
| let first = w.clone().into_ident(next_token.span); | ||
| return self.parse_multiword_type_name(first); | ||
| } | ||
| self.prev_token(); | ||
| let type_name = self.parse_object_name(false)?; | ||
| if let Some(modifiers) = self.parse_optional_type_modifiers()? { | ||
|
|
@@ -13708,6 +13763,10 @@ impl<'a> Parser<'a> { | |
| } | ||
| } | ||
| }, | ||
| Token::SingleQuotedString(value) if self.dialect.supports_multiword_type_names() => { | ||
| let first = Ident::with_quote('\'', value.clone()); | ||
| return self.parse_multiword_type_name(first); | ||
| } | ||
| _ => self.expected_at("a data type name", next_token_index), | ||
| }?; | ||
|
|
||
|
|
@@ -14890,6 +14949,115 @@ impl<'a> Parser<'a> { | |
| } | ||
| } | ||
|
|
||
| fn peek_multiword_type_name_part(&self) -> Option<&TokenWithSpan> { | ||
| let peeked = self.peek_token_ref(); | ||
| let is_part = match &peeked.token { | ||
| Token::Word(w) if w.keyword == Keyword::GENERATED => !matches!( | ||
| &self.peek_nth_token_ref(1).token, | ||
| Token::Word(w) if w.keyword == Keyword::ALWAYS | ||
| ), | ||
| Token::Word(w) => !matches!( | ||
| w.keyword, | ||
| Keyword::ADD | ||
| | Keyword::ALL | ||
| | Keyword::ALTER | ||
| | Keyword::AND | ||
| | Keyword::AS | ||
| | Keyword::AUTOINCREMENT | ||
| | Keyword::BETWEEN | ||
| | Keyword::CASE | ||
| | Keyword::CHECK | ||
| | Keyword::COLLATE | ||
| | Keyword::COMMIT | ||
| | Keyword::CONSTRAINT | ||
| | Keyword::CREATE | ||
| | Keyword::CROSS | ||
| | Keyword::DEFAULT | ||
| | Keyword::DELETE | ||
| | Keyword::DEFERRABLE | ||
| | Keyword::DISTINCT | ||
| | Keyword::DROP | ||
| | Keyword::ELSE | ||
| | Keyword::ESCAPE | ||
| | Keyword::EXCEPT | ||
| | Keyword::EXISTS | ||
| | Keyword::FOREIGN | ||
| | Keyword::FROM | ||
| | Keyword::FULL | ||
| | Keyword::GROUP | ||
| | Keyword::HAVING | ||
| | Keyword::IN | ||
| | Keyword::INDEX | ||
| | Keyword::INDEXED | ||
| | Keyword::INNER | ||
| | Keyword::INSERT | ||
| | Keyword::INTERSECT | ||
| | Keyword::INTO | ||
| | Keyword::IS | ||
| | Keyword::ISNULL | ||
| | Keyword::JOIN | ||
| | Keyword::LEFT | ||
| | Keyword::LIMIT | ||
| | Keyword::NATURAL | ||
| | Keyword::NOT | ||
| | Keyword::NULL | ||
| | Keyword::NOTHING | ||
| | Keyword::NOTNULL | ||
| | Keyword::ON | ||
| | Keyword::OR | ||
| | Keyword::ORDER | ||
| | Keyword::OUTER | ||
| | Keyword::PRIMARY | ||
| | Keyword::REFERENCES | ||
| | Keyword::RETURNING | ||
| | Keyword::RIGHT | ||
| | Keyword::SELECT | ||
| | Keyword::SET | ||
| | Keyword::TABLE | ||
| | Keyword::THEN | ||
| | Keyword::TO | ||
| | Keyword::TRANSACTION | ||
| | Keyword::UNIQUE | ||
| | Keyword::UNION | ||
| | Keyword::UPDATE | ||
| | Keyword::USING | ||
| | Keyword::VALUES | ||
| | Keyword::WHEN | ||
| | Keyword::WHERE | ||
| ), | ||
| Token::SingleQuotedString(_) => true, | ||
| _ => false, | ||
| }; | ||
| is_part.then_some(peeked) | ||
| } | ||
|
|
||
| /// Parse a space-separated multi-word typename per SQLite's `typename` grammar. | ||
| fn parse_multiword_type_name( | ||
| &mut self, | ||
| first: Ident, | ||
| ) -> Result<(DataType, MatchedTrailingBracket), ParserError> { | ||
| let mut parts = Vec::with_capacity(4); | ||
| parts.push(first); | ||
| while let Some(peeked) = self.peek_multiword_type_name_part() { | ||
| let next_part = match &peeked.token { | ||
| Token::Word(word) => word.clone().into_ident(peeked.span), | ||
| Token::SingleQuotedString(value) => Ident::with_quote('\'', value.clone()), | ||
| _ => break, | ||
| }; | ||
| self.advance_token(); | ||
| parts.push(next_part); | ||
| } | ||
| let modifiers = self.parse_optional_type_modifiers()?.unwrap_or_default(); | ||
| if parts.len() == 1 { | ||
| Ok(( | ||
| DataType::Custom(ObjectName::from(vec![parts.remove(0)]), modifiers), | ||
| false.into(), | ||
| )) | ||
| } else { | ||
| Ok((DataType::CustomMultiWord(parts, modifiers), false.into())) | ||
| } | ||
| } | ||
|
|
||
| fn parse_structured_object_type_def(&mut self) -> Result<Vec<ColumnDef>, ParserError> { | ||
| self.expect_token(&Token::LParen)?; | ||
| if self.consume_token(&Token::RParen) { | ||
|
|
||
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.
CREATE TABLE t (a NULL, b INT)is an untyped column with aNULLconstraint in sqlite 3.46 (pragma_table_infotype is empty) but parses here asCustom(NULL);Keyword::NULLis missing from this list although the continuation stop set inparse_multiword_type_namehas it.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.
Leading
NULLis parsed as a constraint on an untyped column in 947dab70, and I added adequate tests for the rejected variants.