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


##########
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:
   Addressed in aa48c8eb5fc1: re-added the `java.util.Collections` import and 
switched the call back to `Collections.singletonList(...)`.



##########
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:
   Addressed in aa48c8eb5fc1: changed the close-path wrapper from 
`RuntimeException` to `HoodieIOException`.



##########
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:
   Addressed in aa48c8eb5fc1: the data-file rollover path now calls 
`flushAppend()` instead of the old data-only flush. That closes any pending 
data and delete native writers together and advances the append version as one 
unit, so `.log.parquet` and `.deletes.parquet` stay aligned for comparator 
ordering.



##########
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:
   Addressed in aa48c8eb5fc1: added `HoodieTableVersion.TEN` (version code 10) 
mapped to release `1.3.0`, while keeping `HoodieTableVersion.current()` at 
`NINE`. `TABLE_STORAGE_LAYOUT` stays `.sinceVersion("1.3.0")`, and the 
validation test now covers `NINE` as invalid and `TEN` as valid.



##########
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:
   Addressed in aa48c8eb5fc1: removed the unused no-arg `flushAppend()` helper 
along with the data-only `flushDataAppend()` path.



##########
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:
   Addressed in aa48c8eb5fc1: restored the logical schema metadata by putting 
`HeaderMetadataType.SCHEMA` back into the header and no longer filtering it out 
when building native data-log footer metadata. Per the follow-up decision, 
footer metadata is only added to native data logs; native delete logs do not 
add a footer.



##########
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:
   Addressed in aa48c8eb5fc1: added `canWriteDeleteFile()` and the native 
append handle now flushes via `flushAppend()` before appending a delete when 
the current delete writer reaches its write limit. Delete-heavy workloads 
should roll to the next native log version instead of growing one delete file 
indefinitely.



-- 
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