Skip to content

Commit 5617a77

Browse files
authored
test: cover filters for mixed-format schema evolution (#390)
1 parent 1491ee0 commit 5617a77

1 file changed

Lines changed: 142 additions & 30 deletions

File tree

crates/integration_tests/tests/read_tables.rs

Lines changed: 142 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,21 @@ fn extract_id_name(batches: &[RecordBatch]) -> Vec<(i32, String)> {
144144
rows
145145
}
146146

147+
fn extract_ids(batches: &[RecordBatch]) -> Vec<i32> {
148+
let mut ids = Vec::new();
149+
for batch in batches {
150+
let id = batch
151+
.column_by_name("id")
152+
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
153+
.expect("id");
154+
for i in 0..batch.num_rows() {
155+
ids.push(id.value(i));
156+
}
157+
}
158+
ids.sort();
159+
ids
160+
}
161+
147162
fn extract_id_name_dt(batches: &[RecordBatch]) -> Vec<(i32, String, String)> {
148163
let mut rows = Vec::new();
149164
for batch in batches {
@@ -1194,8 +1209,12 @@ fn assert_plan_has_multiple_schema_ids(plan: &Plan, table_name: &str) {
11941209
/// Old Parquet files lack the new column; newer ORC/Avro files contain it.
11951210
#[tokio::test]
11961211
async fn test_read_format_schema_evolution_add_column() {
1212+
use paimon::spec::{Datum, PredicateBuilder};
1213+
11971214
let table_name = "format_schema_evolution_add_column";
1198-
let (plan, batches) = scan_and_read_with_fs_catalog(table_name, None).await;
1215+
let catalog = create_file_system_catalog();
1216+
let table = get_table_from_catalog(&catalog, table_name).await;
1217+
let (plan, batches) = scan_and_read(&catalog, table_name, None).await;
11991218
assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
12001219
assert_plan_has_multiple_schema_ids(&plan, table_name);
12011220

@@ -1235,14 +1254,39 @@ async fn test_read_format_schema_evolution_add_column() {
12351254
],
12361255
"Old Parquet rows should have null age and new ORC/Avro rows should keep age values"
12371256
);
1257+
1258+
let pb = PredicateBuilder::new(table.schema().fields());
1259+
let filter = pb
1260+
.equal("age", Datum::Int(30))
1261+
.expect("Failed to build predicate");
1262+
let (_, filtered_batches) =
1263+
scan_and_read_with_projection_and_filter(&table, Some(&["id", "name"]), filter).await;
1264+
assert_eq!(
1265+
extract_id_name(&filtered_batches),
1266+
vec![(3, "carol".to_string())],
1267+
"Projection plus age filter should return only the matching row"
1268+
);
1269+
1270+
let filter = pb.is_null("age").expect("Failed to build predicate");
1271+
let (_, filtered_batches) =
1272+
scan_and_read_with_projection_and_filter(&table, Some(&["id"]), filter).await;
1273+
assert_eq!(
1274+
extract_ids(&filtered_batches),
1275+
vec![1, 2],
1276+
"Projection plus age IS NULL should return rows with null added-column values"
1277+
);
12381278
}
12391279

12401280
/// Test reading mixed-format files after ALTER TABLE ALTER COLUMN TYPE (INT -> BIGINT).
12411281
/// Old Parquet files have INT; newer ORC/Avro files have BIGINT.
12421282
#[tokio::test]
12431283
async fn test_read_format_schema_evolution_type_promotion() {
1284+
use paimon::spec::{Datum, PredicateBuilder};
1285+
12441286
let table_name = "format_schema_evolution_type_promotion";
1245-
let (plan, batches) = scan_and_read_with_fs_catalog(table_name, None).await;
1287+
let catalog = create_file_system_catalog();
1288+
let table = get_table_from_catalog(&catalog, table_name).await;
1289+
let (plan, batches) = scan_and_read(&catalog, table_name, None).await;
12461290
assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
12471291
assert_plan_has_multiple_schema_ids(&plan, table_name);
12481292

@@ -1283,6 +1327,18 @@ async fn test_read_format_schema_evolution_type_promotion() {
12831327
],
12841328
"Old Parquet INT rows should be cast to BIGINT and new ORC/Avro BIGINT rows should match"
12851329
);
1330+
1331+
let pb = PredicateBuilder::new(table.schema().fields());
1332+
let filter = pb
1333+
.greater_than("value", Datum::Long(250))
1334+
.expect("Failed to build predicate");
1335+
let (_, filtered_batches) =
1336+
scan_and_read_with_projection_and_filter(&table, Some(&["id"]), filter).await;
1337+
assert_eq!(
1338+
extract_ids(&filtered_batches),
1339+
vec![3, 4, 5, 6],
1340+
"Projection plus promoted BIGINT filter should return matching promoted values"
1341+
);
12861342
}
12871343

12881344
/// Stats pruning should treat a newly added column as all-NULL for old files.
@@ -1516,14 +1572,14 @@ async fn test_read_schema_evolution_drop_column() {
15161572
/// Old files have the old physical field name; reader should map by field id.
15171573
#[tokio::test]
15181574
async fn test_read_schema_evolution_rename_column() {
1519-
let (plan, batches) =
1520-
scan_and_read_with_fs_catalog("schema_evolution_rename_column", None).await;
1575+
use paimon::spec::{Datum, PredicateBuilder};
15211576

1522-
assert_plan_file_formats(
1523-
&plan,
1524-
&["avro", "orc", "parquet"],
1525-
"schema_evolution_rename_column",
1526-
);
1577+
let catalog = create_file_system_catalog();
1578+
let table_name = "schema_evolution_rename_column";
1579+
let table = get_table_from_catalog(&catalog, table_name).await;
1580+
let (plan, batches) = scan_and_read(&catalog, table_name, None).await;
1581+
1582+
assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
15271583

15281584
let mut rows: Vec<(i32, String)> = Vec::new();
15291585
for batch in &batches {
@@ -1558,8 +1614,7 @@ async fn test_read_schema_evolution_rename_column() {
15581614
);
15591615

15601616
let (_, projected_batches) =
1561-
scan_and_read_with_fs_catalog("schema_evolution_rename_column", Some(&["renamed_payload"]))
1562-
.await;
1617+
scan_and_read(&catalog, table_name, Some(&["renamed_payload"])).await;
15631618
let mut projected_values = Vec::new();
15641619
for batch in &projected_batches {
15651620
assert_eq!(
@@ -1586,14 +1641,30 @@ async fn test_read_schema_evolution_rename_column() {
15861641
],
15871642
"Projection on renamed column should still use field-id mapping"
15881643
);
1644+
1645+
let pb = PredicateBuilder::new(table.schema().fields());
1646+
let filter = pb
1647+
.equal("renamed_payload", Datum::String("parquet-old".into()))
1648+
.expect("Failed to build predicate");
1649+
let (_, filtered_batches) =
1650+
scan_and_read_with_projection_and_filter(&table, Some(&["id"]), filter).await;
1651+
assert_eq!(
1652+
extract_ids(&filtered_batches),
1653+
vec![1],
1654+
"Projection plus filter on renamed column should map old Parquet field ids"
1655+
);
15891656
}
15901657

15911658
/// Test reading a mixed-format table after ALTER TABLE DROP COLUMN.
15921659
/// Old Parquet/ORC data files have the dropped column; new Avro files do not.
15931660
#[tokio::test]
15941661
async fn test_read_mixed_format_schema_evolution_drop_column() {
1662+
use paimon::spec::{Datum, PredicateBuilder};
1663+
15951664
let table_name = "mixed_format_schema_evolution_drop_column";
1596-
let (plan, batches) = scan_and_read_with_fs_catalog(table_name, None).await;
1665+
let catalog = create_file_system_catalog();
1666+
let table = get_table_from_catalog(&catalog, table_name).await;
1667+
let (plan, batches) = scan_and_read(&catalog, table_name, None).await;
15971668
assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
15981669

15991670
for batch in &batches {
@@ -1632,11 +1703,7 @@ async fn test_read_mixed_format_schema_evolution_drop_column() {
16321703
"Mixed-format DROP COLUMN should expose only remaining columns from all file formats"
16331704
);
16341705

1635-
let (_, projected_batches) = scan_and_read_with_fs_catalog(
1636-
"mixed_format_schema_evolution_drop_column",
1637-
Some(&["name", "id"]),
1638-
)
1639-
.await;
1706+
let (_, projected_batches) = scan_and_read(&catalog, table_name, Some(&["name", "id"])).await;
16401707

16411708
let mut projected_rows: Vec<(i32, String)> = Vec::new();
16421709
for batch in &projected_batches {
@@ -1678,21 +1745,32 @@ async fn test_read_mixed_format_schema_evolution_drop_column() {
16781745
],
16791746
"Projection should read remaining columns across old and new file schemas"
16801747
);
1748+
1749+
let pb = PredicateBuilder::new(table.schema().fields());
1750+
let filter = pb
1751+
.equal("name", Datum::String("orc-carol".into()))
1752+
.expect("Failed to build predicate");
1753+
let (_, filtered_batches) =
1754+
scan_and_read_with_projection_and_filter(&table, Some(&["id"]), filter).await;
1755+
assert_eq!(
1756+
extract_ids(&filtered_batches),
1757+
vec![3],
1758+
"Projection plus filter should read remaining columns after DROP COLUMN"
1759+
);
16811760
}
16821761

16831762
/// Test reading a mixed-format table after ALTER COLUMN ... FIRST/AFTER.
16841763
/// Old files keep the original physical column order; new files use moved columns.
16851764
#[tokio::test]
16861765
async fn test_read_mixed_format_schema_evolution_reorder_move_column() {
1687-
let (plan, batches) =
1688-
scan_and_read_with_fs_catalog("mixed_format_schema_evolution_reorder_move_column", None)
1689-
.await;
1766+
use paimon::spec::{Datum, PredicateBuilder};
16901767

1691-
assert_plan_file_formats(
1692-
&plan,
1693-
&["avro", "orc", "parquet"],
1694-
"mixed_format_schema_evolution_reorder_move_column",
1695-
);
1768+
let table_name = "mixed_format_schema_evolution_reorder_move_column";
1769+
let catalog = create_file_system_catalog();
1770+
let table = get_table_from_catalog(&catalog, table_name).await;
1771+
let (plan, batches) = scan_and_read(&catalog, table_name, None).await;
1772+
1773+
assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
16961774

16971775
for batch in &batches {
16981776
let schema = batch.schema();
@@ -1741,11 +1819,8 @@ async fn test_read_mixed_format_schema_evolution_reorder_move_column() {
17411819
"Mixed-format REORDER/MOVE COLUMN should read values by field id, not physical position"
17421820
);
17431821

1744-
let (_, projected_batches) = scan_and_read_with_fs_catalog(
1745-
"mixed_format_schema_evolution_reorder_move_column",
1746-
Some(&["id", "right_value"]),
1747-
)
1748-
.await;
1822+
let (_, projected_batches) =
1823+
scan_and_read(&catalog, table_name, Some(&["id", "right_value"])).await;
17491824
let mut projected_rows: Vec<(i32, String)> = Vec::new();
17501825
for batch in &projected_batches {
17511826
let schema = batch.schema();
@@ -1781,6 +1856,43 @@ async fn test_read_mixed_format_schema_evolution_reorder_move_column() {
17811856
],
17821857
"Projection should still map reordered old and new files by field id"
17831858
);
1859+
1860+
let pb = PredicateBuilder::new(table.schema().fields());
1861+
let filter = pb
1862+
.equal("left_value", Datum::String("orc-left-3".into()))
1863+
.expect("Failed to build predicate");
1864+
let (_, filtered_batches) =
1865+
scan_and_read_with_projection_and_filter(&table, Some(&["right_value", "id"]), filter)
1866+
.await;
1867+
1868+
let mut filtered_rows: Vec<(i32, String)> = Vec::new();
1869+
for batch in &filtered_batches {
1870+
let schema = batch.schema();
1871+
let field_names: Vec<&str> = schema.fields().iter().map(|f| f.name().as_str()).collect();
1872+
assert_eq!(
1873+
field_names,
1874+
vec!["right_value", "id"],
1875+
"Projection plus filter should preserve caller-specified order"
1876+
);
1877+
1878+
let right_value = batch
1879+
.column_by_name("right_value")
1880+
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
1881+
.expect("filtered right_value");
1882+
let id = batch
1883+
.column_by_name("id")
1884+
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
1885+
.expect("filtered id");
1886+
for i in 0..batch.num_rows() {
1887+
filtered_rows.push((id.value(i), right_value.value(i).to_string()));
1888+
}
1889+
}
1890+
filtered_rows.sort_by_key(|(id, _)| *id);
1891+
assert_eq!(
1892+
filtered_rows,
1893+
vec![(3, "orc-right-3".into())],
1894+
"Projection plus filter should map reordered fields by id"
1895+
);
17841896
}
17851897

17861898
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)