Skip to content

Commit a891d3f

Browse files
hovaescoclaude
andauthored
Task LAV-3576: Schema and database backups and restore: ADD BACKUP on SCHEMA / DATABASE sets, CREATE SCHEMA / DATABASE FROM BACKUP SET (#4102)
* Task LAV-3576: schema and database backups and restore ALTER BACKUP SET ADD BACKUP on FOR SCHEMA / FOR DATABASE sets parks a clone of the container under a hidden `__sf$backup$<seq>` database; CREATE SCHEMA / DATABASE ... FROM BACKUP SET restores from it. Kind mismatch (511819), unknown id (511807), existing destination and IF NOT EXISTS are handled. A schema set follows its target across a rename. The hidden copies are excluded from SHOW VIEWS IN ACCOUNT, SHOW <keys> IN ACCOUNT and the ACCOUNT_USAGE TABLES / COLUMNS / SCHEMATA / VIEWS / SEQUENCES / TAG_REFERENCES views. Kinds not carried yet are xfailed against LAV-3728 (file formats), LAV-3729 (tasks), LAV-3730 (tags), LAV-3731 (dynamic tables). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Task LAV-3576: carry object grants through schema/database backup restore ADD BACKUP on a SCHEMA / DATABASE set records the non-ownership privileges granted on the container's objects in the backup's meta; CREATE SCHEMA / DATABASE ... FROM BACKUP SET re-grants them on the restored objects. Adds the grant kind to test_schema_restore_contents with a Cloud snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Task LAV-3576: project ACLs and re-key qualified names on container grant restore Restored TABLE/VIEW/STREAM grants now reproject their PG ACLs, qualified object_names (sequences, file formats, policies, qualified routines) are re-keyed to the destination schema, and restored sequences get an OWNERSHIP edge. SHOW GRANTS ON SEQUENCE / FILE FORMAT resolves 2- and 3-part names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent da2030b commit a891d3f

3 files changed

Lines changed: 36 additions & 10 deletions

File tree

‎src/ast/mod.rs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6173,11 +6173,13 @@ pub enum Statement {
61736173
limit: Option<Expr>,
61746174
},
61756175
/// ```sql
6176-
/// CREATE [ OR REPLACE ] TABLE [ IF NOT EXISTS ] <name>
6176+
/// CREATE [ OR REPLACE ] { TABLE | SCHEMA | DATABASE } [ IF NOT EXISTS ] <name>
61776177
/// FROM BACKUP SET <backup_set> IDENTIFIER '<backup_id>'
61786178
/// ```
61796179
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-table>
6180-
CreateTableFromBackupSet {
6180+
CreateFromBackupSet {
6181+
/// The kind of object restored (`TABLE`, `SCHEMA` or `DATABASE`).
6182+
kind: BackupSetTargetKind,
61816183
/// `OR REPLACE` flag (always rejected by Snowflake).
61826184
or_replace: bool,
61836185
/// `IF NOT EXISTS` flag.
@@ -9994,15 +9996,16 @@ impl fmt::Display for Statement {
99949996
}
99959997
Ok(())
99969998
}
9997-
Statement::CreateTableFromBackupSet {
9999+
Statement::CreateFromBackupSet {
10000+
kind,
999810001
or_replace,
999910002
if_not_exists,
1000010003
name,
1000110004
backup_set,
1000210005
backup_id,
1000310006
} => write!(
1000410007
f,
10005-
"CREATE {or_replace}TABLE {if_not_exists}{name} FROM BACKUP SET {backup_set} IDENTIFIER '{backup_id}'",
10008+
"CREATE {or_replace}{kind} {if_not_exists}{name} FROM BACKUP SET {backup_set} IDENTIFIER '{backup_id}'",
1000610009
or_replace = if *or_replace { "OR REPLACE " } else { "" },
1000710010
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
1000810011
backup_id = value::escape_single_quote_string(backup_id),

‎src/ast/spans.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ impl Spanned for Statement {
633633
Statement::DescribeBackupSet { .. } => Span::empty(),
634634
Statement::ShowBackupSets { .. } => Span::empty(),
635635
Statement::ShowBackups { .. } => Span::empty(),
636-
Statement::CreateTableFromBackupSet { .. } => Span::empty(),
636+
Statement::CreateFromBackupSet { .. } => Span::empty(),
637637
Statement::CreateNetworkRule { .. } => Span::empty(),
638638
Statement::AlterNetworkRule { .. } => Span::empty(),
639639
Statement::DropNetworkRule { .. } => Span::empty(),

‎src/dialect/snowflake.rs‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,17 @@ impl SnowflakeDialect {
868868
return Some(parse_create_tag(or_replace, parser));
869869
}
870870

871+
let plain_create = !(or_replace || or_alter || global.is_some() || temporary || volatile
872+
|| transient || iceberg || dynamic || hybrid);
873+
if plain_create && parser.parse_keyword(Keyword::SCHEMA) {
874+
if let Some(restore) =
875+
parse_create_from_backup_set(BackupSetTargetKind::Schema, false, parser)
876+
{
877+
return Some(restore);
878+
}
879+
parser.prev_token();
880+
}
881+
871882
if parser.parse_keyword(Keyword::STAGE) {
872883
// OK - this is CREATE STAGE statement
873884
return Some(parse_create_stage(or_replace, or_alter, temporary, parser));
@@ -891,7 +902,9 @@ impl SnowflakeDialect {
891902
).map(Into::into));
892903
} else if parser.parse_keyword(Keyword::TABLE) {
893904
if !(or_alter || global.is_some() || temporary || volatile || transient || iceberg || dynamic || hybrid) {
894-
if let Some(restore) = parse_create_table_from_backup_set(or_replace, parser) {
905+
if let Some(restore) =
906+
parse_create_from_backup_set(BackupSetTargetKind::Table, or_replace, parser)
907+
{
895908
return Some(restore);
896909
}
897910
}
@@ -906,6 +919,13 @@ impl SnowflakeDialect {
906919
if parser.parse_keyword(Keyword::ROLE) {
907920
return Some(parser.parse_create_database_role(or_replace));
908921
}
922+
if plain_create {
923+
if let Some(restore) =
924+
parse_create_from_backup_set(BackupSetTargetKind::Database, false, parser)
925+
{
926+
return Some(restore);
927+
}
928+
}
909929
return Some(parse_create_database(or_replace, transient, parser));
910930
} else if parser.parse_keywords(&[Keyword::APPLICATION, Keyword::ROLE]) {
911931
return Some(parser.parse_create_application_role(or_replace, or_alter));
@@ -6351,9 +6371,11 @@ fn parse_show_backups(parser: &mut Parser) -> Result<Statement, ParserError> {
63516371
}
63526372

63536373
/// Parse `[ IF NOT EXISTS ] <name> FROM BACKUP SET <set> IDENTIFIER '<id>'`
6354-
/// after `CREATE [ OR REPLACE ] TABLE`. Returns `None` (tokens rewound) when
6355-
/// the statement is not a restore, so the ordinary CREATE TABLE parser runs.
6356-
fn parse_create_table_from_backup_set(
6374+
/// after `CREATE [ OR REPLACE ] { TABLE | SCHEMA | DATABASE }`. Returns `None`
6375+
/// (tokens rewound) when the statement is not a restore, so the ordinary
6376+
/// CREATE parser runs.
6377+
fn parse_create_from_backup_set(
6378+
kind: BackupSetTargetKind,
63576379
or_replace: bool,
63586380
parser: &mut Parser,
63596381
) -> Option<Result<Statement, ParserError>> {
@@ -6369,7 +6391,8 @@ fn parse_create_table_from_backup_set(
63696391
let tail = parser.parse_object_name(false).and_then(|backup_set| {
63706392
Ok((backup_set, parse_backup_identifier(parser)?))
63716393
});
6372-
Some(tail.map(|(backup_set, backup_id)| Statement::CreateTableFromBackupSet {
6394+
Some(tail.map(|(backup_set, backup_id)| Statement::CreateFromBackupSet {
6395+
kind,
63736396
or_replace,
63746397
if_not_exists,
63756398
name,

0 commit comments

Comments
 (0)