diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 0a93f5b7ec..96e0630e7f 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, @@ -2898,6 +2912,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 @@ -3089,7 +3105,7 @@ impl fmt::Display for CreateTable { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {or_replace}{external}{global}{multiset}{temporary}{transient}{volatile}{dynamic}{iceberg}{snapshot}TABLE {if_not_exists}{name}", + "CREATE {or_replace}{external}{multiset}{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 { "" }, @@ -3108,6 +3124,7 @@ impl fmt::Display for CreateTable { .map(|m| if m { "MULTISET " } else { "SET " }) .unwrap_or(""), temporary = if self.temporary { "TEMPORARY " } else { "" }, + unlogged = if self.unlogged { "UNLOGGED " } else { "" }, transient = if self.transient { "TRANSIENT " } else { "" }, volatile = if self.volatile { "VOLATILE " } else { "" }, iceberg = if self.iceberg { "ICEBERG " } else { "" }, diff --git a/src/ast/helpers/stmt_create_table.rs b/src/ast/helpers/stmt_create_table.rs index 9ec9ab28ce..84a93dd20c 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. @@ -200,6 +202,7 @@ impl CreateTableBuilder { Self { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, if_not_exists: false, @@ -273,6 +276,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; @@ -595,6 +603,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, @@ -680,6 +689,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 f6ba895478..51d04668f7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -547,6 +547,7 @@ impl Spanned for CreateTable { let CreateTable { or_replace: _, // bool temporary: _, // bool + unlogged: _, // bool external: _, // bool global: _, // bool dynamic: _, // bool @@ -1216,6 +1217,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 a0a65be689..21a7a57731 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -597,6 +597,7 @@ define_keywords!( LOCK, LOCKED, LOG, + LOGGED, LOGIN, LOGS, LONG, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 07497b04f6..387939d410 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5144,6 +5144,12 @@ impl<'a> Parser<'a> { 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.peek_keywords(&[Keyword::UNLOGGED, Keyword::TABLE]) { + self.expect_keywords(&[Keyword::UNLOGGED, Keyword::TABLE])?; + let mut create_table = self + .parse_create_table(or_replace, temporary, global, transient, volatile, multiset)?; + create_table.unlogged = true; + Ok(create_table.into()) } else if self.parse_keyword(Keyword::TABLE) { self.parse_create_table(or_replace, temporary, global, transient, volatile, multiset) .map(Into::into) @@ -8664,6 +8670,7 @@ impl<'a> Parser<'a> { Ok(CreateTableBuilder::new(table_name) .temporary(temporary) + .unlogged(false) .columns(columns) .constraints(constraints) .or_replace(or_replace) @@ -10745,6 +10752,10 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) { let name = self.parse_identifier()?; AlterTableOperation::ValidateConstraint { name } + } else if self.parse_keywords(&[Keyword::SET, Keyword::LOGGED]) { + AlterTableOperation::SetLogged + } else if self.parse_keywords(&[Keyword::SET, Keyword::UNLOGGED]) { + AlterTableOperation::SetUnlogged } 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 326fbf678e..558bba6cd7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18951,3 +18951,27 @@ fn parse_non_pg_dialects_keep_xml_names_as_regular_identifiers() { let dialects = all_dialects_except(|d| d.supports_xml_expressions()); dialects.verified_only_select("SELECT xml FROM t"); } + +#[test] +fn parse_unlogged_table_logging_controls_in_all_dialects() { + match all_dialects().verified_stmt("CREATE UNLOGGED TABLE t (a INT)") { + Statement::CreateTable(CreateTable { unlogged, .. }) => { + assert!(unlogged); + } + _ => unreachable!("Expected CREATE TABLE"), + } + + match all_dialects().verified_stmt("ALTER TABLE t SET LOGGED") { + Statement::AlterTable(AlterTable { operations, .. }) => { + assert_eq!(vec![AlterTableOperation::SetLogged], operations); + } + _ => unreachable!("Expected ALTER TABLE"), + } + + match all_dialects().verified_stmt("ALTER TABLE t SET UNLOGGED") { + Statement::AlterTable(AlterTable { operations, .. }) => { + assert_eq!(vec![AlterTableOperation::SetUnlogged], operations); + } + _ => unreachable!("Expected ALTER TABLE"), + } +} diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 86c7658131..f8df431639 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 6e866746d7..3faf56f0d9 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1924,6 +1924,7 @@ fn parse_create_table_with_valid_options() { Statement::CreateTable(CreateTable { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, dynamic: false, @@ -2121,6 +2122,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 11e76dbc7c..1ffa4a341b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1192,6 +1192,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 ()"; @@ -5692,6 +5719,57 @@ 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!(), + } + + let res = pg().parse_sql_statements("CREATE UNLOGGED VIEW v AS SELECT 1"); + assert_eq!( + ParserError::ParserError("Expected: an object type after CREATE, found: UNLOGGED".into()), + res.unwrap_err() + ); +} + #[test] fn parse_join_constraint_unnest_alias() { assert_eq!( @@ -6638,6 +6716,7 @@ fn parse_trigger_related_functions() { CreateTable { or_replace: false, temporary: false, + unlogged: false, external: false, global: None, dynamic: false,