mod common;
use common::{assert_sql_error, collect_id_name, exec, row_count, setup_sql_context};
async fn setup_table_with_snapshots() -> (tempfile::TempDir, paimon_datafusion::SQLContext) {
let (tmp, sql_context) = setup_sql_context().await;
exec(
&sql_context,
"CREATE TABLE paimon.test_db.t1 (id INT, name VARCHAR(100), PRIMARY KEY (id))",
)
.await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.t1 VALUES (1, 'alice')",
)
.await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.t1 VALUES (2, 'bob')",
)
.await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.t1 VALUES (3, 'charlie')",
)
.await;
(tmp, sql_context)
}
#[tokio::test]
async fn test_create_tag() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v1')",
)
.await;
let count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`t1$tags` WHERE tag_name = 'v1'",
)
.await;
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_create_tag_with_snapshot_id() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v1', snapshot_id => '1')",
)
.await;
let count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`t1$tags` WHERE tag_name = 'v1'",
)
.await;
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_create_lumina_index_requires_index_column() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
assert_sql_error(
&sql_context,
"CALL sys.create_lumina_index(table => 'test_db.t1')",
"Missing required argument: 'index_column'",
)
.await;
}
#[tokio::test]
async fn test_create_lumina_index_rejects_invalid_index_type() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
assert_sql_error(
&sql_context,
"CALL sys.create_lumina_index(table => 'test_db.t1', index_column => 'name', index_type => 'btree')",
"Unsupported Lumina index type: btree",
)
.await;
}
#[tokio::test]
async fn test_create_lumina_index_rejects_invalid_options() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
assert_sql_error(
&sql_context,
"CALL sys.create_lumina_index(table => 'test_db.t1', index_column => 'name', options => 'lumina.index.dimension')",
"Expected comma-separated key=value pairs",
)
.await;
}
async fn setup_btree_global_index_table(
table_name: &str,
) -> (tempfile::TempDir, paimon_datafusion::SQLContext) {
let (tmp, sql_context) = setup_sql_context().await;
exec(
&sql_context,
&format!(
"CREATE TABLE paimon.test_db.{table_name} (id INT, name VARCHAR(100)) WITH (\
'row-tracking.enabled' = 'true',\
'data-evolution.enabled' = 'true',\
'global-index.enabled' = 'true',\
'sorted-index.records-per-range' = '10'\
)"
),
)
.await;
(tmp, sql_context)
}
#[tokio::test]
async fn test_create_global_index_requires_index_column() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_missing_col").await;
assert_sql_error(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.btree_missing_col')",
"Missing required argument: 'index_column'",
)
.await;
}
#[tokio::test]
async fn test_create_global_index_rejects_unsupported_index_type() {
let (_tmp, sql_context) = setup_btree_global_index_table("global_index_bad_type").await;
assert_sql_error(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.global_index_bad_type', index_column => 'id', index_type => 'full-text')",
"only supports index_type => 'btree', 'bitmap'",
)
.await;
}
#[tokio::test]
async fn test_create_global_index_rejects_options() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_options").await;
assert_sql_error(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.btree_options', index_column => 'id', options => 'x=y')",
"options are not supported",
)
.await;
}
#[tokio::test]
async fn test_create_global_index_builds_btree_and_filter_reads() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_build").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.btree_build (id, name) VALUES (1, 'alice'), (2, 'bob'), (3, 'carol')",
)
.await;
exec(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.btree_build', index_column => 'id', index_type => 'btree')",
)
.await;
let index_count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`btree_build$table_indexes` \
WHERE index_type = 'btree' AND row_range_start = 0 AND row_range_end = 2 \
AND index_field_name = 'id'",
)
.await;
assert_eq!(index_count, 1);
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_build WHERE id = 2",
)
.await;
assert_eq!(rows, vec![(2, "bob".to_string())]);
}
#[tokio::test]
async fn test_create_global_index_btree_string_fallback_scan_reads() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_string_fallback").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.btree_string_fallback (id, name) VALUES \
(1, 'alice'), (2, 'alpine'), (3, 'bob'), (4, 'carol'), (5, 'malice')",
)
.await;
exec(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.btree_string_fallback', index_column => 'name', index_type => 'btree')",
)
.await;
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_string_fallback WHERE name LIKE 'a%c_'",
)
.await;
assert_eq!(rows, vec![(1, "alice".to_string())]);
}
#[tokio::test]
async fn test_create_global_index_builds_bitmap_with_java_format() {
let (_tmp, sql_context) = setup_btree_global_index_table("bitmap_build").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.bitmap_build (id, name) VALUES (1, 'alice'), (2, 'bob'), (3, 'alice')",
)
.await;
exec(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.bitmap_build', index_column => 'name', index_type => 'bitmap')",
)
.await;
let index_count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`bitmap_build$table_indexes` \
WHERE index_type = 'bitmap' AND row_range_start = 0 AND row_range_end = 2 \
AND index_field_name = 'name'",
)
.await;
assert_eq!(index_count, 1);
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.bitmap_build WHERE name = 'alice'",
)
.await;
assert_eq!(
rows,
vec![(1, "alice".to_string()), (3, "alice".to_string())]
);
let contains_rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.bitmap_build WHERE name LIKE '%lic%'",
)
.await;
assert_eq!(
contains_rows,
vec![(1, "alice".to_string()), (3, "alice".to_string())]
);
}
#[tokio::test]
async fn test_drop_global_index_removes_btree_and_reads_fallback() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_drop").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.btree_drop (id, name) VALUES (1, 'alice'), (2, 'bob')",
)
.await;
exec(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.btree_drop', index_column => 'id')",
)
.await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.btree_drop (id, name) VALUES (3, 'carol')",
)
.await;
let fast_rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_drop WHERE id >= 2",
)
.await;
assert_eq!(fast_rows, vec![(2, "bob".to_string())]);
exec(
&sql_context,
"CALL sys.drop_global_index(table => 'test_db.btree_drop', index_column => 'id', index_type => 'btree')",
)
.await;
let index_count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`btree_drop$table_indexes` \
WHERE index_type = 'btree' AND index_field_name = 'id'",
)
.await;
assert_eq!(index_count, 0);
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_drop WHERE id >= 2",
)
.await;
assert_eq!(rows, vec![(2, "bob".to_string()), (3, "carol".to_string())]);
}
#[tokio::test]
async fn test_drop_global_index_removes_bitmap() {
let (_tmp, sql_context) = setup_btree_global_index_table("bitmap_drop").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.bitmap_drop (id, name) VALUES (1, 'alice'), (2, 'bob')",
)
.await;
exec(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.bitmap_drop', index_column => 'name', index_type => 'bitmap')",
)
.await;
exec(
&sql_context,
"CALL sys.drop_global_index(table => 'test_db.bitmap_drop', index_column => 'name', index_type => 'bitmap')",
)
.await;
let index_count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`bitmap_drop$table_indexes` \
WHERE index_type = 'bitmap' AND index_field_name = 'name'",
)
.await;
assert_eq!(index_count, 0);
}
#[tokio::test]
async fn test_drop_global_index_requires_index_column() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_drop_missing_col").await;
assert_sql_error(
&sql_context,
"CALL sys.drop_global_index(table => 'test_db.btree_drop_missing_col')",
"Missing required argument: 'index_column'",
)
.await;
}
#[tokio::test]
async fn test_drop_global_index_rejects_unsupported_index_type() {
let (_tmp, sql_context) = setup_btree_global_index_table("global_index_drop_bad_type").await;
assert_sql_error(
&sql_context,
"CALL sys.drop_global_index(table => 'test_db.global_index_drop_bad_type', index_column => 'id', index_type => 'full-text')",
"unsupported global index type",
)
.await;
}
#[tokio::test]
async fn test_drop_global_index_accepts_lumina_type() {
let (_tmp, sql_context) = setup_btree_global_index_table("lumina_drop_accepted").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.lumina_drop_accepted (id, name) VALUES (1, 'alice')",
)
.await;
exec(
&sql_context,
"CALL sys.drop_global_index(table => 'test_db.lumina_drop_accepted', index_column => 'id', index_type => 'lumina')",
)
.await;
}
#[tokio::test]
async fn test_drop_global_index_accepts_vindex_type() {
let (_tmp, sql_context) = setup_btree_global_index_table("vindex_drop_accepted").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.vindex_drop_accepted (id, name) VALUES (1, 'alice')",
)
.await;
exec(
&sql_context,
"CALL sys.drop_global_index(table => 'test_db.vindex_drop_accepted', index_column => 'id', index_type => 'ivf-pq')",
)
.await;
}
#[tokio::test]
async fn test_drop_global_index_without_match_succeeds() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_drop_no_match").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.btree_drop_no_match (id, name) VALUES (1, 'alice')",
)
.await;
exec(
&sql_context,
"CALL sys.drop_global_index(table => 'test_db.btree_drop_no_match', index_column => 'id')",
)
.await;
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_drop_no_match WHERE id = 1",
)
.await;
assert_eq!(rows, vec![(1, "alice".to_string())]);
}
#[tokio::test]
async fn test_create_global_index_fast_full_detail_after_append() {
let (_tmp, sql_context) = setup_btree_global_index_table("btree_coverage").await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.btree_coverage (id, name) VALUES (1, 'alice'), (2, 'bob')",
)
.await;
exec(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.btree_coverage', index_column => 'id')",
)
.await;
exec(
&sql_context,
"INSERT INTO paimon.test_db.btree_coverage (id, name) VALUES (3, 'carol')",
)
.await;
let fast_rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_coverage WHERE id >= 2",
)
.await;
assert_eq!(fast_rows, vec![(2, "bob".to_string())]);
exec(
&sql_context,
"SET 'paimon.global-index.search-mode' = 'full'",
)
.await;
let full_rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_coverage WHERE id >= 2",
)
.await;
assert_eq!(
full_rows,
vec![(2, "bob".to_string()), (3, "carol".to_string())]
);
exec(
&sql_context,
"SET 'paimon.global-index.search-mode' = 'detail'",
)
.await;
let detail_rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.btree_coverage WHERE id >= 2",
)
.await;
assert_eq!(
detail_rows,
vec![(2, "bob".to_string()), (3, "carol".to_string())]
);
}
#[tokio::test]
async fn test_create_tag_already_exists() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v1')",
)
.await;
let result = sql_context
.sql("CALL sys.create_tag(table => 'test_db.t1', tag => 'v1')")
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
}
#[tokio::test]
async fn test_delete_tag() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v1')",
)
.await;
exec(
&sql_context,
"CALL sys.delete_tag(table => 'test_db.t1', tag => 'v1')",
)
.await;
let count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`t1$tags` WHERE tag_name = 'v1'",
)
.await;
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_delete_multiple_tags() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v1')",
)
.await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v2', snapshot_id => '1')",
)
.await;
exec(
&sql_context,
"CALL sys.delete_tag(table => 'test_db.t1', tag => 'v1,v2')",
)
.await;
let count = row_count(&sql_context, "SELECT * FROM paimon.test_db.`t1$tags`").await;
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_rollback_to_snapshot() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.rollback_to(table => 'test_db.t1', snapshot_id => '1')",
)
.await;
let count = row_count(&sql_context, "SELECT * FROM paimon.test_db.t1").await;
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_rollback_to_tag() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v1', snapshot_id => '1')",
)
.await;
exec(
&sql_context,
"CALL sys.rollback_to(table => 'test_db.t1', tag => 'v1')",
)
.await;
let count = row_count(&sql_context, "SELECT * FROM paimon.test_db.t1").await;
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_rollback_to_timestamp() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
let batches = sql_context
.sql("SELECT snapshot_id, commit_time FROM paimon.test_db.`t1$snapshots` ORDER BY snapshot_id")
.await
.unwrap()
.collect()
.await
.unwrap();
let snap1_time = batches[0]
.column_by_name("commit_time")
.unwrap()
.as_any()
.downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
.unwrap()
.value(0);
exec(
&sql_context,
&format!(
"CALL sys.rollback_to_timestamp(table => 'test_db.t1', timestamp => '{snap1_time}')"
),
)
.await;
let count = row_count(&sql_context, "SELECT * FROM paimon.test_db.t1").await;
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_create_tag_from_timestamp() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
let batches = sql_context
.sql("SELECT snapshot_id, commit_time FROM paimon.test_db.`t1$snapshots` ORDER BY snapshot_id")
.await
.unwrap()
.collect()
.await
.unwrap();
let snap2_time = batches[0]
.column_by_name("commit_time")
.unwrap()
.as_any()
.downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
.unwrap()
.value(1);
exec(
&sql_context,
&format!(
"CALL sys.create_tag_from_timestamp(table => 'test_db.t1', tag => 'ts_tag', timestamp => '{snap2_time}')"
),
)
.await;
let count = row_count(
&sql_context,
"SELECT * FROM paimon.test_db.`t1$tags` WHERE tag_name = 'ts_tag'",
)
.await;
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_rollback_cleans_newer_tags() {
let (_tmp, sql_context) = setup_table_with_snapshots().await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v2', snapshot_id => '2')",
)
.await;
exec(
&sql_context,
"CALL sys.create_tag(table => 'test_db.t1', tag => 'v3', snapshot_id => '3')",
)
.await;
exec(
&sql_context,
"CALL sys.rollback_to(table => 'test_db.t1', snapshot_id => '1')",
)
.await;
let count = row_count(&sql_context, "SELECT * FROM paimon.test_db.`t1$tags`").await;
assert_eq!(count, 0);
}