voonhous commented on code in PR #19403:
URL: https://github.com/apache/hudi/pull/19403#discussion_r3689099035


##########
hudi-spark-datasource/hudi-spark4-common/src/test/java/org/apache/hudi/variant/TestSpark4VariantShreddingProvider.java:
##########
@@ -87,11 +174,68 @@ void booleanRoundTrips() throws Exception {
     assertScalarRoundTrips("true", 
HoodieSchema.create(HoodieSchemaType.BOOLEAN));
   }
 
+  @Test
+  void binaryShredsToByteBuffer() {
+    byte[] payload = "not-utf8-�ÿ".getBytes(StandardCharsets.ISO_8859_1);
+    assertScalarShredsTo(scalar(b -> b.appendBinary(payload)),
+        HoodieSchema.create(HoodieSchemaType.BYTES), ByteBuffer.wrap(payload));
+  }
+
+  @Test
+  void uuidShredsToString() {
+    UUID uuid = UUID.fromString("12345678-1234-1234-1234-123456789abc");
+    assertScalarShredsTo(scalar(b -> b.appendUuid(uuid)), 
HoodieSchema.createUUID(), uuid.toString());
+  }
+
+  @Test
+  void dateShredsToDaysSinceEpoch() {
+    assertScalarShredsTo(scalar(b -> b.appendDate(19000)), 
HoodieSchema.createDate(), 19000);
+  }
+
+  @Test
+  void timestampMicrosShredsToMicros() {
+    long micros = 1_700_000_000_000_000L;
+    assertScalarShredsTo(scalar(b -> b.appendTimestamp(micros)), 
HoodieSchema.createTimestampMicros(), micros);
+  }
+
+  @Test
+  void localTimestampMicrosShredsToMicros() {
+    long micros = 1_700_000_000_000_000L;
+    assertScalarShredsTo(scalar(b -> b.appendTimestampNtz(micros)), 
HoodieSchema.createLocalTimestampMicros(), micros);
+  }
+
   @Test
   void decimalRoundTrips() throws Exception {
     assertScalarRoundTrips("123.45", HoodieSchema.createDecimal(10, 2));
   }
 
