use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use iceberg::io::StorageFactory;
use iceberg::{Catalog, CatalogBuilder, Error, ErrorKind, Result};
use iceberg_catalog_glue::GlueCatalogBuilder;
use iceberg_catalog_hms::HmsCatalogBuilder;
use iceberg_catalog_rest::RestCatalogBuilder;
use iceberg_catalog_s3tables::S3TablesCatalogBuilder;
use iceberg_catalog_sql::SqlCatalogBuilder;
type CatalogBuilderFactory = fn() -> Box<dyn BoxedCatalogBuilder>;
static CATALOG_REGISTRY: &[(&str, CatalogBuilderFactory)] = &[
("rest", || Box::new(RestCatalogBuilder::default())),
("glue", || Box::new(GlueCatalogBuilder::default())),
("s3tables", || Box::new(S3TablesCatalogBuilder::default())),
("hms", || Box::new(HmsCatalogBuilder::default())),
("sql", || Box::new(SqlCatalogBuilder::default())),
];
pub fn supported_types() -> Vec<&'static str> {
CATALOG_REGISTRY.iter().map(|(k, _)| *k).collect()
}
#[async_trait]
pub trait BoxedCatalogBuilder: Send {
fn with_storage_factory(
self: Box<Self>,
storage_factory: Arc<dyn StorageFactory>,
) -> Box<dyn BoxedCatalogBuilder>;
async fn load(
self: Box<Self>,
name: String,
props: HashMap<String, String>,
) -> Result<Arc<dyn Catalog>>;
}
#[async_trait]
impl<T: CatalogBuilder + 'static> BoxedCatalogBuilder for T {
fn with_storage_factory(
self: Box<Self>,
storage_factory: Arc<dyn StorageFactory>,
) -> Box<dyn BoxedCatalogBuilder> {
Box::new(CatalogBuilder::with_storage_factory(*self, storage_factory))
}
async fn load(
self: Box<Self>,
name: String,
props: HashMap<String, String>,
) -> Result<Arc<dyn Catalog>> {
let builder = *self;
Ok(Arc::new(builder.load(name, props).await?) as Arc<dyn Catalog>)
}
}
pub fn load(r#type: &str) -> Result<Box<dyn BoxedCatalogBuilder>> {
let key = r#type.trim();
if let Some((_, factory)) = CATALOG_REGISTRY
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
{
Ok(factory())
} else {
Err(Error::new(
ErrorKind::FeatureUnsupported,
format!(
"Unsupported catalog type: {}. Supported types: {}",
r#type,
supported_types().join(", ")
),
))
}
}
pub struct CatalogLoader<'a> {
catalog_type: &'a str,
}
impl<'a> From<&'a str> for CatalogLoader<'a> {
fn from(s: &'a str) -> Self {
Self { catalog_type: s }
}
}
impl CatalogLoader<'_> {
pub async fn load(
self,
name: String,
props: HashMap<String, String>,
) -> Result<Arc<dyn Catalog>> {
let builder = load(self.catalog_type)?;
builder.load(name, props).await
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use iceberg::io::LocalFsStorageFactory;
use sqlx::migrate::MigrateDatabase;
use tempfile::TempDir;
use crate::{CatalogLoader, load};
#[tokio::test]
async fn test_load_unsupported_catalog() {
let result = load("unsupported");
assert!(result.is_err());
}
#[tokio::test]
async fn test_catalog_loader_pattern() {
use iceberg_catalog_rest::REST_CATALOG_PROP_URI;
let catalog = CatalogLoader::from("rest")
.load(
"rest".to_string(),
HashMap::from([
(
REST_CATALOG_PROP_URI.to_string(),
"http://localhost:8080".to_string(),
),
("key".to_string(), "value".to_string()),
]),
)
.await;
assert!(catalog.is_ok());
}
#[tokio::test]
async fn test_catalog_loader_pattern_rest_catalog() {
use iceberg_catalog_rest::REST_CATALOG_PROP_URI;
let catalog_loader = load("rest").unwrap();
let catalog = catalog_loader
.load(
"rest".to_string(),
HashMap::from([
(
REST_CATALOG_PROP_URI.to_string(),
"http://localhost:8080".to_string(),
),
("key".to_string(), "value".to_string()),
]),
)
.await;
assert!(catalog.is_ok());
}
#[tokio::test]
async fn test_catalog_loader_pattern_glue_catalog() {
use iceberg_catalog_glue::GLUE_CATALOG_PROP_WAREHOUSE;
let catalog_loader = load("glue").unwrap();
let catalog = catalog_loader
.load(
"glue".to_string(),
HashMap::from([
(
GLUE_CATALOG_PROP_WAREHOUSE.to_string(),
"s3://test".to_string(),
),
("key".to_string(), "value".to_string()),
]),
)
.await;
assert!(catalog.is_ok());
}
#[tokio::test]
async fn test_catalog_loader_pattern_s3tables() {
use iceberg_catalog_s3tables::S3TABLES_CATALOG_PROP_TABLE_BUCKET_ARN;
let catalog = CatalogLoader::from("s3tables")
.load(
"s3tables".to_string(),
HashMap::from([
(
S3TABLES_CATALOG_PROP_TABLE_BUCKET_ARN.to_string(),
"arn:aws:s3tables:us-east-1:123456789012:bucket/test".to_string(),
),
("key".to_string(), "value".to_string()),
]),
)
.await;
assert!(catalog.is_ok());
}
#[tokio::test]
async fn test_catalog_loader_pattern_hms_catalog() {
use iceberg_catalog_hms::{HMS_CATALOG_PROP_URI, HMS_CATALOG_PROP_WAREHOUSE};
let catalog_loader = load("hms").unwrap();
let catalog = catalog_loader
.with_storage_factory(Arc::new(LocalFsStorageFactory))
.load(
"hms".to_string(),
HashMap::from([
(HMS_CATALOG_PROP_URI.to_string(), "127.0.0.1:1".to_string()),
(
HMS_CATALOG_PROP_WAREHOUSE.to_string(),
"s3://warehouse".to_string(),
),
("key".to_string(), "value".to_string()),
]),
)
.await;
assert!(catalog.is_ok());
}
fn temp_path() -> String {
let temp_dir = TempDir::new().unwrap();
temp_dir.path().to_str().unwrap().to_string()
}
#[tokio::test]
async fn test_catalog_loader_pattern_sql_catalog() {
use iceberg_catalog_sql::{SQL_CATALOG_PROP_URI, SQL_CATALOG_PROP_WAREHOUSE};
let uri = format!("sqlite:{}", temp_path());
sqlx::Sqlite::create_database(&uri).await.unwrap();
let catalog_loader = load("sql").unwrap();
let catalog = catalog_loader
.with_storage_factory(Arc::new(LocalFsStorageFactory))
.load(
"sql".to_string(),
HashMap::from([
(SQL_CATALOG_PROP_URI.to_string(), uri),
(
SQL_CATALOG_PROP_WAREHOUSE.to_string(),
"s3://warehouse".to_string(),
),
]),
)
.await;
assert!(catalog.is_ok());
}
#[tokio::test]
async fn test_error_message_includes_supported_types() {
let err = match load("does-not-exist") {
Ok(_) => panic!("expected error for unsupported type"),
Err(e) => e,
};
let msg = err.message().to_string();
assert!(msg.contains("Supported types:"));
assert!(msg.contains("rest"));
}
}