This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new cb802ee  test: add mixed-format data evolution coverage (#405)
cb802ee is described below

commit cb802ee33adf6ababd1434ca9ac78cd1692b735d
Author: Jiwen liu <[email protected]>
AuthorDate: Thu Jun 25 10:42:47 2026 +0800

    test: add mixed-format data evolution coverage (#405)
---
 crates/integration_tests/tests/read_tables.rs | 149 ++++++++++++++++++++++++++
 dev/spark/provision.py                        |  64 +++++++++++
 2 files changed, 213 insertions(+)

diff --git a/crates/integration_tests/tests/read_tables.rs 
b/crates/integration_tests/tests/read_tables.rs
index f8fd62c..e03a123 100644
--- a/crates/integration_tests/tests/read_tables.rs
+++ b/crates/integration_tests/tests/read_tables.rs
@@ -1679,6 +1679,98 @@ async fn test_read_data_evolution_type_promotion() {
     );
 }
 
+#[tokio::test]
+async fn test_read_data_evolution_mixed_format_add_column() {
+    let table_name = "data_evolution_mixed_format_add_column";
+    let (plan, batches) = scan_and_read_with_fs_catalog(table_name, 
None).await;
+    assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
+    assert_plan_has_multiple_schema_ids(&plan, table_name);
+
+    let mut rows: Vec<(i32, String, i32, Option<String>)> = Vec::new();
+    for batch in &batches {
+        let id = batch
+            .column_by_name("id")
+            .and_then(|c| c.as_any().downcast_ref::<Int32Array>())
+            .expect("id");
+        let name = batch
+            .column_by_name("name")
+            .and_then(|c| c.as_any().downcast_ref::<StringArray>())
+            .expect("name");
+        let value = batch
+            .column_by_name("value")
+            .and_then(|c| c.as_any().downcast_ref::<Int32Array>())
+            .expect("value");
+        let extra = batch
+            .column_by_name("extra")
+            .and_then(|c| c.as_any().downcast_ref::<StringArray>())
+            .expect("extra");
+        for i in 0..batch.num_rows() {
+            rows.push((
+                id.value(i),
+                name.value(i).to_string(),
+                value.value(i),
+                (!extra.is_null(i)).then(|| extra.value(i).to_string()),
+            ));
+        }
+    }
+    rows.sort_by_key(|(id, _, _, _)| *id);
+
+    assert_eq!(
+        rows,
+        vec![
+            (1, "alice".into(), 100, None),
+            (2, "bob".into(), 200, None),
+            (3, "carol".into(), 300, Some("orc-extra".into())),
+            (4, "dave".into(), 400, Some("avro-extra".into())),
+        ],
+        "Mixed-format data evolution should null-fill old extra values and 
preserve new values"
+    );
+}
+
+#[tokio::test]
+async fn test_read_data_evolution_mixed_format_type_promotion() {
+    let table_name = "data_evolution_mixed_format_type_promotion";
+    let (plan, batches) = scan_and_read_with_fs_catalog(table_name, 
None).await;
+    assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
+    assert_plan_has_multiple_schema_ids(&plan, table_name);
+
+    for batch in &batches {
+        let value_col = batch.column_by_name("value").expect("value column");
+        assert_eq!(
+            value_col.data_type(),
+            &arrow_array::types::Int64Type::DATA_TYPE,
+            "value column should be Int64 after mixed-format data-evolution 
type promotion"
+        );
+    }
+
+    let mut rows: Vec<(i32, i64)> = Vec::new();
+    for batch in &batches {
+        let id = batch
+            .column_by_name("id")
+            .and_then(|c| c.as_any().downcast_ref::<Int32Array>())
+            .expect("id");
+        let value = batch
+            .column_by_name("value")
+            .and_then(|c| c.as_any().downcast_ref::<Int64Array>())
+            .expect("value as Int64Array");
+        for i in 0..batch.num_rows() {
+            rows.push((id.value(i), value.value(i)));
+        }
+    }
+    rows.sort_by_key(|(id, _)| *id);
+
+    assert_eq!(
+        rows,
+        vec![
+            (1, 100i64),
+            (2, 200i64),
+            (3, 3_000_000_000i64),
+            (4, 4_000_000_000i64),
+        ],
+        "Mixed-format data evolution should promote INT to BIGINT"
+    );
+}
+
 /// Test reading a table after ALTER TABLE DROP COLUMN.
 /// Old data files have the dropped column; reader should ignore it.
 #[tokio::test]
@@ -3168,6 +3260,63 @@ async fn 
test_read_data_evolution_table_only_row_id_with_row_ranges() {
     );
 }
 
