This is an automated email from the ASF dual-hosted git repository.

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 3eef646d3afd fix(hive): read the skeleton file when a bootstrap query 
projects no columns (#19510)
3eef646d3afd is described below

commit 3eef646d3afdc692b84c432120900f86e7ac3a58
Author: Ranga Reddy <[email protected]>
AuthorDate: Thu Aug 27 19:10:34 2026 +0530

    fix(hive): read the skeleton file when a bootstrap query projects no 
columns (#19510)
    
    A BootstrapBaseFileSplit carries two files: the split's own path is the 
skeleton, inside the
    table root, and getBootstrapFileSplit() is the external source file, which 
is not.
    createBootstrappingRecordReader opens one of them when only one is needed, 
and it tested
    "no Hudi meta column projected" first, which resolves to the external file.
    
    SELECT COUNT(*) projects no columns at all, so both single-file conditions 
hold and that
    first branch won: Hive was handed a path outside the table root. Its 
vectorized reader
    resolves partition values by looking the split path up in 
pathToPartitionInfo, which only
    holds the table's own partition directories, so the query failed with
    "cannot find dir = <external path> in pathToPartitionInfo".
    
    Test the external-column condition first, so a projection of no columns 
reads the skeleton.
    Bootstrap keeps a one-to-one row correspondence between the two files, so 
the count is
    unchanged, and the skeleton is inside the table root and much smaller.
    
    Scope, after review:
    
      - This fixes the no-projection shape only. Projecting data columns still 
opens the
        external split, and so does the stitch branch, so a partitioned 
bootstrap table still
        fails for those - filed as #19643 with the
        "set hive.vectorized.execution.enabled=false" workaround.
      - It is not Hive-3 specific. hive-exec 2.3.10 ships the same vectorized 
reader; what
        differs is the default of hive.vectorized.execution.enabled, false in 
hive-common 2.3.10
        and true in 3.1.3 (javap on HiveConf$ConfVars: iconst_0 vs iconst_1). 
Hive 2 with
        vectorization enabled hits it too.
      - MOR is reached only for a bootstrap file slice with no log files. When a
        HoodieRealtimeBootstrapBaseFileSplit has delta logs, 
addVirtualKeysProjection injects the
        meta columns through the 3-arg addProjectionField, which does not 
consult LIST_COLUMNS
        and so always succeeds, leaving this branch unreachable.
    
    The two booleans are now derived from the projected column names alone. The 
previous code
    zipped getReadColumnIDs with getReadColumnNames, which is the pairing
    HoodieColumnProjectionUtils warns about in getReadColumnIDs itself: the id 
list is
    de-duplicated and parsed with Integer.parseInt while the name list is 
neither, so a blank id
    (the HIVE-22438 shape) threw NumberFormatException and a duplicated name 
misaligned the zip.
    The Integer half was never read.
    
    Tests live in TestHoodieParquetInputFormat rather than a new class. The one 
that matters goes
    through getRecordReader: a bootstrap split whose skeleton holds 3 rows and 
whose external file
    holds 7, read with nothing projected, must yield 3. It yields 7 without the 
fix. The remaining
    three combinations are a parameterised truth table over the two booleans.
    
    Closes #15676
---
 .../hudi/hadoop/HoodieParquetInputFormat.java      |  39 ++++++-
 .../hudi/hadoop/TestHoodieParquetInputFormat.java  | 127 +++++++++++++++++++++
 2 files changed, 162 insertions(+), 4 deletions(-)

diff --git 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HoodieParquetInputFormat.java
 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HoodieParquetInputFormat.java
index 6c10945fe39b..6a0a07976e8d 100644
--- 
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HoodieParquetInputFormat.java
+++ 
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/HoodieParquetInputFormat.java
@@ -22,6 +22,7 @@ import org.apache.hudi.common.model.HoodieRecord;
 import org.apache.hudi.common.schema.internal.InternalSchema;
 import org.apache.hudi.common.util.HoodieStorageUtils;
 import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.VisibleForTesting;
 import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.hadoop.avro.HoodieTimestampAwareParquetInputFormat;
@@ -210,10 +211,10 @@ public class HoodieParquetInputFormat extends 
HoodieParquetInputFormatBase {
 
     LOG.info("colNameWithTypes ={}, Num Entries ={}", colNameWithTypes, 
colNameWithTypes.size());
 
-    if (hoodieColsProjected.isEmpty()) {
-      return getRecordReaderInternal(eSplit.getBootstrapFileSplit(), job, 
reporter);
-    } else if (externalColsProjected.isEmpty()) {
-      return getRecordReaderInternal(split, job, reporter);
+    Option<FileSplit> singleSplit = resolveSingleFileSplit(eSplit, 
!hoodieColsProjected.isEmpty(),
+        !externalColsProjected.isEmpty());
+    if (singleSplit.isPresent()) {
+      return getRecordReaderInternal(singleSplit.get(), job, reporter);
     } else {
       FileSplit rightSplit = eSplit.getBootstrapFileSplit();
       // Hive PPD works at row-group level and only enabled when 
hive.optimize.index.filter=true;
@@ -233,4 +234,34 @@ public class HoodieParquetInputFormat extends 
HoodieParquetInputFormatBase {
           true);
     }
   }
+
+  /**
+   * The single file backing this read, or empty when both files are needed 
and have to be stitched.
+   *
+   * <p>A bootstrap split carries two paths: the split itself is the skeleton 
file, which lives inside the
+   * table root, and {@code getBootstrapFileSplit()} is the external source 
file, which does not.
+   *
+   * <p>The two "only one file is needed" cases both apply when a query 
projects no columns at all, as
+   * {@code SELECT COUNT(*)} does, so the order they are tested in decides 
which file is read. Prefer the
+   * skeleton: it is inside the table root, and bootstrap keeps a one-to-one 
row correspondence with the
+   * external file, so a count over it is identical. Handing Hive a path 
outside the table root breaks its
+   * vectorized reader, which derives partition values by looking the split 
path up in
+   * {@code pathToPartitionInfo} (HUDI-5526).
+   *
+   * @param split                  the bootstrap split.
+   * @param anyHoodieColProjected  whether the query projects any Hudi meta 
column.
+   * @param anyExternalColProjected whether the query projects any column from 
the external file.
+   */
+  @VisibleForTesting
+  static Option<FileSplit> resolveSingleFileSplit(BootstrapBaseFileSplit split,
+                                                  boolean 
anyHoodieColProjected,
+                                                  boolean 
anyExternalColProjected) {
+    if (!anyExternalColProjected) {
+      return Option.of(split);
+    } else if (!anyHoodieColProjected) {
+      return Option.of(split.getBootstrapFileSplit());
+    } else {
+      return Option.empty();
+    }
+  }
 }
\ No newline at end of file
diff --git 
a/hudi-hadoop-mr/src/test/java/org/apache/hudi/hadoop/TestHoodieParquetInputFormat.java
 
b/hudi-hadoop-mr/src/test/java/org/apache/hudi/hadoop/TestHoodieParquetInputFormat.java
index fe84fa8c8c56..a7782382835b 100644
--- 
a/hudi-hadoop-mr/src/test/java/org/apache/hudi/hadoop/TestHoodieParquetInputFormat.java
+++ 
b/hudi-hadoop-mr/src/test/java/org/apache/hudi/hadoop/TestHoodieParquetInputFormat.java
@@ -52,6 +52,7 @@ import org.apache.hadoop.io.ArrayWritable;
 import org.apache.hadoop.io.LongWritable;
 import org.apache.hadoop.io.NullWritable;
 import org.apache.hadoop.mapred.FileInputFormat;
+import org.apache.hadoop.mapred.FileSplit;
 import org.apache.hadoop.mapred.InputSplit;
 import org.apache.hadoop.mapred.JobConf;
 import org.apache.hadoop.mapred.RecordReader;
@@ -62,10 +63,14 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.sql.Timestamp;
 import java.text.SimpleDateFormat;
@@ -77,6 +82,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.stream.Stream;
 
 import static 
org.apache.hudi.common.testutils.HoodieTestUtils.COMMIT_METADATA_SER_DE;
 import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_FILE_NAME_GENERATOR;
@@ -86,6 +92,7 @@ import static 
org.apache.hudi.common.testutils.SchemaTestUtil.getSchemaFromResou
 import static 
org.apache.hudi.hadoop.HoodieColumnProjectionUtils.READ_COLUMN_NAMES_CONF_STR;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -837,4 +844,124 @@ public class TestHoodieParquetInputFormat {
       jobConf.set(HoodieReaderConfig.FILE_GROUP_READER_ENABLED.key(), "true");
     }
   }
+
+  /**
+   * A bootstrap split carries two files: the split's own path is the 
skeleton, inside the table root, and
+   * {@code getBootstrapFileSplit()} is the external source file, which is 
not. A query projecting no columns
+   * at all - {@code SELECT COUNT(*)} - satisfies both "only one file is 
needed" conditions at once, so the
+   * order they are tested in decides which file Hive is handed.
+   *
+   * <p>Handing Hive a path outside the table root breaks its vectorized 
reader, which derives partition
+   * values by looking the split path up in {@code pathToPartitionInfo} 
(HUDI-5526, #15676). Hive 2.3 ships
+   * the same reader but defaults {@code hive.vectorized.execution.enabled} to 
false where Hive 3 defaults it
+   * to true, so this is gated by that config rather than by the Hive version.
+   *
+   * <p>Only the no-projection case is new behaviour: TestBootstrap and 
TestOrcBootstrap drive the other
+   * three branches end to end, they have just been disabled (HUDI-7353) since 
#10551.
+   */
+  @Test
+  public void testCountStarReadsSkeletonSoSplitPathStaysInsideTable() throws 
IOException {
+    BootstrapBaseFileSplit split = bootstrapSplit();
+
+    Option<FileSplit> resolved = 
HoodieParquetInputFormat.resolveSingleFileSplit(split, false, false);
+
+    assertTrue(resolved.isPresent(), "a query projecting no columns must 
resolve to a single file");
+    assertSame(split, resolved.get(),
+        "it must be the skeleton, whose path is inside the table root");
+  }
+
+  /**
+   * The remaining three combinations, which behave the same before and after 
the reorder: only meta columns
+   * needs the skeleton, only data columns needs the external file, and both 
needs them stitched.
+   */
+  @ParameterizedTest
+  @MethodSource("singleFileSplitCases")
+  public void testSingleFileSplitSelection(boolean anyHoodieCol, boolean 
anyExternalCol,
+                                           String expected) throws IOException 
{
+    BootstrapBaseFileSplit split = bootstrapSplit();
+
+    Option<FileSplit> resolved =
+        HoodieParquetInputFormat.resolveSingleFileSplit(split, anyHoodieCol, 
anyExternalCol);
+
+    if ("stitch".equals(expected)) {
+      assertFalse(resolved.isPresent(), "both files are needed, so the caller 
must stitch them");
+    } else {
+      assertTrue(resolved.isPresent(), "a single file should have been 
resolved");
+      assertSame("skeleton".equals(expected) ? split : 
split.getBootstrapFileSplit(), resolved.get(),
+          "wrong file chosen for (anyHoodieCol=" + anyHoodieCol + ", 
anyExternalCol=" + anyExternalCol + ")");
+    }
+  }
+
+  private static Stream<Arguments> singleFileSplitCases() {
+    return Stream.of(
+        Arguments.of(true, false, "skeleton"),
+        Arguments.of(false, true, "external"),
+        Arguments.of(true, true, "stitch"));
+  }
+
+  private static BootstrapBaseFileSplit bootstrapSplit() throws IOException {
+    return new BootstrapBaseFileSplit(
+        new FileSplit(new Path("/tbl/event_type=two/skeleton.parquet"), 0, 
100, (String[]) null),
+        new FileSplit(new Path("/src/event_type=two/part-0.parquet"), 0, 100, 
(String[]) null));
+  }
+
+  /**
+   * The end-to-end shape of HUDI-5526: a bootstrap split whose skeleton and 
external file hold a different
+   * number of rows, read through {@link 
HoodieParquetInputFormat#getRecordReader} with nothing projected.
+   * The reader must yield the skeleton's row count. On master it yields the 
external file's.
+   *
+   * <p>No Hudi table is needed: {@code shouldUseFilegroupReader} excludes 
{@code BootstrapBaseFileSplit}, so
+   * this falls straight through to {@code createBootstrappingRecordReader}.
+   */
+  @Test
+  public void testNoProjectionReaderReadsSkeletonRowCount() throws Exception {
+    HoodieSchema schema = SchemaTestUtil.getSchemaFromResource(getClass(), 
"/test_timetype.avsc");
+    java.nio.file.Path skeletonFile = basePath.resolve("skeleton.parquet");
+    java.nio.file.Path externalFile = basePath.resolve("external.parquet");
+    int skeletonRows = 3;
+    int externalRows = 7;
+    writeParquet(skeletonFile, schema, skeletonRows);
+    writeParquet(externalFile, schema, externalRows);
+
+    jobConf.set(HoodieReaderConfig.FILE_GROUP_READER_ENABLED.key(), "false");
+    jobConf.set(IOConstants.COLUMNS, 
"test_timestamp,test_long,test_date,_hoodie_commit_time,_hoodie_commit_seqno");
+    jobConf.set(IOConstants.COLUMNS_TYPES, 
"timestamp,bigint,date,string,string");
+    // SELECT COUNT(*): Hive projects no columns at all.
+    jobConf.set(READ_COLUMN_NAMES_CONF_STR, "");
+    jobConf.set(HoodieColumnProjectionUtils.READ_COLUMN_IDS_CONF_STR, "");
+
+    BootstrapBaseFileSplit split = new BootstrapBaseFileSplit(
+        new FileSplit(new Path(skeletonFile.toString()), 0, 
Files.size(skeletonFile), (String[]) null),
+        new FileSplit(new Path(externalFile.toString()), 0, 
Files.size(externalFile), (String[]) null));
+
+    RecordReader<NullWritable, ArrayWritable> reader = 
inputFormat.getRecordReader(split, jobConf, null);
+    try {
+      NullWritable key = reader.createKey();
+      ArrayWritable value = reader.createValue();
+      int rows = 0;
+      while (reader.next(key, value)) {
+        rows++;
+      }
+      assertEquals(skeletonRows, rows,
+          "a no-projection read must come from the skeleton file, whose path 
is inside the table root");
+    } finally {
+      reader.close();
+    }
+  }
+
+  private static void writeParquet(java.nio.file.Path file, HoodieSchema 
schema, int numRows) throws IOException {
+    try (AvroParquetWriter parquetWriter =
+             new AvroParquetWriter(new Path(file.toString()), 
schema.toAvroSchema())) {
+      for (int i = 0; i < numRows; i++) {
+        GenericData.Record record = new 
GenericData.Record(schema.toAvroSchema());
+        record.put("test_timestamp", (long) i);
+        record.put("test_long", (long) i);
+        record.put("test_date", i);
+        record.put("_hoodie_commit_time", "20160628071126");
+        record.put("_hoodie_commit_seqno", "20160628071126_" + i);
+        parquetWriter.write(record);
+      }
+    }
+  }
+
 }

Reply via email to