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

yihua pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 1e59c9eb6d9e fix(core): record the ordering field when upgrading a 
table from version 1 (#19803)
1e59c9eb6d9e is described below

commit 1e59c9eb6d9e341d5fd10e1418c0c3deb34b6579
Author: Y Ethan Guo <[email protected]>
AuthorDate: Mon Aug 31 21:30:39 2026 -0700

    fix(core): record the ordering field when upgrading a table from version 1 
(#19803)
---
 .../hudi/table/upgrade/OneToTwoUpgradeHandler.java |  85 +++++++++++++-
 .../table/upgrade/TestOneToTwoUpgradeHandler.java  | 126 +++++++++++++++++++++
 .../table/upgrade/TestUpgradeDowngradeLegacy.java  |  98 ++++++++++++++++
 .../org/apache/hudi/TestHoodieSparkSqlWriter.scala |  55 +++++++++
 4 files changed, 363 insertions(+), 1 deletion(-)

diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/upgrade/OneToTwoUpgradeHandler.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/upgrade/OneToTwoUpgradeHandler.java
index 79e29a9d2e60..0677e955dbaa 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/upgrade/OneToTwoUpgradeHandler.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/upgrade/OneToTwoUpgradeHandler.java
@@ -20,18 +20,32 @@ package org.apache.hudi.table.upgrade;
 
 import org.apache.hudi.common.config.ConfigProperty;
 import org.apache.hudi.common.engine.HoodieEngineContext;
+import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.TableSchemaResolver;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.config.HoodieWriteConfig;
 import org.apache.hudi.keygen.constant.KeyGeneratorOptions;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * Upgrade handle to assist in upgrading hoodie table from version 1 to 2.
  */
 public class OneToTwoUpgradeHandler implements UpgradeHandler {
 
+  private static final Logger LOG = 
LoggerFactory.getLogger(OneToTwoUpgradeHandler.class);
+  private static final String NESTED_FIELD_SEPARATOR = ".";
+
   @Override
   public UpgradeDowngrade.TableConfigChangeSet upgrade(
       HoodieWriteConfig config,
@@ -42,6 +56,75 @@ public class OneToTwoUpgradeHandler implements 
UpgradeHandler {
     tablePropsToAdd.put(HoodieTableConfig.PARTITION_FIELDS, 
upgradeDowngradeHelper.getPartitionColumns(config));
     tablePropsToAdd.put(HoodieTableConfig.RECORDKEY_FIELDS, 
config.getString(KeyGeneratorOptions.RECORDKEY_FIELD_NAME.key()));
     tablePropsToAdd.put(HoodieTableConfig.BASE_FILE_FORMAT, 
config.getString(HoodieTableConfig.BASE_FILE_FORMAT));
-    return new UpgradeDowngrade.TableConfigChangeSet();
+    HoodieTableMetaClient metaClient = upgradeDowngradeHelper.getTable(config, 
context).getMetaClient();
+    getOrderingFieldsToRecord(config, metaClient)
+        .ifPresent(orderingFields -> 
tablePropsToAdd.put(HoodieTableConfig.PRECOMBINE_FIELD, orderingFields));
+    return new UpgradeDowngrade.TableConfigChangeSet(tablePropsToAdd, 
Collections.emptySet());
+  }
+
+  /**
+   * Returns the ordering fields to record in {@code hoodie.properties}, if 
they can be established.
+   *
+   * <p>The ordering field is only written at table creation, and only since 
0.8.0, so a table
+   * created before that records none even though its writer merges on one, 
and everything that
+   * resolves ordering from the table config alone then falls back to no 
ordering. Only a table that
+   * records none is filled in, and only from this upgrade. A top level field 
is recorded once the
+   * schema has it, which is what keeps out a materialized default; a field 
nested under dot
+   * notation is recorded as configured, since a default is never nested. They 
are recorded under
+   * the deprecated {@link HoodieTableConfig#PRECOMBINE_FIELD}, the key a 
table at this version is
+   * expected to carry and the one a 0.x reader understands; {@code 
EightToNineUpgradeHandler}
+   * migrates it to {@link HoodieTableConfig#ORDERING_FIELDS} in turn.
+   */
+  private static Option<String> getOrderingFieldsToRecord(HoodieWriteConfig 
config, HoodieTableMetaClient metaClient) {
+    if (!metaClient.getTableConfig().getOrderingFields().isEmpty()) {
+      // the table already records them, and the upgrade only fills in missing 
ordering fields
+      return Option.empty();
+    }
+    List<String> orderingFields = config.getPreCombineFields();
+    if (orderingFields.isEmpty()) {
+      return Option.empty();
+    }
+    // only an explicit config can name a nested field, so take the writer at 
its word for those
+    List<String> topLevelFields = orderingFields.stream()
+        .filter(field -> !field.contains(NESTED_FIELD_SEPARATOR))
+        .collect(Collectors.toList());
+    if (topLevelFields.isEmpty()) {
+      return Option.of(String.join(",", orderingFields));
+    }
+    Option<HoodieSchema> schema = resolveSchema(config, metaClient);
+    if (!schema.isPresent()) {
+      LOG.warn("Skipping the ordering fields {} while upgrading {} to table 
version two: no schema is available to "
+          + "resolve them against", orderingFields, config.getBasePath());
+      return Option.empty();
+    }
+    if (!topLevelFields.stream().allMatch(field -> 
schema.get().getField(field).isPresent())) {
+      LOG.warn("Skipping the ordering fields {} while upgrading {} to table 
version two: the schema does not have "
+          + "all of them as top level fields", orderingFields, 
config.getBasePath());
+      return Option.empty();
+    }
+    return Option.of(String.join(",", orderingFields));
+  }
+
+  /**
+   * The table's own schema, falling back to the writer's for a table with no 
committed data, and to
+   * nothing if neither can be read.
+   */
+  private static Option<HoodieSchema> resolveSchema(HoodieWriteConfig config, 
HoodieTableMetaClient metaClient) {
+    try {
+      Option<HoodieSchema> tableSchema = new 
TableSchemaResolver(metaClient).getTableSchemaIfPresent(false);
+      if (tableSchema.isPresent()) {
+        return tableSchema;
+      }
+      String writeSchema = config.getWriteSchema();
+      return StringUtils.isNullOrEmpty(writeSchema)
+          ? Option.empty()
+          : Option.of(HoodieSchema.parse(writeSchema));
+    } catch (Exception e) {
+      // the upgrade gates every write, so a schema that cannot be read or 
parsed leaves the fields
+      // unrecorded rather than blocking the table
+      LOG.warn("Failed to resolve the schema of " + config.getBasePath()
+          + " while upgrading to table version two, leaving the ordering 
fields unrecorded", e);
+      return Option.empty();
+    }
   }
 }
diff --git 
a/hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/table/upgrade/TestOneToTwoUpgradeHandler.java
 
b/hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/table/upgrade/TestOneToTwoUpgradeHandler.java
new file mode 100644
index 000000000000..b595d86642e4
--- /dev/null
+++ 
b/hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/table/upgrade/TestOneToTwoUpgradeHandler.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hudi.table.upgrade;
+
+import org.apache.hudi.common.config.ConfigProperty;
+import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.keygen.constant.KeyGeneratorOptions;
+import org.apache.hudi.testutils.HoodieClientTestBase;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Unit tests {@link OneToTwoUpgradeHandler}. The handler is exercised 
directly rather than through
+ * {@link UpgradeDowngrade}, which rejects any table below version six.
+ */
+class TestOneToTwoUpgradeHandler extends HoodieClientTestBase {
+
+  @Test
+  void testUpgradeRecordsKeySchemaAndOrderingField() {
+    Map<ConfigProperty, String> tableProps = 
upgrade(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA, "timestamp");
+    assertEquals("uuid", tableProps.get(HoodieTableConfig.RECORDKEY_FIELDS));
+    assertEquals("partition_path", 
tableProps.get(HoodieTableConfig.PARTITION_FIELDS));
+    assertEquals(HoodieTableConfig.BASE_FILE_FORMAT.defaultValue().name(), 
tableProps.get(HoodieTableConfig.BASE_FILE_FORMAT));
+    assertEquals("timestamp", 
tableProps.get(HoodieTableConfig.PRECOMBINE_FIELD));
+  }
+
+  /**
+   * A nested ordering field is recorded as configured. The schema check 
exists to catch a
+   * materialized default, and a default is never nested, so an explicitly 
configured nested field
+   * is taken at face value.
+   */
+  @ParameterizedTest
+  @ValueSource(strings = {"fare.amount", "not_a_record.not_a_column"})
+  void testRecordsNestedOrderingFieldAsConfigured(String orderingField) {
+    assertEquals(orderingField,
+        upgrade(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA, 
orderingField).get(HoodieTableConfig.PRECOMBINE_FIELD));
+  }
+
+  /** A table that already records ordering fields keeps them, even if the 
writer configures another. */
+  @Test
+  void testLeavesRecordedOrderingFieldsAlone() {
+    Properties recordedProps = new Properties();
+    recordedProps.setProperty(HoodieTableConfig.PRECOMBINE_FIELD.key(), 
"timestamp");
+    HoodieTableConfig.update(metaClient.getStorage(), 
metaClient.getMetaPath(), recordedProps);
+
+    assertNull(upgrade(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA, 
"_row_key").get(HoodieTableConfig.PRECOMBINE_FIELD));
+  }
+
+  /** Every field of a multi field ordering config has to resolve for any of 
it to be recorded. */
+  @Test
+  void testRecordsMultipleOrderingFields() {
+    assertEquals("timestamp,_row_key",
+        upgrade(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA, 
"timestamp,_row_key").get(HoodieTableConfig.PRECOMBINE_FIELD));
+    assertNull(upgrade(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA, 
"timestamp,not_a_column").get(HoodieTableConfig.PRECOMBINE_FIELD));
+  }
+
+  /**
+   * An ordering field the schema cannot resolve is left unrecorded, so the 
table config never ends
+   * up with one no reader can resolve.
+   */
+  @ParameterizedTest
+  @ValueSource(strings = {"not_a_column", "ts"})
+  void testSkipsOrderingFieldTheSchemaCannotResolve(String orderingField) {
+    Map<ConfigProperty, String> tableProps = 
upgrade(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA, orderingField);
+    assertEquals("uuid", tableProps.get(HoodieTableConfig.RECORDKEY_FIELDS));
+    assertNull(tableProps.get(HoodieTableConfig.PRECOMBINE_FIELD));
+  }
+
+  @Test
+  void testSkipsEmptyOrderingField() {
+    assertNull(upgrade(HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA, 
"").get(HoodieTableConfig.PRECOMBINE_FIELD));
+  }
+
+  /** With no committed data and no writer schema there is nothing to resolve 
the field against. */
+  @Test
+  void testSkipsOrderingFieldWhenNoSchemaIsAvailable() {
+    HoodieWriteConfig config = HoodieWriteConfig.newBuilder()
+        
.withPath(basePath).forTable("test-trip-table").withProps(keySchemaParams("timestamp")).build();
+    Map<ConfigProperty, String> tableProps = new OneToTwoUpgradeHandler()
+        .upgrade(config, context, null, 
SparkUpgradeDowngradeHelper.getInstance()).propertiesToUpdate();
+    assertNull(tableProps.get(HoodieTableConfig.PRECOMBINE_FIELD));
+  }
+
+  private Map<ConfigProperty, String> upgrade(String schema, String 
orderingField) {
+    HoodieWriteConfig config = 
getConfigBuilder(schema).withProps(keySchemaParams(orderingField)).build();
+    return new OneToTwoUpgradeHandler()
+        .upgrade(config, context, null, 
SparkUpgradeDowngradeHelper.getInstance())
+        .propertiesToUpdate();
+  }
+
+  private Map<String, String> keySchemaParams(String orderingField) {
+    Map<String, String> params = new HashMap<>();
+    params.put(KeyGeneratorOptions.RECORDKEY_FIELD_NAME.key(), "uuid");
+    params.put(KeyGeneratorOptions.PARTITIONPATH_FIELD_NAME.key(), 
"partition_path");
+    params.put(HoodieTableConfig.BASE_FILE_FORMAT.key(), 
HoodieTableConfig.BASE_FILE_FORMAT.defaultValue().name());
+    params.put(HoodieWriteConfig.PRECOMBINE_FIELD_NAME.key(), orderingField);
+    return params;
+  }
+}
diff --git 
a/hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/table/upgrade/TestUpgradeDowngradeLegacy.java
 
b/hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/table/upgrade/TestUpgradeDowngradeLegacy.java
index 34f7f3c48b9c..02b95111e6c0 100644
--- 
a/hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/table/upgrade/TestUpgradeDowngradeLegacy.java
+++ 
b/hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/table/upgrade/TestUpgradeDowngradeLegacy.java
@@ -269,6 +269,7 @@ public class TestUpgradeDowngradeLegacy extends 
HoodieClientTestBase {
     // init config, table and client.
     Map<String, String> params = new HashMap<>();
     addNewTableParamsToProps(params);
+    params.put(HoodieWriteConfig.PRECOMBINE_FIELD_NAME.key(), "timestamp");
     if (tableType == HoodieTableType.MERGE_ON_READ) {
       params.put(TYPE.key(), HoodieTableType.MERGE_ON_READ.name());
       metaClient = HoodieTestUtils.init(storageConf, basePath, 
HoodieTableType.MERGE_ON_READ);
@@ -280,6 +281,8 @@ public class TestUpgradeDowngradeLegacy extends 
HoodieClientTestBase {
 
     // downgrade table props
     downgradeTableConfigsFromTwoToOne(cfg);
+    // a table written before 0.8.0 records no ordering field at all
+    assertTrue(metaClient.getTableConfig().getOrderingFields().isEmpty());
 
     // perform upgrade
     new UpgradeDowngrade(metaClient, cfg, context, 
SparkUpgradeDowngradeHelper.getInstance())
@@ -293,6 +296,78 @@ public class TestUpgradeDowngradeLegacy extends 
HoodieClientTestBase {
 
     // verify table props
     assertTableProps(cfg);
+    // the ordering field the writer merges on is now recorded for readers 
that only see the table config
+    assertEquals(Collections.singletonList("timestamp"), 
metaClient.getTableConfig().getOrderingFields());
+  }
+
+  /**
+   * The migration case: a table with committed data, whose ordering field 
resolves against the
+   * table's own schema rather than the schema the writer brings along.
+   */
+  @Disabled("HUDI-9700")
+  @Test
+  void testUpgradeOneToTwoRecordsOrderingFieldFromTableSchema() throws 
IOException {
+    Map<String, String> params = new HashMap<>();
+    addNewTableParamsToProps(params);
+    params.put(HoodieWriteConfig.PRECOMBINE_FIELD_NAME.key(), "timestamp");
+    HoodieWriteConfig cfg = 
getConfigBuilder().withRollbackUsingMarkers(false).withProps(params).build();
+    doInsert(getHoodieWriteClient(cfg));
+
+    downgradeTableConfigsFromTwoToOne(cfg);
+    assertTrue(metaClient.getTableConfig().getOrderingFields().isEmpty());
+
+    new UpgradeDowngrade(metaClient, cfg, context, 
SparkUpgradeDowngradeHelper.getInstance())
+        .run(HoodieTableVersion.TWO, null);
+
+    metaClient = reloadMetaClientAtVersionTwo(cfg);
+    assertEquals(Collections.singletonList("timestamp"), 
metaClient.getTableConfig().getOrderingFields());
+  }
+
+  /**
+   * A table that already recorded an ordering field keeps the one it has, 
rather than having the
+   * writer's config written over it.
+   */
+  @Disabled("HUDI-9700")
+  @ParameterizedTest
+  @EnumSource(value = HoodieTableType.class)
+  void testUpgradeOneToTwoKeepsRecordedOrderingField(HoodieTableType 
tableType) throws IOException {
+    Map<String, String> params = new HashMap<>();
+    addNewTableParamsToProps(params);
+    params.put(HoodieWriteConfig.PRECOMBINE_FIELD_NAME.key(), "_row_key");
+    initTableOfType(tableType, params);
+    HoodieWriteConfig cfg = 
getConfigBuilder().withRollbackUsingMarkers(false).withProps(params).build();
+    doInsert(getHoodieWriteClient(cfg));
+
+    downgradeTableConfigsFromTwoToOne(cfg, "timestamp");
+    assertEquals(Collections.singletonList("timestamp"), 
metaClient.getTableConfig().getOrderingFields());
+
+    new UpgradeDowngrade(metaClient, cfg, context, 
SparkUpgradeDowngradeHelper.getInstance())
+        .run(HoodieTableVersion.TWO, null);
+
+    metaClient = reloadMetaClientAtVersionTwo(cfg);
+    assertEquals(Collections.singletonList("timestamp"), 
metaClient.getTableConfig().getOrderingFields());
+  }
+
+  /**
+   * An ordering field the schema cannot resolve is left unrecorded, so the 
table config never ends
+   * up with one no reader can resolve.
+   */
+  @Disabled("HUDI-9700")
+  @Test
+  void testUpgradeOneToTwoSkipsOrderingFieldMissingFromSchema() throws 
IOException {
+    Map<String, String> params = new HashMap<>();
+    addNewTableParamsToProps(params);
+    params.put(HoodieWriteConfig.PRECOMBINE_FIELD_NAME.key(), "not_a_column");
+    HoodieWriteConfig cfg = 
getConfigBuilder().withRollbackUsingMarkers(false).withProps(params).build();
+    doInsert(getHoodieWriteClient(cfg));
+
+    downgradeTableConfigsFromTwoToOne(cfg);
+
+    new UpgradeDowngrade(metaClient, cfg, context, 
SparkUpgradeDowngradeHelper.getInstance())
+        .run(HoodieTableVersion.TWO, null);
+
+    metaClient = reloadMetaClientAtVersionTwo(cfg);
+    assertTrue(metaClient.getTableConfig().getOrderingFields().isEmpty());
   }
 
   @Disabled("HUDI-9700")
@@ -501,13 +576,36 @@ public class TestUpgradeDowngradeLegacy extends 
HoodieClientTestBase {
     client.insert(writeRecords, commit1).collect();
   }
 
+  private HoodieTableMetaClient reloadMetaClientAtVersionTwo(HoodieWriteConfig 
cfg) throws IOException {
+    HoodieTableMetaClient reloaded = HoodieTableMetaClient.builder()
+        
.setConf(context.getStorageConf().newInstance()).setBasePath(cfg.getBasePath())
+        .setLayoutVersion(Option.of(new 
TimelineLayoutVersion(cfg.getTimelineLayoutVersion()))).build();
+    assertTableVersionOnDataAndMetadataTable(reloaded, HoodieTableVersion.TWO);
+    return reloaded;
+  }
+
+  private void initTableOfType(HoodieTableType tableType, Map<String, String> 
params) throws IOException {
+    if (tableType == HoodieTableType.MERGE_ON_READ) {
+      params.put(TYPE.key(), HoodieTableType.MERGE_ON_READ.name());
+      metaClient = HoodieTestUtils.init(storageConf, basePath, 
HoodieTableType.MERGE_ON_READ);
+    }
+  }
+
   private void downgradeTableConfigsFromTwoToOne(HoodieWriteConfig cfg) throws 
IOException {
+    downgradeTableConfigsFromTwoToOne(cfg, null);
+  }
+
+  private void downgradeTableConfigsFromTwoToOne(HoodieWriteConfig cfg, String 
orderingField) throws IOException {
     Properties properties = new Properties(cfg.getProps());
     properties.remove(HoodieTableConfig.RECORDKEY_FIELDS.key());
     properties.remove(HoodieTableConfig.PARTITION_FIELDS.key());
     properties.remove(HoodieTableConfig.NAME.key());
     properties.remove(BASE_FILE_FORMAT.key());
+    properties.remove(HoodieTableConfig.PRECOMBINE_FIELD.key());
     properties.setProperty(HoodieTableConfig.VERSION.key(), "1");
+    if (orderingField != null) {
+      properties.setProperty(HoodieTableConfig.PRECOMBINE_FIELD.key(), 
orderingField);
+    }
 
     metaClient = HoodieTestUtils.init(storageConf, basePath, getTableType(), 
properties);
     // set hoodie.table.version to 1 in hoodie.properties file
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieSparkSqlWriter.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieSparkSqlWriter.scala
index e4ae6bbdd086..7177e6e527bc 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieSparkSqlWriter.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieSparkSqlWriter.scala
@@ -23,6 +23,7 @@ import org.apache.hudi.common.config.{HoodieConfig, 
HoodieMetadataConfig, Record
 import org.apache.hudi.common.model.{DefaultHoodieRecordPayload, 
HoodieFileFormat, HoodieRecord, HoodieRecordPayload, 
HoodieReplaceCommitMetadata, HoodieTableType, MetaFieldsMode, 
WriteOperationType}
 import org.apache.hudi.common.schema.HoodieSchema
 import org.apache.hudi.common.table.{HoodieTableConfig, HoodieTableMetaClient, 
TableSchemaResolver}
+import org.apache.hudi.common.table.HoodieTableVersion
 import org.apache.hudi.common.table.timeline.{HoodieTimeline, TimelineUtils}
 import org.apache.hudi.common.testutils.HoodieTestDataGenerator
 import org.apache.hudi.config.{HoodieBootstrapConfig, HoodieIndexConfig, 
HoodieWriteConfig}
@@ -40,6 +41,7 @@ import org.apache.spark.sql.{DataFrame, Row, SaveMode, 
SparkSession}
 import org.apache.spark.sql.functions.{expr, lit}
 import org.apache.spark.sql.hudi.command.SqlKeyGenerator
 import org.junit.jupiter.api.Assertions.{assertEquals, assertFalse, 
assertNotNull, assertNull, assertTrue, fail}
+import org.junit.jupiter.api.Disabled
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.{Arguments, CsvSource, EnumSource, 
MethodSource, ValueSource}
@@ -61,6 +63,59 @@ import scala.collection.JavaConverters._
  */
 class TestHoodieSparkSqlWriter extends HoodieSparkWriterTestBase {
 
+  case class OrderedRecord(uuid: String, version: Long, ts: Long, value: 
String)
+
+  /**
+   * A writer that configures no ordering field resolves it from the table 
config, so without the
+   * field the version 1 to 2 upgrade records, the "ts" fallback lets an older 
record overwrite a
+   * newer one. Disabled alongside the rest of the legacy upgrade coverage 
under HUDI-9700, since
+   * UpgradeDowngrade refuses any table below version 6 and the upgrading 
write throws before the
+   * backfill runs.
+   */
+  @Disabled("HUDI-9700")
+  @Test
+  def testUpgradeFromTableVersionOneRestoresOrderingOnUpdates(): Unit = {
+    val writeParams = Map("path" -> tempBasePath,
+      HoodieWriteConfig.TBL_NAME.key -> hoodieFooTableName,
+      DataSourceWriteOptions.RECORDKEY_FIELD.key -> "uuid",
+      DataSourceWriteOptions.PARTITIONPATH_FIELD.key -> "",
+      DataSourceWriteOptions.KEYGENERATOR_CLASS_NAME.key -> 
classOf[NonpartitionedKeyGenerator].getName,
+      DataSourceWriteOptions.PAYLOAD_CLASS_NAME.key -> 
classOf[DefaultHoodieRecordPayload].getName)
+    val orderingParams = writeParams + 
(DataSourceWriteOptions.PRECOMBINE_FIELD.key -> "version")
+    HoodieSparkSqlWriter.write(sqlContext, SaveMode.Overwrite, orderingParams,
+      orderedRecordFrame("key1", version = 2, ts = 1, value = "new"))
+
+    // a table written before 0.8.0 records no ordering field
+    dropRecordedOrderingFieldAndSetVersionOne()
+
+    // the upgrading write records the ordering field it merges on
+    HoodieSparkSqlWriter.write(sqlContext, SaveMode.Append, orderingParams,
+      orderedRecordFrame("key2", version = 1, ts = 1, value = "other"))
+    assertEquals(Collections.singletonList("version"),
+      createMetaClient(spark, tempBasePath).getTableConfig.getOrderingFields)
+
+    // the lower version loses the merge even though its "ts" is higher
+    HoodieSparkSqlWriter.write(sqlContext, SaveMode.Append, writeParams,
+      orderedRecordFrame("key1", version = 1, ts = 5, value = "old"))
+    assertEquals("new", readValueOf("key1"))
+  }
+
+  private def orderedRecordFrame(uuid: String, version: Long, ts: Long, value: 
String): DataFrame =
+    spark.createDataFrame(Seq(OrderedRecord(uuid, version, ts, value)))
+
+  private def dropRecordedOrderingFieldAndSetVersionOne(): Unit = {
+    val metaClient = createMetaClient(spark, tempBasePath)
+    HoodieTableConfig.delete(metaClient.getStorage, metaClient.getMetaPath,
+      Collections.singleton(HoodieTableConfig.PRECOMBINE_FIELD.key))
+    val versionProps = new java.util.Properties()
+    versionProps.setProperty(HoodieTableConfig.VERSION.key, 
String.valueOf(HoodieTableVersion.ONE.versionCode))
+    HoodieTableConfig.update(metaClient.getStorage, metaClient.getMetaPath, 
versionProps)
+  }
+
+  private def readValueOf(uuid: String): String =
+    spark.read.format("hudi").load(tempBasePath).where(s"uuid = '$uuid'")
+      .select("value").collect().head.getString(0)
+
   /**
    * Local utility method for performing bulk insert  tests.
    *

Reply via email to