From 6880a93ef44ff37727c4cfdae4c3587093e488fd Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 27 Oct 2026 22:29:30 +0100 Subject: [PATCH 1/3] 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 e434c58994..1bf6ed9ae8 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 da24d047a4..7a338a4859 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9141,7 +9141,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 b20fb610ee..ae79a3970f 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1348,6 +1348,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 15ae25cf96..235184c802 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -402,6 +402,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 e9861f97fe..131b9b5cba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11091,6 +11091,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()?; @@ -11698,6 +11703,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 8375e4a43e..72331a7cce 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22851,3 +22851,30 @@ fn parse_grant_on_tables_named_after_object_types() { verified_stmt(&format!("GRANT SELECT ON {name}.t TO r")); } } + +#[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 7249394de9..8f37e7b170 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()) ); @@ -11708,3 +11708,52 @@ fn parse_interval_followed_by_alias() { ParserError::ParserError("Expected: end of statement, found: WEEK".to_string()) ); } + +#[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()) + ); +} From fb401e9808ddcc945773a8bfa7a22d94bbbee69e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 20:12:51 +0100 Subject: [PATCH 2/3] Test storage parameter option error paths --- tests/sqlparser_postgres.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 8f37e7b170..aa9cc38412 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11756,4 +11756,16 @@ fn parse_storage_parameters_without_value() { .unwrap_err(), ParserError::ParserError("Expected: =, found: )".to_string()) ); + assert_eq!( + dialects + .parse_sql_statements("CREATE TABLE t (a INT) WITH (fillfactor =)") + .unwrap_err(), + ParserError::ParserError("Expected: an expression, found: )".to_string()) + ); + assert_eq!( + dialects + .parse_sql_statements("ALTER TABLE t SET TBLPROPERTIES (fillfactor 70)") + .unwrap_err(), + ParserError::ParserError("Expected: =, found: 70".to_string()) + ); } From e7014dd27a4434cf2fb9fe9037854dffd23fc933 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 21:52:13 +0100 Subject: [PATCH 3/3] Accept namespaced storage parameters in ALTER TABLE RESET --- fuzz/fuzz_seeds/postgres_2744_alter_table_reset.sql | 1 + src/ast/ddl.rs | 4 ++-- src/ast/spans.rs | 2 +- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 11 +++++++++-- 5 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_2744_alter_table_reset.sql diff --git a/fuzz/fuzz_seeds/postgres_2744_alter_table_reset.sql b/fuzz/fuzz_seeds/postgres_2744_alter_table_reset.sql new file mode 100644 index 0000000000..eee5b05601 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2744_alter_table_reset.sql @@ -0,0 +1 @@ +ALTER TABLE t RESET (fillfactor, toast.autovacuum_enabled) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1bf6ed9ae8..7439fd3b00 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -567,8 +567,8 @@ pub enum AlterTableOperation { /// ``` /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-altertable.html) ResetOptions { - /// Parameters to reset. - options: Vec, + /// Parameters to reset, optionally namespaced as in `toast.autovacuum_enabled`. + options: Vec, }, } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 3760d1db0e..ee6c6993ad 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1313,7 +1313,7 @@ impl Spanned for AlterTableOperation { union_spans(options.iter().map(|i| i.span())) } AlterTableOperation::ResetOptions { options } => { - union_spans(options.iter().map(|i| i.span)) + union_spans(options.iter().map(|i| i.span())) } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 131b9b5cba..55fbdd525d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11705,7 +11705,7 @@ impl<'a> Parser<'a> { AlterTableOperation::SetUnlogged } else if self.parse_keyword(Keyword::RESET) { self.expect_token(&Token::LParen)?; - let options = self.parse_comma_separated(Parser::parse_identifier)?; + let options = self.parse_comma_separated(|p| p.parse_object_name(false))?; self.expect_token(&Token::RParen)?; AlterTableOperation::ResetOptions { options } } else { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 72331a7cce..61fb856a74 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22855,12 +22855,15 @@ fn parse_grant_on_tables_named_after_object_types() { #[test] fn parse_alter_table_reset_options() { match alter_table_op(verified_stmt( - "ALTER TABLE tab RESET (fillfactor, autovacuum_enabled)", + "ALTER TABLE tab RESET (fillfactor, toast.autovacuum_enabled)", )) { AlterTableOperation::ResetOptions { options } => { assert_eq!( options, - [Ident::new("fillfactor"), Ident::new("autovacuum_enabled")] + [ + ObjectName::from(vec![Ident::new("fillfactor")]), + ObjectName::from(vec![Ident::new("toast"), Ident::new("autovacuum_enabled")]), + ] ); } _ => unreachable!(), @@ -22870,6 +22873,10 @@ fn parse_alter_table_reset_options() { ("ALTER TABLE tab RESET ()", "Expected: identifier, found: )"), ("ALTER TABLE tab RESET (a = 1)", "Expected: ), found: ="), ("ALTER TABLE tab RESET a", "Expected: (, found: a"), + ( + "ALTER TABLE tab RESET (toast.)", + "Expected: identifier, found: )", + ), ] { assert_eq!( all_dialects().parse_sql_statements(sql).unwrap_err(),