diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 0a31ead3f3..acb1b2ab9c 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::{display_comma_separated, Expr, ObjectName, StructField, UnionField}; +use crate::ast::{display_comma_separated, Expr, Ident, ObjectName, StructField, UnionField}; use super::{value::escape_single_quote_string, ColumnDef}; @@ -42,6 +42,29 @@ pub enum EnumMember { NamedValue(String, Expr), } +/// One entry of a custom data type's parenthesized modifier list. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TypeModifier { + /// Identifier or keyword segment, quotes carried by `Ident`. + Identifier(Ident), + /// Numeric literal, the `bool` carries the `L` long suffix like `Value::Number`. + Number(String, bool), + /// Single quoted string literal. + String(String), +} + +impl fmt::Display for TypeModifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Identifier(id) => write!(f, "{id}"), + Self::Number(n, long) => write!(f, "{}{long}", n, long = if *long { "L" } else { "" }), + Self::String(s) => write!(f, "'{}'", escape_single_quote_string(s)), + } + } +} + /// SQL data types #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -441,8 +464,8 @@ pub enum DataType { /// /// [PostgreSQL]: https://www.postgresql.org/docs/current/datatype.html VarBit(Option), - /// Custom types. - Custom(ObjectName, Vec), + /// Custom type with a modifier list, e.g. `GEOMETRY(POINT, 4326)`. + Custom(ObjectName, Vec), /// Arrays. Array(ArrayElemTypeDef), /// Map, see [ClickHouse], [Hive]. @@ -744,7 +767,7 @@ impl fmt::Display for DataType { if modifiers.is_empty() { write!(f, "{ty}") } else { - write!(f, "{}({})", ty, modifiers.join(", ")) + write!(f, "{ty}({})", display_comma_separated(modifiers)) } } DataType::Enum(vals, bits) => { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb5bc6cc75..a96d1dfe03 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -55,7 +55,7 @@ use crate::{ pub use self::data_type::{ ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember, - ExactNumberInfo, IntervalFields, MapBracketKind, StructBracketKind, TimezoneInfo, + ExactNumberInfo, IntervalFields, MapBracketKind, StructBracketKind, TimezoneInfo, TypeModifier, }; pub use self::dcl::{ AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 276b252ed2..f632c8a4d3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14627,15 +14627,19 @@ impl<'a> Parser<'a> { } /// Parse optional type modifiers appearing in parentheses e.g. `(UNSIGNED, ZEROFILL)`. - pub fn parse_optional_type_modifiers(&mut self) -> Result>, ParserError> { + pub fn parse_optional_type_modifiers( + &mut self, + ) -> Result>, ParserError> { if self.consume_token(&Token::LParen) { let mut modifiers = Vec::new(); loop { let next_token = self.next_token(); match next_token.token { - Token::Word(w) => modifiers.push(w.to_string()), - Token::Number(n, _) => modifiers.push(n), - Token::SingleQuotedString(s) => modifiers.push(s), + Token::Word(w) => { + modifiers.push(TypeModifier::Identifier(w.to_ident(next_token.span))) + } + Token::Number(n, long) => modifiers.push(TypeModifier::Number(n, long)), + Token::SingleQuotedString(s) => modifiers.push(TypeModifier::String(s)), Token::Comma => { continue; @@ -21791,7 +21795,8 @@ mod tests { #[cfg(test)] mod test_parse_data_type { use crate::ast::{ - CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, ObjectName, TimezoneInfo, + CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, Ident, ObjectName, + TimezoneInfo, TypeModifier, }; use crate::dialect::{AnsiDialect, GenericDialect, PostgreSqlDialect}; use crate::test_utils::TestedDialects; @@ -21982,7 +21987,7 @@ mod tests { "GEOMETRY(POINT)", DataType::Custom( ObjectName::from(vec!["GEOMETRY".into()]), - vec!["POINT".to_string()] + vec![TypeModifier::Identifier(Ident::new("POINT"))] ) ); @@ -21991,7 +21996,10 @@ mod tests { "GEOMETRY(POINT, 4326)", DataType::Custom( ObjectName::from(vec!["GEOMETRY".into()]), - vec!["POINT".to_string(), "4326".to_string()] + vec![ + TypeModifier::Identifier(Ident::new("POINT")), + TypeModifier::Number("4326".to_string(), false), + ] ) ); } diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 45b7649d1a..5b88aa0d7f 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1990,7 +1990,7 @@ fn parse_object_type_parameter() { columns[0].data_type, DataType::Custom( ObjectName::from(vec![Ident::new("Object")]), - vec!["json".to_string()] + vec![TypeModifier::String("json".to_string())] ) ); @@ -2001,13 +2001,26 @@ fn parse_object_type_parameter() { ); } + clickhouse_and_generic().verified_stmt("CREATE TABLE t (o Object('json'))"); clickhouse_and_generic().verified_stmt("CREATE TABLE t (o OBJECT())"); clickhouse_and_generic().verified_stmt("CREATE TABLE t (o OBJECT(city VARCHAR NOT NULL))"); for (sql, modifiers) in [ - ("CREATE TABLE t (o OBJECT(10))", vec!["10"]), - ("CREATE TABLE t (o OBJECT(foo))", vec!["foo"]), - ("CREATE TABLE t (o OBJECT(foo, bar))", vec!["foo", "bar"]), + ( + "CREATE TABLE t (o OBJECT(10))", + vec![TypeModifier::Number("10".to_string(), false)], + ), + ( + "CREATE TABLE t (o OBJECT(foo))", + vec![TypeModifier::Identifier(Ident::new("foo"))], + ), + ( + "CREATE TABLE t (o OBJECT(foo, bar))", + vec![ + TypeModifier::Identifier(Ident::new("foo")), + TypeModifier::Identifier(Ident::new("bar")), + ], + ), ] { for dialects in [ clickhouse(), @@ -2023,10 +2036,7 @@ fn parse_object_type_parameter() { columns[0].data_type, DataType::Custom( ObjectName::from(vec![Ident::new("OBJECT")]), - modifiers - .iter() - .map(|modifier| (*modifier).to_owned()) - .collect(), + modifiers.clone() ) ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 87fb61dc17..6402a5b4e0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6953,7 +6953,7 @@ fn parse_text_type_modifier_double_colon_cast() { expr: Box::new(Expr::Identifier(Ident::new("ID"))), data_type: DataType::Custom( ObjectName::from(vec![Ident::new("TEXT")]), - vec!["16777216".to_string()] + vec![TypeModifier::Number("16777216".to_string(), false)] ), format: None, } @@ -21022,3 +21022,35 @@ fn parse_window_spec_keyword_base_window_name() { "SELECT count(*) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t", ); } + +#[test] +fn parse_custom_type_modifier_spellings() { + for sql in [ + r#"CREATE TABLE t (c foo('a b'))"#, + r#"CREATE TABLE t (c foo(')'))"#, + r#"CREATE TABLE t (c foo('it''s'))"#, + "CREATE TABLE t (c foo(bar, 9))", + ] { + all_dialects().verified_stmt(sql); + } + let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]); + generic.verified_stmt(r#"CREATE TABLE t (c foo("quoted"))"#); + generic.verified_stmt("CREATE TABLE t (c foo(9L))"); + + let statements = generic + .parse_sql_statements("CREATE TABLE t (c foo(bar, 9, 'z', 42L))") + .unwrap(); + let Statement::CreateTable(create_table) = &statements[0] else { + panic!("expected CREATE TABLE") + }; + let DataType::Custom(_, modifiers) = &create_table.columns[0].data_type else { + panic!("expected a custom type") + }; + let expected = vec![ + TypeModifier::Identifier(Ident::new("bar")), + TypeModifier::Number("9".to_string(), false), + TypeModifier::String("z".to_string()), + TypeModifier::Number("42".to_string(), true), + ]; + assert_eq!(modifiers, &expected); +}