use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use datafusion::arrow::array::{Array, Int64Array};
use datafusion::catalog::CatalogProvider;
use datafusion::datasource::MemTable;
use paimon::catalog::{list_partitions_from_file_system, Identifier};
use paimon::spec::{
ArrayType, BinaryType, BlobType, CharType, DataType, FloatType, IntType,
LocalZonedTimestampType, MapType, MultisetType, SchemaChange, TimeType, VarBinaryType,
VarCharType, VectorType,
};
use paimon::table::{BranchManager, SnapshotManager, TagManager};
use paimon::{Catalog, CatalogOptions, FileSystemCatalog, Options};
use paimon_datafusion::{PaimonCatalogProvider, SQLContext};
use tempfile::TempDir;
fn create_test_env() -> (TempDir, Arc<FileSystemCatalog>) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let warehouse = format!("file://{}", temp_dir.path().display());
let mut options = Options::new();
options.set(CatalogOptions::WAREHOUSE, warehouse);
let catalog = FileSystemCatalog::new(options).expect("Failed to create catalog");
(temp_dir, Arc::new(catalog))
}
async fn create_sql_context(catalog: Arc<FileSystemCatalog>) -> SQLContext {
let mut ctx = SQLContext::new();
ctx.register_catalog("paimon", catalog).await.unwrap();
ctx
}
struct MetadataListingCatalog {
get_table_calls: AtomicUsize,
}
impl MetadataListingCatalog {
fn new() -> Self {
Self {
get_table_calls: AtomicUsize::new(0),
}
}
fn get_table_calls(&self) -> usize {
self.get_table_calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl Catalog for MetadataListingCatalog {
async fn list_databases(&self) -> paimon::Result<Vec<String>> {
Ok(vec!["default".to_string()])
}
async fn create_database(
&self,
_name: &str,
_ignore_if_exists: bool,
_properties: std::collections::HashMap<String, String>,
) -> paimon::Result<()> {
Ok(())
}
async fn get_database(&self, name: &str) -> paimon::Result<paimon::catalog::Database> {
Ok(paimon::catalog::Database::new(
name.to_string(),
std::collections::HashMap::new(),
None,
))
}
async fn drop_database(
&self,
_name: &str,
_ignore_if_not_exists: bool,
_cascade: bool,
) -> paimon::Result<()> {
Ok(())
}
async fn get_table(&self, _identifier: &Identifier) -> paimon::Result<paimon::table::Table> {
self.get_table_calls.fetch_add(1, Ordering::SeqCst);
Err(paimon::Error::Unsupported {
message: "table loading is unavailable".to_string(),
})
}
async fn list_tables(&self, _database_name: &str) -> paimon::Result<Vec<String>> {
Ok(vec!["metadata_only".to_string()])
}
async fn list_views(&self, _database_name: &str) -> paimon::Result<Vec<String>> {
Ok(vec!["metadata_view".to_string()])
}
async fn create_table(
&self,
_identifier: &Identifier,
_creation: paimon::spec::Schema,
_ignore_if_exists: bool,
) -> paimon::Result<()> {
Ok(())
}
async fn drop_table(
&self,
_identifier: &Identifier,
_ignore_if_not_exists: bool,
) -> paimon::Result<()> {
Ok(())
}
async fn rename_table(
&self,
_from: &Identifier,
_to: &Identifier,
_ignore_if_not_exists: bool,
) -> paimon::Result<()> {
Ok(())
}
async fn alter_table(
&self,
_identifier: &Identifier,
_changes: Vec<SchemaChange>,
_ignore_if_not_exists: bool,
) -> paimon::Result<()> {
Ok(())
}
}
struct PartitionCatalog {
inner: Arc<FileSystemCatalog>,
fail_list_partitions: AtomicBool,
partition_identifiers: Mutex<Vec<Identifier>>,
}
impl PartitionCatalog {
fn new(inner: Arc<FileSystemCatalog>) -> Self {
Self {
inner,
fail_list_partitions: AtomicBool::new(false),
partition_identifiers: Mutex::new(Vec::new()),
}
}
fn set_fail_list_partitions(&self, fail: bool) {
self.fail_list_partitions.store(fail, Ordering::SeqCst);
}
fn take_partition_identifiers(&self) -> Vec<Identifier> {
std::mem::take(&mut *self.partition_identifiers.lock().unwrap())
}
}
#[async_trait]
impl Catalog for PartitionCatalog {
async fn list_databases(&self) -> paimon::Result<Vec<String>> {
self.inner.list_databases().await
}
async fn create_database(
&self,
name: &str,
ignore_if_exists: bool,
properties: std::collections::HashMap<String, String>,
) -> paimon::Result<()> {
self.inner
.create_database(name, ignore_if_exists, properties)
.await
}
async fn get_database(&self, name: &str) -> paimon::Result<paimon::catalog::Database> {
self.inner.get_database(name).await
}
async fn drop_database(
&self,
name: &str,
ignore_if_not_exists: bool,
cascade: bool,
) -> paimon::Result<()> {
self.inner
.drop_database(name, ignore_if_not_exists, cascade)
.await
}
async fn get_table(&self, identifier: &Identifier) -> paimon::Result<paimon::table::Table> {
self.inner.get_table(identifier).await
}
async fn list_tables(&self, database_name: &str) -> paimon::Result<Vec<String>> {
self.inner.list_tables(database_name).await
}
async fn create_table(
&self,
identifier: &Identifier,
creation: paimon::spec::Schema,
ignore_if_exists: bool,
) -> paimon::Result<()> {
self.inner
.create_table(identifier, creation, ignore_if_exists)
.await
}
async fn drop_table(
&self,
identifier: &Identifier,
ignore_if_not_exists: bool,
) -> paimon::Result<()> {
self.inner
.drop_table(identifier, ignore_if_not_exists)
.await
}
async fn rename_table(
&self,
from: &Identifier,
to: &Identifier,
ignore_if_not_exists: bool,
) -> paimon::Result<()> {
self.inner
.rename_table(from, to, ignore_if_not_exists)
.await
}
async fn alter_table(
&self,
identifier: &Identifier,
changes: Vec<SchemaChange>,
ignore_if_not_exists: bool,
) -> paimon::Result<()> {
self.inner
.alter_table(identifier, changes, ignore_if_not_exists)
.await
}
async fn list_partitions(
&self,
identifier: &Identifier,
) -> paimon::Result<Vec<paimon::spec::Partition>> {
self.partition_identifiers
.lock()
.unwrap()
.push(identifier.clone());
let Some(branch) = identifier.branch_name()? else {
return self.inner.list_partitions(identifier).await;
};
if self.fail_list_partitions.load(Ordering::SeqCst) {
return Err(paimon::Error::Unsupported {
message: "injected list_partitions failure".to_string(),
});
}
let base = Identifier::new(identifier.database(), identifier.table_name()?);
let table = self.inner.get_table(&base).await?;
let table = table.copy_with_branch(&branch).await?;
let mut partitions = list_partitions_from_file_system(&table).await?;
for partition in &mut partitions {
partition.created_by = Some("catalog".to_string());
}
Ok(partitions)
}
}
async fn collect_ids(sql_context: &SQLContext, sql: &str) -> Vec<i32> {
let batches = sql_context.sql(sql).await.unwrap().collect().await.unwrap();
let mut ids = Vec::new();
for batch in batches {
let id_array = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.expect("id column");
for row in 0..batch.num_rows() {
ids.push(id_array.value(row));
}
}
ids.sort_unstable();
ids
}
async fn collect_i64_column(sql_context: &SQLContext, sql: &str, column: &str) -> Vec<i64> {
let batches = sql_context.sql(sql).await.unwrap().collect().await.unwrap();
let mut values = Vec::new();
for batch in batches {
let array = batch
.column_by_name(column)
.and_then(|c| c.as_any().downcast_ref::<Int64Array>())
.expect(column);
for row in 0..batch.num_rows() {
values.push(array.value(row));
}
}
values.sort_unstable();
values
}
async fn collect_string_column(sql_context: &SQLContext, sql: &str, column: &str) -> Vec<String> {
let batches = sql_context.sql(sql).await.unwrap().collect().await.unwrap();
let mut values = Vec::new();
for batch in batches {
let array = batch
.column_by_name(column)
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.expect(column);
for row in 0..batch.num_rows() {
if !array.is_null(row) {
values.push(array.value(row).to_string());
}
}
}
values.sort_unstable();
values
}
async fn assert_sql_error_contains(sql_context: &SQLContext, sql: &str, expected: &str) {
let err = match sql_context.sql(sql).await {
Ok(df) => df
.collect()
.await
.expect_err("SQL should fail but succeeded")
.to_string(),
Err(err) => err.to_string(),
};
assert!(
err.contains(expected),
"expected error containing '{expected}', got: {err}"
);
}
#[tokio::test]
async fn test_show_tables_is_enabled() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog).await;
sql_context
.sql("SHOW TABLES")
.await
.expect("SHOW TABLES should be planned when information_schema is enabled")
.collect()
.await
.expect("SHOW TABLES should execute");
}
#[tokio::test]
async fn test_show_tables_does_not_load_table_providers() {
let catalog = Arc::new(MetadataListingCatalog::new());
let mut sql_context = SQLContext::new();
sql_context
.register_catalog("paimon", catalog.clone())
.await
.unwrap();
let table_names = collect_string_column(&sql_context, "SHOW TABLES", "table_name").await;
assert!(table_names.contains(&"metadata_only".to_string()));
assert_eq!(catalog.get_table_calls(), 0);
assert!(sql_context
.sql("SELECT * FROM metadata_only")
.await
.is_err());
assert!(catalog.get_table_calls() > 0);
}
#[tokio::test]
async fn test_show_tables_preserves_catalog_view_type() {
let catalog = Arc::new(MetadataListingCatalog::new());
let mut sql_context = SQLContext::new();
sql_context
.register_catalog("paimon", catalog.clone())
.await
.unwrap();
let table_types = collect_string_column(
&sql_context,
"SELECT table_type FROM information_schema.tables \
WHERE table_schema = 'default' AND table_name = 'metadata_view'",
"table_type",
)
.await;
assert_eq!(table_types, vec!["VIEW"]);
assert_eq!(catalog.get_table_calls(), 0);
}
#[tokio::test]
async fn test_select_branch_table_reads_branch_snapshot() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE TABLE paimon.default.branch_orders (id INT, name STRING)")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.default.branch_orders VALUES (1, 'branch')")
.await
.unwrap()
.collect()
.await
.unwrap();
let identifier = Identifier::new("default", "branch_orders");
let table = catalog.get_table(&identifier).await.unwrap();
let snapshot_manager =
SnapshotManager::new(table.file_io().clone(), table.location().to_string());
let snapshot = snapshot_manager
.get_latest_snapshot()
.await
.unwrap()
.unwrap();
let tag_manager = TagManager::new(table.file_io().clone(), table.location().to_string());
tag_manager.create("branch_base", &snapshot).await.unwrap();
let branch_manager = BranchManager::new(table.file_io().clone(), table.location().to_string());
branch_manager
.create_branch_from_tag("b1", "branch_base")
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.default.branch_orders VALUES (2, 'main')")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(
collect_ids(&sql_context, "SELECT id FROM paimon.default.branch_orders").await,
vec![1, 2]
);
assert_eq!(
collect_ids(
&sql_context,
"SELECT id FROM paimon.default.branch_orders$branch_b1"
)
.await,
vec![1]
);
assert_eq!(
collect_i64_column(
&sql_context,
"SELECT snapshot_id FROM paimon.default.branch_orders$snapshots",
"snapshot_id"
)
.await,
vec![1, 2]
);
assert_eq!(
collect_i64_column(
&sql_context,
"SELECT snapshot_id FROM paimon.default.branch_orders$branch_b1$snapshots",
"snapshot_id"
)
.await,
vec![1]
);
assert_eq!(
collect_i64_column(
&sql_context,
"SELECT record_count FROM paimon.default.branch_orders$files VERSION AS OF 'branch_base'",
"record_count"
)
.await,
vec![1]
);
assert_eq!(
collect_i64_column(
&sql_context,
"SELECT record_count FROM paimon.default.branch_orders$branch_b1$files VERSION AS OF 'branch_base'",
"record_count"
)
.await,
vec![1]
);
assert!(!collect_string_column(
&sql_context,
"SELECT file_name FROM paimon.default.branch_orders$branch_b1$manifests",
"file_name",
)
.await
.is_empty());
let branch_table = table.copy_with_branch("b1").await.unwrap();
let write_builder = branch_table.new_write_builder();
assert!(write_builder.new_write().is_err());
assert!(write_builder.new_update(vec!["name".to_string()]).is_err());
assert!(write_builder.new_delete().is_err());
assert!(write_builder.try_new_commit().is_err());
assert_sql_error_contains(
&sql_context,
"INSERT INTO paimon.default.branch_orders$branch_b1 VALUES (3, 'blocked')",
"Writing to Paimon branch 'b1' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"INSERT INTO paimon.default.branch_orders$branch_main VALUES (3, 'blocked')",
"Writing to Paimon branch 'main' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"UPDATE paimon.default.branch_orders$branch_b1 SET name = 'blocked' WHERE id = 1",
"UPDATE on Paimon branch 'b1' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"UPDATE paimon.default.branch_orders$branch_main SET name = 'blocked' WHERE id = 1",
"UPDATE on Paimon branch 'main' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"DELETE FROM paimon.default.branch_orders$branch_b1 WHERE id = 1",
"DELETE on Paimon branch 'b1' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"DELETE FROM paimon.default.branch_orders$branch_main WHERE id = 1",
"DELETE on Paimon branch 'main' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"MERGE INTO paimon.default.branch_orders$branch_main AS target \
USING (SELECT 1 AS id, 'blocked' AS name) AS source \
ON target.id = source.id \
WHEN MATCHED THEN UPDATE SET name = source.name",
"MERGE INTO on Paimon branch 'main' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"TRUNCATE TABLE paimon.default.branch_orders$branch_main",
"TRUNCATE TABLE on Paimon branch 'main' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"ALTER TABLE paimon.default.branch_orders$branch_main ADD COLUMN blocked INT",
"ALTER TABLE on Paimon branch 'main' is not supported",
)
.await;
assert_sql_error_contains(
&sql_context,
"INSERT OVERWRITE paimon.default.branch_orders$branch_main \
PARTITION (id = 1) SELECT 'blocked'",
"INSERT OVERWRITE on Paimon branch 'main' is not supported",
)
.await;
}
#[tokio::test]
async fn test_branch_partitions_system_table_reads_branch_snapshot() {
let (_tmp, file_catalog) = create_test_env();
let catalog = Arc::new(PartitionCatalog::new(file_catalog.clone()));
let mut sql_context = SQLContext::new();
sql_context
.register_catalog("paimon", catalog.clone())
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.default.branch_partition_orders \
(id INT, name STRING) PARTITIONED BY (id)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.default.branch_partition_orders VALUES (1, 'branch')")
.await
.unwrap()
.collect()
.await
.unwrap();
let identifier = Identifier::new("default", "branch_partition_orders");
let table = file_catalog.get_table(&identifier).await.unwrap();
let snapshot_manager =
SnapshotManager::new(table.file_io().clone(), table.location().to_string());
let snapshot = snapshot_manager
.get_latest_snapshot()
.await
.unwrap()
.unwrap();
let tag_manager = TagManager::new(table.file_io().clone(), table.location().to_string());
tag_manager
.create("partition_branch_base", &snapshot)
.await
.unwrap();
let branch_manager = BranchManager::new(table.file_io().clone(), table.location().to_string());
branch_manager
.create_branch_from_tag("b1", "partition_branch_base")
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.default.branch_partition_orders VALUES (2, 'main')")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(
collect_string_column(
&sql_context,
"SELECT \"partition\" FROM paimon.default.branch_partition_orders$partitions",
"partition",
)
.await,
vec!["id=1".to_string(), "id=2".to_string()]
);
catalog.take_partition_identifiers();
assert_eq!(
collect_string_column(
&sql_context,
"SELECT created_by FROM paimon.default.branch_partition_orders$branch_b1$partitions",
"created_by",
)
.await,
vec!["catalog".to_string()]
);
assert_eq!(
catalog.take_partition_identifiers(),
vec![Identifier::new(
"default",
"branch_partition_orders$branch_b1"
)]
);
assert_eq!(
collect_string_column(
&sql_context,
"SELECT \"partition\" FROM paimon.default.branch_partition_orders$branch_main$partitions",
"partition",
)
.await,
vec!["id=1".to_string(), "id=2".to_string()]
);
assert_eq!(
catalog.take_partition_identifiers(),
vec![Identifier::new("default", "branch_partition_orders")]
);
catalog.set_fail_list_partitions(true);
assert_eq!(
collect_string_column(
&sql_context,
"SELECT \"partition\" FROM paimon.default.branch_partition_orders$branch_b1$partitions",
"partition",
)
.await,
vec!["id=1".to_string()]
);
assert_eq!(
catalog.take_partition_identifiers(),
vec![Identifier::new(
"default",
"branch_partition_orders$branch_b1"
)]
);
assert_eq!(
collect_string_column(
&sql_context,
"SELECT \"partition\" FROM paimon.default.branch_partition_orders$partitions \
VERSION AS OF 'partition_branch_base'",
"partition",
)
.await,
vec!["id=1".to_string()]
);
assert_eq!(
collect_string_column(
&sql_context,
"SELECT \"partition\" FROM paimon.default.branch_partition_orders$branch_b1$partitions \
VERSION AS OF 'partition_branch_base'",
"partition",
)
.await,
vec!["id=1".to_string()]
);
assert!(catalog.take_partition_identifiers().is_empty());
}
#[tokio::test]
async fn test_create_schema() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
let databases = catalog.list_databases().await.unwrap();
assert!(
databases.contains(&"test_db".to_string()),
"Database test_db should exist after CREATE SCHEMA"
);
}
#[tokio::test]
async fn test_drop_schema() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("drop_me", false, Default::default())
.await
.unwrap();
sql_context
.sql("DROP SCHEMA paimon.drop_me CASCADE")
.await
.expect("DROP SCHEMA should succeed");
let databases = catalog.list_databases().await.unwrap();
assert!(
!databases.contains(&"drop_me".to_string()),
"Database drop_me should not exist after DROP SCHEMA"
);
}
#[tokio::test]
async fn test_schema_names_via_catalog_provider() {
let (_tmp, catalog) = create_test_env();
let provider = PaimonCatalogProvider::new(
None,
catalog.clone(),
Default::default(),
Default::default(),
None,
);
catalog
.create_database("db_a", false, Default::default())
.await
.unwrap();
catalog
.create_database("db_b", false, Default::default())
.await
.unwrap();
let names = provider.schema_names();
assert!(names.contains(&"db_a".to_string()));
assert!(names.contains(&"db_b".to_string()));
}
#[tokio::test]
async fn test_create_table() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.mydb.users (
id INT NOT NULL,
name STRING,
age INT,
PRIMARY KEY (id)
)",
)
.await
.expect("CREATE TABLE should succeed");
let tables = catalog.list_tables("mydb").await.unwrap();
assert!(
tables.contains(&"users".to_string()),
"Table users should exist after CREATE TABLE"
);
let table = catalog
.get_table(&Identifier::new("mydb", "users"))
.await
.unwrap();
let schema = table.schema();
assert_eq!(schema.fields().len(), 3);
assert_eq!(schema.primary_keys(), &["id"]);
}
#[tokio::test]
async fn test_create_table_with_blob_type() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.mydb.assets (
id INT NOT NULL,
payload BLOB,
PRIMARY KEY (id)
) WITH ('data-evolution.enabled' = 'true')",
)
.await
.expect("CREATE TABLE with BLOB should succeed");
let table = catalog
.get_table(&Identifier::new("mydb", "assets"))
.await
.unwrap();
let schema = table.schema();
assert_eq!(schema.fields().len(), 2);
assert_eq!(schema.primary_keys(), &["id"]);
assert_eq!(
*schema.fields()[1].data_type(),
DataType::Blob(BlobType::new())
);
}
#[tokio::test]
async fn test_create_table_with_partition() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.mydb.events (
id INT NOT NULL,
name STRING,
dt STRING,
PRIMARY KEY (id, dt)
) PARTITIONED BY (dt)
WITH ('bucket' = '2')",
)
.await
.expect("CREATE TABLE with partition should succeed");
let table = catalog
.get_table(&Identifier::new("mydb", "events"))
.await
.unwrap();
let schema = table.schema();
assert_eq!(schema.partition_keys(), &["dt"]);
assert_eq!(schema.primary_keys(), &["id", "dt"]);
assert_eq!(
schema.options().get("bucket"),
Some(&"2".to_string()),
"Table option 'bucket' should be preserved"
);
}
#[tokio::test]
async fn test_create_table_partitioned_by_rejects_typed_columns() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
let err = sql_context
.sql(
"CREATE TABLE paimon.mydb.events (
id INT NOT NULL,
dt STRING
) PARTITIONED BY (dt STRING)",
)
.await
.expect_err("PARTITIONED BY with typed columns should fail");
let msg = err.to_string();
assert!(
msg.contains("should not specify a type"),
"unexpected error: {msg}"
);
}
#[tokio::test]
async fn test_create_table_if_not_exists() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
let sql = "CREATE TABLE IF NOT EXISTS paimon.mydb.t1 (
id INT NOT NULL
)";
sql_context
.sql(sql)
.await
.expect("First CREATE should succeed");
sql_context
.sql(sql)
.await
.expect("Second CREATE with IF NOT EXISTS should succeed");
}
#[tokio::test]
async fn test_create_external_table_rejected() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
let result = sql_context
.sql(
"CREATE EXTERNAL TABLE paimon.mydb.bad (
id INT NOT NULL
) STORED AS PARQUET
LOCATION '/some/path'",
)
.await;
assert!(result.is_err(), "CREATE EXTERNAL TABLE should be rejected");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("CREATE EXTERNAL TABLE is not supported"),
"Error should mention CREATE EXTERNAL TABLE is not supported, got: {err_msg}"
);
}
#[tokio::test]
async fn test_create_table_with_array_and_map() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.mydb.complex_types (
id INT NOT NULL,
tags ARRAY<STRING>,
props MAP(STRING, INT),
PRIMARY KEY (id)
)",
)
.await
.expect("CREATE TABLE with ARRAY and MAP should succeed");
let table = catalog
.get_table(&Identifier::new("mydb", "complex_types"))
.await
.unwrap();
let schema = table.schema();
assert_eq!(schema.fields().len(), 3);
assert_eq!(schema.primary_keys(), &["id"]);
let tags_field = &schema.fields()[1];
assert_eq!(tags_field.name(), "tags");
assert_eq!(
*tags_field.data_type(),
DataType::Array(ArrayType::new(
DataType::VarChar(VarCharType::string_type())
))
);
let props_field = &schema.fields()[2];
assert_eq!(props_field.name(), "props");
assert_eq!(
*props_field.data_type(),
DataType::Map(MapType::new(
DataType::VarChar(VarCharType::string_type())
.copy_with_nullable(false)
.unwrap(),
DataType::Int(IntType::new()),
))
);
}
#[tokio::test]
async fn test_create_table_with_row_type() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.mydb.row_table (
id INT NOT NULL,
address STRUCT<city STRING, zip INT>,
PRIMARY KEY (id)
)",
)
.await
.expect("CREATE TABLE with STRUCT should succeed");
let table = catalog
.get_table(&Identifier::new("mydb", "row_table"))
.await
.unwrap();
let schema = table.schema();
assert_eq!(schema.fields().len(), 2);
let address_field = &schema.fields()[1];
assert_eq!(address_field.name(), "address");
if let DataType::Row(row) = address_field.data_type() {
assert_eq!(row.fields().len(), 2);
assert_eq!(row.fields()[0].name(), "city");
assert!(matches!(row.fields()[0].data_type(), DataType::VarChar(_)));
assert_eq!(row.fields()[1].name(), "zip");
assert!(matches!(row.fields()[1].data_type(), DataType::Int(_)));
} else {
panic!("expected Row type for address column");
}
}
#[tokio::test]
async fn test_drop_table() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
let schema = paimon::spec::Schema::builder()
.column(
"id",
paimon::spec::DataType::Int(paimon::spec::IntType::new()),
)
.build()
.unwrap();
catalog
.create_table(&Identifier::new("mydb", "to_drop"), schema, false)
.await
.unwrap();
assert!(catalog
.list_tables("mydb")
.await
.unwrap()
.contains(&"to_drop".to_string()));
sql_context
.sql("DROP TABLE paimon.mydb.to_drop")
.await
.expect("DROP TABLE should succeed");
assert!(
!catalog
.list_tables("mydb")
.await
.unwrap()
.contains(&"to_drop".to_string()),
"Table should not exist after DROP TABLE"
);
}
#[tokio::test]
async fn test_alter_table_add_column() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
let schema = paimon::spec::Schema::builder()
.column(
"id",
paimon::spec::DataType::Int(paimon::spec::IntType::new()),
)
.column(
"name",
paimon::spec::DataType::VarChar(paimon::spec::VarCharType::string_type()),
)
.build()
.unwrap();
catalog
.create_table(&Identifier::new("mydb", "alter_test"), schema, false)
.await
.unwrap();
sql_context
.sql("ALTER TABLE paimon.mydb.alter_test ADD COLUMN age INT")
.await
.expect("ALTER TABLE ADD COLUMN should succeed");
let table = catalog
.get_table(&Identifier::new("mydb", "alter_test"))
.await
.unwrap();
let names: Vec<&str> = table.schema().fields().iter().map(|f| f.name()).collect();
assert_eq!(names, vec!["id", "name", "age"]);
}
#[tokio::test]
async fn test_alter_table_rename() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
let schema = paimon::spec::Schema::builder()
.column(
"id",
paimon::spec::DataType::Int(paimon::spec::IntType::new()),
)
.build()
.unwrap();
catalog
.create_table(&Identifier::new("mydb", "old_name"), schema, false)
.await
.unwrap();
sql_context
.sql("ALTER TABLE mydb.old_name RENAME TO new_name")
.await
.expect("ALTER TABLE RENAME should succeed");
let tables = catalog.list_tables("mydb").await.unwrap();
assert!(
!tables.contains(&"old_name".to_string()),
"old_name should not exist after rename"
);
assert!(
tables.contains(&"new_name".to_string()),
"new_name should exist after rename"
);
}
#[tokio::test]
async fn test_ddl_context_delegates_select() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
let schema = paimon::spec::Schema::builder()
.column(
"id",
paimon::spec::DataType::Int(paimon::spec::IntType::new()),
)
.build()
.unwrap();
catalog
.create_table(&Identifier::new("mydb", "t1"), schema, false)
.await
.unwrap();
let df = sql_context
.sql("SELECT * FROM paimon.mydb.t1")
.await
.expect("SELECT should be delegated to DataFusion");
let batches = df.collect().await.expect("SELECT should execute");
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 0, "Empty table should return 0 rows");
}
#[tokio::test]
async fn test_multi_catalog_register_and_query() {
let (_tmp1, catalog1) = create_test_env();
let (_tmp2, catalog2) = create_test_env();
let mut ctx = SQLContext::new();
ctx.register_catalog("cat1", catalog1).await.unwrap();
ctx.register_catalog("cat2", catalog2).await.unwrap();
ctx.sql("CREATE SCHEMA cat1.db1").await.unwrap();
ctx.sql("CREATE SCHEMA cat2.db2").await.unwrap();
ctx.sql("CREATE TABLE cat1.db1.t (id INT NOT NULL, name STRING, PRIMARY KEY (id))")
.await
.unwrap();
ctx.sql("CREATE TABLE cat2.db2.t (id INT NOT NULL, value STRING, PRIMARY KEY (id))")
.await
.unwrap();
ctx.sql("INSERT INTO cat1.db1.t VALUES (1, 'alice')")
.await
.unwrap()
.collect()
.await
.unwrap();
ctx.sql("INSERT INTO cat2.db2.t VALUES (2, 'hello')")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = ctx
.sql("SELECT id, name FROM cat1.db1.t")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
let batches = ctx
.sql("SELECT id, value FROM cat2.db2.t")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
}
#[tokio::test]
async fn test_set_current_catalog() {
let (_tmp1, catalog1) = create_test_env();
let (_tmp2, catalog2) = create_test_env();
let mut ctx = SQLContext::new();
ctx.register_catalog("cat1", catalog1).await.unwrap();
ctx.register_catalog("cat2", catalog2).await.unwrap();
ctx.sql("CREATE SCHEMA cat1.mydb").await.unwrap();
ctx.sql("CREATE TABLE cat1.mydb.t (id INT NOT NULL, PRIMARY KEY (id))")
.await
.unwrap();
ctx.set_current_catalog("cat1").await.unwrap();
ctx.set_current_database("mydb").await.unwrap();
let df = ctx.sql("SELECT * FROM t").await;
assert!(
df.is_ok(),
"Unqualified table should resolve via current catalog/database"
);
let err = ctx.set_current_catalog("nonexistent").await;
assert!(err.is_err());
}
#[tokio::test]
async fn test_set_default_catalog_via_datafusion_config() {
let (_tmp1, catalog1) = create_test_env();
let (_tmp2, catalog2) = create_test_env();
let mut ctx = SQLContext::new();
ctx.register_catalog("cat1", catalog1).await.unwrap();
ctx.register_catalog("cat2", catalog2).await.unwrap();
ctx.sql("CREATE SCHEMA cat2.mydb").await.unwrap();
ctx.sql("CREATE TABLE cat2.mydb.t (id INT NOT NULL, name VARCHAR, PRIMARY KEY (id))")
.await
.unwrap();
ctx.sql("INSERT INTO cat2.mydb.t VALUES (1, 'hello')")
.await
.unwrap()
.collect()
.await
.unwrap();
ctx.sql("SET datafusion.catalog.default_catalog = 'cat2'")
.await
.unwrap();
ctx.set_current_database("mydb").await.unwrap();
let batches = ctx
.sql("SELECT id, name FROM t")
.await
.expect("Unqualified table should resolve via DataFusion default_catalog config")
.collect()
.await
.unwrap();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 1);
ctx.sql("CREATE TABLE mydb.t2 (id INT NOT NULL, PRIMARY KEY (id))")
.await
.expect("CREATE TABLE should resolve against cat2 after SET default_catalog");
let df = ctx.sql("SELECT * FROM cat2.mydb.t2").await;
assert!(
df.is_ok(),
"Table t2 should exist in cat2.mydb after unqualified CREATE TABLE"
);
}
#[tokio::test]
async fn test_first_registered_catalog_is_default() {
let (_tmp, catalog) = create_test_env();
let mut ctx = SQLContext::new();
ctx.register_catalog("paimon", catalog).await.unwrap();
ctx.sql("CREATE SCHEMA paimon.mydb").await.unwrap();
ctx.sql("CREATE TABLE paimon.mydb.t (id INT NOT NULL, PRIMARY KEY (id))")
.await
.unwrap();
ctx.set_current_database("mydb").await.unwrap();
let df = ctx.sql("SELECT * FROM t").await;
assert!(
df.is_ok(),
"First registered catalog should be the default for unqualified queries"
);
}
#[tokio::test]
async fn test_one_part_table_name_uses_current_database() {
let (_tmp, catalog) = create_test_env();
let mut ctx = SQLContext::new();
ctx.register_catalog("paimon", catalog.clone())
.await
.unwrap();
catalog
.create_database("mydb", false, Default::default())
.await
.unwrap();
ctx.set_current_database("mydb").await.unwrap();
ctx.sql(
"CREATE TABLE users (
id INT NOT NULL,
name STRING,
PRIMARY KEY (id)
)",
)
.await
.expect("CREATE TABLE with 1-part name should succeed");
let tables = catalog.list_tables("mydb").await.unwrap();
assert!(
tables.contains(&"users".to_string()),
"Table should be created in the current database"
);
let df = ctx.sql("SELECT * FROM users").await;
assert!(
df.is_ok(),
"SELECT with 1-part name should resolve correctly"
);
}
use datafusion::arrow::array::Int32Array;
use datafusion::arrow::datatypes::{DataType as ArrowDataType, Field as ArrowField};
#[tokio::test]
async fn test_register_temp_table_fully_qualified() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
)
.unwrap();
let mem_table = MemTable::try_new(schema, vec![vec![batch]]).unwrap();
ctx.register_temp_table("paimon.my_db.my_temp", Arc::new(mem_table))
.unwrap();
let batches = ctx
.sql("SELECT * FROM paimon.my_db.my_temp")
.await
.unwrap()
.collect()
.await
.unwrap();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 3);
}
#[tokio::test]
async fn test_register_temp_table_database_qualified() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema = Arc::new(Schema::new(vec![
ArrowField::new("id", ArrowDataType::Int32, false),
ArrowField::new("name", ArrowDataType::Utf8, true),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from(vec![1, 2, 3, 4])),
Arc::new(StringArray::from(vec![
Some("alice"),
Some("bob"),
Some("charlie"),
Some("dave"),
])),
],
)
.unwrap();
let mem_table = MemTable::try_new(schema, vec![vec![batch]]).unwrap();
ctx.register_temp_table("my_db.users", Arc::new(mem_table))
.unwrap();
let batches = ctx
.sql("SELECT id, name FROM paimon.my_db.users WHERE id > 2")
.await
.unwrap()
.collect()
.await
.unwrap();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 2);
}
#[tokio::test]
async fn test_not_filter_pushdown_keeps_sql_null_semantics() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
catalog
.create_database("my_db", false, Default::default())
.await
.unwrap();
ctx.sql("CREATE TABLE paimon.my_db.t (id INT, name STRING)")
.await
.unwrap()
.collect()
.await
.unwrap();
ctx.sql("INSERT INTO paimon.my_db.t VALUES (1, 'one'), (2, 'two'), (NULL, 'nil')")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = ctx
.sql("SELECT id FROM paimon.my_db.t WHERE NOT (id = 1) ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
let values = batches
.iter()
.flat_map(|batch| {
let ids = batch
.column_by_name("id")
.and_then(|column| {
column
.as_any()
.downcast_ref::<datafusion::arrow::array::Int32Array>()
})
.expect("id column");
(0..ids.len()).map(|row| ids.value(row)).collect::<Vec<_>>()
})
.collect::<Vec<_>>();
assert_eq!(values, vec![2]);
}
#[tokio::test]
async fn test_register_temp_table_bare() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
ctx.sql("CREATE DATABASE paimon.my_db").await.unwrap();
ctx.set_current_database("my_db").await.unwrap();
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
)
.unwrap();
let mem_table = MemTable::try_new(schema, vec![vec![batch]]).unwrap();
ctx.register_temp_table("my_temp", Arc::new(mem_table))
.unwrap();
let batches = ctx
.sql("SELECT * FROM paimon.my_db.my_temp")
.await
.unwrap()
.collect()
.await
.unwrap();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 3);
}
#[tokio::test]
async fn test_register_temp_table_unknown_catalog() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let mem_table = MemTable::try_new(schema, vec![vec![]]).unwrap();
let result = ctx.register_temp_table("nonexistent.my_db.t", Arc::new(mem_table));
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Unknown catalog"));
}
#[tokio::test]
async fn test_deregister_temp_table() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let batch =
RecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(vec![1, 2]))]).unwrap();
let mem_table = MemTable::try_new(schema.clone(), vec![vec![batch]]).unwrap();
ctx.register_temp_table("paimon.my_db.my_temp", Arc::new(mem_table))
.unwrap();
ctx.deregister_temp_table("paimon.my_db.my_temp").unwrap();
let result = ctx.sql("SELECT * FROM paimon.my_db.my_temp").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_multiple_temp_tables_in_same_database() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema1 = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let batch1 = RecordBatch::try_new(
schema1.clone(),
vec![Arc::new(Int32Array::from(vec![1, 2]))],
)
.unwrap();
let schema2 = Arc::new(Schema::new(vec![ArrowField::new(
"value",
ArrowDataType::Int32,
false,
)]));
let batch2 = RecordBatch::try_new(
schema2.clone(),
vec![Arc::new(Int32Array::from(vec![10, 20, 30]))],
)
.unwrap();
let mem_table = MemTable::try_new(schema1, vec![vec![batch1]]).unwrap();
ctx.register_temp_table("my_db.t1", Arc::new(mem_table))
.unwrap();
let mem_table = MemTable::try_new(schema2, vec![vec![batch2]]).unwrap();
ctx.register_temp_table("my_db.t2", Arc::new(mem_table))
.unwrap();
let rows1 = ctx
.sql("SELECT * FROM paimon.my_db.t1")
.await
.unwrap()
.collect()
.await
.unwrap()
.iter()
.map(|b| b.num_rows())
.sum::<usize>();
assert_eq!(rows1, 2);
let rows2 = ctx
.sql("SELECT * FROM paimon.my_db.t2")
.await
.unwrap()
.collect()
.await
.unwrap()
.iter()
.map(|b| b.num_rows())
.sum::<usize>();
assert_eq!(rows2, 3);
}
use datafusion::arrow::array::StringArray;
use datafusion::arrow::datatypes::Schema;
use datafusion::arrow::record_batch::RecordBatch;
#[tokio::test]
async fn test_create_temporary_table_as_select() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
ctx.sql("CREATE TEMPORARY TABLE paimon.my_db.source AS SELECT * FROM (VALUES (1, 'alice'), (2, 'bob')) AS t(id, name)")
.await
.unwrap();
let batches = ctx
.sql("SELECT * FROM paimon.my_db.source ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 2);
}
#[tokio::test]
async fn test_drop_temporary_table() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
ctx.sql("CREATE TEMPORARY TABLE paimon.my_db.source AS SELECT * FROM (VALUES (1, 'alice'), (2, 'bob')) AS t(id, name)")
.await
.unwrap();
let batches = ctx
.sql("SELECT * FROM paimon.my_db.source ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 2);
ctx.sql("DROP TEMPORARY TABLE paimon.my_db.source")
.await
.unwrap();
let result = ctx.sql("SELECT * FROM paimon.my_db.source").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_drop_temporary_table_if_exists() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
ctx.sql("DROP TEMPORARY TABLE IF EXISTS paimon.my_db.nonexistent")
.await
.unwrap();
let result = ctx
.sql("DROP TEMPORARY TABLE paimon.my_db.nonexistent")
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_temporary_view_fully_qualified() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema = Arc::new(Schema::new(vec![
ArrowField::new("id", ArrowDataType::Int32, false),
ArrowField::new("name", ArrowDataType::Utf8, true),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from(vec![1, 2])),
Arc::new(StringArray::from(vec![Some("alice"), Some("bob")])),
],
)
.unwrap();
let mem_table = MemTable::try_new(schema, vec![vec![batch]]).unwrap();
ctx.register_temp_table("paimon.my_db.users", Arc::new(mem_table))
.unwrap();
ctx.sql("CREATE TEMPORARY VIEW paimon.my_db.my_view AS SELECT * FROM paimon.my_db.users WHERE id > 0")
.await
.unwrap();
let batches = ctx
.sql("SELECT * FROM paimon.my_db.my_view")
.await
.unwrap()
.collect()
.await
.unwrap();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 2);
}
#[tokio::test]
async fn test_create_temporary_view_database_qualified() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"value",
ArrowDataType::Int32,
false,
)]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vec![10, 20, 30]))],
)
.unwrap();
let mem_table = MemTable::try_new(schema, vec![vec![batch]]).unwrap();
ctx.register_temp_table("paimon.my_db.data", Arc::new(mem_table))
.unwrap();
ctx.sql("CREATE TEMPORARY VIEW my_db.summary AS SELECT value FROM paimon.my_db.data WHERE value > 5")
.await
.unwrap();
let batches = ctx
.sql("SELECT value FROM paimon.my_db.summary WHERE value > 15")
.await
.unwrap()
.collect()
.await
.unwrap();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 2);
}
#[tokio::test]
async fn test_create_temporary_view_bare() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
ctx.sql("CREATE DATABASE paimon.my_db").await.unwrap();
ctx.set_current_database("my_db").await.unwrap();
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vec![100, 200]))],
)
.unwrap();
let mem_table = MemTable::try_new(schema, vec![vec![batch]]).unwrap();
ctx.register_temp_table("my_db.source", Arc::new(mem_table))
.unwrap();
ctx.sql("CREATE TEMPORARY VIEW my_view AS SELECT id FROM paimon.my_db.source")
.await
.unwrap();
let batches = ctx
.sql("SELECT * FROM paimon.my_db.my_view")
.await
.unwrap()
.collect()
.await
.unwrap();
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 2);
}
#[tokio::test]
async fn test_drop_temporary_view() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let batch =
RecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(vec![1, 2]))]).unwrap();
let mem_table = MemTable::try_new(schema, vec![vec![batch]]).unwrap();
ctx.register_temp_table("paimon.my_db.source", Arc::new(mem_table))
.unwrap();
ctx.sql("CREATE TEMPORARY VIEW paimon.my_db.my_view AS SELECT * FROM paimon.my_db.source")
.await
.unwrap();
ctx.sql("DROP TEMPORARY VIEW paimon.my_db.my_view")
.await
.unwrap();
let result = ctx.sql("SELECT * FROM paimon.my_db.my_view").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_multiple_temporary_views_in_same_database() {
let (_tmp, catalog) = create_test_env();
let ctx = create_sql_context(catalog.clone()).await;
let schema1 = Arc::new(Schema::new(vec![ArrowField::new(
"id",
ArrowDataType::Int32,
false,
)]));
let batch1 = RecordBatch::try_new(
schema1.clone(),
vec![Arc::new(Int32Array::from(vec![1, 2]))],
)
.unwrap();
let mem_table = MemTable::try_new(schema1, vec![vec![batch1]]).unwrap();
ctx.register_temp_table("paimon.my_db.t1", Arc::new(mem_table))
.unwrap();
let schema2 = Arc::new(Schema::new(vec![ArrowField::new(
"name",
ArrowDataType::Utf8,
true,
)]));
let batch2 = RecordBatch::try_new(
schema2.clone(),
vec![Arc::new(StringArray::from(vec![
Some("x"),
Some("y"),
Some("z"),
]))],
)
.unwrap();
let mem_table = MemTable::try_new(schema2, vec![vec![batch2]]).unwrap();
ctx.register_temp_table("paimon.my_db.t2", Arc::new(mem_table))
.unwrap();
ctx.sql("CREATE TEMPORARY VIEW my_db.v1 AS SELECT id FROM paimon.my_db.t1")
.await
.unwrap();
ctx.sql("CREATE TEMPORARY VIEW my_db.v2 AS SELECT name FROM paimon.my_db.t2")
.await
.unwrap();
let rows1 = ctx
.sql("SELECT * FROM paimon.my_db.v1")
.await
.unwrap()
.collect()
.await
.unwrap()
.iter()
.map(|b| b.num_rows())
.sum::<usize>();
assert_eq!(rows1, 2);
let rows2 = ctx
.sql("SELECT * FROM paimon.my_db.v2")
.await
.unwrap()
.collect()
.await
.unwrap()
.iter()
.map(|b| b.num_rows())
.sum::<usize>();
assert_eq!(rows2, 3);
}
async fn collect_definition(sql_context: &SQLContext, table_ref: &str) -> String {
let rows = sql_context
.sql(&format!("SHOW CREATE TABLE {}", table_ref))
.await
.expect("SHOW CREATE TABLE should plan")
.collect()
.await
.expect("SHOW CREATE TABLE should execute");
assert_eq!(
rows.len(),
1,
"SHOW CREATE TABLE should return exactly one row"
);
let row = &rows[0];
assert_eq!(
row.num_rows(),
1,
"SHOW CREATE TABLE should return exactly one row"
);
let val = row.column(3); let def = val
.as_any()
.downcast_ref::<datafusion::arrow::array::StringArray>()
.expect("definition column should be a StringArray")
.value(0);
def.to_string()
}
#[tokio::test]
async fn test_show_create_table_simple() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
sql_context
.sql("CREATE TABLE paimon.test_db.t (id INT, name VARCHAR(100))")
.await
.expect("CREATE TABLE should succeed");
let definition = collect_definition(&sql_context, "paimon.test_db.t").await;
assert!(
definition.contains("CREATE TABLE \"test_db\".\"t\""),
"definition should start with CREATE TABLE \"test_db\".\"t\", got: {definition}"
);
assert!(
definition.contains("\"id\" INT"),
"definition should contain `\"id\" INT`, got: {definition}"
);
assert!(
definition.contains("\"name\" VARCHAR("),
"definition should contain `\"name\" VARCHAR(...)`, got: {definition}"
);
}
#[tokio::test]
async fn test_show_create_table_with_primary_key() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
sql_context
.sql("CREATE TABLE paimon.test_db.t (id INT NOT NULL, name VARCHAR, PRIMARY KEY (id))")
.await
.expect("CREATE TABLE should succeed");
let definition = collect_definition(&sql_context, "paimon.test_db.t").await;
assert!(
definition.contains("PRIMARY KEY (\"id\")"),
"definition should contain PRIMARY KEY (\"id\"), got: {definition}"
);
}
#[tokio::test]
async fn test_show_create_table_with_partition_and_options() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
sql_context
.sql(
"CREATE TABLE paimon.test_db.t (id INT, name VARCHAR, pt INT) \
PARTITIONED BY (pt) WITH ('bucket' = '4', 'file.format' = 'parquet')",
)
.await
.expect("CREATE TABLE should succeed");
let definition = collect_definition(&sql_context, "paimon.test_db.t").await;
assert!(
definition.contains("PARTITIONED BY (\"pt\")"),
"definition should contain PARTITIONED BY (\"pt\"), got: {definition}"
);
assert!(
definition.contains("'bucket' = '4'"),
"definition should contain bucket option, got: {definition}"
);
assert!(
definition.contains("'file.format' = 'parquet'"),
"definition should contain file.format option, got: {definition}"
);
}
#[tokio::test]
async fn test_show_create_table_excludes_session_dynamic_options() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
sql_context
.sql(
"CREATE TABLE paimon.test_db.t (id INT, name VARCHAR, pt INT) \
PARTITIONED BY (pt) WITH ('file.format' = 'parquet')",
)
.await
.expect("CREATE TABLE should succeed");
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 'one', 1)")
.await
.expect("INSERT should plan")
.collect()
.await
.expect("INSERT should execute");
sql_context
.sql("CALL sys.create_tag(table => 'test_db.t', tag => 'before_age')")
.await
.expect("CREATE TAG should succeed");
sql_context
.sql("ALTER TABLE paimon.test_db.t ADD COLUMN age INT")
.await
.expect("ALTER TABLE should succeed");
sql_context
.sql("SET 'paimon.scan.version' = 'before_age'")
.await
.expect("SET scan.version should succeed");
sql_context
.sql("SET 'paimon.blob-as-descriptor' = 'true'")
.await
.expect("SET blob-as-descriptor should succeed");
let definition = collect_definition(&sql_context, "paimon.test_db.t").await;
assert!(
definition.contains("'file.format' = 'parquet'"),
"definition should keep persisted table options, got: {definition}"
);
assert!(
definition.contains("\"age\" INT"),
"definition should use current persisted schema, got: {definition}"
);
for dynamic_option in ["scan.version", "blob-as-descriptor"] {
assert!(
!definition.contains(dynamic_option),
"definition should not contain session dynamic option {dynamic_option}, got: {definition}"
);
}
}
#[tokio::test]
async fn test_dynamic_scan_ignores_current_show_create_unsupported_type() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
sql_context
.sql("CREATE TABLE paimon.test_db.t (id INT)")
.await
.expect("CREATE TABLE should succeed");
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1)")
.await
.expect("INSERT should plan")
.collect()
.await
.expect("INSERT should execute");
sql_context
.sql("CALL sys.create_tag(table => 'test_db.t', tag => 'before_time')")
.await
.expect("CREATE TAG should succeed");
let identifier = Identifier::new("test_db", "t");
catalog
.alter_table(
&identifier,
vec![SchemaChange::add_column(
"unsupported_col".to_string(),
DataType::Time(TimeType::new(3).unwrap()),
)],
false,
)
.await
.expect("ALTER TABLE should add unsupported SHOW CREATE type");
sql_context
.sql("SET 'paimon.scan.version' = 'before_time'")
.await
.expect("SET scan.version should succeed");
let rows = sql_context
.sql("SELECT * FROM paimon.test_db.t")
.await
.expect("dynamic scan should plan with historical schema")
.collect()
.await
.expect("dynamic scan should execute");
assert_eq!(rows[0].schema().fields().len(), 1);
assert_eq!(rows[0].schema().field(0).name(), "id");
let row_count: usize = rows.iter().map(|batch| batch.num_rows()).sum();
assert_eq!(row_count, 1);
}
#[tokio::test]
async fn test_show_create_table_various_types() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
sql_context
.sql(
"CREATE TABLE paimon.test_db.t (\
a BOOLEAN, \
b TINYINT, \
c SMALLINT, \
d BIGINT, \
e DECIMAL(10, 2), \
f DOUBLE, \
g FLOAT, \
h DATE, \
i TIMESTAMP(3), \
j BLOB) \
WITH ('data-evolution.enabled' = 'true')",
)
.await
.expect("CREATE TABLE should succeed");
let definition = collect_definition(&sql_context, "paimon.test_db.t").await;
for needle in [
"\"a\" BOOLEAN",
"\"b\" TINYINT",
"\"c\" SMALLINT",
"\"d\" BIGINT",
"\"e\" DECIMAL(10, 2)",
"\"f\" DOUBLE",
"\"g\" FLOAT",
"\"h\" DATE",
"\"i\" TIMESTAMP(3)",
"\"j\" BLOB",
] {
assert!(
definition.contains(needle),
"definition should contain `{needle}`, got: {definition}"
);
}
}
fn assert_schema_equivalent(left: &paimon::spec::TableSchema, right: &paimon::spec::TableSchema) {
assert_eq!(
left.fields().len(),
right.fields().len(),
"field count mismatch\nleft (original): {:?}\nright (recreated): {:?}",
left.fields(),
right.fields()
);
for (lf, rf) in left.fields().iter().zip(right.fields().iter()) {
assert_eq!(
lf.id(),
rf.id(),
"field id mismatch for `{}`: {} vs {}",
lf.name(),
lf.id(),
rf.id()
);
assert_eq!(
lf.name(),
rf.name(),
"field name mismatch: `{}` vs `{}`",
lf.name(),
rf.name()
);
assert_eq!(
lf.data_type(),
rf.data_type(),
"field type mismatch for `{}`: {:?} vs {:?}",
lf.name(),
lf.data_type(),
rf.data_type()
);
}
assert_eq!(
left.primary_keys(),
right.primary_keys(),
"primary keys mismatch: {:?} vs {:?}",
left.primary_keys(),
right.primary_keys()
);
assert_eq!(
left.partition_keys(),
right.partition_keys(),
"partition keys mismatch: {:?} vs {:?}",
left.partition_keys(),
right.partition_keys()
);
}
#[tokio::test]
async fn test_show_create_table_round_trip() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
sql_context
.sql(
"CREATE TABLE paimon.test_db.t1 (\
id INT NOT NULL, \
name VARCHAR NOT NULL, \
tags ARRAY<STRING>, \
props MAP(INT, VARCHAR), \
addr STRUCT<city VARCHAR, zip VARCHAR>, \
meta STRUCT<kv MAP(STRING, STRING), tags ARRAY<INT>>, \
PRIMARY KEY (id)) \
PARTITIONED BY (name) \
WITH ('bucket' = '2', 'file.format' = 'parquet')",
)
.await
.expect("CREATE TABLE should succeed");
let identifier = Identifier::new("test_db", "t1");
let original = catalog.get_table(&identifier).await.expect("table exists");
let original_schema = original.schema().clone();
let definition = collect_definition(&sql_context, "paimon.test_db.t1").await;
assert!(
definition.starts_with("CREATE TABLE \"test_db\".\"t1\""),
"definition should start with `CREATE TABLE \"test_db\".\"t1\"`, got: {definition}"
);
catalog
.drop_table(&identifier, false)
.await
.expect("drop should succeed");
sql_context
.sql(&definition)
.await
.expect("DDL should re-execute")
.collect()
.await
.expect("DDL should execute");
let recreated = catalog
.get_table(&identifier)
.await
.expect("recreated table exists");
assert_schema_equivalent(&original_schema, recreated.schema());
}
#[tokio::test]
async fn test_show_create_table_round_trip_with_quoted_identifiers_and_options() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
let identifier = Identifier::new("test_db", "select");
let schema = paimon::spec::Schema::builder()
.column("group", DataType::Int(IntType::with_nullable(false)))
.column("order", DataType::Int(IntType::with_nullable(false)))
.column("a\"b,c", DataType::Int(IntType::new()))
.column(
"nested",
DataType::Row(paimon::spec::RowType::new(vec![
paimon::spec::DataField::new(
0,
"from".to_string(),
DataType::VarChar(VarCharType::new(VarCharType::MAX_LENGTH).unwrap()),
),
])),
)
.column(
"ts_ltz",
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(3).unwrap()),
)
.column("fixed_char", DataType::Char(CharType::new(7).unwrap()))
.column(
"bounded_varchar",
DataType::VarChar(VarCharType::new(42).unwrap()),
)
.column(
"fixed_binary",
DataType::Binary(BinaryType::new(8).unwrap()),
)
.column(
"bounded_varbinary",
DataType::VarBinary(VarBinaryType::try_new(true, 32).unwrap()),
)
.primary_key(vec!["group", "order"])
.partition_keys(vec!["a\"b,c"])
.option("comment", "Bob's table")
.build()
.expect("schema should build");
catalog
.create_table(&identifier, schema, false)
.await
.expect("table should be created");
let original = catalog.get_table(&identifier).await.expect("table exists");
let original_schema = original.schema().clone();
let definition = collect_definition(&sql_context, "paimon.test_db.\"select\"").await;
assert!(
definition.starts_with("CREATE TABLE \"test_db\".\"select\""),
"definition should quote table identifiers, got: {definition}"
);
assert!(
definition.contains("\"order\" INT NOT NULL"),
"definition should quote column identifiers, got: {definition}"
);
assert!(
definition.contains("PRIMARY KEY (\"group\", \"order\")"),
"definition should quote primary key identifiers, got: {definition}"
);
assert!(
definition.contains("\"a\"\"b,c\" INT"),
"definition should escape quoted column identifiers, got: {definition}"
);
assert!(
definition.contains("PARTITIONED BY (\"a\"\"b,c\")"),
"definition should escape quoted partition identifiers, got: {definition}"
);
assert!(
definition.contains("STRUCT<\"from\" VARCHAR"),
"definition should quote nested struct field identifiers, got: {definition}"
);
assert!(
definition.contains("'comment' = 'Bob''s table'"),
"definition should escape string literals, got: {definition}"
);
assert!(
definition.contains("\"ts_ltz\" TIMESTAMP(3) WITH TIME ZONE"),
"definition should render TIMESTAMP WITH TIME ZONE for LTZ, got: {definition}"
);
assert!(
definition.contains("\"fixed_char\" CHAR(7)"),
"definition should preserve CHAR length, got: {definition}"
);
assert!(
definition.contains("\"bounded_varchar\" VARCHAR(42)"),
"definition should preserve VARCHAR length, got: {definition}"
);
assert!(
definition.contains("\"fixed_binary\" BINARY(8)"),
"definition should preserve BINARY length, got: {definition}"
);
assert!(
definition.contains("\"bounded_varbinary\" VARBINARY(32)"),
"definition should preserve VARBINARY length, got: {definition}"
);
catalog
.drop_table(&identifier, false)
.await
.expect("drop should succeed");
sql_context
.sql(&definition)
.await
.expect("DDL should re-execute")
.collect()
.await
.expect("DDL should execute");
let recreated = catalog
.get_table(&identifier)
.await
.expect("recreated table exists");
assert_schema_equivalent(&original_schema, recreated.schema());
}
#[tokio::test]
async fn test_show_create_table_rejects_non_round_trippable_types() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.expect("CREATE SCHEMA should succeed");
for (table_name, data_type, type_name) in [
("time_t", DataType::Time(TimeType::new(3).unwrap()), "TIME"),
(
"multiset_t",
DataType::Multiset(MultisetType::new(DataType::Int(IntType::new()))),
"MULTISET",
),
(
"vector_t",
DataType::Vector(VectorType::new(4, DataType::Float(FloatType::new())).unwrap()),
"VECTOR",
),
] {
let identifier = Identifier::new("test_db", table_name);
let schema = paimon::spec::Schema::builder()
.column("unsupported_col", data_type)
.build()
.expect("schema should build");
catalog
.create_table(&identifier, schema, false)
.await
.expect("table should be created");
sql_context
.sql("SET 'paimon.blob-as-descriptor' = 'true'")
.await
.expect("SET blob-as-descriptor should succeed");
let err = sql_context
.sql(&format!("SHOW CREATE TABLE paimon.test_db.{table_name}"))
.await
.expect_err("SHOW CREATE TABLE should reject unsupported type");
assert!(
err.to_string().contains(type_name),
"error should mention {type_name}, got: {err}"
);
}
}