Skip to content
6 changes: 6 additions & 0 deletions fuzz/fuzz_seeds/sqlite_2556_multiword_type_names.sql
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;
13 changes: 12 additions & 1 deletion src/ast/data_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "visitor")]
use sqlparser_derive::{Visit, VisitMut};

use crate::ast::{display_comma_separated, Expr, Ident, ObjectName, StructField, UnionField};
use crate::ast::{
display_comma_separated, display_separated, Expr, Ident, ObjectName, StructField, UnionField,
};

use super::{value::escape_single_quote_string, ColumnDef};

Expand Down Expand Up @@ -466,6 +468,8 @@ pub enum DataType {
VarBit(Option<u64>),
/// Custom type with a modifier list, e.g. `GEOMETRY(POINT, 4326)`.
Custom(ObjectName, Vec<TypeModifier>),
/// Multi-word type name whose parts are space-separated, as in SQLite's `typename` grammar.
CustomMultiWord(Vec<Ident>, Vec<TypeModifier>),
/// Arrays.
Array(ArrayElemTypeDef),
/// Map, see [ClickHouse], [Hive].
Expand Down Expand Up @@ -770,6 +774,13 @@ impl fmt::Display for DataType {
write!(f, "{ty}({})", display_comma_separated(modifiers))
}
}
DataType::CustomMultiWord(parts, modifiers) => {
write!(f, "{}", display_separated(parts, " "))?;
if !modifiers.is_empty() {
write!(f, "({})", display_comma_separated(modifiers))?;
}
Ok(())
}
DataType::Enum(vals, bits) => {
match bits {
Some(bits) => write!(f, "ENUM{bits}"),
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,11 @@ pub trait Dialect: Debug + Any {
false
}

/// Whether type names accept space-separated words or strings, including built-in type keywords.
fn supports_multiword_type_names(&self) -> bool {
false
}

/// Returns true if the dialects supports specifying null treatment
/// as part of a window function's parameter list as opposed
/// to after the parameter list.
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ impl Dialect for SQLiteDialect {
true
}

fn supports_multiword_type_names(&self) -> bool {
true
}

fn supports_double_eq_assignment(&self) -> bool {
true
}
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ define_keywords!(
INVOKER,
IO,
IS,
ISNULL,
ISODOW,
ISOLATION,
ISOWEEK,
Expand Down
176 changes: 172 additions & 4 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1848,7 +1848,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)
Expand All @@ -1864,7 +1868,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 {
Expand Down Expand Up @@ -10058,6 +10062,7 @@ impl<'a> Parser<'a> {
Keyword::CONSTRAINT
| Keyword::PRIMARY
| Keyword::NOT
| Keyword::NULL
| Keyword::UNIQUE
| Keyword::CHECK
| Keyword::DEFAULT
Expand All @@ -10066,7 +10071,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,

Copy link
Copy Markdown
Contributor

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 a NULL constraint in sqlite 3.46 (pragma_table_info type is empty) but parses here as Custom(NULL); Keyword::NULL is missing from this list although the continuation stop set in parse_multiword_type_name has it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leading NULL is parsed as a constraint on an untyped column in 947dab70, and I added adequate tests for the rejected variants.

_ => true,
}
} else {
false
Expand Down Expand Up @@ -13274,10 +13281,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;
Expand Down Expand Up @@ -13731,13 +13774,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 {
Expand All @@ -13751,6 +13802,10 @@ impl<'a> Parser<'a> {
Ok(DataType::TsQuery)
}
_ => {
if self.dialect.supports_multiword_type_names() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Types with a dedicated arm never reach this path, so CREATE TABLE t (a INT FOO) and a TEXT FOO PRIMARY KEY still fail on SQLite while a FOO INT parses; sqlite 3.46 accepts both and stores the declared type as INT FOO. Rewinding to next_token_index and taking parse_multiword_type_name when the token after the dedicated arm would continue the name covers it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 ENUM FOO and TIMESTAMP WITH FOO.

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()? {
Expand All @@ -13760,6 +13815,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),
}?;

Expand Down Expand Up @@ -14942,6 +15001,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) {
Expand Down
Loading
Loading