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 76f24cb  test: add partitioned schema evolution coverage (#406)
76f24cb is described below

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

    test: add partitioned schema evolution coverage (#406)
---
 crates/integration_tests/tests/read_tables.rs      | 130 +++++++++++++++++++++
 .../integrations/datafusion/tests/read_tables.rs   |  31 +++++
 dev/spark/provision.py                             |  46 ++++++++
 3 files changed, 207 insertions(+)

diff --git a/crates/integration_tests/tests/read_tables.rs 
b/crates/integration_tests/tests/read_tables.rs
index 7f945e0..f8fd62c 100644
--- a/crates/integration_tests/tests/read_tables.rs
+++ b/crates/integration_tests/tests/read_tables.rs
@@ -1300,6 +1300,136 @@ async fn test_read_format_schema_evolution_add_column() 
{
     );
 }
 
+#[tokio::test]
+async fn test_read_partitioned_format_schema_evolution_add_column() {
+    use paimon::spec::{Datum, Predicate, PredicateBuilder};
+
+    let table_name = "partitioned_format_schema_evolution_add_column";
+    let catalog = create_file_system_catalog();
+    let table = get_table_from_catalog(&catalog, table_name).await;
+    let (plan, batches) = scan_and_read(&catalog, table_name, None).await;
+    assert_plan_file_formats(&plan, &["avro", "orc", "parquet"], table_name);
+    assert_plan_has_multiple_schema_ids(&plan, table_name);
+    assert_eq!(
+        extract_plan_partitions(&plan),
+        HashSet::from([
+            "2024-01-01".to_string(),
+            "2024-01-02".to_string(),
+            "2024-01-03".to_string(),
+        ]),
+        "Full scan should include all dt partitions"
+    );
+
+    let mut rows: Vec<(String, i32, String, Option<String>)> = Vec::new();
+    for batch in &batches {
+        let dt = batch
+            .column_by_name("dt")
+            .and_then(|c| c.as_any().downcast_ref::<StringArray>())
+            .expect("dt");
+        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 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((
+                dt.value(i).to_string(),
+                id.value(i),
+                name.value(i).to_string(),
+                (!extra.is_null(i)).then(|| extra.value(i).to_string()),
+            ));
+        }
+    }
+    rows.sort_by(|left, right| 
left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
+
+    assert_eq!(
+        rows,
+        vec![
+            ("2024-01-01".into(), 1, "alice".into(), None),
+            (
+                "2024-01-01".into(),
+                3,
+                "carol".into(),
+                Some("orc-extra-1".into()),
+            ),
+            ("2024-01-02".into(), 2, "bob".into(), None),
+            (
+                "2024-01-02".into(),
+                5,
+                "eve".into(),
+                Some("avro-extra-1".into()),
+            ),
+            (
+                "2024-01-03".into(),
+                4,
+                "dave".into(),
+                Some("orc-extra-2".into()),
+            ),
+            (
+                "2024-01-03".into(),
+                6,
+                "frank".into(),
+                Some("avro-extra-2".into()),
+            ),
+        ],
+        "Old partitioned Parquet rows should null-fill extra and new ORC/Avro 
rows should keep values"
+    );
+
+    let pb = PredicateBuilder::new(table.schema().fields());
+    let filter = pb
+        .equal("dt", Datum::String("2024-01-02".into()))
+        .expect("Failed to build predicate");
+    let (plan, batches) = scan_and_read_with_filter(&table, filter).await;
+    assert_eq!(
+        extract_plan_partitions(&plan),
+        HashSet::from(["2024-01-02".to_string()]),
+        "dt filter should prune unrelated partitions"
+    );
+    assert_eq!(
+        extract_id_name_dt(&batches),
+        vec![
+            (2, "bob".into(), "2024-01-02".into()),
+            (5, "eve".into(), "2024-01-02".into()),
+        ]
+    );
+
+    let filter = Predicate::and(vec![
+        pb.equal("dt", Datum::String("2024-01-03".into())).unwrap(),
+        pb.equal("extra", Datum::String("avro-extra-2".into()))
+            .unwrap(),
+    ]);
+    let (plan, batches) =
+        scan_and_read_with_projection_and_filter(&table, Some(&["dt", "id", 
"extra"]), filter)
+            .await;
+    assert_eq!(
+        extract_plan_partitions(&plan),
+        HashSet::from(["2024-01-03".to_string()]),
+        "Partition predicate should still prune when projection includes dt 
and filter uses extra"
+    );
+    assert_eq!(extract_ids(&batches), vec![6]);
+
+    let filter = Predicate::and(vec![
+        pb.equal("dt", Datum::String("2024-01-01".into())).unwrap(),
+        pb.is_null("extra").unwrap(),
+    ]);
+    let (plan, batches) =
+        scan_and_read_with_projection_and_filter(&table, Some(&["dt", "id", 
"extra"]), filter)
+            .await;
+    assert_eq!(
+        extract_plan_partitions(&plan),
+        HashSet::from(["2024-01-01".to_string()]),
+        "extra IS NULL with dt filter should retain only the matching 
old-schema partition file"
+    );
+    assert_eq!(extract_ids(&batches), vec![1]);
+}
+
 /// Test reading mixed-format files after ALTER TABLE ALTER COLUMN TYPE (INT 
-> BIGINT).
 /// Old Parquet files have INT; newer ORC/Avro files have BIGINT.
 #[tokio::test]
