From 75694a2b74a357489a2d99423f89df14b0e10e2d Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 27 Oct 2026 22:21:03 +0100 Subject: [PATCH 1/3] PostgreSQL: Parse namespaced storage parameters --- src/ast/mod.rs | 5 ++-- src/ast/spans.rs | 2 +- src/parser/mod.rs | 11 +++++-- tests/sqlparser_bigquery.rs | 26 ++++++++-------- tests/sqlparser_common.rs | 46 +++++++++++++++------------- tests/sqlparser_mssql.rs | 4 +-- tests/sqlparser_mysql.rs | 60 ++++++++++++++++++------------------- tests/sqlparser_postgres.rs | 53 ++++++++++++++++++++++++++------ 8 files changed, 127 insertions(+), 80 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2fc79ed26b..562411c67e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9156,9 +9156,10 @@ pub enum SqlOption { /// Any option that consists of a key value pair where the value is an expression. e.g. /// /// WITH(DISTRIBUTION = ROUND_ROBIN) + /// WITH (toast.autovacuum_enabled = false) KeyValue { - /// The option key identifier. - key: Ident, + /// The option key. + key: ObjectName, /// The expression value for the option. value: Expr, }, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b9104447f0..674dfff24a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1103,7 +1103,7 @@ impl Spanned for SqlOption { match self { SqlOption::Clustered(table_options_clustered) => table_options_clustered.span(), SqlOption::Ident(ident) => ident.span, - SqlOption::KeyValue { key, value } => key.span.union(&value.span()), + SqlOption::KeyValue { key, value } => key.span().union(&value.span()), SqlOption::Partition { column_name, range_direction: _, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2a5ca2d947..8bf9c5368c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9926,7 +9926,10 @@ impl<'a> Parser<'a> { None => Expr::Identifier(self.parse_identifier()?), }; - Ok(Some(SqlOption::KeyValue { key, value })) + Ok(Some(SqlOption::KeyValue { + key: key.into(), + value, + })) } /// Parse plain options. @@ -11116,10 +11119,14 @@ impl<'a> Parser<'a> { { return Ok(SqlOption::Ident(name)); } + let mut key = vec![name]; + while self.consume_token(&Token::Period) { + key.push(self.parse_identifier()?); + } self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; - Ok(SqlOption::KeyValue { key: name, value }) + Ok(SqlOption::KeyValue { key: key.into(), value }) } } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 9b36709fdd..5035975898 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -359,7 +359,7 @@ fn parse_create_view_with_options() { data_type: None, options: Some(ColumnOptions::CommaSeparated(vec![ColumnOption::Options( vec![SqlOption::KeyValue { - key: Ident::new("description"), + key: Ident::new("description").into(), value: Expr::Value( Value::DoubleQuotedString("field age".to_string()).with_span( Span::new(Location::new(1, 42), Location::new(1, 52)) @@ -384,7 +384,7 @@ fn parse_create_view_with_options() { }; assert_eq!( &SqlOption::KeyValue { - key: Ident::new("description"), + key: Ident::new("description").into(), value: Expr::Value( Value::DoubleQuotedString("a view that expires in 2 days".to_string()) .with_empty_span() @@ -511,7 +511,7 @@ fn parse_create_table_with_options() { ColumnOptionDef { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { - key: Ident::new("description"), + key: Ident::new("description").into(), value: Expr::Value( Value::DoubleQuotedString("field x".to_string()).with_span( Span::new(Location::new(1, 42), Location::new(1, 52)) @@ -527,7 +527,7 @@ fn parse_create_table_with_options() { options: vec![ColumnOptionDef { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { - key: Ident::new("description"), + key: Ident::new("description").into(), value: Expr::Value( Value::DoubleQuotedString("field y".to_string()).with_span( Span::new(Location::new(1, 42), Location::new(1, 52)) @@ -548,7 +548,7 @@ fn parse_create_table_with_options() { ])), CreateTableOptions::Options(vec![ SqlOption::KeyValue { - key: Ident::new("partition_expiration_days"), + key: Ident::new("partition_expiration_days").into(), value: Expr::Value( number("1").with_span(Span::new( Location::new(1, 42), @@ -557,7 +557,7 @@ fn parse_create_table_with_options() { ), }, SqlOption::KeyValue { - key: Ident::new("description"), + key: Ident::new("description").into(), value: Expr::Value( Value::DoubleQuotedString("table option description".to_string()) .with_span(Span::new( @@ -2342,7 +2342,7 @@ fn test_bigquery_create_function() { number("42").with_empty_span() ))), options: Some(vec![SqlOption::KeyValue { - key: Ident::new("x"), + key: Ident::new("x").into(), value: Expr::Value(Value::SingleQuotedString("y".into()).with_empty_span()), }]), behavior: None, @@ -2710,20 +2710,20 @@ fn test_export_data() { Statement::ExportData(ExportData { options: vec![ SqlOption::KeyValue { - key: Ident::new("uri"), + key: Ident::new("uri").into(), value: Expr::Value( Value::SingleQuotedString("gs://bucket/folder/*".to_owned()) .with_empty_span() ), }, SqlOption::KeyValue { - key: Ident::new("format"), + key: Ident::new("format").into(), value: Expr::Value( Value::SingleQuotedString("PARQUET".to_owned()).with_empty_span() ), }, SqlOption::KeyValue { - key: Ident::new("overwrite"), + key: Ident::new("overwrite").into(), value: Expr::Value(Value::Boolean(true).with_empty_span()), }, ], @@ -2812,20 +2812,20 @@ fn test_export_data() { Statement::ExportData(ExportData { options: vec![ SqlOption::KeyValue { - key: Ident::new("uri"), + key: Ident::new("uri").into(), value: Expr::Value( Value::SingleQuotedString("gs://bucket/folder/*".to_owned()) .with_empty_span() ), }, SqlOption::KeyValue { - key: Ident::new("format"), + key: Ident::new("format").into(), value: Expr::Value( Value::SingleQuotedString("PARQUET".to_owned()).with_empty_span() ), }, SqlOption::KeyValue { - key: Ident::new("overwrite"), + key: Ident::new("overwrite").into(), value: Expr::Value(Value::Boolean(true).with_empty_span()), }, ], diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index db7786d681..cdf336879e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5257,13 +5257,13 @@ fn parse_create_table_with_options() { assert_eq!( vec![ SqlOption::KeyValue { - key: "foo".into(), + key: Ident::new("foo").into(), value: Expr::Value( (Value::SingleQuotedString("bar".into())).with_empty_span() ), }, SqlOption::KeyValue { - key: "a".into(), + key: Ident::new("a").into(), value: Expr::value(number("123")), }, ], @@ -5514,7 +5514,8 @@ fn parse_alter_table() { value: "classification".to_string(), quote_style: Some('\''), span: Span::empty(), - }, + } + .into(), value: Expr::Value( (Value::SingleQuotedString("parquet".to_string())).with_empty_span() ), @@ -5535,7 +5536,8 @@ fn parse_alter_table() { value: "autovacuum_vacuum_scale_factor".to_string(), quote_style: None, span: Span::empty(), - }, + } + .into(), value: Expr::Value(test_utils::number("0.01").with_empty_span()), }, SqlOption::KeyValue { @@ -5543,7 +5545,8 @@ fn parse_alter_table() { value: "autovacuum_vacuum_threshold".to_string(), quote_style: None, span: Span::empty(), - }, + } + .into(), value: Expr::Value(test_utils::number("500").with_empty_span()), } ], @@ -5688,13 +5691,13 @@ fn parse_alter_view_with_options() { assert_eq!( vec![ SqlOption::KeyValue { - key: "foo".into(), + key: Ident::new("foo").into(), value: Expr::Value( (Value::SingleQuotedString("bar".into())).with_empty_span() ), }, SqlOption::KeyValue { - key: "a".into(), + key: Ident::new("a").into(), value: Expr::value(number("123")), }, ], @@ -9241,13 +9244,13 @@ fn parse_create_view_with_options() { assert_eq!( CreateTableOptions::With(vec![ SqlOption::KeyValue { - key: "foo".into(), + key: Ident::new("foo").into(), value: Expr::Value( (Value::SingleQuotedString("bar".into())).with_empty_span() ), }, SqlOption::KeyValue { - key: "a".into(), + key: Ident::new("a").into(), value: Expr::value(number("123")), }, ]), @@ -12043,11 +12046,11 @@ fn parse_cache_table() { has_as: false, options: vec![ SqlOption::KeyValue { - key: Ident::with_quote('\'', "K1"), + key: Ident::with_quote('\'', "K1").into(), value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { - key: Ident::with_quote('\'', "K2"), + key: Ident::with_quote('\'', "K2").into(), value: Expr::value(number("0.88")), }, ], @@ -12068,11 +12071,11 @@ fn parse_cache_table() { has_as: false, options: vec![ SqlOption::KeyValue { - key: Ident::with_quote('\'', "K1"), + key: Ident::with_quote('\'', "K1").into(), value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { - key: Ident::with_quote('\'', "K2"), + key: Ident::with_quote('\'', "K2").into(), value: Expr::value(number("0.88")), }, ], @@ -12093,11 +12096,11 @@ fn parse_cache_table() { has_as: true, options: vec![ SqlOption::KeyValue { - key: Ident::with_quote('\'', "K1"), + key: Ident::with_quote('\'', "K1").into(), value: Expr::Value((Value::SingleQuotedString("V1".into())).with_empty_span()), }, SqlOption::KeyValue { - key: Ident::with_quote('\'', "K2"), + key: Ident::with_quote('\'', "K2").into(), value: Expr::value(number("0.88")), }, ], @@ -13656,7 +13659,8 @@ fn parse_unload() { value: "format".to_string(), quote_style: None, span: Span::empty(), - }, + } + .into(), value: Expr::Value( (Value::SingleQuotedString("AVRO".to_string())).with_empty_span() ) @@ -15907,13 +15911,13 @@ fn test_create_connector() { with_dcproperties, Some(vec![ SqlOption::KeyValue { - key: Ident::with_quote('\'', "user"), + key: Ident::with_quote('\'', "user").into(), value: Expr::Value( (Value::SingleQuotedString("root".to_string())).with_empty_span() ) }, SqlOption::KeyValue { - key: Ident::with_quote('\'', "password"), + key: Ident::with_quote('\'', "password").into(), value: Expr::Value( (Value::SingleQuotedString("password".to_string())).with_empty_span() ) @@ -15978,13 +15982,13 @@ fn test_alter_connector() { properties, Some(vec![ SqlOption::KeyValue { - key: Ident::with_quote('\'', "user"), + key: Ident::with_quote('\'', "user").into(), value: Expr::Value( (Value::SingleQuotedString("root".to_string())).with_empty_span() ) }, SqlOption::KeyValue { - key: Ident::with_quote('\'', "password"), + key: Ident::with_quote('\'', "password").into(), value: Expr::Value( (Value::SingleQuotedString("password".to_string())).with_empty_span() ) @@ -22142,7 +22146,7 @@ fn parse_cache_table_options_without_flag() { table_name: ObjectName::from(vec![Ident::new("t")]), has_as, options: vec![SqlOption::KeyValue { - key: Ident::with_quote('\'', "k"), + key: Ident::with_quote('\'', "k").into(), value: Expr::Value(Value::SingleQuotedString("v".into()).with_empty_span()), }], query, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 71e3b6cf7b..11a98a1895 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1745,7 +1745,7 @@ fn parse_create_table_with_valid_options() { value: "DISTRIBUTION".to_string(), quote_style: None, span: Span::empty(), - }, + }.into(), value: Expr::Identifier(Ident { value: "ROUND_ROBIN".to_string(), quote_style: None, @@ -1824,7 +1824,7 @@ fn parse_create_table_with_valid_options() { value: "DISTRIBUTION".to_string(), quote_style: None, span: Span::empty(), - }, + }.into(), value: Expr::Function( Function { name: ObjectName::from( diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 0e21857976..a16a81c8d5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1028,7 +1028,7 @@ fn parse_create_table_auto_increment_offset() { }; assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("AUTO_INCREMENT"), + key: Ident::new("AUTO_INCREMENT").into(), value: Expr::Value(test_utils::number("123").with_empty_span()) })); } @@ -1065,7 +1065,7 @@ fn parse_create_table_multiple_options_order_independent() { ))); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("KEY_BLOCK_SIZE"), + key: Ident::new("KEY_BLOCK_SIZE").into(), value: Expr::Value(test_utils::number("8").with_empty_span()) })); @@ -1073,7 +1073,7 @@ fn parse_create_table_multiple_options_order_independent() { .contains(&SqlOption::Comment(CommentDef::WithEq("abc".to_owned())))); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("ROW_FORMAT"), + key: Ident::new("ROW_FORMAT").into(), value: Expr::Identifier(Ident::new("DYNAMIC".to_owned())) })); } @@ -1109,91 +1109,91 @@ fn parse_create_table_with_all_table_options() { ))); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("COLLATE"), + key: Ident::new("COLLATE").into(), value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("DEFAULT CHARSET"), + key: Ident::new("DEFAULT CHARSET").into(), value: Expr::Identifier(Ident::new("utf8mb4".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("AUTO_INCREMENT"), + key: Ident::new("AUTO_INCREMENT").into(), value: Expr::value(test_utils::number("123")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("KEY_BLOCK_SIZE"), + key: Ident::new("KEY_BLOCK_SIZE").into(), value: Expr::value(test_utils::number("8")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("ROW_FORMAT"), + key: Ident::new("ROW_FORMAT").into(), value: Expr::Identifier(Ident::new("DYNAMIC".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("PACK_KEYS"), + key: Ident::new("PACK_KEYS").into(), value: Expr::value(test_utils::number("1")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("STATS_AUTO_RECALC"), + key: Ident::new("STATS_AUTO_RECALC").into(), value: Expr::value(test_utils::number("1")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("STATS_PERSISTENT"), + key: Ident::new("STATS_PERSISTENT").into(), value: Expr::value(test_utils::number("0")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("STATS_SAMPLE_PAGES"), + key: Ident::new("STATS_SAMPLE_PAGES").into(), value: Expr::value(test_utils::number("128")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("STATS_SAMPLE_PAGES"), + key: Ident::new("STATS_SAMPLE_PAGES").into(), value: Expr::value(test_utils::number("128")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("INSERT_METHOD"), + key: Ident::new("INSERT_METHOD").into(), value: Expr::Identifier(Ident::new("FIRST".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("COMPRESSION"), + key: Ident::new("COMPRESSION").into(), value: Expr::value(Value::SingleQuotedString("ZLIB".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("ENCRYPTION"), + key: Ident::new("ENCRYPTION").into(), value: Expr::value(Value::SingleQuotedString("Y".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("MAX_ROWS"), + key: Ident::new("MAX_ROWS").into(), value: Expr::value(test_utils::number("10000")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("MIN_ROWS"), + key: Ident::new("MIN_ROWS").into(), value: Expr::value(test_utils::number("10")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("AUTOEXTEND_SIZE"), + key: Ident::new("AUTOEXTEND_SIZE").into(), value: Expr::value(test_utils::number("64")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("AVG_ROW_LENGTH"), + key: Ident::new("AVG_ROW_LENGTH").into(), value: Expr::value(test_utils::number("128")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("CHECKSUM"), + key: Ident::new("CHECKSUM").into(), value: Expr::value(test_utils::number("1")) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("CONNECTION"), + key: Ident::new("CONNECTION").into(), value: Expr::value(Value::SingleQuotedString("mysql://localhost".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("ENGINE_ATTRIBUTE"), + key: Ident::new("ENGINE_ATTRIBUTE").into(), value: Expr::value(Value::SingleQuotedString("primary".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("PASSWORD"), + key: Ident::new("PASSWORD").into(), value: Expr::value(Value::SingleQuotedString("secure_password".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("SECONDARY_ENGINE_ATTRIBUTE"), + key: Ident::new("SECONDARY_ENGINE_ATTRIBUTE").into(), value: Expr::value(Value::SingleQuotedString("secondary_attr".to_owned())) })); assert!(plain_options.contains(&SqlOption::Ident(Ident::new( @@ -1219,11 +1219,11 @@ fn parse_create_table_with_all_table_options() { ))); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("DATA DIRECTORY"), + key: Ident::new("DATA DIRECTORY").into(), value: Expr::value(Value::SingleQuotedString("/var/lib/mysql/data".to_owned())) })); assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("INDEX DIRECTORY"), + key: Ident::new("INDEX DIRECTORY").into(), value: Expr::value(Value::SingleQuotedString("/var/lib/mysql/index".to_owned())) })); } @@ -1289,7 +1289,7 @@ fn parse_create_table_engine_default_charset() { }; assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("DEFAULT CHARSET"), + key: Ident::new("DEFAULT CHARSET").into(), value: Expr::Identifier(Ident::new("utf8mb3".to_owned())) })); @@ -1331,7 +1331,7 @@ fn parse_create_table_collate() { }; assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("COLLATE"), + key: Ident::new("COLLATE").into(), value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) })); } @@ -1357,7 +1357,7 @@ fn parse_create_table_both_options_and_as_query() { }; assert!(plain_options.contains(&SqlOption::KeyValue { - key: Ident::new("COLLATE"), + key: Ident::new("COLLATE").into(), value: Expr::Identifier(Ident::new("utf8mb4_0900_ai_ci".to_owned())) })); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 03355e497c..505fd38cd6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -497,15 +497,15 @@ fn parse_create_table_with_defaults() { with_options, vec![ SqlOption::KeyValue { - key: "fillfactor".into(), + key: Ident::new("fillfactor").into(), value: Expr::value(number("20")) }, SqlOption::KeyValue { - key: "user_catalog_table".into(), + key: Ident::new("user_catalog_table").into(), value: Expr::Value((Value::Boolean(true)).with_empty_span()) }, SqlOption::KeyValue { - key: "autovacuum_vacuum_threshold".into(), + key: Ident::new("autovacuum_vacuum_threshold").into(), value: Expr::value(number("100")) }, ] @@ -1080,11 +1080,11 @@ fn parse_create_collation() { name: ObjectName::from(vec![Ident::new("test3")]), definition: CreateCollationDefinition::Options(vec![ SqlOption::KeyValue { - key: Ident::new("provider"), + key: Ident::new("provider").into(), value: Expr::Identifier(Ident::new("icu")), }, SqlOption::KeyValue { - key: Ident::new("lc_collate"), + key: Ident::new("lc_collate").into(), value: Expr::Value( Value::SingleQuotedString("en_US.utf8".to_string()).with_empty_span(), ), @@ -1112,11 +1112,11 @@ fn parse_create_collation() { name: ObjectName::from(vec![Ident::new("testx")]), definition: CreateCollationDefinition::Options(vec![ SqlOption::KeyValue { - key: Ident::new("provider"), + key: Ident::new("provider").into(), value: Expr::Identifier(Ident::new("icu")), }, SqlOption::KeyValue { - key: Ident::new("locale"), + key: Ident::new("locale").into(), value: Expr::Value( Value::SingleQuotedString("nonsense-nowhere".to_string()).with_empty_span(), ), @@ -6868,13 +6868,13 @@ fn parse_create_table_with_options() { assert_eq!( vec![ SqlOption::KeyValue { - key: "foo".into(), + key: Ident::new("foo").into(), value: Expr::Value( (Value::SingleQuotedString("bar".into())).with_empty_span() ), }, SqlOption::KeyValue { - key: "a".into(), + key: Ident::new("a").into(), value: Expr::value(number("123")), }, ], @@ -11855,6 +11855,41 @@ fn parse_storage_parameters_without_value() { .parse_sql_statements("ALTER TABLE t SET TBLPROPERTIES (fillfactor 70)") .unwrap_err(), ParserError::ParserError("Expected: =, found: 70".to_string()) +fn parse_namespaced_storage_parameters() { + let expected = vec![SqlOption::KeyValue { + key: ObjectName::from(vec![Ident::new("toast"), Ident::new("autovacuum_enabled")]), + value: Expr::Value(Value::Boolean(false).with_empty_span()), + }]; + + match pg().verified_stmt("CREATE TABLE t (a INT) WITH (toast.autovacuum_enabled = false)") { + Statement::CreateTable(CreateTable { + table_options: CreateTableOptions::With(options), + .. + }) => assert_eq!(options, expected), + _ => unreachable!(), + } + + match alter_table_op( + pg().verified_stmt("ALTER TABLE tab SET (toast.autovacuum_enabled = false)"), + ) { + AlterTableOperation::SetOptionsParens { options } => assert_eq!(options, expected), + _ => unreachable!(), + } + + match pg().verified_stmt( + "CREATE MATERIALIZED VIEW m WITH (toast.autovacuum_enabled = false) AS SELECT 1", + ) { + Statement::CreateView(create_view) => { + assert_eq!(create_view.options, CreateTableOptions::With(expected)) + } + _ => unreachable!(), + } + + assert_eq!( + pg().parse_sql_statements("CREATE TABLE t (a INT) WITH (toast. = false)") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: =" ); } From dc2b5ef9fed35e0c4795fab4c360e11e1e3c1d38 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 27 Oct 2026 22:34:32 +0100 Subject: [PATCH 2/3] Test the span of a namespaced storage parameter --- src/ast/spans.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 674dfff24a..028746e729 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -3446,4 +3446,22 @@ SETTINGS update_parallel_mode = 'auto'"#; assert_eq!(test.get_source(expr.span()), expected, "{sql}"); } } + + #[test] + fn test_namespaced_storage_parameter_span() { + let dialect = &crate::dialect::PostgreSqlDialect {}; + let sql = "CREATE TABLE t (a INT) WITH (toast.autovacuum_enabled = false)"; + let mut test = SpanTest::new(dialect, sql); + let options = match test.0.parse_statement().unwrap() { + Statement::CreateTable(crate::ast::CreateTable { + table_options: crate::ast::CreateTableOptions::With(options), + .. + }) => options, + stmt => panic!("expected CREATE TABLE WITH, got {stmt:?}"), + }; + assert_eq!( + test.get_source(options[0].span()), + "toast.autovacuum_enabled = false" + ); + } } From 066377c5adb780fe09d8eb89918667ab524a436a Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 31 Oct 2026 08:43:08 +0100 Subject: [PATCH 3/3] Accept namespaced storage parameters without a value --- src/ast/mod.rs | 6 ++--- src/ast/spans.rs | 2 +- src/parser/mod.rs | 14 +++++------ tests/sqlparser_mssql.rs | 2 +- tests/sqlparser_mysql.rs | 6 ++--- tests/sqlparser_postgres.rs | 47 +++++++++++++++++++++++++++++++++++-- 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 562411c67e..e66cdaa08a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9148,11 +9148,11 @@ pub enum SqlOption { /// /// Clustered(TableOptionsClustered), - /// Single identifier options, e.g. `HEAP` for MSSQL or a PostgreSQL storage parameter - /// named without a value. + /// Option name without a value, e.g. `HEAP` for MSSQL or a PostgreSQL storage + /// parameter. The name may be namespaced, e.g. `toast.autovacuum_enabled`. /// /// - Ident(Ident), + Ident(ObjectName), /// Any option that consists of a key value pair where the value is an expression. e.g. /// /// WITH(DISTRIBUTION = ROUND_ROBIN) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 028746e729..ca48359557 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1102,7 +1102,7 @@ impl Spanned for SqlOption { fn span(&self) -> Span { match self { SqlOption::Clustered(table_options_clustered) => table_options_clustered.span(), - SqlOption::Ident(ident) => ident.span, + SqlOption::Ident(ident) => ident.span(), SqlOption::KeyValue { key, value } => key.span().union(&value.span()), SqlOption::Partition { column_name, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8bf9c5368c..f6ee9bdd02 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9740,7 +9740,9 @@ impl<'a> Parser<'a> { // Single parameter option // if self.parse_keywords(&[Keyword::START, Keyword::TRANSACTION]) { - return Ok(Some(SqlOption::Ident(Ident::new("START TRANSACTION")))); + return Ok(Some(SqlOption::Ident( + Ident::new("START TRANSACTION").into(), + ))); } // Custom option @@ -11104,7 +11106,7 @@ impl<'a> Parser<'a> { match &self.peek_token_ref().token { Token::Word(w) if w.keyword == Keyword::HEAP && is_mssql => { - Ok(SqlOption::Ident(self.parse_identifier()?)) + Ok(SqlOption::Ident(ObjectName::from(self.parse_identifier()?))) } Token::Word(w) if w.keyword == Keyword::PARTITION && is_mssql => { self.parse_option_partition() @@ -11113,20 +11115,16 @@ impl<'a> Parser<'a> { self.parse_option_clustered() } _ => { - let name = self.parse_identifier()?; + let name = ObjectName::from(self.parse_period_separated(|p| p.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)); } - let mut key = vec![name]; - while self.consume_token(&Token::Period) { - key.push(self.parse_identifier()?); - } self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; - Ok(SqlOption::KeyValue { key: key.into(), value }) + Ok(SqlOption::KeyValue { key: name, value }) } } } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 11a98a1895..4b9b4cfa96 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1875,7 +1875,7 @@ fn parse_create_table_with_valid_options() { }, ), }, - SqlOption::Ident("HEAP".into()), + SqlOption::Ident(ObjectName::from(vec![Ident::new("HEAP")])), ], ), ]; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a16a81c8d5..d5ba2a9056 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1196,9 +1196,9 @@ fn parse_create_table_with_all_table_options() { key: Ident::new("SECONDARY_ENGINE_ATTRIBUTE").into(), value: Expr::value(Value::SingleQuotedString("secondary_attr".to_owned())) })); - assert!(plain_options.contains(&SqlOption::Ident(Ident::new( - "START TRANSACTION".to_owned() - )))); + assert!(plain_options.contains(&SqlOption::Ident( + Ident::new("START TRANSACTION".to_owned()).into() + ))); assert!( plain_options.contains(&SqlOption::TableSpace(TablespaceOption { name: "my_tablespace".to_string(), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 505fd38cd6..824ab78351 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11800,9 +11800,9 @@ fn parse_interval_followed_by_alias() { #[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 bare = SqlOption::Ident(Ident::new("security_barrier").into()); let key_value = SqlOption::KeyValue { - key: Ident::new("security_invoker"), + key: Ident::new("security_invoker").into(), value: Expr::Value(Value::Boolean(true).with_empty_span()), }; @@ -11855,6 +11855,10 @@ fn parse_storage_parameters_without_value() { .parse_sql_statements("ALTER TABLE t SET TBLPROPERTIES (fillfactor 70)") .unwrap_err(), ParserError::ParserError("Expected: =, found: 70".to_string()) + ); +} + +#[test] fn parse_namespaced_storage_parameters() { let expected = vec![SqlOption::KeyValue { key: ObjectName::from(vec![Ident::new("toast"), Ident::new("autovacuum_enabled")]), @@ -12165,3 +12169,42 @@ fn display_create_user_mapping_propagates_formatter_errors() { assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); } } + +#[test] +fn parse_namespaced_storage_parameters_without_value() { + let dialects = all_dialects_where(|d| d.supports_sql_option_without_value()); + let bare = SqlOption::Ident(ObjectName::from(vec![ + Ident::new("toast"), + Ident::new("autovacuum_enabled"), + ])); + + match dialects.verified_stmt("CREATE TABLE t (a INT) WITH (toast.autovacuum_enabled)") { + Statement::CreateTable(CreateTable { table_options, .. }) => { + assert_eq!(table_options, CreateTableOptions::With(vec![bare.clone()])) + } + _ => unreachable!(), + } + + match dialects.verified_stmt("ALTER TABLE t SET (toast.autovacuum_enabled, fillfactor = 70)") { + Statement::AlterTable(AlterTable { operations, .. }) => assert_eq!( + operations, + [AlterTableOperation::SetOptionsParens { + options: vec![ + bare, + SqlOption::KeyValue { + key: ObjectName::from(vec![Ident::new("fillfactor")]), + value: Expr::value(number("70")), + }, + ], + }] + ), + _ => unreachable!(), + } + + assert_eq!( + dialects + .parse_sql_statements("CREATE TABLE t (a INT) WITH (toast.)") + .unwrap_err(), + ParserError::ParserError("Expected: identifier, found: )".to_string()) + ); +}