Skip to content

Commit ef09f00

Browse files
hovaescoclaude
andauthored
Task LAV-3573: BACKUP SET catalog lifecycle (CREATE / CREATE OR ALTER / DESC / SHOW / DROP / ALTER) (#3878)
BACKUP SET becomes a schema-scoped catalog object with its own backup_sets store table (soft drop, live-row unique index), carried through schema/database drop tombstones and purges. - Grammar: CREATE [OR REPLACE | OR ALTER] BACKUP SET [IF NOT EXISTS] ... FOR { [DYNAMIC] TABLE | SCHEMA | DATABASE } with WITH BACKUP POLICY, [WITH] TAG and COMMENT; ALTER RENAME TO / SET COMMENT / UNSET COMMENT; DROP [IF EXISTS]; DESC[RIBE]; SHOW BACKUP SETS. The TAG and BACKUP POLICY clauses parse and are rejected as unsupported until their sibling tasks land. - Fifteen-column read-back shared by SHOW and DESC; bare SHOW ... IN DATABASE now scopes to the session database for the shared scope args. - Reject matrix recorded against real Snowflake: 002002, 001004, 002003 (missing table / dynamic table / set), 511841 (view target), 001521, 002030 (GET_DDL), 090106 (CREATE SNAPSHOT SET leak). - persistence_check.py seeds and verifies a backup set. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f8135e7 commit ef09f00

4 files changed

Lines changed: 279 additions & 2 deletions

File tree

‎src/ast/ddl.rs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2241,6 +2241,60 @@ impl fmt::Display for AlterAuthenticationPolicyOperation {
22412241
}
22422242
}
22432243

