use crate::catalog::DEFAULT_MAIN_BRANCH;
use crate::io::FileIO;
use crate::table::{SchemaManager, SnapshotManager, TagManager};
use opendal::raw::get_basename;
const BRANCH_DIR: &str = "branch";
const BRANCH_PREFIX: &str = "branch-";
#[derive(Debug, Clone)]
pub struct BranchManager {
file_io: FileIO,
table_path: String,
}
impl BranchManager {
pub fn new(file_io: FileIO, table_path: String) -> Self {
Self {
file_io,
table_path,
}
}
pub fn branch_directory(&self) -> String {
format!("{}/{}", self.table_path, BRANCH_DIR)
}
pub fn branch_path(&self, branch_name: &str) -> String {
format!(
"{}/{}{}",
self.branch_directory(),
BRANCH_PREFIX,
branch_name
)
}
fn validate_branch_name(branch_name: &str) -> crate::Result<()> {
if branch_name == DEFAULT_MAIN_BRANCH {
return Err(crate::Error::DataInvalid {
message: format!(
"Branch name '{}' is the default branch and cannot be used.",
DEFAULT_MAIN_BRANCH
),
source: None,
});
}
if branch_name.trim().is_empty() {
return Err(crate::Error::DataInvalid {
message: format!("Branch name '{}' is blank.", branch_name),
source: None,
});
}
if branch_name.chars().all(|c| c.is_ascii_digit()) {
return Err(crate::Error::DataInvalid {
message: format!(
"Branch name cannot be pure numeric string but is '{}'.",
branch_name
),
source: None,
});
}
Ok(())
}
pub async fn validate_branch(&self, branch_name: &str) -> crate::Result<()> {
Self::validate_branch_name(branch_name)?;
if self.branch_exists(branch_name).await? {
return Err(crate::Error::DataInvalid {
message: format!("Branch name '{}' already exists.", branch_name),
source: None,
});
}
Ok(())
}
pub async fn branch_exists(&self, branch_name: &str) -> crate::Result<bool> {
let path = format!("{}/", self.branch_path(branch_name));
self.file_io.exists(&path).await
}
pub async fn list_all(&self) -> crate::Result<Vec<String>> {
let branch_dir = self.branch_directory();
let statuses = match self.file_io.list_status(&branch_dir).await {
Ok(s) => s,
Err(crate::Error::IoUnexpected { ref source, .. })
if source.kind() == opendal::ErrorKind::NotFound =>
{
return Ok(Vec::new());
}
Err(e) => return Err(e),
};
let mut names: Vec<String> = statuses
.into_iter()
.filter(|s| s.is_dir)
.filter_map(|s| {
get_basename(&s.path)
.strip_suffix("/")
.and_then(|base| base.strip_prefix(BRANCH_PREFIX))
.map(str::to_string)
})
.collect();
names.sort_unstable();
Ok(names)
}
pub async fn create_branch(&self, branch_name: &str) -> crate::Result<()> {
self.validate_branch(branch_name).await?;
let schema_manager = SchemaManager::new(self.file_io.clone(), self.table_path.clone());
if let Some(latest) = schema_manager.latest().await? {
self.copy_schemas_to_branch(branch_name, latest.id())
.await?;
}
Ok(())
}
pub async fn create_branch_from_tag(
&self,
branch_name: &str,
tag_name: &str,
) -> crate::Result<()> {
self.validate_branch(branch_name).await?;
let tag_manager = TagManager::new(self.file_io.clone(), self.table_path.clone());
let snapshot =
tag_manager
.get(tag_name)
.await?
.ok_or_else(|| crate::Error::DataInvalid {
message: format!("Tag '{}' does not exist.", tag_name),
source: None,
})?;
let snapshot_manager = SnapshotManager::new(self.file_io.clone(), self.table_path.clone());
let tag_src = tag_manager.tag_path(tag_name);
let tag_dst = tag_manager.with_branch(branch_name).tag_path(tag_name);
self.file_io.copy_file(&tag_src, &tag_dst).await?;
let snap_src = snapshot_manager.snapshot_path(snapshot.id());
let snap_dst = snapshot_manager
.with_branch(branch_name)
.snapshot_path(snapshot.id());
self.file_io.copy_file(&snap_src, &snap_dst).await?;
self.copy_schemas_to_branch(branch_name, snapshot.schema_id())
.await?;
Ok(())
}
pub async fn drop_branch(&self, branch_name: &str) -> crate::Result<()> {
if !self.branch_exists(branch_name).await? {
return Err(crate::Error::DataInvalid {
message: format!("Branch name '{}' doesn't exist.", branch_name),
source: None,
});
}
let path = self.branch_path(branch_name);
self.file_io.delete_dir(&path).await?;
Ok(())
}
pub async fn rename_branch(&self, from: &str, to: &str) -> crate::Result<()> {
if from == DEFAULT_MAIN_BRANCH {
return Err(crate::Error::DataInvalid {
message: "Cannot rename the main branch.".to_string(),
source: None,
});
}
if !self.branch_exists(from).await? {
return Err(crate::Error::DataInvalid {
message: format!("Branch name '{}' doesn't exist.", from),
source: None,
});
}
Self::validate_branch_name(to)?;
if self.branch_exists(to).await? {
return Err(crate::Error::DataInvalid {
message: format!("Branch name '{}' already exists.", to),
source: None,
});
}
let src = self.branch_path(from);
let dst = self.branch_path(to);
match self.file_io.rename(&src, &dst).await {
Ok(()) => Ok(()),
Err(crate::Error::IoUnexpected { ref source, .. })
if source.kind() == opendal::ErrorKind::Unsupported =>
{
self.copy_dir_recursive(&src, &dst).await?;
self.file_io.delete_dir(&src).await?;
Ok(())
}
Err(e) => Err(e),
}
}
async fn copy_dir_recursive(&self, src: &str, dst: &str) -> crate::Result<()> {
use std::collections::VecDeque;
self.file_io.mkdirs(dst).await?;
let mut queue = VecDeque::new();
queue.push_back((src.to_string(), dst.to_string()));
while let Some((current_src, current_dst)) = queue.pop_front() {
self.file_io.mkdirs(¤t_dst).await?;
let statuses = self.file_io.list_status(¤t_src).await?;
for status in statuses {
let name = get_basename(&status.path).trim_end_matches('/');
let src_path = format!("{}/{}", current_src.trim_end_matches('/'), name);
let dst_path = format!("{}/{}", current_dst.trim_end_matches('/'), name);
if status.is_dir {
queue.push_back((src_path, dst_path));
} else {
self.file_io.copy_file(&src_path, &dst_path).await?;
}
}
}
Ok(())
}
async fn copy_schemas_to_branch(&self, branch_name: &str, schema_id: i64) -> crate::Result<()> {
let schema_manager = SchemaManager::new(self.file_io.clone(), self.table_path.clone());
let ids = schema_manager.list_all_ids().await?;
let branch_schema_manager = schema_manager.with_branch(branch_name);
for id in ids {
if id <= schema_id {
let src = schema_manager.schema_path(id);
let dst = branch_schema_manager.schema_path(id);
self.file_io.copy_file(&src, &dst).await?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::FileIOBuilder;
use crate::spec::{CommitKind, Schema, Snapshot, TableSchema};
use bytes::Bytes;
fn test_file_io() -> FileIO {
FileIOBuilder::new("memory").build().unwrap()
}
fn test_schema() -> TableSchema {
let schema = Schema::builder().build().unwrap();
TableSchema::new(0, &schema)
}
fn test_snapshot(id: i64) -> Snapshot {
Snapshot::builder()
.version(3)
.id(id)
.schema_id(0)
.base_manifest_list("base-list".to_string())
.delta_manifest_list("delta-list".to_string())
.commit_user("test-user".to_string())
.commit_identifier(0)
.commit_kind(CommitKind::APPEND)
.time_millis(1000 * id as u64)
.build()
}
async fn write_schema(file_io: &FileIO, schema_manager: &SchemaManager, schema: &TableSchema) {
let path = schema_manager.schema_path(schema.id());
let json = serde_json::to_string(schema).unwrap();
let output = file_io.new_output(&path).unwrap();
output.write(Bytes::from(json)).await.unwrap();
}
async fn write_snapshot(
file_io: &FileIO,
snapshot_manager: &SnapshotManager,
snapshot: &Snapshot,
) {
let path = snapshot_manager.snapshot_path(snapshot.id());
let json = serde_json::to_string(snapshot).unwrap();
let output = file_io.new_output(&path).unwrap();
output.write(Bytes::from(json)).await.unwrap();
}
async fn write_tag(
file_io: &FileIO,
tag_manager: &TagManager,
name: &str,
snapshot: &Snapshot,
) {
let path = tag_manager.tag_path(name);
let json = serde_json::to_string(snapshot).unwrap();
let output = file_io.new_output(&path).unwrap();
output.write(Bytes::from(json)).await.unwrap();
}
#[tokio::test]
async fn test_list_all_missing_dir_returns_empty() {
let file_io = test_file_io();
let bm = BranchManager::new(file_io, "memory:/test_branch_missing".to_string());
assert!(bm.list_all().await.unwrap().is_empty());
}
#[tokio::test]
async fn test_list_all_sorted() {
let file_io = test_file_io();
let table_path = "memory:/test_branch_sorted".to_string();
let branch_dir = format!("{table_path}/branch");
file_io.mkdirs(&branch_dir).await.unwrap();
for name in ["b", "a", "c"] {
let path = format!("{branch_dir}/branch-{name}");
file_io.mkdirs(&path).await.unwrap();
}
let bm = BranchManager::new(file_io, table_path);
assert_eq!(bm.list_all().await.unwrap(), vec!["a", "b", "c"]);
}
#[tokio::test]
async fn test_branch_exists() {
let file_io = test_file_io();
let table_path = "memory:/test_branch_exists".to_string();
let branch_dir = format!("{table_path}/branch");
file_io.mkdirs(&branch_dir).await.unwrap();
file_io
.mkdirs(&format!("{branch_dir}/branch-foo"))
.await
.unwrap();
let bm = BranchManager::new(file_io, table_path);
assert!(bm.branch_exists("foo").await.unwrap());
assert!(!bm.branch_exists("bar").await.unwrap());
}
#[tokio::test]
async fn test_validate_branch_name_rejects_main() {
let result = BranchManager::validate_branch_name("main");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("main"));
}
#[tokio::test]
async fn test_validate_branch_name_rejects_blank() {
let result = BranchManager::validate_branch_name("");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("blank"));
}
#[tokio::test]
async fn test_validate_branch_name_rejects_numeric() {
let result = BranchManager::validate_branch_name("123");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("numeric"));
}
#[tokio::test]
async fn test_validate_branch_name_accepts_valid() {
assert!(BranchManager::validate_branch_name("my_branch").is_ok());
assert!(BranchManager::validate_branch_name("branch-1").is_ok());
}
#[tokio::test]
async fn test_create_branch() {
let file_io = test_file_io();
let table_path = "memory:/test_create_branch".to_string();
let schema_manager = SchemaManager::new(file_io.clone(), table_path.clone());
write_schema(&file_io, &schema_manager, &test_schema()).await;
let bm = BranchManager::new(file_io.clone(), table_path.clone());
bm.create_branch("my_branch").await.unwrap();
assert!(bm.branch_exists("my_branch").await.unwrap());
let branch_schema_manager = schema_manager.with_branch("my_branch");
let schemas = branch_schema_manager.list_all().await.unwrap();
assert_eq!(schemas.len(), 1);
assert_eq!(schemas[0].id(), 0);
}
#[tokio::test]
async fn test_create_branch_already_exists() {
let file_io = test_file_io();
let table_path = "memory:/test_create_branch_exists".to_string();
let schema_manager = SchemaManager::new(file_io.clone(), table_path.clone());
write_schema(&file_io, &schema_manager, &test_schema()).await;
let bm = BranchManager::new(file_io, table_path);
bm.create_branch("my_branch").await.unwrap();
let result = bm.create_branch("my_branch").await;
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("already exists"));
}
#[tokio::test]
async fn test_create_branch_from_tag() {
let file_io = test_file_io();
let table_path = "memory:/test_create_branch_from_tag".to_string();
let schema_manager = SchemaManager::new(file_io.clone(), table_path.clone());
let snapshot_manager = SnapshotManager::new(file_io.clone(), table_path.clone());
let tag_manager = TagManager::new(file_io.clone(), table_path.clone());
write_schema(&file_io, &schema_manager, &test_schema()).await;
let snap = test_snapshot(1);
write_snapshot(&file_io, &snapshot_manager, &snap).await;
write_tag(&file_io, &tag_manager, "v1", &snap).await;
let bm = BranchManager::new(file_io.clone(), table_path.clone());
bm.create_branch_from_tag("my_branch", "v1").await.unwrap();
assert!(bm.branch_exists("my_branch").await.unwrap());
let branch_snap_manager = snapshot_manager.with_branch("my_branch");
let copied = branch_snap_manager.get_snapshot(1).await.unwrap();
assert_eq!(copied.id(), 1);
let branch_tag_manager = tag_manager.with_branch("my_branch");
let tag_snap = branch_tag_manager.get("v1").await.unwrap();
assert!(tag_snap.is_some());
let branch_schema_manager = schema_manager.with_branch("my_branch");
let schemas = branch_schema_manager.list_all().await.unwrap();
assert_eq!(schemas.len(), 1);
}
#[tokio::test]
async fn test_drop_branch() {
let file_io = test_file_io();
let table_path = "memory:/test_drop_branch".to_string();
let schema_manager = SchemaManager::new(file_io.clone(), table_path.clone());
write_schema(&file_io, &schema_manager, &test_schema()).await;
let bm = BranchManager::new(file_io, table_path);
bm.create_branch("to_drop").await.unwrap();
assert!(bm.branch_exists("to_drop").await.unwrap());
bm.drop_branch("to_drop").await.unwrap();
assert!(!bm.branch_exists("to_drop").await.unwrap());
}
#[tokio::test]
async fn test_drop_nonexistent_branch() {
let file_io = test_file_io();
let bm = BranchManager::new(file_io, "memory:/test_drop_none".to_string());
let result = bm.drop_branch("nonexistent").await;
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("doesn't exist"));
}
#[tokio::test]
async fn test_rename_branch() {
let file_io = test_file_io();
let table_path = "memory:/test_rename_branch".to_string();
let schema_manager = SchemaManager::new(file_io.clone(), table_path.clone());
write_schema(&file_io, &schema_manager, &test_schema()).await;
let bm = BranchManager::new(file_io, table_path);
bm.create_branch("old_branch").await.unwrap();
assert!(bm.branch_exists("old_branch").await.unwrap());
bm.rename_branch("old_branch", "new_branch").await.unwrap();
assert!(!bm.branch_exists("old_branch").await.unwrap());
assert!(bm.branch_exists("new_branch").await.unwrap());
let branches = bm.list_all().await.unwrap();
assert!(branches.contains(&"new_branch".to_string()));
assert!(!branches.contains(&"old_branch".to_string()));
}
#[tokio::test]
async fn test_rename_nonexistent_branch() {
let file_io = test_file_io();
let bm = BranchManager::new(file_io, "memory:/test_rename_none".to_string());
let result = bm.rename_branch("nonexistent", "new_name").await;
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("doesn't exist"));
}
#[tokio::test]
async fn test_rename_to_existing_branch() {
let file_io = test_file_io();
let table_path = "memory:/test_rename_to_existing".to_string();
let schema_manager = SchemaManager::new(file_io.clone(), table_path.clone());
write_schema(&file_io, &schema_manager, &test_schema()).await;
let bm = BranchManager::new(file_io, table_path);
bm.create_branch("branch1").await.unwrap();
bm.create_branch("branch2").await.unwrap();
let result = bm.rename_branch("branch1", "branch2").await;
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("already exists"));
}
#[tokio::test]
async fn test_rename_main_branch_should_fail() {
let file_io = test_file_io();
let bm = BranchManager::new(file_io, "memory:/test_rename_main".to_string());
let result = bm.rename_branch("main", "new_name").await;
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("Cannot rename the main branch"));
}
#[tokio::test]
async fn test_rename_branch_multiple_times() {
let file_io = test_file_io();
let table_path = "memory:/test_rename_multi".to_string();
let schema_manager = SchemaManager::new(file_io.clone(), table_path.clone());
write_schema(&file_io, &schema_manager, &test_schema()).await;
let bm = BranchManager::new(file_io, table_path);
bm.create_branch("branch1").await.unwrap();
bm.rename_branch("branch1", "branch2").await.unwrap();
bm.rename_branch("branch2", "branch3").await.unwrap();
assert!(!bm.branch_exists("branch1").await.unwrap());
assert!(!bm.branch_exists("branch2").await.unwrap());
assert!(bm.branch_exists("branch3").await.unwrap());
}
}