+#[tokio::test]
+async fn test_read_data_evolution_mixed_format_row_id_projection() {
+    let catalog = create_file_system_catalog();
+    let table = get_table_from_catalog(&catalog, 
"data_evolution_mixed_format_add_column").await;
+
+    let mut read_builder = table.new_read_builder();
+    read_builder.with_projection(&["_ROW_ID", "id"]);
+    let scan = read_builder.new_scan();
+    let plan = scan.plan().await.expect("Failed to plan scan");
+
+    assert_plan_file_formats(
+        &plan,
+        &["avro", "orc", "parquet"],
+        "data_evolution_mixed_format_add_column",
+    );
+    assert_plan_has_multiple_schema_ids(&plan, 
"data_evolution_mixed_format_add_column");
+
+    let read = read_builder.new_read().expect("Failed to create read");
+    let stream = read
+        .to_arrow(plan.splits())
+        .expect("Failed to create arrow stream");
+    let batches: Vec<RecordBatch> = stream
+        .try_collect()
+        .await
+        .expect("Failed to collect batches");
+
+    let mut row_ids = Vec::new();
+    let mut ids = Vec::new();
+    for batch in &batches {
+        let row_id = batch
+            .column_by_name("_ROW_ID")
+            .and_then(|c| c.as_any().downcast_ref::<Int64Array>())
+            .expect("_ROW_ID");
+        let id = batch
+            .column_by_name("id")
+            .and_then(|c| c.as_any().downcast_ref::<Int32Array>())
+            .expect("id");
+        for i in 0..batch.num_rows() {
+            row_ids.push(row_id.value(i));
+            ids.push(id.value(i));
+        }
+    }
+
+    assert_eq!(row_ids.len(), ids.len());
+    assert_eq!(ids.len(), 4, "Expected all fixture rows to be readable");
+    assert!(
+        row_ids.iter().all(|&row_id| row_id >= 0),
+        "All _ROW_ID values should be non-negative"
+    );
+    let unique: HashSet<i64> = row_ids.iter().copied().collect();
+    assert_eq!(
+        unique.len(),
+        row_ids.len(),
+        "_ROW_ID values should be unique"
+    );
+}
+
 // ---------------------------------------------------------------------------
 // Full types integration tests (parquet + orc + avro)
 // ---------------------------------------------------------------------------
diff --git a/dev/spark/provision.py b/dev/spark/provision.py
index 93a80fb..2ce1c72 100644
--- a/dev/spark/provision.py
+++ b/dev/spark/provision.py
@@ -541,6 +541,70 @@ def main():
         "INSERT INTO format_schema_evolution_type_promotion VALUES (5, 
5000000000), (6, 6000000000)"
     )
 
+    # ===== Mixed-format Data Evolution: Add Column =====
+    # Combines row-tracking/data-evolution with ADD COLUMN and mixed file 
formats.
+    # Old Parquet files lack extra; new ORC/Avro files contain extra.
+    spark.sql(
+        """
+        CREATE TABLE IF NOT EXISTS data_evolution_mixed_format_add_column (
+            id INT,
+            name STRING,
+            value INT
+        ) USING paimon
+        TBLPROPERTIES (
+            'row-tracking.enabled' = 'true',
+            'data-evolution.enabled' = 'true',
+            'file.format' = 'parquet'
+        )
+        """
+    )
+    spark.sql(
+        """
+        INSERT INTO data_evolution_mixed_format_add_column VALUES
+            (1, 'alice', 100),
+            (2, 'bob', 200)
+        """
+    )
+    spark.sql("ALTER TABLE data_evolution_mixed_format_add_column ADD COLUMNS 
(extra STRING)")
+    spark.sql("ALTER TABLE data_evolution_mixed_format_add_column SET 
TBLPROPERTIES ('file.format' = 'orc')")
+    spark.sql(
+        "INSERT INTO data_evolution_mixed_format_add_column VALUES (3, 
'carol', 300, 'orc-extra')"
+    )
+    spark.sql("ALTER TABLE data_evolution_mixed_format_add_column SET 
TBLPROPERTIES ('file.format' = 'avro')")
+    spark.sql(
+        "INSERT INTO data_evolution_mixed_format_add_column VALUES (4, 'dave', 
400, 'avro-extra')"
+    )
+
+    # ===== Mixed-format Data Evolution: Type Promotion =====
+    # Old Parquet files have INT; new ORC/Avro files have BIGINT.
+    spark.sql(
+        """
+        CREATE TABLE IF NOT EXISTS data_evolution_mixed_format_type_promotion (
+            id INT,
+            value INT
+        ) USING paimon
+        TBLPROPERTIES (
+            'row-tracking.enabled' = 'true',
+            'data-evolution.enabled' = 'true',
+            'file.format' = 'parquet'
+        )
+        """
+    )
+    spark.sql(
+        "INSERT INTO data_evolution_mixed_format_type_promotion VALUES (1, 
100), (2, 200)"
+    )
+    spark.sql(
+        "ALTER TABLE data_evolution_mixed_format_type_promotion ALTER COLUMN 
value TYPE BIGINT"
+    )
+    spark.sql("ALTER TABLE data_evolution_mixed_format_type_promotion SET 
TBLPROPERTIES ('file.format' = 'orc')")
+    spark.sql(
+        "INSERT INTO data_evolution_mixed_format_type_promotion VALUES (3, 
3000000000)"
+    )
+    spark.sql("ALTER TABLE data_evolution_mixed_format_type_promotion SET 
TBLPROPERTIES ('file.format' = 'avro')")
+    spark.sql(
+        "INSERT INTO data_evolution_mixed_format_type_promotion VALUES (4, 
4000000000)"
+    )
+
     # ===== Data Evolution + Schema Evolution: Add Column =====
     # Combines data-evolution (row-tracking + MERGE INTO) with ALTER TABLE ADD 
COLUMNS.
     # Old files lack the new column; MERGE INTO produces partial-column files.

Reply via email to