This is an automated email from the ASF dual-hosted git repository.
yihua pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hudi-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 441ff6b3 docs: add a feature overview, rust quick start, and doc links
to the readme (#940)
441ff6b3 is described below
commit 441ff6b3ce3b34e2495f3c63bdc908bf248c41e4
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Oct 30 13:56:21 2026 -0700
docs: add a feature overview, rust quick start, and doc links to the readme
(#940)
---
CONTRIBUTING.md | 2 +-
README.md | 217 ++++++++++++++++++++++++++++----
benchmark/filegroup/README.md | 2 +-
benchmark/filegroup/src/main.rs | 2 +-
crates/core/src/avro_to_arrow/README.md | 2 +-
docs/reader-spec.md | 13 +-
docs/writer-design.md | 39 ++++--
7 files changed, 236 insertions(+), 41 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f8df7ed7..ccf9e9dc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,7 +30,7 @@ in https://github.com/apache/hudi-rs/contribute.
## File an issue
Testing and reporting bugs are also valueable contributions. Please follow
-the [issue
template](https://github.com/apache/hudi-rs/issues/new?template=bug_report.yml)
to file bug reports.
+the [issue
template](https://github.com/apache/hudi-rs/issues/new?template=1-bug.yml) to
file bug reports.
## Issue tracking
diff --git a/README.md b/README.md
index c2414d14..851e1f2e 100644
--- a/README.md
+++ b/README.md
@@ -56,17 +56,27 @@ bindings. It creates, writes, and reads Hudi tables from
Rust and Python in a si
[crates]: https://crates.io/crates/hudi
[crates-badge]:
https://img.shields.io/crates/d/hudi?style=flat-square&color=163669
-The `hudi` crate carries two features: `datafusion` (off by default, see
-[Apache DataFusion](#apache-datafusion)) and `spill-rocksdb` (on by default),
the merge map's
-on-disk tier, which a merge-on-read merge spills to when a file group's log
records exceed
-`hoodie.memory.merge.max.size`. RocksDB is built from source with `bindgen`,
so it needs `libclang`
-and a C++ toolchain; `default-features = false` drops it, and a merge that
would have spilled then
-fails instead.
+## Features
+
+| Area | Supported
|
+|-------------------|-----------------------------------------------------------------------------------------------------------------|
+| Table types | Copy-on-Write (COW), Merge-on-Read (MOR)
|
+| Table versions | Read: 6, 8, 9 (Hudi 0.14 and later). Write: 8, 9 (Hudi
1.0 and later), plus a narrow append path on 6 |
+| Queries | Snapshot, time travel, incremental, read-optimized
(MOR), streaming snapshot reads, column filters with pruning |
+| Writes | Create, append, upsert, update, delete (by predicate or
key), overwrite, dynamic partition overwrite |
+| Table services | Merge-on-read compaction (experimental: on demand,
inline, or in the background), rollback of failed writes, timeline archival |
+| Metadata table | File listings, column and partition stats, record-level
index, kept up to date on every write and compacted as it grows (on by default)
|
+| Base file formats | Parquet (read and write), Lance (read), HFile (metadata
table) |
+| Storage | Local file system, Amazon S3, Azure Blob Storage, Google
Cloud Storage |
+| APIs | Rust and Python (read and write); C++ and a C ABI
(`hudi-ffi`) for file-slice reads |
+| Query engines | [Apache DataFusion](#apache-datafusion) (`datafusion`
feature in Rust, included in Python); see [Other
Integrations](#other-integrations) |
## Quick start
Create a table, upsert and delete rows, then read what changed after the first
commit.
+### Python
+
```python
import tempfile
@@ -100,8 +110,102 @@ snapshot = pa.Table.from_batches(table.read())
print(sorted(snapshot.column("doc_id").to_pylist())) # ['doc1', 'doc2',
'doc4']
```
+### Rust
+
+```shell
+cargo add hudi
+cargo add arrow@59
+cargo add tokio@1 --features macros,rt-multi-thread
+```
+
+```rust
+use std::sync::Arc;
+
+use arrow::array::{Int64Array, RecordBatch, StringArray};
+use arrow::datatypes::{DataType, Field, Schema};
+use hudi::error::Result;
+use hudi::table::{QueryType, ReadOptions, Table};
+
+fn docs(ids: &[&str], version: i64) -> RecordBatch {
+ let schema = Arc::new(Schema::new(vec![
+ Field::new("doc_id", DataType::Utf8, false),
+ Field::new("title", DataType::Utf8, false),
+ Field::new("version", DataType::Int64, false),
+ ]));
+ let titles: Vec<String> = ids.iter().map(|d| format!("{d}
v{version}")).collect();
+ RecordBatch::try_new(
+ schema,
+ vec![
+ Arc::new(StringArray::from(ids.to_vec())),
+ Arc::new(StringArray::from(titles)),
+ Arc::new(Int64Array::from(vec![version; ids.len()])),
+ ],
+ )
+ .unwrap()
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ let base_uri = std::env::temp_dir().join(format!("docs_{}",
std::process::id()));
+ let mut table = Table::create(base_uri.to_str().unwrap())
+ .with_table_name("docs")
+ .with_record_key_fields(["doc_id"])
+ .with_ordering_fields(["version"])
+ .create()
+ .await?;
+ let first = table.append([docs(&["doc1", "doc2", "doc3"], 1)]).await?;
+
+ let result = table.upsert([docs(&["doc2", "doc4"], 2)]).await?;
+ println!("{} {}", result.num_updates, result.num_inserts); // 1 1
+
+ table.delete("doc_id = 'doc3'").await?;
+
+ // Commits that completed after the first one.
+ let options = ReadOptions::new()
+ .with_query_type(QueryType::Incremental)
+ .with_start_timestamp(first.completion_time.unwrap());
+ let changed = table.read(&options).await?;
+ let snapshot = table.read(&ReadOptions::new()).await?;
+ let num_rows = |batches: &[RecordBatch]| batches.iter().map(|b|
b.num_rows()).sum::<usize>();
+ println!("{} {}", num_rows(&changed), num_rows(&snapshot)); // 2 3
+ Ok(())
+}
+```
+
See [Writing Tables](#writing-tables) for the full write API, and
-[`table_writes.rs`](crates/core/examples/table_writes.rs) for a Rust example.
+[`crates/core/examples/table_writes.rs`](crates/core/examples/table_writes.rs)
+(`cargo run -p hudi-core --example table_writes`) for a runnable Rust tour of
it. The
+[Python/Rust quick start
guide](https://hudi.apache.org/docs/python-rust-quick-start-guide) on the
+Hudi website goes further, with DuckDB, Polars, and DataFusion queries. API
references are listed
+under [Documentation](#documentation).
+
+## Requirements
+
+- Python 3.10 or later. Wheels are published to PyPI.
+- Rust 1.94.1 or later.
+- The `hudi` crate carries two features: `datafusion` (off by default, see
+ [Apache DataFusion](#apache-datafusion)) and `spill-rocksdb` (on by
default), the merge map's
+ on-disk tier, which a merge-on-read merge spills to when a file group's log
records exceed
+ `hoodie.memory.merge.max.size`. RocksDB is built from source with `bindgen`,
so it needs
+ `libclang` and a C++ toolchain; `default-features = false` drops it, and a
merge that would have
+ spilled then fails instead.
+
+## Documentation
+
+- Quick start: the [Python and Rust quick start
guide](https://hudi.apache.org/docs/python-rust-quick-start-guide)
+ on the Hudi website, and the shorter [quick start
above](https://github.com/apache/hudi-rs#quick-start).
+- Rust API reference: [docs.rs/hudi](https://docs.rs/hudi) (the `hudi` crate
re-exports
+ [`hudi-core`](https://docs.rs/hudi-core) and, with the `datafusion` feature,
+ [`hudi-datafusion`](https://docs.rs/hudi-datafusion)).
+- Python API reference: the package ships type stubs with docstrings
+
([`python/hudi/_internal.pyi`](https://github.com/apache/hudi-rs/blob/main/python/hudi/_internal.pyi)),
which IDEs and
+ type checkers read.
+- Runnable Rust example:
[`crates/core/examples/table_writes.rs`](https://github.com/apache/hudi-rs/blob/main/crates/core/examples/table_writes.rs).
+- Reader semantics (read options, filters, guarantees):
[`docs/reader-spec.md`](https://github.com/apache/hudi-rs/blob/main/docs/reader-spec.md).
+- Writer design:
[`docs/writer-design.md`](https://github.com/apache/hudi-rs/blob/main/docs/writer-design.md).
+- Packages: [PyPI](https://pypi.org/project/hudi/) and
[crates.io](https://crates.io/crates/hudi).
+ Source: [github.com/apache/hudi-rs](https://github.com/apache/hudi-rs).
Contributing:
[`CONTRIBUTING.md`](https://github.com/apache/hudi-rs/blob/main/CONTRIBUTING.md).
+- Hudi concepts (timeline, table types, query types):
[hudi.apache.org/docs/overview](https://hudi.apache.org/docs/overview).
## Reading Tables
@@ -214,7 +318,10 @@ Timestamp strings without a timezone offset (for example
`yyyy-MM-dd'T'HH:mm:ss`
### Incremental Query
-Incremental query reads the changed data from the table for a given time range.
+Incremental query reads the changed data from the table for a given time
range, half-open
+`(start, end]`. On table version 8 and later (timeline layout v2) the bounds
are commit
+completion times, so start from a write result's `completion_time`; on older
tables they are
+requested times. The end defaults to everything committed so far.
#### Python
@@ -229,7 +336,7 @@ batches = hudi_table.read(
.with_end_timestamp(t2)
)
-# read the records after t1 (end defaults to the latest commit)
+# read the records after t1 (end defaults to everything committed so far)
batches = hudi_table.read(
HudiReadOptions()
.with_query_type(HudiQueryType.Incremental)
@@ -257,7 +364,7 @@ let options = ReadOptions::new()
.with_end_timestamp(t2);
let batches = hudi_table.read(&options).await?;
-// read the records after t1 (end defaults to the latest commit)
+// read the records after t1 (end defaults to everything committed so far)
let options = ReadOptions::new()
.with_query_type(QueryType::Incremental)
.with_start_timestamp(t1);
@@ -365,8 +472,9 @@ ArrowArrayStream* stream_ptr =
reader->read_file_slice_from_paths("relative/path
## Writing Tables
hudi-rs includes a native, single-node write path that is storage-compatible
-with the Apache Hudi Spark writer: tables written here are readable — and
-serviceable (compaction, clustering, cleaning) — by Spark, and vice versa.
+with the Apache Hudi Spark writer: Spark reads tables written here and runs
+its table services on them (compaction, clustering, cleaning), and hudi-rs
reads
+and writes tables Spark wrote.
Writes target COPY_ON_WRITE and MERGE_ON_READ tables at table version 9
(Hudi 1.1.x; version 8 via `with_table_version(8)`), and maintain the
metadata table (file listings, column/partition stats, record-level index) on
@@ -434,19 +542,19 @@ table.append(batch)
#### Rust
```rust
-use arrow_array::RecordBatch;
+use arrow::array::RecordBatch;
-let batch: RecordBatch = /* columns: uuid, city, ts, fare, ... */
-# unimplemented!();
+let batch: RecordBatch = todo!("columns: uuid, city, ts, fare, ...");
table.append([batch]).await?;
```
### Upsert
-`upsert` tags incoming records against the record-level index: existing keys
+`upsert` tags incoming records against the table's index (the record-level
+index when enabled, otherwise a scan of the base files' keys): existing keys
update their file group in place (a rewritten base file on COW, a log file on
-MOR); new keys pack into existing small files first and then form new file
-groups.
+MOR). On COW, new keys fill existing small files first and then form new file
+groups; on MOR, they always form new file groups.
#### Python
@@ -529,6 +637,55 @@ table.overwrite([batch]).await?;
table.dynamic_partition_overwrite([batch]).await?;
```
+### Compaction (Experimental)
+
+On a merge-on-read table, upserts, updates, and deletes land in log files.
`compact` folds
+them into new base files: every file group whose latest file slice has log
files gets a new
+base file of its merged rows, under one compaction commit. Rows keep their
commit time, so
+incremental reads see no new rows from a compaction. When no file group has
log files,
+nothing is written. A copy-on-write table has nothing to compact and returns
an error.
+
+Compaction can also run after delta commits once the number since the latest
compaction
+reaches `hoodie.compact.inline.max.delta.commits` (default 5): in the write
call with
+`hoodie.compact.inline=true`, or on a background task with
+`hoodie.datasource.compaction.async.enable=true`, which `await_compaction`
waits for. Both
+are off by default. The metadata table compacts itself as commits accumulate.
+
+#### Python
+
+```python
+mor = HudiTable.create(
+ "/tmp/trips_mor",
+ record_key="uuid",
+ table_type="mor",
+ # Optional: also compact in the write call every 5 delta commits.
+ hudi_options={"hoodie.compact.inline": "true"},
+)
+mor.append(batch)
+mor.upsert(batch) # updates go to log files
+
+result = mor.compact()
+print(result.num_file_groups, result.num_log_files, result.num_writes)
+```
+
+#### Rust
+
+```rust
+let mut mor = Table::create("/tmp/trips_mor")
+ .with_table_name("trips")
+ .with_table_type(TableTypeValue::MergeOnRead)
+ .with_record_key_fields(["uuid"])
+ // Optional: also compact in the write call every 5 delta commits.
+ .with_option("hoodie.compact.inline", "true")
+ .create()
+ .await?;
+mor.append([batch.clone()]).await?;
+mor.upsert([batch]).await?; // updates go to log files
+
+let result = mor.compact().await?;
+println!("{} {} {}", result.num_file_groups, result.num_log_files,
result.num_writes);
+```
+
### Behavior Notes
- Every write runs as a full Hudi transaction: instants are requested and
@@ -552,11 +709,11 @@ Create a Hudi table instance using its constructor or the
`TableBuilder` API.
All read APIs accept a `ReadOptions` (Rust) / `HudiReadOptions` (Python)
value. It stores three fields — `filters`, `projection`, and `hudi_options` —
and exposes chainable `with_*` builders for the rest. The available knobs:
- `query_type` (`with_query_type`) — `Snapshot` (default) or `Incremental`.
Drives dispatch in `read`, `read_stream`, and `get_file_slices`.
-- `filters` — column filters as `(field, op, value)` tuples. The field can be
any column (partition or data). Used for partition pruning, file-level stats
pruning (snapshot only), and row-level filtering.
+- `filters` — column filters as `(field, op, value)` tuples. The field can be
any column (partition or data). Used for partition pruning, file-level stats
pruning (copy-on-write and read-optimized reads), file-group selection through
the record-level index for `=`/`IN` filters on the record key (latest snapshot,
not read-optimized), predicate pushdown into the parquet reader, and row-level
filtering.
- `projection` — columns to return. Streaming pushes the projection down to
the parquet reader; eager reads project after merging.
- `batch_size` (`with_batch_size`) — rows per batch (streaming only; eager
reads return one batch per file slice).
- `as_of_timestamp` (`with_as_of_timestamp`) — snapshot/time-travel timestamp
(defaults to latest commit).
-- `start_timestamp` / `end_timestamp` (`with_start_timestamp` /
`with_end_timestamp`) — incremental range (defaults to earliest…latest).
+- `start_timestamp` / `end_timestamp` (`with_start_timestamp` /
`with_end_timestamp`) — incremental range, bounding completion times on table
version 8 and later (defaults to the beginning of the timeline…everything
committed so far).
- `hudi_options` — Hudi configs for this read (e.g.
`hoodie.read.use.read_optimized.mode`). A config that selects *which* read to
perform — `hoodie.read.query.type` and the as-of/start/end timestamps — is
per-read only and is dropped when set on the table. The rest describe *how* to
read: set them on the table and override them here. See [Read
configs](#read-configs).
| Stage | API
| Description
|
@@ -577,7 +734,7 @@ silently redirect every later read.
|-----------------------------------------------------------------|---------------|---------------|--------------------------------------------------------------------------------------------------------------------------|
| `hoodie.read.query.type` | `snapshot`
| per read | `snapshot` or `incremental`.
|
| `hoodie.read.as.of.timestamp` | latest
commit | per read | Snapshot time-travel point.
|
-| `hoodie.read.start.timestamp` / `hoodie.read.end.timestamp` | earliest /
latest | per read | Incremental window, half-open `(start, end]`.
|
+| `hoodie.read.start.timestamp` / `hoodie.read.end.timestamp` | earliest /
latest | per read | Incremental window, half-open `(start, end]`; completion
times on table version 8 and later, requested times before. |
| `hoodie.read.file.group.reader.version` | `2`
| table or read | Which file group reader merges a slice. Version 2 is the
default; a read it cannot serve falls back to version 1. |
| `hoodie.read.use.read_optimized.mode` | `false`
| table or read | Read base files only, skipping the log files, on MOR
tables. |
| `hoodie.read.stream.batch_size` | `1024`
| table or read | Rows per batch for streaming reads.
|
@@ -619,7 +776,7 @@ bindings) are not tied to these versions. The
`hudi[datafusion]` extra first shi
| hudi-rs | DataFusion | arrow | Python `datafusion` package |
|---------|------------|-------|-----------------------------|
-| 0.6 (unreleased) | 55 | 59 | `>=55.0.0,<56` |
+| 0.6 | 55 | 59 | `>=55.0.0,<56` |
| 0.5 | 54 | 58 | `>=54.0.0,<55` |
| 0.4 | 46 | 54 | not available |
| 0.3 | 43 | 53 | not available |
@@ -642,7 +799,7 @@ slows compilation noticeably. It goes away once a Lance
release shares the works
```shell
cargo new my_project --bin && cd my_project
-cargo add tokio@1 datafusion@55
+cargo add tokio@1 datafusion@55 # the DataFusion major that pairs with the
hudi release, see the table above
cargo add hudi --features datafusion
```
@@ -735,6 +892,20 @@ async fn main() -> Result<()> {
}
```
+## Project layout
+
+| Path | Contents
|
+|---------------------|-----------------------------------------------------------------------------|
+| `crates/hudi` | The `hudi` crate: re-exports `hudi-core`, and
`hudi-datafusion` with the `datafusion` feature |
+| `crates/core` | `hudi-core`: tables, timeline, file groups, metadata
table, readers, writers |
+| `crates/datafusion` | `hudi-datafusion`: the DataFusion `TableProvider`
|
+| `crates/ffi` | `hudi-ffi`: a C ABI for engines, over the Arrow C Data
Interface |
+| `python` | The `hudi` Python package (PyO3)
|
+| `cpp` | The C++ binding (`cxx`)
|
+| `crates/test` | Shared test fixtures
|
+| `benchmark` | Benchmark harnesses
|
+| `docs` | Reader spec and writer design
|
+
## Contributing
Check out the [contributing guide](./CONTRIBUTING.md) for all the details
about making contributions to the project.
diff --git a/benchmark/filegroup/README.md b/benchmark/filegroup/README.md
index be6e7f91..382739f3 100644
--- a/benchmark/filegroup/README.md
+++ b/benchmark/filegroup/README.md
@@ -40,7 +40,7 @@ cargo build -p fg-bench --release
--columns id,ts,value --iterations 5 --output-json run.json
# Override the streaming chunk size (hoodie.read.stream.batch_size). Omit to
use
-# the reader default (4096 rows).
+# the reader default (1024 rows).
./target/release/fg-bench --table /abs/path/to/table --batch-size 1024
```
diff --git a/benchmark/filegroup/src/main.rs b/benchmark/filegroup/src/main.rs
index 4423413a..13acb328 100644
--- a/benchmark/filegroup/src/main.rs
+++ b/benchmark/filegroup/src/main.rs
@@ -69,7 +69,7 @@ struct Args {
columns: Option<Vec<String>>,
/// Streaming chunk size (`hoodie.read.stream.batch_size`). When
- /// omitted the reader uses its built-in default (DEFAULT_BATCH_SIZE =
4096).
+ /// omitted the reader uses its built-in default (1024 rows).
/// Set this to measure the effect of chunk granularity on wall time. (The
/// peak-RSS effect shows only on the streaming `open()` path; this bench
/// drives `read()`, which retains the full output, so `max_rss` is
diff --git a/crates/core/src/avro_to_arrow/README.md
b/crates/core/src/avro_to_arrow/README.md
index 06595b93..5793f83f 100644
--- a/crates/core/src/avro_to_arrow/README.md
+++ b/crates/core/src/avro_to_arrow/README.md
@@ -27,4 +27,4 @@
- Removed `reader.rs` (not needed for Hudi use case).
- Original tests are removed.
- DataFusion errors (`datafusion_common::error::Result`) replaced with Hudi
errors (`crate::error::Result`).
-- Adjusted `AvroArrowArrayReader` to work with `Iterator<Item =
AvroResult<Value>>` instead of a reader-based approach.
+- Only the schema conversion (`schema.rs`, `to_arrow_schema`) is kept; the
array reader is removed.
diff --git a/docs/reader-spec.md b/docs/reader-spec.md
index 1eae8581..4448b5e5 100644
--- a/docs/reader-spec.md
+++ b/docs/reader-spec.md
@@ -38,7 +38,7 @@ For tutorials and end-to-end examples, see the
[README](../README.md). Covers Ru
The two `QueryType` variants on `ReadOptions`:
- **Snapshot** — latest table state at one commit (the latest by default; an
explicit `as_of_timestamp` for time-travel).
-- **Incremental** — records changed in the half-open range (`start_timestamp`,
`end_timestamp`].
+- **Incremental** — records changed in the half-open range (`start_timestamp`,
`end_timestamp`]. On timeline layout v2 (table version 8 and later) the bounds
are commit completion times, so pass a write result's `completion_time` as the
start; on layout v1 they are requested times.
Snapshot reads have an **eager** form returning all batches and a
**streaming** form yielding batches as they're read. Incremental reads
currently expose only the eager form.
@@ -59,7 +59,7 @@ Per-slice reads — reading a single `FileSlice` the caller
already selected (ty
| `with_query_type(QueryType)` | `hoodie.read.query.type`
| `snapshot` |
| `with_as_of_timestamp(ts)` | `hoodie.read.as.of.timestamp`
| latest commit (Snapshot only) |
| `with_start_timestamp(ts)` | `hoodie.read.start.timestamp`
| `19700101000000000` (Incremental only) |
-| `with_end_timestamp(ts)` | `hoodie.read.end.timestamp`
| latest commit (Incremental only) |
+| `with_end_timestamp(ts)` | `hoodie.read.end.timestamp`
| everything committed so far (Incremental only) |
| `with_batch_size(n)` | `hoodie.read.stream.batch_size`
| `1024` (streaming, base-file-only slices) |
`with_batch_size` scope: it sizes the batches of a slice that has **no log
files**, which is read straight from the base file. A slice **with** log files
is merged, and the merge sizes its own chunks (`MERGE_CHUNK_ROWS`) so that one
chunk of merging is a bounded amount of synchronous work;
`hoodie.read.stream.batch_size` has no effect there. The two happen to agree at
1024 today.
@@ -98,11 +98,12 @@ For `IN` / `NOT IN`, the value string is split on unescaped
commas and trimmed:
`with_filters` parses and cardinality-validates upfront; an unrecognized
operator or empty `IN` / `NOT IN` value list errors at the builder rather than
at read time.
-The `field` may be any column. Filters drive three things:
+The `field` may be any column. Filters drive these:
- **Partition pruning** when the field is a partition column. Always applied.
-- **File-level stats pruning** when the field is a data column with min/max
stats in the metadata table. Snapshot/time-travel only — incremental file
planning does not stats-prune.
-- **Row-group pruning** inside a base file, from the parquet footer's own
statistics. Conservative: a row group that cannot match is never fetched, and
one that might match is kept.
+- **File-level stats pruning** when the field is a data column with min/max
stats, from the metadata table's column stats, falling back to the parquet
footer for a file without a stats record. Copy-on-write tables and
read-optimized reads only, since a merged slice's rows are not described by its
base file's stats. Snapshot/time-travel only — incremental file planning does
not stats-prune.
+- **Record-index file-group selection** when the record-level index is enabled
and an `=` / `IN` filter names the record key: a latest-snapshot read (not time
travel, not read-optimized) keeps only the file groups the index names.
+- **Parquet pushdown** inside a base file: row groups that cannot match are
skipped using the footer's statistics, and a row filter decodes the filter
columns first and the rest only for rows that pass. A slice without log files
pushes every filter; one that merges log files pushes only the record-key
filters. Conservative: a row group or row that might match is kept.
- **Row-level mask** applied to every returned batch. This is the
authoritative filter; pruning is best-effort.
Values are strings; they are cast to the target column's Arrow type at filter
time. Unparseable values (e.g. `"abc"` against `Int64`) error.
@@ -297,7 +298,7 @@ A single-use iterator returned by streaming APIs. `for
batch in stream:` or `nex
### Snapshot atomicity
-A snapshot read is pinned to one completed commit timestamp; mid-read writes
are not visible. The timeline is loaded once when `Table` / `HudiTable` is
constructed and reused for every subsequent read on that instance — commits
that land *after* construction require building a new instance to observe. When
`as_of_timestamp` is unset, "latest commit" means the latest commit in the
cached timeline, not in storage at call time.
+A snapshot read is pinned to one completed commit timestamp; mid-read writes
are not visible. The timeline is loaded once when `Table` / `HudiTable` is
constructed and reused for every subsequent read on that instance. Writes,
`compact`, and `await_compaction` through that instance refresh it; commits
made by other writers or other instances after construction require building a
new instance to observe. When `as_of_timestamp` is unset, "latest commit" means
the latest commit in the cache [...]
### Incremental semantics
diff --git a/docs/writer-design.md b/docs/writer-design.md
index af6523f4..c604b67f 100644
--- a/docs/writer-design.md
+++ b/docs/writer-design.md
@@ -37,7 +37,7 @@ design as implemented in `crates/core/src/write/`.
- Table versions 8 (Hudi 1.0.x) and 9 (Hudi 1.1.x, the default), timeline
layout v2 with completion-time instants. The one exception is
`append` on a table below version 8 (timeline layout v1), which takes
- a narrow path in `table/writer.rs`: unpartitioned append-only tables with
+ a narrow path in `write/append.rs`: unpartitioned append-only tables with
`hoodie.populate.meta.fields=false` and no metadata table, one base file
per call.
- Metadata table (MDT) with `files`, `column_stats`, `partition_stats`, and
@@ -49,7 +49,8 @@ design as implemented in `crates/core/src/write/`.
**Not yet implemented**
-- Write bindings for Python / C++ / DataFusion (the read path covers those).
+- Write bindings for C++ / DataFusion (the read path covers those; Python has
the
+ full write API).
- Clustering and cleaning as *drivers* (hudi-rs interoperates with Spark-run
clustering and cleaning, but does not schedule or execute them natively);
log compaction; compaction strategies other than "every file slice with log
@@ -176,7 +177,7 @@ deltacommit (§3.3).
| Verb | Semantics |
|------|-----------|
| `append` | Bulk-insert-like: new file groups per call, no index lookup,
size-split into `hoodie.parquet.max.file.size` buckets. Below version 8, one
base file per call on unpartitioned append-only tables (see §1). |
-| `upsert` / `upsert_with` | Index-tagged updates + inserts. Updates rewrite
their file group (COW) or append log blocks to their file slice (MOR). Inserts
pack into existing small files first. |
+| `upsert` / `upsert_with` | Index-tagged updates + inserts. Updates rewrite
their file group (COW) or append log blocks to their file slice (MOR). Inserts
pack into existing small files first on COW; on MOR they always form new file
groups (§4.3). |
| `update` | SQL-style column update on rows matching a filter. |
| `delete` / `delete_keys` | Filter- or key-based deletes. A file group left
empty still receives an empty base file version so the deletion is durable for
all readers. |
| `overwrite` | `INSERT_OVERWRITE_TABLE`: replaces all file groups via a
replacecommit. |
@@ -218,7 +219,18 @@ and assembled in plan order, so results are deterministic.
- Commit metadata records the **data schema** (no `_hoodie_*` meta fields).
- Incoming batches may differ from the table schema in nullability only
(Spark evolves table schemas to all-nullable); such batches are aligned
- rather than rejected. Name/type/order changes still error.
+ rather than rejected. Fixed-size lists are written as lists and list item
+ fields are named `element`, as Java's Avro conversion reads them back.
+ Name/type/order changes still error.
+- Record keys: one or more key fields of string, boolean, or integer type.
+ Several fields form a composite key encoded as Java's complex key
+ generator does (`field:value` pairs joined by commas). Date, decimal,
+ timestamp, and floating-point key fields are rejected, since Java writers
+ store them in more than one form (`keygen/record_key.rs`).
+- Base file footers carry the record-key bloom filter only when
+ `hoodie.parquet.bloom.filter.enabled=true` or `hoodie.index.type` names a
+ bloom index: the record index locates keys without it. A Java writer that
+ tags with a BLOOM or SIMPLE index needs it set on tables hudi-rs writes.
### 4.6 Compaction
@@ -328,7 +340,8 @@ the next one runs.
- **Incremental** reads follow Hudi 1.x completion-time semantics: instants
are selected by *completion* time in `(start, end]`; rows and MOR log blocks
are then filtered by membership in the exact selected instant set. The
- default `end` is the latest completion time. This matches the Spark reader,
+ default `end` is the greatest completion time, so it covers everything
+ committed so far. This matches the Spark reader,
including the boundary case where an instant's request time equals `start`
but its completion is later.
@@ -378,6 +391,13 @@ The acceptance gate for interop lives in
Spark-created and to an rs-created partitioned table; filtered Spark reads
with data skipping (column_stats + partition_stats pruning) must match the
same reads without it.
+- **I — Spark indexes on rs tables**: Spark upserts with SIMPLE, BLOOM, and
+ GLOBAL_BLOOM indexes on rs-written COW/MOR tables find every key rs wrote
+ and re-insert a key rs deleted without duplicates. These tables set
+ `hoodie.parquet.bloom.filter.enabled=true`, since rs leaves the footer
+ bloom filter out by default.
+- **J — Spark SQL DML on rs tables**: Spark SQL UPDATE/DELETE/INSERT on
+ rs-written COW/MOR tables in a non-default database, then rs writes on top.
- **K — rs compaction**: rs compacts an rs-written MOR table, and its handles
compact the metadata table inline every two delta commits; Spark reads the
result (snapshot and time travel, with and without the metadata table),
@@ -393,8 +413,10 @@ crates/core/src/write/
schema alignment, log/parquet file tasks
create.rs Table::create builder, hoodie.properties, MDT bootstrap
append.rs append verb, instant time generation, parquet writer props
- rewrite.rs upsert/update/delete/overwrite engines (COW rewrite groups,
- MOR delta logs), instant request/complete helpers
+ rewrite/ upsert/update/delete/overwrite engines: mod.rs (instant
+ request/complete helpers), cow.rs (COW rewrite groups),
+ mor.rs (MOR delta logs), rows.rs (row selection and merging),
+ validate.rs (write preconditions)
sizing.rs small-file packing: SizingConfig, insert bucket assignment
metadata.rs MDT updates: files, record_index, column/partition stats
metadata_compaction.rs
@@ -405,7 +427,8 @@ crates/core/src/write/
rollback.rs eager rollback of failed writes
archival.rs LSM timeline archival
lock.rs LockProvider + InProcessLockProvider
- keygen.rs record key generation
+ keygen.rs record keys and partition paths for write batches
+ (key encoding shared with readers: crates/core/src/keygen/)
crates/core/src/metadata/
payload_merger.rs HoodieMetadataPayload-equivalent record merger