#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CopyIntent {
BackupTenant { tenant_id: u64 },
RestoreTenant {
tenant_id: u64,
dry_run: bool,
force: bool,
},
}
pub fn detect(sql: &str) -> Option<CopyIntent> {
let trimmed = sql.trim().trim_end_matches(';').trim();
let upper = trimmed.to_ascii_uppercase();
if !upper.starts_with("COPY ") && !upper.starts_with("COPY\t") {
return None;
}
if let Some(intent) = match_backup(trimmed, &upper) {
return Some(intent);
}
if let Some(intent) = match_restore(trimmed, &upper) {
return Some(intent);
}
None
}
fn match_backup(sql: &str, upper: &str) -> Option<CopyIntent> {
let after_copy = upper.strip_prefix("COPY")?.trim_start();
let after_paren = after_copy.strip_prefix('(')?.trim_start();
let after_backup = after_paren.strip_prefix("BACKUP")?.trim_start();
let after_tenant = after_backup.strip_prefix("TENANT")?.trim_start();
let close_idx = after_tenant.find(')')?;
let id_token = after_tenant[..close_idx].trim();
let tenant_id: u64 = id_token.parse().ok()?;
let after_close = after_tenant[close_idx + 1..].trim_start();
let after_to = after_close.strip_prefix("TO")?.trim_start();
if !after_to.starts_with("STDOUT") {
return None;
}
let tail = after_to.strip_prefix("STDOUT")?.trim();
if !tail.is_empty() {
return None;
}
let _ = sql; Some(CopyIntent::BackupTenant { tenant_id })
}
fn match_restore(sql: &str, upper: &str) -> Option<CopyIntent> {
let after_copy = upper.strip_prefix("COPY")?.trim_start();
let after_fn = after_copy.strip_prefix("TENANT_RESTORE")?.trim_start();
let after_paren = after_fn.strip_prefix('(')?.trim_start();
let close_idx = after_paren.find(')')?;
let id_token = after_paren[..close_idx].trim();
let tenant_id: u64 = id_token.parse().ok()?;
let after_close = after_paren[close_idx + 1..].trim_start();
let after_from = after_close.strip_prefix("FROM")?.trim_start();
let after_stdin = after_from.strip_prefix("STDIN")?.trim_start();
let normalized = after_stdin.trim().replace("DRY RUN", "DRYRUN");
let mut dry_run = false;
let mut force = false;
for token in normalized.split_whitespace() {
match token {
"DRYRUN" => dry_run = true,
"FORCE" => force = true,
_ => return None,
}
}
let _ = sql;
Some(CopyIntent::RestoreTenant {
tenant_id,
dry_run,
force,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_backup() {
assert_eq!(
detect("COPY (BACKUP TENANT 7) TO STDOUT"),
Some(CopyIntent::BackupTenant { tenant_id: 7 })
);
}
#[test]
fn detects_backup_with_trailing_semicolon_and_whitespace() {
assert_eq!(
detect(" copy ( backup tenant 42 ) to stdout ; "),
Some(CopyIntent::BackupTenant { tenant_id: 42 })
);
}
#[test]
fn detects_restore() {
assert_eq!(
detect("COPY tenant_restore(7) FROM STDIN"),
Some(CopyIntent::RestoreTenant {
tenant_id: 7,
dry_run: false,
force: false
})
);
}
#[test]
fn detects_restore_dry_run() {
assert_eq!(
detect("COPY tenant_restore(7) FROM STDIN DRY RUN"),
Some(CopyIntent::RestoreTenant {
tenant_id: 7,
dry_run: true,
force: false
})
);
assert_eq!(
detect("COPY tenant_restore(7) FROM STDIN DRYRUN"),
Some(CopyIntent::RestoreTenant {
tenant_id: 7,
dry_run: true,
force: false
})
);
}
#[test]
fn detects_restore_force() {
assert_eq!(
detect("COPY tenant_restore(7) FROM STDIN FORCE"),
Some(CopyIntent::RestoreTenant {
tenant_id: 7,
dry_run: false,
force: true
})
);
}
#[test]
fn detects_restore_force_and_dry_run_any_order() {
assert_eq!(
detect("COPY tenant_restore(7) FROM STDIN FORCE DRY RUN"),
Some(CopyIntent::RestoreTenant {
tenant_id: 7,
dry_run: true,
force: true
})
);
assert_eq!(
detect("COPY tenant_restore(7) FROM STDIN DRY RUN FORCE"),
Some(CopyIntent::RestoreTenant {
tenant_id: 7,
dry_run: true,
force: true
})
);
}
#[test]
fn rejects_unknown_restore_modifier() {
assert_eq!(detect("COPY tenant_restore(7) FROM STDIN BOGUS"), None);
}
#[test]
fn rejects_legacy_path_form() {
assert_eq!(detect("BACKUP TENANT 7 TO '/tmp/x.bak'"), None);
assert_eq!(detect("RESTORE TENANT 7 FROM '/tmp/x.bak'"), None);
}
#[test]
fn rejects_garbage() {
assert_eq!(detect("SELECT 1"), None);
assert_eq!(detect("COPY tenant_restore"), None);
assert_eq!(detect("COPY (BACKUP TENANT abc) TO STDOUT"), None);
assert_eq!(detect("COPY (BACKUP TENANT 7) TO STDIN"), None);
}
}