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

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


The following commit(s) were added to refs/heads/asf-site by this push:
     new 9d7e7fd66da7 docs(record-merger): document projection compatibility 
and mandatory merge fields (#19586)
9d7e7fd66da7 is described below

commit 9d7e7fd66da7dfedae8693e193c5cdfec9cfd104
Author: Ranga Reddy <[email protected]>
AuthorDate: Mon Aug 31 15:19:38 2026 +0530

    docs(record-merger): document projection compatibility and mandatory merge 
fields (#19586)
    
    The custom-merger section sketched HoodieRecordMerger as merge, 
partialMerge, getRecordType
    and getMergingStrategy, and its Implementation Guidelines listed five 
rules. Neither mentioned
    isProjectionCompatible() or getMandatoryFieldsForMerging(), even though the 
two decide whether
    a MOR read is allowed to project columns and, if so, which columns the 
merger still needs.
    
    Someone implementing a merger against these docs therefore had no way to 
learn that:
    
      - isProjectionCompatible() defaults to false, which makes the file-group 
reader fall back to
        the full table schema for a CUSTOM merger (FileGroupReaderSchemaHandler 
returns tableSchema
        when the merger is not projection compatible). Correct, but no column 
pruning.
      - once it returns true, the reader reads the query's columns plus whatever
        getMandatoryFieldsForMerging() names, and for CUSTOM mode that method 
is the only source of
        the extra columns.
      - its default is the record key field plus the ordering fields, so a 
merger reading anything
        else has to say so.
    
    Adds both methods to the interface sketch, a sixth guideline, and a 
"Projection compatibility"
    subsection covering the pairing, the trade-off and the failure mode. The 
worked example mirrors
    MaxRankRecordMerger in hudi-trino's tests, which is real compiling code, 
rather than an invented
    snippet: it keeps the interface defaults via HoodieRecordMerger.super and 
adds its own column.
    
    Also corrects the partialMerge signature in the sketch, which still showed 
Avro Schema for the
    reader schema. Master and 1.2.0 take HoodieSchema - release-1.2.md already 
lists partialMerge
    among the source-breaking signature changes.
    
    Applied to three copies, and the signature genuinely differs between them, 
so they are not
    identical: docs/ and version-1.2.0 use HoodieSchema, while version-1.1.1 
keeps Avro Schema in
    both partialMerge and getMandatoryFieldsForMerging, which is what 
release-1.1.1 shipped.
    version-1.0.x carries the interface sketch but no Implementation Guidelines 
section, so it is
    left alone.
    
    Closes #16697
---
 website/docs/record_merger.md                      | 63 +++++++++++++++++++++-
 .../versioned_docs/version-1.1.1/record_merger.md  | 61 +++++++++++++++++++++
 .../versioned_docs/version-1.2.0/record_merger.md  | 63 +++++++++++++++++++++-
 3 files changed, 185 insertions(+), 2 deletions(-)

diff --git a/website/docs/record_merger.md b/website/docs/record_merger.md
index 1bbd44117578..4970b18d646b 100644
--- a/website/docs/record_merger.md
+++ b/website/docs/record_merger.md
@@ -93,7 +93,7 @@ interface HoodieRecordMerger {
     }
 
     <T> BufferedRecord<T> partialMerge(BufferedRecord<T> older, 
BufferedRecord<T> newer,
-                                       Schema readerSchema, RecordContext<T> 
recordContext,
+                                       HoodieSchema readerSchema, 
RecordContext<T> recordContext,
                                        TypedProperties props) throws 
IOException {
         // Merges records which can contain partial updates.
         // Returns a non-null BufferedRecord with only changed fields included.
@@ -102,6 +102,20 @@ interface HoodieRecordMerger {
         ...
     }
 
+    boolean isProjectionCompatible() {
+        // Whether this merger can work on a projection of the table schema 
rather than every column.
+        // Defaults to false, which makes MOR reads fetch all columns. See 
"Projection compatibility" below.
+        ...
+    }
+
+    String[] getMandatoryFieldsForMerging(HoodieSchema dataSchema, 
HoodieTableConfig cfg,
+                                          TypedProperties properties) {
+        // The columns merge() and partialMerge() read, beyond those the query 
itself requests.
+        // Only consulted when isProjectionCompatible() returns true.
+        // Defaults to the record key field plus the ordering fields.
+        ...
+    }
+
     HoodieRecordType getRecordType() {...}
 
     String getMergingStrategy() {...}
@@ -122,8 +136,55 @@ When implementing the `HoodieRecordMerger` interface, 
follow these guidelines to
 
 5. **Associative property**: The `merge()` method should be associative: 
`merge(a, merge(b, c))` should yield the same result as `merge(merge(a, b), c)` 
for any three versions A, B, C of the same record.
 
+6. **Declare every column your merge logic reads**: if you make the merger 
projection compatible, `getMandatoryFieldsForMerging()` must list every column 
`merge()` and `partialMerge()` touch that the query does not already request. 
See below.
+
 For more details on the implementation, see [RFC 
101](https://github.com/apache/hudi/blob/master/rfc/rfc-101/rfc-101.md).
 
+#### Projection compatibility
+
+On a Merge-on-Read table the reader would rather fetch only the columns a 
query asks for. It cannot do that
+safely for a custom merger, because it has no way to know which columns your 
merge logic reads. Two methods
+resolve that, and they work as a pair:
+
+* `isProjectionCompatible()` — defaults to **`false`**, which tells the reader 
to fall back to reading the
+  **full table schema**. Merging is always correct in this mode, but a query 
selecting two columns still
+  reads every column of every log block, so it is the slower option. 
`COMMIT_TIME_ORDERING` and
+  `EVENT_TIME_ORDERING` are projection compatible; a `CUSTOM` merger is not 
until you say so.
+* `getMandatoryFieldsForMerging()` — only consulted once 
`isProjectionCompatible()` returns `true`. The
+  reader then reads the columns the query requested **plus** the ones this 
method names. Its default is the
+  record key field and the ordering fields, which is what a merger comparing 
only an ordering value needs.
+
+The two must agree, and this is where it goes wrong: if you return `true` from
+`isProjectionCompatible()` and your merge logic reads a column that is neither 
requested by the query nor
+returned by `getMandatoryFieldsForMerging()`, that column is absent from the 
records your merger receives.
+What happens next is up to your code — reading the missing field typically 
fails with a
+`NullPointerException`, and a merger that tolerates a null there will instead 
make its decision on
+incomplete data and return a silently wrong result. Either way it is 
query-dependent, showing up only for
+queries that do not happen to select the column themselves, which makes it 
easy to miss in testing.
+
+So if your merger compares, say, a `priority` column to decide a winner, 
either declare it:
+
+```Java
+@Override
+public boolean isProjectionCompatible() {
+    return true;
+}
+
+@Override
+public String[] getMandatoryFieldsForMerging(HoodieSchema dataSchema, 
HoodieTableConfig cfg,
+                                             TypedProperties properties) {
+    // Keep the defaults - record key and ordering fields - and add the column 
merge() reads, so the
+    // reader includes it even when a query does not project it.
+    LinkedHashSet<String> fields = new LinkedHashSet<>(
+            
Arrays.asList(HoodieRecordMerger.super.getMandatoryFieldsForMerging(dataSchema, 
cfg, properties)));
+    fields.add("priority");
+    return fields.toArray(new String[0]);
+}
+```
+
+or leave `isProjectionCompatible()` at its default `false` and accept the 
full-schema read. Both are correct;
+only the half-way position, projection compatible without declaring the 
fields, is not.
+
 ### Merge Mode Configs
 
 The record merge mode and optional record merge strategy ID and custom merge 
implementation classes can be specified using the below configs.
diff --git a/website/versioned_docs/version-1.1.1/record_merger.md 
b/website/versioned_docs/version-1.1.1/record_merger.md
index 1bbd44117578..bf6ac125ffc8 100644
--- a/website/versioned_docs/version-1.1.1/record_merger.md
+++ b/website/versioned_docs/version-1.1.1/record_merger.md
@@ -102,6 +102,20 @@ interface HoodieRecordMerger {
         ...
     }
 
+    boolean isProjectionCompatible() {
+        // Whether this merger can work on a projection of the table schema 
rather than every column.
+        // Defaults to false, which makes MOR reads fetch all columns. See 
"Projection compatibility" below.
+        ...
+    }
+
+    String[] getMandatoryFieldsForMerging(Schema dataSchema, HoodieTableConfig 
cfg,
+                                          TypedProperties properties) {
+        // The columns merge() and partialMerge() read, beyond those the query 
itself requests.
+        // Only consulted when isProjectionCompatible() returns true.
+        // Defaults to the record key field plus the ordering fields.
+        ...
+    }
+
     HoodieRecordType getRecordType() {...}
 
     String getMergingStrategy() {...}
@@ -122,8 +136,55 @@ When implementing the `HoodieRecordMerger` interface, 
follow these guidelines to
 
 5. **Associative property**: The `merge()` method should be associative: 
`merge(a, merge(b, c))` should yield the same result as `merge(merge(a, b), c)` 
for any three versions A, B, C of the same record.
 
+6. **Declare every column your merge logic reads**: if you make the merger 
projection compatible, `getMandatoryFieldsForMerging()` must list every column 
`merge()` and `partialMerge()` touch that the query does not already request. 
See below.
+
 For more details on the implementation, see [RFC 
101](https://github.com/apache/hudi/blob/master/rfc/rfc-101/rfc-101.md).
 
+#### Projection compatibility
+
+On a Merge-on-Read table the reader would rather fetch only the columns a 
query asks for. It cannot do that
+safely for a custom merger, because it has no way to know which columns your 
merge logic reads. Two methods
+resolve that, and they work as a pair:
+
+* `isProjectionCompatible()` — defaults to **`false`**, which tells the reader 
to fall back to reading the
+  **full table schema**. Merging is always correct in this mode, but a query 
selecting two columns still
+  reads every column of every log block, so it is the slower option. 
`COMMIT_TIME_ORDERING` and
+  `EVENT_TIME_ORDERING` are projection compatible; a `CUSTOM` merger is not 
until you say so.
+* `getMandatoryFieldsForMerging()` — only consulted once 
`isProjectionCompatible()` returns `true`. The
+  reader then reads the columns the query requested **plus** the ones this 
method names. Its default is the
+  record key field and the ordering fields, which is what a merger comparing 
only an ordering value needs.
+
+The two must agree, and this is where it goes wrong: if you return `true` from
+`isProjectionCompatible()` and your merge logic reads a column that is neither 
requested by the query nor
+returned by `getMandatoryFieldsForMerging()`, that column is absent from the 
records your merger receives.
+What happens next is up to your code — reading the missing field typically 
fails with a
+`NullPointerException`, and a merger that tolerates a null there will instead 
make its decision on
+incomplete data and return a silently wrong result. Either way it is 
query-dependent, showing up only for
+queries that do not happen to select the column themselves, which makes it 
easy to miss in testing.
+
+So if your merger compares, say, a `priority` column to decide a winner, 
either declare it:
+
+```Java
+@Override
+public boolean isProjectionCompatible() {
+    return true;
+}
+
+@Override
+public String[] getMandatoryFieldsForMerging(Schema dataSchema, 
HoodieTableConfig cfg,
+                                             TypedProperties properties) {
+    // Keep the defaults - record key and ordering fields - and add the column 
merge() reads, so the
+    // reader includes it even when a query does not project it.
+    LinkedHashSet<String> fields = new LinkedHashSet<>(
+            
Arrays.asList(HoodieRecordMerger.super.getMandatoryFieldsForMerging(dataSchema, 
cfg, properties)));
+    fields.add("priority");
+    return fields.toArray(new String[0]);
+}
+```
+
+or leave `isProjectionCompatible()` at its default `false` and accept the 
full-schema read. Both are correct;
+only the half-way position, projection compatible without declaring the 
fields, is not.
+
 ### Merge Mode Configs
 
 The record merge mode and optional record merge strategy ID and custom merge 
implementation classes can be specified using the below configs.
diff --git a/website/versioned_docs/version-1.2.0/record_merger.md 
b/website/versioned_docs/version-1.2.0/record_merger.md
index 1bbd44117578..4970b18d646b 100644
--- a/website/versioned_docs/version-1.2.0/record_merger.md
+++ b/website/versioned_docs/version-1.2.0/record_merger.md
@@ -93,7 +93,7 @@ interface HoodieRecordMerger {
     }
 
     <T> BufferedRecord<T> partialMerge(BufferedRecord<T> older, 
BufferedRecord<T> newer,
-                                       Schema readerSchema, RecordContext<T> 
recordContext,
+                                       HoodieSchema readerSchema, 
RecordContext<T> recordContext,
                                        TypedProperties props) throws 
IOException {
         // Merges records which can contain partial updates.
         // Returns a non-null BufferedRecord with only changed fields included.
@@ -102,6 +102,20 @@ interface HoodieRecordMerger {
         ...
     }
 
+    boolean isProjectionCompatible() {
+        // Whether this merger can work on a projection of the table schema 
rather than every column.
+        // Defaults to false, which makes MOR reads fetch all columns. See 
"Projection compatibility" below.
+        ...
+    }
+
+    String[] getMandatoryFieldsForMerging(HoodieSchema dataSchema, 
HoodieTableConfig cfg,
+                                          TypedProperties properties) {
+        // The columns merge() and partialMerge() read, beyond those the query 
itself requests.
+        // Only consulted when isProjectionCompatible() returns true.
+        // Defaults to the record key field plus the ordering fields.
+        ...
+    }
+
     HoodieRecordType getRecordType() {...}
 
     String getMergingStrategy() {...}
@@ -122,8 +136,55 @@ When implementing the `HoodieRecordMerger` interface, 
follow these guidelines to
 
 5. **Associative property**: The `merge()` method should be associative: 
`merge(a, merge(b, c))` should yield the same result as `merge(merge(a, b), c)` 
for any three versions A, B, C of the same record.
 
+6. **Declare every column your merge logic reads**: if you make the merger 
projection compatible, `getMandatoryFieldsForMerging()` must list every column 
`merge()` and `partialMerge()` touch that the query does not already request. 
See below.
+
 For more details on the implementation, see [RFC 
101](https://github.com/apache/hudi/blob/master/rfc/rfc-101/rfc-101.md).
 
+#### Projection compatibility
+
+On a Merge-on-Read table the reader would rather fetch only the columns a 
query asks for. It cannot do that
+safely for a custom merger, because it has no way to know which columns your 
merge logic reads. Two methods
+resolve that, and they work as a pair:
+
+* `isProjectionCompatible()` — defaults to **`false`**, which tells the reader 
to fall back to reading the
+  **full table schema**. Merging is always correct in this mode, but a query 
selecting two columns still
+  reads every column of every log block, so it is the slower option. 
`COMMIT_TIME_ORDERING` and
+  `EVENT_TIME_ORDERING` are projection compatible; a `CUSTOM` merger is not 
until you say so.
+* `getMandatoryFieldsForMerging()` — only consulted once 
`isProjectionCompatible()` returns `true`. The
+  reader then reads the columns the query requested **plus** the ones this 
method names. Its default is the
+  record key field and the ordering fields, which is what a merger comparing 
only an ordering value needs.
+
+The two must agree, and this is where it goes wrong: if you return `true` from
+`isProjectionCompatible()` and your merge logic reads a column that is neither 
requested by the query nor
+returned by `getMandatoryFieldsForMerging()`, that column is absent from the 
records your merger receives.
+What happens next is up to your code — reading the missing field typically 
fails with a
+`NullPointerException`, and a merger that tolerates a null there will instead 
make its decision on
+incomplete data and return a silently wrong result. Either way it is 
query-dependent, showing up only for
+queries that do not happen to select the column themselves, which makes it 
easy to miss in testing.
+
+So if your merger compares, say, a `priority` column to decide a winner, 
either declare it:
+
+```Java
+@Override
+public boolean isProjectionCompatible() {
+    return true;
+}
+
+@Override
+public String[] getMandatoryFieldsForMerging(HoodieSchema dataSchema, 
HoodieTableConfig cfg,
+                                             TypedProperties properties) {
+    // Keep the defaults - record key and ordering fields - and add the column 
merge() reads, so the
+    // reader includes it even when a query does not project it.
+    LinkedHashSet<String> fields = new LinkedHashSet<>(
+            
Arrays.asList(HoodieRecordMerger.super.getMandatoryFieldsForMerging(dataSchema, 
cfg, properties)));
+    fields.add("priority");
+    return fields.toArray(new String[0]);
+}
+```
+
+or leave `isProjectionCompatible()` at its default `false` and accept the 
full-schema read. Both are correct;
+only the half-way position, projection compatible without declaring the 
fields, is not.
+
 ### Merge Mode Configs
 
 The record merge mode and optional record merge strategy ID and custom merge 
implementation classes can be specified using the below configs.

Reply via email to