vinishjail97 commented on code in PR #835:
URL: https://github.com/apache/incubator-xtable/pull/835#discussion_r4108273843


##########
xtable-core/src/main/java/org/apache/xtable/hudi/BaseFileUpdatesExtractor.java:
##########
@@ -273,17 +315,31 @@ private WriteStatus toWriteStatus(
     WriteStatus writeStatus = new WriteStatus();
     Path path = new CachingPath(file.getPhysicalPath());
     String partitionPath =
-        partitionPathOptional.orElseGet(() -> 
HudiPathUtils.getPartitionPath(tableBasePath, path));
+        partitionPathOptional.orElseGet(() -> truePartitionPath(tableBasePath, 
path));
     String fileId = getFileId(path);
     String filePath =
         
path.toUri().getPath().substring(tableBasePath.toUri().getPath().length() + 1);
     String fileName = path.getName();
+    Optional<String> fileGroupPrefix = externalFileGroupPrefix(path);
+    // For external bucketed files encode the file-group prefix in the marker 
and keep the file name
+    // (not the bucket-relative path) as the marked name; otherwise fall back 
to the plain marker on
+    // the full relative path. In both cases the directory portion is 
preserved as-is.
+    String markedPath =
+        fileGroupPrefix
+            .map(
+                prefix ->
+                    filePath.substring(0, filePath.length() - 
fileName.length())
+                        + 
ExternalFilePathUtil.appendCommitTimeAndExternalFileMarker(
+                            fileName, commitTime, prefix))

Review Comment:
   This writes the `_fg%3D<prefix>_hudiext` marker for v6 targets as well. Hudi 
0.15's `ExternalFilePathUtil` only strips the last `_<segment>`, so a 0.x 
reader would look for `data.parquet_<commitTime>` in the wrong directory. Would 
it be reasonable to use the prefix only when the table version is NINE, since 
the PR keeps v6 as the default for 0.x readers?



##########
xtable-core/src/main/java/org/apache/xtable/hudi/BaseFileUpdatesExtractor.java:
##########
@@ -75,6 +75,13 @@ public class BaseFileUpdatesExtractor {
   private static final Pattern HUDI_BASE_FILE_PATTERN =
       Pattern.compile(
           
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[0-9]_[0-9a-fA-F-]+_[0-9]+\\.");
+  // External bucketed sources (e.g. Paimon) lay files out as {@code 
<partition>/bucket-N/<file>}.
+  // Hudi treats the trailing {@code bucket-N} directory as a file-group 
prefix within the partition
+  // rather than as part of the partition path (see Hudi PR #17788). Detecting 
such a directory lets
+  // us register the file under its true partition with the prefix encoded 
into the external-file
+  // marker, so an unpartitioned external table is not mistakenly read as 
partitioned by "bucket-N".
+  private static final Pattern EXTERNAL_FILE_GROUP_PREFIX_PATTERN =

Review Comment:
   This pattern applies to every source format, not only Paimon. A 
non-hive-style partition value such as `bucket-3` (for example, a Hudi-style 
layout read through the Iceberg or Delta source with a custom location) would 
lose its partition segment. Would it be reasonable to enable this only for the 
Paimon source, for example through a flag on the target's source-format context?



##########
xtable-core/src/test/java/org/apache/xtable/ITConversionController.java:
##########
@@ -153,7 +155,16 @@ private static Stream<Arguments> 
generateTestParametersForFormatsSyncModesAndPar
     for (String sourceFormat : Arrays.asList(HUDI, DELTA, ICEBERG, PAIMON)) {
       for (SyncMode syncMode : SyncMode.values()) {
         for (boolean isPartitioned : new boolean[] {true, false}) {
-          arguments.add(Arguments.of(sourceFormat, syncMode, isPartitioned));
+          if (sourceFormat.equals(HUDI)) {
+            // Hudi is the source here (not a target), so the Hudi target 
version does not apply.

Review Comment:
   Every Hudi source table in this class is written at v6, so the v9 
completion-time source path is never exercised through `ConversionController`, 
where `lastInstantSynced` and the pending list round-trip through target 
metadata. The `ITHudiConversionSource` out-of-order test calls 
`getCommitsBacklog` with a synthetic checkpoint that a real run does not 
produce. Would it be reasonable to add v9-source cases here? I have three 
locally (snapshot with an inflight commit, incremental with an inflight commit, 
and the lagging-target case).



##########
xtable-core/src/test/java/org/apache/xtable/hudi/ITHudiConversionSource.java:
##########
@@ -337,9 +431,9 @@ public void testForIncrementalSyncSafetyCheck() {
     HoodieTableType tableType = HoodieTableType.COPY_ON_WRITE;
     HudiTestUtil.PartitionConfig partitionConfig = 
HudiTestUtil.PartitionConfig.of(null, null);
     String tableName = GenericTable.getTableName();
-    try (TestJavaHudiTable table =
-        TestJavaHudiTable.forStandardSchema(
-            tableName, tempDir, partitionConfig.getHudiConfig(), tableType)) {
+    try (TestSparkHudiTable table =

Review Comment:
   `testForIncrementalSyncSafetyCheck` and `testOnlyUpsertsAfterInserts` still 
run at v6 only, so `isAffectedByCleanupProcess` has no v9 coverage. Would it 
work to parameterize them over SIX and NINE like the other cases?



##########
xtable-core/src/main/java/org/apache/xtable/hudi/BaseFileUpdatesExtractor.java:
##########
@@ -250,7 +257,42 @@ private String getFileId(Path filePath) {
     if (isFileCreatedByHudiWriter(fileName)) {
       return FSUtils.getFileId(fileName);
     }
-    return fileName;
+    // External bucketed files keep their file-group prefix as part of the 
fileId so the prefix can
+    // be recovered when Hudi resolves the physical path of the externally 
created file.
+    return externalFileGroupPrefix(filePath)
+        .map(prefix -> prefix + "/" + fileName)
+        .orElse(fileName);
+  }
+
+  /**
+   * Returns the external file-group prefix (e.g. Paimon's {@code bucket-N}) 
when the file's
+   * immediate parent directory denotes a file group within the partition 
rather than a partition
+   * segment, otherwise empty.
+   */
+  private Optional<String> externalFileGroupPrefix(Path filePath) {
+    Path parent = filePath.getParent();
+    if (parent == null) {
+      return Optional.empty();
+    }
+    String parentName = parent.getName();
+    return EXTERNAL_FILE_GROUP_PREFIX_PATTERN.matcher(parentName).matches()
+        ? Optional.of(parentName)
+        : Optional.empty();
+  }
+
+  /**
+   * Resolves the true Hudi partition path for a file, stripping any trailing 
external file-group
+   * prefix directory (e.g. {@code bucket-N}) so it is not treated as part of 
the partition.
+   */
+  private String truePartitionPath(Path tableBasePath, Path filePath) {

Review Comment:
   On a Paimon to Hudi target created by 0.4.0, files live in partition 
`dt=x/bucket-0` with fileId `<fileName>`. After this change, incremental 
removals are keyed as partition `dt=x` and fileId `bucket-0/<fileName>`, so I 
am not sure the replace commit matches the old file groups, and compacted-away 
files could stay visible. A snapshot sync looks self-healing through 
`partitionPathsToDrop`. Would it work to force a snapshot sync when the 
existing target still has `bucket-N` partitions, or to note this in the upgrade 
docs?



##########
xtable-core/src/main/java/org/apache/xtable/hudi/HudiConversionSource.java:
##########
@@ -237,10 +248,86 @@ private HoodieTimeline getCompletedCommits() {
     return metaClient.getActiveTimeline().filterCompletedInstants();
   }
 
+  /**
+   * Table version 8+ (Hudi 1.x, timeline layout V2) makes a commit visible at 
its completion time
+   * rather than its requested (instant) time, so incremental selection and 
ordering must be based
+   * on completion time. Table version 6 keeps the legacy requested-time 
ordering.
+   */
+  private boolean usesCompletionTimeOrdering() {
+    return metaClient
+        .getTableConfig()
+        .getTableVersion()
+        .greaterThanOrEquals(HoodieTableVersion.EIGHT);
+  }
+
+  private HoodieInstant getLatestCompletedInstant(HoodieTimeline 
completedTimeline) {
+    Option<HoodieInstant> latestCommit =
+        usesCompletionTimeOrdering()
+            ? Option.fromJavaOptional(
+                completedTimeline
+                    .getInstantsOrderedByCompletionTime()
+                    .reduce((first, second) -> second))
+            : completedTimeline.lastInstant();
+    return latestCommit.orElseThrow(
+        () -> new ReadException("Unable to read latest commit from Hudi source 
table"));
+  }
+
+  /**
+   * Selects the commits that completed after the last synced commit's 
completion time, ordered by
+   * completion time. Unlike the requested-time path this also surfaces 
commits whose requested time
+   * is older than the last synced commit but whose completion is newer 
(out-of-order completion).
+   */
+  private CommitsPair getCompletedAndPendingCommitsAfterCompletionTime(
+      HoodieInstant commitInstant) {
+    List<HoodieInstant> modifiedAfter =
+        metaClient
+            .getActiveTimeline()
+            
.findInstantsModifiedAfterByCompletionTime(commitInstant.getCompletionTime())
+            .getInstants();
+    List<HoodieInstant> completedInstants =
+        modifiedAfter.stream()
+            .filter(HoodieInstant::isCompleted)
+            .sorted(Comparator.comparing(HoodieInstant::getCompletionTime))
+            .collect(Collectors.toList());
+    List<Instant> pendingInstants =
+        modifiedAfter.stream()
+            .filter(hoodieInstant -> hoodieInstant.isInflight() || 
hoodieInstant.isRequested())
+            .map(
+                hoodieInstant ->
+                    
HudiInstantUtils.parseFromInstantTime(hoodieInstant.requestedTime()))
+            .collect(Collectors.toList());
+    return CommitsPair.builder()
+        .completedCommits(completedInstants)
+        .pendingCommits(pendingInstants)
+        .build();
+  }
+
+  /**
+   * Merges two completed-commit lists, dedupes by requested time and action, 
and orders by
+   * completion time. The action is part of the dedup key because distinct 
actions can legally share
+   * a requested time: a savepoint instant reuses the requested time of the 
commit it pins, and
+   * keying on requested time alone would drop it from the backlog.
+   */
+  private List<HoodieInstant> orderByCompletionTimeAndDedup(

Review Comment:
   With a completion-ordered backlog, a target that syncs `[late, early]` in 
one run records `early`'s requested time as `lastInstantSynced`. When another 
target lags, the next run replays `[late, early]`, and 
`TableFormatSync.isChangeApplicableForLastSyncMetadata` re-applies `late` 
because its requested time is after `early`. A local `ITConversionController` 
test (sync Iceberg alone through both commits, then sync Iceberg and Delta) 
gives Iceberg 200 rows against 150 in the source on v9, and passes on v6. Would 
using completion time as the v9 sync checkpoint work here? I think this blocks 
enabling `table_version=9` sources for multi-target syncs, and I can push the 
repro test.



##########
xtable-core/src/main/java/org/apache/xtable/hudi/HudiConversionTarget.java:
##########
@@ -288,7 +296,9 @@ public void syncFilesForDiff(InternalFilesDiff 
internalFilesDiff) {
   public void beginSync(InternalTable table) {
     if (!metaClient.isPresent()) {
       metaClient =
-          Optional.of(hudiTableManager.initializeHudiTable(tableDataPath, 
table, databaseName));
+          Optional.of(
+              hudiTableManager.initializeHudiTable(
+                  tableDataPath, table, databaseName, tableVersion));

Review Comment:
   The configured version applies only when the table is created. An existing 
v6 table with `table_version=9` stays at v6 without a message. A WARN with the 
base path when the two differ would help users. This is a suggestion and does 
not block.



##########
xtable-core/src/main/java/org/apache/xtable/hudi/HudiTargetConfig.java:
##########
@@ -0,0 +1,57 @@
+/*
+ * 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.xtable.hudi;
+
+import java.util.Properties;
+
+import lombok.Value;
+
+import org.apache.hudi.common.table.HoodieTableVersion;
+
+/** Configuration of the Hudi conversion target. */
+@Value
+public class HudiTargetConfig {
+  /**
+   * Table format version to write for the Hudi target. Supported values are 
{@code 6} (the legacy
+   * 0.x timeline layout, column-stats index V1) and {@code 9} (the Hudi 1.x 
timeline layout,
+   * column-stats index V2). Defaults to {@code 9}.

Review Comment:
   Nits: this Javadoc and the comments at `HudiConversionTarget.java:117`, 
`HudiConversionTarget.java:641` and `HudiTableManager.java:127` still say the 
default is 9. On line 46, a value like `abc` throws a raw 
`NumberFormatException`, and an unknown code can fail in `fromVersionCode` 
before the clearer message below, so a single try/catch that rethrows the 
`IllegalArgumentException` would cover both.



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