hudi-agent commented on code in PR #18949:
URL: https://github.com/apache/hudi/pull/18949#discussion_r3867697924
##########
hudi-utilities/src/test/java/org/apache/hudi/utilities/sources/TestS3EventsHoodieIncrSource.java:
##########
@@ -335,16 +346,87 @@ public void testSplitSnapshotLoad(String
snapshotCheckPoint, String exptected1,
// Verify the partitions being passed in getCloudObjectDataDF are correct.
ArgumentCaptor<Integer> argumentCaptor =
ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<Integer> argumentCaptorForMetrics =
ArgumentCaptor.forClass(Integer.class);
- verify(mockCloudObjectsSelectorCommon,
atLeastOnce()).loadAsDataset(Mockito.any(), Mockito.any(), Mockito.any(),
Mockito.eq(schemaProvider), argumentCaptor.capture());
+ verify(mockCloudObjectsSelectorCommon, atLeastOnce()).loadAsDataset(any(),
any(), any(), eq(schemaProvider), argumentCaptor.capture());
verify(metrics,
atLeastOnce()).updateStreamerSourceParallelism(argumentCaptorForMetrics.capture());
List<Integer> numPartitions;
if (snapshotCheckPoint.equals("1") || snapshotCheckPoint.equals("2")) {
numPartitions = Arrays.asList(12, 3, sourcePartitions);
} else {
numPartitions = Arrays.asList(23, sourcePartitions);
}
- Assertions.assertEquals(numPartitions, argumentCaptor.getAllValues());
- Assertions.assertEquals(numPartitions,
argumentCaptorForMetrics.getAllValues());
+ assertEquals(numPartitions, argumentCaptor.getAllValues());
+ assertEquals(numPartitions, argumentCaptorForMetrics.getAllValues());
+ }
+
+ /**
+ * Resume from `commit#fileKey` must re-include the start commit; runs on v6
and v8, COW and MOR
+ * source meta-tables since cloud event sources always use V1/requested-time
regardless of version.
+ */
+ @ParameterizedTest
+ @CsvSource({"6,COPY_ON_WRITE", "8,COPY_ON_WRITE", "6,MERGE_ON_READ",
"8,MERGE_ON_READ"})
+ void testRealQueryRunnerResumesMidCommitPagination(String
sourceTableVersion, HoodieTableType tableType) throws IOException {
+ Properties tableProps = new Properties();
+ tableProps.put(HoodieTableConfig.POPULATE_META_FIELDS.key(),
String.valueOf(true));
+ tableProps.put("hoodie.datasource.write.recordkey.field", "_row_key");
+ tableProps.put("hoodie.datasource.write.partitionpath.field", "");
+ tableProps.put(HoodieTableConfig.RECORDKEY_FIELDS.key(), "_row_key");
+ tableProps.put(HoodieTableConfig.PARTITION_FIELDS.key(), "");
+ tableProps.put(WRITE_TABLE_VERSION.key(), sourceTableVersion);
+ metaClient = getHoodieMetaClient(storageConf(), basePath(), tableProps,
tableType);
+
+ // timestamp-format instants: the incremental read normalizes
START_COMMIT/END_COMMIT
+ // through HoodieSqlCommonUtils.formatIncrementalInstant, which rejects
other formats
+ String startCommit = "20260601000001";
+ String laterCommit = "20260601000002";
+ writeS3MetadataRecords(startCommit, Arrays.asList(
+ Pair.of("path/to/file-01.json", 100L),
+ Pair.of("path/to/file-02.json", 100L),
+ Pair.of("path/to/file-03.json", 100L),
+ Pair.of("path/to/file-04.json", 100L),
+ Pair.of("path/to/file-05.json", 100L)));
+ // the second commit re-writes an existing key (an S3 re-upload), landing
in a log file on MOR
+ writeS3MetadataRecords(laterCommit,
Arrays.asList(Pair.of("path/to/file-05.json", 100L)));
+ if (tableType == HoodieTableType.MERGE_ON_READ) {
+ boolean hasLogFiles = Arrays.stream(fs().listStatus(new
Path(basePath())))
+ .anyMatch(f -> f.getPath().getName().contains(".log."));
+ assertTrue(hasLogFiles, "Expected log files in the MOR source
meta-table");
+ }
+
+ TypedProperties props = setProps(READ_UPTO_LATEST_COMMIT);
+ props.setProperty(CloudSourceConfig.ENABLE_EXISTS_CHECK.key(), "false");
+ when(mockCloudObjectsSelectorCommon.loadAsDataset(
+ any(), any(), any(), eq(schemaProvider), anyInt()))
+ .thenReturn(Option.empty());
+ when(sourceProfileSupplier.getSourceProfile()).thenReturn(null);
+
+ // Real QueryRunner so the actual Spark incremental read against the
on-disk meta-table runs.
+ S3EventsHoodieIncrSource incrSource = new S3EventsHoodieIncrSource(
+ props, jsc(), spark(),
+ new QueryRunner(spark(), props),
+ new CloudDataFetcher(props, jsc(), spark(), metrics,
mockCloudObjectsSelectorCommon),
+ new DefaultStreamContext(schemaProvider.orElse(null),
Option.of(sourceProfileSupplier)));
+
+ // Resume mid-commit at file-02; sourceLimit=250B fits file-03+file-04,
file-05 would exceed.
+ Checkpoint resumeFrom = new StreamerCheckpointV1(startCommit +
"#path/to/file-02.json");
+ Pair<Option<Dataset<Row>>, Checkpoint> result =
incrSource.fetchNextBatch(Option.of(resumeFrom), 250L);
+
+ assertEquals(
+ new StreamerCheckpointV1(startCommit + "#path/to/file-04.json"),
+ result.getRight(),
+ "Next batch must continue within the start commit, not advance to a
bare instant.");
+
+ // Filter must pass exactly file-03 and file-04 to downstream loading.
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<List<CloudObjectMetadata>> captor =
ArgumentCaptor.forClass((Class) List.class);
+ verify(mockCloudObjectsSelectorCommon).loadAsDataset(
+ any(), captor.capture(), any(), eq(schemaProvider), anyInt());
+ List<String> selectedPaths = captor.getValue().stream()
+ .map(CloudObjectMetadata::getPath)
+ .sorted()
+ .collect(java.util.stream.Collectors.toList());
+ assertEquals(2, selectedPaths.size(), "Expected file-03 and file-04, got:
" + selectedPaths);
+ assertTrue(selectedPaths.get(0).endsWith("/path/to/file-03.json"),
selectedPaths.get(0));
+ assertTrue(selectedPaths.get(1).endsWith("/path/to/file-04.json"),
selectedPaths.get(1));
}
Review Comment:
🤖 nit: `java.util.stream.Collectors.toList()` is fully-qualified here while
the GCS counterpart imports `Collectors` and uses the short form — could you
add the import and align with the GCS test style?
<sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag
quality.</i></sub>
##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/HoodieFileGroupReaderBasedFileFormat.scala:
##########
@@ -590,6 +600,18 @@ class HoodieFileGroupReaderBasedFileFormat(tablePath:
String,
*/
private def isNestedPartitionField(name: String): Boolean =
name.contains(".")
+ /**
+ * Projects to `to` only when the read schema was augmented with filter-only
columns;
Review Comment:
🤖 nit: `projectIfNeeded` compares only field *names* via `sameElements` to
decide whether to skip projection — have you considered whether a comment
noting this intentional name-only check (e.g. types are guaranteed identical
when augmenting with filter-only columns) would help a future reader who
wonders why types aren't compared?
<sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag
quality.</i></sub>
--
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]