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
7 changes: 7 additions & 0 deletions src/ast/data_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ pub enum DataType {
///
/// [ClickHouse]: https://clickhouse.com/docs/en/sql-reference/data-types/nested-data-structures/nested
Nested(Vec<ColumnDef>),
/// Structured object type, see [Snowflake].
///
/// [Snowflake]: https://docs.snowflake.com/en/sql-reference/data-types-structured#structured-object-types
Object(Vec<ColumnDef>),
/// Enum type.
Enum(Vec<EnumMember>, Option<u8>),
/// Set type.
Expand Down Expand Up @@ -802,6 +806,9 @@ impl fmt::Display for DataType {
DataType::Nested(fields) => {
write!(f, "Nested({})", display_comma_separated(fields))
}
DataType::Object(fields) => {
write!(f, "OBJECT({})", display_comma_separated(fields))
}
DataType::Unspecified => Ok(()),
DataType::Trigger => write!(f, "TRIGGER"),
DataType::AnyType => write!(f, "ANY TYPE"),
Expand Down
26 changes: 26 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13116,6 +13116,9 @@ impl<'a> Parser<'a> {
))))
}
}
Keyword::OBJECT if self.peek_token_ref().token == Token::LParen => {
Ok(DataType::Object(self.parse_structured_object_type_def()?))
}
Keyword::STRUCT if dialect_is!(dialect is DuckDbDialect) => {
self.prev_token();
let field_defs = self.parse_duckdb_struct_type_def()?;
Expand Down Expand Up @@ -14333,6 +14336,29 @@ impl<'a> Parser<'a> {
}
}

fn parse_structured_object_type_def(&mut self) -> Result<Vec<ColumnDef>, ParserError> {
self.expect_token(&Token::LParen)?;
let fields = self.parse_comma_separated(|parser| {
let name = parser.parse_identifier()?;
let data_type = parser.parse_data_type()?;
let options = if parser.parse_keywords(&[Keyword::NOT, Keyword::NULL]) {
vec![ColumnOptionDef {
name: None,
option: ColumnOption::NotNull,
}]
} else {
vec![]
};
Ok(ColumnDef {
name,
data_type,
options,
})
})?;
self.expect_token(&Token::RParen)?;
Ok(fields)
}

/// Parse a parenthesized sub data type
fn parse_sub_type<F>(&mut self, parent_type: F) -> Result<DataType, ParserError>
where
Expand Down
32 changes: 32 additions & 0 deletions tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4912,3 +4912,35 @@ fn test_select_dollar_column_from_stage() {
// With table function args, without alias
snowflake().verified_stmt("SELECT $1, $2 FROM @mystage1(file_format => 'myformat')");
}
#[test]
fn test_structured_object_type() {
snowflake_and_generic().verified_stmt(
"SELECT payload::OBJECT(address OBJECT(city VARCHAR NOT NULL), zip NUMBER) FROM t",
);

let select = snowflake().verified_only_select(
"SELECT CAST(payload AS OBJECT(city VARCHAR, zip NUMBER NOT NULL)) FROM t",
);
let Expr::Cast { data_type, .. } = expr_from_projection(only(&select.projection)) else {
unreachable!();
};
let DataType::Object(fields) = data_type else {
unreachable!();
};
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, Ident::new("city"));
assert!(fields[0].options.is_empty());
assert_eq!(fields[1].name, Ident::new("zip"));
assert_eq!(fields[1].options.len(), 1);
assert_eq!(fields[1].options[0].option, ColumnOption::NotNull);

snowflake_and_generic().verified_stmt("CREATE TABLE t (o OBJECT)");

for sql in [
"CREATE TABLE t (o OBJECT(VARCHAR))",
"CREATE TABLE t (o OBJECT(city VARCHAR NULL))",
"CREATE TABLE t (o OBJECT(city VARCHAR)",
] {
assert!(snowflake().parse_sql_statements(sql).is_err(), "{sql}");
}
}
Loading