cshuo commented on code in PR #19067:
URL: https://github.com/apache/hudi/pull/19067#discussion_r3473211456


##########
hudi-common/src/main/java/org/apache/hudi/common/table/HoodieTableConfig.java:
##########
@@ -240,6 +240,12 @@ public class HoodieTableConfig extends HoodieConfig {
       .withAlternatives("hoodie.table.rt.file.format")
       .withDocumentation("Log format used for the delta logs.");
 
+  public static final ConfigProperty<String> TABLE_STORAGE_LAYOUT = 
ConfigProperty
+      .key("hoodie.table.storage.layout")
+      .defaultValue(TableStorageLayout.DEFAULT.configValue)
+      .sinceVersion("1.3.0")

Review Comment:
   `HoodieTableConfig.TABLE_STORAGE_LAYOUT` is declared with 
`.sinceVersion("1.3.0")`, but `HoodieTableVersion` only maps releases through 
`1.1.0`. During table config creation, `validateConfigVersion(...)` calls 
`HoodieTableVersion.fromReleaseVersion("1.3.0")`, which throws before the table 
config can be persisted.
   
   This means the new config that enables the LSM/native-log write path fails 
at creation time, so the writer selection based on `isLSMTreeStorageLayout()` 
is not reachable.
   
   Maybe we should add the corresponding table version/release mapping before 
using `sinceVersion("1.3.0")`, or avoid using an unmapped `sinceVersion` value 
for this config until table version support is added.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogFormatWriter.java:
##########
@@ -0,0 +1,341 @@
+/*
+ * 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.io;
+
+import org.apache.hudi.common.engine.RecordContext;
+import org.apache.hudi.common.engine.TaskContextSupplier;
+import org.apache.hudi.common.fs.FSUtils;
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieLogFile;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemas;
+import org.apache.hudi.common.table.HoodieTableVersion;
+import org.apache.hudi.common.table.log.AppendResult;
+import org.apache.hudi.common.table.log.HoodieLogFormat;
+import org.apache.hudi.common.table.log.LogFileCreationCallback;
+import org.apache.hudi.common.table.log.block.HoodieLogBlock;
+import 
org.apache.hudi.common.table.log.block.HoodieLogBlock.HeaderMetadataType;
+import org.apache.hudi.common.table.read.BufferedRecord;
+import org.apache.hudi.common.table.read.BufferedRecords;
+import org.apache.hudi.common.util.JsonUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.OrderingValues;
+import org.apache.hudi.common.util.collection.ArrayComparable;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.io.storage.HoodieFileWriter;
+import org.apache.hudi.io.storage.HoodieFileWriterFactory;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.apache.hudi.common.model.LogExtensions.DATA_LOG_EXTENSION;
+import static org.apache.hudi.common.model.LogExtensions.DELETE_LOG_EXTENSION;
+
+/**
+ * Writes MOR log blocks as native files, for example {@code .log.parquet} and 
{@code .deletes.parquet}.
+ */
+public class HoodieNativeLogFormatWriter extends HoodieLogFormat.Writer {
+
+  private static final int NATIVE_LOG_FORMAT_VERSION = 2;
+  private static final String LOG_FORMAT_METADATA_FOOTER_KEY = 
"hudi.log.format.metadata";
+
+  private final HoodieWriteConfig writeConfig;
+  private final HoodieSchema tableSchema;
+  private final TaskContextSupplier taskContextSupplier;
+  private final RecordContext recordContext;
+  private final List<String> orderingFieldNames;
+  private final Properties recordProperties;
+  private HoodieFileWriter dataFileWriter;
+  private HoodieFileWriter deleteFileWriter;
+  private HoodieLogFile dataLogFile;
+  private HoodieLogFile deleteLogFile;
+  private HoodieSchema deleteLogSchema;
+  private int currentAppendVersion = -1;
+  private List<AppendResult> lastAppendResults = new ArrayList<>();
+  private Option<Object> lastDataFileFormatMetadata = Option.empty();
+
+  public HoodieNativeLogFormatWriter(Integer bufferSize,
+                                     HoodieStorage storage,
+                                     StoragePath parentPath,
+                                     String logFileId,
+                                     String instantTime,
+                                     Integer logVersion,
+                                     String logWriteToken,
+                                     Long sizeThreshold,
+                                     LogFileCreationCallback 
fileCreationCallback,
+                                     HoodieTableVersion tableVersion,
+                                     HoodieWriteConfig writeConfig,
+                                     HoodieSchema tableSchema,
+                                     TaskContextSupplier taskContextSupplier,
+                                     RecordContext recordContext,
+                                     List<String> orderingFieldNames) throws 
IOException {
+    super(bufferSize, storage, parentPath, logFileId, DATA_LOG_EXTENSION, 
instantTime, logVersion, logWriteToken,
+        null, 0L, sizeThreshold, fileCreationCallback, tableVersion);
+    this.writeConfig = writeConfig;
+    this.tableSchema = tableSchema;
+    this.taskContextSupplier = taskContextSupplier;
+    this.recordContext = recordContext;
+    this.orderingFieldNames = orderingFieldNames;
+    this.recordProperties = new Properties();
+    this.recordProperties.putAll(writeConfig.getProps());
+    this.logFile = new HoodieLogFile(makeNativeLogPath(this.logVersion, 
DATA_LOG_EXTENSION), this.fileSize);
+  }
+
+  public List<AppendResult> getLastAppendResults() {
+    return lastAppendResults;
+  }
+
+  public Option<Object> getLastDataFileFormatMetadata() {
+    return lastDataFileFormatMetadata;
+  }
+
+  @Override
+  public AppendResult appendBlock(HoodieLogBlock block) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public AppendResult appendBlocks(List<HoodieLogBlock> blocks) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public long getCurrentSize() {
+    return lastAppendResults.stream().mapToLong(AppendResult::size).sum();
+  }
+
+  @Override
+  public void sync() {
+    // Native log files are write-once; closing the file writer publishes the 
file.
+  }
+
+  @Override
+  public void close() {
+    try {
+      closeFileWriters();
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to close native log file writers", e);
+    }
+  }
+
+  public boolean hasPendingWrites() {
+    return dataFileWriter != null || deleteFileWriter != null;
+  }
+
+  public boolean canWriteDataFile() {
+    return dataFileWriter == null || dataFileWriter.canWrite();
+  }
+
+  public void appendRecord(HoodieRecord record, HoodieSchema recordSchema, 
String keyFieldName) throws IOException {
+    ensureDataFileWriter(recordSchema);
+    dataFileWriter.write(record.getRecordKey(recordSchema, keyFieldName),
+        record, recordSchema, recordProperties);
+  }
+
+  public void appendDeleteRecord(HoodieRecord record, HoodieSchema 
recordSchema, String keyFieldName) throws IOException {

Review Comment:
   The existing inline log append path has rollover at the append/log-writer 
level. Records and deletes are buffered in the same handle, then 
`HoodieInlineLogAppendHandle.appendDataAndDeleteBlocks(...)` writes both data 
blocks and `HoodieDeleteBlock`s through `HoodieLogFormat.Writer`. That writer 
is created with `config.getLogFileMaxSize()`, so delete blocks participate in 
the same log-file size/rollover behavior as data blocks.
   
   While `HoodieNativeLogFormatWriter.appendDeleteRecord(...)` always writes to 
the current delete writer. There is no delete-side equivalent of 
`canWriteDataFile()`, so delete-heavy workloads can keep appending to one 
`.deletes.parquet` file past the configured log/native file size threshold. 
Should we also add delete-writer size/rollover handling for native logs as well?
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogFormatWriter.java:
##########
@@ -0,0 +1,341 @@
+/*
+ * 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.io;
+
+import org.apache.hudi.common.engine.RecordContext;
+import org.apache.hudi.common.engine.TaskContextSupplier;
+import org.apache.hudi.common.fs.FSUtils;
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieLogFile;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemas;
+import org.apache.hudi.common.table.HoodieTableVersion;
+import org.apache.hudi.common.table.log.AppendResult;
+import org.apache.hudi.common.table.log.HoodieLogFormat;
+import org.apache.hudi.common.table.log.LogFileCreationCallback;
+import org.apache.hudi.common.table.log.block.HoodieLogBlock;
+import 
org.apache.hudi.common.table.log.block.HoodieLogBlock.HeaderMetadataType;
+import org.apache.hudi.common.table.read.BufferedRecord;
+import org.apache.hudi.common.table.read.BufferedRecords;
+import org.apache.hudi.common.util.JsonUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.OrderingValues;
+import org.apache.hudi.common.util.collection.ArrayComparable;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.io.storage.HoodieFileWriter;
+import org.apache.hudi.io.storage.HoodieFileWriterFactory;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.apache.hudi.common.model.LogExtensions.DATA_LOG_EXTENSION;
+import static org.apache.hudi.common.model.LogExtensions.DELETE_LOG_EXTENSION;
+
+/**
+ * Writes MOR log blocks as native files, for example {@code .log.parquet} and 
{@code .deletes.parquet}.
+ */
+public class HoodieNativeLogFormatWriter extends HoodieLogFormat.Writer {
+
+  private static final int NATIVE_LOG_FORMAT_VERSION = 2;
+  private static final String LOG_FORMAT_METADATA_FOOTER_KEY = 
"hudi.log.format.metadata";
+
+  private final HoodieWriteConfig writeConfig;
+  private final HoodieSchema tableSchema;
+  private final TaskContextSupplier taskContextSupplier;
+  private final RecordContext recordContext;
+  private final List<String> orderingFieldNames;
+  private final Properties recordProperties;
+  private HoodieFileWriter dataFileWriter;
+  private HoodieFileWriter deleteFileWriter;
+  private HoodieLogFile dataLogFile;
+  private HoodieLogFile deleteLogFile;
+  private HoodieSchema deleteLogSchema;
+  private int currentAppendVersion = -1;
+  private List<AppendResult> lastAppendResults = new ArrayList<>();
+  private Option<Object> lastDataFileFormatMetadata = Option.empty();
+
+  public HoodieNativeLogFormatWriter(Integer bufferSize,
+                                     HoodieStorage storage,
+                                     StoragePath parentPath,
+                                     String logFileId,
+                                     String instantTime,
+                                     Integer logVersion,
+                                     String logWriteToken,
+                                     Long sizeThreshold,
+                                     LogFileCreationCallback 
fileCreationCallback,
+                                     HoodieTableVersion tableVersion,
+                                     HoodieWriteConfig writeConfig,
+                                     HoodieSchema tableSchema,
+                                     TaskContextSupplier taskContextSupplier,
+                                     RecordContext recordContext,
+                                     List<String> orderingFieldNames) throws 
IOException {
+    super(bufferSize, storage, parentPath, logFileId, DATA_LOG_EXTENSION, 
instantTime, logVersion, logWriteToken,
+        null, 0L, sizeThreshold, fileCreationCallback, tableVersion);
+    this.writeConfig = writeConfig;
+    this.tableSchema = tableSchema;
+    this.taskContextSupplier = taskContextSupplier;
+    this.recordContext = recordContext;
+    this.orderingFieldNames = orderingFieldNames;
+    this.recordProperties = new Properties();
+    this.recordProperties.putAll(writeConfig.getProps());
+    this.logFile = new HoodieLogFile(makeNativeLogPath(this.logVersion, 
DATA_LOG_EXTENSION), this.fileSize);
+  }
+
+  public List<AppendResult> getLastAppendResults() {
+    return lastAppendResults;
+  }
+
+  public Option<Object> getLastDataFileFormatMetadata() {
+    return lastDataFileFormatMetadata;
+  }
+
+  @Override
+  public AppendResult appendBlock(HoodieLogBlock block) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public AppendResult appendBlocks(List<HoodieLogBlock> blocks) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public long getCurrentSize() {
+    return lastAppendResults.stream().mapToLong(AppendResult::size).sum();
+  }
+
+  @Override
+  public void sync() {
+    // Native log files are write-once; closing the file writer publishes the 
file.
+  }
+
+  @Override
+  public void close() {
+    try {
+      closeFileWriters();
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to close native log file writers", e);
+    }
+  }
+
+  public boolean hasPendingWrites() {
+    return dataFileWriter != null || deleteFileWriter != null;
+  }
+
+  public boolean canWriteDataFile() {
+    return dataFileWriter == null || dataFileWriter.canWrite();
+  }
+
+  public void appendRecord(HoodieRecord record, HoodieSchema recordSchema, 
String keyFieldName) throws IOException {
+    ensureDataFileWriter(recordSchema);
+    dataFileWriter.write(record.getRecordKey(recordSchema, keyFieldName),
+        record, recordSchema, recordProperties);
+  }
+
+  public void appendDeleteRecord(HoodieRecord record, HoodieSchema 
recordSchema, String keyFieldName) throws IOException {
+    ensureDeleteFileWriter();
+    String recordKey = record.getRecordKey(recordSchema, keyFieldName);
+    Comparable orderingValue = record.getOrderingValue(
+        recordSchema, recordProperties, orderingFieldNames.toArray(new 
String[0]));
+    Object deleteEngineRecord = recordContext.constructEngineRecord(
+        deleteLogSchema, createDeleteLogFieldValues(recordKey, orderingValue));
+    // Keep isDelete=false here so RecordContext constructs a data-bearing 
HoodieRecord
+    // with the native delete-log row. The delete semantics come from the 
delete log file
+    // itself; setting isDelete=true would create a HoodieEmptyRecord and lose 
the row.
+    BufferedRecord deleteRecord = BufferedRecords.fromEngineRecord(
+        deleteEngineRecord, deleteLogSchema, recordContext, orderingValue, 
recordKey, false);
+    deleteFileWriter.write(recordKey, 
recordContext.constructHoodieRecord(deleteRecord, record.getPartitionPath()),
+        deleteLogSchema, recordProperties);
+  }
+
+  private Object[] createDeleteLogFieldValues(String recordKey, Comparable 
orderingValue) {
+    Object[] fieldValues = new Object[deleteLogSchema.getFields().size()];
+    fieldValues[0] = recordContext.convertValueToEngineType(recordKey);
+    if (!OrderingValues.isCommitTimeOrderingValue(orderingValue)) {
+      if (orderingFieldNames.size() == 1) {
+        fieldValues[1] = orderingValue;
+      } else {
+        List<Comparable> orderingValues = 
OrderingValues.getValues((ArrayComparable) orderingValue);
+        for (int i = 0; i < orderingValues.size(); i++) {
+          fieldValues[i + 1] = orderingValues.get(i);
+        }
+      }
+    }
+    return fieldValues;
+  }
+
+  public AppendResult flushAppend() throws IOException {

Review Comment:
   Never used, can be removed?



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogAppendHandle.java:
##########
@@ -0,0 +1,227 @@
+/*
+ * 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.io;
+
+import org.apache.hudi.common.engine.TaskContextSupplier;
+import org.apache.hudi.common.fs.FSUtils;
+import org.apache.hudi.common.model.FileSlice;
+import org.apache.hudi.common.model.HoodieDeltaWriteStat;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.table.log.AppendResult;
+import 
org.apache.hudi.common.table.log.block.HoodieLogBlock.HeaderMetadataType;
+import org.apache.hudi.common.util.ConfigUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.ParquetUtils;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.exception.HoodieAppendException;
+import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.metadata.HoodieIndexVersion;
+import org.apache.hudi.metadata.HoodieTableMetadataUtil;
+import org.apache.hudi.stats.HoodieColumnRangeMetadata;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.table.HoodieTable;
+import org.apache.hudi.util.Lazy;
+
+import org.apache.parquet.hadoop.metadata.ParquetMetadata;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static 
org.apache.hudi.metadata.HoodieTableMetadataUtil.PARTITION_NAME_COLUMN_STATS;
+
+/**
+ * Append handle for native log files. Unlike {@link 
HoodieInlineLogAppendHandle}, this handle streams
+ * records directly into native format writers and does not buffer records or 
build inline log blocks.
+ */
+public class HoodieNativeLogAppendHandle<T, I, K, O> extends 
HoodieAppendHandle<T, I, K, O> {
+
+  private HoodieNativeLogFormatWriter writer;
+
+  public HoodieNativeLogAppendHandle(HoodieWriteConfig config, String 
instantTime, HoodieTable<T, I, K, O> hoodieTable,
+                                     String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr,
+                                     TaskContextSupplier taskContextSupplier) {
+    this(config, instantTime, hoodieTable, partitionPath, fileId, recordItr, 
taskContextSupplier, false, Collections.emptyMap());
+  }
+
+  public HoodieNativeLogAppendHandle(HoodieWriteConfig config, String 
instantTime, HoodieTable<T, I, K, O> hoodieTable,
+                                     String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr,
+                                     TaskContextSupplier taskContextSupplier, 
Map<HeaderMetadataType, String> header) {
+    this(config, instantTime, hoodieTable, partitionPath, fileId, recordItr, 
taskContextSupplier, true, header);
+  }
+
+  public HoodieNativeLogAppendHandle(HoodieWriteConfig config, String 
instantTime, HoodieTable<T, I, K, O> hoodieTable,
+                                     String partitionPath, String fileId, 
TaskContextSupplier taskContextSupplier) {
+    this(config, instantTime, hoodieTable, partitionPath, fileId, null, 
taskContextSupplier);
+  }
+
+  private HoodieNativeLogAppendHandle(HoodieWriteConfig config, String 
instantTime, HoodieTable<T, I, K, O> hoodieTable,
+                                      String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr,
+                                      TaskContextSupplier taskContextSupplier, 
boolean preserveMetadata,
+                                      Map<HeaderMetadataType, String> header) {
+    super(config, instantTime, hoodieTable, partitionPath, fileId, recordItr, 
taskContextSupplier, preserveMetadata);
+    this.header.putAll(header);
+  }
+
+  @Override
+  protected void createLogWriterForAppend(String instantTime, 
Option<FileSlice> fileSliceOpt) {
+    try {
+      this.writer = new HoodieNativeLogFormatWriter(
+          storage.getDefaultBufferSize(),
+          storage,
+          
FSUtils.constructAbsolutePath(hoodieTable.getMetaClient().getBasePath(), 
partitionPath),
+          fileId,
+          instantTime,
+          null,
+          writeToken,
+          config.getLogFileMaxSize(),
+          getLogCreationCallback(),
+          config.getWriteVersion(),
+          config,
+          writeSchemaWithMetaFields,
+          taskContextSupplier,
+          
hoodieTable.getReaderContextFactoryForWrite().getContext().getRecordContext(),
+          
Arrays.stream(ConfigUtils.getOrderingFields(recordProperties)).collect(Collectors.toList()));
+    } catch (IOException e) {
+      throw new HoodieException("Creating native log writer with fileId: " + 
fileId + ", "
+          + "delta commit time: " + instantTime + " error", e);
+    }
+  }
+
+  @Override
+  protected void writeInsertAndUpdate(HoodieSchema schema, HoodieRecord<T> 
hoodieRecord, boolean isUpdateRecord) throws IOException {
+    if (hoodieRecord.shouldIgnore(schema, recordProperties)) {
+      return;
+    }
+    HoodieRecord populatedRecord = hoodieRecord.prependMetaFields(
+        schema, writeSchemaWithMetaFields, 
populateMetadataFields(hoodieRecord), recordProperties);
+    String keyField = config.populateMetaFields()
+        ? HoodieRecord.RECORD_KEY_METADATA_FIELD
+        : hoodieTable.getMetaClient().getTableConfig().getRecordKeyFieldProp();
+    if (!writer.canWriteDataFile()) {

Review Comment:
   `HoodieNativeLogAppendHandle.writeInsertAndUpdate(...)` flushes only the 
data native log when `!writer.canWriteDataFile()`. 
`HoodieNativeLogFormatWriter.flushDataAppend(...)` closes `dataFileWriter` and 
resets `currentAppendVersion`, but leaves an already-open 
`deleteFileWriter/deleteLogFile` under the old version.
   
   A single append can therefore produce:
   
   - `v1.log.parquet`
   - `v2.log.parquet`
   - `v1.deletes.parquet`
   
   `HoodieLogFile.LogFileComparator` sorts by log version before extension 
precedence, so `v1.deletes.parquet` sorts before `v2.log.parquet`. That breaks 
the intended same-key ordering where deletes should be applied after all data 
records for the same append/version sequence.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogFormatWriter.java:
##########
@@ -0,0 +1,341 @@
+/*
+ * 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.io;
+
+import org.apache.hudi.common.engine.RecordContext;
+import org.apache.hudi.common.engine.TaskContextSupplier;
+import org.apache.hudi.common.fs.FSUtils;
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieLogFile;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemas;
+import org.apache.hudi.common.table.HoodieTableVersion;
+import org.apache.hudi.common.table.log.AppendResult;
+import org.apache.hudi.common.table.log.HoodieLogFormat;
+import org.apache.hudi.common.table.log.LogFileCreationCallback;
+import org.apache.hudi.common.table.log.block.HoodieLogBlock;
+import 
org.apache.hudi.common.table.log.block.HoodieLogBlock.HeaderMetadataType;
+import org.apache.hudi.common.table.read.BufferedRecord;
+import org.apache.hudi.common.table.read.BufferedRecords;
+import org.apache.hudi.common.util.JsonUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.OrderingValues;
+import org.apache.hudi.common.util.collection.ArrayComparable;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.io.storage.HoodieFileWriter;
+import org.apache.hudi.io.storage.HoodieFileWriterFactory;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.apache.hudi.common.model.LogExtensions.DATA_LOG_EXTENSION;
+import static org.apache.hudi.common.model.LogExtensions.DELETE_LOG_EXTENSION;
+
+/**
+ * Writes MOR log blocks as native files, for example {@code .log.parquet} and 
{@code .deletes.parquet}.
+ */
+public class HoodieNativeLogFormatWriter extends HoodieLogFormat.Writer {
+
+  private static final int NATIVE_LOG_FORMAT_VERSION = 2;
+  private static final String LOG_FORMAT_METADATA_FOOTER_KEY = 
"hudi.log.format.metadata";
+
+  private final HoodieWriteConfig writeConfig;
+  private final HoodieSchema tableSchema;
+  private final TaskContextSupplier taskContextSupplier;
+  private final RecordContext recordContext;
+  private final List<String> orderingFieldNames;
+  private final Properties recordProperties;
+  private HoodieFileWriter dataFileWriter;
+  private HoodieFileWriter deleteFileWriter;
+  private HoodieLogFile dataLogFile;
+  private HoodieLogFile deleteLogFile;
+  private HoodieSchema deleteLogSchema;
+  private int currentAppendVersion = -1;
+  private List<AppendResult> lastAppendResults = new ArrayList<>();
+  private Option<Object> lastDataFileFormatMetadata = Option.empty();
+
+  public HoodieNativeLogFormatWriter(Integer bufferSize,
+                                     HoodieStorage storage,
+                                     StoragePath parentPath,
+                                     String logFileId,
+                                     String instantTime,
+                                     Integer logVersion,
+                                     String logWriteToken,
+                                     Long sizeThreshold,
+                                     LogFileCreationCallback 
fileCreationCallback,
+                                     HoodieTableVersion tableVersion,
+                                     HoodieWriteConfig writeConfig,
+                                     HoodieSchema tableSchema,
+                                     TaskContextSupplier taskContextSupplier,
+                                     RecordContext recordContext,
+                                     List<String> orderingFieldNames) throws 
IOException {
+    super(bufferSize, storage, parentPath, logFileId, DATA_LOG_EXTENSION, 
instantTime, logVersion, logWriteToken,
+        null, 0L, sizeThreshold, fileCreationCallback, tableVersion);
+    this.writeConfig = writeConfig;
+    this.tableSchema = tableSchema;
+    this.taskContextSupplier = taskContextSupplier;
+    this.recordContext = recordContext;
+    this.orderingFieldNames = orderingFieldNames;
+    this.recordProperties = new Properties();
+    this.recordProperties.putAll(writeConfig.getProps());
+    this.logFile = new HoodieLogFile(makeNativeLogPath(this.logVersion, 
DATA_LOG_EXTENSION), this.fileSize);
+  }
+
+  public List<AppendResult> getLastAppendResults() {
+    return lastAppendResults;
+  }
+
+  public Option<Object> getLastDataFileFormatMetadata() {
+    return lastDataFileFormatMetadata;
+  }
+
+  @Override
+  public AppendResult appendBlock(HoodieLogBlock block) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public AppendResult appendBlocks(List<HoodieLogBlock> blocks) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public long getCurrentSize() {
+    return lastAppendResults.stream().mapToLong(AppendResult::size).sum();
+  }
+
+  @Override
+  public void sync() {
+    // Native log files are write-once; closing the file writer publishes the 
file.
+  }
+
+  @Override
+  public void close() {
+    try {
+      closeFileWriters();
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to close native log file writers", e);

Review Comment:
   Here`IOException` is  wrapped as RuntimeException; the codebase convention 
is HoodieIOException.
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogFormatWriter.java:
##########
@@ -0,0 +1,341 @@
+/*
+ * 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.io;
+
+import org.apache.hudi.common.engine.RecordContext;
+import org.apache.hudi.common.engine.TaskContextSupplier;
+import org.apache.hudi.common.fs.FSUtils;
+import org.apache.hudi.common.model.HoodieFileFormat;
+import org.apache.hudi.common.model.HoodieLogFile;
+import org.apache.hudi.common.model.HoodieRecord;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemas;
+import org.apache.hudi.common.table.HoodieTableVersion;
+import org.apache.hudi.common.table.log.AppendResult;
+import org.apache.hudi.common.table.log.HoodieLogFormat;
+import org.apache.hudi.common.table.log.LogFileCreationCallback;
+import org.apache.hudi.common.table.log.block.HoodieLogBlock;
+import 
org.apache.hudi.common.table.log.block.HoodieLogBlock.HeaderMetadataType;
+import org.apache.hudi.common.table.read.BufferedRecord;
+import org.apache.hudi.common.table.read.BufferedRecords;
+import org.apache.hudi.common.util.JsonUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.OrderingValues;
+import org.apache.hudi.common.util.collection.ArrayComparable;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.io.storage.HoodieFileWriter;
+import org.apache.hudi.io.storage.HoodieFileWriterFactory;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.apache.hudi.common.model.LogExtensions.DATA_LOG_EXTENSION;
+import static org.apache.hudi.common.model.LogExtensions.DELETE_LOG_EXTENSION;
+
+/**
+ * Writes MOR log blocks as native files, for example {@code .log.parquet} and 
{@code .deletes.parquet}.
+ */
+public class HoodieNativeLogFormatWriter extends HoodieLogFormat.Writer {
+
+  private static final int NATIVE_LOG_FORMAT_VERSION = 2;
+  private static final String LOG_FORMAT_METADATA_FOOTER_KEY = 
"hudi.log.format.metadata";
+
+  private final HoodieWriteConfig writeConfig;
+  private final HoodieSchema tableSchema;
+  private final TaskContextSupplier taskContextSupplier;
+  private final RecordContext recordContext;
+  private final List<String> orderingFieldNames;
+  private final Properties recordProperties;
+  private HoodieFileWriter dataFileWriter;
+  private HoodieFileWriter deleteFileWriter;
+  private HoodieLogFile dataLogFile;
+  private HoodieLogFile deleteLogFile;
+  private HoodieSchema deleteLogSchema;
+  private int currentAppendVersion = -1;
+  private List<AppendResult> lastAppendResults = new ArrayList<>();
+  private Option<Object> lastDataFileFormatMetadata = Option.empty();
+
+  public HoodieNativeLogFormatWriter(Integer bufferSize,
+                                     HoodieStorage storage,
+                                     StoragePath parentPath,
+                                     String logFileId,
+                                     String instantTime,
+                                     Integer logVersion,
+                                     String logWriteToken,
+                                     Long sizeThreshold,
+                                     LogFileCreationCallback 
fileCreationCallback,
+                                     HoodieTableVersion tableVersion,
+                                     HoodieWriteConfig writeConfig,
+                                     HoodieSchema tableSchema,
+                                     TaskContextSupplier taskContextSupplier,
+                                     RecordContext recordContext,
+                                     List<String> orderingFieldNames) throws 
IOException {
+    super(bufferSize, storage, parentPath, logFileId, DATA_LOG_EXTENSION, 
instantTime, logVersion, logWriteToken,
+        null, 0L, sizeThreshold, fileCreationCallback, tableVersion);
+    this.writeConfig = writeConfig;
+    this.tableSchema = tableSchema;
+    this.taskContextSupplier = taskContextSupplier;
+    this.recordContext = recordContext;
+    this.orderingFieldNames = orderingFieldNames;
+    this.recordProperties = new Properties();
+    this.recordProperties.putAll(writeConfig.getProps());
+    this.logFile = new HoodieLogFile(makeNativeLogPath(this.logVersion, 
DATA_LOG_EXTENSION), this.fileSize);
+  }
+
+  public List<AppendResult> getLastAppendResults() {
+    return lastAppendResults;
+  }
+
+  public Option<Object> getLastDataFileFormatMetadata() {
+    return lastDataFileFormatMetadata;
+  }
+
+  @Override
+  public AppendResult appendBlock(HoodieLogBlock block) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public AppendResult appendBlocks(List<HoodieLogBlock> blocks) {
+    throw new UnsupportedOperationException("HoodieNativeLogFormatWriter 
writes native records directly.");
+  }
+
+  @Override
+  public long getCurrentSize() {
+    return lastAppendResults.stream().mapToLong(AppendResult::size).sum();
+  }
+
+  @Override
+  public void sync() {
+    // Native log files are write-once; closing the file writer publishes the 
file.
+  }
+
+  @Override
+  public void close() {
+    try {
+      closeFileWriters();
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to close native log file writers", e);
+    }
+  }
+
+  public boolean hasPendingWrites() {
+    return dataFileWriter != null || deleteFileWriter != null;
+  }
+
+  public boolean canWriteDataFile() {
+    return dataFileWriter == null || dataFileWriter.canWrite();
+  }
+
+  public void appendRecord(HoodieRecord record, HoodieSchema recordSchema, 
String keyFieldName) throws IOException {
+    ensureDataFileWriter(recordSchema);
+    dataFileWriter.write(record.getRecordKey(recordSchema, keyFieldName),
+        record, recordSchema, recordProperties);
+  }
+
+  public void appendDeleteRecord(HoodieRecord record, HoodieSchema 
recordSchema, String keyFieldName) throws IOException {
+    ensureDeleteFileWriter();
+    String recordKey = record.getRecordKey(recordSchema, keyFieldName);
+    Comparable orderingValue = record.getOrderingValue(
+        recordSchema, recordProperties, orderingFieldNames.toArray(new 
String[0]));
+    Object deleteEngineRecord = recordContext.constructEngineRecord(
+        deleteLogSchema, createDeleteLogFieldValues(recordKey, orderingValue));
+    // Keep isDelete=false here so RecordContext constructs a data-bearing 
HoodieRecord
+    // with the native delete-log row. The delete semantics come from the 
delete log file
+    // itself; setting isDelete=true would create a HoodieEmptyRecord and lose 
the row.
+    BufferedRecord deleteRecord = BufferedRecords.fromEngineRecord(
+        deleteEngineRecord, deleteLogSchema, recordContext, orderingValue, 
recordKey, false);
+    deleteFileWriter.write(recordKey, 
recordContext.constructHoodieRecord(deleteRecord, record.getPartitionPath()),
+        deleteLogSchema, recordProperties);
+  }
+
+  private Object[] createDeleteLogFieldValues(String recordKey, Comparable 
orderingValue) {
+    Object[] fieldValues = new Object[deleteLogSchema.getFields().size()];
+    fieldValues[0] = recordContext.convertValueToEngineType(recordKey);
+    if (!OrderingValues.isCommitTimeOrderingValue(orderingValue)) {
+      if (orderingFieldNames.size() == 1) {
+        fieldValues[1] = orderingValue;
+      } else {
+        List<Comparable> orderingValues = 
OrderingValues.getValues((ArrayComparable) orderingValue);
+        for (int i = 0; i < orderingValues.size(); i++) {
+          fieldValues[i + 1] = orderingValues.get(i);
+        }
+      }
+    }
+    return fieldValues;
+  }
+
+  public AppendResult flushAppend() throws IOException {
+    return flushAppend(Collections.emptyMap());
+  }
+
+  public AppendResult flushDataAppend(Map<HeaderMetadataType, String> header) 
throws IOException {
+    int appendVersion = currentAppendVersion;
+    lastAppendResults = new ArrayList<>();
+    lastDataFileFormatMetadata = Option.empty();
+    addFooterMetadataToDataFile(header);
+    if (dataFileWriter != null) {
+      dataFileWriter.close();
+      lastDataFileFormatMetadata = 
Option.ofNullable(dataFileWriter.getFileFormatMetadata());
+      dataFileWriter = null;
+    }
+    if (dataLogFile != null) {
+      lastAppendResults.add(toAppendResult(dataLogFile));
+    }
+    dataLogFile = null;
+    currentAppendVersion = -1;
+    return completeAppend(appendVersion);
+  }
+
+  public AppendResult flushAppend(Map<HeaderMetadataType, String> header) 
throws IOException {
+    int appendVersion = currentAppendVersion;
+    lastAppendResults = new ArrayList<>();
+    lastDataFileFormatMetadata = Option.empty();
+    addFooterMetadata(header);
+    closeFileWriters();
+    if (dataLogFile != null) {
+      lastAppendResults.add(toAppendResult(dataLogFile));
+    }
+    if (deleteLogFile != null) {
+      lastAppendResults.add(toAppendResult(deleteLogFile));
+    }
+    dataLogFile = null;
+    deleteLogFile = null;
+    currentAppendVersion = -1;
+    return completeAppend(appendVersion);
+  }
+
+  private void addFooterMetadata(Map<HeaderMetadataType, String> header) 
throws IOException {
+    Map<String, String> footerMetadata = getFooterMetadata(header);
+    if (dataFileWriter != null) {
+      dataFileWriter.addFooterMetadata(footerMetadata);
+    }
+    if (deleteFileWriter != null) {
+      deleteFileWriter.addFooterMetadata(footerMetadata);
+    }
+  }
+
+  private void addFooterMetadataToDataFile(Map<HeaderMetadataType, String> 
header) throws IOException {
+    if (dataFileWriter != null) {
+      dataFileWriter.addFooterMetadata(getFooterMetadata(header));
+    }
+  }
+
+  private Map<String, String> getFooterMetadata(Map<HeaderMetadataType, 
String> header) throws IOException {
+    Map<String, String> logFormatMetadata = new LinkedHashMap<>();
+    logFormatMetadata.put(HeaderMetadataType.VERSION.name(), 
String.valueOf(NATIVE_LOG_FORMAT_VERSION));
+    header.forEach((key, value) -> {
+      if (value != null && key != HeaderMetadataType.SCHEMA) {
+        // filter out the redundant schema since the footer already carries it.

Review Comment:
   Which schema does "the footer already carries" mean — the physical Parquet 
MessageType, or a full logical schema? HoodieSchema carries type info that 
doesn't round-trip losslessly through MessageType, for example VECTOR type. 
Dropping SCHEMA unconditionally may lose info for native log files.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieAppendHandle.java:
##########
@@ -35,182 +34,251 @@
 import org.apache.hudi.common.model.IOType;
 import org.apache.hudi.common.model.MetadataValues;
 import org.apache.hudi.common.schema.HoodieSchema;
-import org.apache.hudi.common.schema.HoodieSchemaField;
-import org.apache.hudi.common.schema.HoodieSchemaUtils;
 import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.log.AppendResult;
-import org.apache.hudi.common.table.log.HoodieLogFormat;
-import org.apache.hudi.common.table.log.block.HoodieAvroDataBlock;
-import org.apache.hudi.common.table.log.block.HoodieDeleteBlock;
-import org.apache.hudi.common.table.log.block.HoodieHFileDataBlock;
 import org.apache.hudi.common.table.log.block.HoodieLogBlock;
 import 
org.apache.hudi.common.table.log.block.HoodieLogBlock.HeaderMetadataType;
-import org.apache.hudi.common.table.log.block.HoodieParquetDataBlock;
 import org.apache.hudi.common.table.timeline.HoodieInstantTimeGenerator;
 import org.apache.hudi.common.table.view.TableFileSystemView;
-import org.apache.hudi.common.util.ConfigUtils;
-import org.apache.hudi.common.util.DefaultSizeEstimator;
 import org.apache.hudi.common.util.HoodieRecordUtils;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.ReflectionUtils;
-import org.apache.hudi.common.util.SizeEstimator;
-import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.config.HoodieWriteConfig;
-import org.apache.hudi.exception.HoodieAppendException;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieUpsertException;
-import org.apache.hudi.metadata.HoodieIndexVersion;
-import org.apache.hudi.metadata.HoodieTableMetadataUtil;
-import org.apache.hudi.stats.HoodieColumnRangeMetadata;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.table.HoodieTable;
-import org.apache.hudi.util.CommonClientUtils;
-import org.apache.hudi.util.Lazy;
 
 import lombok.extern.slf4j.Slf4j;
 
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
-import java.util.Set;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
 
-import static 
org.apache.hudi.common.config.HoodieStorageConfig.BLOOM_FILTER_DYNAMIC_MAX_ENTRIES;
-import static 
org.apache.hudi.common.config.HoodieStorageConfig.BLOOM_FILTER_FPP_VALUE;
-import static 
org.apache.hudi.common.config.HoodieStorageConfig.BLOOM_FILTER_NUM_ENTRIES_VALUE;
-import static 
org.apache.hudi.common.config.HoodieStorageConfig.BLOOM_FILTER_TYPE;
-import static 
org.apache.hudi.common.config.HoodieStorageConfig.HFILE_COMPRESSION_ALGORITHM_NAME;
-import static 
org.apache.hudi.common.config.HoodieStorageConfig.HFILE_WITH_BLOOM_FILTER_ENABLED;
-import static 
org.apache.hudi.metadata.HoodieTableMetadataUtil.PARTITION_NAME_COLUMN_STATS;
-import static 
org.apache.hudi.metadata.HoodieTableMetadataUtil.collectColumnRangeMetadata;
-
 /**
- * IO Operation to append data onto an existing file.
+ * Shared base for append handles writing merge-on-read log files.
+ *
+ * <p>This class owns the common append lifecycle:
+ * <ol>
+ *   <li>Initialize write status and log writer state for the target file 
group.</li>
+ *   <li>Iterate incoming {@link HoodieRecord}s and route each record as an 
insert/update or delete.</li>
+ *   <li>Track write counters, record locations, write-status success/failure 
state, and runtime stats.</li>
+ *   <li>Flush pending records to one or more physical log files and update 
{@link HoodieDeltaWriteStat}s.</li>
+ *   <li>Close the record iterator and log writer, then publish 
secondary-index streaming stats when enabled.</li>
+ * </ol>
+ *
+ * <p>Concrete subclasses provide only the physical log representation. 
Inline-log handles buffer records
+ * into Hudi log blocks, while native-log handles stream records into 
engine-native file writers. Both variants
+ * share the same accounting and status update logic through this class.
  */
 @Slf4j
-public class HoodieAppendHandle<T, I, K, O> extends HoodieWriteHandle<T, I, K, 
O> {
-  // This acts as the sequenceID for records written
+public abstract class HoodieAppendHandle<T, I, K, O> extends 
HoodieWriteHandle<T, I, K, O> {
+
   private static final AtomicLong RECORD_COUNTER = new AtomicLong(1);
-  private static final int NUMBER_OF_RECORDS_TO_ESTIMATE_RECORD_SIZE = 100;
 
-  // Buffer for holding records in memory before they are flushed to disk
-  protected final List<HoodieRecord> recordList = new ArrayList<>();
-  // Buffer for holding records (to be deleted), along with their position in 
log block, in memory before they are flushed to disk
-  protected final List<Pair<DeleteRecord, Long>> recordsToDeleteWithPositions 
= new ArrayList<>();
-  // Base file instant time of the record positions
-  protected final Option<String> baseFileInstantTimeOfPositions;
-  // Incoming records to be written to logs.
   protected Iterator<HoodieRecord<T>> recordItr;
-  // Writer to log into the file group's latest slice.
-  protected HoodieLogFormat.Writer writer;
-
-  protected final List<WriteStatus> statuses;
-  // Total number of records written during appending
+  protected final List<WriteStatus> statuses = new ArrayList<>();
+  protected final Map<HeaderMetadataType, String> header = new HashMap<>();
+  protected final Properties recordProperties = new Properties();
+  protected final Option<String> baseFileInstantTimeOfPositions;
+  protected boolean isLogCompaction = false;
+  protected boolean useWriterSchema = false;
+  protected boolean doInit = true;
   protected long recordsWritten = 0;
-  // Total number of records deleted during appending
   protected long recordsDeleted = 0;
-  // Total number of records updated during appending
   protected long updatedRecordsWritten = 0;
-  // Total number of new records inserted into the delta file
   protected long insertRecordsWritten = 0;
+  protected Option<HoodieLogBlock> appendDataBlock = Option.empty();
 
-  // Average record size for a HoodieRecord. This size is updated at the end 
of every log block flushed to disk
-  private long averageRecordSize = 0;
-  // Flag used to initialize some metadata
-  private boolean doInit = true;
-  // Total number of bytes written during this append phase (an estimation)
-  protected long estimatedNumberOfBytesWritten;
-  // Number of records that must be written to meet the max block size for a 
log block
-  private long numberOfRecords = 0;
-  // Max block size to limit to for a log block
-  private final long maxBlockSize = config.getLogFileDataBlockMaxSize();
-  // Header metadata for a log block
-  protected final Map<HeaderMetadataType, String> header = new HashMap<>();
-  private final SizeEstimator<HoodieRecord> sizeEstimator;
-  // This is used to distinguish between normal append and logcompaction's 
append operation.
-  private boolean isLogCompaction = false;
-  // use writer schema for log compaction.
-  private boolean useWriterSchema = false;
-
-  private final Properties recordProperties = new Properties();
-  private final String[] orderingFields;
+  protected HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
+                               String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr,
+                               TaskContextSupplier taskContextSupplier) {
+    this(config, instantTime, hoodieTable, partitionPath, fileId, recordItr, 
taskContextSupplier, false);
+  }
 
-  /**
-   * This is used by log compaction only.
-   */
-  public HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
-                            String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr,
-                            TaskContextSupplier taskContextSupplier, 
Map<HeaderMetadataType, String> header) {
+  protected HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
+                               String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr,
+                               TaskContextSupplier taskContextSupplier, 
Map<HeaderMetadataType, String> header) {
     this(config, instantTime, hoodieTable, partitionPath, fileId, recordItr, 
taskContextSupplier, true);
-    this.useWriterSchema = true;
-    this.isLogCompaction = true;
     this.header.putAll(header);
   }
 
-  public HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
-                            String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr, TaskContextSupplier taskContextSupplier) {
-    this(config, instantTime, hoodieTable, partitionPath, fileId, recordItr, 
taskContextSupplier, false);
+  protected HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
+                               String partitionPath, String fileId, 
TaskContextSupplier taskContextSupplier) {
+    this(config, instantTime, hoodieTable, partitionPath, fileId, null, 
taskContextSupplier);
   }
 
-  private HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
-                             String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr, TaskContextSupplier taskContextSupplier, 
boolean preserveMetadata) {
+  protected HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
+                               String partitionPath, String fileId, 
Iterator<HoodieRecord<T>> recordItr,
+                               TaskContextSupplier taskContextSupplier, 
boolean preserveMetadata) {
     super(config, instantTime, partitionPath, fileId, hoodieTable,
         config.shouldWritePartialUpdates()
-            // When enabling writing partial updates to the data blocks in log 
files,
-            // i.e., partial update schema is set, the writer schema is the 
partial
-            // schema containing the updated fields only
             ? Option.of(HoodieSchema.parse(config.getPartialUpdateSchema()))
             : Option.empty(),
         taskContextSupplier,
         preserveMetadata);
     this.recordItr = recordItr;
-    this.sizeEstimator = getSizeEstimator();
-    this.statuses = new ArrayList<>();
+    this.isLogCompaction = preserveMetadata;
+    this.useWriterSchema = preserveMetadata;
     this.recordProperties.putAll(config.getProps());
-    this.orderingFields = ConfigUtils.getOrderingFields(recordProperties);
-    boolean shouldWriteRecordPositions = config.shouldWriteRecordPositions()
-        // record positions supported only from table version 8
-        && 
config.getWriteVersion().greaterThanOrEquals(HoodieTableVersion.EIGHT);
-    this.baseFileInstantTimeOfPositions = shouldWriteRecordPositions
+    this.baseFileInstantTimeOfPositions = config.shouldWriteRecordPositions()
+        && 
config.getWriteVersion().greaterThanOrEquals(HoodieTableVersion.EIGHT)
         ? getBaseFileInstantTimeOfPositions()
         : Option.empty();
   }
 
-  public HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
HoodieTable<T, I, K, O> hoodieTable,
-                            String partitionPath, String fileId, 
TaskContextSupplier sparkTaskContextSupplier) {
-    this(config, instantTime, hoodieTable, partitionPath, fileId, null, 
sparkTaskContextSupplier);
+  protected HoodieAppendHandle(HoodieWriteConfig config, String instantTime, 
String partitionPath, String fileId,
+                               HoodieTable<T, I, K, O> hoodieTable, 
Option<HoodieSchema> writerSchema,
+                               TaskContextSupplier taskContextSupplier, 
boolean preserveMetadata) {
+    super(config, instantTime, partitionPath, fileId, hoodieTable, 
writerSchema, taskContextSupplier, preserveMetadata);
+    this.baseFileInstantTimeOfPositions = config.shouldWriteRecordPositions()
+        && 
config.getWriteVersion().greaterThanOrEquals(HoodieTableVersion.EIGHT)
+        ? getBaseFileInstantTimeOfPositions()
+        : Option.empty();
   }
 
-  protected SizeEstimator<HoodieRecord> getSizeEstimator() {
-    return new DefaultSizeEstimator<>();
+  /**
+   * Writes all records from the incoming iterator and flushes remaining 
pending data.
+   */
+  public void doAppend() {
+    while (recordItr.hasNext()) {
+      HoodieRecord<T> record = recordItr.next();
+      writeRecord(record);
+    }
+    flushAppend();
   }
 
-  protected Option<String> getBaseFileInstantTimeOfPositions() {
-    return hoodieTable.getHoodieView().getLatestBaseFile(partitionPath, fileId)
-        .map(HoodieBaseFile::getCommitTime);
+  /**
+   * Writes one record for APIs that call the handle directly instead of using 
{@link #doAppend()}.
+   */
+  @Override
+  protected void doWrite(HoodieRecord record, HoodieSchema schema, 
TypedProperties props) {
+    writeRecord(record);
+  }
+
+  /**
+   * Writes a keyed record map, primarily used by log compaction paths.
+   */
+  @Override
+  public void write(Map<String, HoodieRecord<T>> recordMap) {
+    try {
+      for (Map.Entry<String, HoodieRecord<T>> entry : recordMap.entrySet()) {
+        HoodieRecord<T> record = entry.getValue();
+        writeRecord(record);
+      }
+      flushAppend();
+    } catch (Exception e) {
+      throw new HoodieUpsertException("Failed to compact blocks for fileId " + 
fileId, e);
+    }
+  }
+
+  /**
+   * Flushes pending data, closes resources, finalizes log file sizes, and 
publishes secondary-index stats.
+   */
+  @Override
+  public List<WriteStatus> close() {
+    try {
+      if (isClosed()) {
+        return statuses.isEmpty() ? 
java.util.Collections.singletonList(writeStatus) : statuses;

Review Comment:
   uses fully-qualified java.util.Collections.singletonList(...) since the 
import was dropped from the base class; prefer re-adding the import.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to