mod common;
use common::{
collect_id_name, collect_id_value, collect_int_int_str, create_sql_context, create_test_env,
row_count, setup_sql_context, string_value,
};
use datafusion::arrow::array::{Array, Int32Array, Int64Array, Int8Array, RecordBatch};
use datafusion::arrow::datatypes::{
DataType as ArrowDataType, Field as ArrowField, Schema as ArrowSchema,
};
use paimon::catalog::Identifier;
use paimon::Catalog;
use paimon_datafusion::PaimonTableProvider;
use std::collections::HashMap;
use std::sync::Arc;
#[tokio::test]
async fn test_pk_basic_write_read() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t1 (
id INT NOT NULL, name STRING,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t1 VALUES (1, 'alice'), (2, 'bob'), (3, 'carol')")
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.t1 ORDER BY id",
)
.await;
assert_eq!(
rows,
vec![
(1, "alice".to_string()),
(2, "bob".to_string()),
(3, "carol".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_partial_update_fixed_bucket_e2e() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_partial_update (
id INT NOT NULL, v_int INT, v_str STRING,
PRIMARY KEY (id)
) WITH ('bucket' = '1', 'merge-engine' = 'partial-update')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_partial_update VALUES
(1, 10, 'old-1'),
(2, 20, 'old-2')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_partial_update VALUES
(1, CAST(NULL AS INT), 'new-1'),
(2, 200, CAST(NULL AS STRING)),
(3, 30, CAST(NULL AS STRING))",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_partial_update VALUES
(1, 111, CAST(NULL AS STRING))",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, v_int, v_str FROM paimon.test_db.t_partial_update ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let ints = batch
.column_by_name("v_int")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let strs = batch.column_by_name("v_str").unwrap();
for i in 0..batch.num_rows() {
rows.push((
ids.value(i),
if ints.is_null(i) {
None
} else {
Some(ints.value(i))
},
if strs.is_null(i) {
None
} else {
Some(string_value(strs.as_ref(), i).to_string())
},
));
}
}
assert_eq!(
rows,
vec![
(1, Some(111), Some("new-1".to_string())),
(2, Some(200), Some("old-2".to_string())),
(3, Some(30), None),
]
);
}
#[tokio::test]
async fn test_pk_partial_update_sequence_group_aggregation_read_e2e() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_partial_update_aggregation (
id INT NOT NULL,
version INT,
amount INT,
tag STRING,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'partial-update'
)",
)
.await
.unwrap();
for values in [
"(1, 10, 10, 'b'), (2, 5, 3, 'x')",
"(1, 9, 20, 'a'), (2, 4, 4, 'w')",
"(1, 11, 5, 'c')",
] {
sql_context
.sql(&format!(
"INSERT INTO paimon.test_db.t_partial_update_aggregation VALUES {values}"
))
.await
.unwrap()
.collect()
.await
.unwrap();
}
let table = catalog
.get_table(&Identifier::new("test_db", "t_partial_update_aggregation"))
.await
.unwrap()
.copy_with_options(HashMap::from([
(
"fields.version.sequence-group".to_string(),
"amount,tag".to_string(),
),
(
"fields.amount.aggregate-function".to_string(),
"sum".to_string(),
),
(
"fields.tag.aggregate-function".to_string(),
"listagg".to_string(),
),
]));
let provider = PaimonTableProvider::try_new(table).unwrap();
sql_context
.register_temp_table(
"paimon.test_db.t_partial_update_aggregation",
Arc::new(provider),
)
.unwrap();
let batches = sql_context
.sql(
"SELECT id
FROM paimon.test_db.t_partial_update_aggregation
WHERE amount = 7 AND tag = 'w,x'",
)
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 1);
let batch = &batches[0];
assert_eq!(
batch
.column_by_name("id")
.unwrap()
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.value(0),
2
);
assert_eq!(
batch
.schema()
.fields()
.iter()
.map(|field| field.name().as_str())
.collect::<Vec<_>>(),
vec!["id"]
);
}
#[tokio::test]
async fn test_pk_partial_update_ignore_delete_alias_e2e() {
let (_tmp, catalog) = create_test_env();
let sql_context = create_sql_context(catalog.clone()).await;
sql_context
.sql("CREATE SCHEMA paimon.test_db")
.await
.unwrap();
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_partial_update_ignore_delete (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'partial-update',
'partial-update.ignore-delete' = 'true'
)",
)
.await
.unwrap();
let table = catalog
.get_table(&Identifier::new(
"test_db",
"t_partial_update_ignore_delete",
))
.await
.unwrap();
let batch = RecordBatch::try_new(
Arc::new(ArrowSchema::new(vec![
ArrowField::new("id", ArrowDataType::Int32, false),
ArrowField::new("value", ArrowDataType::Int32, true),
ArrowField::new("_VALUE_KIND", ArrowDataType::Int8, false),
])),
vec![
Arc::new(Int32Array::from(vec![1, 1, 2, 1])),
Arc::new(Int32Array::from(vec![
Some(10),
Some(999),
Some(200),
Some(20),
])),
Arc::new(Int8Array::from(vec![0, 3, 1, 2])),
],
)
.unwrap();
let write_builder = table.new_write_builder();
let mut write = write_builder.new_write().unwrap();
write.write_arrow_batch(&batch).await.unwrap();
let messages = write.prepare_commit().await.unwrap();
write_builder.new_commit().commit(messages).await.unwrap();
assert_eq!(
collect_id_value(
&sql_context,
"SELECT id, value
FROM paimon.test_db.t_partial_update_ignore_delete
ORDER BY id",
)
.await,
vec![(1, 20)]
);
}
#[tokio::test]
async fn test_pk_partial_update_merges_within_single_commit() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_pu_flush_merge (
id INT NOT NULL, v_int INT, v_str STRING,
PRIMARY KEY (id)
) WITH ('bucket' = '1', 'merge-engine' = 'partial-update')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_pu_flush_merge VALUES
(1, 10, CAST(NULL AS STRING)),
(1, CAST(NULL AS INT), 'hello'),
(1, 100, CAST(NULL AS STRING)),
(2, 200, 'world')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, v_int, v_str FROM paimon.test_db.t_pu_flush_merge ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(
collect_int_int_str(&batches),
vec![(1, 100, "hello".to_string()), (2, 200, "world".to_string())]
);
let batches = sql_context
.sql("SELECT COUNT(*) FROM paimon.test_db.t_pu_flush_merge")
.await
.unwrap()
.collect()
.await
.unwrap();
let count = batches[0]
.column(0)
.as_any()
.downcast_ref::<datafusion::arrow::array::Int64Array>()
.unwrap()
.value(0);
assert_eq!(count, 2, "COUNT(*) must count merged rows");
}
#[tokio::test]
async fn test_pk_dedup_within_single_commit() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_dedup (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_dedup VALUES (1, 10), (2, 20), (1, 100), (2, 200)")
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_dedup ORDER BY id",
)
.await;
assert_eq!(rows, vec![(1, 100), (2, 200)]);
}
#[tokio::test]
async fn test_pk_dedup_across_commits() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_cross (
id INT NOT NULL, name STRING,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_cross VALUES (1, 'alice'), (2, 'bob'), (3, 'carol')")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_cross VALUES (1, 'alice-v2'), (3, 'carol-v2'), (4, 'dave')")
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.t_cross ORDER BY id",
)
.await;
assert_eq!(
rows,
vec![
(1, "alice-v2".to_string()),
(2, "bob".to_string()),
(3, "carol-v2".to_string()),
(4, "dave".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_three_commits() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_three (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_three VALUES (1, 10), (2, 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_three VALUES (2, 200), (3, 30)")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_three VALUES (1, 100), (3, 300), (4, 40)")
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_three ORDER BY id",
)
.await;
assert_eq!(rows, vec![(1, 100), (2, 200), (3, 300), (4, 40)]);
}
#[tokio::test]
async fn test_pk_partitioned_write_read() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_part (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_part VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-01', 2, 'bob'), \
('2024-01-02', 1, 'carol'), ('2024-01-02', 2, 'dave')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.t_part ORDER BY id, name",
)
.await;
assert_eq!(
rows,
vec![
(1, "alice".to_string()),
(1, "carol".to_string()),
(2, "bob".to_string()),
(2, "dave".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_partitioned_dedup_across_commits() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_part_dedup (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_part_dedup VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-01', 2, 'bob'), \
('2024-01-02', 1, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_part_dedup VALUES \
('2024-01-01', 1, 'alice-v2'), ('2024-01-02', 2, 'dave')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, id, name FROM paimon.test_db.t_part_dedup ORDER BY dt, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
ids.value(i),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![
("2024-01-01".to_string(), 1, "alice-v2".to_string()),
("2024-01-01".to_string(), 2, "bob".to_string()),
("2024-01-02".to_string(), 1, "carol".to_string()),
("2024-01-02".to_string(), 2, "dave".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_partitioned_filter() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_part_filter (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_part_filter VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-01', 2, 'bob'), \
('2024-01-02', 3, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.t_part_filter WHERE dt = '2024-01-01' ORDER BY id",
)
.await;
assert_eq!(rows, vec![(1, "alice".to_string()), (2, "bob".to_string())]);
}
#[tokio::test]
async fn test_pk_multi_bucket() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_mbucket (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '4')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_mbucket VALUES \
(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60), (7, 70), (8, 80)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_mbucket VALUES (2, 200), (5, 500), (8, 800)")
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_mbucket ORDER BY id",
)
.await;
assert_eq!(
rows,
vec![
(1, 10),
(2, 200),
(3, 30),
(4, 40),
(5, 500),
(6, 60),
(7, 70),
(8, 800),
]
);
}
#[tokio::test]
async fn test_pk_column_projection() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_proj (
id INT NOT NULL, name STRING, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_proj VALUES (1, 'alice', 10), (2, 'bob', 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_proj VALUES (1, 'alice-v2', 100)")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT name FROM paimon.test_db.t_proj ORDER BY name")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut names = Vec::new();
for batch in &batches {
let arr = batch.column(0);
for i in 0..batch.num_rows() {
names.push(string_value(arr.as_ref(), i).to_string());
}
}
names.sort();
assert_eq!(names, vec!["alice-v2", "bob"]);
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_proj ORDER BY id",
)
.await;
assert_eq!(rows, vec![(1, 100), (2, 20)]);
}
#[tokio::test]
async fn test_pk_sequence_field() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_seqf (
id INT NOT NULL, version INT, name STRING,
PRIMARY KEY (id)
) WITH ('bucket' = '1', 'sequence.field' = 'version')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_seqf VALUES (1, 2, 'alice-v2'), (2, 1, 'bob-v1')")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_seqf VALUES (1, 1, 'alice-v1'), (2, 2, 'bob-v2')")
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.t_seqf ORDER BY id",
)
.await;
assert_eq!(
rows,
vec![
(1, "alice-v2".to_string()), (2, "bob-v2".to_string()), ]
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_partitioned() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_overwrite (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_overwrite VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-01', 2, 'bob'), \
('2024-01-02', 3, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT OVERWRITE paimon.test_db.t_overwrite VALUES ('2024-01-01', 10, 'new_alice')")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, id, name FROM paimon.test_db.t_overwrite ORDER BY dt, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
ids.value(i),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![
("2024-01-01".to_string(), 10, "new_alice".to_string()),
("2024-01-02".to_string(), 3, "carol".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_with_partition_clause() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_ow_part (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_ow_part VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-01', 2, 'bob'), \
('2024-01-02', 3, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT OVERWRITE paimon.test_db.t_ow_part PARTITION (dt = '2024-01-01') \
VALUES (10, 'new_alice'), (20, 'new_bob')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, id, name FROM paimon.test_db.t_ow_part ORDER BY dt, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
ids.value(i),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![
("2024-01-01".to_string(), 10, "new_alice".to_string()),
("2024-01-01".to_string(), 20, "new_bob".to_string()),
("2024-01-02".to_string(), 3, "carol".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_partial_partition_clause() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_multi_part (
dt STRING, region STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, region, id)
) PARTITIONED BY (dt, region)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_multi_part VALUES \
('2024-01-01', 'us', 1, 'alice'), \
('2024-01-01', 'eu', 2, 'bob'), \
('2024-01-02', 'us', 3, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT OVERWRITE paimon.test_db.t_multi_part PARTITION (dt = '2024-01-01') \
VALUES ('us', 10, 'new_alice')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, region, id, name FROM paimon.test_db.t_multi_part ORDER BY dt, region, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let regions = batch.column_by_name("region").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
string_value(regions.as_ref(), i).to_string(),
ids.value(i),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![
(
"2024-01-01".to_string(),
"us".to_string(),
10,
"new_alice".to_string()
),
(
"2024-01-02".to_string(),
"us".to_string(),
3,
"carol".to_string()
),
]
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_partition_truncate() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_trunc (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_trunc VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-01', 2, 'bob'), \
('2024-01-02', 3, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT OVERWRITE paimon.test_db.t_trunc PARTITION (dt = '2024-01-01') \
SELECT id, name FROM paimon.test_db.t_trunc WHERE false",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, id, name FROM paimon.test_db.t_trunc ORDER BY dt, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
ids.value(i),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![("2024-01-02".to_string(), 3, "carol".to_string()),]
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_partition_non_partition_column_error() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_err (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
let result = sql_context
.sql(
"INSERT OVERWRITE paimon.test_db.t_err PARTITION (name = 'alice') \
VALUES (1)",
)
.await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("not a partition column"),
"Expected 'not a partition column' error, got: {err_msg}"
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_dynamic_partition_preserves_other_partitions() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_dyn (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_dyn VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-02', 2, 'bob')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT OVERWRITE paimon.test_db.t_dyn PARTITION (dt) \
VALUES ('2024-01-01', 10, 'new_alice')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, id, name FROM paimon.test_db.t_dyn ORDER BY dt, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
ids.value(i),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![
("2024-01-01".to_string(), 10, "new_alice".to_string()),
("2024-01-02".to_string(), 2, "bob".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_empty_source_wrong_columns_error() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_empty_err (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_empty_err VALUES \
('2024-01-01', 1, 'alice')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let result = sql_context
.sql(
"INSERT OVERWRITE paimon.test_db.t_empty_err PARTITION (dt = '2024-01-01') \
SELECT id FROM paimon.test_db.t_empty_err WHERE false",
)
.await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("expected 2 non-partition columns"),
"Expected column count mismatch error, got: {err_msg}"
);
}
#[tokio::test]
async fn test_pk_insert_overwrite_with_after_columns_reorder() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_reorder (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT OVERWRITE paimon.test_db.t_reorder (name, id) PARTITION (dt = '2024-01-01') \
VALUES ('alice', 1), ('bob', 2)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, id, name FROM paimon.test_db.t_reorder ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
ids.value(i),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![
("2024-01-01".to_string(), 1, "alice".to_string()),
("2024-01-01".to_string(), 2, "bob".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_composite_key() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_composite (
region STRING NOT NULL, id INT NOT NULL, value INT,
PRIMARY KEY (region, id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_composite VALUES \
('us', 1, 10), ('eu', 1, 20), ('us', 2, 30)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_composite VALUES ('us', 1, 100)")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT region, id, value FROM paimon.test_db.t_composite ORDER BY region, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let regions = batch.column_by_name("region").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let vals = batch
.column_by_name("value")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(regions.as_ref(), i).to_string(),
ids.value(i),
vals.value(i),
));
}
}
assert_eq!(
rows,
vec![
("eu".to_string(), 1, 20), ("us".to_string(), 1, 100), ("us".to_string(), 2, 30), ]
);
}
#[tokio::test]
async fn test_pk_empty_table_read() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_empty (
id INT NOT NULL, name STRING,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
let count = row_count(&sql_context, "SELECT id, name FROM paimon.test_db.t_empty").await;
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_pk_large_batch_dedup() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_large (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
let mut values1 = Vec::new();
let mut values2 = Vec::new();
for i in 1..=100 {
values1.push(format!("({i}, {i})")); values2.push(format!("({i}, {})", i * 10)); }
sql_context
.sql(&format!(
"INSERT INTO paimon.test_db.t_large VALUES {}",
values1.join(", ")
))
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(&format!(
"INSERT INTO paimon.test_db.t_large VALUES {}",
values2.join(", ")
))
.await
.unwrap()
.collect()
.await
.unwrap();
let count = row_count(&sql_context, "SELECT * FROM paimon.test_db.t_large").await;
assert_eq!(count, 100, "Dedup should keep exactly 100 unique keys");
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_large WHERE id IN (1, 50, 100) ORDER BY id",
)
.await;
assert_eq!(rows, vec![(1, 10), (50, 500), (100, 1000)]);
}
#[tokio::test]
async fn test_pk_partitioned_multi_bucket() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_part_mb (
dt STRING, id INT NOT NULL, value INT,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '2')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_part_mb VALUES \
('2024-01-01', 1, 10), ('2024-01-01', 2, 20), \
('2024-01-01', 3, 30), ('2024-01-01', 4, 40), \
('2024-01-02', 1, 100), ('2024-01-02', 2, 200)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_part_mb VALUES \
('2024-01-01', 2, 222), ('2024-01-02', 1, 111)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT dt, id, value FROM paimon.test_db.t_part_mb ORDER BY dt, id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let dts = batch.column_by_name("dt").unwrap();
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let vals = batch
.column_by_name("value")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(dts.as_ref(), i).to_string(),
ids.value(i),
vals.value(i),
));
}
}
assert_eq!(
rows,
vec![
("2024-01-01".to_string(), 1, 10),
("2024-01-01".to_string(), 2, 222),
("2024-01-01".to_string(), 3, 30),
("2024-01-01".to_string(), 4, 40),
("2024-01-02".to_string(), 1, 111),
("2024-01-02".to_string(), 2, 200),
]
);
}
#[tokio::test]
async fn test_pk_input_changelog_write_read() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_changelog (
id INT NOT NULL, name STRING,
PRIMARY KEY (id)
) WITH ('bucket' = '1', 'changelog-producer' = 'input')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_changelog VALUES
(1, 'alice'), (1, 'bob'), (2, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_name(
&sql_context,
"SELECT id, name FROM paimon.test_db.t_changelog ORDER BY id",
)
.await;
assert_eq!(rows, vec![(1, "bob".to_string()), (2, "carol".to_string())]);
}
#[tokio::test]
async fn test_pk_string_key() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_strpk (
code STRING NOT NULL, name STRING,
PRIMARY KEY (code)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_strpk VALUES \
('A001', 'alice'), ('B002', 'bob'), ('C003', 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_strpk VALUES ('A001', 'alice-v2')")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT code, name FROM paimon.test_db.t_strpk ORDER BY code")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let codes = batch.column_by_name("code").unwrap();
let names = batch.column_by_name("name").unwrap();
for i in 0..batch.num_rows() {
rows.push((
string_value(codes.as_ref(), i).to_string(),
string_value(names.as_ref(), i).to_string(),
));
}
}
assert_eq!(
rows,
vec![
("A001".to_string(), "alice-v2".to_string()),
("B002".to_string(), "bob".to_string()),
("C003".to_string(), "carol".to_string()),
]
);
}
#[tokio::test]
async fn test_pk_multiple_value_columns() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_multi_val (
id INT NOT NULL, col_a INT, col_b STRING, col_c INT,
PRIMARY KEY (id)
) WITH ('bucket' = '1')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_multi_val VALUES (1, 10, 'x', 100), (2, 20, 'y', 200)")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_multi_val VALUES (1, 11, 'xx', 111)")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, col_a, col_b, col_c FROM paimon.test_db.t_multi_val ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows = Vec::new();
for batch in &batches {
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let as_ = batch
.column_by_name("col_a")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let bs = batch.column_by_name("col_b").unwrap();
let cs = batch
.column_by_name("col_c")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
for i in 0..batch.num_rows() {
rows.push((
ids.value(i),
as_.value(i),
string_value(bs.as_ref(), i).to_string(),
cs.value(i),
));
}
}
assert_eq!(
rows,
vec![
(1, 11, "xx".to_string(), 111), (2, 20, "y".to_string(), 200), ]
);
}
#[tokio::test]
async fn test_pk_first_row_insert_overwrite() {
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 failed");
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_fr_ow (
dt STRING, id INT NOT NULL, name STRING,
PRIMARY KEY (dt, id)
) PARTITIONED BY (dt)
WITH ('bucket' = '1', 'merge-engine' = 'first-row')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_fr_ow VALUES \
('2024-01-01', 1, 'alice'), ('2024-01-01', 2, 'bob'), \
('2024-01-02', 3, 'carol')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let table = catalog
.get_table(&Identifier::new("test_db", "t_fr_ow"))
.await
.unwrap();
let plan = table
.new_read_builder()
.new_scan()
.with_scan_all_files()
.plan()
.await
.unwrap();
let file_count: usize = plan.splits().iter().map(|s| s.data_files().len()).sum();
assert_eq!(
file_count, 2,
"After INSERT: 2 level-0 files (one per partition)"
);
sql_context
.sql("INSERT OVERWRITE paimon.test_db.t_fr_ow VALUES ('2024-01-01', 10, 'new_alice')")
.await
.unwrap()
.collect()
.await
.unwrap();
let table = catalog
.get_table(&Identifier::new("test_db", "t_fr_ow"))
.await
.unwrap();
let plan = table
.new_read_builder()
.new_scan()
.with_scan_all_files()
.plan()
.await
.unwrap();
let file_count: usize = plan.splits().iter().map(|s| s.data_files().len()).sum();
assert_eq!(
file_count, 2,
"After OVERWRITE: 2 files (1 replaced for 2024-01-01 + 1 unchanged for 2024-01-02)"
);
sql_context
.sql("INSERT OVERWRITE paimon.test_db.t_fr_ow VALUES ('2024-01-01', 20, 'newer_alice')")
.await
.unwrap()
.collect()
.await
.unwrap();
let table = catalog
.get_table(&Identifier::new("test_db", "t_fr_ow"))
.await
.unwrap();
let plan = table
.new_read_builder()
.new_scan()
.with_scan_all_files()
.plan()
.await
.unwrap();
let file_count: usize = plan.splits().iter().map(|s| s.data_files().len()).sum();
assert_eq!(
file_count, 2,
"After second OVERWRITE: still 2 files (no stale level-0 files accumulated)"
);
}
#[tokio::test]
async fn test_postpone_write_invisible_to_select() {
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 failed");
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_postpone (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '-2')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_postpone VALUES (1, 10), (2, 20), (3, 30)")
.await
.unwrap()
.collect()
.await
.unwrap();
let table = catalog
.get_table(&Identifier::new("test_db", "t_postpone"))
.await
.unwrap();
let plan = table
.new_read_builder()
.new_scan()
.with_scan_all_files()
.plan()
.await
.unwrap();
let file_count: usize = plan.splits().iter().map(|s| s.data_files().len()).sum();
assert_eq!(file_count, 1, "scan_all_files should find 1 postpone file");
let count = row_count(&sql_context, "SELECT * FROM paimon.test_db.t_postpone").await;
assert_eq!(count, 0, "SELECT should return 0 rows for postpone table");
}
#[tokio::test]
async fn test_postpone_insert_overwrite() {
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 failed");
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_postpone_ow (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '-2')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_postpone_ow VALUES (1, 10), (2, 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
let table = catalog
.get_table(&Identifier::new("test_db", "t_postpone_ow"))
.await
.unwrap();
let plan = table
.new_read_builder()
.new_scan()
.with_scan_all_files()
.plan()
.await
.unwrap();
let file_count: usize = plan.splits().iter().map(|s| s.data_files().len()).sum();
assert_eq!(file_count, 1, "After INSERT: 1 postpone file");
sql_context
.sql("INSERT OVERWRITE paimon.test_db.t_postpone_ow VALUES (3, 30)")
.await
.unwrap()
.collect()
.await
.unwrap();
let table = catalog
.get_table(&Identifier::new("test_db", "t_postpone_ow"))
.await
.unwrap();
let plan = table
.new_read_builder()
.new_scan()
.with_scan_all_files()
.plan()
.await
.unwrap();
let file_count: usize = plan.splits().iter().map(|s| s.data_files().len()).sum();
assert_eq!(
file_count, 1,
"After OVERWRITE: only 1 new file (old file deleted)"
);
}
#[tokio::test]
async fn test_pk_partitioned_fixed_bucket_predicate_query() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_bk_pred (
pt STRING, id INT NOT NULL, value INT,
PRIMARY KEY (pt, id)
) PARTITIONED BY (pt)
WITH ('bucket' = '2')",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_bk_pred VALUES \
('a', 1, 10), ('a', 2, 20), ('b', 3, 30), ('b', 4, 40)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_bk_pred WHERE pt = 'a' AND id = 1",
)
.await;
assert_eq!(rows, vec![(1, 10)], "Predicate query must find the row");
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_bk_pred WHERE pt = 'b' AND id = 4",
)
.await;
assert_eq!(rows, vec![(4, 40)], "Predicate query must find the row");
}
#[tokio::test]
async fn test_pk_dv_deduplicate_read_no_error() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_dv_dedup (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH ('bucket' = '1', 'deletion-vectors.enabled' = 'true')",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_dv_dedup VALUES (1, 10), (2, 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_dv_dedup VALUES (2, 200), (3, 30)")
.await
.unwrap()
.collect()
.await
.unwrap();
let result = sql_context
.sql("SELECT * FROM paimon.test_db.t_dv_dedup")
.await
.unwrap()
.collect()
.await;
assert!(
result.is_ok(),
"DV + Deduplicate read should not error: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_pk_dedup_merges_across_tiny_splits() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_tiny_split (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'source.split.target-size' = '1b',
'source.split.open-file-cost' = '1b'
)",
)
.await
.unwrap();
for value in [10, 20, 30] {
sql_context
.sql(&format!(
"INSERT INTO paimon.test_db.t_tiny_split VALUES (1, {value})"
))
.await
.unwrap()
.collect()
.await
.unwrap();
}
let rows = collect_id_value(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_tiny_split",
)
.await;
assert_eq!(rows, vec![(1, 30)]);
}
#[tokio::test]
async fn test_pk_limit_not_starved_by_merge_splits() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_tiny_split_limit (
id INT NOT NULL, value INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'source.split.target-size' = '1b',
'source.split.open-file-cost' = '1b'
)",
)
.await
.unwrap();
for value in [10, 20, 30] {
sql_context
.sql(&format!(
"INSERT INTO paimon.test_db.t_tiny_split_limit VALUES (1, {value})"
))
.await
.unwrap()
.collect()
.await
.unwrap();
}
sql_context
.sql("INSERT INTO paimon.test_db.t_tiny_split_limit VALUES (2, 200)")
.await
.unwrap()
.collect()
.await
.unwrap();
let returned = row_count(
&sql_context,
"SELECT id, value FROM paimon.test_db.t_tiny_split_limit LIMIT 2",
)
.await;
assert_eq!(returned, 2, "LIMIT 2 must yield 2 rows");
let batches = sql_context
.sql("SELECT COUNT(*) FROM paimon.test_db.t_tiny_split_limit")
.await
.unwrap()
.collect()
.await
.unwrap();
let count = batches[0]
.column(0)
.as_any()
.downcast_ref::<datafusion::arrow::array::Int64Array>()
.unwrap()
.value(0);
assert_eq!(count, 2, "COUNT(*) must count merged rows");
}
#[tokio::test]
async fn test_pk_partial_update_count_and_limit_see_merged_rows() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_pu_count (
id INT NOT NULL, v_int INT, v_str STRING,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'partial-update',
'source.split.target-size' = '1b',
'source.split.open-file-cost' = '1b'
)",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_pu_count VALUES
(1, 10, CAST(NULL AS STRING)),
(1, CAST(NULL AS INT), 'hello'),
(1, 100, CAST(NULL AS STRING)),
(2, 200, 'world')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT COUNT(*) FROM paimon.test_db.t_pu_count")
.await
.unwrap()
.collect()
.await
.unwrap();
let count = batches[0]
.column(0)
.as_any()
.downcast_ref::<datafusion::arrow::array::Int64Array>()
.unwrap()
.value(0);
assert_eq!(count, 2, "COUNT(*) must count merged rows");
let returned = row_count(
&sql_context,
"SELECT id, v_int FROM paimon.test_db.t_pu_count LIMIT 2",
)
.await;
assert_eq!(returned, 2, "LIMIT 2 must yield 2 rows");
}
#[tokio::test]
async fn test_pk_partial_update_merges_across_tiny_splits() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_tiny_split_pu (
id INT NOT NULL, v_int INT, v_str STRING,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'partial-update',
'source.split.target-size' = '1b',
'source.split.open-file-cost' = '1b'
)",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_tiny_split_pu VALUES (1, 10, CAST(NULL AS STRING))")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_tiny_split_pu VALUES (1, CAST(NULL AS INT), 'hello')")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_tiny_split_pu VALUES (1, 100, CAST(NULL AS STRING))")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, v_int, v_str FROM paimon.test_db.t_tiny_split_pu")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(
collect_int_int_str(&batches),
vec![(1, 100, "hello".to_string())]
);
}
#[tokio::test]
async fn test_pk_aggregation_sum_and_listagg_fixed_multi_bucket_e2e() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_sum (
id INT NOT NULL, amount INT, tag STRING,
PRIMARY KEY (id)
) WITH (
'bucket' = '4',
'merge-engine' = 'aggregation',
'fields.amount.aggregate-function' = 'sum',
'fields.tag.aggregate-function' = 'listagg',
'fields.tag.list-agg-delimiter' = '|'
)",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_agg_sum VALUES \
(1, 10, 'a'), (2, 20, 'x')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_agg_sum VALUES \
(1, 5, 'b'), (2, 7, CAST(NULL AS STRING)), (3, 99, 'solo')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, amount, tag FROM paimon.test_db.t_agg_sum ORDER BY id")
.await
.unwrap()
.collect()
.await
.unwrap();
let mut rows: Vec<(i32, Option<i32>, Option<String>)> = Vec::new();
for batch in &batches {
let ids = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let amounts = batch
.column_by_name("amount")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let tags = batch.column_by_name("tag").unwrap();
for i in 0..batch.num_rows() {
rows.push((
ids.value(i),
if amounts.is_null(i) {
None
} else {
Some(amounts.value(i))
},
if tags.is_null(i) {
None
} else {
Some(string_value(tags.as_ref(), i).to_string())
},
));
}
}
assert_eq!(
rows,
vec![
(1, Some(15), Some("a|b".to_string())),
(2, Some(27), Some("x".to_string())),
(3, Some(99), Some("solo".to_string())),
]
);
}
#[tokio::test]
async fn test_pk_aggregation_default_function() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_default (
id INT NOT NULL, a INT, b STRING,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.default-aggregate-function' = 'last_non_null_value'
)",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_agg_default VALUES (1, 10, 'old')")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_agg_default VALUES \
(1, CAST(NULL AS INT), 'new')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_agg_default VALUES (1, 99, CAST(NULL AS STRING))")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, a, b FROM paimon.test_db.t_agg_default")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
let batch = &batches[0];
let id = batch
.column_by_name("id")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let a = batch
.column_by_name("a")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let b = batch.column_by_name("b").unwrap();
assert_eq!(id.value(0), 1);
assert_eq!(a.value(0), 99); assert_eq!(string_value(b.as_ref(), 0), "new"); }
#[tokio::test]
async fn test_pk_aggregation_mixed_aggregators() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_mixed (
id INT NOT NULL, total INT, peak INT, ok BOOLEAN, first_seen STRING,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.total.aggregate-function' = 'sum',
'fields.peak.aggregate-function' = 'max',
'fields.ok.aggregate-function' = 'bool_or',
'fields.first_seen.aggregate-function' = 'first_non_null_value'
)",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_agg_mixed VALUES \
(1, 10, 5, false, 'a'), \
(1, 5, 8, true, 'b'), \
(1, 3, 7, false, 'c')",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, total, peak, ok, first_seen FROM paimon.test_db.t_agg_mixed")
.await
.unwrap()
.collect()
.await
.unwrap();
use datafusion::arrow::array::BooleanArray;
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
let batch = &batches[0];
let total = batch
.column_by_name("total")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let peak = batch
.column_by_name("peak")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let ok = batch
.column_by_name("ok")
.and_then(|c| c.as_any().downcast_ref::<BooleanArray>())
.unwrap();
let first_seen = batch.column_by_name("first_seen").unwrap();
assert_eq!(total.value(0), 18); assert_eq!(peak.value(0), 8); assert!(ok.value(0)); assert_eq!(string_value(first_seen.as_ref(), 0), "a"); }
#[tokio::test]
async fn test_pk_aggregation_sequence_field_forced_last_value() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_seq (
id INT NOT NULL, amount INT, ts INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'sequence.field' = 'ts',
'fields.amount.aggregate-function' = 'sum',
'fields.default-aggregate-function' = 'sum'
)",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_agg_seq VALUES \
(1, 10, 100), (1, 20, 250)",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, amount, ts FROM paimon.test_db.t_agg_seq")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
let batch = &batches[0];
let amount = batch
.column_by_name("amount")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
let ts = batch
.column_by_name("ts")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
assert_eq!(amount.value(0), 30); assert_eq!(ts.value(0), 250); }
#[tokio::test]
async fn test_pk_aggregation_rejects_delete() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_del (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.amount.aggregate-function' = 'sum'
)",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_agg_del VALUES (1, 10), (2, 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
let plan_result = sql_context
.sql("DELETE FROM paimon.test_db.t_agg_del WHERE id = 1")
.await;
match plan_result {
Ok(df) => {
let exec = df.collect().await;
assert!(exec.is_err(), "DELETE on aggregation table should fail");
let msg = format!("{:?}", exec.err().unwrap());
assert!(
msg.contains("aggregation")
|| msg.contains("DELETE")
|| msg.contains("UPDATE_BEFORE"),
"expected aggregation engine to reject DELETE at execution, got {msg}"
);
}
Err(e) => {
let msg = format!("{e:?}");
assert!(
msg.contains("aggregation")
|| msg.contains("DELETE")
|| msg.contains("Unsupported"),
"expected aggregation engine to reject DELETE at planning, got {msg}"
);
}
}
}
#[tokio::test]
async fn test_pk_aggregation_default_fallback_is_last_non_null_value() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_fallback (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation'
)",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_agg_fallback VALUES (1, 10), (1, 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, amount FROM paimon.test_db.t_agg_fallback")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
let amount = batches[0]
.column_by_name("amount")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
assert_eq!(amount.value(0), 20); }
#[tokio::test]
async fn test_pk_aggregation_rejects_unsupported_options_at_create() {
let (_tmp, sql_context) = setup_sql_context().await;
let err = sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_bad (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.amount.aggregate-function' = 'sum',
'fields.amount.ignore-retract' = 'true'
)",
)
.await
.expect_err("CREATE TABLE with ignore-retract should fail in basic mode");
let msg = format!("{err:?}");
assert!(
msg.contains("ignore-retract"),
"expected create-time rejection to mention ignore-retract, got {msg}"
);
}
#[tokio::test]
async fn test_pk_aggregation_create_table_rejects_unknown_field() {
let (_tmp, sql_context) = setup_sql_context().await;
let err = sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_typo (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.amout.aggregate-function' = 'sum'
)",
)
.await
.expect_err("CREATE TABLE with unknown field should fail at create time");
let msg = format!("{err:?}");
assert!(
msg.contains("amout") && msg.contains("amount"),
"expected unknown-field error to name the typo and surface the available \
columns, got {msg}"
);
}
#[tokio::test]
async fn test_pk_aggregation_create_table_rejects_unknown_function() {
let (_tmp, sql_context) = setup_sql_context().await;
let err = sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_badfn (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.amount.aggregate-function' = 'sume'
)",
)
.await
.expect_err("CREATE TABLE with unknown function should fail at create time");
let msg = format!("{err:?}");
assert!(
msg.contains("sume"),
"expected unknown-function error to surface the bad name, got {msg}"
);
}
#[tokio::test]
async fn test_pk_aggregation_create_table_rejects_incompatible_type() {
let (_tmp, sql_context) = setup_sql_context().await;
let err = sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_badtype (
id INT NOT NULL, tag STRING,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.tag.aggregate-function' = 'sum'
)",
)
.await
.expect_err("CREATE TABLE with incompatible function/type should fail at create time");
let msg = format!("{err:?}");
assert!(
msg.contains("sum") && msg.contains("tag"),
"expected incompatible-type error to mention the function and field, got {msg}"
);
}
#[tokio::test]
async fn test_pk_aggregation_create_table_rejects_sequence_field_function() {
let (_tmp, sql_context) = setup_sql_context().await;
let err = sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_seq_bad (
id INT NOT NULL, amount INT, v INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'sequence.field' = 'amount',
'fields.amount.aggregate-function' = 'listagg',
'fields.v.aggregate-function' = 'sum'
)",
)
.await
.expect_err("CREATE TABLE with aggregation on sequence.field should fail");
let msg = format!("{err:?}");
assert!(
msg.contains("sequence field") && msg.contains("amount"),
"expected sequence-field aggregation rejection, got {msg}"
);
}
#[tokio::test]
async fn test_pk_aggregation_create_table_accepts_ignored_function_on_pk() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_pk_ok (
id INT NOT NULL, v INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.id.aggregate-function' = 'listagg',
'fields.v.aggregate-function' = 'sum'
)",
)
.await
.expect("CREATE TABLE with runtime-ignored PK function/type pair should succeed");
}
#[tokio::test]
async fn test_pk_aggregation_sum_all_null_emits_null_for_nullable_column() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_null (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.amount.aggregate-function' = 'sum'
)",
)
.await
.unwrap();
sql_context
.sql(
"INSERT INTO paimon.test_db.t_agg_null VALUES \
(1, CAST(NULL AS INT)), (1, CAST(NULL AS INT))",
)
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT id, amount FROM paimon.test_db.t_agg_null")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
let amount = batches[0]
.column_by_name("amount")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
assert!(amount.is_null(0), "sum over all-NULL group should be NULL");
}
#[tokio::test]
async fn test_pk_aggregation_routing_uses_kv_path() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_route (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.amount.aggregate-function' = 'sum'
)",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_agg_route VALUES (1, 10), (1, 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
let n = row_count(&sql_context, "SELECT * FROM paimon.test_db.t_agg_route").await;
assert_eq!(
n, 1,
"aggregation table must collapse same-PK rows; got {n} rows which suggests \
to_arrow fell through to read_raw"
);
let batches = sql_context
.sql("SELECT amount FROM paimon.test_db.t_agg_route")
.await
.unwrap()
.collect()
.await
.unwrap();
let amount = batches[0]
.column_by_name("amount")
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
.unwrap();
assert_eq!(amount.value(0), 30);
}
#[tokio::test]
async fn test_pk_aggregation_count_star_empty_projection() {
let (_tmp, sql_context) = setup_sql_context().await;
sql_context
.sql(
"CREATE TABLE paimon.test_db.t_agg_count (
id INT NOT NULL, amount INT,
PRIMARY KEY (id)
) WITH (
'bucket' = '1',
'merge-engine' = 'aggregation',
'fields.amount.aggregate-function' = 'sum'
)",
)
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_agg_count VALUES (1, 10), (2, 20)")
.await
.unwrap()
.collect()
.await
.unwrap();
sql_context
.sql("INSERT INTO paimon.test_db.t_agg_count VALUES (1, 5), (2, 7), (3, 99)")
.await
.unwrap()
.collect()
.await
.unwrap();
let batches = sql_context
.sql("SELECT COUNT(*) FROM paimon.test_db.t_agg_count")
.await
.unwrap()
.collect()
.await
.unwrap();
let count = batches[0]
.column(0)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap();
assert_eq!(
count.value(0),
3,
"COUNT(*) over an aggregation table must reflect merged rows; a 0/wrong \
count means the empty-projection reorder dropped the row count"
);
}