voonhous commented on code in PR #19869:
URL: https://github.com/apache/hudi/pull/19869#discussion_r3965129497


##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/index/record/BaseRecordIndexer.java:
##########
@@ -478,11 +478,48 @@ private HoodieData<HoodieRecord> 
getRecordIndexAdditionalUpserts(
     } else if (operationType == WriteOperationType.DELETE_PARTITION) {
       // all records from the target partition(s) to be deleted from RLI
       return getRecordIndexReplacedRecords((HoodieReplaceCommitMetadata) 
commitMetadata, fsView);
+    } else if (commitMetadata instanceof HoodieReplaceCommitMetadata && 
operationType != WriteOperationType.CLUSTER) {
+      // a replace commit that is neither a table service nor an overwrite, 
e.g. files written outside Hudi being
+      // registered in the table. The replaced file groups are dropped without 
their records being rewritten under
+      // the same key, so the records of the replaced base files are deleted 
from RLI unless this commit wrote them again.
+      return 
getRecordIndexReplacedFileGroupRecords((HoodieReplaceCommitMetadata) 
commitMetadata, fsView)
+          .mapToPair(r -> Pair.of(r.getKey(), r))
+          .leftOuterJoin(updatesFromWriteStatuses.mapToPair(r -> 
Pair.of(r.getKey(), r)))
+          .values()
+          .filter(p -> !p.getRight().isPresent())
+          .map(Pair::getLeft);
     } else {
       return engineContext.emptyHoodieData();
     }
   }
 
+  /**
+   * Reads the record keys of the latest base file of every file group 
replaced by the given commit and
+   * returns a delete record for each of them.
+   */
+  private HoodieData<HoodieRecord> 
getRecordIndexReplacedFileGroupRecords(HoodieReplaceCommitMetadata 
replaceCommitMetadata, Lazy<HoodieTableFileSystemView> fsView) {

Review Comment:
   **major:** This covers the incremental path, but RLI initialize still cannot 
run over registered external files. `readRecordKeysFromFileSliceSnapshot` (line 
268) projects the key from `populateMetaFields ? recordKeySchema : 
projectSchema(dataSchema, recordKeyFields)`, which is empty for this table 
config, and line 288 hardcodes `fileIdEncoding = 0`, so 
`HoodieMetadataPayload:703` throws `Invalid UUID or index: 
fileID=file_1.parquet`. SI initialize goes through the fixed generator, so the 
two indexes diverge on bootstrap. The functional test never reaches this 
because init returns empty when there is no completed commit. Could the init 
path get the positional fallback plus `getWritesFileIdEncoding()`, with a test 
that registers files first and enables RLI+SI afterwards?
   



##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metadata/index/secondary/TestSecondaryIndexer.java:
##########
@@ -171,6 +173,34 @@ void testBuildUpdateForCompactReturnsEmpty() {
         commitMetadata)).isEmpty());
   }
 
+  @Test
+  void testBuildUpdateForReplaceCommitFromExternalWriterIsNotRejected() {
+    // files written outside Hudi are registered through replace commits with 
an unknown operation type
+    HoodieEngineContext engineContext = mock(HoodieEngineContext.class);
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);
+    HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+    HoodieTableConfig tableConfig = mock(HoodieTableConfig.class);
+    HoodieIndexMetadata indexMetadata = mock(HoodieIndexMetadata.class);
+    HoodieIndexDefinition indexDefinition = mock(HoodieIndexDefinition.class);
+
+    
when(metaClient.getIndexMetadata()).thenReturn(org.apache.hudi.common.util.Option.of(indexMetadata));
+    when(metaClient.getTableConfig()).thenReturn(tableConfig);
+    
when(tableConfig.getMetadataPartitions()).thenReturn(Collections.emptySet());
+    
when(indexMetadata.getIndexDefinitions()).thenReturn(Collections.singletonMap("secondary_index_idx",
 indexDefinition));
