From fe92bd9e9f8a471043149370ca661bf45916271b Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:05:56 +0100 Subject: [PATCH 1/4] postgres: support UNLOGGED tables and SET LOGGED/UNLOGGED Add parser and AST support for PostgreSQL CREATE UNLOGGED TABLE and\nALTER TABLE ... SET LOGGED|UNLOGGED operations.\n\n- add LOGGED keyword\n- add CreateTable.unlogged and wire it through CreateTableBuilder\n- render UNLOGGED in CreateTable display\n- add AlterTableOperation::SetLogged and ::SetUnlogged display/spans\n- parse UNLOGGED only for PostgreSqlDialect|GenericDialect\n- parse ALTER TABLE SET LOGGED|UNLOGGED operations --- src/ast/ddl.rs | 19 ++++++++++++- src/ast/helpers/stmt_create_table.rs | 10 +++++++ src/ast/spans.rs | 3 ++ src/keywords.rs | 1 + src/parser/mod.rs | 42 +++++++++++++++++++--------- 5 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0c4f93e647..707e9dd67c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -442,6 +442,14 @@ pub enum AlterTableOperation { /// Table properties specified as SQL options. table_properties: Vec, }, + /// `SET LOGGED` + /// + /// Note: this is PostgreSQL-specific. + SetLogged, + /// `SET UNLOGGED` + /// + /// Note: this is PostgreSQL-specific. + SetUnlogged, /// `OWNER TO { | CURRENT_ROLE | CURRENT_USER | SESSION_USER }` /// /// Note: this is PostgreSQL-specific @@ -965,6 +973,12 @@ impl fmt::Display for AlterTableOperation { display_comma_separated(table_properties) ) } + AlterTableOperation::SetLogged => { + write!(f, "SET LOGGED") + } + AlterTableOperation::SetUnlogged => { + write!(f, "SET UNLOGGED") + } AlterTableOperation::FreezePartition { partition, with_name, @@ -2889,6 +2903,8 @@ pub struct CreateTable { pub or_replace: bool, /// `TEMP` or `TEMPORARY` clause pub temporary: bool, + /// `UNLOGGED` clause + pub unlogged: bool, /// `EXTERNAL` clause pub external: bool, /// `DYNAMIC` clause @@ -3045,7 +3061,7 @@ impl fmt::Display for CreateTable { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{temporary}{transient}{volatile}{dynamic}{iceberg}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{temporary}{unlogged}{transient}{volatile}{dynamic}{iceberg}TABLE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, external = if self.external { "EXTERNAL " } else { "" }, global = self.global @@ -3059,6 +3075,7 @@ impl fmt::Display for CreateTable { .unwrap_or(""), if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" }, temporary = if self.temporary { "TEMPORARY " } else { "" }, + unlogged = if self.unlogged { "UNLOGGED " } else { "" }, transient = if self.transient { "TRANSIENT " } else { "" }, volatile = if self.volatile { "VOLATILE " } else { "" }, // Only for Snowflake diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index e63c90dbcf..4c95265c6f 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -68,6 +68,8 @@ pub struct CreateTableBuilder { pub or_replace: bool, /// Whether the table is `TEMPORARY`. pub temporary: bool, + /// Whether the table is `UNLOGGED`. + pub unlogged: bool, /// Whether the table is `EXTERNAL`. pub external: bool, /// Optional `GLOBAL` flag for dialects that support it. @@ -178,6 +180,7 @@ impl CreateTableBuilder { Self { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, if_not_exists: false, @@ -241,6 +244,11 @@ impl CreateTableBuilder { self.temporary = temporary; self } + /// Mark the table as `UNLOGGED`. + pub fn unlogged(mut self, unlogged: bool) -> Self { + self.unlogged = unlogged; + self + } /// Mark the table as `EXTERNAL`. pub fn external(mut self, external: bool) -> Self { self.external = external; @@ -509,6 +517,7 @@ impl CreateTableBuilder { CreateTable { or_replace: self.or_replace, temporary: self.temporary, + unlogged: self.unlogged, external: self.external, global: self.global, if_not_exists: self.if_not_exists, @@ -584,6 +593,7 @@ impl From for CreateTableBuilder { Self { or_replace: table.or_replace, temporary: table.temporary, + unlogged: table.unlogged, external: table.external, global: table.global, if_not_exists: table.if_not_exists, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0b95c3ed70..aa4224e285 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -531,6 +531,7 @@ impl Spanned for CreateTable { let CreateTable { or_replace: _, // bool temporary: _, // bool + unlogged: _, // bool external: _, // bool global: _, // bool dynamic: _, // bool @@ -1184,6 +1185,8 @@ impl Spanned for AlterTableOperation { AlterTableOperation::SetTblProperties { table_properties } => { union_spans(table_properties.iter().map(|i| i.span())) } + AlterTableOperation::SetLogged => Span::empty(), + AlterTableOperation::SetUnlogged => Span::empty(), AlterTableOperation::OwnerTo { .. } => Span::empty(), AlterTableOperation::ClusterBy { exprs } => union_spans(exprs.iter().map(|e| e.span())), AlterTableOperation::DropClusteringKey => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index cc2b9e9dd0..665b8454c3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -587,6 +587,7 @@ define_keywords!( LOCK, LOCKED, LOG, + LOGGED, LOGIN, LOGS, LONG, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bea566bbe8..15c2e98913 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5097,12 +5097,16 @@ impl<'a> Parser<'a> { let temporary = self .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) .is_some(); + let unlogged = dialect_of!(self is PostgreSqlDialect | GenericDialect) + && self.parse_keyword(Keyword::UNLOGGED); let persistent = dialect_of!(self is DuckDbDialect) && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some(); let create_view_params = self.parse_create_view_params()?; if self.parse_keyword(Keyword::TABLE) { - self.parse_create_table(or_replace, temporary, global, transient) + self.parse_create_table(or_replace, temporary, unlogged, global, transient) .map(Into::into) + } else if unlogged { + self.expected_ref("TABLE after UNLOGGED", self.peek_token_ref()) } else if self.peek_keyword(Keyword::MATERIALIZED) || self.peek_keyword(Keyword::VIEW) || self.peek_keywords(&[Keyword::SECURE, Keyword::MATERIALIZED, Keyword::VIEW]) @@ -8264,6 +8268,7 @@ impl<'a> Parser<'a> { &mut self, or_replace: bool, temporary: bool, + unlogged: bool, global: Option, transient: bool, ) -> Result { @@ -8382,6 +8387,7 @@ impl<'a> Parser<'a> { Ok(CreateTableBuilder::new(table_name) .temporary(temporary) + .unlogged(unlogged) .columns(columns) .constraints(constraints) .or_replace(or_replace) @@ -10377,21 +10383,31 @@ impl<'a> Parser<'a> { let name = self.parse_identifier()?; AlterTableOperation::ValidateConstraint { name } } else { - let mut options = - self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; - if !options.is_empty() { - AlterTableOperation::SetTblProperties { - table_properties: options, - } + if dialect_of!(self is PostgreSqlDialect | GenericDialect) + && self.parse_keywords(&[Keyword::SET, Keyword::LOGGED]) + { + AlterTableOperation::SetLogged + } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) + && self.parse_keywords(&[Keyword::SET, Keyword::UNLOGGED]) + { + AlterTableOperation::SetUnlogged } else { - options = self.parse_options(Keyword::SET)?; + let mut options = + self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; if !options.is_empty() { - AlterTableOperation::SetOptionsParens { options } + AlterTableOperation::SetTblProperties { + table_properties: options, + } } else { - return self.expected_ref( - "ADD, RENAME, PARTITION, SWAP, DROP, REPLICA IDENTITY, SET, or SET TBLPROPERTIES after ALTER TABLE", - self.peek_token_ref(), - ); + options = self.parse_options(Keyword::SET)?; + if !options.is_empty() { + AlterTableOperation::SetOptionsParens { options } + } else { + return self.expected_ref( + "ADD, RENAME, PARTITION, SWAP, DROP, REPLICA IDENTITY, SET, or SET TBLPROPERTIES after ALTER TABLE", + self.peek_token_ref(), + ); + } } } }; From 1233fc1758dc2542d013391fbebec5e64e065543 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:06:02 +0100 Subject: [PATCH 2/4] tests: cover unlogged table and logged state operations Add PostgreSQL regression coverage for the new syntax support and\nupdate existing struct-literal CreateTable expectations with the\nnew unlogged field in cross-dialect tests.\n\n- add parse_create_unlogged_table\n- add parse_alter_table_set_logged_unlogged\n- set unlogged defaults in duckdb/mssql fixture assertions --- tests/sqlparser_duckdb.rs | 1 + tests/sqlparser_mssql.rs | 2 + tests/sqlparser_postgres.rs | 73 +++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index e0e3f143b6..b2221e2b8f 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -699,6 +699,7 @@ fn test_duckdb_union_datatype() { Statement::CreateTable(CreateTable { or_replace: Default::default(), temporary: Default::default(), + unlogged: Default::default(), external: Default::default(), global: Default::default(), if_not_exists: Default::default(), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 6c8412a4ae..7a96313cb0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1923,6 +1923,7 @@ fn parse_create_table_with_valid_options() { Statement::CreateTable(CreateTable { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, dynamic: false, @@ -2110,6 +2111,7 @@ fn parse_create_table_with_identity_column() { Statement::CreateTable(CreateTable { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, dynamic: false, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7c19f51e5e..02aab9874a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -988,6 +988,33 @@ fn parse_alter_table_owner_to() { ); } +#[test] +fn parse_alter_table_set_logged_unlogged() { + let sql = "ALTER TABLE unlogged1 SET LOGGED"; + match pg_and_generic().verified_stmt(sql) { + Statement::AlterTable(AlterTable { + name, operations, .. + }) => { + assert_eq!("unlogged1", name.to_string()); + assert_eq!(vec![AlterTableOperation::SetLogged], operations); + } + _ => unreachable!(), + } + pg_and_generic().one_statement_parses_to(sql, sql); + + let sql = "ALTER TABLE unlogged1 SET UNLOGGED"; + match pg_and_generic().verified_stmt(sql) { + Statement::AlterTable(AlterTable { + name, operations, .. + }) => { + assert_eq!("unlogged1", name.to_string()); + assert_eq!(vec![AlterTableOperation::SetUnlogged], operations); + } + _ => unreachable!(), + } + pg_and_generic().one_statement_parses_to(sql, sql); +} + #[test] fn parse_create_table_if_not_exists() { let sql = "CREATE TABLE IF NOT EXISTS uk_cities ()"; @@ -5389,6 +5416,51 @@ fn parse_create_table_with_partition_by() { } } +#[test] +fn parse_create_unlogged_table() { + let sql = "CREATE UNLOGGED TABLE public.unlogged2 (a int primary key)"; + match pg_and_generic().one_statement_parses_to( + sql, + "CREATE UNLOGGED TABLE public.unlogged2 (a INT PRIMARY KEY)", + ) { + Statement::CreateTable(CreateTable { name, unlogged, .. }) => { + assert!(unlogged); + assert_eq!("public.unlogged2", name.to_string()); + } + _ => unreachable!(), + } + + let sql = "CREATE UNLOGGED TABLE pg_temp.unlogged3 (a int primary key)"; + match pg_and_generic().one_statement_parses_to( + sql, + "CREATE UNLOGGED TABLE pg_temp.unlogged3 (a INT PRIMARY KEY)", + ) { + Statement::CreateTable(CreateTable { name, unlogged, .. }) => { + assert!(unlogged); + assert_eq!("pg_temp.unlogged3", name.to_string()); + } + _ => unreachable!(), + } + + let sql = "CREATE UNLOGGED TABLE unlogged1 (a int) PARTITION BY RANGE (a)"; + match pg_and_generic().one_statement_parses_to( + sql, + "CREATE UNLOGGED TABLE unlogged1 (a INT) PARTITION BY RANGE(a)", + ) { + Statement::CreateTable(CreateTable { + name, + unlogged, + partition_by, + .. + }) => { + assert!(unlogged); + assert_eq!("unlogged1", name.to_string()); + assert!(partition_by.is_some()); + } + _ => unreachable!(), + } +} + #[test] fn parse_join_constraint_unnest_alias() { assert_eq!( @@ -6304,6 +6376,7 @@ fn parse_trigger_related_functions() { CreateTable { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, dynamic: false, From 1e7e4c7c1dab9b1b545ca1c9d269c7ff36bb88f6 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:53:08 +0100 Subject: [PATCH 3/4] parser: collapse clippy-collapsible match branches Refactor parser match arms to use guards instead of nested if blocks where Clippy flagged collapsible_match. This keeps behavior unchanged while making control flow clearer in statement parsing, wildcard qualification handling, terminal keyword detection, and Hive row format delimiter parsing. --- src/parser/mod.rs | 169 ++++++++++++++++++++++------------------------ 1 file changed, 79 insertions(+), 90 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 15c2e98913..7381fa76d5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -508,10 +508,10 @@ impl<'a> Parser<'a> { Token::EOF => break, // end of statement - Token::Word(word) => { - if expecting_statement_delimiter && word.keyword == Keyword::END { - break; - } + Token::Word(word) + if expecting_statement_delimiter && word.keyword == Keyword::END => + { + break; } _ => {} } @@ -1298,41 +1298,40 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { - t @ (Token::Word(_) | Token::SingleQuotedString(_)) => { - if self.peek_token_ref().token == Token::Period { - let mut id_parts: Vec = vec![match t { - Token::Word(w) => w.into_ident(next_token.span), - Token::SingleQuotedString(s) => Ident::with_quote('\'', s), - _ => { - return Err(ParserError::ParserError( - "Internal parser error: unexpected token type".to_string(), - )) + t @ (Token::Word(_) | Token::SingleQuotedString(_)) + if self.peek_token_ref().token == Token::Period => + { + let mut id_parts: Vec = vec![match t { + Token::Word(w) => w.into_ident(next_token.span), + Token::SingleQuotedString(s) => Ident::with_quote('\'', s), + _ => { + return Err(ParserError::ParserError( + "Internal parser error: unexpected token type".to_string(), + )) + } + }]; + + while self.consume_token(&Token::Period) { + let next_token = self.next_token(); + match next_token.token { + Token::Word(w) => id_parts.push(w.into_ident(next_token.span)), + Token::SingleQuotedString(s) => { + // SQLite has single-quoted identifiers + id_parts.push(Ident::with_quote('\'', s)) } - }]; - - while self.consume_token(&Token::Period) { - let next_token = self.next_token(); - match next_token.token { - Token::Word(w) => id_parts.push(w.into_ident(next_token.span)), - Token::SingleQuotedString(s) => { - // SQLite has single-quoted identifiers - id_parts.push(Ident::with_quote('\'', s)) - } - Token::Placeholder(s) => { - // Snowflake uses $1, $2, etc. for positional column references - // in staged data queries like: SELECT t.$1 FROM @stage t - id_parts.push(Ident::new(s)) - } - Token::Mul => { - return Ok(Expr::QualifiedWildcard( - ObjectName::from(id_parts), - AttachedToken(next_token), - )); - } - _ => { - return self - .expected("an identifier or a '*' after '.'", next_token); - } + Token::Placeholder(s) => { + // Snowflake uses $1, $2, etc. for positional column references + // in staged data queries like: SELECT t.$1 FROM @stage t + id_parts.push(Ident::new(s)) + } + Token::Mul => { + return Ok(Expr::QualifiedWildcard( + ObjectName::from(id_parts), + AttachedToken(next_token), + )); + } + _ => { + return self.expected("an identifier or a '*' after '.'", next_token); } } } @@ -4990,10 +4989,10 @@ impl<'a> Parser<'a> { loop { match &self.peek_nth_token_ref(0).token { Token::EOF => break, - Token::Word(w) => { - if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) { - break; - } + Token::Word(w) + if w.quote_style.is_none() && terminal_keywords.contains(&w.keyword) => + { + break; } _ => {} } @@ -8177,70 +8176,60 @@ impl<'a> Parser<'a> { Keyword::LINES, Keyword::NULL, ]) { - Some(Keyword::FIELDS) => { - if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) { + Some(Keyword::FIELDS) + if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::FieldsTerminatedBy, + char: self.parse_identifier()?, + }); + + if self.parse_keywords(&[Keyword::ESCAPED, Keyword::BY]) { row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::FieldsTerminatedBy, + delimiter: HiveDelimiter::FieldsEscapedBy, char: self.parse_identifier()?, }); - - if self.parse_keywords(&[Keyword::ESCAPED, Keyword::BY]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::FieldsEscapedBy, - char: self.parse_identifier()?, - }); - } - } else { - break; } } - Some(Keyword::COLLECTION) => { + Some(Keyword::COLLECTION) if self.parse_keywords(&[ Keyword::ITEMS, Keyword::TERMINATED, Keyword::BY, - ]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::CollectionItemsTerminatedBy, - char: self.parse_identifier()?, - }); - } else { - break; - } + ]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::CollectionItemsTerminatedBy, + char: self.parse_identifier()?, + }); } - Some(Keyword::MAP) => { + Some(Keyword::MAP) if self.parse_keywords(&[ Keyword::KEYS, Keyword::TERMINATED, Keyword::BY, - ]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::MapKeysTerminatedBy, - char: self.parse_identifier()?, - }); - } else { - break; - } + ]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::MapKeysTerminatedBy, + char: self.parse_identifier()?, + }); } - Some(Keyword::LINES) => { - if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::LinesTerminatedBy, - char: self.parse_identifier()?, - }); - } else { - break; - } + Some(Keyword::LINES) + if self.parse_keywords(&[Keyword::TERMINATED, Keyword::BY]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::LinesTerminatedBy, + char: self.parse_identifier()?, + }); } - Some(Keyword::NULL) => { - if self.parse_keywords(&[Keyword::DEFINED, Keyword::AS]) { - row_delimiters.push(HiveRowDelimiter { - delimiter: HiveDelimiter::NullDefinedAs, - char: self.parse_identifier()?, - }); - } else { - break; - } + Some(Keyword::NULL) + if self.parse_keywords(&[Keyword::DEFINED, Keyword::AS]) => + { + row_delimiters.push(HiveRowDelimiter { + delimiter: HiveDelimiter::NullDefinedAs, + char: self.parse_identifier()?, + }); } _ => { break; From cffc8bb68252e22b3586d131449590b96f7ead05 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 09:00:20 +0100 Subject: [PATCH 4/4] parser: flatten ALTER TABLE SET branch chain Refactor parse_alter_table_operation to collapse a nested else-if structure into a direct else-if chain for PostgreSQL/GEDERIC SET LOGGED and SET UNLOGGED handling. This is a no-behavior-change control-flow cleanup aligned with Clippy's collapsible_else_if lint under -D warnings. --- src/parser/mod.rs | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7381fa76d5..7c9627f4b9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10371,32 +10371,30 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { let name = self.parse_identifier()?; AlterTableOperation::ValidateConstraint { name } + } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) + && self.parse_keywords(&[Keyword::SET, Keyword::LOGGED]) + { + AlterTableOperation::SetLogged + } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) + && self.parse_keywords(&[Keyword::SET, Keyword::UNLOGGED]) + { + AlterTableOperation::SetUnlogged } else { - if dialect_of!(self is PostgreSqlDialect | GenericDialect) - && self.parse_keywords(&[Keyword::SET, Keyword::LOGGED]) - { - AlterTableOperation::SetLogged - } else if dialect_of!(self is PostgreSqlDialect | GenericDialect) - && self.parse_keywords(&[Keyword::SET, Keyword::UNLOGGED]) - { - AlterTableOperation::SetUnlogged + let mut options = + self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; + if !options.is_empty() { + AlterTableOperation::SetTblProperties { + table_properties: options, + } } else { - let mut options = - self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; + options = self.parse_options(Keyword::SET)?; if !options.is_empty() { - AlterTableOperation::SetTblProperties { - table_properties: options, - } + AlterTableOperation::SetOptionsParens { options } } else { - options = self.parse_options(Keyword::SET)?; - if !options.is_empty() { - AlterTableOperation::SetOptionsParens { options } - } else { - return self.expected_ref( - "ADD, RENAME, PARTITION, SWAP, DROP, REPLICA IDENTITY, SET, or SET TBLPROPERTIES after ALTER TABLE", - self.peek_token_ref(), - ); - } + return self.expected_ref( + "ADD, RENAME, PARTITION, SWAP, DROP, REPLICA IDENTITY, SET, or SET TBLPROPERTIES after ALTER TABLE", + self.peek_token_ref(), + ); } } };