use crate::io::FileIO;
use crate::spec::Snapshot;
use futures::future::try_join_all;
use opendal::raw::get_basename;
const TAG_DIR: &str = "tag";
const TAG_PREFIX: &str = "tag-";
#[derive(Debug, Clone)]
pub struct TagManager {
file_io: FileIO,
table_path: String,
}
impl TagManager {
pub fn new(file_io: FileIO, table_path: String) -> Self {
Self {
file_io,
table_path,
}
}
pub fn tag_directory(&self) -> String {
format!("{}/{}", self.table_path, TAG_DIR)
}
pub fn with_branch(&self, branch_name: &str) -> Self {
let branch_path = format!("{}/branch/branch-{}", self.table_path, branch_name);
Self::new(self.file_io.clone(), branch_path)
}
pub fn tag_path(&self, tag_name: &str) -> String {
format!("{}/{}{}", self.tag_directory(), TAG_PREFIX, tag_name)
}
pub async fn tag_exists(&self, tag_name: &str) -> crate::Result<bool> {
let path = self.tag_path(tag_name);
let input = self.file_io.new_input(&path)?;
input.exists().await
}
pub async fn get(&self, tag_name: &str) -> crate::Result<Option<Snapshot>> {
let path = self.tag_path(tag_name);
let input = self.file_io.new_input(&path)?;
let bytes = match input.read().await {
Ok(b) => b,
Err(crate::Error::IoUnexpected { ref source, .. })
if source.kind() == opendal::ErrorKind::NotFound =>
{
return Ok(None);
}
Err(e) => return Err(e),
};
let snapshot: Snapshot =
serde_json::from_slice(&bytes).map_err(|e| crate::Error::DataInvalid {
message: format!("tag '{tag_name}' JSON invalid: {e}"),
source: Some(Box::new(e)),
})?;
Ok(Some(snapshot))
}
pub async fn list_all_names(&self) -> crate::Result<Vec<String>> {
let tag_dir = self.tag_directory();
let statuses = match self.file_io.list_status(&tag_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_prefix(TAG_PREFIX)
.map(str::to_string)
})
.collect();
names.sort_unstable();
Ok(names)
}
pub async fn create(&self, tag_name: &str, snapshot: &Snapshot) -> crate::Result<()> {
let path = self.tag_path(tag_name);
let json = serde_json::to_string(snapshot).map_err(|e| crate::Error::DataInvalid {
message: format!("failed to serialize snapshot for tag '{tag_name}': {e}"),
source: Some(Box::new(e)),
})?;
let output = self.file_io.new_output(&path)?;
output.write(bytes::Bytes::from(json)).await
}
pub async fn delete(&self, tag_name: &str) -> crate::Result<()> {
let path = self.tag_path(tag_name);
self.file_io.delete_file(&path).await
}
pub async fn list_all(&self) -> crate::Result<Vec<(String, Snapshot)>> {
let names = self.list_all_names().await?;
try_join_all(names.into_iter().map(|name| async move {
let snap = self
.get(&name)
.await?
.ok_or_else(|| crate::Error::DataInvalid {
message: format!("tag '{name}' disappeared during listing"),
source: None,
})?;
Ok::<_, crate::Error>((name, snap))
}))
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::FileIOBuilder;
use crate::spec::CommitKind;
use bytes::Bytes;
fn test_file_io() -> FileIO {
FileIOBuilder::new("memory").build().unwrap()
}
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_tag(file_io: &FileIO, tm: &TagManager, name: &str, snapshot: &Snapshot) {
let path = tm.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_names_missing_dir_returns_empty() {
let file_io = test_file_io();
let tm = TagManager::new(file_io, "memory:/test_tag_missing".to_string());
assert!(tm.list_all_names().await.unwrap().is_empty());
assert!(tm.list_all().await.unwrap().is_empty());
}
#[tokio::test]
async fn test_list_all_names_sorted() {
let file_io = test_file_io();
let table_path = "memory:/test_tag_sorted".to_string();
file_io.mkdirs(&format!("{table_path}/tag/")).await.unwrap();
let tm = TagManager::new(file_io.clone(), table_path);
for name in ["v3", "v1", "v2"] {
write_tag(&file_io, &tm, name, &test_snapshot(1)).await;
}
assert_eq!(tm.list_all_names().await.unwrap(), vec!["v1", "v2", "v3"]);
}
#[tokio::test]
async fn test_list_all_loads_pairs() {
let file_io = test_file_io();
let table_path = "memory:/test_tag_pairs".to_string();
file_io.mkdirs(&format!("{table_path}/tag/")).await.unwrap();
let tm = TagManager::new(file_io.clone(), table_path);
write_tag(&file_io, &tm, "a", &test_snapshot(1)).await;
write_tag(&file_io, &tm, "b", &test_snapshot(2)).await;
let pairs = tm.list_all().await.unwrap();
let names: Vec<&str> = pairs.iter().map(|(n, _)| n.as_str()).collect();
let ids: Vec<i64> = pairs.iter().map(|(_, s)| s.id()).collect();
assert_eq!(names, vec!["a", "b"]);
assert_eq!(ids, vec![1, 2]);
}
}