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 53a403c8 chore: let consumers opt out of the merge map's on-disk tier
(#681)
53a403c8 is described below
commit 53a403c8ce7d4f5ee5d1931e220be431501d27e9
Author: Lin Liu <[email protected]>
AuthorDate: Tue Sep 1 18:39:35 2026 -0700
chore: let consumers opt out of the merge map's on-disk tier (#681)
---
.github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++
AGENTS.md | 24 ++++++++++++++------
crates/core/src/schema/delete.rs | 16 ++++++++++++++
crates/datafusion/Cargo.toml | 12 +++++++++-
crates/hudi/Cargo.toml | 15 +++++++++++--
5 files changed, 104 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2b951027..44d0ec67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -105,6 +105,53 @@ jobs:
run: cargo clippy -p hudi-core --lib --no-default-features -- -D
warnings
- name: Unit tests without the spill backend
run: cargo test -p hudi-core --lib --no-default-features --no-fail-fast
+ # The umbrella crate is where a consumer actually opts out, and it
reaches
+ # hudi-core twice: directly, and through hudi-datafusion. Either edge
taking
+ # default features hands the tier back without the steps above noticing,
+ # since they only build hudi-core. Both shapes are built here, where
there
+ # is no libclang to build rocksdb with.
+ - name: Check the umbrella crate without the spill backend
+ run: cargo check -p hudi --no-default-features
+ - name: Check the umbrella crate without the spill backend, with
datafusion
+ run: cargo check -p hudi --no-default-features --features datafusion
+ # The steps above prove the opt-out COMPILES. This proves it actually
drops
+ # the dependency, and unlike them it does not rest on libclang being
absent,
+ # so it keeps working if this job ever gains a C++ toolchain.
+ - name: Assert the opt-out drops rocksdb from the dependency graph
+ run: |
+ # A graph assertion has two ways to look like it passed: the tier
+ # really is gone, or cargo tree never ran. `-i` exits non-zero with
an
+ # empty stdout in BOTH cases, so neither the output nor the status
+ # alone tells them apart, and discarding stderr makes a broken
command
+ # indistinguishable from a working opt-out. Each check below
therefore
+ # pins the specific reason rather than inferring from emptiness.
+
+ # Under defaults the tier must be reachable. If this ever stops
+ # holding, the absence checks below are asserting nothing.
+ for args in "" "--features datafusion"; do
+ if ! out=$(cargo tree -p hudi $args -i rocksdb 2>&1) \
+ || ! printf '%s\n' "$out" | grep -q '^rocksdb'; then
+ echo "cargo tree did not find rocksdb under default features:
'$args'"
+ echo "so the absence checks below cannot tell an opt-out from a
broken command"
+ printf '%s\n' "$out"
+ exit 1
+ fi
+ done
+
+ # With the opt-out it must be gone — and gone because the package is
+ # absent from the graph, which cargo says in those words, not because
+ # the command failed for some other reason.
+ for args in "--no-default-features" "--no-default-features
--features datafusion"; do
+ if out=$(cargo tree -p hudi $args -i rocksdb 2>&1); then
+ echo "rocksdb is still reachable from hudi with: $args"
+ printf '%s\n' "$out"
+ exit 1
+ elif ! printf '%s\n' "$out" | grep -q 'did not match any
packages'; then
+ echo "cargo tree failed for a reason other than rocksdb being
absent: '$args'"
+ printf '%s\n' "$out"
+ exit 1
+ fi
+ done
rust-tests-windows:
runs-on: windows-2025
diff --git a/AGENTS.md b/AGENTS.md
index 881031b3..cdf5f24d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -45,13 +45,23 @@ cargo test -p hudi-core --lib --no-default-features
### Features
-`hudi-core` has one default-on feature, `spill-rocksdb`, carrying the
merge-on-read merge map's
-on-disk tier. It is separable because `rocksdb` bundles RocksDB and runs
`bindgen`, so leaving it
-unconditional puts libclang and a C++ toolchain in front of every consumer —
the Python wheel and
-the cxx bridge included — for a tier that only engages when a merge exceeds
-`hoodie.memory.merge.max.size`. A test that needs the disk tier must be
-`#[cfg(feature = "spill-rocksdb")]`; the `--no-default-features` CI leg
installs no libclang, so a
-change that reintroduces the native dependency fails to compile there rather
than passing quietly.
+`spill-rocksdb` carries the merge-on-read merge map's on-disk tier. It is
separable because
+`rocksdb` bundles RocksDB and runs `bindgen`, so leaving it unconditional puts
libclang and a C++
+toolchain in front of every consumer, for a tier that only engages when a
merge exceeds
+`hoodie.memory.merge.max.size`.
+
+It is **default-on in `hudi-core`, `hudi-datafusion` and `hudi`**, and the
latter two forward it
+rather than taking `hudi-core` with its defaults: a consumer can only opt out
of a feature its
+direct dependency exposes, and unification through `hudi-datafusion` would
otherwise hand the tier
+back. The opt-out is therefore `default-features = false` on `hudi`, and it
holds with the
+`datafusion` feature on. **The Python wheel and the cxx bridge do not opt
out** — both take `hudi`
+with defaults and build RocksDB. Dropping the feature removes the tier, and a
merge past the budget
+then fails with a `CoreError::Unsupported` naming the config rather than
spilling, which is why the
+published artifacts keep it.
+
+A test that needs the disk tier must be `#[cfg(feature = "spill-rocksdb")]`;
the
+`--no-default-features` CI leg installs no libclang, so a change that
reintroduces the native
+dependency fails to compile there rather than passing quietly.
### Dependencies
diff --git a/crates/core/src/schema/delete.rs b/crates/core/src/schema/delete.rs
index e62a3c79..883bb236 100644
--- a/crates/core/src/schema/delete.rs
+++ b/crates/core/src/schema/delete.rs
@@ -27,6 +27,19 @@ use once_cell::sync::Lazy;
use serde_json::Value as JsonValue;
use std::sync::Arc;
+/// One delete record on its own, as it looks once unwrapped out of the list.
+///
+/// The wire schema is [`DELETE_RECORD_LIST_AVRO_SCHEMA_STR`]
+/// (`HoodieDeleteRecordList.avsc`) — that is what a delete log block is
written
+/// as and what the live decode reads. This file is its post-unwrap companion,
+/// kept as the reference spelling of a single record's shape.
+///
+/// The two helpers over it, [`unwrap_ordering_value`] and
+/// [`avro_schema_for_delete_record`], have no callers beyond this module's own
+/// tests. Their plural namesakes are the live path and are different
functions:
+/// `unwrap_ordering_values` and [`avro_schema_for_delete_record_list`], both
in
+/// `file_group::log_file::content`'s decode. Singular here means one record;
+/// plural means the block.
static DELETE_RECORD_AVRO_SCHEMA_STR: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/schemas/HoodieDeleteRecord.avsc"
@@ -159,6 +172,9 @@ pub fn avro_schema_for_delete_record(delete_record_value:
&AvroValue) -> Result<
AvroSchema::parse(&json).map_err(CoreError::AvroError)
}
+/// The wire schema: what a delete log block is written as, and what the live
+/// decode reads. [`DELETE_RECORD_AVRO_SCHEMA_STR`] is the single-record
+/// companion to this one.
static DELETE_RECORD_LIST_AVRO_SCHEMA_STR: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/schemas/HoodieDeleteRecordList.avsc"
diff --git a/crates/datafusion/Cargo.toml b/crates/datafusion/Cargo.toml
index 6e6bb333..5a9a8b7d 100644
--- a/crates/datafusion/Cargo.toml
+++ b/crates/datafusion/Cargo.toml
@@ -27,8 +27,18 @@ description.workspace = true
homepage.workspace = true
repository.workspace = true
+[features]
+default = ["spill-rocksdb"]
+# Forwarded so a consumer reaching hudi-core through this crate can still drop
+# the merge map's on-disk tier. Taking hudi-core with its defaults here would
+# re-enable that tier through feature unification whatever the umbrella crate
+# asked for.
+spill-rocksdb = ["hudi-core/spill-rocksdb"]
+
[dependencies]
-hudi-core = { version = "0.5.0-dev", path = "../core", features =
["datafusion"] }
+hudi-core = { version = "0.5.0-dev", path = "../core", default-features =
false, features = [
+ "datafusion",
+] }
# arrow
arrow = { workspace = true }
arrow-arith = { workspace = true }
diff --git a/crates/hudi/Cargo.toml b/crates/hudi/Cargo.toml
index 67c4cfd5..77c6a151 100644
--- a/crates/hudi/Cargo.toml
+++ b/crates/hudi/Cargo.toml
@@ -28,10 +28,21 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
-hudi-core = { version = "0.5.0-dev", path = "../core" }
-hudi-datafusion = { version = "0.5.0-dev", path = "../datafusion", optional =
true }
+hudi-core = { version = "0.5.0-dev", path = "../core", default-features =
false }
+hudi-datafusion = { version = "0.5.0-dev", path = "../datafusion", optional =
true, default-features = false }
[features]
+default = ["spill-rocksdb"]
+# On by default, so this crate and everything downstream of it keep the merge
+# map's on-disk tier unless they say otherwise. Forwarded rather than left to
+# hudi-core's own default because a consumer can only opt out of a feature its
+# direct dependency exposes: without this, `default-features = false` here
would
+# still get the tier back through hudi-core. Dropping it removes the tier, and
a
+# merge exceeding `hoodie.memory.merge.max.size` then fails rather than
spilling.
+spill-rocksdb = [
+ "hudi-core/spill-rocksdb",
+ "hudi-datafusion?/spill-rocksdb",
+]
datafusion = [
"hudi-datafusion",
]