hudi-agent commented on code in PR #19869:
URL: https://github.com/apache/hudi/pull/19869#discussion_r3965426768


##########
hudi-common/src/main/java/org/apache/hudi/metadata/BaseFileRecordParsingUtils.java:
##########
@@ -67,7 +69,9 @@ public static Iterator<HoodieRecord> 
generateRLIMetadataHoodieRecordsForBaseFile
                                                                                
    boolean isPartitionedRLI) {
     String partition = writeStat.getPartitionPath();
     String latestFileName = FSUtils.getFileNameFromPath(writeStat.getPath());
-    String fileId = FSUtils.getFileId(latestFileName);
+    // a file written outside Hudi keeps its own name, which may contain 
underscores, so the file id is parsed from the marker
+    String fileId = 
FileNameParser.parseBaseFile(latestFileName).map(FileNameParser.BaseFileName::getFileId)

Review Comment:
   🤖 With the default `_hoodie.writes.fileid.encoding` (0 = UUID), 
`createRecordIndexUpdate` below calls `UUID.fromString` on this file id, so for 
an external file id like `file_1.parquet` the first RLI update throws 
`HoodieMetadataException("Invalid UUID or index")` unless the caller sets the 
internal config to 1 (the new functional test does; only Kafka Connect does in 
production). Since `HoodieRecordIndexInfo` stores `fileIdEncoding` per record, 
would it make sense to fall back to raw-string encoding here when 
`FileNameParser.parseBaseFile` reports an external file, so XTable-registered 
tables don't need to know about that config?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/SecondaryIndexRecordGenerationUtils.java:
##########
@@ -205,6 +221,44 @@ public static <T> HoodieData<HoodieRecord> 
convertWriteStatsToSecondaryIndexReco
     return HoodieTableMetadataUtil.reduceByKeys(secondaryIndexRecords, 
parallelism, false);
   }
 
+  /**
+   * Generates delete records for every record of the file groups that the 
given replace commit replaces without
+   * writing to them again, e.g. files written outside Hudi that are 
superseded by newer files. Records that the same

Review Comment:
   🤖 nit: `convertReplacedFileGroupsToSecondaryIndexRecords` takes 9 
parameters, most of which are just threaded through from the caller. Might be 
worth bundling the shared context (`writeConfig`, `metadataConfig`, 
`dataMetaClient`, `engineContext`) so the signature is easier to read and 
extend.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



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

Review Comment:
   🤖 nit: the branch condition `commitMetadata instanceof 
HoodieReplaceCommitMetadata && operationType != WriteOperationType.CLUSTER` 
mixes an instanceof check with an operation-type check inline in the else-if 
chain — could you pull this into a named boolean/helper (e.g. 
`isExternalFileRegistrationReplaceCommit(...)`) so the intent ("replace commit 
that isn't a known table-service replace") is clearer at the call site?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/SecondaryIndexRecordGenerationUtils.java:
##########
@@ -197,6 +207,12 @@ public static <T> HoodieData<HoodieRecord> 
convertWriteStatsToSecondaryIndexReco
       return records.iterator();
     });
 
+    if (commitMetadata instanceof HoodieReplaceCommitMetadata) {

Review Comment:
   🤖 Confirmed from the code: `BaseRecordIndexer.buildUpdate` calls 
`getRecordIndexAdditionalUpserts` unconditionally (line 147), so the new 
replace-commit branch emits RLI deletes even when `partitionToWriteStats` is 
empty, while `SecondaryIndexer.getSecondaryIndexUpdates` returns at the 
`allWriteStats.isEmpty()` check on line 147 before 
`convertReplacedFileGroupsToSecondaryIndexRecords` can run. A drop-only replace 
commit would leave SI with dangling entries; gating that early return on 
`!(commitMetadata instanceof HoodieReplaceCommitMetadata && 
!getPartitionToReplaceFileIds().isEmpty())` should close the gap.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/SecondaryIndexRecordGenerationUtils.java:
##########
@@ -319,17 +380,32 @@ public boolean hasNext() {
         while (recordIterator.hasNext()) {
           T record = recordIterator.next();
           Object secondaryKey = 
readerContext.getRecordContext().getValue(record, requestedSchema, 
secondaryKeyField);
-            nextValidRecord = Pair.of(
-                readerContext.getRecordContext().getRecordKey(record, 
requestedSchema),
-                secondaryKey == null ? null : secondaryKey.toString()
-            );
+          nextValidRecord = Pair.of(getRecordKey(record), secondaryKey == null 
? null : secondaryKey.toString());
+          rowPosition++;
           return true;
         }
 
         // If no valid records are found
         return false;
       }
 
+      private String getRecordKey(T record) {
+        Object recordKey;
+        if (hasRecordKeyMetaField) {
+          recordKey = readerContext.getRecordContext().getValue(record, 
requestedSchema, RECORD_KEY_METADATA_FIELD);
+        } else if (hasRecordKeyFields) {
+          recordKey = readerContext.getRecordContext().getRecordKey(record, 
requestedSchema);
+        } else {
+          recordKey = null;

Review Comment:
   🤖 One more angle on this: the branch order in `getRecordKey` means that 
whenever the schema carries `_hoodie_record_key` (any `MetaFieldsMode != 
NONE`), the `hasRecordKeyFields` branch is never tried, so a keyed table in a 
selective mode goes straight from a null meta column to a positional key rather 
than deriving the key from its record key fields the way the old 
`recordKeyExtractor` path would. Falling through to the key-fields extractor 
when the meta value is null, and only generating `path_pos` when neither yields 
a key, would keep both keyed layouts correct.



##########
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:
   🤖 Checked this and it lines up: `readRecordKeysFromFileSliceSnapshot` is the 
only RLI init path (`initialize` line 114), it hard-codes `fileIdEncoding = 0` 
at line 288 while `buildUpdate` uses 
`dataTableWriteConfig.getWritesFileIdEncoding()`, and the functional test sets 
`withWritesFileIdEncoding(1)` up front so init never runs. This is reachable in 
practice whenever RLI is enabled after the first replace commit (e.g. via the 
async indexer), so a test in that order would catch it.



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