diff --git a/crates/integrations/datafusion/tests/read_tables.rs 
b/crates/integrations/datafusion/tests/read_tables.rs
index 9c70255..625a686 100644
--- a/crates/integrations/datafusion/tests/read_tables.rs
+++ b/crates/integrations/datafusion/tests/read_tables.rs
@@ -946,6 +946,37 @@ async fn 
test_sql_read_format_schema_evolution_add_column() {
     .await;
 }
 
+#[tokio::test]
+async fn test_sql_read_partitioned_format_schema_evolution_add_column() {
+    assert_sql_rows(
+        "SELECT id, name, extra FROM 
paimon.default.partitioned_format_schema_evolution_add_column WHERE dt = 
'2024-01-02' ORDER BY id",
+        &[
+            &["2", "bob", "NULL"],
+            &["5", "eve", "avro-extra-1"],
+        ],
+    )
+    .await;
+
+    assert_sql_rows(
+        "SELECT id, name, extra FROM 
paimon.default.partitioned_format_schema_evolution_add_column WHERE dt = 
'2024-01-01' AND extra IS NULL ORDER BY id",
+        &[&["1", "alice", "NULL"]],
+    )
+    .await;
+
+    assert_sql_rows(
+        "SELECT dt, id, extra FROM 
paimon.default.partitioned_format_schema_evolution_add_column ORDER BY id",
+        &[
+            &["2024-01-01", "1", "NULL"],
+            &["2024-01-02", "2", "NULL"],
+            &["2024-01-01", "3", "orc-extra-1"],
+            &["2024-01-03", "4", "orc-extra-2"],
+            &["2024-01-02", "5", "avro-extra-1"],
+            &["2024-01-03", "6", "avro-extra-2"],
+        ],
+    )
+    .await;
+}
+
 #[tokio::test]
 async fn test_sql_read_format_schema_evolution_type_promotion() {
     assert_sql_rows(
diff --git a/dev/spark/provision.py b/dev/spark/provision.py
index a10a8cc..93a80fb 100644
--- a/dev/spark/provision.py
+++ b/dev/spark/provision.py
@@ -467,6 +467,52 @@ def main():
         "INSERT INTO format_schema_evolution_add_column VALUES (5, 'eve', 50), 
(6, 'frank', 60)"
     )
 
+    # ===== Partitioned Mixed-format Schema Evolution: Add Column =====
+    # Old Parquet files lack extra; new ORC/Avro files contain extra across dt 
partitions.
+    spark.sql(
+        """
+        CREATE TABLE IF NOT EXISTS 
partitioned_format_schema_evolution_add_column (
+            id INT,
+            name STRING,
+            dt STRING
+        ) USING paimon
+        PARTITIONED BY (dt)
+        TBLPROPERTIES (
+            'file.format' = 'parquet'
+        )
+        """
+    )
+    spark.sql(
+        """
+        INSERT INTO partitioned_format_schema_evolution_add_column VALUES
+            (1, 'alice', '2024-01-01'),
+            (2, 'bob', '2024-01-02')
+        """
+    )
+    spark.sql(
+        "ALTER TABLE partitioned_format_schema_evolution_add_column ADD 
COLUMNS (extra STRING)"
+    )
+    spark.sql(
+        "ALTER TABLE partitioned_format_schema_evolution_add_column SET 
TBLPROPERTIES ('file.format' = 'orc')"
+    )
+    spark.sql(
+        """
+        INSERT INTO partitioned_format_schema_evolution_add_column (id, name, 
extra, dt) VALUES
+            (3, 'carol', 'orc-extra-1', '2024-01-01'),
+            (4, 'dave', 'orc-extra-2', '2024-01-03')
+        """
+    )
+    spark.sql(
+        "ALTER TABLE partitioned_format_schema_evolution_add_column SET 
TBLPROPERTIES ('file.format' = 'avro')"
+    )
+    spark.sql(
+        """
+        INSERT INTO partitioned_format_schema_evolution_add_column (id, name, 
extra, dt) VALUES
+            (5, 'eve', 'avro-extra-1', '2024-01-02'),
+            (6, 'frank', 'avro-extra-2', '2024-01-03')
+        """
+    )
+
     # ===== Mixed-format Schema Evolution: Type Promotion (INT -> BIGINT) =====
     # Old Parquet files have value as INT; new ORC/Avro files have value as 
BIGINT.
     spark.sql(

Reply via email to