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


##########
hudi-common/src/main/java/org/apache/hudi/common/schema/HoodieSchemaUtils.java:
##########
@@ -731,11 +721,41 @@ public static HoodieSchema 
getRecordKeyPartitionPathSchema() {
     return HoodieSchema.createRecord("HoodieRecordKey", "", "", false, 
toBeAddedFields);
   }
 
+  /**
+   * Schema of a native delete log record: the record key plus the ordering 
fields, which are
+   * always nullable (see the comment in the body).
+   */
+  public static HoodieSchema createDeleteLogSchema(HoodieSchema tableSchema, 
List<String> orderingFieldNames) {
+    // Native delete logs store only the record key plus optional ordering 
values, so ordering fields in
+    // the delete-log schema must always be nullable even when the table 
schema marks them required.
+    // A delete record such as HoodieEmptyRecord may carry 
OrderingValues.getDefault() as an in-memory
+    // sentinel rather than a real field value. Persist NULL for that missing 
value so readers can map it
+    // back to the default ordering without confusing it with a real business 
value such as 0.
+    List<HoodieSchemaField> fields = Stream.concat(
+        Stream.of(createNewSchemaField(
+            HoodieRecord.RECORD_KEY_METADATA_FIELD, 
HoodieSchema.create(HoodieSchemaType.STRING), null, null)),
+        orderingFieldNames.stream().map(orderingFieldName -> 
tableSchema.getField(orderingFieldName)

Review Comment:
   **major:** pre-existing, not introduced by this move. 
`tableSchema.getField(name)` is an exact top-level lookup 
(`HoodieSchema.java:1215`), so a nested ordering field such as 
`nested_record.level` throws `IllegalArgumentException` here on the first 
delete of a v10 native-log table: nested precombine is supported (HUDI-4051) 
and `HoodieNativeLogAppendHandle.java:93` passes the names through verbatim. 
Out of scope for a behavior-preserving PR; should the fix (writer 
`createDeleteLogFieldValues` and reader `getValue` aligned on a flat field 
name) go in a separate PR, with a note on #16639 meanwhile?



##########
hudi-common/src/test/java/org/apache/hudi/common/schema/TestHoodieSchemaUtils.java:
##########
@@ -2198,4 +2085,48 @@ public void testGetNestedFieldComplexNestedMapAndArray() 
{
     assertEquals("value", result.get().getRight().name());
     assertEquals(HoodieSchemaType.LONG, 
result.get().getRight().schema().getType());
   }
+
+  private static HoodieSchema deleteLogTableSchema() {
+    return HoodieSchema.createRecord(
+        "TestRecord",
+        null,
+        null,
+        Arrays.asList(
+            HoodieSchemaField.of("ts", 
HoodieSchema.create(HoodieSchemaType.LONG), "ordering field doc", null),
+            HoodieSchemaField.of("name", 
HoodieSchema.create(HoodieSchemaType.STRING))
+        )
+    );
+  }
+
+  @Test
+  public void testCreateDeleteLogSchema() {

Review Comment:
   **minor:** not blocking. Every caller and test passes one LONG ordering 
field, so an implementation that hardcoded LONG or kept only the first field 
would still pass; order is load-bearing since 
`HoodieNativeLogFormatWriter.java:186-199` fills `fieldValues[i + 1]` by 
position. An already-nullable `["long","null"]` field also differs: 
`HoodieSchemaField.of` drops the NULL default for a non-null-first union, so 
`defaultVal()` comes back empty. Could the helper schema gain a STRING `seq` 
and a `["long","null"]` field, with cases for `["ts","seq"]` and `emptyList()`?



##########
hudi-common/src/main/java/org/apache/hudi/common/schema/HoodieSchemaUtils.java:
##########
@@ -731,11 +721,41 @@ public static HoodieSchema 
getRecordKeyPartitionPathSchema() {
     return HoodieSchema.createRecord("HoodieRecordKey", "", "", false, 
toBeAddedFields);
   }
 
+  /**
+   * Schema of a native delete log record: the record key plus the ordering 
fields, which are
+   * always nullable (see the comment in the body).
+   */
+  public static HoodieSchema createDeleteLogSchema(HoodieSchema tableSchema, 
List<String> orderingFieldNames) {
+    // Native delete logs store only the record key plus optional ordering 
values, so ordering fields in
+    // the delete-log schema must always be nullable even when the table 
schema marks them required.
+    // A delete record such as HoodieEmptyRecord may carry 
OrderingValues.getDefault() as an in-memory
+    // sentinel rather than a real field value. Persist NULL for that missing 
value so readers can map it
+    // back to the default ordering without confusing it with a real business 
value such as 0.
+    List<HoodieSchemaField> fields = Stream.concat(
+        Stream.of(createNewSchemaField(
+            HoodieRecord.RECORD_KEY_METADATA_FIELD, 
HoodieSchema.create(HoodieSchemaType.STRING), null, null)),
+        orderingFieldNames.stream().map(orderingFieldName -> 
tableSchema.getField(orderingFieldName)
+            .map(field -> createNewSchemaField(
+                field.name(), HoodieSchema.createNullable(field.schema()), 
field.doc().orElse(null), HoodieSchema.NULL_VALUE))
+            .orElseThrow(() ->
+                new IllegalArgumentException("Ordering field " + 
orderingFieldName + " not found in table schema"))))
+        .collect(Collectors.toList());
+    return HoodieSchema.createRecord("hudi_delete_log_record", null, null, 
fields);
+  }
+
   /**
    * Fetches projected schema given list of fields to project. The field can 
be nested in format `a.b.c` where a is
-   * the top level field, b is at second level and so on.
+   * the top level field, b is at second level and so on. Field names are 
matched case-sensitively.
    * This is equivalent to {@link HoodieAvroUtils#projectSchema(Schema, List)} 
but operates on HoodieSchema.
    *
+   * <p>The two sibling projection helpers differ:</p>
+   * <ul>
+   *   <li>{@link #generateProjectionSchema(HoodieSchema, List)} - top-level 
fields only, matched
+   *       case-insensitively</li>

Review Comment:
   **nit:** feel free to ignore. This newly documents case-insensitive 
matching, and `HiveHoodieReaderContext.java:236-241` relies on it (it 
lowercases every name before calling), but no test passes a differently-cased 
name (`TestHoodieSchemaUtils.java:1037-1050` uses exact case). Could we add one 
`generateProjectionSchema(schema, Arrays.asList("_ROW_KEY"))` case asserting 
the field comes back with the schema's original casing?



##########
hudi-common/src/main/java/org/apache/hudi/common/schema/HoodieSchemaUtils.java:
##########
@@ -779,7 +799,8 @@ public static boolean hasDecimalField(HoodieSchema schema) {
    * @param schema the input schema to search
    * @return true if the schema contains a small precision decimal field and 
false otherwise
    */
-  public static boolean hasSmallPrecisionDecimalField(HoodieSchema schema) {
+  @VisibleForTesting
+  static boolean hasSmallPrecisionDecimalField(HoodieSchema schema) {

Review Comment:
   **minor:** not blocking. `hasSmallPrecisionDecimalField` and 
`isSmallPrecisionDecimalField` have had no production caller since 4d95b2c2d165 
(#13882) removed `canUseRowWriter`; only `TestHoodieSchemaUtils.java:1334-1336` 
calls them, while the sibling `hasDecimalField` is live 
(`SourceFormatAdapter.java:261`). Given this PR's dead-API scope, could we 
delete both methods and those three assertions instead of keeping them as 
test-only API?



##########
hudi-common/src/main/java/org/apache/hudi/common/avro/HoodieAvroUtils.java:
##########
@@ -221,7 +259,7 @@ public static byte[] avroToFileBytes(IndexedRecord record) {
    * @param record The GenericRecord to convert
    * @param pretty Whether to pretty-print the json output
    */
-  public static String avroToJsonString(GenericRecord record, boolean pretty) 
throws IOException {
+  private static String avroToJsonString(GenericRecord record, boolean pretty) 
throws IOException {

Review Comment:
   **nit:** feel free to ignore. Line 268 (`safeAvroToJsonString` javadoc) 
still says "Use this method over {@link HoodieAvroUtils#avroToJsonString}", 
which this commit makes private, so the alternative it names is no longer 
callable from outside the class. Could we point it at the public 
`avroToJson(GenericRecord, boolean)` (same throwing contract) or drop the 
comparison?



##########
hudi-common/src/main/java/org/apache/hudi/common/schema/LocalHoodieSchemaCache.java:
##########
@@ -46,7 +46,10 @@ private LocalHoodieSchemaCache() {
     this.schemaToVersionId = new HashMap<>();
   }
 
-  public static LocalHoodieSchemaCache getInstance() {
+  /**
+   * Returns a new, empty cache. Each caller owns its own version-id space.
+   */
+  public static LocalHoodieSchemaCache create() {

Review Comment:
   **minor:** not blocking. This class has no direct test: 
`TestLocalAvroSchemaCache` was deleted with the rename in c177e2be6b35 (#17740) 
and nothing replaced it, so the property `RecordContext.encodeSchema` relies on 
(equal-but-distinct schemas map to one version id) is unpinned; the remaining 
consumers mock the context. Since this PR already moves and renames the class, 
could we restore the two deleted tests as `TestLocalHoodieSchemaCache` in 
`common.schema`?



##########
hudi-common/src/test/java/org/apache/hudi/common/avro/TestHoodieAvroUtils.java:
##########
@@ -955,6 +1079,21 @@ void 
testGetSortColumnValuesWithPartitionPathAndRecordKey(boolean suffixRecordKe
     }
   }
 
+  @Test
+  void testGetRecordColumnValues() {

Review Comment:
   **minor:** not blocking. Lines 1084-1090 are byte-identical to 1065-1071 in 
`testGetSortColumnValuesWithPartitionPathAndRecordKey`, and this test covers a 
strict subset of it; `consistentLogicalTimestampEnabled` is inert in both 
because `EXAMPLE_SCHEMA.timestamp` is a double. The changed caller 
`HoodieTableMetadataUtil.java:331` is where nested col-stats names arrive 
(HUDI-8582). Could the two tests share a record helper, and could this one pin 
a nested `a.b` column and a missing column (null, since `returnNullIfNotFound` 
is hardcoded true)?



##########
hudi-common/src/main/java/org/apache/hudi/common/avro/AvroSchemaUtils.java:
##########
@@ -39,7 +39,14 @@
 import static org.apache.hudi.common.util.ValidationUtils.checkState;
 
 /**
- * Utils for Avro Schema.
+ * Avro-typed schema helpers, retained only as the delegate target of the call 
sites that have not moved to
+ * HoodieSchema yet: {@link 
org.apache.hudi.common.schema.HoodieSchemaUtils#asNullable(HoodieSchema)} and
+ * {@code HoodieSchemaUtils#createNullableSchema}, the field construction 
inside {@link HoodieSchema.Blob},
+ * and a handful of internal uses in {@link HoodieAvroUtils}.
+ *
+ * <p>This class is being retired under #16639. Do not add methods here: the 
HoodieSchema twin of every
+ * method on this class already exists, so use {@link 
org.apache.hudi.common.schema.HoodieSchema} or
+ * {@link org.apache.hudi.common.schema.HoodieSchemaUtils} instead.</p>

Review Comment:
   **nit:** feel free to ignore. "the HoodieSchema twin of every method on this 
class already exists" does not hold for `getNonNullTypeFromUnion`: 
`HoodieSchema#getNonNullType` is the lenient variant, and the union-unwrapping 
note this PR adds to `HoodieAvroUtils` says the strict/lenient split is 
deliberate.
   ```suggestion
    * <p>This class is being retired under #16639. Do not add methods here: 
every method on this class except
    * {@link #getNonNullTypeFromUnion(Schema)} already has a HoodieSchema twin, 
so use
    * {@link org.apache.hudi.common.schema.HoodieSchema} or {@link 
org.apache.hudi.common.schema.HoodieSchemaUtils} instead.</p>
   ```



##########
hudi-client/hudi-spark-client/src/main/scala/org/apache/spark/sql/HoodieInternalRowUtils.scala:
##########
@@ -123,7 +123,7 @@ object HoodieInternalRowUtils {
 
   /**
    * Get or create [[StructType]] for provided [[Schema]]
-   * @param schema [[Schema]] to convert to [[StructType]], NOTE: It is best 
that the schema passed in is cached through 
[[org.apache.hudi.common.avro.AvroSchemaCache]], so that we can reduce the 
overhead of schema lookup in the map
+   * @param schema [[Schema]] to convert to [[StructType]], NOTE: It is best 
that the schema passed in is cached through 
[[org.apache.hudi.common.schema.HoodieAvroSchemaCache]], so that we can reduce 
the overhead of schema lookup in the map
    * @return [[StructType]] for provided [[Schema]]

Review Comment:
   **nit:** feel free to ignore. `getCachedSchema` has taken a `HoodieSchema` 
since the migration, so `[[Schema]]` is stale on the lines this PR edits, and 
for a `HoodieSchema` the direct interner is `HoodieSchemaCache` 
(`HoodieAvroSchemaCache` is the Avro-keyed front onto it).
   ```suggestion
      * Get or create [[StructType]] for provided [[HoodieSchema]]
      * @param schema [[HoodieSchema]] to convert to [[StructType]], NOTE: It 
is best that the schema passed in is cached through 
[[org.apache.hudi.common.schema.HoodieSchemaCache]], so that we can reduce the 
overhead of schema lookup in the map
      * @return [[StructType]] for provided [[HoodieSchema]]
   ```



##########
hudi-common/src/test/java/org/apache/hudi/common/avro/TestHoodieAvroUtils.java:
##########
@@ -566,6 +567,129 @@ public void 
testConvertValueForAvroLogicalTypesCrossAvroVersion() {
     assertEquals(FIXTURE_EPOCH_MICROS, 
HoodieAvroUtils.convertValueForAvroLogicalTypes(LOCAL_TS_MICROS_SCHEMA, 
FIXTURE_LOCAL_DT_MICROS, false));
   }
 
+  @Test
+  public void testConvertValueForSpecificDataTypes_NullSchema() {
+    // Test with null schema - should return value unchanged
+    String testValue = "test_value";
+    Object result = HoodieAvroUtils.convertValueForSpecificDataTypes(null, 
testValue, false);
+    assertEquals(testValue, result);
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_NullValue_NullableSchema() {
+    // Test with null value and nullable schema - should return null
+    Schema nullableIntSchema = 
HoodieSchema.createNullable(HoodieSchema.create(HoodieSchemaType.INT)).toAvroSchema();
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(nullableIntSchema, null, 
false);
+    assertNull(result);
+  }
+
+  @Test
+  public void 
testConvertValueForSpecificDataTypes_NullValue_NonNullableSchema() {
+    // Test with null value and non-nullable schema - should throw exception
+    Schema nonNullableSchema = Schema.create(Schema.Type.STRING);
+    assertThrows(IllegalStateException.class, () ->
+        HoodieAvroUtils.convertValueForSpecificDataTypes(nonNullableSchema, 
null, false));
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_DateLogicalType() {
+    // Create date schema
+    Schema dateSchema = HoodieSchema.createDate().toAvroSchema();
+
+    // Test value: epoch days for 2023-01-01
+    int epochDays = 19358;
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(dateSchema, epochDays, false);
+    assertNotNull(result);
+    assertTrue(result instanceof LocalDate);
+    assertEquals(LocalDate.of(2023, 1, 1), result);
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_TimestampMillis_Enabled() {
+    // Create timestamp-millis schema
+    Schema timestampMillisSchema = 
HoodieSchema.createTimestampMillis().toAvroSchema();
+
+    // Test value: milliseconds for 2023-01-01 00:00:00
+    long millis = 1672560000000L;
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(timestampMillisSchema, millis, 
true);
+    assertNotNull(result);
+    assertTrue(result instanceof Timestamp);
+    assertEquals(new Timestamp(millis), result);
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_TimestampMillis_Disabled() {
+    // Create timestamp-millis schema
+    Schema timestampMillisSchema = 
HoodieSchema.createTimestampMillis().toAvroSchema();
+    long millis = 1672560000000L;
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(timestampMillisSchema, millis, 
false);
+    assertEquals(millis, result);
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_TimestampMicros_Enabled() {
+    // Create timestamp-micros schema
+    Schema timestampMicrosSchema = 
HoodieSchema.createTimestampMicros().toAvroSchema();
+
+    // Test value: microseconds for 2023-01-01 00:00:00
+    long micros = 1672560000000000L;
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(timestampMicrosSchema, micros, 
true);
+    assertNotNull(result);
+    assertTrue(result instanceof Timestamp);
+    assertEquals(new Timestamp(micros / 1000), result);
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_DecimalBytes() {
+    // Create decimal schema with precision=10, scale=2
+    Schema decimalSchema = HoodieSchema.createDecimal(10, 2).toAvroSchema();
+
+    // Create test value: 1234.56
+    BigDecimal expectedDecimal = new BigDecimal("1234.56");
+    ByteBuffer byteBuffer = 
ByteBuffer.wrap(expectedDecimal.unscaledValue().toByteArray());
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(decimalSchema, byteBuffer, 
false);
+    assertNotNull(result);
+    assertTrue(result instanceof BigDecimal);
+    assertEquals(expectedDecimal, result);
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_NonLogicalType() {
+    // Test with non-logical type (plain string) - should return unchanged
+    Schema stringSchema = Schema.create(Schema.Type.STRING);
+    String testValue = "test_string";
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(stringSchema, testValue, 
false);
+    assertEquals(testValue, result);
+  }
+
+  @Test
+  public void testConvertValueForSpecificDataTypes_UnionWithNull() {
+    // Test with union type containing null
+    Schema nullableDateSchema = 
HoodieSchema.createNullable(HoodieSchema.createDate()).toAvroSchema();
+
+    // Test with non-null value
+    int epochDays = 19358; // 2023-01-01
+    Object result = 
HoodieAvroUtils.convertValueForSpecificDataTypes(nullableDateSchema, epochDays, 
false);
+    assertNotNull(result);
+    assertTrue(result instanceof LocalDate);
+    assertEquals(LocalDate.of(2023, 1, 1), result);
+  }
+
+  @Test
+  public void testConvertBytesToBigDecimalWithHoodieSchema() {
+    HoodieSchema decimalSchema = HoodieSchema.createDecimal(10, 2);
+    BigDecimal expected = new BigDecimal("1234.56");
+    assertEquals(expected,
+        
HoodieAvroUtils.convertBytesToBigDecimal(expected.unscaledValue().toByteArray(),
 decimalSchema));
+  }
+
+  @Test
+  public void testConvertBytesToBigDecimalWithNonDecimalHoodieSchema() {
+    HoodieSchema stringSchema = HoodieSchema.create(HoodieSchemaType.STRING);
+    assertThrows(IllegalArgumentException.class, () ->
+        HoodieAvroUtils.convertBytesToBigDecimal(new byte[] {0x01}, 
stringSchema));
+  }

Review Comment:
   **nit:** feel free to ignore. Codecov's one partial line is 
`HoodieAvroUtils.java:1348`, the `decimalSchema != null` guard, whose null side 
nothing exercises; the cast disambiguates from the `LogicalTypes.Decimal` 
overload.
   ```suggestion
     }
   
     @Test
     public void testConvertBytesToBigDecimalWithNullHoodieSchema() {
       assertThrows(IllegalArgumentException.class, () ->
           HoodieAvroUtils.convertBytesToBigDecimal(new byte[] {0x01}, 
(HoodieSchema) null));
     }
   ```



##########
hudi-common/src/main/java/org/apache/hudi/common/avro/HoodieAvroUtils.java:
##########
@@ -883,15 +923,15 @@ private static Object 
normalizeAvroLogicalTypeToPrimitive(Object value, Schema s
    *
    * @param record  Hoodie record.
    * @param columns Names of the columns to get values.
-   * @param schema  {@link Schema} instance.
+   * @param schema  {@link HoodieSchema} instance.
    * @return Column value.
    */
   public static Object[] getRecordColumnValues(HoodieRecord record,
                                                String[] columns,
-                                               Schema schema,
+                                               HoodieSchema schema,
                                                boolean 
consistentLogicalTimestampEnabled) {
     try {
-      GenericRecord genericRecord = (GenericRecord) 
(record.toIndexedRecord(HoodieAvroSchemaCache.intern(schema), new 
Properties()).get()).getData();
+      GenericRecord genericRecord = (GenericRecord) 
(record.toIndexedRecord(schema, new Properties()).get()).getData();

Review Comment:
   **minor:** not blocking; mechanism verified, regression not demonstrated. 
The dropped `HoodieAvroSchemaCache.intern` also decided which `Schema` instance 
reached `BaseAvroPayload.getRecord` (`BaseAvroPayload.java:130`, 
`record.getSchema() == schema`; a miss re-serializes and calls 
`fromAvroBytes`). `RDDBucketIndexPartitioner.java:89` and 
`RDDCustomColumnsSortPartitioner.java:52` pass a bare `HoodieSchema.parse(...)` 
from a sort comparator, two calls per comparison. Could we keep 
`HoodieSchemaCache.intern(schema)` here, or intern in those two partitioners 
the way `SparkLazyInsertIterable.java:74` does?



##########
hudi-common/src/test/java/org/apache/hudi/common/table/read/lsm/TestLsmFileGroupRecordIterator.java:
##########
@@ -166,7 +166,7 @@ void testUpdateProcessorOnlyRunsForRecordsFromLogs() throws 
Exception {
 
   @Test
   void testDeleteLogSchemaUsesRecordKeyAndOrderingFields() {
-    HoodieSchema deleteLogSchema = 
HoodieSchemas.createDeleteLogSchema(tableSchema(), Arrays.asList("ts"));
+    HoodieSchema deleteLogSchema = 
HoodieSchemaUtils.createDeleteLogSchema(tableSchema(), Arrays.asList("ts"));

Review Comment:
   **minor:** not blocking. `testDeleteLogSchemaUsesRecordKeyAndOrderingFields` 
is now a strict subset of `TestHoodieSchemaUtils#testCreateDeleteLogSchema` 
(same single `ts` LONG field; every assertion here is repeated there, plus 
record name, key non-nullability and doc). Could we drop this method and the 
then-unused `HoodieSchemaUtils` import, since the util test now owns 
`createDeleteLogSchema`?



##########
hudi-common/src/main/java/org/apache/hudi/common/schema/HoodieSchemaTypePromotion.java:
##########
@@ -19,20 +19,28 @@
 package org.apache.hudi.common.schema;
 
 /**
- * Defines type promotion rules for HoodieSchema compatibility checking.
+ * The single table of primitive widening promotions, used by {@link 
HoodieSchemaProjectionChecker}.
  *
- * <p>Type promotion allows a reader schema with a "wider" type to read data
- * written with a "narrower" type. This follows Avro's type promotion 
rules.</p>
- *
- * <p>Supported promotions:
+ * <p>A promotion lets a reader schema with a wider type read data written 
with a narrower one:</p>
  * <ul>
- *   <li>INT → LONG, FLOAT, DOUBLE</li>
- *   <li>LONG → FLOAT, DOUBLE</li>
- *   <li>FLOAT → DOUBLE</li>
- *   <li>STRING ↔ BYTES (bidirectional)</li>
- *   <li>Decimal precision widening: (p2-s2) ≥ (p1-s1) and s2 ≥ s1</li>
+ *   <li>INT -&gt; LONG, FLOAT, DOUBLE</li>
+ *   <li>LONG -&gt; FLOAT, DOUBLE</li>
+ *   <li>FLOAT -&gt; DOUBLE</li>
+ *   <li>STRING &lt;-&gt; BYTES (bidirectional)</li>
+ *   <li>STRING &lt;- any numeric type</li>
+ *   <li>decimal widening, see {@link #isDecimalWidening(HoodieSchema, 
HoodieSchema)}</li>
  * </ul>
- * </p>
+ *
+ * <p>Logical-type-over-primitive promotions are deliberately NOT in this 
table. A TIMESTAMP reader over a
+ * LONG writer, or a UUID reader over a STRING writer, is accepted by
+ * {@link HoodieSchemaCompatibilityChecker} for reader/writer compatibility, 
but it must not make a bare
+ * long a "compatible projection" of a timestamp: writer-schema deduction 
would then silently drop the
+ * logical type. Compatibility and projection are different questions, so they 
use different tables.</p>
+ *
+ * <p>One more difference is documented rather than resolved:
+ * {@link #isDecimalWidening(HoodieSchema, HoodieSchema)} additionally 
requires the same backing (fixed
+ * versus bytes) and, for fixed, an equal fixed size, whereas the decimal 
check in
+ * {@code HoodieSchemaCompatibilityChecker} compares only precision and 
scale.</p>
  *
  * <p>This class is package-private and used internally by schema 
compatibility checkers.</p>

Review Comment:
   **nit:** feel free to ignore. The new opening line says the table is used by 
`HoodieSchemaProjectionChecker` (its only production user) and the paragraph 
above explains why the compatibility checker deliberately does not use it, but 
this trailing sentence still says the opposite.
   ```suggestion
    * <p>This class is package-private and used only by {@link 
HoodieSchemaProjectionChecker}.</p>
   ```



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