Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,14 @@ pub enum AlterTableOperation {
/// Table properties specified as SQL options.
table_properties: Vec<SqlOption>,
},
/// `SET LOGGED`
///
/// Note: this is PostgreSQL-specific.
SetLogged,
/// `SET UNLOGGED`
///
/// Note: this is PostgreSQL-specific.
SetUnlogged,
/// `OWNER TO { <new_owner> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }`
///
/// Note: this is PostgreSQL-specific <https://www.postgresql.org/docs/current/sql-altertable.html>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2903,6 +2917,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
Expand Down Expand Up @@ -3094,7 +3110,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}{global}{multiset}{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 { "" },
Expand All @@ -3113,6 +3129,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 { "" },
Expand Down
10 changes: 10 additions & 0 deletions src/ast/helpers/stmt_create_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -200,6 +202,7 @@ impl CreateTableBuilder {
Self {
or_replace: false,
temporary: false,
unlogged: false,
external: false,
global: None,
if_not_exists: false,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -680,6 +689,7 @@ impl From<CreateTable> 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,
Expand Down
3 changes: 3 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ impl Spanned for CreateTable {
let CreateTable {
or_replace: _, // bool
temporary: _, // bool
unlogged: _, // bool
external: _, // bool
global: _, // bool
dynamic: _, // bool
Expand Down Expand Up @@ -1228,6 +1229,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(),
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ define_keywords!(
LOCK,
LOCKED,
LOG,
LOGGED,
LOGIN,
LOGS,
LONG,
Expand Down
11 changes: 11 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5238,6 +5238,12 @@ impl<'a> Parser<'a> {
self.parse_create_snapshot_table().map(Into::into)
} else if self.peek_keywords(&[Keyword::TEXT, Keyword::SEARCH]) {
self.parse_create_text_search().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)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this instead use the same pattern as temporary/volatile etc, that we flag the bool beforehand, so that we reuse the same call to parse_create_table?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok now unlogged is flagged up front and reuses the single parse_create_table call.

I kept a peek_keywords([UNLOGGED, TABLE]) guard instead of an unconditional parse_keyword like volatile. Unconditional consume means every other CREATE branch swallows a stray UNLOGGED and drops it:

CREATE UNLOGGED VIEW v AS SELECT 1   => CREATE VIEW v AS SELECT 1
CREATE UNLOGGED INDEX idx ON t (a)   => CREATE INDEX idx ON t(a)
CREATE UNLOGGED DATABASE d           => CREATE DATABASE d
CREATE UNLOGGED SEQUENCE seq         => CREATE SEQUENCE seq   -- valid PG, modifier silently lost

The peek keeps these erroring via the existing final else.

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)
Expand Down Expand Up @@ -8834,6 +8840,7 @@ impl<'a> Parser<'a> {

Ok(CreateTableBuilder::new(table_name)
.temporary(temporary)
.unlogged(false)
.columns(columns)
.constraints(constraints)
.or_replace(or_replace)
Expand Down Expand Up @@ -11024,6 +11031,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])?;
Expand Down
24 changes: 24 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19515,3 +19515,27 @@ fn parse_table_factor_paren_chain_no_exponential_blowup() {
rx.recv_timeout(Duration::from_secs(5))
.expect("parser should reject this quickly, not loop exponentially");
}

#[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"),
}
}
1 change: 1 addition & 0 deletions tests/sqlparser_duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
79 changes: 79 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,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 ()";
Expand Down Expand Up @@ -5873,6 +5900,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!(
Expand Down Expand Up @@ -6819,6 +6897,7 @@ fn parse_trigger_related_functions() {
CreateTable {
or_replace: false,
temporary: false,
unlogged: false,
external: false,
global: None,
dynamic: false,
Expand Down
Loading