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


##########
hudi-common/src/main/java/org/apache/hudi/common/schema/internal/utils/AvroSchemaEvolutionUtils.java:
##########
@@ -308,21 +308,26 @@ public static SchemaCompatibilityException 
timestampPrecisionChangeError(String
    * and target is long and since int can be promoted to long), colC will be 
long data type in output schema.
    *
    *
+   * <p>Precedence when a side is degenerate (null, the NULL type, or no 
fields): an absent source resolves to

Review Comment:
   🤖 nit: this paragraph is a bit hard to follow for a reader who just wants 
the contract. Could you trim it to the outcome ("an absent/empty source 
resolves to target, checked before target") and move the "keep this check 
first" rationale to a short inline comment on the source-null branch, where 
someone reordering the code will actually see it?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-utilities/src/test/java/org/apache/hudi/utilities/streamer/TestHoodieIncrSourceE2E.java:
##########
@@ -520,4 +528,85 @@ public void testTargetCheckpointV2ForS3Gcs() {
     assertFalse(StreamerCheckpointUtils.shouldTargetCheckpointV2(8, 
GcsEventsHoodieIncrSource.class.getName()));
     assertFalse(StreamerCheckpointUtils.shouldTargetCheckpointV2(6, 
GcsEventsHoodieIncrSource.class.getName()));
   }
-}
\ No newline at end of file
+
+  /**
+   * An empty batch from a source whose schema provider reports no schema at 
all must still land an empty
+   * commit when the table's latest commit carries a schema without fields 
(what a sync over column-less
+   * input leaves behind), so the checkpoint keeps advancing. With an 
unchanged checkpoint the empty commit
+   * is gated by allowCommitOnNoCheckpointChange, as for any other source. The 
non-empty batch that follows
+   * must then evolve the field-less table schema and land its rows.
+   */
+  @ParameterizedTest
+  @CsvSource({
+      "6, 80, false, true",
+      "8, 80, false, true",
+      "8, 70, false, false",
+      "8, 70, true, true"})
+  void testSyncE2EEmptyBatchWithAbsentSchemaAndFieldlessTableSchema(
+      String tableVersion, String returnedCheckpoint, boolean 
allowCommitOnNoCheckpointChange, boolean expectNewCommit) throws Exception {
+    metaClient = getHoodieMetaClientWithTableVersion(storageConf(), 
basePath(), tableVersion);
+    Schema fieldlessSchema = Schema.createRecord("hoodie_trips_record", null, 
"hoodie.hoodie_trips", false, Collections.emptyList());
+    HoodieCommitMetadata seed = new HoodieCommitMetadata();
+    seed.setOperationType(WriteOperationType.INSERT);
+    seed.addMetadata(HoodieCommitMetadata.SCHEMA_KEY, 
fieldlessSchema.toString());
+    seed.addMetadata(STREAMER_CHECKPOINT_KEY_V1, "70");
+    
HoodieTestTable.of(metaClient).addCommit(metaClient.createNewInstantTime(false),
 Option.of(seed));
+
+    TypedProperties props = setupBaseProperties(tableVersion);
+    props.put(OP_FETCH_NEXT_BATCH, OP_EMPTY_ROW_SET_NONE_NULL_CKP_V1_KEY);
+    props.put(RETURN_CHECKPOINT_KEY, returnedCheckpoint);
+    props.put(VAL_INPUT_CKP, VAL_NON_EMPTY_CKP_ALL_MEMBERS);
+    props.put(VAL_CKP_KEY_EQ_VAL, "70");
+    props.put(HoodieCommonConfig.SET_NULL_FOR_MISSING_COLUMNS.key(), "true");
+    props.put("hoodie.datasource.write.recordkey.field", "_row_key");
+    props.put("hoodie.datasource.write.partitionpath.field", "partition_path");
+
+    HoodieDeltaStreamer.Config cfg = createConfig(basePath(), null);
+    cfg.operation = WriteOperationType.UPSERT;
+    // an empty batch never reaches the transformer, but its presence selects 
the row-based path in StreamSync
+    cfg.transformerClassNames = 
Collections.singletonList(FlatteningTransformer.class.getName());
+    cfg.schemaProviderClassName = AbsentSchemaProvider.class.getName();
+    cfg.allowCommitOnNoCheckpointChange = allowCommitOnNoCheckpointChange;
+    new HoodieDeltaStreamer(cfg, jsc, Option.of(props)).sync();
+
+    metaClient.reloadActiveTimeline();
+    HoodieTimeline commits = 
metaClient.getActiveTimeline().getCommitsTimeline().filterCompletedInstants();
+    assertEquals(expectNewCommit ? 2 : 1, commits.countInstants());
+    HoodieCommitMetadata last = 
HoodieClientTestUtils.getCommitMetadataForInstant(metaClient, 
commits.lastInstant().get()).get();
+    assertEquals(returnedCheckpoint, 
last.getMetadata(STREAMER_CHECKPOINT_KEY_V1));
+    assertEquals(0, last.getWriteStats().size());
+    assertEquals(0, new 
Schema.Parser().parse(last.getMetadata(HoodieCommitMetadata.SCHEMA_KEY)).getFields().size());
+
+    props.put(MOCK_ROWS_JSON, 
"{\"_row_key\":\"k1\",\"partition_path\":\"p1\",\"timestamp\":1,\"name\":\"a\"}\n"
+        + 
"{\"_row_key\":\"k2\",\"partition_path\":\"p1\",\"timestamp\":2,\"name\":\"b\"}");
+    props.put(RETURN_CHECKPOINT_KEY, "90");
+    props.put(VAL_CKP_KEY_EQ_VAL, returnedCheckpoint);
+    new HoodieDeltaStreamer(cfg, jsc, Option.of(props)).sync();
+
+    metaClient.reloadActiveTimeline();
+    commits = 
metaClient.getActiveTimeline().getCommitsTimeline().filterCompletedInstants();
+    assertEquals(expectNewCommit ? 3 : 2, commits.countInstants());
+    last = HoodieClientTestUtils.getCommitMetadataForInstant(metaClient, 
commits.lastInstant().get()).get();
+    assertEquals("90", last.getMetadata(STREAMER_CHECKPOINT_KEY_V1));
+    assertEquals(2, last.fetchTotalRecordsWritten());
+    Schema evolved = new 
Schema.Parser().parse(last.getMetadata(HoodieCommitMetadata.SCHEMA_KEY));
+    for (String column : new String[] {"_row_key", "partition_path", 
"timestamp", "name"}) {

Review Comment:
   🤖 nit: `assertNotNull(evolved.getField(column), column)` reads more directly 
than `assertFalse(... == null, column)`.
   
   <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]

Reply via email to