From 6c0e437b90e931a3239a2a3ce420e205bf391969 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:05:56 +0100 Subject: [PATCH 1/3] 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 | 16 +++++++++++++++- 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 879740f03..0eb461936 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 @@ -971,6 +979,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, @@ -2899,6 +2913,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 @@ -3073,7 +3089,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}{snapshot}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{global}{temporary}{unlogged}{transient}{volatile}{dynamic}{iceberg}{snapshot}TABLE {if_not_exists}{name}", or_replace = if self.or_replace { "OR REPLACE " } else { "" }, external = if self.external { "EXTERNAL " } else { "" }, snapshot = if self.snapshot { "SNAPSHOT " } else { "" }, @@ -3088,6 +3104,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 ab2feb693..a9daebdf7 100644 --- a/src/ast/helpers/stmt_create_table.rs +++ b/src/ast/helpers/stmt_create_table.rs @@ -69,6 +69,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. @@ -191,6 +193,7 @@ impl CreateTableBuilder { Self { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, if_not_exists: false, @@ -260,6 +263,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; @@ -561,6 +569,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, @@ -642,6 +651,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 d80a3f4d5..ead198c99 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -533,6 +533,7 @@ impl Spanned for CreateTable { let CreateTable { or_replace: _, // bool temporary: _, // bool + unlogged: _, // bool external: _, // bool global: _, // bool dynamic: _, // bool @@ -1196,6 +1197,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 f0f37b1c0..c74d4a044 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -593,6 +593,7 @@ define_keywords!( LOCK, LOCKED, LOG, + LOGGED, LOGIN, LOGS, LONG, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6282ed3d7..4cbadd707 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5108,14 +5108,18 @@ 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.peek_keywords(&[Keyword::SNAPSHOT, Keyword::TABLE]) { self.parse_create_snapshot_table().map(Into::into) } else 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]) @@ -8340,6 +8344,7 @@ impl<'a> Parser<'a> { &mut self, or_replace: bool, temporary: bool, + unlogged: bool, global: Option, transient: bool, ) -> Result { @@ -8489,6 +8494,7 @@ impl<'a> Parser<'a> { Ok(CreateTableBuilder::new(table_name) .temporary(temporary) + .unlogged(unlogged) .columns(columns) .constraints(constraints) .or_replace(or_replace) @@ -10525,6 +10531,14 @@ 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 { let mut options = self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?; From 2a6609b81bf14116510d9987bb087084cc92d643 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 08:06:02 +0100 Subject: [PATCH 2/3] 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 df6268580..56c2348e4 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -703,6 +703,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 e8ed79492..2e4a4da76 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, @@ -2116,6 +2117,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 af0f2be33..9bb99aa3b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1027,6 +1027,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 ()"; @@ -5508,6 +5535,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!( @@ -6426,6 +6498,7 @@ fn parse_trigger_related_functions() { CreateTable { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, dynamic: false, From 65a9d7bfaa8caef5a70b67b501028c89b3d34889 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 1 Apr 2026 21:52:54 +0200 Subject: [PATCH 3/3] Extended test coverage --- tests/sqlparser_postgres.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9bb99aa3b..3c7e5b416 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5578,6 +5578,13 @@ fn parse_create_unlogged_table() { } _ => unreachable!(), } + + // Negative test: UNLOGGED without TABLE should error + let res = pg().parse_sql_statements("CREATE UNLOGGED VIEW v AS SELECT 1"); + assert!( + res.is_err(), + "CREATE UNLOGGED should only be followed by TABLE" + ); } #[test]