vinishjail97 commented on code in PR #19869:
URL: https://github.com/apache/hudi/pull/19869#discussion_r3965696244
##########
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:
Fixed in b10d8167. For a table without a record key
(`HoodieTableConfig.hasRecordKey()` is false) the initialize path now keys
every row positionally through the same `FileFormatUtils.readRowKeys(storage,
file, basePath)` call the update path uses, so bootstrap and incremental
updates produce identical keys by construction. It uses
`getWritesFileIdEncoding()` instead of the hardcoded 0, and fails if the slice
has log files or no base file.
`testIndexesAreBuiltFromRegisteredFilesWhenEnabledLater` registers a file with
both indexes off, enables RLI and SI, registers a second file and asserts both
indexes cover both files, then drops the first file and asserts both indexes
forget it.
##########
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:
Fixed in b10d8167: the initialize path uses `baseFile.getStoragePath()`. The
functional test is now parameterized over the four shapes
`TestExternalPathHandling` uses (partitioned and unpartitioned, flat and
`bucket-0/` prefix), with the same `fileId = prefix/fileName` and marker path
construction. I kept the coverage in the new class so the RLI and SI assertions
stay together; see my reply on the test placement thread.
##########
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:
Rewritten in b10d8167. The test now sets the operation type to null
explicitly, stubs a `secondary_index_idx` partition, uses a replace commit
without write stats, and asserts one `IndexPartitionAndRecords` that carries
the delete record returned by a mocked
`convertWriteStatsToSecondaryIndexRecords`. It fails when either the null guard
or the early-return change is reverted. One note: a fresh
`HoodieCommitMetadata` initializes `operationType` to `UNKNOWN`, so "never set"
is `UNKNOWN`; null only comes from an explicit `setOperationType(null)` or a
JSON null, which is why the guard accepts both.
##########
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:
Done in b10d8167:
`WriteOperationType.DELETE_PARTITION.equals(context.commitMetadata().getOperationType())`.
I left the setter alone; rejecting null there touches every writer and belongs
in its own change.
##########
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:
Took the suggestion in b10d8167.
`fallsBackToTheCommitSchemaWhenTheTableHasNoCompletedCommit` covers the
fallback and asserts the error names `SCHEMA_KEY` when it is missing.
##########
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:
Done in b10d8167 through `WriteOperationType.isUnknown(op)` (`null ||
UNKNOWN`). The same predicate gates the RLI branch, the SI replaced-file-group
generation, and the SI early return.
##########
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:
I parameterized the new test over the same four shapes and borrowed the
`fileId = prefix/fileName` and marker path construction, but kept it in
`TestExternalFileRecordAndSecondaryIndex`: the RLI and SI assertions, the
bootstrap flow and the drop-only commit share the helpers there, and
`TestExternalPathHandling` would otherwise need index setup on top of its
column stats flow. Happy to move it if you prefer a single class.
--
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]