Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -586,6 +586,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 @@ -566,6 +566,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 @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -9986,6 +9990,7 @@ impl<'a> Parser<'a> {
Keyword::CONSTRAINT
| Keyword::PRIMARY
| Keyword::NOT
| Keyword::NULL
| Keyword::UNIQUE
| Keyword::CHECK
| Keyword::DEFAULT
Expand All @@ -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,

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 @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -13699,6 +13750,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 @@ -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),
}?;

Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading