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 6ac51eb51807 fix(core): write the key generator class for tables below 
version 8 (#20276)
6ac51eb51807 is described below

commit 6ac51eb518073d6a6fdccc200d38ffed45755925
Author: Y Ethan Guo <[email protected]>
AuthorDate: Mon Oct 26 14:40:00 2026 -0700

    fix(core): write the key generator class for tables below version 8 (#20276)
---
 .../apache/hudi/client/BaseHoodieWriteClient.java  |  35 ++++-
 .../hudi/common/table/HoodieTableConfig.java       |  18 +++
 .../hudi/common/testutils/HoodieTestUtils.java     |  15 +++
 .../org/apache/hudi/utils/TestStreamerUtil.java    |  99 ++++++++------
 .../common/table/TestHoodieTableMetaClient.java    |  74 +++++++++--
 .../org/apache/hudi/TestHoodieSparkSqlWriter.scala |  24 ----
 .../hudi/TestMultipleTableVersionWriting.scala     |  17 ---
 .../apache/hudi/functional/TestCOWDataSource.scala | 131 +++++++++++++-----
 .../apache/hudi/functional/TestMORDataSource.scala |  52 ++++----
 .../spark/sql/hudi/ddl/TestCreateTable.scala       | 148 +++++++++++++--------
 .../deltastreamer/TestHoodieDeltaStreamer.java     |  74 ++++++++++-
 11 files changed, 478 insertions(+), 209 deletions(-)

diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java
index d93108c35b1f..26f26900b201 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java
@@ -116,6 +116,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
@@ -1143,7 +1144,7 @@ public abstract class BaseHoodieWriteClient<T, I, K, O> 
extends BaseHoodieClient
    */
   @VisibleForTesting
   String startCommit(Option<String> providedInstantTime, String actionType, 
HoodieTableMetaClient metaClient) {
-    if (needsUpgrade(metaClient)) {
+    if (needsUpgradeOrBackfill(metaClient)) {
       // unclear what instant to use, since upgrade does have a given instant.
       executeUsingTxnManager(Option.empty(), () -> tryUpgrade(metaClient, 
Option.empty()));
     }
@@ -1475,7 +1476,7 @@ public abstract class BaseHoodieWriteClient<T, I, K, O> 
extends BaseHoodieClient
       ownerInstant = 
Option.of(metaClient.createNewInstant(HoodieInstant.State.INFLIGHT, 
CommitUtils.getCommitActionType(operationType,
           metaClient.getTableType()), instantTime.get()));
     }
-    boolean requiresInitTable = needsUpgrade(metaClient) || 
config.isMetadataTableEnabled();
+    boolean requiresInitTable = needsUpgradeOrBackfill(metaClient) || 
config.isMetadataTableEnabled();
     if (requiresInitTable) {
       executeUsingTxnManager(ownerInstant, () -> {
         tryUpgrade(metaClient, instantTime);
@@ -1695,8 +1696,10 @@ public abstract class BaseHoodieWriteClient<T, I, K, O> 
extends BaseHoodieClient
   }
 
   /**
-   * Upgrades the hoodie table if need be when moving to a new Hudi version.
-   * This method is called within a lock. Try to avoid double locking from 
within this method.
+   * Upgrades the hoodie table if need be when moving to a new Hudi version, 
and backfills the table configs
+   * that readers of the table version need but the table does not carry.
+   * This method is called within a lock, except when the Flink stream write 
coordinator calls it on start.
+   * Try to avoid double locking from within this method.
    * @param metaClient instance of {@link HoodieTableMetaClient} to use.
    * @param instantTime instant time of interest if we have one.
    */
@@ -1721,11 +1724,31 @@ public abstract class BaseHoodieWriteClient<T, I, K, O> 
extends BaseHoodieClient
       metaClient.reloadTableConfig();
       metaClient.reloadActiveTimeline();
     }
+
+    backfillTableConfigs(metaClient);
+  }
+
+  private void backfillTableConfigs(HoodieTableMetaClient metaClient) {
+    if 
(!HoodieTableConfig.getMissingKeyGeneratorClass(metaClient.getTableConfig()).isPresent())
 {
+      return;
+    }
+    // another writer may have backfilled the table since this meta client 
loaded its config
+    metaClient.reloadTableConfig();
+    Option<String> missingKeyGeneratorClass = 
HoodieTableConfig.getMissingKeyGeneratorClass(metaClient.getTableConfig());
+    if (missingKeyGeneratorClass.isPresent()) {
+      log.info("Backfilling {}={} for table {}", 
HoodieTableConfig.KEY_GENERATOR_CLASS_NAME.key(),
+          missingKeyGeneratorClass.get(), metaClient.getBasePath());
+      Properties updatedProps = new Properties();
+      
updatedProps.setProperty(HoodieTableConfig.KEY_GENERATOR_CLASS_NAME.key(), 
missingKeyGeneratorClass.get());
+      HoodieTableConfig.update(metaClient.getStorage(), 
metaClient.getMetaPath(), updatedProps);
+      metaClient.reloadTableConfig();
+    }
   }
 
-  private boolean needsUpgrade(HoodieTableMetaClient metaClient) {
+  private boolean needsUpgradeOrBackfill(HoodieTableMetaClient metaClient) {
     UpgradeDowngrade upgradeDowngrade = new UpgradeDowngrade(metaClient, 
config, context, upgradeDowngradeHelper);
-    return upgradeDowngrade.needsUpgrade(config.getWriteVersion());
+    return upgradeDowngrade.needsUpgrade(config.getWriteVersion())
+        || 
HoodieTableConfig.getMissingKeyGeneratorClass(metaClient.getTableConfig()).isPresent();
   }
 
   /**
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/HoodieTableConfig.java 
b/hudi-common/src/main/java/org/apache/hudi/common/table/HoodieTableConfig.java
index 538bc467a393..6c9e935b878e 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/HoodieTableConfig.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/HoodieTableConfig.java
@@ -701,12 +701,30 @@ public class HoodieTableConfig extends HoodieConfig {
         hoodieConfig.setValue(POPULATE_META_FIELDS,
             Boolean.toString(metaFieldsMode.toLegacyPopulateMetaFields()));
       }
+      getMissingKeyGeneratorClass(hoodieConfig).ifPresent(
+          keyGeneratorClass -> hoodieConfig.setValue(KEY_GENERATOR_CLASS_NAME, 
keyGeneratorClass));
 
       dropInvalidConfigs(hoodieConfig);
       storeProperties(hoodieConfig.getProps(), outputStream, propertyPath);
     }
   }
 
+  /**
+   * Returns the key generator class that a table below version 8 records only 
through its key generator type.
+   * Hudi 0.x reads only the class, so such a table must carry it as well.
+   */
+  public static Option<String> getMissingKeyGeneratorClass(HoodieConfig 
config) {
+    if (!getTableVersion(config).lesserThan(HoodieTableVersion.EIGHT)
+        || config.contains(KEY_GENERATOR_CLASS_NAME) || 
!config.contains(KEY_GENERATOR_TYPE)) {
+      return Option.empty();
+    }
+    String keyGeneratorType = config.getString(KEY_GENERATOR_TYPE);
+    return Option.fromJavaOptional(Arrays.stream(KeyGeneratorType.values())
+        .filter(type -> type != KeyGeneratorType.USER_PROVIDED && 
type.name().equals(keyGeneratorType))
+        .map(KeyGeneratorType::getClassName)
+        .findFirst());
+  }
+
   public static long generateChecksum(Properties props) {
     if (!props.containsKey(NAME.key())) {
       throw new IllegalArgumentException(NAME.key() + " property needs to be 
specified");
diff --git 
a/hudi-common/src/test/java/org/apache/hudi/common/testutils/HoodieTestUtils.java
 
b/hudi-common/src/test/java/org/apache/hudi/common/testutils/HoodieTestUtils.java
index ae9deef4f7e5..a01ea6908bf9 100644
--- 
a/hudi-common/src/test/java/org/apache/hudi/common/testutils/HoodieTestUtils.java
+++ 
b/hudi-common/src/test/java/org/apache/hudi/common/testutils/HoodieTestUtils.java
@@ -86,6 +86,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
+import java.util.TreeMap;
 import java.util.UUID;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
@@ -554,6 +555,20 @@ public class HoodieTestUtils {
     }
   }
 
+  /**
+   * Asserts that hoodie.properties holds exactly the expected table configs, 
ignoring the checksum and the
+   * create schema. The table-config tests of each engine pin table creation 
per table version through this
+   * check, so a table config change updates their expected configs instead of 
adding a new test.
+   */
+  public static void assertTableConfigs(HoodieStorage storage, String 
basePath, Map<String, String> expectedConfigs) {
+    Map<String, String> actualConfigs = new TreeMap<>();
+    HoodieTableConfig.loadFromHoodieProps(storage, basePath).getProps()
+        .forEach((key, value) -> actualConfigs.put((String) key, (String) 
value));
+    actualConfigs.remove(HoodieTableConfig.TABLE_CHECKSUM.key());
+    actualConfigs.remove(HoodieTableConfig.CREATE_SCHEMA.key());
+    assertEquals(new TreeMap<>(expectedConfigs), actualConfigs);
+  }
+
   public static void validateTableConfig(HoodieStorage storage,
                                          String basePath,
                                          Map<String, String> expectedConfigs,
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
index f917593dd8de..41e81bd8066e 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
@@ -50,9 +50,9 @@ import org.apache.hudi.hadoop.fs.HadoopFSUtils;
 import org.apache.hudi.index.HoodieIndex;
 import org.apache.hudi.io.util.FileIOUtils;
 import org.apache.hudi.keygen.ComplexAvroKeyGenerator;
-import org.apache.hudi.keygen.SimpleAvroKeyGenerator;
 import org.apache.hudi.sink.FlinkCheckpointClient;
 import org.apache.hudi.sink.muttley.AthenaIngestionGateway;
+import org.apache.hudi.storage.HoodieStorage;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.storage.StoragePathInfo;
 import org.apache.hudi.streamer.FlinkStreamerConfig;
@@ -65,6 +65,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.NullSource;
 import org.mockito.Mockito;
 
 import java.io.File;
@@ -77,9 +78,9 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import static 
org.apache.hudi.common.testutils.HoodieTestUtils.assertTableConfigs;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -185,26 +186,6 @@ class TestStreamerUtil {
     
assertTrue(StreamerUtil.getIndexConfig(conf).getBoolean(HoodieIndexConfig.BUCKET_PARTITIONER));
   }
 
-  @Test
-  void testInitTableWithSpecificVersion() throws IOException {
-    Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
-
-    // Test for partitioned table.
-    conf.set(FlinkOptions.PARTITION_PATH_FIELD, "p0,p1");
-    conf.set(FlinkOptions.WRITE_TABLE_VERSION, 
HoodieTableVersion.SIX.versionCode());
-    conf.setString(HoodieTableConfig.TABLE_STORAGE_LAYOUT.key(),
-        HoodieTableConfig.TableStorageLayout.DEFAULT.configValue());
-    StreamerUtil.initTableIfNotExists(conf);
-
-    // Validate the partition fields & preCombineField in hoodie.properties.
-    HoodieTableMetaClient metaClient1 = 
HoodieTestUtils.createMetaClient(tempFile.getAbsolutePath());
-    assertArrayEquals(metaClient1.getTableConfig().getPartitionFields().get(), 
new String[] {"p0", "p1"});
-    assertNotNull(metaClient1.getTableConfig().getKeyGeneratorClassName());
-    assertEquals(HoodieTableVersion.SIX, 
metaClient1.getTableConfig().getTableVersion());
-    assertEquals(HoodieTableConfig.TableStorageLayout.DEFAULT.configValue(),
-        conf.getString(HoodieTableConfig.TABLE_STORAGE_LAYOUT.key(), null));
-  }
-
   @Test
   void testInitLsmTreeTable() throws IOException {
     Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
@@ -235,32 +216,74 @@ class TestStreamerUtil {
     assertTrue(metaClient.getTableConfig().isLSMTreeStorageLayout());
   }
 
-  @Test
-  void testInitTableIfNotExists() throws IOException {
+  /**
+   * A null version leaves the write table version unset, so the default 
version is covered as well.
+   */
+  @ParameterizedTest
+  @NullSource
+  @EnumSource(value = HoodieTableVersion.class, names = {"SIX", "EIGHT", 
"NINE", "TEN"})
+  void testInitTableIfNotExists(HoodieTableVersion version) throws IOException 
{
     Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
+    if (version != null) {
+      conf.set(FlinkOptions.WRITE_TABLE_VERSION, version.versionCode());
+    }
+    HoodieTableVersion expectedVersion = version == null
+        ? 
HoodieTableVersion.fromVersionCode(FlinkOptions.WRITE_TABLE_VERSION.defaultValue())
 : version;
+    conf.set(FlinkOptions.ORDERING_FIELDS, "ts");
 
     // Test for partitioned table.
-    conf.set(FlinkOptions.ORDERING_FIELDS, "ts");
     conf.set(FlinkOptions.PARTITION_PATH_FIELD, "p0,p1");
     StreamerUtil.initTableIfNotExists(conf);
-
-    // Validate the partition fields & preCombineField in hoodie.properties.
-    HoodieTableMetaClient metaClient1 = 
HoodieTestUtils.createMetaClient(tempFile.getAbsolutePath());
-    assertTrue(metaClient1.getTableConfig().getPartitionFields().isPresent(),
-        "Missing partition columns in the hoodie.properties.");
-    assertArrayEquals(metaClient1.getTableConfig().getPartitionFields().get(), 
new String[] {"p0", "p1"});
-    assertEquals(metaClient1.getTableConfig().getOrderingFieldsStr().get(), 
"ts");
-    assertEquals(metaClient1.getTableConfig().getKeyGeneratorClassName(), 
SimpleAvroKeyGenerator.class.getName());
-    assertEquals(HoodieTableVersion.current(), 
metaClient1.getTableConfig().getTableVersion());
-    assertTrue(metaClient1.getTableConfig().isLSMTreeStorageLayout());
+    HoodieStorage storage = 
HoodieTestUtils.createMetaClient(tempFile.getAbsolutePath()).getStorage();
+    assertTableConfigs(storage, tempFile.getAbsolutePath(), 
expectedTableConfigs(expectedVersion, true));
 
     // Test for non-partitioned table.
     conf.removeConfig(FlinkOptions.PARTITION_PATH_FIELD);
     FileIOUtils.deleteDirectory(tempFile);
     StreamerUtil.initTableIfNotExists(conf);
-    HoodieTableMetaClient metaClient2 = 
HoodieTestUtils.createMetaClient(tempFile.getAbsolutePath());
-    assertFalse(metaClient2.getTableConfig().getPartitionFields().isPresent());
-    assertEquals(metaClient2.getTableConfig().getKeyGeneratorClassName(), 
SimpleAvroKeyGenerator.class.getName());
+    assertTableConfigs(storage, tempFile.getAbsolutePath(), 
expectedTableConfigs(expectedVersion, false));
+  }
+
+  private static Map<String, String> expectedTableConfigs(HoodieTableVersion 
version, boolean partitioned) {
+    Map<String, String> configs = new HashMap<>();
+    configs.put("hoodie.table.name", "TestHoodieTable");
+    configs.put("hoodie.table.type", "COPY_ON_WRITE");
+    configs.put("hoodie.table.version", String.valueOf(version.versionCode()));
+    configs.put("hoodie.table.recordkey.fields", "uuid");
+    configs.put("hoodie.table.ordering.fields", "ts");
+    configs.put("hoodie.table.keygenerator.type", "SIMPLE_AVRO");
+    configs.put("hoodie.table.format", "native");
+    configs.put("hoodie.table.cdc.enabled", "false");
+    configs.put("hoodie.meta.fields.mode", "ALL");
+    configs.put("hoodie.datasource.write.drop.partition.columns", "false");
+    configs.put("hoodie.datasource.write.hive_style_partitioning", "false");
+    configs.put("hoodie.datasource.write.partitionpath.urlencode", "false");
+    configs.put("hoodie.archivelog.folder", "history");
+    configs.put("hoodie.timeline.path", "timeline");
+    configs.put("hoodie.timeline.history.path", "history");
+    if (partitioned) {
+      configs.put("hoodie.table.partition.fields", "p0,p1");
+    }
+    if (version.lesserThan(HoodieTableVersion.EIGHT)) {
+      configs.put("hoodie.timeline.layout.version", "1");
+      configs.put("hoodie.table.keygenerator.class", 
"org.apache.hudi.keygen.SimpleAvroKeyGenerator");
+    } else {
+      configs.put("hoodie.timeline.layout.version", "2");
+      configs.put("hoodie.table.initial.version", 
String.valueOf(version.versionCode()));
+      configs.put("hoodie.record.merge.mode", "EVENT_TIME_ORDERING");
+    }
+    if (version.lesserThan(HoodieTableVersion.NINE)) {
+      configs.put("hoodie.compaction.payload.class", 
"org.apache.hudi.common.model.EventTimeAvroPayload");
+      configs.put("hoodie.record.merge.strategy.id", 
"eeb8d96f-b1e4-49fd-bbf8-28ac514178e5");
+    } else {
+      configs.put("hoodie.table.legacy.payload.class", 
"org.apache.hudi.common.model.EventTimeAvroPayload");
+    }
+    if (version.lesserThan(HoodieTableVersion.TEN)) {
+      configs.put("hoodie.populate.meta.fields", "true");
+    } else {
+      configs.put("hoodie.table.storage.layout", "lsm_tree");
+    }
+    return configs;
   }
 
   @Test
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/TestHoodieTableMetaClient.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/TestHoodieTableMetaClient.java
index 6ee96663cd63..ccf8335f6e53 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/TestHoodieTableMetaClient.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/TestHoodieTableMetaClient.java
@@ -43,6 +43,9 @@ import org.apache.hadoop.fs.Path;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -58,8 +61,10 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_GENERATOR;
+import static 
org.apache.hudi.common.testutils.HoodieTestUtils.assertTableConfigs;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -292,21 +297,68 @@ class TestHoodieTableMetaClient extends 
HoodieCommonTestHarness {
     }
   }
 
-  @Test
-  void testTableVersion() throws IOException {
+  /**
+   * A null version leaves the table version unset on the builder, so the 
default version is covered as well.
+   */
+  @ParameterizedTest
+  @MethodSource("tableVersionsAndKeyGeneratorSettings")
+  void testTableConfigsBasedOnVersion(HoodieTableVersion version, boolean 
setKeyGeneratorType) throws IOException {
     final String basePath = tempDir.toAbsolutePath() + Path.SEPARATOR + "t1";
-    HoodieTableMetaClient metaClient1 = HoodieTableMetaClient.newTableBuilder()
+    HoodieTableMetaClient.TableBuilder tableBuilder = 
HoodieTableMetaClient.newTableBuilder()
         .setTableType(HoodieTableType.MERGE_ON_READ.name())
         .setTableName("table-version-test")
-        .setTableVersion(HoodieTableVersion.SIX.versionCode())
-        .initTable(this.metaClient.getStorageConf(), basePath);
-    assertEquals(HoodieTableVersion.SIX, 
metaClient1.getTableConfig().getTableVersion());
+        .setRecordKeyFields("key")
+        .setPartitionFields("partition")
+        .setOrderingFields("ts");
+    if (version != null) {
+      tableBuilder.setTableVersion(version.versionCode());
+    }
+    if (setKeyGeneratorType) {
+      tableBuilder.setKeyGeneratorType(KeyGeneratorType.SIMPLE.name());
+    } else {
+      
tableBuilder.setKeyGeneratorClassProp(KeyGeneratorType.SIMPLE.getClassName());
+    }
+    HoodieTableVersion expectedVersion = version == null ? 
HoodieTableVersion.current() : version;
+    HoodieTableMetaClient createdMetaClient = 
tableBuilder.initTable(this.metaClient.getStorageConf(), basePath);
+    assertEquals(expectedVersion, 
createdMetaClient.getTableConfig().getTableVersion());
+    assertTableConfigs(createdMetaClient.getStorage(), basePath, 
expectedTableConfigs(expectedVersion));
+  }
 
-    HoodieTableMetaClient metaClient2 = HoodieTableMetaClient.builder()
-        .setConf(this.metaClient.getStorageConf())
-        .setBasePath(basePath)
-        .build();
-    assertEquals(HoodieTableVersion.SIX, 
metaClient2.getTableConfig().getTableVersion());
+  private static Stream<Arguments> tableVersionsAndKeyGeneratorSettings() {
+    return Stream.of(null, HoodieTableVersion.SIX, HoodieTableVersion.EIGHT, 
HoodieTableVersion.NINE, HoodieTableVersion.TEN)
+        .flatMap(version -> Stream.of(Arguments.of(version, false), 
Arguments.of(version, true)));
+  }
+
+  private static Map<String, String> expectedTableConfigs(HoodieTableVersion 
version) {
+    Map<String, String> configs = new HashMap<>();
+    configs.put("hoodie.table.name", "table-version-test");
+    configs.put("hoodie.table.type", "MERGE_ON_READ");
+    configs.put("hoodie.table.version", String.valueOf(version.versionCode()));
+    configs.put("hoodie.table.recordkey.fields", "key");
+    configs.put("hoodie.table.partition.fields", "partition");
+    configs.put("hoodie.table.ordering.fields", "ts");
+    configs.put("hoodie.table.keygenerator.type", "SIMPLE");
+    configs.put("hoodie.meta.fields.mode", "ALL");
+    configs.put("hoodie.datasource.write.drop.partition.columns", "false");
+    configs.put("hoodie.archivelog.folder", "archived");
+    configs.put("hoodie.timeline.path", "timeline");
+    configs.put("hoodie.timeline.history.path", "history");
+    if (version.lesserThan(HoodieTableVersion.EIGHT)) {
+      configs.put("hoodie.timeline.layout.version", "1");
+      configs.put("hoodie.table.keygenerator.class", 
"org.apache.hudi.keygen.SimpleKeyGenerator");
+    } else {
+      configs.put("hoodie.timeline.layout.version", "2");
+      configs.put("hoodie.table.initial.version", 
String.valueOf(version.versionCode()));
+      configs.put("hoodie.record.merge.mode", "EVENT_TIME_ORDERING");
+    }
+    if (version.lesserThan(HoodieTableVersion.NINE)) {
+      configs.put("hoodie.compaction.payload.class", 
"org.apache.hudi.common.model.DefaultHoodieRecordPayload");
+      configs.put("hoodie.record.merge.strategy.id", 
"eeb8d96f-b1e4-49fd-bbf8-28ac514178e5");
+    }
+    if (version.lesserThan(HoodieTableVersion.TEN)) {
+      configs.put("hoodie.populate.meta.fields", "true");
+    }
+    return configs;
   }
 
   @Test
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 7177e6e527bc..7904e9b4abde 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
@@ -294,30 +294,6 @@ class TestHoodieSparkSqlWriter extends 
HoodieSparkWriterTestBase {
     assert(HoodieSparkSqlWriter.write(sqlContext, SaveMode.Overwrite, 
tableModifier2, dataFrame2)._1)
   }
 
-  /**
-   * Test case for do not let the parttitonpath field change
-   */
-  @Test
-  def testChangeWriteTableVersion(): Unit = {
-    Seq(6, 8).foreach { tableVersion =>
-      val tempPath = s"$tempBasePath/${tableVersion}"
-      val tableModifier1 = Map(
-        "path" -> tempPath,
-        HoodieWriteConfig.TBL_NAME.key -> hoodieFooTableName,
-        "hoodie.write.table.version" -> s"$tableVersion",
-        "hoodie.datasource.write.recordkey.field" -> "uuid",
-        "hoodie.datasource.write.partitionpath.field" -> "ts"
-      )
-      val dataFrame = 
spark.createDataFrame(Seq(StringLongTest(UUID.randomUUID().toString, new 
Date().getTime)))
-      HoodieSparkSqlWriter.write(sqlContext, SaveMode.Overwrite, 
tableModifier1, dataFrame)
-
-      // Make sure table version is adopted.
-      val metaClient = HoodieTableMetaClient.builder().setBasePath(tempPath)
-        
.setConf(HadoopFSUtils.getStorageConf(spark.sessionState.newHadoopConf())).build()
-      assertEquals(metaClient.getTableConfig.getTableVersion.versionCode(), 
tableVersion)
-    }
-  }
-
   /**
    * Test case for each bulk insert sort mode
    *
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestMultipleTableVersionWriting.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestMultipleTableVersionWriting.scala
index 6c965a28d15e..3c59fe24ac17 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestMultipleTableVersionWriting.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestMultipleTableVersionWriting.scala
@@ -23,7 +23,6 @@ package org.apache.hudi
 import org.apache.hudi.common.model.HoodieTableType
 import org.apache.hudi.common.table.HoodieTableVersion
 import org.apache.hudi.common.testutils.HoodieTestUtils
-import org.apache.hudi.config.HoodieWriteConfig
 
 import org.apache.spark.sql.SaveMode
 import org.junit.jupiter.api.Assertions.assertEquals
@@ -31,22 +30,6 @@ import org.junit.jupiter.api.Test
 
 class TestMultipleTableVersionWriting extends HoodieSparkWriterTestBase {
 
-  @Test
-  def testTableVersionAndWriteVersionMatching(): Unit = {
-    val basePath = s"$tempBasePath/tbl_1"
-    val df = spark.range(1).selectExpr("1 as id", "1 as name", "1 as 
partition")
-
-    // write table with current version
-    df.write.format("hudi")
-      .option(HoodieWriteConfig.TBL_NAME.key, "tbl_1")
-      .mode(SaveMode.Overwrite)
-      .save(basePath)
-
-    val metaClient = HoodieTestUtils.createMetaClient(basePath)
-    assertEquals(HoodieTableVersion.current().versionCode(),
-      metaClient.getTableConfig.getTableVersion.versionCode())
-  }
-
   @Test
   def testThrowsExceptionForIncompatibleTableVersion(): Unit = {
     val basePath = s"$tempBasePath/tbl_2"
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestCOWDataSource.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestCOWDataSource.scala
index 38cdbb3af6c6..3bf0e029b072 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestCOWDataSource.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestCOWDataSource.scala
@@ -34,7 +34,7 @@ import org.apache.hudi.common.table.{HoodieTableConfig, 
HoodieTableMetaClient, H
 import org.apache.hudi.common.table.timeline.{HoodieInstant, HoodieTimeline, 
TimelineUtils}
 import org.apache.hudi.common.testutils.{HoodieTestDataGenerator, 
HoodieTestUtils}
 import 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.{deleteRecordsToStrings,
 recordsToStrings}
-import 
org.apache.hudi.common.testutils.HoodieTestUtils.{INSTANT_FILE_NAME_GENERATOR, 
INSTANT_GENERATOR}
+import org.apache.hudi.common.testutils.HoodieTestUtils.{assertTableConfigs, 
INSTANT_FILE_NAME_GENERATOR, INSTANT_GENERATOR}
 import org.apache.hudi.common.util.{ClusteringUtils, Option}
 import org.apache.hudi.config.HoodieWriteConfig
 import org.apache.hudi.exception.HoodieException
@@ -144,15 +144,71 @@ class TestCOWDataSource extends HoodieSparkClientTestBase 
with ScalaAssertionSup
         .options(writeOptions).mode(SaveMode.Overwrite).save(basePath)
       metaClient = 
HoodieTableMetaClient.builder.setConf(storageConf).setBasePath(basePath).build
       // If no write version is specified, use current.
-      if (!targetTableVersion.equals("null")) {
-        assertEquals(
-          
HoodieTableVersion.fromVersionCode(Integer.valueOf(targetTableVersion)),
-          metaClient.getTableConfig.getTableVersion)
+      val expectedTableVersion = if (targetTableVersion.equals("null")) {
+        HoodieTableVersion.current
       } else {
-        // Otherwise, the table version is the target table version.
-        assertEquals(HoodieTableVersion.current, 
metaClient.getTableConfig.getTableVersion)
+        HoodieTableVersion.fromVersionCode(Integer.valueOf(targetTableVersion))
       }
+      assertEquals(expectedTableVersion, 
metaClient.getTableConfig.getTableVersion)
+      assertTableConfigs(storage, basePath, 
expectedTableConfigs(expectedTableVersion).asJava)
+
+      // the next write backfills the key generator class of a table created 
without it
+      HoodieTableConfig.delete(storage, metaClient.getMetaPath,
+        
java.util.Collections.singleton(HoodieTableConfig.KEY_GENERATOR_CLASS_NAME.key))
+      inputDF.write.format("hudi").partitionBy("partition")
+        .options(writeOptions).mode(SaveMode.Append).save(basePath)
+      assertTableConfigs(storage, basePath, 
expectedTableConfigs(expectedTableVersion).asJava)
+    }
+  }
+
+  private def expectedTableConfigs(tableVersion: HoodieTableVersion): 
Map[String, String] = {
+    val commonConfigs = Map(
+      "hoodie.table.name" -> "testTableCreation",
+      "hoodie.table.type" -> "COPY_ON_WRITE",
+      "hoodie.table.version" -> tableVersion.versionCode().toString,
+      "hoodie.table.partition.fields" -> "partition",
+      "hoodie.table.keygenerator.type" -> "SIMPLE",
+      "hoodie.table.base.file.format" -> "PARQUET",
+      "hoodie.table.format" -> "native",
+      "hoodie.table.cdc.enabled" -> "false",
+      "hoodie.table.timeline.timezone" -> "LOCAL",
+      "hoodie.table.metadata.partitions.inflight" -> "",
+      "hoodie.table.index.defs.path" -> ".hoodie/.index_defs/index.json",
+      "hoodie.table.partition_extractor_class" -> 
"org.apache.hudi.hive.SinglePartPartitionValueExtractor",
+      "hoodie.meta.fields.mode" -> "ALL",
+      "hoodie.partition.metafile.use.base.format" -> "false",
+      "hoodie.datasource.write.drop.partition.columns" -> "false",
+      "hoodie.datasource.write.hive_style_partitioning" -> "false",
+      "hoodie.datasource.write.partitionpath.urlencode" -> "false",
+      "hoodie.archivelog.folder" -> "history",
+      "hoodie.timeline.path" -> "timeline",
+      "hoodie.timeline.history.path" -> "history")
+    val layoutConfigs = if (tableVersion.lesserThan(HoodieTableVersion.EIGHT)) 
{
+      Map(
+        "hoodie.timeline.layout.version" -> "1",
+        "hoodie.table.keygenerator.class" -> 
"org.apache.hudi.keygen.SimpleKeyGenerator",
+        "hoodie.table.metadata.partitions" -> "column_stats,files")
+    } else {
+      Map(
+        "hoodie.timeline.layout.version" -> "2",
+        "hoodie.table.initial.version" -> tableVersion.versionCode().toString,
+        "hoodie.record.merge.mode" -> "COMMIT_TIME_ORDERING",
+        "hoodie.table.multiple.base.file.formats.enable" -> "false",
+        "hoodie.table.metadata.partitions" -> 
"column_stats,files,partition_stats")
+    }
+    val mergeConfigs = if (tableVersion.lesserThan(HoodieTableVersion.NINE)) {
+      Map(
+        "hoodie.compaction.payload.class" -> 
"org.apache.hudi.common.model.OverwriteWithLatestAvroPayload",
+        "hoodie.record.merge.strategy.id" -> 
"ce9acb64-bde0-424c-9b91-f6ebba25356d")
+    } else {
+      Map.empty[String, String]
     }
+    val metaFieldsConfigs = if 
(tableVersion.lesserThan(HoodieTableVersion.TEN)) {
+      Map("hoodie.populate.meta.fields" -> "true")
+    } else {
+      Map("hoodie.table.storage.layout" -> "default")
+    }
+    commonConfigs ++ layoutConfigs ++ mergeConfigs ++ metaFieldsConfigs
   }
 
   @ParameterizedTest
@@ -2527,9 +2583,14 @@ class TestCOWDataSource extends 
HoodieSparkClientTestBase with ScalaAssertionSup
   @ParameterizedTest
   @MethodSource(Array("provideParamsForKeyGenTest"))
   def testUserProvidedKeyGeneratorClass(keyGenClass: Option[String],
-                                        keyGenType: Option[String]): Unit = {
+                                        keyGenType: Option[String],
+                                        tableVersion: 
Option[HoodieTableVersion]): Unit = {
     val recordType = HoodieRecordType.AVRO
-    var opts: Map[String, String] = Map()
+    // the table is new, so the guard against a legacy single-field complex 
key encoding below version 9 does not apply
+    var opts: Map[String, String] = 
Map(HoodieWriteConfig.ENABLE_COMPLEX_KEYGEN_VALIDATION.key -> "false")
+    if (tableVersion.isPresent) {
+      opts = opts ++ Map(HoodieWriteConfig.WRITE_TABLE_VERSION.key -> 
tableVersion.get.versionCode.toString)
+    }
     if (keyGenClass.isPresent) {
       opts = opts ++ Map(HoodieWriteConfig.KEYGENERATOR_CLASS_NAME.key -> 
keyGenClass.get)
     }
@@ -2560,14 +2621,19 @@ class TestCOWDataSource extends 
HoodieSparkClientTestBase with ScalaAssertionSup
       .sort("_row_key")
     var actualKeyDF = 
actualDF.select("_hoodie_record_key").sort("_hoodie_record_key")
     assertTrue(inputKeyDF.except(actualKeyDF).isEmpty && 
actualKeyDF.except(inputKeyDF).isEmpty)
-    val metaClient = getHoodieMetaClient(storageConf, basePath)
-    val actualKeyGenType = metaClient.getTableConfig
-      .getProps.getString(HoodieTableConfig.KEY_GENERATOR_TYPE.key, null)
-    assertEquals(expectedKeyGenType, actualKeyGenType)
-    // For USER_PROVIDED type, the class should exist in table config.
-    if (KeyGeneratorType.USER_PROVIDED.name == actualKeyGenType) {
-      assertEquals(keyGenClass.get(), 
metaClient.getTableConfig.getKeyGeneratorClassName)
+    val tableConfig = getHoodieMetaClient(storageConf, basePath).getTableConfig
+    val expectedTableVersion = 
TestCOWDataSource.writeTableVersionOrDefault(tableVersion)
+    assertEquals(expectedTableVersion, tableConfig.getTableVersion)
+    assertEquals(expectedKeyGenType, 
tableConfig.getProps.getString(HoodieTableConfig.KEY_GENERATOR_TYPE.key, null))
+    // A user-provided key generator is persisted as a class at every table 
version, the others below version 8.
+    val expectedKeyGenClass = if (KeyGeneratorType.USER_PROVIDED.name == 
expectedKeyGenType) {
+      keyGenClass.get
+    } else if (expectedTableVersion.lesserThan(HoodieTableVersion.EIGHT)) {
+      KeyGeneratorType.valueOf(expectedKeyGenType).getClassName
+    } else {
+      null
     }
+    assertEquals(expectedKeyGenClass, 
tableConfig.getProps.getString(HoodieTableConfig.KEY_GENERATOR_CLASS_NAME.key, 
null))
 
     // First update.
     val firstUpdate = 
recordsToStrings(dataGen.generateUpdatesForAllRecords("001")).asScala.toList
@@ -2950,6 +3016,14 @@ object TestCOWDataSource {
       s"Top-level level='L1' should return key1 for $tableType")
   }
 
+  /**
+   * The table version a write with the given write table version produces, 
falling back to the default write
+   * version when it is unset so that a test arm without one follows a change 
of the default.
+   */
+  def writeTableVersionOrDefault(tableVersion: Option[HoodieTableVersion]): 
HoodieTableVersion = {
+    
tableVersion.orElse(HoodieTableVersion.fromVersionCode(HoodieWriteConfig.WRITE_TABLE_VERSION.defaultValue()))
+  }
+
   def convertColumnsToNullable(df: DataFrame, cols: String*): DataFrame = {
     cols.foldLeft(df) { (df, c) =>
       // NOTE: This is the trick to make Spark convert a non-null column "c" 
into a nullable
@@ -2971,26 +3045,21 @@ object TestCOWDataSource {
 
   def tableVersionCreationTestCases = {
     val autoUpgradeValues = Array("true", "false")
-    val targetVersions = Array("1", "2", "3", "4", "5", "6", "7", "8", "9", 
"null")
+    val targetVersions = Array("1", "2", "3", "4", "5", "6", "7", "8", "9", 
"10", "null")
     autoUpgradeValues.flatMap(
       (autoUpgrade: String) => targetVersions.map(
         (targetVersion: String) => Arguments.of(autoUpgrade, targetVersion)))
   }
 
   def provideParamsForKeyGenTest(): java.util.List[Arguments] = {
-    java.util.Arrays.asList(
-      Arguments.of(
-        Option.of("org.apache.hudi.keygen.MockUserProvidedKeyGenerator"),
-        Option.of(KeyGeneratorType.USER_PROVIDED.name())),
-      Arguments.of(
-        Option.empty(),
-        Option.of(KeyGeneratorType.SIMPLE.name())),
-      Arguments.of(
-        Option.of("org.apache.hudi.keygen.SimpleAvroKeyGenerator"),
-        Option.of(KeyGeneratorType.SIMPLE.name())),
-      Arguments.of(
-        Option.of("org.apache.hudi.keygen.ComplexKeyGenerator"),
-        Option.empty())
-    )
+    val keyGenSettings = Seq(
+      (Option.of("org.apache.hudi.keygen.MockUserProvidedKeyGenerator"), 
Option.of(KeyGeneratorType.USER_PROVIDED.name())),
+      (Option.empty[String](), Option.of(KeyGeneratorType.SIMPLE.name())),
+      (Option.of("org.apache.hudi.keygen.SimpleAvroKeyGenerator"), 
Option.of(KeyGeneratorType.SIMPLE.name())),
+      (Option.of("org.apache.hudi.keygen.ComplexKeyGenerator"), 
Option.empty[String]()))
+    val tableVersions = Seq(Option.of(HoodieTableVersion.SIX), 
Option.of(HoodieTableVersion.EIGHT),
+      Option.of(HoodieTableVersion.NINE), Option.of(HoodieTableVersion.TEN), 
Option.empty[HoodieTableVersion]())
+    (for ((keyGenClass, keyGenType) <- keyGenSettings; tableVersion <- 
tableVersions)
+      yield Arguments.of(keyGenClass, keyGenType, tableVersion)).asJava
   }
 }
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
index 359e1fd54fc6..29185b6e1a12 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSource.scala
@@ -2462,12 +2462,18 @@ class TestMORDataSource extends 
HoodieSparkClientTestBase with SparkDatasetMixin
   @ParameterizedTest
   @MethodSource(Array("provideParamsForKeyGenTest"))
   def testUserProvidedKeyGeneratorClass(keyGenClass: Option[String],
-                                        keyGenType: Option[String]): Unit = {
+                                        keyGenType: Option[String],
+                                        tableVersion: 
Option[HoodieTableVersion]): Unit = {
     val recordType = HoodieRecordType.AVRO
+    // the table is new, so the guard against a legacy single-field complex 
key encoding below version 9 does not apply
     var opts: Map[String, String] = Map(
       DataSourceWriteOptions.TABLE_TYPE.key -> 
DataSourceWriteOptions.MOR_TABLE_TYPE_OPT_VAL,
-      HoodieWriteConfig.MERGE_SMALL_FILE_GROUP_CANDIDATES_LIMIT.key -> "0"
+      HoodieWriteConfig.MERGE_SMALL_FILE_GROUP_CANDIDATES_LIMIT.key -> "0",
+      HoodieWriteConfig.ENABLE_COMPLEX_KEYGEN_VALIDATION.key -> "false"
     )
+    if (tableVersion.isPresent) {
+      opts = opts ++ Map(HoodieWriteConfig.WRITE_TABLE_VERSION.key -> 
tableVersion.get.versionCode.toString)
+    }
     if (keyGenClass.isPresent) {
       opts = opts ++ Map(HoodieWriteConfig.KEYGENERATOR_CLASS_NAME.key -> 
keyGenClass.get)
     }
@@ -2498,14 +2504,19 @@ class TestMORDataSource extends 
HoodieSparkClientTestBase with SparkDatasetMixin
       .sort("_row_key")
     var actualKeyDF = 
actualDF.select("_hoodie_record_key").sort("_hoodie_record_key")
     assertTrue(inputKeyDF.except(actualKeyDF).isEmpty && 
actualKeyDF.except(inputKeyDF).isEmpty)
-    val metaClient = getHoodieMetaClient(storageConf, basePath)
-    val actualKeyGenType = metaClient.getTableConfig
-      .getProps.getString(HoodieTableConfig.KEY_GENERATOR_TYPE.key, null)
-    assertEquals(expectedKeyGenType, actualKeyGenType)
-    // For USER_PROVIDED type, the class should exist in table config.
-    if (KeyGeneratorType.USER_PROVIDED.name == actualKeyGenType) {
-      assertEquals(keyGenClass.get(), 
metaClient.getTableConfig.getKeyGeneratorClassName)
+    val tableConfig = getHoodieMetaClient(storageConf, basePath).getTableConfig
+    val expectedTableVersion = 
TestCOWDataSource.writeTableVersionOrDefault(tableVersion)
+    assertEquals(expectedTableVersion, tableConfig.getTableVersion)
+    assertEquals(expectedKeyGenType, 
tableConfig.getProps.getString(HoodieTableConfig.KEY_GENERATOR_TYPE.key, null))
+    // A user-provided key generator is persisted as a class at every table 
version, the others below version 8.
+    val expectedKeyGenClass = if (KeyGeneratorType.USER_PROVIDED.name == 
expectedKeyGenType) {
+      keyGenClass.get
+    } else if (expectedTableVersion.lesserThan(HoodieTableVersion.EIGHT)) {
+      KeyGeneratorType.valueOf(expectedKeyGenType).getClassName
+    } else {
+      null
     }
+    assertEquals(expectedKeyGenClass, 
tableConfig.getProps.getString(HoodieTableConfig.KEY_GENERATOR_CLASS_NAME.key, 
null))
 
     // First update.
     val firstUpdate = 
recordsToStrings(dataGen.generateUpdatesForAllRecords("001")).asScala.toList
@@ -2718,19 +2729,14 @@ class TestMORDataSource extends 
HoodieSparkClientTestBase with SparkDatasetMixin
 
 object TestMORDataSource {
   def provideParamsForKeyGenTest(): java.util.List[Arguments] = {
-    java.util.Arrays.asList(
-      Arguments.of(
-        Option.of("org.apache.hudi.keygen.MockUserProvidedKeyGenerator"),
-        Option.of(KeyGeneratorType.USER_PROVIDED.name())),
-      Arguments.of(
-        Option.empty(),
-        Option.of(KeyGeneratorType.SIMPLE.name())),
-      Arguments.of(
-        Option.of("org.apache.hudi.keygen.SimpleAvroKeyGenerator"),
-        Option.of(KeyGeneratorType.SIMPLE.name())),
-      Arguments.of(
-        Option.of("org.apache.hudi.keygen.ComplexKeyGenerator"),
-        Option.empty())
-    )
+    val keyGenSettings = Seq(
+      (Option.of("org.apache.hudi.keygen.MockUserProvidedKeyGenerator"), 
Option.of(KeyGeneratorType.USER_PROVIDED.name())),
+      (Option.empty[String](), Option.of(KeyGeneratorType.SIMPLE.name())),
+      (Option.of("org.apache.hudi.keygen.SimpleAvroKeyGenerator"), 
Option.of(KeyGeneratorType.SIMPLE.name())),
+      (Option.of("org.apache.hudi.keygen.ComplexKeyGenerator"), 
Option.empty[String]()))
+    val tableVersions = Seq(Option.of(HoodieTableVersion.SIX), 
Option.of(HoodieTableVersion.EIGHT),
+      Option.of(HoodieTableVersion.NINE), Option.of(HoodieTableVersion.TEN), 
Option.empty[HoodieTableVersion]())
+    (for ((keyGenClass, keyGenType) <- keyGenSettings; tableVersion <- 
tableVersions)
+      yield Arguments.of(keyGenClass, keyGenType, tableVersion)).asJava
   }
 }
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
index a10ae3599e8d..3e3f3cf774ec 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
@@ -21,14 +21,15 @@ import org.apache.hudi.DataSourceWriteOptions
 import org.apache.hudi.DataSourceWriteOptions._
 import org.apache.hudi.common.model.{HoodieRecord, HoodieTableType, 
WriteOperationType}
 import org.apache.hudi.common.schema.{HoodieSchema, HoodieSchemaType}
-import org.apache.hudi.common.table.{HoodieTableConfig, HoodieTableMetaClient}
+import org.apache.hudi.common.table.{HoodieTableConfig, HoodieTableMetaClient, 
HoodieTableVersion}
 import org.apache.hudi.common.testutils.HoodieTestUtils
+import org.apache.hudi.common.testutils.HoodieTestUtils.assertTableConfigs
 import org.apache.hudi.common.util.HoodieStorageUtils
 import org.apache.hudi.common.util.PartitionPathEncodeUtils.escapePathName
 import org.apache.hudi.config.HoodieWriteConfig
 import org.apache.hudi.hadoop.fs.HadoopFSUtils
 import org.apache.hudi.hadoop.realtime.HoodieParquetRealtimeInputFormat
-import org.apache.hudi.keygen.constant.{ComplexKeyGenEncoding, 
KeyGeneratorType}
+import org.apache.hudi.keygen.constant.ComplexKeyGenEncoding
 import org.apache.hudi.storage.{HoodieStorage, StoragePath}
 import org.apache.hudi.storage.hadoop.HadoopStorageConfiguration
 import org.apache.hudi.testutils.Assertions
@@ -103,57 +104,100 @@ class TestCreateTable extends HoodieSparkSqlTestBase 
with ExtendedParserTestHelp
     spark.sql("use default")
   }
 
-  test("Test Create Hoodie Table With Options") {
-    val tableName = generateTableName
-    spark.sql(
-      s"""
-         | create table $tableName (
-         |  id int,
-         |  name string,
-         |  price double,
-         |  ts long,
-         |  dt string
-         | ) using hudi
-         | partitioned by (dt)
-         | options (
-         |   hoodie.database.name = "databaseName",
-         |   hoodie.table.name = "tableName",
-         |   primaryKey = 'id',
-         |   orderingFields = 'ts',
-         |   hoodie.datasource.write.operation = 'upsert'
-         | )
-       """.stripMargin)
-    val table = 
spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))
-    assertResult(table.properties("type"))("cow")
-    assertResult(table.properties("primaryKey"))("id")
-    assertResult(table.properties("orderingFields"))("ts")
-    assertResult(tableName)(table.identifier.table)
-    assertResult("hudi")(table.provider.get)
-    assertResult(CatalogTableType.MANAGED)(table.tableType)
-    assertResult(
-      HoodieRecord.HOODIE_META_COLUMNS.asScala.map(StructField(_, StringType))
-        ++ Seq(
-        StructField("id", IntegerType),
-        StructField("name", StringType),
-        StructField("price", DoubleType),
-        StructField("ts", LongType),
-        StructField("dt", StringType))
-    )(table.schema.fields)
-    
assertFalse(table.properties.contains(HoodieTableConfig.DATABASE_NAME.key()))
-    assertFalse(table.properties.contains(HoodieTableConfig.NAME.key()))
-    assertFalse(table.properties.contains(OPERATION.key()))
+  // the arm without a table version leaves the write table version unset, so 
the default version is covered as well
+  Seq(Some(HoodieTableVersion.SIX), Some(HoodieTableVersion.EIGHT), 
Some(HoodieTableVersion.NINE), Some(HoodieTableVersion.TEN), None).foreach { 
tableVersion =>
+    test(s"Test Create Hoodie Table With Options 
(tableVersion=${tableVersion.map(_.versionCode().toString).getOrElse("default")})")
 {
+      val tableName = generateTableName
+      val tableVersionOption = tableVersion.map(v => 
s"hoodie.write.table.version = '${v.versionCode()}',").getOrElse("")
+      spark.sql(
+        s"""
+           | create table $tableName (
+           |  id int,
+           |  name string,
+           |  price double,
+           |  ts long,
+           |  dt string
+           | ) using hudi
+           | partitioned by (dt)
+           | options (
+           |   hoodie.database.name = "databaseName",
+           |   hoodie.table.name = "tableName",
+           |   $tableVersionOption
+           |   primaryKey = 'id',
+           |   orderingFields = 'ts',
+           |   hoodie.datasource.write.operation = 'upsert'
+           | )
+         """.stripMargin)
+      val table = 
spark.sessionState.catalog.getTableMetadata(TableIdentifier(tableName))
+      assertResult(table.properties("type"))("cow")
+      assertResult(table.properties("primaryKey"))("id")
+      assertResult(table.properties("orderingFields"))("ts")
+      assertResult(tableName)(table.identifier.table)
+      assertResult("hudi")(table.provider.get)
+      assertResult(CatalogTableType.MANAGED)(table.tableType)
+      assertResult(
+        HoodieRecord.HOODIE_META_COLUMNS.asScala.map(StructField(_, 
StringType))
+          ++ Seq(
+          StructField("id", IntegerType),
+          StructField("name", StringType),
+          StructField("price", DoubleType),
+          StructField("ts", LongType),
+          StructField("dt", StringType))
+      )(table.schema.fields)
+      
assertFalse(table.properties.contains(HoodieTableConfig.DATABASE_NAME.key()))
+      assertFalse(table.properties.contains(HoodieTableConfig.NAME.key()))
+      assertFalse(table.properties.contains(OPERATION.key()))
 
-    val tablePath = table.storage.properties("path")
-    val metaClient = createMetaClient(spark, tablePath)
-    val tableConfig = metaClient.getTableConfig.getProps.asScala.toMap
-    
assertResult(true)(tableConfig.contains(HoodieTableConfig.CREATE_SCHEMA.key))
-    assertResult("dt")(tableConfig(HoodieTableConfig.PARTITION_FIELDS.key))
-    assertResult("id")(tableConfig(HoodieTableConfig.RECORDKEY_FIELDS.key))
-    assertResult("ts")(tableConfig(HoodieTableConfig.ORDERING_FIELDS.key))
-    
assertResult(KeyGeneratorType.SIMPLE.name())(tableConfig(HoodieTableConfig.KEY_GENERATOR_TYPE.key))
-    assertResult("default")(tableConfig(HoodieTableConfig.DATABASE_NAME.key()))
-    assertResult(tableName)(tableConfig(HoodieTableConfig.NAME.key()))
-    assertFalse(tableConfig.contains(OPERATION.key()))
+      val tablePath = table.storage.properties("path")
+      val metaClient = createMetaClient(spark, tablePath)
+      
assertTrue(metaClient.getTableConfig.contains(HoodieTableConfig.CREATE_SCHEMA))
+      val expectedTableVersion = tableVersion.getOrElse(
+        
HoodieTableVersion.fromVersionCode(HoodieWriteConfig.WRITE_TABLE_VERSION.defaultValue()))
+      assertTableConfigs(metaClient.getStorage, tablePath, 
expectedTableConfigs(tableName, expectedTableVersion).asJava)
+    }
+  }
+
+  private def expectedTableConfigs(tableName: String, tableVersion: 
HoodieTableVersion): Map[String, String] = {
+    val commonConfigs = Map(
+      "hoodie.database.name" -> "default",
+      "hoodie.table.name" -> tableName,
+      "hoodie.table.type" -> "COPY_ON_WRITE",
+      "hoodie.table.version" -> tableVersion.versionCode().toString,
+      "hoodie.table.recordkey.fields" -> "id",
+      "hoodie.table.partition.fields" -> "dt",
+      "hoodie.table.ordering.fields" -> "ts",
+      "hoodie.table.keygenerator.type" -> "SIMPLE",
+      "hoodie.table.format" -> "native",
+      "hoodie.meta.fields.mode" -> "ALL",
+      "hoodie.datasource.write.drop.partition.columns" -> "false",
+      "hoodie.datasource.write.hive_style_partitioning" -> "true",
+      "hoodie.datasource.write.partitionpath.urlencode" -> "false",
+      "hoodie.archivelog.folder" -> "archived",
+      "hoodie.timeline.path" -> "timeline",
+      "hoodie.timeline.history.path" -> "history")
+    val layoutConfigs = if (tableVersion.lesserThan(HoodieTableVersion.EIGHT)) 
{
+      Map(
+        "hoodie.timeline.layout.version" -> "1",
+        "hoodie.table.keygenerator.class" -> 
"org.apache.hudi.keygen.SimpleKeyGenerator")
+    } else {
+      Map(
+        "hoodie.timeline.layout.version" -> "2",
+        "hoodie.table.initial.version" -> tableVersion.versionCode().toString,
+        "hoodie.record.merge.mode" -> "EVENT_TIME_ORDERING")
+    }
+    val mergeConfigs = if (tableVersion.lesserThan(HoodieTableVersion.NINE)) {
+      Map(
+        "hoodie.compaction.payload.class" -> 
"org.apache.hudi.common.model.DefaultHoodieRecordPayload",
+        "hoodie.record.merge.strategy.id" -> 
"eeb8d96f-b1e4-49fd-bbf8-28ac514178e5")
+    } else {
+      Map.empty[String, String]
+    }
+    val metaFieldsConfigs = if 
(tableVersion.lesserThan(HoodieTableVersion.TEN)) {
+      Map("hoodie.populate.meta.fields" -> "true")
+    } else {
+      Map.empty[String, String]
+    }
+    commonConfigs ++ layoutConfigs ++ mergeConfigs ++ metaFieldsConfigs
   }
 
   test("Test Create External Hoodie Table") {
diff --git 
a/hudi-utilities/src/test/java/org/apache/hudi/utilities/deltastreamer/TestHoodieDeltaStreamer.java
 
b/hudi-utilities/src/test/java/org/apache/hudi/utilities/deltastreamer/TestHoodieDeltaStreamer.java
index 8d24aa3e3156..62f9d674e051 100644
--- 
a/hudi-utilities/src/test/java/org/apache/hudi/utilities/deltastreamer/TestHoodieDeltaStreamer.java
+++ 
b/hudi-utilities/src/test/java/org/apache/hudi/utilities/deltastreamer/TestHoodieDeltaStreamer.java
@@ -171,6 +171,7 @@ import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
 import org.junit.jupiter.params.provider.ValueSource;
 
 import java.io.IOException;
@@ -205,6 +206,7 @@ import static 
org.apache.hudi.common.table.checkpoint.StreamerCheckpointV1.STREA
 import static 
org.apache.hudi.common.table.checkpoint.StreamerCheckpointV2.STREAMER_CHECKPOINT_KEY_V2;
 import static 
org.apache.hudi.common.table.timeline.InstantComparison.GREATER_THAN;
 import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_FILE_NAME_GENERATOR;
+import static 
org.apache.hudi.common.testutils.HoodieTestUtils.assertTableConfigs;
 import static org.apache.hudi.common.util.StringUtils.EMPTY_STRING;
 import static 
org.apache.hudi.config.HoodieErrorTableConfig.ERROR_TABLE_PERSIST_SOURCE_RDD;
 import static org.apache.hudi.testutils.HoodieClientTestUtils.createMetaClient;
@@ -617,22 +619,80 @@ public class TestHoodieDeltaStreamer extends 
HoodieDeltaStreamerTestBase {
     deltaStreamer.shutdownGracefully();
   }
 
+  /**
+   * A null version leaves the write table version unset, so the default 
version is covered as well.
+   */
   @ParameterizedTest
-  @EnumSource(value = HoodieTableVersion.class, names = {"SIX", "EIGHT"})
-  public void testPartitionKeyFieldsBasedOnVersion(HoodieTableVersion version) 
throws IOException {
-    String tablePath = basePath + "/partition_key_fields_meta_client" + 
version.versionCode();
+  @NullSource
+  @EnumSource(value = HoodieTableVersion.class, names = {"SIX", "EIGHT", 
"NINE", "TEN"})
+  void testTableConfigsBasedOnVersion(HoodieTableVersion version) throws 
IOException {
+    HoodieTableVersion expectedVersion = version == null
+        ? 
HoodieTableVersion.fromVersionCode(HoodieWriteConfig.WRITE_TABLE_VERSION.defaultValue())
 : version;
+    String tablePath = basePath + "/table_configs_v" + (version == null ? 
"default" : String.valueOf(version.versionCode()));
     HoodieDeltaStreamer.Config cfg = TestHelpers.makeConfig(tablePath, 
WriteOperationType.INSERT);
-    cfg.configs.add(HoodieWriteConfig.WRITE_TABLE_VERSION.key() + "=" + 
version.versionCode());
+    if (version != null) {
+      cfg.configs.add(HoodieWriteConfig.WRITE_TABLE_VERSION.key() + "=" + 
version.versionCode());
+    }
     cfg.configs.add(HoodieWriteConfig.KEYGENERATOR_CLASS_NAME.key() + "=" + 
CustomKeyGenerator.class.getName());
     
cfg.configs.add("hoodie.datasource.write.partitionpath.field=partition_path:simple");
     HoodieDeltaStreamer deltaStreamer = new HoodieDeltaStreamer(cfg, jsc);
     deltaStreamer.getIngestionService().ingestOnce();
-    HoodieTableMetaClient metaClient = 
HoodieTestUtils.createMetaClient(context, tablePath);
-    String expectedPartitionFields = version.equals(HoodieTableVersion.SIX) ? 
"partition_path" : "partition_path:simple";
-    assertEquals(expectedPartitionFields, 
metaClient.getTableConfig().getString(HoodieTableConfig.PARTITION_FIELDS));
+    assertTableConfigs(storage, tablePath, 
expectedTableConfigs(expectedVersion));
+    deltaStreamer.shutdownGracefully();
+
+    // the next write backfills the key generator class of a table created 
without it
+    HoodieTableConfig.delete(storage, new StoragePath(tablePath, 
HoodieTableMetaClient.METAFOLDER_NAME),
+        
Collections.singleton(HoodieTableConfig.KEY_GENERATOR_CLASS_NAME.key()));
+    deltaStreamer = new HoodieDeltaStreamer(cfg, jsc);
+    deltaStreamer.getIngestionService().ingestOnce();
+    assertTableConfigs(storage, tablePath, 
expectedTableConfigs(expectedVersion));
     deltaStreamer.shutdownGracefully();
   }
 
+  private static Map<String, String> expectedTableConfigs(HoodieTableVersion 
version) {
+    Map<String, String> configs = new HashMap<>();
+    configs.put("hoodie.table.name", "hoodie_trips");
+    configs.put("hoodie.table.type", "COPY_ON_WRITE");
+    configs.put("hoodie.table.version", String.valueOf(version.versionCode()));
+    configs.put("hoodie.table.recordkey.fields", "_row_key");
+    configs.put("hoodie.table.ordering.fields", "timestamp");
+    configs.put("hoodie.table.keygenerator.type", "CUSTOM");
+    configs.put("hoodie.table.base.file.format", "PARQUET");
+    configs.put("hoodie.table.format", "native");
+    configs.put("hoodie.table.cdc.enabled", "false");
+    configs.put("hoodie.table.metadata.partitions.inflight", "");
+    configs.put("hoodie.table.index.defs.path", 
".hoodie/.index_defs/index.json");
+    configs.put("hoodie.table.partition_extractor_class", 
"org.apache.hudi.hive.MultiPartKeysValueExtractor");
+    configs.put("hoodie.meta.fields.mode", "ALL");
+    configs.put("hoodie.partition.metafile.use.base.format", "false");
+    configs.put("hoodie.datasource.write.drop.partition.columns", "false");
+    configs.put("hoodie.datasource.write.hive_style_partitioning", "false");
+    configs.put("hoodie.datasource.write.partitionpath.urlencode", "false");
+    configs.put("hoodie.archivelog.folder", "history");
+    configs.put("hoodie.timeline.path", "timeline");
+    configs.put("hoodie.timeline.history.path", "history");
+    if (version.lesserThan(HoodieTableVersion.EIGHT)) {
+      configs.put("hoodie.table.partition.fields", "partition_path");
+      configs.put("hoodie.table.metadata.partitions", "column_stats,files");
+      configs.put("hoodie.timeline.layout.version", "1");
+      configs.put("hoodie.table.keygenerator.class", 
"org.apache.hudi.keygen.CustomKeyGenerator");
+    } else {
+      configs.put("hoodie.table.partition.fields", "partition_path:simple");
+      configs.put("hoodie.table.metadata.partitions", 
"column_stats,files,partition_stats");
+      configs.put("hoodie.timeline.layout.version", "2");
+      configs.put("hoodie.table.initial.version", 
String.valueOf(version.versionCode()));
+      configs.put("hoodie.record.merge.mode", "COMMIT_TIME_ORDERING");
+    }
+    if (version.lesserThan(HoodieTableVersion.NINE)) {
+      configs.put("hoodie.compaction.payload.class", 
"org.apache.hudi.common.model.OverwriteWithLatestAvroPayload");
+      configs.put("hoodie.record.merge.strategy.id", 
"ce9acb64-bde0-424c-9b91-f6ebba25356d");
+    }
+    if (version.lesserThan(HoodieTableVersion.TEN)) {
+      configs.put("hoodie.populate.meta.fields", "true");
+    }
+    return configs;
+  }
+
   @ParameterizedTest
   @EnumSource(value = HoodieRecordType.class, names = {"AVRO", "SPARK"})
   public void testBulkInsertsAndUpsertsWithBootstrap(HoodieRecordType 
recordType) throws Exception {

Reply via email to