+    when(indexDefinition.getIndexName()).thenReturn("secondary_index_idx");
+
+    HoodieReplaceCommitMetadata commitMetadata = new 
HoodieReplaceCommitMetadata();
+    commitMetadata.setOperationType(WriteOperationType.UNKNOWN);
+    commitMetadata.addReplaceFileId("p1", "file_1.parquet");
+
+    SecondaryIndexer indexer = new SecondaryIndexer(engineContext, 
writeConfig, metaClient);
+    assertTrue(indexer.buildUpdate(IndexUpdateContext.of(

Review Comment:
   **major:** This test passes with the production change reverted. `UNKNOWN` 
is non-null, so the added `operationType != null` guard is never the deciding 
term, and `getMetadataPartitions()` is stubbed to an empty set, so 
`buildUpdate` returns an empty list before the loop body runs, whatever the 
branch does. Could it leave `operationType` unset (null, which is what a client 
that never calls `setOperationType` produces) and stub a `secondary_index_*` 
partition, asserting one `IndexPartitionAndRecords` instead of empty?
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/index/secondary/SecondaryIndexer.java:
##########
@@ -115,7 +115,7 @@ public List<IndexPartitionAndRecords> 
buildUpdate(IndexUpdateContext context) {
     // If write operation type based on commit metadata is COMPACT or CLUSTER 
then no need to update,
     // because these operations do not change the secondary key - record key 
mapping.
     WriteOperationType operationType = 
context.commitMetadata().getOperationType();
-    if (operationType.isInsertOverwriteOrDeletePartition()) {
+    if (operationType != null && 
operationType.isInsertOverwriteOrDeletePartition()) {

Review Comment:
   **minor:** Not blocking. The same value is dereferenced unguarded in 
`PartitionStatsIndexer.buildUpdate:117` 
(`getOperationType().equals(DELETE_PARTITION)`), so a null operation type NPEs 
there whenever column stats is on, regardless of this guard. XTable itself sets 
`UNKNOWN`, so this has the same defensive scope as this line. Should that call 
get the same null tolerance, or should `HoodieCommitMetadata.setOperationType` 
reject null so it fails in one place?
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/index/record/BaseRecordIndexer.java:
##########
@@ -478,11 +478,48 @@ private HoodieData<HoodieRecord> 
getRecordIndexAdditionalUpserts(
     } else if (operationType == WriteOperationType.DELETE_PARTITION) {
       // all records from the target partition(s) to be deleted from RLI
       return getRecordIndexReplacedRecords((HoodieReplaceCommitMetadata) 
commitMetadata, fsView);
+    } else if (commitMetadata instanceof HoodieReplaceCommitMetadata && 
operationType != WriteOperationType.CLUSTER) {

Review Comment:
   **minor:** Not blocking. This is an exclusion list of one, so `null`, 
`UNKNOWN`, and any replace operation type added later all inherit "delete every 
record of every replaced file group" plus a full read of those files. Today 
only `null` / `UNKNOWN` reach it (bucket rescale is recorded as 
`INSERT_OVERWRITE`, `DatasetBulkInsertOverwriteCommitActionExecutor:104`). 
Could the condition be positive, so a future operation type has to opt in?
   
   ```suggestion
       } else if (commitMetadata instanceof HoodieReplaceCommitMetadata
           && (operationType == null || operationType == 
WriteOperationType.UNKNOWN)) {
   ```
   



##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metadata/index/record/TestRecordIndexer.java:
##########
@@ -227,6 +238,82 @@ void 
testBuildUpdateWithNonEmptyCommitMetadataProducesPartitionEntry() {
     assertEquals(fileID, location.getFileId());
   }
 
+  @Test
+  @SuppressWarnings("unchecked")
+  void testBuildUpdateDeletesRecordsOfFileGroupsReplacedByExternalWriter() {
+    // files written outside Hudi are registered through replace commits with 
an unknown operation type. The records of the
+    // replaced file groups leave the index unless the same key is written 
again in the commit.
+    HoodieEngineContext engineContext = new 
HoodieLocalEngineContext(getDefaultStorageConf());
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);

Review Comment:
   **minor:** Not blocking. These mock lines are the setup of 
`testBuildUpdateWithNonEmptyCommitMetadataProducesPartitionEntry` (lines 
177-196) copied verbatim, inline 
`(org.apache.hudi.storage.StorageConfiguration)` cast included. Could the block 
move to a private fixture helper, with `StorageConfiguration` imported so the 
cast reads normally in both places?
   



##########
hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java:
##########
@@ -165,12 +185,21 @@ private static Set<Pair<String, Long>> 
filterParquetRowKeys(HoodieStorage storag
     AvroReadSupport.setAvroReadSchema(conf, readSchema.toAvroSchema());
     AvroReadSupport.setRequestedProjection(conf, readSchema.toAvroSchema());
     Set<Pair<String, Long>> rowKeys = new HashSet<>();
+    Option<String> relativeFilePath = basePath.map(path -> 
FSUtils.getRelativePartitionPath(path, convertToStoragePath(filePath)));
     long rowPosition = 0;
     try (ParquetReader reader = 
AvroParquetReader.builder(filePath).withConf(conf).build()) {
       Object obj = reader.read();
       while (obj != null) {
         if (obj instanceof GenericRecord) {
-          String recordKey = ((GenericRecord) 
obj).get(HoodieRecord.RECORD_KEY_METADATA_FIELD).toString();
+          Object recordKeyValue = ((GenericRecord) 
obj).get(HoodieRecord.RECORD_KEY_METADATA_FIELD);
+          String recordKey;
+          if (recordKeyValue != null) {
+            recordKey = recordKeyValue.toString();
+          } else {
+            ValidationUtils.checkArgument(relativeFilePath.isPresent(),

Review Comment:
   **minor:** Not blocking. This `checkArgument` sits inside the per-row loop 
and its message is a string concat, so on the common path for an external file 
(every row null-keyed) it builds the message once per row. `relativeFilePath` 
is already computed above the loop. Could the presence check move up there too, 
so the loop only calls `relativeFilePath.get()`? Same pattern at 
`SecondaryIndexRecordGenerationUtils:404`.
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/index/record/BaseRecordIndexer.java:
##########
@@ -478,11 +478,48 @@ private HoodieData<HoodieRecord> 
getRecordIndexAdditionalUpserts(
     } else if (operationType == WriteOperationType.DELETE_PARTITION) {
       // all records from the target partition(s) to be deleted from RLI
       return getRecordIndexReplacedRecords((HoodieReplaceCommitMetadata) 
commitMetadata, fsView);

Review Comment:
   **minor:** Not blocking, pre-existing. These three branches read keys 
through `readRecordKeysFromBaseFiles`, i.e. 
`HoodieAvroParquetReader.getRecordKeyIterator`, whose line 231 is 
`.get(RECORD_KEY_METADATA_FIELD).toString()` with no null guard, the pattern 
this PR guards in `ParquetUtils`. It only matters if an external writer uses 
`INSERT_OVERWRITE`; XTable uses `UNKNOWN`, but Hudi's own 
`TestExternalPathHandling:115` does. Is `INSERT_OVERWRITE` meant to be 
supported for external files, and if so should that reader get the same 
fallback?
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/SecondaryIndexRecordGenerationUtils.java:
##########
@@ -287,6 +341,12 @@ private static <T> ClosableIterator<Pair<String, String>> 
createSecondaryIndexRe
                                                                                
                 boolean allowInflightInstants) throws IOException {
     String secondaryKeyField = indexDefinition.getSourceFieldsKey();
     HoodieSchema requestedSchema = 
getRequestedSchemaForSecondaryIndex(metaClient, tableSchema, secondaryKeyField);
+    // Files written outside Hudi may carry neither the record key meta field 
nor record key fields. Their rows are
+    // keyed by the file path relative to the table base path and the row 
position, the same key the record index uses.
+    boolean hasRecordKeyMetaField = 
tableSchema.getField(RECORD_KEY_METADATA_FIELD).isPresent();
+    boolean hasRecordKeyFields = 
metaClient.getTableConfig().getRecordKeyFields().map(fields -> fields.length > 
0).orElse(false);
+    Option<String> relativeFilePath = fileSlice.getBaseFile()

Review Comment:
   **major:** This side uses `baseFile.getStoragePath()`, which keeps the 
`fg%3D<prefix>` directory. The SI initialize path at line 315 rebuilds the path 
as `getAbsoluteFilePath(basePath, partition, baseFile.getFileName())`; 
`getFileName()` is the last path component, so `base/p/bucket-0/f.parquet` 
becomes `base/p/f.parquet` and `readSchema` opens a missing file. #17788 fixed 
this exact shape, and `TestExternalPathHandling` already parameterizes it as 
`paimonExternal`. Could line 315 use `getStoragePath()` too, and could that 
prefix generator be added to the functional coverage?
   



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/SecondaryIndexRecordGenerationUtils.java:
##########
@@ -107,7 +114,10 @@ public static <T> HoodieData<HoodieRecord> 
convertWriteStatsToSecondaryIndexReco
 
     HoodieSchema tableSchema;
     try {
-      tableSchema = tryResolveSchemaForTable(dataMetaClient).get();
+      // a table without any completed commit, e.g. one that registers files 
written outside Hudi for the first time,
+      // only has the schema of the current commit.
+      tableSchema = tryResolveSchemaForTable(dataMetaClient)
+          .orElseGet(() -> 
HoodieSchema.parse(commitMetadata.getMetadata(HoodieCommitMetadata.SCHEMA_KEY)));

Review Comment:
   **minor:** Not blocking. `getMetadata(SCHEMA_KEY)` returns null when the key 
is absent (or when `commitMetadata` is null, as in the two unit tests), so 
`HoodieSchema.parse(null)` NPEs and surfaces as "Failed to get latest schema 
for ...", pointing at the wrong cause. The fallback itself has no positive 
test. Could it name the missing key explicitly, plus one test that enters this 
branch?
   
   ```suggestion
             .orElseGet(() -> 
Option.ofNullable(commitMetadata.getMetadata(HoodieCommitMetadata.SCHEMA_KEY))
                 .map(HoodieSchema::parse)
                 .orElseThrow(() -> new HoodieException("No completed commit 
and no " + HoodieCommitMetadata.SCHEMA_KEY + " in commit metadata")));
   ```
   



##########
hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/client/functional/TestExternalFileRecordAndSecondaryIndex.java:
##########
@@ -0,0 +1,219 @@
+/*
+ * 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.client.functional;
+
+import org.apache.hudi.client.WriteStatus;
+import org.apache.hudi.common.config.HoodieMetadataConfig;
+import org.apache.hudi.common.model.HoodieDeltaWriteStat;
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+import org.apache.hudi.common.model.WriteOperationType;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaField;
+import org.apache.hudi.common.schema.HoodieSchemaType;
+import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.util.ExternalFilePathUtil;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.config.HoodieIndexConfig;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.data.HoodieJavaRDD;
+import org.apache.hudi.metadata.HoodieBackedTableMetadata;
+import org.apache.hudi.metadata.HoodieTableMetadataUtil;
+import org.apache.hudi.testutils.HoodieClientTestBase;
+
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.parquet.avro.AvroParquetWriter;
+import org.apache.parquet.hadoop.ParquetWriter;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_GENERATOR;
+import static org.apache.hudi.index.HoodieIndex.IndexType.INMEMORY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Asserts that the record index and the secondary index are maintained for 
parquet files written outside Hudi
+ * and registered in the table through replace commits, the way tables 
converted from other formats are.
+ * Such files carry no record key, so every row is keyed by the file path 
relative to the table and the row position.
+ */
+public class TestExternalFileRecordAndSecondaryIndex extends 
HoodieClientTestBase {

Review Comment:
   **minor:** Not blocking. `TestExternalPathHandling` (hudi-spark-client) 
already drives this exact registration flow, parameterized over the flat and 
`paimonExternal` (`bucket-0/` prefix) name shapes and over partitioned / 
unpartitioned tables, with `createWriteStatus` / `getPath` helpers of the same 
shape as `commitExternalFile` here. Could the RLI/SI assertions land there as a 
second `@ParameterizedTest`, or at least borrow its `FileIdAndNameGenerator`, 
so the prefix shape is covered end to end?
   



##########
hudi-common/src/test/java/org/apache/hudi/common/util/TestExternalFilePathUtil.java:
##########
@@ -230,4 +230,19 @@ public void testRoundTrip_WithPrefix() {
     assertEquals(prefix + "/" + originalFile, parsed[0]);
     assertEquals(COMMIT_TIME, parsed[1]);
   }
+
+  @Test
+  public void testGetFilePathInPartition() {

Review Comment:
   **nit:** Feel free to ignore. `getFilePathInPartition` is a one-line 
delegate to `parseFileIdAndCommitTimeFromExternalFile`, whose legacy and prefix 
inputs `testParseFileIdAndCommitTimeFromExternalFile_LegacyFormat` / 
`_WithPrefix` already assert. Could this keep only the non-external passthrough 
case and add the nested-prefix input that 
`testGetFullPath_OfPartition_WithNestedPrefix_2Levels` covers for the sibling 
method?
   



##########
hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/client/functional/TestExternalFileRecordAndSecondaryIndex.java:
##########
@@ -0,0 +1,219 @@
+/*
+ * 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.client.functional;
+
+import org.apache.hudi.client.WriteStatus;
+import org.apache.hudi.common.config.HoodieMetadataConfig;
+import org.apache.hudi.common.model.HoodieDeltaWriteStat;
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+import org.apache.hudi.common.model.WriteOperationType;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaField;
+import org.apache.hudi.common.schema.HoodieSchemaType;
+import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.util.ExternalFilePathUtil;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.config.HoodieIndexConfig;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.data.HoodieJavaRDD;
+import org.apache.hudi.metadata.HoodieBackedTableMetadata;
+import org.apache.hudi.metadata.HoodieTableMetadataUtil;
+import org.apache.hudi.testutils.HoodieClientTestBase;
+
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.parquet.avro.AvroParquetWriter;
+import org.apache.parquet.hadoop.ParquetWriter;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_GENERATOR;
+import static org.apache.hudi.index.HoodieIndex.IndexType.INMEMORY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Asserts that the record index and the secondary index are maintained for 
parquet files written outside Hudi
+ * and registered in the table through replace commits, the way tables 
converted from other formats are.
+ * Such files carry no record key, so every row is keyed by the file path 
relative to the table and the row position.
+ */
+public class TestExternalFileRecordAndSecondaryIndex extends 
HoodieClientTestBase {
+
+  private static final String ID_FIELD = "id";
+  private static final String NAME_FIELD = "name";
+  private static final HoodieSchema SCHEMA = 
HoodieSchema.createRecord("external", null, null, false, Arrays.asList(
+      HoodieSchemaField.of(ID_FIELD, 
HoodieSchema.create(HoodieSchemaType.INT)),
+      HoodieSchemaField.of(NAME_FIELD, 
HoodieSchema.create(HoodieSchemaType.STRING))));
+
+  @ParameterizedTest
+  @ValueSource(strings = {"americas/brazil", ""})
+  public void testRecordAndSecondaryIndexForExternalFiles(String 
partitionPath) throws Exception {
+    // tables registered from other formats carry neither meta fields nor 
record key fields
+    Properties tableProperties = new Properties();
+    tableProperties.setProperty(HoodieTableConfig.POPULATE_META_FIELDS.key(), 
"false");
+    tableProperties.setProperty(HoodieTableConfig.RECORDKEY_FIELDS.key(), "");
+    initMetaClient(tableProperties);
+    HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
+        .withPath(metaClient.getBasePath())
+        .withSchema(SCHEMA.toString())
+        .withPopulateMetaFields(false)
+        // file ids of external files are file names, not UUIDs, so the record 
index stores them as raw strings
+        .withWritesFileIdEncoding(1)
+        
.withIndexConfig(HoodieIndexConfig.newBuilder().withIndexType(INMEMORY).build())
+        .withEmbeddedTimelineServerEnabled(false)
+        .withMetadataConfig(HoodieMetadataConfig.newBuilder()
+            .enable(true)
+            .withMetadataIndexColumnStats(false)
+            .withEnableGlobalRecordLevelIndex(true)
+            .withSecondaryIndexEnabled(true)
+            .withSecondaryIndexForColumn(NAME_FIELD)
+            .build())
+        .build();
+    writeClient = getHoodieWriteClient(writeConfig);
+    writeClient.setOperationType(WriteOperationType.UNKNOWN);
+
+    // first commit registers one file with three rows
+    String fileName1 = "file_1.parquet";
+    List<Pair<Integer, String>> rows1 = Arrays.asList(Pair.of(1, "alice"), 
Pair.of(2, "bob"), Pair.of(3, "alice"));
+    commitExternalFile(partitionPath, fileName1, rows1, 
Collections.emptyMap());
+
+    String relativePath1 = relativeFilePath(partitionPath, fileName1);
+    HoodieBackedTableMetadata tableMetadata = new HoodieBackedTableMetadata(
+        context, metaClient.getStorage(), writeConfig.getMetadataConfig(), 
writeConfig.getBasePath(), true);
+    Map<String, HoodieRecordGlobalLocation> recordIndex = 
readRecordIndex(tableMetadata, generatedKeys(relativePath1, 3));
+    assertEquals(3, recordIndex.size());
+    recordIndex.values().forEach(location -> {
+      assertEquals(partitionPath, location.getPartitionPath());
+      assertEquals(fileName1, location.getFileId());
+    });
+    String secondaryIndexPartition = secondaryIndexPartition();
+    assertEquals(new HashMap<String, Set<String>>() {

Review Comment:
   **nit:** Feel free to ignore. Double-brace initialization creates an 
anonymous `HashMap` subclass that captures the test instance (here and at line 
141), and line 213 spells out `java.util.stream.IntStream` inline. Could these 
use a `mapOf(...)` helper next to the existing `setOf(...)`, plus an 
`IntStream` import?
   



##########
hudi-spark-datasource/hudi-spark/src/test/java/org/apache/hudi/client/functional/TestExternalFileRecordAndSecondaryIndex.java:
##########
@@ -0,0 +1,219 @@
+/*
+ * 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.client.functional;
+
+import org.apache.hudi.client.WriteStatus;
+import org.apache.hudi.common.config.HoodieMetadataConfig;
+import org.apache.hudi.common.model.HoodieDeltaWriteStat;
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+import org.apache.hudi.common.model.WriteOperationType;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaField;
+import org.apache.hudi.common.schema.HoodieSchemaType;
+import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.util.ExternalFilePathUtil;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.config.HoodieIndexConfig;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.data.HoodieJavaRDD;
+import org.apache.hudi.metadata.HoodieBackedTableMetadata;
+import org.apache.hudi.metadata.HoodieTableMetadataUtil;
+import org.apache.hudi.testutils.HoodieClientTestBase;
+
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.parquet.avro.AvroParquetWriter;
+import org.apache.parquet.hadoop.ParquetWriter;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_GENERATOR;
+import static org.apache.hudi.index.HoodieIndex.IndexType.INMEMORY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Asserts that the record index and the secondary index are maintained for 
parquet files written outside Hudi
+ * and registered in the table through replace commits, the way tables 
converted from other formats are.
+ * Such files carry no record key, so every row is keyed by the file path 
relative to the table and the row position.
+ */
+public class TestExternalFileRecordAndSecondaryIndex extends 
HoodieClientTestBase {
+
+  private static final String ID_FIELD = "id";
+  private static final String NAME_FIELD = "name";
+  private static final HoodieSchema SCHEMA = 
HoodieSchema.createRecord("external", null, null, false, Arrays.asList(
+      HoodieSchemaField.of(ID_FIELD, 
HoodieSchema.create(HoodieSchemaType.INT)),
+      HoodieSchemaField.of(NAME_FIELD, 
HoodieSchema.create(HoodieSchemaType.STRING))));
+
+  @ParameterizedTest
+  @ValueSource(strings = {"americas/brazil", ""})
+  public void testRecordAndSecondaryIndexForExternalFiles(String 
partitionPath) throws Exception {
+    // tables registered from other formats carry neither meta fields nor 
record key fields
+    Properties tableProperties = new Properties();
+    tableProperties.setProperty(HoodieTableConfig.POPULATE_META_FIELDS.key(), 
"false");
+    tableProperties.setProperty(HoodieTableConfig.RECORDKEY_FIELDS.key(), "");
+    initMetaClient(tableProperties);
+    HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
+        .withPath(metaClient.getBasePath())
+        .withSchema(SCHEMA.toString())
+        .withPopulateMetaFields(false)
+        // file ids of external files are file names, not UUIDs, so the record 
index stores them as raw strings
+        .withWritesFileIdEncoding(1)
+        
.withIndexConfig(HoodieIndexConfig.newBuilder().withIndexType(INMEMORY).build())
+        .withEmbeddedTimelineServerEnabled(false)
+        .withMetadataConfig(HoodieMetadataConfig.newBuilder()
+            .enable(true)
+            .withMetadataIndexColumnStats(false)
+            .withEnableGlobalRecordLevelIndex(true)

Review Comment:
   **nit:** Feel free to ignore. Every new test pins `isPartitionedRLI == 
false` (global RLI here, `eq(false)` in `TestRecordIndexer`), so the 
`EmptyHoodieRecordPayloadWithPartition` delete variant that 
`createRecordIndexDelete` produces for a partitioned RLI is never constructed. 
Would one parameterization with `withEnableRecordLevelIndex(true)` be worth 
adding?
   



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