GitHub user ravjot28 added a comment to the discussion: MoR support for the 
standalone Java client (new HoodieJavaReadClient)

Following up on the `RecordMerger` direction, here is the reader design I plan 
to open as a draft PR. I would like feedback on the API, and on whether you 
would prefer this as an RFC first.

**Shape.** A reader class in `hudi-java-client`, built on what master already 
has:

- each read call is planned from one loaded timeline: file listing, the latest 
instant or the time-travel instant, the schema, and one 
`TableState.snapshotOf(...)` that is passed to every file group reader of that 
call;
- listing goes through `BaseHoodieTableFileIndex` (a small Java subclass, like 
`HiveHoodieTableFileIndex`), so metadata-table listing, read-optimized listing 
and time-travel validation are the code Spark and Hive use;
- each file slice is read by a `HoodieFileGroupReader` with the 
`AvroReaderContextFactory`, so merging follows the table's merge mode: 
event-time ordering, commit-time ordering, or a configured custom 
`HoodieRecordMerger`. There is no Java-specific merge logic;
- the reader opens one file slice at a time, and each merge buffer can spill to 
disk.

**API sketch** (signatures only):

```java
@PublicAPIClass(maturity = ApiMaturityLevel.EVOLVING)
public class HoodieJavaTableReader {
  public HoodieJavaTableReader(HoodieEngineContext context, String basePath, 
TypedProperties props)
  public HoodieJavaTableReader(HoodieEngineContext context, 
HoodieTableMetaClient metaClient, TypedProperties props)

  // latest snapshot: base files merged with log files on merge-on-read tables
  public ClosableIterator<IndexedRecord> readSnapshot()
  public ClosableIterator<IndexedRecord> readSnapshot(ReadOptions options)
  // base files only
  public ClosableIterator<IndexedRecord> readOptimized(ReadOptions options)
}

// public, built with a builder; applies to both reads
ReadOptions.builder()
    .asOf("20261101093000000")              // time travel; the file index 
takes a query instant for either read
    .columns(Arrays.asList("id", "ts"))     // projection; default is the full 
table schema
    .partitions(Arrays.asList("2026/11/01")) // partition paths relative to the 
base path; default is all
    .build()
```

- Records are `IndexedRecord` in the requested schema. The caller closes each 
returned iterator: it holds the open files and the planning state of its call, 
and a failure before the iterator is returned releases them. The reader object 
owns no open read resources between calls, so it is not `AutoCloseable`. A 
reader built from a meta client reloads its own copy of the timeline for each 
call and does not change the one passed in.
- Time travel uses Spark's file-index settings and 
`TimelineUtils#validateTimestampAsOf`. The timestamp must be earlier than the 
first incomplete commit that is not a clustering commit, when there is one. 
When the active timeline has a completed clean, the timestamp must also not be 
earlier than that clean's earliest commit to retain. If that clean records no 
earliest commit to retain, the check falls back to the first completed write 
instant of the active timeline, and this fallback can refuse an archived 
timestamp even if its files remain. With non-blocking concurrency control, an 
inflight writer therefore blocks time travel at or after its requested instant, 
as it does on Spark.

**Incremental reads (second PR, after this is agreed).** 
`readIncremental(IncrementalOptions)` by completion time, with Spark's 
incremental defaults: the start is exclusive and the end inclusive 
(`OPEN_CLOSED`, the range type of Spark's incremental relation), compaction 
commits are not skipped (as `hoodie.datasource.read.incr.skip_compact` defaults 
to `false`), and clustering commits are kept (Spark's incremental relation 
keeps them; skipping them is disabled there because of a duplication problem). 
The start is required: `"earliest"` must be given explicitly, and `null` is 
rejected. The end is optional. I plan to use `IncrementalQueryAnalyzer` to 
select instants by completion time, archived instants included, and the shared 
file index for listing. Spark's incremental relation adds more on top: 
affected-file collection from commit metadata, a check for missing files, and a 
configurable fallback to a full table scan. The second PR will specify and test 
that behavior against Spark rath
 er than assume it.

Filtering log records by instant range before merging can give incomplete or 
wrong results: for partial updates, for event-time ordering when an in-range 
write with a lower ordering value displaces an earlier higher value, and 
possibly for custom mergers that need earlier records. #18943 reports the 
partial-update case, and #19005 proposes merging whole file groups before 
filtering the output. The second PR will settle these semantics, and document 
any case it does not handle.

**Limitations of the first version:** no key or predicate filters (the Avro 
reader context has key-filter overloads, so that would be a follow-up); no CDC 
output (CDC-enabled tables read as merged rows); tables with the LSM storage 
layout (`hoodie.table.storage.layout=lsm_tree`) are rejected unless the Java 
writer path for them is tested end to end in the same PR; tables without meta 
fields can be read as snapshots, and incremental reads of them are refused.

**Questions.**

1. RFC or not? Related read API designs have been proposed as RFCs: RFC-64 
(#7080) for query engine integrations and RFC-98 for the Spark DataSource V2 
read path. I am happy to write this up as an RFC if you prefer; otherwise I 
would open the first PR as a draft linked here.
2. Package: `org.apache.hudi.client`, next to `HoodieJavaWriteClient`, or under 
`org.apache.hudi.core`? Master already has `org.apache.hudi.core.read`, where 
`BaseHoodieTableFileIndex` lives, and the draft #19214 moves more reader code 
under `org.apache.hudi.core`.
3. Name: this is the class the first post called `HoodieJavaReadClient`. 
`HoodieJavaTableReader` keeps this general table reader apart from Spark's 
index-oriented `SparkRDDReadClient` / `HoodieReadClient`, which also provide 
key-based read-optimized reads. `HoodieJavaReadClient` would match 
`HoodieJavaWriteClient` instead. Which would you prefer?

The write-side PRs (simple bucket index on the Java engine, delete partitions 
and partition TTL, log compaction, and a few small fixes) are separate and do 
not depend on this reader.


GitHub link: 
https://github.com/apache/hudi/discussions/19798#discussioncomment-19545215

----
This is an automatically sent email for [email protected].
To unsubscribe, please send an email to: [email protected]

Reply via email to