diff --git a/fuzz/fuzz_seeds/sqlite_2556_multiword_type_names.sql b/fuzz/fuzz_seeds/sqlite_2556_multiword_type_names.sql new file mode 100644 index 0000000000..1f663d7336 --- /dev/null +++ b/fuzz/fuzz_seeds/sqlite_2556_multiword_type_names.sql @@ -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; diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index acb1b2ab9c..5c43f9e2dc 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -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}; @@ -466,6 +468,8 @@ pub enum DataType { VarBit(Option), /// Custom type with a modifier list, e.g. `GEOMETRY(POINT, 4326)`. Custom(ObjectName, Vec), + /// Multi-word type name whose parts are space-separated, as in SQLite's `typename` grammar. + CustomMultiWord(Vec, Vec), /// Arrays. Array(ArrayElemTypeDef), /// Map, see [ClickHouse], [Hive]. @@ -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}"), diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c867b76f61..95a61d2f5e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -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. diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 44dcc9d267..5054427d17 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -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 } diff --git a/src/keywords.rs b/src/keywords.rs index 8a6c980cab..86b8fdab69 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -566,6 +566,7 @@ define_keywords!( INVOKER, IO, IS, + ISNULL, ISODOW, ISOLATION, ISOWEEK, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..230a98da4c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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,6 +13722,10 @@ 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 { @@ -13686,6 +13733,10 @@ impl<'a> Parser<'a> { } } 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() { + 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, ParserError> { self.expect_token(&Token::LParen)?; if self.consume_token(&Token::RParen) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f71869e820..4abde5ccf3 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5776,7 +5776,7 @@ fn parse_bad_constraint() { fn parse_sequence_options_in_any_order() { // The sequence options form an unordered list, so every permutation below // is accepted and round-trips in the order written. - let dialects = all_dialects(); + let dialects = all_dialects_where(|d| !d.supports_multiword_type_names()); for sql in [ "CREATE SEQUENCE sa START WITH 1 INCREMENT BY 1", "CREATE SEQUENCE sb INCREMENT BY 1 START WITH 1", @@ -5890,7 +5890,7 @@ fn parse_sequence_options_in_any_order() { #[test] fn sequence_options_reject_non_numeric_values() { - let dialects = all_dialects(); + let dialects = all_dialects_where(|d| !d.supports_multiword_type_names()); for (sql, expected) in [ ( "CREATE SEQUENCE s MINVALUE x", @@ -10009,7 +10009,8 @@ fn parse_set_role_as_variable() { #[test] fn parse_double_colon_cast_at_timezone() { let sql = "SELECT '2001-01-01T00:00:00.000Z'::TIMESTAMP AT TIME ZONE 'Europe/Brussels' FROM t"; - let select = verified_only_select(sql); + let select = + all_dialects_where(|d| !d.supports_multiword_type_names()).verified_only_select(sql); assert_eq!( &Expr::AtTimeZone { @@ -13332,10 +13333,10 @@ fn parse_execute_immediate() { fn parse_create_table_collate() { all_dialects().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); // check ordering is preserved - all_dialects().verified_stmt( + all_dialects_where(|d| !d.supports_multiword_type_names()).verified_stmt( "CREATE TABLE tbl (foo INT, bar TEXT CHARACTER SET utf8mb4 COLLATE \"de_DE\")", ); - all_dialects().verified_stmt( + all_dialects_where(|d| !d.supports_multiword_type_names()).verified_stmt( "CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\" CHARACTER SET utf8mb4)", ); } @@ -13671,7 +13672,9 @@ fn test_parse_inline_comment() { "CREATE TABLE t0 (id INT COMMENT 'comment without equal') COMMENT = 'comment with equal'"; // Hive dialect doesn't support `=` in table comment, please refer: // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable) - match all_dialects_except(|d| d.is::()).verified_stmt(sql) { + match all_dialects_except(|d| d.is::() || d.supports_multiword_type_names()) + .verified_stmt(sql) + { Statement::CreateTable(CreateTable { columns, table_options, @@ -14166,38 +14169,39 @@ fn test_group_by_grouping_sets_bare_columns() { #[test] fn test_xmltable() { - all_dialects() + let dialects = all_dialects_where(|d| !d.supports_multiword_type_names()); + dialects .verified_only_select("SELECT * FROM XMLTABLE('/root' PASSING data COLUMNS element TEXT)"); // Minimal meaningful working example: returns a single row with a single column named y containing the value z - all_dialects().verified_only_select( + dialects.verified_only_select( "SELECT y FROM XMLTABLE('/X' PASSING 'z' COLUMNS y TEXT)", ); // Test using subqueries - all_dialects().verified_only_select("SELECT y FROM XMLTABLE((SELECT '/X') PASSING (SELECT CAST('z' AS xml)) COLUMNS y TEXT PATH (SELECT 'y'))"); + dialects.verified_only_select("SELECT y FROM XMLTABLE((SELECT '/X') PASSING (SELECT CAST('z' AS xml)) COLUMNS y TEXT PATH (SELECT 'y'))"); // NOT NULL - all_dialects().verified_only_select( + dialects.verified_only_select( "SELECT y FROM XMLTABLE('/X' PASSING '' COLUMNS y TEXT NOT NULL)", ); - all_dialects().verified_only_select("SELECT * FROM XMLTABLE('/root/row' PASSING xmldata COLUMNS id INT PATH '@id', name TEXT PATH 'name/text()', value FLOAT PATH 'value')"); + dialects.verified_only_select("SELECT * FROM XMLTABLE('/root/row' PASSING xmldata COLUMNS id INT PATH '@id', name TEXT PATH 'name/text()', value FLOAT PATH 'value')"); - all_dialects().verified_only_select("SELECT * FROM XMLTABLE('//ROWS/ROW' PASSING data COLUMNS row_num FOR ORDINALITY, id INT PATH '@id', name TEXT PATH 'NAME' DEFAULT 'unnamed')"); + dialects.verified_only_select("SELECT * FROM XMLTABLE('//ROWS/ROW' PASSING data COLUMNS row_num FOR ORDINALITY, id INT PATH '@id', name TEXT PATH 'NAME' DEFAULT 'unnamed')"); // Example from https://www.postgresql.org/docs/15/functions-xml.html#FUNCTIONS-XML-PROCESSING - all_dialects().verified_only_select( + dialects.verified_only_select( "SELECT xmltable.* FROM xmldata, XMLTABLE('//ROWS/ROW' PASSING data COLUMNS id INT PATH '@id', ordinality FOR ORDINALITY, \"COUNTRY_NAME\" TEXT, country_id TEXT PATH 'COUNTRY_ID', size_sq_km FLOAT PATH 'SIZE[@unit = \"sq_km\"]', size_other TEXT PATH 'concat(SIZE[@unit!=\"sq_km\"], \" \", SIZE[@unit!=\"sq_km\"]/@unit)', premier_name TEXT PATH 'PREMIER_NAME' DEFAULT 'not specified')" ); // Example from DB2 docs without explicit PASSING clause: https://www.ibm.com/docs/en/db2/12.1.0?topic=xquery-simple-column-name-passing-xmlexists-xmlquery-xmltable - all_dialects().verified_only_select( + dialects.verified_only_select( "SELECT X.* FROM T1, XMLTABLE('$CUSTLIST/customers/customerinfo' COLUMNS \"Cid\" BIGINT PATH '@Cid', \"Info\" XML PATH 'document{.}', \"History\" XML PATH 'NULL') AS X" ); // Example from PostgreSQL with XMLNAMESPACES - all_dialects().verified_only_select( + dialects.verified_only_select( "SELECT xmltable.* FROM XMLTABLE(XMLNAMESPACES('http://example.com/myns' AS x, 'http://example.com/b' AS \"B\"), '/x:example/x:item' PASSING (SELECT data FROM xmldata) COLUMNS foo INT PATH '@foo', bar INT PATH '@B:bar')" ); } @@ -19907,8 +19911,9 @@ fn parse_adjacent_string_literal_concatenation() { #[test] fn parse_invisible_column() { + let dialects = all_dialects_where(|d| !d.supports_multiword_type_names()); let sql = r#"CREATE TABLE t (foo INT, bar INT INVISIBLE)"#; - let stmt = verified_stmt(sql); + let stmt = dialects.verified_stmt(sql); match stmt { Statement::CreateTable(CreateTable { columns, .. }) => { assert_eq!( @@ -19934,7 +19939,7 @@ fn parse_invisible_column() { } let sql = r#"ALTER TABLE t ADD COLUMN bar INT INVISIBLE"#; - let stmt = verified_stmt(sql); + let stmt = dialects.verified_stmt(sql); match stmt { Statement::AlterTable(alter_table) => { assert_eq!( @@ -20975,7 +20980,9 @@ fn parse_alter_table_column_position() { .unwrap_err() ); - let dialects = all_dialects_where(|d| !d.supports_alter_column_position()); + let dialects = all_dialects_where(|d| { + !d.supports_alter_column_position() && !d.supports_multiword_type_names() + }); for (sql, found) in [ ("ALTER TABLE tab ADD COLUMN c INT FIRST", "FIRST"), ("ALTER TABLE tab ADD COLUMN c INT AFTER b", "AFTER"), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 233a5403a5..1f61fab660 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1953,8 +1953,10 @@ fn parse_zerofill() { )) ); assert_eq!( - all_dialects_except(|d| d.supports_data_type_zerofill_suffix()) - .parse_sql_statements("CREATE TABLE t (a INT ZEROFILL)"), + all_dialects_except(|d| { + d.supports_data_type_zerofill_suffix() || d.supports_multiword_type_names() + }) + .parse_sql_statements("CREATE TABLE t (a INT ZEROFILL)"), Err(ParserError::ParserError( "Expected: ',' or ')' after column definition, found: ZEROFILL".to_string() )) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index aa9252125b..c169a8f622 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -672,8 +672,10 @@ fn parse_exclude_constraint() { // Dialects that do not opt in via `supports_exclude_constraint` must // refuse to parse `EXCLUDE` constraints. assert_eq!( - all_dialects_where(|d| !d.supports_exclude_constraint()) - .parse_sql_statements("CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =))"), + all_dialects_where(|d| { + !d.supports_exclude_constraint() && !d.supports_multiword_type_names() + }) + .parse_sql_statements("CREATE TABLE t (col INT, EXCLUDE USING gist (col WITH =))"), Err(ParserError::ParserError( "Expected: ',' or ')' after column definition, found: gist".to_string() )) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index ad80e95a93..0f64419943 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1043,6 +1043,102 @@ fn parse_create_table_string_column_names() { ); } +#[test] +fn test_multiword_type_names() { + // A SQLite type name is one or more names or strings + sqlite().verified_stmt("CREATE TABLE t (a UNSIGNED BIG INT)"); + sqlite().verified_stmt("CREATE TABLE t (a foo bar baz)"); + sqlite().verified_stmt("CREATE TABLE t (a 'text')"); + sqlite().verified_stmt("SELECT CAST(a AS foo bar) FROM t"); + sqlite().verified_stmt("ALTER TABLE t ADD b foo bar"); + // Keywords without a dedicated parser arm also form multi-word type names + sqlite().verified_stmt("CREATE TABLE t (a NATIVE CHARACTER(70))"); + sqlite().verified_stmt("CREATE TABLE t (a VARYING CHARACTER(10))"); + sqlite().verified_stmt("SELECT CAST(x AS VARYING CHARACTER) FROM t"); + // Column constraints still terminate the type name + sqlite().verified_stmt("CREATE TABLE t (a foo bar NOT NULL)"); + sqlite().verified_stmt("CREATE TABLE t (a foo bar DEFAULT 0)"); + // Standard constraint keywords are not consumed as type words + sqlite().verified_stmt("CREATE TABLE t (a INT PRIMARY KEY)"); + sqlite().verified_stmt("CREATE TABLE t (a TEXT COLLATE NOCASE)"); + sqlite().verified_stmt("CREATE TABLE t (a INT GENERATED ALWAYS AS (1))"); + sqlite().verified_stmt("CREATE TABLE t (a INT AS (1))"); + // GENERATED is a type word unless ALWAYS follows + sqlite().verified_stmt("CREATE TABLE t (a foo GENERATED, b INT)"); + sqlite().verified_stmt("CREATE TABLE t (a foo GENERATED ALWAYS AS (1))"); + // PATH is a valid type word, not a terminator + sqlite().verified_stmt("CREATE TABLE t (a foo bar PATH)"); + sqlite().verified_stmt("CREATE TABLE t (a foo PATH bar)"); + // fully-reserved keywords end the type name + for (sql, err) in [ + ( + "CREATE TABLE t (a foo AND bar)", + "Expected: ',' or ')' after column definition, found: AND", + ), + ( + "CREATE TABLE t (a foo bar ON)", + "Expected: ',' or ')' after column definition, found: ON", + ), + ( + "CREATE TABLE t (a foo bar SELECT)", + "Expected: ',' or ')' after column definition, found: SELECT", + ), + ( + "CREATE TABLE t (a foo ISNULL, b INT)", + "Expected: ',' or ')' after column definition, found: ISNULL", + ), + ( + "CREATE TABLE t (a foo INDEXED, b INT)", + "Expected: ',' or ')' after column definition, found: INDEXED", + ), + ] { + assert_eq!( + sqlite().parse_sql_statements(sql), + Err(ParserError::ParserError(err.to_string())) + ); + } +} + +#[test] +fn display_multiword_type_name_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(char); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let data_type = DataType::CustomMultiWord( + vec![Ident::new("signed"), Ident::new("int")], + vec![TypeModifier::Number("1".to_string(), false)], + ); + // A writer that rejects the type name fails before the modifier list + let mut writer = RejectingWriter('s'); + assert_eq!(write!(writer, "{data_type}"), Err(fmt::Error)); + // A writer that only rejects the parenthesized list fails on the modifiers + let mut writer = RejectingWriter('('); + assert_eq!(write!(writer, "{data_type}"), Err(fmt::Error)); +} + +#[test] +fn test_multiword_type_names_rejected_by_other_dialects() { + // GenericDialect does not support multi-word type names + assert_eq!( + TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements("CREATE TABLE t (a foo bar)"), + Err(ParserError::ParserError( + "Expected: ',' or ')' after column definition, found: bar".to_string() + )) + ); +} + #[test] fn test_cast_empty_type() { // SQLite allows CAST(expr AS) with an empty type name (typetoken can be empty) @@ -1687,6 +1783,52 @@ fn parse_left_shift_and_bitwise_and_are_left_associative() { ); } +#[test] +fn sqlite_type_name_builtin_prefixes() { + for type_name in [ + "INT FOO", + "TEXT FOO", + "DOUBLE PRECISION FOO", + "INT FIRST", + "INT INVISIBLE", + ] { + let sql = format!("CREATE TABLE t (a {type_name})"); + let Statement::CreateTable(table) = sqlite().verified_stmt(&sql) else { + unreachable!() + }; + assert_eq!( + table.columns[0].data_type, + DataType::CustomMultiWord( + type_name.split_whitespace().map(Ident::new).collect(), + vec![] + ), + ); + sqlite().verified_stmt(&format!("SELECT CAST(1 AS {type_name})")); + sqlite().verified_stmt(&format!("ALTER TABLE t ADD a {type_name}")); + } + sqlite().verified_stmt("CREATE TABLE t (a INT FOO(10, 2))"); + let Statement::CreateTable(table) = + sqlite().verified_stmt("CREATE TABLE t (a INT COMMENT 'note')") + else { + unreachable!() + }; + assert_eq!( + table.columns[0], + ColumnDef { + name: Ident::new("a"), + data_type: DataType::CustomMultiWord( + vec![ + Ident::new("INT"), + Ident::new("COMMENT"), + Ident::with_quote('\'', "note"), + ], + vec![], + ), + options: vec![], + }, + ); +} + #[test] fn parse_right_shift_and_bitwise_and_are_left_associative() { assert_eq!( @@ -1786,3 +1928,83 @@ fn parse_bitwise_shift_precedence_boundaries() { assert_eq!(sqlite().verified_expr(sql), expected, "{sql}"); } } + +#[test] +fn sqlite_type_name_builtin_parse_errors() { + for type_name in ["ENUM FOO", "TIMESTAMP WITH FOO"] { + sqlite().verified_stmt(&format!("CREATE TABLE t (a {type_name})")); + sqlite().verified_stmt(&format!("SELECT CAST(1 AS {type_name})")); + sqlite().verified_stmt(&format!("ALTER TABLE t ADD a {type_name}")); + } +} + +#[test] +fn sqlite_type_name_builtin_prefix_constraint_boundaries() { + for (constraint, option) in [ + ("NOT NULL", ColumnOption::NotNull), + ("NULL", ColumnOption::Null), + ("DEFAULT 0", ColumnOption::Default(Expr::value(number("0")))), + ( + "COLLATE NOCASE", + ColumnOption::Collation(ObjectName::from(vec![Ident::new("NOCASE")])), + ), + ] { + let sql = format!("CREATE TABLE t (a TEXT FOO {constraint})"); + let Statement::CreateTable(table) = sqlite().verified_stmt(&sql) else { + unreachable!() + }; + assert_eq!( + table.columns[0].data_type, + DataType::CustomMultiWord(vec![Ident::new("TEXT"), Ident::new("FOO")], vec![]), + ); + assert_eq!( + table.columns[0].options, + vec![ColumnOptionDef { name: None, option }], + ); + } + sqlite().verified_stmt("CREATE TABLE t (a TEXT FOO PRIMARY KEY)"); + sqlite().verified_stmt("CREATE TABLE t (a INT FOO GENERATED ALWAYS AS (1), b INT)"); + sqlite().verified_stmt("CREATE TABLE t (a INT FOO GENERATED, b INT)"); +} + +#[test] +fn sqlite_type_name_untyped_null_constraint() { + let Statement::CreateTable(table) = sqlite().verified_stmt("CREATE TABLE t (a NULL, b INT)") + else { + unreachable!() + }; + assert_eq!(table.columns[0].data_type, DataType::Unspecified); + assert_eq!( + table.columns[0].options, + vec![ColumnOptionDef { + name: None, + option: ColumnOption::Null, + }], + ); + let AlterTableOperation::AddColumn { column_def, .. } = + alter_table_op_with_name(sqlite().verified_stmt("ALTER TABLE t ADD a NULL"), "t") + else { + unreachable!() + }; + assert_eq!(column_def, table.columns[0]); + sqlite().verified_stmt(r#"CREATE TABLE t (a "NULL" foo)"#); +} + +#[test] +fn sqlite_type_name_rejects_words_after_null_constraint() { + for (sql, error) in [ + ( + "CREATE TABLE t (a NULL foo)", + "Expected: ',' or ')' after column definition, found: foo", + ), + ( + "ALTER TABLE t ADD a NULL foo", + "Expected: end of statement, found: foo", + ), + ] { + assert_eq!( + sqlite().parse_sql_statements(sql), + Err(ParserError::ParserError(error.to_string())), + ); + } +}