use std::sync::Arc;
use arrow::array::{Float64Array, Int32Array, Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use datafusion::execution::runtime_env::RuntimeEnv;
use datafusion::prelude::*;
use datafusion_ducklake::{
DuckLakeCatalog, DuckLakeTableWriter, MetadataProvider, MetadataWriter, MulticatalogManager,
MulticatalogProvider, PostgresMetadataWriter, initialize_multicatalog_schema,
};
use object_store::local::LocalFileSystem;
use sqlx::Row;
use sqlx::postgres::PgPoolOptions;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 3 {
eprintln!("Usage: {} <POSTGRES_URL> <DATA_DIR>", args[0]);
std::process::exit(1);
}
let pg_url = &args[1];
let data_dir = std::path::PathBuf::from(&args[2]);
std::fs::create_dir_all(&data_dir)?;
let data_dir_str = data_dir.canonicalize()?.to_string_lossy().to_string();
println!("== Multicatalog Postgres writer demo ==");
println!("postgres : {}", pg_url);
println!("data dir : {}", data_dir_str);
println!();
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(pg_url)
.await?;
initialize_multicatalog_schema(&pool).await?;
println!("✓ schema bootstrapped");
let mgr = MulticatalogManager::new(pool.clone());
let cat_pg = mgr.create_catalog("pg_prod").await?;
let cat_mysql = mgr.create_catalog("mysql_prod").await?;
println!(
"✓ catalogs: pg_prod -> {}, mysql_prod -> {}",
cat_pg, cat_mysql
);
let object_store: Arc<dyn object_store::ObjectStore> = Arc::new(LocalFileSystem::new());
let writer_pg = Arc::new(PostgresMetadataWriter::with_pool(pool.clone(), cat_pg).await?);
writer_pg.set_data_path(&data_dir_str)?;
let users_batch = build_users_batch();
let tw_pg = DuckLakeTableWriter::new(writer_pg.clone(), Arc::clone(&object_store))?;
let users_result = tw_pg
.write_table("public", "users", std::slice::from_ref(&users_batch))
.await?;
println!(
"✓ wrote pg_prod.public.users — snapshot {}, file count {}, rows {}",
users_result.snapshot_id, users_result.files_written, users_result.records_written
);
let users_dml = tw_pg
.write_table("public", "users", std::slice::from_ref(&users_batch))
.await?;
println!(
"✓ wrote pg_prod.public.users AGAIN (DML) — snapshot {}",
users_dml.snapshot_id
);
let writer_mysql = Arc::new(PostgresMetadataWriter::with_pool(pool.clone(), cat_mysql).await?);
let orders_batch = build_orders_batch();
let tw_mysql = DuckLakeTableWriter::new(writer_mysql.clone(), Arc::clone(&object_store))?;
let orders_result = tw_mysql
.write_table("public", "orders", std::slice::from_ref(&orders_batch))
.await?;
println!(
"✓ wrote mysql_prod.public.orders — snapshot {}, file count {}, rows {}",
orders_result.snapshot_id, orders_result.files_written, orders_result.records_written
);
let users_v2_batch = build_users_v2_batch();
let users_v2 = tw_pg
.write_table("public", "users", std::slice::from_ref(&users_v2_batch))
.await?;
println!(
"✓ wrote pg_prod.public.users WITH age column (DDL) — snapshot {}",
users_v2.snapshot_id
);
println!();
println!("== Catalog state ==");
dump_query(
&pool,
"ducklake_catalog",
"SELECT catalog_id, catalog_name FROM ducklake_catalog ORDER BY catalog_id",
)
.await?;
dump_query(
&pool,
"ducklake_catalog_snapshot_map",
"SELECT catalog_id, snapshot_id FROM ducklake_catalog_snapshot_map ORDER BY catalog_id, snapshot_id",
)
.await?;
dump_query(
&pool,
"ducklake_catalog_schema_map",
"SELECT catalog_id, schema_id FROM ducklake_catalog_schema_map ORDER BY catalog_id",
)
.await?;
dump_query(
&pool,
"ducklake_snapshot",
"SELECT snapshot_id, schema_version FROM ducklake_snapshot ORDER BY snapshot_id",
)
.await?;
dump_query(
&pool,
"ducklake_schema",
"SELECT schema_id, schema_name, path, begin_snapshot, end_snapshot FROM ducklake_schema ORDER BY schema_id",
)
.await?;
dump_query(
&pool,
"ducklake_table",
"SELECT table_id, schema_id, table_name, begin_snapshot, end_snapshot FROM ducklake_table ORDER BY table_id",
)
.await?;
dump_query(
&pool,
"ducklake_schema_versions",
"SELECT begin_snapshot, schema_version, table_id FROM ducklake_schema_versions ORDER BY begin_snapshot",
)
.await?;
dump_query(
&pool,
"ducklake_data_file",
"SELECT data_file_id, table_id, path, record_count, begin_snapshot, end_snapshot FROM ducklake_data_file ORDER BY data_file_id",
)
.await?;
println!();
println!("== Reading via MulticatalogProvider + DuckLakeCatalog ==");
println!();
read_via_multicatalog(&pool, "pg_prod", "users", "SELECT * FROM users ORDER BY id").await?;
read_via_multicatalog(
&pool,
"mysql_prod",
"orders",
"SELECT * FROM orders ORDER BY order_id",
)
.await?;
println!("\n -- cross-catalog leakage check (pg_prod must NOT see 'orders') --");
let cross = pg_prod_sees_orders(&pool).await?;
if cross {
println!(" LEAK! pg_prod can see mysql_prod's table");
} else {
println!(" ✓ pg_prod cannot see mysql_prod.orders — isolation works");
}
println!("\n✓ end-to-end demo complete");
Ok(())
}
async fn read_via_multicatalog(
pool: &sqlx::PgPool,
catalog_name: &str,
expected_table: &str,
sql: &str,
) -> Result<(), Box<dyn std::error::Error>> {
println!("\n -- {} via MulticatalogProvider --", catalog_name);
let provider = MulticatalogProvider::with_pool(pool.clone(), catalog_name).await?;
let snapshot = provider.get_current_snapshot()?;
println!(
" catalog_id={}, current snapshot={}",
provider.catalog_id(),
snapshot
);
let catalog = DuckLakeCatalog::with_snapshot(Arc::new(provider), snapshot)?;
let runtime = Arc::new(RuntimeEnv::default());
let config = SessionConfig::new().with_default_catalog_and_schema(catalog_name, "public");
let ctx = SessionContext::new_with_config_rt(config, runtime);
ctx.register_catalog(catalog_name, Arc::new(catalog));
if let Some(cat) = ctx.catalog(catalog_name) {
for schema_name in cat.schema_names() {
if schema_name == "information_schema" {
continue;
}
let schema = cat.schema(&schema_name).unwrap();
println!(
" schema {} -> tables {:?}",
schema_name,
schema.table_names()
);
}
}
println!(" query: {}", sql);
let df = ctx.sql(sql).await?;
df.show().await?;
let _ = expected_table; Ok(())
}
async fn pg_prod_sees_orders(pool: &sqlx::PgPool) -> Result<bool, Box<dyn std::error::Error>> {
let provider = MulticatalogProvider::with_pool(pool.clone(), "pg_prod").await?;
let sn = provider.get_current_snapshot()?;
let catalog = DuckLakeCatalog::with_snapshot(Arc::new(provider), sn)?;
let ctx = SessionContext::new();
ctx.register_catalog("pg_prod", Arc::new(catalog));
let cat = ctx.catalog("pg_prod").unwrap();
let schema = match cat.schema("public") {
Some(s) => s,
None => return Ok(false),
};
Ok(schema.table_names().iter().any(|n| n == "orders"))
}
fn build_users_batch() -> RecordBatch {
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("name", DataType::Utf8, true),
]));
RecordBatch::try_new(
schema,
vec![
Arc::new(Int32Array::from(vec![1, 2, 3])),
Arc::new(StringArray::from(vec![
Some("Alice"),
Some("Bob"),
Some("Carol"),
])),
],
)
.unwrap()
}
fn build_users_v2_batch() -> RecordBatch {
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("name", DataType::Utf8, true),
Field::new("age", DataType::Int32, true),
]));
RecordBatch::try_new(
schema,
vec![
Arc::new(Int32Array::from(vec![1, 2, 3])),
Arc::new(StringArray::from(vec![
Some("Alice"),
Some("Bob"),
Some("Carol"),
])),
Arc::new(Int32Array::from(vec![Some(30), Some(25), None])),
],
)
.unwrap()
}
fn build_orders_batch() -> RecordBatch {
let schema = Arc::new(Schema::new(vec![
Field::new("order_id", DataType::Int64, false),
Field::new("amount", DataType::Float64, false),
]));
RecordBatch::try_new(
schema,
vec![
Arc::new(Int64Array::from(vec![100, 101, 102])),
Arc::new(Float64Array::from(vec![19.99, 4.50, 250.00])),
],
)
.unwrap()
}
async fn dump_query(
pool: &sqlx::PgPool,
label: &str,
sql: &str,
) -> Result<(), Box<dyn std::error::Error>> {
println!("\n -- {} --", label);
let rows = sqlx::query(sql).fetch_all(pool).await?;
if rows.is_empty() {
println!(" (no rows)");
return Ok(());
}
let header: Vec<String> = rows[0]
.columns()
.iter()
.map(|c| sqlx::Column::name(c).to_string())
.collect();
println!(" {}", header.join(" | "));
println!(
" {}",
"-".repeat(header.iter().map(|s| s.len()).sum::<usize>() + header.len() * 3)
);
for row in &rows {
let cols: Vec<String> = (0..row.len()).map(|i| format_col(row, i)).collect();
println!(" {}", cols.join(" | "));
}
Ok(())
}
fn format_col(row: &sqlx::postgres::PgRow, i: usize) -> String {
if let Ok(v) = row.try_get::<Option<i64>, _>(i) {
return v.map(|x| x.to_string()).unwrap_or("NULL".into());
}
if let Ok(v) = row.try_get::<Option<i32>, _>(i) {
return v.map(|x| x.to_string()).unwrap_or("NULL".into());
}
if let Ok(v) = row.try_get::<Option<bool>, _>(i) {
return v.map(|x| x.to_string()).unwrap_or("NULL".into());
}
if let Ok(v) = row.try_get::<Option<String>, _>(i) {
return v.unwrap_or("NULL".into());
}
"<unprintable>".into()
}
#[allow(dead_code)]
async fn visible_files_for_catalog(
pool: &sqlx::PgPool,
catalog_id: i64,
table_name: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let cur: i64 = sqlx::query(
"SELECT COALESCE(MAX(snapshot_id), 0) FROM ducklake_catalog_snapshot_map WHERE catalog_id = $1",
)
.bind(catalog_id)
.fetch_one(pool)
.await?
.try_get(0)?;
let rows = sqlx::query(
"SELECT f.path FROM ducklake_data_file f
JOIN ducklake_table t ON t.table_id = f.table_id
JOIN ducklake_schema s ON s.schema_id = t.schema_id
JOIN ducklake_catalog_schema_map m ON m.schema_id = s.schema_id
WHERE m.catalog_id = $1
AND t.table_name = $2
AND f.begin_snapshot <= $3
AND (f.end_snapshot IS NULL OR f.end_snapshot > $3)
ORDER BY f.path",
)
.bind(catalog_id)
.bind(table_name)
.bind(cur)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|r| r.try_get(0).unwrap()).collect())
}