From 0047cdbed0a4ced7de21c0017ded9c4619db4c8a Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 27 Oct 2026 22:29:30 +0100 Subject: [PATCH] PostgreSQL: Support ALTER TABLE RESET and storage parameters without a value --- src/ast/ddl.rs | 13 +++++++++ src/ast/mod.rs | 3 ++- src/ast/spans.rs | 3 +++ src/dialect/generic.rs | 4 +++ src/dialect/mod.rs | 8 ++++++ src/dialect/postgresql.rs | 4 +++ src/parser/mod.rs | 10 +++++++ tests/sqlparser_common.rs | 27 +++++++++++++++++++ tests/sqlparser_postgres.rs | 53 +++++++++++++++++++++++++++++++++++-- 9 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 10e278483a..ce7f70a364 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -560,6 +560,16 @@ pub enum AlterTableOperation { /// Parenthesized options supplied to `SET (...)`. options: Vec, }, + /// Parenthesized `RESET` of storage parameters. + /// + /// ```sql + /// RESET ( storage_parameter [, ... ] ) + /// ``` + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertable.html) + ResetOptions { + /// Parameters to reset. + options: Vec, + }, } /// An `ALTER Policy` (`Statement::AlterPolicy`) operation @@ -1122,6 +1132,9 @@ impl fmt::Display for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { write!(f, "SET ({})", display_comma_separated(options)) } + AlterTableOperation::ResetOptions { options } => { + write!(f, "RESET ({})", display_comma_separated(options)) + } } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0fa3841fd5..f876371629 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9104,7 +9104,8 @@ pub enum SqlOption { /// /// Clustered(TableOptionsClustered), - /// Single identifier options, e.g. `HEAP` for MSSQL. + /// Single identifier options, e.g. `HEAP` for MSSQL or a PostgreSQL storage parameter + /// named without a value. /// /// Ident(Ident), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 704b583b8b..3760d1db0e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1312,6 +1312,9 @@ impl Spanned for AlterTableOperation { AlterTableOperation::SetOptionsParens { options } => { union_spans(options.iter().map(|i| i.span())) } + AlterTableOperation::ResetOptions { options } => { + union_spans(options.iter().map(|i| i.span)) + } } } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 6745bf8d11..cafa085275 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -141,6 +141,10 @@ impl Dialect for GenericDialect { true } + fn supports_sql_option_without_value(&self) -> bool { + true + } + fn supports_explain_with_utility_options(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8d92f0b57d..6bd56f09cd 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1334,6 +1334,14 @@ pub trait Dialect: Debug + Any { false } + /// Does the dialect accept an option named without a value, + /// e.g. `CREATE VIEW v WITH (security_barrier) AS SELECT 1`? + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) + fn supports_sql_option_without_value(&self) -> bool { + false + } + /// Whether `INTERVAL` expressions require units (called "qualifiers" in the ANSI SQL spec) to be specified, /// e.g. `INTERVAL 1 DAY` vs `INTERVAL 1`. /// diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 1455e9f06f..1c57e44cd9 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -398,6 +398,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_sql_option_without_value(&self) -> bool { + true + } + /// see fn supports_explain_with_utility_options(&self) -> bool { true diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 90296755ab..b09d90a0ba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11089,6 +11089,11 @@ impl<'a> Parser<'a> { } _ => { let name = self.parse_identifier()?; + if self.dialect.supports_sql_option_without_value() + && matches!(self.peek_token_ref().token, Token::Comma | Token::RParen) + { + return Ok(SqlOption::Ident(name)); + } self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; @@ -11696,6 +11701,11 @@ impl<'a> Parser<'a> { AlterTableOperation::SetLogged } else if self.parse_keywords(&[Keyword::SET, Keyword::UNLOGGED]) { AlterTableOperation::SetUnlogged + } else if self.parse_keyword(Keyword::RESET) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_token(&Token::RParen)?; + AlterTableOperation::ResetOptions { options } } else { let mut options = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 379e37a660..63f8816e9c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22744,3 +22744,30 @@ fn display_create_procedure_parameter_list_propagates_write_errors() { let mut writer = RejectingWriter(" ("); assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); } + +#[test] +fn parse_alter_table_reset_options() { + match alter_table_op(verified_stmt( + "ALTER TABLE tab RESET (fillfactor, autovacuum_enabled)", + )) { + AlterTableOperation::ResetOptions { options } => { + assert_eq!( + options, + [Ident::new("fillfactor"), Ident::new("autovacuum_enabled")] + ); + } + _ => unreachable!(), + } + + for (sql, expected) in [ + ("ALTER TABLE tab RESET ()", "Expected: identifier, found: )"), + ("ALTER TABLE tab RESET (a = 1)", "Expected: ), found: ="), + ("ALTER TABLE tab RESET a", "Expected: (, found: a"), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + "{sql}" + ); + } +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index facc859fe6..fa84ecb15d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9122,9 +9122,9 @@ fn parse_create_text_search() { ) ); - // CREATE options are key-value pairs in PostgreSQL syntax. assert_eq!( - pg().parse_sql_statements("CREATE TEXT SEARCH DICTIONARY d (template)") + all_dialects_where(|d| !d.supports_sql_option_without_value()) + .parse_sql_statements("CREATE TEXT SEARCH DICTIONARY d (template)") .unwrap_err(), ParserError::ParserError("Expected: =, found: )".to_string()) ); @@ -11351,3 +11351,52 @@ fn parse_drop_foreign_table_rejects_modifiers() { ); } } + +#[test] +fn parse_storage_parameters_without_value() { + let dialects = all_dialects_where(|d| d.supports_sql_option_without_value()); + let bare = SqlOption::Ident(Ident::new("security_barrier")); + let key_value = SqlOption::KeyValue { + key: Ident::new("security_invoker"), + value: Expr::Value(Value::Boolean(true).with_empty_span()), + }; + + match dialects + .verified_stmt("CREATE VIEW v WITH (security_invoker = true, security_barrier) AS SELECT 1") + { + Statement::CreateView(CreateView { options, .. }) => assert_eq!( + options, + CreateTableOptions::With(vec![key_value.clone(), bare.clone()]) + ), + _ => unreachable!(), + } + match dialects.verified_stmt("CREATE TABLE t (a INT) WITH (security_barrier)") { + Statement::CreateTable(CreateTable { table_options, .. }) => { + assert_eq!(table_options, CreateTableOptions::With(vec![bare.clone()])) + } + _ => unreachable!(), + } + match dialects.verified_stmt("ALTER TABLE t SET (security_barrier, security_invoker = true)") { + Statement::AlterTable(AlterTable { operations, .. }) => assert_eq!( + operations, + [AlterTableOperation::SetOptionsParens { + options: vec![bare, key_value], + }] + ), + _ => unreachable!(), + } + dialects.verified_stmt("CREATE MATERIALIZED VIEW m WITH (autovacuum_enabled) AS SELECT 1"); + + assert_eq!( + dialects + .parse_sql_statements("CREATE TABLE t (a INT) WITH (fillfactor 70)") + .unwrap_err(), + ParserError::ParserError("Expected: =, found: 70".to_string()) + ); + assert_eq!( + all_dialects_where(|d| !d.supports_sql_option_without_value()) + .parse_sql_statements("ALTER TABLE t SET (autovacuum_enabled)") + .unwrap_err(), + ParserError::ParserError("Expected: =, found: )".to_string()) + ); +}