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 64c64a30 fix(data-evolution): require row tracking and row IDs (#718)
64c64a30 is described below

commit 64c64a3096ff991bb41e08fcd2ed8fbef24bbbc2
Author: XiaoHongbo <[email protected]>
AuthorDate: Mon Aug 17 09:06:03 2026 +0800

    fix(data-evolution): require row tracking and row IDs (#718)
---
 crates/integrations/datafusion/src/sql_context.rs  |   4 +-
 crates/integrations/datafusion/src/table/mod.rs    |   1 +
 .../datafusion/tests/merge_into_tests.rs           |  51 ++------
 .../datafusion/tests/sql_context_tests.rs          |  15 ++-
 .../integrations/datafusion/tests/update_tests.rs  |  16 +--
 .../datafusion/tests/variant_pushdown.rs           |   1 +
 crates/paimon/src/spec/core_options.rs             |   9 ++
 crates/paimon/src/spec/schema.rs                   | 135 ++++++++++++++++++++-
 crates/paimon/src/table/cow_writer.rs              |   1 +
 crates/paimon/src/table/data_evolution_reader.rs   |  65 +++++++++-
 crates/paimon/src/table/hybrid_search_builder.rs   |   1 -
 .../paimon/src/table/lumina_index_build_builder.rs |  12 +-
 crates/paimon/src/table/read_builder.rs            |   2 +-
 crates/paimon/src/table/table_scan.rs              |  40 +++---
 crates/paimon/src/table/table_write.rs             |   2 +
 crates/paimon/tests/incremental_batch_scan_test.rs |   1 -
 16 files changed, 263 insertions(+), 93 deletions(-)

diff --git a/crates/integrations/datafusion/src/sql_context.rs 
b/crates/integrations/datafusion/src/sql_context.rs
index 4c39c49a..ae02f0a9 100644
--- a/crates/integrations/datafusion/src/sql_context.rs
+++ b/crates/integrations/datafusion/src/sql_context.rs
@@ -6421,7 +6421,7 @@ mod tests {
         let sql_context = make_sql_context(catalog.clone()).await;
 
         sql_context
-            .sql("CREATE TABLE mydb.t1 (id INT, payload BLOB NOT NULL) WITH 
('data-evolution.enabled' = 'true')")
+            .sql("CREATE TABLE mydb.t1 (id INT, payload BLOB NOT NULL) WITH 
('data-evolution.enabled' = 'true', 'row-tracking.enabled' = 'true')")
             .await
             .unwrap();
 
@@ -6451,7 +6451,7 @@ mod tests {
                  photo BYTES COMMENT '__BLOB_FIELD; raw photo', \
                  thumb BINARY COMMENT '__BLOB_DESCRIPTOR_FIELD', \
                  preview VARBINARY COMMENT '__BLOB_VIEW_FIELD; preview ref'\
-                 ) WITH ('data-evolution.enabled' = 'true')",
+                 ) WITH ('data-evolution.enabled' = 'true', 
'row-tracking.enabled' = 'true')",
             )
             .await
             .unwrap();
diff --git a/crates/integrations/datafusion/src/table/mod.rs 
b/crates/integrations/datafusion/src/table/mod.rs
index 9e3007e2..3e4ad496 100644
--- a/crates/integrations/datafusion/src/table/mod.rs
+++ b/crates/integrations/datafusion/src/table/mod.rs
@@ -713,6 +713,7 @@ mod tests {
             .column("id", DataType::Int(IntType::new()))
             .column("name", DataType::Int(IntType::new()))
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
         let table_schema = TableSchema::new(0, &schema);
diff --git a/crates/integrations/datafusion/tests/merge_into_tests.rs 
b/crates/integrations/datafusion/tests/merge_into_tests.rs
index 4598b9a7..6a21c0d5 100644
--- a/crates/integrations/datafusion/tests/merge_into_tests.rs
+++ b/crates/integrations/datafusion/tests/merge_into_tests.rs
@@ -1152,7 +1152,7 @@ async fn test_rejects_partition_column_in_set() {
 }
 
 #[tokio::test]
-async fn test_rejects_table_without_row_tracking() {
+async fn test_rejects_enabling_data_evolution_without_row_tracking() {
     let (_tmp, catalog) = create_test_env();
     let sql_context = create_sql_context(catalog.clone()).await;
 
@@ -1169,29 +1169,11 @@ async fn test_rejects_table_without_row_tracking() {
         .await
         .unwrap();
 
-    sql_context
-        .sql("INSERT INTO paimon.test_db.no_tracking (id, name) VALUES (1, 
'alice')")
-        .await
-        .unwrap()
-        .collect()
+    let error = sql_context
+        .sql("ALTER TABLE paimon.test_db.no_tracking SET 
TBLPROPERTIES('data-evolution.enabled' = 'true')")
         .await
-        .unwrap();
-
-    sql_context.sql("ALTER TABLE paimon.test_db.no_tracking SET 
TBLPROPERTIES('data-evolution.enabled' = 'true')").await.unwrap();
-
-    register_source(
-        &sql_context,
-        "CREATE TEMPORARY TABLE paimon.test_db.src_nrt AS SELECT * FROM 
(VALUES (1, 'ALICE')) AS t(id, name)",
-    )
-    .await;
-
-    assert_merge_error(
-        &sql_context,
-        "MERGE INTO paimon.test_db.no_tracking t USING paimon.test_db.src_nrt 
s ON t.id = s.id \
-         WHEN MATCHED THEN UPDATE SET name = s.name",
-        "row-tracking.enabled",
-    )
-    .await;
+        .unwrap_err();
+    assert!(error.to_string().contains("row-tracking.enabled"));
 }
 
 #[tokio::test]
@@ -1471,7 +1453,7 @@ async fn test_rejects_table_with_primary_keys() {
         .sql("CREATE SCHEMA paimon.test_db")
         .await
         .unwrap();
-    sql_context
+    let error = sql_context
         .sql(
             "CREATE TABLE paimon.test_db.pk_target (\
                 id INT NOT NULL, name STRING, PRIMARY KEY (id)\
@@ -1480,23 +1462,10 @@ async fn test_rejects_table_with_primary_keys() {
             )",
         )
         .await
-        .unwrap();
-
-    register_source(
-        &sql_context,
-        "CREATE TEMPORARY TABLE paimon.test_db.src_pk AS SELECT * FROM (VALUES 
(1, 'ALICE')) AS t(id, name)",
-    )
-    .await;
-
-    sql_context.sql("ALTER TABLE paimon.test_db.pk_target SET 
TBLPROPERTIES('data-evolution.enabled' = 'true')").await.unwrap();
-
-    assert_merge_error(
-        &sql_context,
-        "MERGE INTO paimon.test_db.pk_target t USING paimon.test_db.src_pk s 
ON t.id = s.id \
-         WHEN MATCHED THEN UPDATE SET name = s.name",
-        "does not support primary keys",
-    )
-    .await;
+        .unwrap_err();
+    assert!(error
+        .to_string()
+        .contains("Cannot define primary-key for row tracking table"));
 }
 
 #[tokio::test]
diff --git a/crates/integrations/datafusion/tests/sql_context_tests.rs 
b/crates/integrations/datafusion/tests/sql_context_tests.rs
index 37b08c20..e308d192 100644
--- a/crates/integrations/datafusion/tests/sql_context_tests.rs
+++ b/crates/integrations/datafusion/tests/sql_context_tests.rs
@@ -1036,9 +1036,11 @@ async fn test_create_table_with_blob_type() {
         .sql(
             "CREATE TABLE paimon.mydb.assets (
                 id INT NOT NULL,
-                payload BLOB,
-                PRIMARY KEY (id)
-            ) WITH ('data-evolution.enabled' = 'true')",
+                payload BLOB
+            ) WITH (
+                'data-evolution.enabled' = 'true',
+                'row-tracking.enabled' = 'true'
+            )",
         )
         .await
         .expect("CREATE TABLE with BLOB should succeed");
@@ -1049,7 +1051,7 @@ async fn test_create_table_with_blob_type() {
         .unwrap();
     let schema = table.schema();
     assert_eq!(schema.fields().len(), 2);
-    assert_eq!(schema.primary_keys(), &["id"]);
+    assert!(schema.primary_keys().is_empty());
     assert_eq!(
         *schema.fields()[1].data_type(),
         DataType::Blob(BlobType::new())
@@ -2512,7 +2514,10 @@ async fn test_show_create_table_various_types() {
              h DATE, \
              i TIMESTAMP(3), \
              j BLOB) \
-             WITH ('data-evolution.enabled' = 'true')",
+             WITH (\
+                 'data-evolution.enabled' = 'true', \
+                 'row-tracking.enabled' = 'true'\
+             )",
         )
         .await
         .expect("CREATE TABLE should succeed");
diff --git a/crates/integrations/datafusion/tests/update_tests.rs 
b/crates/integrations/datafusion/tests/update_tests.rs
index edc39260..2c451d8c 100644
--- a/crates/integrations/datafusion/tests/update_tests.rs
+++ b/crates/integrations/datafusion/tests/update_tests.rs
@@ -507,14 +507,14 @@ async fn 
test_update_rejects_primary_key_table_without_data_evolution() {
 }
 
 #[tokio::test]
-async fn test_update_rejects_primary_key_table_with_data_evolution() {
+async fn test_rejects_primary_key_data_evolution_table_at_create() {
     let (tmp, catalog) = create_test_env();
     let sql_context = create_sql_context(catalog).await;
     sql_context
         .sql("CREATE SCHEMA paimon.test_db")
         .await
         .unwrap();
-    sql_context
+    let error = sql_context
         .sql(
             "CREATE TABLE paimon.test_db.pk_de_t (\
                 id INT NOT NULL, name VARCHAR, PRIMARY KEY (id)\
@@ -525,14 +525,10 @@ async fn 
test_update_rejects_primary_key_table_with_data_evolution() {
             )",
         )
         .await
-        .unwrap();
-
-    assert_sql_error(
-        &sql_context,
-        "UPDATE paimon.test_db.pk_de_t SET name = 'x' WHERE id = 1",
-        "does not support primary keys",
-    )
-    .await;
+        .unwrap_err();
+    assert!(error
+        .to_string()
+        .contains("Cannot define primary-key for row tracking table"));
     drop(tmp);
 }
 
diff --git a/crates/integrations/datafusion/tests/variant_pushdown.rs 
b/crates/integrations/datafusion/tests/variant_pushdown.rs
index f5dc32f0..2fb60653 100644
--- a/crates/integrations/datafusion/tests/variant_pushdown.rs
+++ b/crates/integrations/datafusion/tests/variant_pushdown.rs
@@ -67,6 +67,7 @@ async fn 
setup_data_evolution_shredded_variant_table_with_rows() -> (tempfile::T
         ) WITH (
             'file.format' = 'parquet',
             'data-evolution.enabled' = 'true',
+            'row-tracking.enabled' = 'true',
             'variant.shreddingSchema' =
                 
'{"type":"ROW","fields":[{"name":"payload","type":{"type":"ROW","fields":[{"name":"age","type":"INT"},{"name":"city","type":"STRING"}]}}]}'
         )
diff --git a/crates/paimon/src/spec/core_options.rs 
b/crates/paimon/src/spec/core_options.rs
index 3f806fd9..1d45d49a 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -69,6 +69,7 @@ const DEFAULT_METADATA_STATS_KEEP_FIRST_N_COLUMNS: i32 = -1;
 const FIELDS_PREFIX: &str = "fields";
 const STATS_MODE_SUFFIX: &str = "stats-mode";
 const ROW_TRACKING_ENABLED_OPTION: &str = "row-tracking.enabled";
+const CLUSTERING_INCREMENTAL_OPTION: &str = "clustering.incremental";
 pub(crate) const TABLE_TYPE_OPTION: &str = "type";
 pub(crate) const FORMAT_TABLE_TYPE: &str = "format-table";
 pub(crate) const PATH_OPTION: &str = "path";
@@ -975,6 +976,14 @@ impl<'a> CoreOptions<'a> {
             .unwrap_or(false)
     }
 
+    /// Whether incremental clustering is enabled. Default is false.
+    pub fn clustering_incremental_enabled(&self) -> bool {
+        self.options
+            .get(CLUSTERING_INCREMENTAL_OPTION)
+            .map(|v| v.eq_ignore_ascii_case("true"))
+            .unwrap_or(false)
+    }
+
     /// Suggested target size for a manifest file. Default is 8 MiB.
     ///
     /// `manifest.target-file-size` is the Java/Python option. The shorter
diff --git a/crates/paimon/src/spec/schema.rs b/crates/paimon/src/spec/schema.rs
index c763f0ba..3238ccee 100644
--- a/crates/paimon/src/spec/schema.rs
+++ b/crates/paimon/src/spec/schema.rs
@@ -1157,6 +1157,7 @@ impl Schema {
     ) -> crate::Result<()> {
         validate_no_reserved_field_names(fields)?;
         Self::validate_key_field_types(fields, primary_keys, options)?;
+        Self::validate_row_tracking(primary_keys, options)?;
         Self::validate_blob_fields(fields, partition_keys, options)?;
         Self::validate_vector_store_fields(fields, partition_keys, options)?;
         
PartialUpdateConfig::new(options).validate_create_mode(!primary_keys.is_empty())?;
@@ -1175,6 +1176,42 @@ impl Schema {
         Ok(())
     }
 
+    fn validate_row_tracking(
+        primary_keys: &[String],
+        options: &HashMap<String, String>,
+    ) -> crate::Result<()> {
+        let core_options = CoreOptions::new(options);
+        if core_options.row_tracking_enabled() {
+            if !primary_keys.is_empty() {
+                return Err(crate::Error::ConfigInvalid {
+                    message: "Cannot define primary-key for row tracking 
table.".to_string(),
+                });
+            }
+            if core_options.bucket() != -1 {
+                return Err(crate::Error::ConfigInvalid {
+                    message:
+                        "Cannot define bucket for row tracking table, it only 
support bucket = -1"
+                            .to_string(),
+                });
+            }
+        }
+        if core_options.data_evolution_enabled() {
+            if !core_options.row_tracking_enabled() {
+                return Err(crate::Error::ConfigInvalid {
+                    message: "Data evolution config must enabled with 
row-tracking.enabled"
+                        .to_string(),
+                });
+            }
+            if core_options.clustering_incremental_enabled() {
+                return Err(crate::Error::ConfigInvalid {
+                    message: "Data evolution config must disabled with 
clustering.incremental"
+                        .to_string(),
+                });
+            }
+        }
+        Ok(())
+    }
+
     /// Normalize primary keys: optionally take from table options 
(`primary-key`), remove from options.
     /// Corresponds to Java `normalizePrimaryKeys`.
     fn normalize_primary_keys(
@@ -2518,6 +2555,85 @@ mod tests {
         assert_eq!(schema.primary_keys(), &["a", "b"]);
     }
 
+    #[test]
+    fn test_row_tracking_rejects_primary_key_and_bucket() {
+        assert_config_invalid(
+            Schema::builder()
+                .column("id", DataType::Int(IntType::new()))
+                .primary_key(vec!["id".to_string()])
+                .option("row-tracking.enabled", "true")
+                .build(),
+            "primary-key",
+        );
+
+        assert_config_invalid(
+            Schema::builder()
+                .column("id", DataType::Int(IntType::new()))
+                .option("row-tracking.enabled", "true")
+                .option("bucket", "1")
+                .build(),
+            "bucket = -1",
+        );
+
+        // Combination reported in review: PK + bucket=1 + row tracking + DE.
+        assert_config_invalid(
+            Schema::builder()
+                .column("id", DataType::Int(IntType::new()))
+                .primary_key(vec!["id".to_string()])
+                .option("bucket", "1")
+                .option("row-tracking.enabled", "true")
+                .option("data-evolution.enabled", "true")
+                .build(),
+            "row tracking",
+        );
+    }
+
+    #[test]
+    fn test_data_evolution_rejects_incremental_clustering() {
+        assert_config_invalid(
+            Schema::builder()
+                .column("id", DataType::Int(IntType::new()))
+                .option("row-tracking.enabled", "true")
+                .option("data-evolution.enabled", "true")
+                .option("clustering.incremental", "true")
+                .build(),
+            "clustering.incremental",
+        );
+    }
+
+    #[test]
+    fn test_data_evolution_requires_row_tracking() {
+        assert_config_invalid(
+            Schema::builder()
+                .column("id", DataType::Int(IntType::new()))
+                .option("data-evolution.enabled", "true")
+                .build(),
+            "row-tracking.enabled",
+        );
+
+        Schema::builder()
+            .column("id", DataType::Int(IntType::new()))
+            .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
+            .build()
+            .unwrap();
+
+        let table_schema = TableSchema::new(
+            0,
+            &Schema::builder()
+                .column("id", DataType::Int(IntType::new()))
+                .build()
+                .unwrap(),
+        );
+        assert_config_invalid(
+            
table_schema.apply_changes(vec![crate::spec::SchemaChange::set_option(
+                "data-evolution.enabled".to_string(),
+                "true".to_string(),
+            )]),
+            "row-tracking.enabled",
+        );
+    }
+
     #[test]
     fn test_blob_schema_validation_requires_data_evolution() {
         let err = Schema::builder()
@@ -2537,6 +2653,7 @@ mod tests {
         let err = Schema::builder()
             .column("payload", DataType::Blob(BlobType::new()))
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap_err();
 
@@ -2553,6 +2670,7 @@ mod tests {
             .column("payload", DataType::Blob(BlobType::new()))
             .partition_keys(["payload"])
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap_err();
 
@@ -2568,6 +2686,7 @@ mod tests {
             .column("id", DataType::Int(IntType::new()))
             .column("payload", DataType::Blob(BlobType::new()))
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
 
@@ -2587,6 +2706,7 @@ mod tests {
             )
             .option("blob-field", "payload")
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
 
@@ -2606,6 +2726,7 @@ mod tests {
                 Some("__BLOB_FIELD; payload bytes".to_string()),
             )
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
 
@@ -2630,6 +2751,7 @@ mod tests {
                 Some("__BLOB_FIEL; payload bytes".to_string()),
             )
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap_err();
 
@@ -2652,6 +2774,7 @@ mod tests {
                 Some("__BLOB_FIELD ; payload bytes".to_string()),
             )
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap_err();
 
@@ -2675,6 +2798,7 @@ mod tests {
             )
             .option("blob-descriptor-field", "preview")
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap_err();
 
@@ -2694,6 +2818,7 @@ mod tests {
             .column("thumb", DataType::Blob(BlobType::new()))
             .option("blob-descriptor-field", "thumb")
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
 
@@ -2730,6 +2855,7 @@ mod tests {
         let schema = Schema::builder()
             .column("id", DataType::Int(IntType::new()))
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
 
@@ -2758,6 +2884,7 @@ mod tests {
             .column("payload", DataType::Int(IntType::new()))
             .option("blob-field", "payload")
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap_err();
 
@@ -3339,6 +3466,7 @@ mod tests {
                     
DataType::Array(ArrayType::new(DataType::Blob(BlobType::new()))),
                 )
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -3386,6 +3514,7 @@ mod tests {
                     
DataType::Array(ArrayType::new(DataType::Blob(BlobType::new()))),
                 )
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -3473,6 +3602,10 @@ mod tests {
                     "data-evolution.enabled".to_string(),
                     "true".to_string(),
                 ),
+                crate::spec::SchemaChange::set_option(
+                    "row-tracking.enabled".to_string(),
+                    "true".to_string(),
+                ),
                 crate::spec::SchemaChange::add_column(
                     "payload".to_string(),
                     DataType::Blob(BlobType::new()),
@@ -3719,7 +3852,7 @@ mod tests {
             .unwrap_err();
         assert!(
             matches!(err, crate::Error::ConfigInvalid { ref message }
-                if message.contains("Row tracking config must enabled")),
+                if message.contains("row-tracking.enabled")),
             "dedicated VECTOR storage should require row-tracking.enabled, got 
{err:?}"
         );
     }
diff --git a/crates/paimon/src/table/cow_writer.rs 
b/crates/paimon/src/table/cow_writer.rs
index 1f77c872..d0913e26 100644
--- a/crates/paimon/src/table/cow_writer.rs
+++ b/crates/paimon/src/table/cow_writer.rs
@@ -576,6 +576,7 @@ mod tests {
         let schema = Schema::builder()
             .column("id", DataType::Int(IntType::new()))
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
         let table = Table::new(
diff --git a/crates/paimon/src/table/data_evolution_reader.rs 
b/crates/paimon/src/table/data_evolution_reader.rs
index cf04829b..f3f10a25 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -3628,6 +3628,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -3714,6 +3715,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -3813,6 +3815,7 @@ mod tests {
                     
DataType::Array(ArrayType::new(DataType::Blob(BlobType::new()))),
                 )
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -3936,6 +3939,7 @@ mod tests {
                     
DataType::Array(ArrayType::new(DataType::Blob(BlobType::new()))),
                 )
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4066,6 +4070,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4233,6 +4238,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4243,6 +4249,7 @@ mod tests {
                 .column("payload", DataType::Blob(BlobType::new()))
                 .column("added", DataType::Int(IntType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4362,6 +4369,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4466,6 +4474,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4541,6 +4550,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4631,6 +4641,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4729,6 +4740,7 @@ mod tests {
                 .column("payload", DataType::Blob(BlobType::new()))
                 .column("payload2", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -4959,6 +4971,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5079,6 +5092,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5217,6 +5231,7 @@ mod tests {
                 .column("emb1", vector_float_type(2))
                 .column("emb2", vector_float_type(3))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5347,6 +5362,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5419,6 +5435,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5514,6 +5531,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5624,6 +5642,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5795,6 +5814,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -5925,6 +5945,10 @@ mod tests {
                 Some(vec!["payload"]),
             ));
         }
+        for file in &mut files {
+            file.first_row_id = None;
+            file.file_source = Some(0);
+        }
 
         let file_io = FileIOBuilder::new("file").build().unwrap();
         let table_schema = TableSchema::new(
@@ -5934,6 +5958,7 @@ mod tests {
                 .column("embedding", vector_float_type(2))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -6043,7 +6068,7 @@ mod tests {
         let normal_path = bucket_dir.join("data.parquet");
         write_int_parquet_file(&normal_path, vec![("id", vec![1, 2, 3, 4, 5, 
6])], None);
 
-        let files = vec![
+        let mut files = vec![
             data_file_meta_with_path(
                 "data.parquet",
                 0,
@@ -6060,6 +6085,14 @@ mod tests {
                 1,
                 Some(vec!["embedding"]),
             ),
+            data_file_meta_with_path(
+                "emb-middle.vector.parquet",
+                2,
+                2,
+                1,
+                1,
+                Some(vec!["embedding"]),
+            ),
             data_file_meta_with_path(
                 "emb-right.vector.parquet",
                 4,
@@ -6069,8 +6102,13 @@ mod tests {
                 Some(vec!["embedding"]),
             ),
             data_file_meta_with_path("payload-left.blob", 0, 2, 1, 1, 
Some(vec!["payload"])),
+            data_file_meta_with_path("payload-middle.blob", 2, 2, 1, 1, 
Some(vec!["payload"])),
             data_file_meta_with_path("payload-right.blob", 4, 2, 1, 1, 
Some(vec!["payload"])),
         ];
+        for file in &mut files {
+            file.first_row_id = None;
+            file.file_source = Some(0);
+        }
 
         let file_io = FileIOBuilder::new("file").build().unwrap();
         let table_schema = TableSchema::new(
@@ -6080,6 +6118,7 @@ mod tests {
                 .column("embedding", vector_float_type(2))
                 .column("payload", DataType::Blob(BlobType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -6090,7 +6129,8 @@ mod tests {
             table_schema,
             None,
         );
-        TableCommit::new(table.clone(), 
"missing-selected-dedicated-test".to_string())
+        let commit = TableCommit::new(table.clone(), 
"missing-selected-dedicated-test".to_string());
+        commit
             .commit(vec![CommitMessage::new(
                 BinaryRowBuilder::new(0).build_serialized(),
                 0,
@@ -6098,6 +6138,23 @@ mod tests {
             )])
             .await
             .unwrap();
+        let mut delete_middle =
+            CommitMessage::new(BinaryRowBuilder::new(0).build_serialized(), 0, 
Vec::new());
+        delete_middle.deleted_files = vec![
+            data_file_meta_with_path(
+                "emb-middle.vector.parquet",
+                2,
+                2,
+                1,
+                1,
+                Some(vec!["embedding"]),
+            ),
+            data_file_meta_with_path("payload-middle.blob", 2, 2, 1, 1, 
Some(vec!["payload"])),
+        ];
+        for file in &mut delete_middle.deleted_files {
+            file.file_source = Some(0);
+        }
+        commit.commit(vec![delete_middle]).await.unwrap();
 
         for (field, provider_kind) in [("embedding", "Vector"), ("payload", 
"Blob")] {
             let mut builder = table.new_read_builder();
@@ -6226,6 +6283,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("embedding", vector_float_type(2))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -6507,6 +6565,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("value", DataType::Int(IntType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -6536,6 +6595,7 @@ mod tests {
                 .column("payload", DataType::Blob(BlobType::new()))
                 .column("id", DataType::Int(IntType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
         );
@@ -6869,6 +6929,7 @@ mod tests {
                 .column("id", DataType::Int(IntType::new()))
                 .column("value", DataType::Int(IntType::new()))
                 .option("data-evolution.enabled", "true")
+                .option("row-tracking.enabled", "true")
                 .option("read.batch-size", "2")
                 .build()
                 .unwrap(),
diff --git a/crates/paimon/src/table/hybrid_search_builder.rs 
b/crates/paimon/src/table/hybrid_search_builder.rs
index a276f30d..3f5006cb 100644
--- a/crates/paimon/src/table/hybrid_search_builder.rs
+++ b/crates/paimon/src/table/hybrid_search_builder.rs
@@ -1725,7 +1725,6 @@ mod pk_hybrid_tests {
                     )
                 })
                 .column(TEXT_COLUMN, 
DataType::VarChar(VarCharType::string_type()))
-                .option("bucket", "1")
                 .option("row-tracking.enabled", "true")
                 .build()
                 .unwrap(),
diff --git a/crates/paimon/src/table/lumina_index_build_builder.rs 
b/crates/paimon/src/table/lumina_index_build_builder.rs
index b64a8e90..e98aedbe 100644
--- a/crates/paimon/src/table/lumina_index_build_builder.rs
+++ b/crates/paimon/src/table/lumina_index_build_builder.rs
@@ -265,6 +265,11 @@ pub(crate) struct LuminaIndexShard {
 }
 
 fn validate_table_options(table: &Table, core_options: &CoreOptions) -> 
Result<()> {
+    if !table.schema().primary_keys().is_empty() {
+        return Err(Error::Unsupported {
+            message: "Lumina index build does not support primary-key 
tables".to_string(),
+        });
+    }
     if !core_options.row_tracking_enabled() {
         return Err(Error::DataInvalid {
             message: "Lumina index build requires 'row-tracking.enabled' = 
'true'".to_string(),
@@ -283,11 +288,6 @@ fn validate_table_options(table: &Table, core_options: 
&CoreOptions) -> Result<(
             source: None,
         });
     }
-    if !table.schema().primary_keys().is_empty() {
-        return Err(Error::Unsupported {
-            message: "Lumina index build does not support primary-key 
tables".to_string(),
-        });
-    }
     if core_options.deletion_vectors_enabled() {
         return Err(Error::Unsupported {
             message:
@@ -1225,7 +1225,7 @@ mod tests {
     #[tokio::test]
     async fn test_execute_rejects_primary_key_table() {
         let table = test_table_with_schema(
-            vector_schema_builder(table_options("10"))
+            vector_schema_builder(HashMap::new())
                 .primary_key(["id"])
                 .build()
                 .unwrap(),
diff --git a/crates/paimon/src/table/read_builder.rs 
b/crates/paimon/src/table/read_builder.rs
index 9b482d33..ec8ef966 100644
--- a/crates/paimon/src/table/read_builder.rs
+++ b/crates/paimon/src/table/read_builder.rs
@@ -1233,7 +1233,7 @@ mod tests {
             "the conjunct must stay so the read can reject it"
         );
 
-        for (row_tracking, data_evolution) in [(true, false), (false, true), 
(true, true)] {
+        for (row_tracking, data_evolution) in [(true, false), (true, true)] {
             let table = row_id_table(row_tracking, data_evolution);
             let mut builder = PaimonReadBuilder::new(&table);
             builder.with_filter(row_id_leaf(PredicateOperator::GtEq, 102));
diff --git a/crates/paimon/src/table/table_scan.rs 
b/crates/paimon/src/table/table_scan.rs
index 35b8b1a8..5c121922 100644
--- a/crates/paimon/src/table/table_scan.rs
+++ b/crates/paimon/src/table/table_scan.rs
@@ -336,20 +336,19 @@ fn retain_manifest_entry_row_ranges(
 fn data_evolution_row_range_groups(
     data_files: Vec<DataFileMeta>,
     row_ranges: Option<&[RowRange]>,
-) -> (Vec<Vec<DataFileMeta>>, usize) {
+) -> crate::Result<(Vec<Vec<DataFileMeta>>, usize)> {
     if data_files.is_empty() {
-        return (Vec::new(), 0);
+        return Ok((Vec::new(), 0));
+    }
+    if let Some(file) = data_files.iter().find(|file| 
file.first_row_id.is_none()) {
+        return Err(crate::Error::DataInvalid {
+            message: format!("First row id of '{}' should not be null.", 
file.file_name),
+            source: None,
+        });
     }
     let Some(row_ranges) = row_ranges else {
-        return (group_by_overlapping_row_id(data_files), 0);
+        return Ok((group_by_overlapping_row_id(data_files), 0));
     };
-    let all_ranges_known = data_files.iter().all(|file| 
file.row_id_range().is_some());
-    if !all_ranges_known {
-        // Avoid unchecked row-range arithmetic in downstream grouping and keep
-        // the whole bucket as one non-raw group. The reader will then fail on
-        // invalid metadata instead of silently losing dedicated providers.
-        return (vec![data_files], 0);
-    }
     let row_id_groups = group_by_overlapping_row_id(data_files);
     let groups_before_pruning = row_id_groups.len();
 
@@ -362,7 +361,7 @@ fn data_evolution_row_range_groups(
         })
         .collect::<Vec<_>>();
     let pruned = groups_before_pruning - retained.len();
-    (retained, pruned)
+    Ok((retained, pruned))
 }
 
 fn split_row_ranges_for_files(
@@ -1930,7 +1929,7 @@ impl<'a> PaimonTableScan<'a> {
             // Data-evolution reads merge overlapping row-id groups 
column-wise.
             let file_groups: Vec<SplitGroup> = if data_evolution_enabled {
                 let (row_id_groups, groups_pruned_by_row_ranges) =
-                    data_evolution_row_range_groups(data_files, 
effective_row_ranges.as_deref());
+                    data_evolution_row_range_groups(data_files, 
effective_row_ranges.as_deref())?;
                 if let Some(trace) = trace.as_deref_mut() {
                     trace.data_evolution_groups_before_stats += 
row_id_groups.len();
                     trace.data_evolution_groups_pruned_by_row_ranges += 
groups_pruned_by_row_ranges;
@@ -2336,7 +2335,7 @@ mod tests {
     }
 
     #[test]
-    fn 
test_data_evolution_row_range_group_pruning_fails_open_on_unknown_range() {
+    fn test_data_evolution_row_range_group_rejects_unknown_range() {
         let files = vec![
             make_evo_file("unknown-anchor", 1, 6, 0, None),
             make_evo_file("left.blob", 1, 2, 0, Some(0)),
@@ -2344,16 +2343,11 @@ mod tests {
         ];
         let ranges = [RowRange::new(2, 2)];
 
-        let (groups, pruned) = data_evolution_row_range_groups(files, 
Some(&ranges));
-
-        assert_eq!(pruned, 0);
-        let mut names = groups
-            .into_iter()
-            .flatten()
-            .map(|file| file.file_name)
-            .collect::<Vec<_>>();
-        names.sort();
-        assert_eq!(names, vec!["left.blob", "right.blob", "unknown-anchor"]);
+        for row_ranges in [None, Some(ranges.as_slice())] {
+            let error = data_evolution_row_range_groups(files.clone(), 
row_ranges).unwrap_err();
+            assert!(matches!(error, Error::DataInvalid { message, .. }
+                if message == "First row id of 'unknown-anchor' should not be 
null."));
+        }
     }
 
     #[test]
diff --git a/crates/paimon/src/table/table_write.rs 
b/crates/paimon/src/table/table_write.rs
index d4a7725d..1d13f3b3 100644
--- a/crates/paimon/src/table/table_write.rs
+++ b/crates/paimon/src/table/table_write.rs
@@ -1083,6 +1083,7 @@ pub(in crate::table) mod tests {
             .column("id", DataType::Int(IntType::new()))
             .column("payload", DataType::Blob(BlobType::new()))
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
         TableSchema::new(0, &schema)
@@ -2239,6 +2240,7 @@ pub(in crate::table) mod tests {
             .column("a", DataType::Int(IntType::new()))
             .column("b", DataType::Int(IntType::new()))
             .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
             .build()
             .unwrap();
         let table = Table::new(
diff --git a/crates/paimon/tests/incremental_batch_scan_test.rs 
b/crates/paimon/tests/incremental_batch_scan_test.rs
index 5fe3cdcb..30217e7b 100644
--- a/crates/paimon/tests/incremental_batch_scan_test.rs
+++ b/crates/paimon/tests/incremental_batch_scan_test.rs
@@ -697,7 +697,6 @@ async fn diff_rejects_row_ranges_instead_of_dropping_them() 
{
             ("changelog-producer", "none"),
             ("merge-engine", "deduplicate"),
             ("bucket", "1"),
-            ("row-tracking.enabled", "true"),
             ("target-file-size", "1b"),
             ("source.split.target-size", "1b"),
             ("source.split.open-file-cost", "1b"),

Reply via email to