2244+
/// The target of a `CREATE BACKUP SET … FOR <target>` statement.
2245+
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
2246+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2247+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
2248+
pub enum BackupSetTargetKind {
2249+
/// `FOR TABLE <table>`
2250+
Table,
2251+
/// `FOR DYNAMIC TABLE <table>`
2252+
DynamicTable,
2253+
/// `FOR SCHEMA <schema>`
2254+
Schema,
2255+
/// `FOR DATABASE <database>`
2256+
Database,
2257+
}
2258+
2259+
impl fmt::Display for BackupSetTargetKind {
2260+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2261+
f.write_str(match self {
2262+
BackupSetTargetKind::Table => "TABLE",
2263+
BackupSetTargetKind::DynamicTable => "DYNAMIC TABLE",
2264+
BackupSetTargetKind::Schema => "SCHEMA",
2265+
BackupSetTargetKind::Database => "DATABASE",
2266+
})
2267+
}
2268+
}
2269+
2270+
/// An operation on a backup set in an `ALTER BACKUP SET` statement.
2271+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
2272+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2273+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
2274+
pub enum AlterBackupSetOperation {
2275+
/// `RENAME TO <name>`
2276+
RenameTo {
2277+
/// The new backup set name.
2278+
new_name: ObjectName,
2279+
},
2280+
/// `SET COMMENT = '<string>'`
2281+
SetComment(String),
2282+
/// `UNSET COMMENT`
2283+
UnsetComment,
2284+
}
2285+
2286+
impl fmt::Display for AlterBackupSetOperation {
2287+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2288+
match self {
2289+
AlterBackupSetOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"),
2290+
AlterBackupSetOperation::SetComment(comment) => {
2291+
write!(f, "SET COMMENT = '{}'", escape_single_quote_string(comment))
2292+
}
2293+
AlterBackupSetOperation::UnsetComment => f.write_str("UNSET COMMENT"),
2294+
}
2295+
}
2296+
}
2297+
22442298
impl fmt::Display for AlterBackupPolicyOperation {
22452299
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22462300
match self {

‎src/ast/mod.rs‎

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub use self::dcl::{
6363
};
6464
pub use self::ddl::{
6565
Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, AlterConnectorOwner,
66-
AlterBackupPolicyOperation, AlterFunction, AlterFunctionAction, AlterFunctionKind,
66+
AlterBackupPolicyOperation, AlterBackupSetOperation, BackupSetTargetKind, AlterFunction, AlterFunctionAction, AlterFunctionKind,
6767
AlterFunctionOperation,
6868
AlterIndexOperation, AlterMaskingPolicyOperation, AlterNetworkRuleOperation, AlterOperator,
6969
AlterRowAccessPolicyOperation,
@@ -6104,6 +6104,65 @@ pub enum Statement {
61046104
show_options: ShowStatementOptions,
61056105
},
61066106
/// ```sql
6107+
/// CREATE [ OR REPLACE | OR ALTER ] BACKUP SET [ IF NOT EXISTS ] <name>
6108+
/// FOR { [ DYNAMIC ] TABLE <t> | SCHEMA <s> | DATABASE <d> }
6109+
/// [ WITH BACKUP POLICY <p> ] [ [ WITH ] TAG ( ... ) ] [ COMMENT = '<c>' ]
6110+
/// ```
6111+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-backup-set>
6112+
CreateBackupSet {
6113+
/// `OR REPLACE` flag.
6114+
or_replace: bool,
6115+
/// `OR ALTER` flag.
6116+
or_alter: bool,
6117+
/// `IF NOT EXISTS` flag.
6118+
if_not_exists: bool,
6119+
/// Backup set name.
6120+
name: ObjectName,
6121+
/// The kind of object the set backs up.
6122+
target_kind: BackupSetTargetKind,
6123+
/// The object the set backs up.
6124+
target: ObjectName,
6125+
/// `WITH BACKUP POLICY <p>`
6126+
backup_policy: Option<ObjectName>,
6127+
/// `[ WITH ] TAG ( <tag> = '<value>' [ , ... ] )`
6128+
with_tags: Option<Vec<Tag>>,
6129+
/// `COMMENT = '<c>'`
6130+
comment: Option<String>,
6131+
},
6132+
/// ```sql
6133+
/// ALTER BACKUP SET <name>
6134+
/// { RENAME TO <name> | SET COMMENT = '<c>' | UNSET COMMENT }
6135+
/// ```
6136+
AlterBackupSet {
6137+
/// Backup set name.
6138+
name: ObjectName,
6139+
/// The operation to apply.
6140+
operation: AlterBackupSetOperation,
6141+
},
6142+
/// ```sql
6143+
/// DROP BACKUP SET [IF EXISTS] <name>
6144+
/// ```
6145+
DropBackupSet {
6146+
/// `IF EXISTS` flag.
6147+
if_exists: bool,
6148+
/// Backup set name.
6149+
name: ObjectName,
6150+
},
6151+
/// ```sql
6152+
/// DESC[RIBE] BACKUP SET <name>
6153+
/// ```
6154+
DescribeBackupSet {
6155+
/// Backup set name.
6156+
name: ObjectName,
6157+
},
6158+
/// ```sql
6159+
/// SHOW BACKUP SETS [ LIKE '<pattern>' ] [ IN <scope> ]
6160+
/// ```
6161+
ShowBackupSets {
6162+
/// Options controlling the SHOW output (filter, `IN <scope>`, etc.).
6163+
show_options: ShowStatementOptions,
6164+
},
6165+
/// ```sql
61076166
/// CREATE [OR REPLACE] AUTHENTICATION POLICY [IF NOT EXISTS] <name>
61086167
/// [ <property> = <value> ... ] [ COMMENT = '<comment>' ]
61096168
/// ```
@@ -9858,6 +9917,49 @@ impl fmt::Display for Statement {
98589917
Statement::ShowBackupPolicies { show_options } => {
98599918
write!(f, "SHOW BACKUP POLICIES{show_options}")
98609919
}
9920+
Statement::CreateBackupSet {
9921+
or_replace,
9922+
or_alter,
9923+
if_not_exists,
9924+
name,
9925+
target_kind,
9926+
target,
9927+
backup_policy,
9928+
with_tags,
9929+
comment,
9930+
} => {
9931+
write!(
9932+
f,
9933+
"CREATE {or_replace}{or_alter}BACKUP SET {if_not_exists}{name} FOR {target_kind} {target}",
9934+
or_replace = if *or_replace { "OR REPLACE " } else { "" },
9935+
or_alter = if *or_alter { "OR ALTER " } else { "" },
9936+
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
9937+
)?;
9938+
if let Some(policy) = backup_policy {
9939+
write!(f, " WITH BACKUP POLICY {policy}")?;
9940+
}
9941+
if let Some(tags) = with_tags {
9942+
write!(f, " WITH TAG ({})", display_comma_separated(tags))?;
9943+
}
9944+
if let Some(comment) = comment {
9945+
write!(f, " COMMENT = '{}'", value::escape_single_quote_string(comment))?;
9946+
}
9947+
Ok(())
9948+
}
9949+
Statement::AlterBackupSet { name, operation } => {
9950+
write!(f, "ALTER BACKUP SET {name} {operation}")
9951+
}
9952+
Statement::DropBackupSet { if_exists, name } => {
9953+
write!(
9954+
f,
9955+
"DROP BACKUP SET {if_exists}{name}",
9956+
if_exists = if *if_exists { "IF EXISTS " } else { "" },
9957+
)
9958+
}
9959+
Statement::DescribeBackupSet { name } => write!(f, "DESCRIBE BACKUP SET {name}"),
9960+
Statement::ShowBackupSets { show_options } => {
9961+
write!(f, "SHOW BACKUP SETS{show_options}")
9962+
}
98619963
Statement::CreateNetworkRule {
98629964
or_replace,
98639965
if_not_exists,

‎src/ast/spans.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,11 @@ impl Spanned for Statement {
627627
Statement::DropBackupPolicy { .. } => Span::empty(),
628628
Statement::DescribeBackupPolicy { .. } => Span::empty(),
629629
Statement::ShowBackupPolicies { .. } => Span::empty(),
630+
Statement::CreateBackupSet { .. } => Span::empty(),
631+
Statement::AlterBackupSet { .. } => Span::empty(),
632+
Statement::DropBackupSet { .. } => Span::empty(),
633+
Statement::DescribeBackupSet { .. } => Span::empty(),
634+
Statement::ShowBackupSets { .. } => Span::empty(),
630635
Statement::CreateNetworkRule { .. } => Span::empty(),
631636
Statement::AlterNetworkRule { .. } => Span::empty(),
632637
Statement::DropNetworkRule { .. } => Span::empty(),

‎src/dialect/snowflake.rs‎

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::ast::helpers::stmt_data_loading::{
2828
};
2929
use crate::ast::{
3030
visit_expressions,
31-
AlterAlertOperation, AlterBackupPolicyOperation, AlterColumnOperation,
31+
AlterAlertOperation, AlterBackupPolicyOperation, AlterBackupSetOperation, AlterColumnOperation, BackupSetTargetKind,
3232
AlterExternalVolumeOperation, AlterFileFormatOperation, AlterStreamlitOperation, AlterMaskingPolicyOperation,
3333
AlterAuthenticationPolicyOperation, AlterDatabaseRoleOperation, AlterNetworkRuleOperation,
3434
AlterPasswordPolicyOperation, AlterRoleOperation, AlterSemanticViewOperation,
@@ -420,6 +420,11 @@ impl SnowflakeDialect {
420420
return Some(parse_alter_authentication_policy(parser));
421421
}
422422

423+
if parser.parse_keywords(&[Keyword::ALTER, Keyword::BACKUP, Keyword::SET]) {
424+
// ALTER BACKUP SET
425+
return Some(parse_alter_backup_set(parser));
426+
}
427+
423428
if parser.parse_keywords(&[Keyword::ALTER, Keyword::BACKUP, Keyword::POLICY]) {
424429
// ALTER BACKUP POLICY
425430
return Some(parse_alter_backup_policy(parser));
@@ -539,6 +544,11 @@ impl SnowflakeDialect {
539544
return Some(parse_drop_authentication_policy(parser));
540545
}
541546

547+
if parser.parse_keywords(&[Keyword::DROP, Keyword::BACKUP, Keyword::SET]) {
548+
// DROP BACKUP SET
549+
return Some(parse_drop_backup_set(parser));
550+
}
551+
542552
if parser.parse_keywords(&[Keyword::DROP, Keyword::BACKUP, Keyword::POLICY]) {
543553
// DROP BACKUP POLICY
544554
return Some(parse_drop_backup_policy(parser));
@@ -627,6 +637,10 @@ impl SnowflakeDialect {
627637
// DESC[RIBE] AUTHENTICATION POLICY
628638
return Some(parse_describe_authentication_policy(parser));
629639
}
640+
if parser.parse_keywords(&[Keyword::BACKUP, Keyword::SET]) {
641+
// DESC[RIBE] BACKUP SET
642+
return Some(parse_describe_backup_set(parser));
643+
}
630644
if parser.parse_keywords(&[Keyword::BACKUP, Keyword::POLICY]) {
631645
// DESC[RIBE] BACKUP POLICY
632646
return Some(parse_describe_backup_policy(parser));
@@ -743,6 +757,11 @@ impl SnowflakeDialect {
743757
return Some(parse_create_authentication_policy(or_replace, parser));
744758
}
745759

760+
// CREATE [OR REPLACE | OR ALTER] BACKUP SET
761+
if parser.parse_keywords(&[Keyword::BACKUP, Keyword::SET]) {
762+
return Some(parse_create_backup_set(or_replace, or_alter, parser));
763+
}
764+
746765
// CREATE [OR REPLACE | OR ALTER] BACKUP POLICY
747766
if parser.parse_keywords(&[Keyword::BACKUP, Keyword::POLICY]) {
748767
return Some(parse_create_backup_policy(or_replace, or_alter, parser));
@@ -1050,6 +1069,10 @@ impl SnowflakeDialect {
10501069
if parser.parse_keywords(&[Keyword::AUTHENTICATION, Keyword::POLICIES]) {
10511070
return Some(parse_show_authentication_policies(parser));
10521071
}
1072+
if parser.parse_keywords(&[Keyword::BACKUP, Keyword::SETS]) {
1073+
let show_options = parser.parse_show_stmt_options();
1074+
return Some(show_options.map(|show_options| Statement::ShowBackupSets { show_options }));
1075+
}
10531076
if parser.parse_keywords(&[Keyword::BACKUP, Keyword::POLICIES]) {
10541077
return Some(parse_show_backup_policies(parser));
10551078
}
@@ -6124,6 +6147,99 @@ fn parse_show_authentication_policies(parser: &mut Parser) -> Result<Statement,
61246147
})
61256148
}
61266149

6150+
/// Parse `CREATE [ OR REPLACE | OR ALTER ] BACKUP SET [ IF NOT EXISTS ] <name>
6151+
/// FOR { [ DYNAMIC ] TABLE <t> | SCHEMA <s> | DATABASE <d> }` followed, in any
6152+
/// order, by `WITH BACKUP POLICY <p>`, `[ WITH ] TAG ( ... )` and `COMMENT = '<c>'`.
6153+
fn parse_create_backup_set(
6154+
or_replace: bool,
6155+
or_alter: bool,
6156+
parser: &mut Parser,
6157+
) -> Result<Statement, ParserError> {
6158+
let if_not_exists = parser.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
6159+
let name = parser.parse_object_name(false)?;
6160+
parser.expect_keyword_is(Keyword::FOR)?;
6161+
let target_kind = if parser.parse_keywords(&[Keyword::DYNAMIC, Keyword::TABLE]) {
6162+
BackupSetTargetKind::DynamicTable
6163+
} else if parser.parse_keyword(Keyword::TABLE) {
6164+
BackupSetTargetKind::Table
6165+
} else if parser.parse_keyword(Keyword::SCHEMA) {
6166+
BackupSetTargetKind::Schema
6167+
} else if parser.parse_keyword(Keyword::DATABASE) {
6168+
BackupSetTargetKind::Database
6169+
} else {
6170+
return parser.expected_ref(
6171+
"TABLE, DYNAMIC TABLE, SCHEMA or DATABASE",
6172+
parser.peek_token_ref(),
6173+
);
6174+
};
6175+
let target = parser.parse_object_name(false)?;
6176+
let mut backup_policy = None;
6177+
let mut with_tags = None;
6178+
let mut comment = None;
6179+
loop {
6180+
if parser.parse_keywords(&[Keyword::WITH, Keyword::BACKUP, Keyword::POLICY]) {
6181+
backup_policy = Some(parser.parse_object_name(false)?);
6182+
} else if parser.parse_keywords(&[Keyword::WITH, Keyword::TAG])
6183+
|| parser.parse_keyword(Keyword::TAG)
6184+
{
6185+
parser.expect_token(&Token::LParen)?;
6186+
with_tags = Some(parser.parse_comma_separated(Parser::parse_tag)?);
6187+
parser.expect_token(&Token::RParen)?;
6188+
} else if parser.parse_keyword(Keyword::COMMENT) {
6189+
parser.expect_token(&Token::Eq)?;
6190+
comment = Some(parser.parse_comment_value()?);
6191+
} else {
6192+
break;
6193+
}
6194+
}
6195+
Ok(Statement::CreateBackupSet {
6196+
or_replace,
6197+
or_alter,
6198+
if_not_exists,
6199+
name,
6200+
target_kind,
6201+
target,
6202+
backup_policy,
6203+
with_tags,
6204+
comment,
6205+
})
6206+
}
6207+
6208+
/// Parse `ALTER BACKUP SET <name>
6209+
/// { RENAME TO <name> | SET COMMENT = '<c>' | UNSET COMMENT }`.
6210+
fn parse_alter_backup_set(parser: &mut Parser) -> Result<Statement, ParserError> {
6211+
let name = parser.parse_object_name(false)?;
6212+
let operation = if parser.parse_keywords(&[Keyword::RENAME, Keyword::TO]) {
6213+
AlterBackupSetOperation::RenameTo {
6214+
new_name: parser.parse_object_name(false)?,
6215+
}
6216+
} else if parser.parse_keywords(&[Keyword::SET, Keyword::COMMENT]) {
6217+
parser.expect_token(&Token::Eq)?;
6218+
AlterBackupSetOperation::SetComment(parser.parse_comment_value()?)
6219+
} else if parser.parse_keywords(&[Keyword::UNSET, Keyword::COMMENT]) {
6220+
AlterBackupSetOperation::UnsetComment
6221+
} else {
6222+
return parser.expected_ref(
6223+
"RENAME TO, SET COMMENT or UNSET COMMENT",
6224+
parser.peek_token_ref(),
6225+
);
6226+
};
6227+
Ok(Statement::AlterBackupSet { name, operation })
6228+
}
6229+
6230+
/// Parse `DROP BACKUP SET [IF EXISTS] <name>`
6231+
fn parse_drop_backup_set(parser: &mut Parser) -> Result<Statement, ParserError> {
6232+
let if_exists = parser.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);
6233+
let name = parser.parse_object_name(false)?;
6234+
Ok(Statement::DropBackupSet { if_exists, name })
6235+
}
6236+
6237+
/// Parse `DESC[RIBE] BACKUP SET <name>`
6238+
fn parse_describe_backup_set(parser: &mut Parser) -> Result<Statement, ParserError> {
6239+
let name = parser.parse_object_name(false)?;
6240+
Ok(Statement::DescribeBackupSet { name })
6241+
}
6242+
61276243
/// Parse `ALTER BACKUP POLICY <name>
61286244
/// { SET <prop> = <v> [, ...] | UNSET <prop> [, ...] | RENAME TO <name> }`.
61296245
fn parse_alter_backup_policy(parser: &mut Parser) -> Result<Statement, ParserError> {

0 commit comments

Comments
 (0)