+  // 
---------------------------------------------------------------------------
+  // "Decline to shred" fallbacks: value stays in the residual binary.
+  // 
---------------------------------------------------------------------------
+
+  @Test
+  void millisTimestampIsNotShreddedIntoMicrosLeaf() {
+    // A millisecond-precision typed_value cannot represent a micros variant 
timestamp, so
+    // avroTypeToScalarType returns null and the value is left unshredded in 
the residual.
+    assertStaysInResidual(scalar(b -> 
b.appendTimestamp(1_700_000_000_000_000L)),
+        HoodieSchema.createTimestampMillis());
+    assertStaysInResidual(scalar(b -> 
b.appendTimestampNtz(1_700_000_000_000_000L)),
+        HoodieSchema.createLocalTimestampMillis());
+  }
+
+  @Test
+  void fixedLeafShredsBinaryToByteBuffer() {

Review Comment:
   Addressed in caba485a3f5b: dropped `fixedLeafShredsBinaryToByteBuffer` 
rather than pin the broken behavior (inverting it would fail until the provider 
is fixed). FIXED handling tracked in #19442.



##########
hudi-spark-datasource/hudi-spark4-common/src/test/java/org/apache/hudi/variant/TestSpark4VariantShreddingProvider.java:
##########
@@ -22,61 +22,148 @@
 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.exception.HoodieException;
 
+import org.apache.avro.Conversions;
 import org.apache.avro.Schema;
 import org.apache.avro.generic.GenericData;
 import org.apache.avro.generic.GenericRecord;
 import org.apache.spark.types.variant.Variant;
 import org.apache.spark.types.variant.VariantBuilder;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.time.ZoneOffset;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.UUID;
 
+import static 
org.apache.hudi.common.schema.HoodieSchema.Variant.VARIANT_METADATA_FIELD;
+import static 
org.apache.hudi.common.schema.HoodieSchema.Variant.VARIANT_TYPED_VALUE_FIELD;
+import static 
org.apache.hudi.common.schema.HoodieSchema.Variant.VARIANT_VALUE_FIELD;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
- * Round-trip coverage for {@link Spark4VariantShreddingProvider}: shred an 
unshredded variant, then
- * reconstruct it, and assert it round-trips. This exercises {@code 
rebuildVariantRecord} and the
- * {@code AvroVariantRow}/{@code AvroObjectRow}/{@code AvroArrayRow} accessors 
across scalar, object,
- * and array shapes - the AVRO read-path reconstruction (#18931) that the 
Spark MOR SQL test cannot
- * reach (Spark compaction reads base files via the InternalRow reader, not 
HoodieAvroParquetReader).
+ * Round-trip and behavior-pinning coverage for {@link 
Spark4VariantShreddingProvider}: shred an
+ * unshredded variant, then reconstruct it, asserting both the intermediate 
shredded schema/values and
+ * the reconstructed variant. This exercises {@code shredVariantRecord}, 
{@code rebuildVariantRecord},
+ * {@code avroTypeToScalarType}, {@code convertScalarToAvro}, and the
+ * {@code AvroVariantRow}/{@code AvroObjectRow}/{@code AvroArrayRow} accessors 
across every scalar leaf
+ * type, object/array shapes, partial (residual) shredding, and the null/error 
guards - the AVRO
+ * read-path reconstruction that the Spark MOR SQL test cannot reach (Spark 
compaction reads base
+ * files via the InternalRow reader, not HoodieAvroParquetReader).
  */
 class TestSpark4VariantShreddingProvider {
 
   private final Spark4VariantShreddingProvider provider = new 
Spark4VariantShreddingProvider();
   private final Schema unshreddedSchema = 
HoodieSchema.createVariant().getAvroSchema();
 
-  /** Parse json to a variant, shred it to {@code shredded}, rebuild it, 
assert the json round-trips. */
-  private void assertRoundTrips(String json, HoodieSchema.Variant shredded) 
throws Exception {
-    Variant variant = VariantBuilder.parseJson(json, false);
-    GenericRecord unshreddedRecord = new GenericData.Record(unshreddedSchema);
-    unshreddedRecord.put(HoodieSchema.Variant.VARIANT_METADATA_FIELD, 
ByteBuffer.wrap(variant.getMetadata()));
-    unshreddedRecord.put(HoodieSchema.Variant.VARIANT_VALUE_FIELD, 
ByteBuffer.wrap(variant.getValue()));
+  /** Wrap a fully built {@link Variant} into the unshredded {metadata, value} 
Avro record. */
+  private GenericRecord unshredded(Variant variant) {
+    GenericRecord record = new GenericData.Record(unshreddedSchema);
+    record.put(VARIANT_METADATA_FIELD, ByteBuffer.wrap(variant.getMetadata()));
+    record.put(VARIANT_VALUE_FIELD, ByteBuffer.wrap(variant.getValue()));
+    return record;
+  }
+
+  private GenericRecord shred(Variant variant, HoodieSchema.Variant shredded) {
+    return provider.shredVariantRecord(unshredded(variant), 
shredded.getAvroSchema(), shredded);
+  }
 
-    Schema shreddedSchema = shredded.getAvroSchema();
-    GenericRecord shreddedRecord = 
provider.shredVariantRecord(unshreddedRecord, shreddedSchema, shredded);
-    GenericRecord rebuilt = provider.rebuildVariantRecord(shreddedRecord, 
shreddedSchema, unshreddedSchema);
+  private Variant rebuild(GenericRecord shreddedRecord, HoodieSchema.Variant 
shredded) {
+    GenericRecord rebuilt =
+        provider.rebuildVariantRecord(shreddedRecord, 
shredded.getAvroSchema(), unshreddedSchema);
+    return new Variant(toBytes(rebuilt.get(VARIANT_VALUE_FIELD)), 
toBytes(rebuilt.get(VARIANT_METADATA_FIELD)));
+  }
 
-    Variant rebuiltVariant = new Variant(
-        toBytes(rebuilt.get(HoodieSchema.Variant.VARIANT_VALUE_FIELD)),
-        toBytes(rebuilt.get(HoodieSchema.Variant.VARIANT_METADATA_FIELD)));
-    assertEquals(variant.toJson(ZoneOffset.UTC), 
rebuiltVariant.toJson(ZoneOffset.UTC),
-        "variant did not round-trip through shred/rebuild for: " + json);
+  private void assertRoundTrips(Variant variant, HoodieSchema.Variant 
shredded) {
+    Variant rebuilt = rebuild(shred(variant, shredded), shredded);
+    assertEquals(variant.toJson(ZoneOffset.UTC), 
rebuilt.toJson(ZoneOffset.UTC),
+        "variant did not round-trip through shred/rebuild");
+  }
+
+  /** Parse json to a variant, shred it, rebuild it, assert the json 
round-trips. */
+  private void assertRoundTrips(String json, HoodieSchema.Variant shredded) 
throws Exception {
+    assertRoundTrips(VariantBuilder.parseJson(json, false), shredded);
   }
 
   private void assertScalarRoundTrips(String json, HoodieSchema typedValue) 
throws Exception {
     assertRoundTrips(json, HoodieSchema.createVariantShredded(typedValue));
   }
 
+  /**
+   * Shred {@code variant} into a scalar {@code typedValue} schema, assert the 
value fully shredded
+   * (into {@code typed_value}, exactly {@code expectedTypedValue}, no 
residual {@code value}) and
+   * round-trips back to the original variant.
+   */
+  private void assertScalarShredsTo(Variant variant, HoodieSchema typedValue, 
Object expectedTypedValue) {
+    HoodieSchema.Variant shredded = 
HoodieSchema.createVariantShredded(typedValue);

Review Comment:
   Addressed in caba485a3f5b: new `shreddedLeaf()` helper wraps every scalar 
leaf in `createNullable`; all scalar helpers and the guard tests now build that 
shape. `assertDecimalRebuildsFromEncoding` now encodes against the concrete 
decimal branch, avoiding the union NPE.



##########
hudi-spark-datasource/hudi-spark4-common/src/test/java/org/apache/hudi/variant/TestSpark4VariantShreddingProvider.java:
##########
@@ -22,61 +22,148 @@
 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.exception.HoodieException;
 
+import org.apache.avro.Conversions;
 import org.apache.avro.Schema;
 import org.apache.avro.generic.GenericData;
 import org.apache.avro.generic.GenericRecord;
 import org.apache.spark.types.variant.Variant;
 import org.apache.spark.types.variant.VariantBuilder;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.time.ZoneOffset;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.UUID;
 
+import static 
org.apache.hudi.common.schema.HoodieSchema.Variant.VARIANT_METADATA_FIELD;
+import static 
org.apache.hudi.common.schema.HoodieSchema.Variant.VARIANT_TYPED_VALUE_FIELD;
+import static 
org.apache.hudi.common.schema.HoodieSchema.Variant.VARIANT_VALUE_FIELD;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
- * Round-trip coverage for {@link Spark4VariantShreddingProvider}: shred an 
unshredded variant, then
- * reconstruct it, and assert it round-trips. This exercises {@code 
rebuildVariantRecord} and the
- * {@code AvroVariantRow}/{@code AvroObjectRow}/{@code AvroArrayRow} accessors 
across scalar, object,
- * and array shapes - the AVRO read-path reconstruction (#18931) that the 
Spark MOR SQL test cannot
- * reach (Spark compaction reads base files via the InternalRow reader, not 
HoodieAvroParquetReader).
+ * Round-trip and behavior-pinning coverage for {@link 
Spark4VariantShreddingProvider}: shred an
+ * unshredded variant, then reconstruct it, asserting both the intermediate 
shredded schema/values and
+ * the reconstructed variant. This exercises {@code shredVariantRecord}, 
{@code rebuildVariantRecord},
+ * {@code avroTypeToScalarType}, {@code convertScalarToAvro}, and the
+ * {@code AvroVariantRow}/{@code AvroObjectRow}/{@code AvroArrayRow} accessors 
across every scalar leaf
+ * type, object/array shapes, partial (residual) shredding, and the null/error 
guards - the AVRO
+ * read-path reconstruction that the Spark MOR SQL test cannot reach (Spark 
compaction reads base
+ * files via the InternalRow reader, not HoodieAvroParquetReader).
  */
 class TestSpark4VariantShreddingProvider {
 
   private final Spark4VariantShreddingProvider provider = new 
Spark4VariantShreddingProvider();
   private final Schema unshreddedSchema = 
HoodieSchema.createVariant().getAvroSchema();
 
-  /** Parse json to a variant, shred it to {@code shredded}, rebuild it, 
assert the json round-trips. */
-  private void assertRoundTrips(String json, HoodieSchema.Variant shredded) 
throws Exception {
-    Variant variant = VariantBuilder.parseJson(json, false);
-    GenericRecord unshreddedRecord = new GenericData.Record(unshreddedSchema);
-    unshreddedRecord.put(HoodieSchema.Variant.VARIANT_METADATA_FIELD, 
ByteBuffer.wrap(variant.getMetadata()));
-    unshreddedRecord.put(HoodieSchema.Variant.VARIANT_VALUE_FIELD, 
ByteBuffer.wrap(variant.getValue()));
+  /** Wrap a fully built {@link Variant} into the unshredded {metadata, value} 
Avro record. */
+  private GenericRecord unshredded(Variant variant) {
+    GenericRecord record = new GenericData.Record(unshreddedSchema);
+    record.put(VARIANT_METADATA_FIELD, ByteBuffer.wrap(variant.getMetadata()));
+    record.put(VARIANT_VALUE_FIELD, ByteBuffer.wrap(variant.getValue()));
+    return record;
+  }
+
+  private GenericRecord shred(Variant variant, HoodieSchema.Variant shredded) {

Review Comment:
   Addressed in caba485a3f5b: `shred()` now asserts 
`ConvertingGenericData.INSTANCE.validate(...)` on every shredded record. The 
functional write/read-back test through 
`HoodieAvroWriteSupport`/`HoodieAvroParquetReader` is deferred to #19442.



##########
hudi-spark-datasource/hudi-spark4-common/src/test/java/org/apache/hudi/variant/TestSpark4VariantShreddingProvider.java:
##########
@@ -87,11 +174,68 @@ void booleanRoundTrips() throws Exception {
     assertScalarRoundTrips("true", 
HoodieSchema.create(HoodieSchemaType.BOOLEAN));
   }
 
+  @Test
+  void binaryShredsToByteBuffer() {
+    byte[] payload = "not-utf8-�ÿ".getBytes(StandardCharsets.ISO_8859_1);
+    assertScalarShredsTo(scalar(b -> b.appendBinary(payload)),
+        HoodieSchema.create(HoodieSchemaType.BYTES), ByteBuffer.wrap(payload));
+  }
+
+  @Test
+  void uuidShredsToString() {
+    UUID uuid = UUID.fromString("12345678-1234-1234-1234-123456789abc");
+    assertScalarShredsTo(scalar(b -> b.appendUuid(uuid)), 
HoodieSchema.createUUID(), uuid.toString());
+  }
+
+  @Test
+  void dateShredsToDaysSinceEpoch() {
+    assertScalarShredsTo(scalar(b -> b.appendDate(19000)), 
HoodieSchema.createDate(), 19000);
+  }
+
+  @Test
+  void timestampMicrosShredsToMicros() {
+    long micros = 1_700_000_000_000_000L;
+    assertScalarShredsTo(scalar(b -> b.appendTimestamp(micros)), 
HoodieSchema.createTimestampMicros(), micros);
+  }
+
+  @Test
+  void localTimestampMicrosShredsToMicros() {
+    long micros = 1_700_000_000_000_000L;
+    assertScalarShredsTo(scalar(b -> b.appendTimestampNtz(micros)), 
HoodieSchema.createLocalTimestampMicros(), micros);
+  }
+
   @Test
   void decimalRoundTrips() throws Exception {

Review Comment:
   Addressed in caba485a3f5b: added `numericScaleChangesShredWhenExact` (long 5 
-> `5.00`, decimal `5.00` -> `5L`) and `lossyDecimalIsNotShredded` (`123.456` 
into `decimal(10,2)` stays in the residual).



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