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

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 4cd8be954f docs(parquet): add OpenDAL read and write example (#11014)
4cd8be954f is described below

commit 4cd8be954f6bc6b6dd265140207365b59a9900ec
Author: Xuanwo <[email protected]>
AuthorDate: Sat Sep 12 04:47:41 2026 +0800

    docs(parquet): add OpenDAL read and write example (#11014)
    
    # Which issue does this PR close?
    
    Related to #10308.
    
    # Rationale for this change
    
    Demonstrate how to use OpenDAL with Parquet's generic async I/O
    interfaces, alongside the existing object_store example.
    
    # What changes are included in this PR?
    
    Add an in-memory read/write example with AsyncFileReader and
    AsyncFileWriter adapters and a SpawnedReader demonstration. Point
    application developers to parquet_opendal for ready-made adapters.
    OpenDAL 0.59 is a development dependency with only the memory service
    enabled.
    
    # Are these changes tested?
    
    The example asserts that both ordinary and dedicated-runtime reads
    reproduce the written RecordBatch. Formatting and whitespace checks
    pass.
    
    # Are there any user-facing changes?
    
    A new example, runnable with `cargo run -p parquet --example opendal
    --features async`. No public API changes. OpenDAL 0.59 requires Rust
    1.91, above this workspace's declared Rust 1.88 MSRV; this affects
    development targets that resolve/build the new dependency.
---
 Cargo.lock                  | 174 +++++++++++++++++++++++++++++++++++++++++---
 parquet/Cargo.toml          |   6 ++
 parquet/examples/opendal.rs | 155 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 326 insertions(+), 9 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 4573d81078..358062a672 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -512,7 +512,7 @@ dependencies = [
 name = "arrow-schema"
 version = "60.0.0"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
  "criterion",
  "insta",
  "postcard",
@@ -598,6 +598,16 @@ dependencies = [
  "syn 3.0.5",
 ]
 
+[[package]]
+name = "asyncband"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "94a214ba60d6231afd0e805e3c27c45a1626d9debaa5a5061c45a1ea1b2f1ed0"
+dependencies = [
+ "hashbrown 0.17.1",
+ "slab",
+]
+
 [[package]]
 name = "atoi"
 version = "3.1.0"
@@ -711,6 +721,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
 [[package]]
 name = "bitflags"
 version = "2.13.1"
@@ -1176,6 +1192,37 @@ dependencies = [
  "syn 2.0.119",
 ]
 
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.19",
+]
+
 [[package]]
 name = "derive_arbitrary"
 version = "1.4.2"
@@ -1285,7 +1332,7 @@ version = "25.12.19"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
  "rustc_version",
 ]
 
@@ -1880,6 +1927,61 @@ version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
 
+[[package]]
+name = "jiff"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "js-sys",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
 [[package]]
 name = "jni"
 version = "0.22.4"
@@ -2108,6 +2210,16 @@ version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
 
+[[package]]
+name = "md-5"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
 [[package]]
 name = "md5"
 version = "0.8.1"
@@ -2168,7 +2280,7 @@ version = "0.31.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
  "cfg-if",
  "cfg_aliases",
  "libc",
@@ -2269,7 +2381,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
 ]
 
 [[package]]
@@ -2341,6 +2453,40 @@ version = "11.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
 
+[[package]]
+name = "opendal"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f950151f9587a51a7bed70a15fa0cff464eae96e41ae7499f97067bdafdf43eb"
+dependencies = [
+ "opendal-core",
+]
+
+[[package]]
+name = "opendal-core"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "a43405d217dfdfb543f58847336d3af672897dd1939bb7dcf314b63cf364f1c9"
+dependencies = [
+ "anyhow",
+ "asyncband",
+ "base64 0.23.1",
+ "bytes",
+ "futures",
+ "http",
+ "jiff",
+ "log",
+ "md-5",
+ "percent-encoding",
+ "quick-xml",
+ "serde",
+ "serde_json",
+ "tokio",
+ "url",
+ "uuid",
+ "web-time",
+]
+
 [[package]]
 name = "openssl-probe"
 version = "0.2.1"
@@ -2435,6 +2581,7 @@ dependencies = [
  "num-integer",
  "num-traits",
  "object_store",
+ "opendal",
  "parquet-geospatial",
  "parquet-variant",
  "parquet-variant-compute",
@@ -2602,6 +2749,15 @@ version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
 
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
 [[package]]
 name = "postcard"
 version = "1.1.3"
@@ -2706,7 +2862,7 @@ version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
  "num-traits",
  "rand 0.9.5",
  "rand_chacha",
@@ -2987,7 +3143,7 @@ version = "0.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
 ]
 
 [[package]]
@@ -3099,7 +3255,7 @@ version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -3225,7 +3381,7 @@ version = "3.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
  "core-foundation",
  "core-foundation-sys",
  "libc",
@@ -3825,7 +3981,7 @@ version = "0.6.11"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
 dependencies = [
- "bitflags",
+ "bitflags 2.13.1",
  "bytes",
  "futures-util",
  "http",
diff --git a/parquet/Cargo.toml b/parquet/Cargo.toml
index e4d4855d99..d918be65d8 100644
--- a/parquet/Cargo.toml
+++ b/parquet/Cargo.toml
@@ -91,6 +91,7 @@ arrow-cast = { workspace = true }
 tokio = { version = "1.0", default-features = false, features = ["macros", 
"rt-multi-thread", "io-util", "fs", "sync"] }
 rand = { version = "0.10", default-features = false, features = ["std", 
"std_rng", "thread_rng"] }
 object_store = { workspace = true, features = ["azure", "fs"] }
+opendal = { version = "0.59.1", default-features = false, features = 
["services-memory"] }
 sysinfo = { version = "0.39.6", default-features = false, features = 
["system"] }
 
 [package.metadata.docs.rs]
@@ -163,6 +164,11 @@ name = "object_store"
 required-features = ["arrow", "async"]
 path = "./examples/object_store.rs"
 
+[[example]]
+name = "opendal"
+required-features = ["arrow", "async"]
+path = "./examples/opendal.rs"
+
 [[example]]
 name = "custom_page_index"
 required-features = ["arrow"]
diff --git a/parquet/examples/opendal.rs b/parquet/examples/opendal.rs
new file mode 100644
index 0000000000..0006c1d2ee
--- /dev/null
+++ b/parquet/examples/opendal.rs
@@ -0,0 +1,155 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Read and write Parquet with Apache OpenDAL using an in-memory service.
+//!
+//! Run with `cargo run -p parquet --example opendal --features async`.
+//! Replace the Memory service with another OpenDAL service to use remote 
storage.
+//! The adapters use OpenDAL's native buffer APIs for range reads and writes.
+//!
+//! Applications can also use the [`parquet_opendal`] crate, which provides
+//! ready-made `AsyncReader` and `AsyncWriter` adapters to avoid writing this 
glue code.
+//!
+//! [`parquet_opendal`]: https://docs.rs/parquet_opendal
+
+use arrow_array::{ArrayRef, Int64Array, RecordBatch};
+use bytes::Bytes;
+use futures::future::BoxFuture;
+use futures::{FutureExt, TryStreamExt};
+use opendal::{Operator, Reader, Writer, services::Memory};
+use parquet::arrow::arrow_reader::ArrowReaderOptions;
+use parquet::arrow::async_reader::{AsyncFileReader, SpawnedReader};
+use parquet::arrow::async_writer::AsyncFileWriter;
+use parquet::arrow::{AsyncArrowWriter, ParquetRecordBatchStreamBuilder};
+use parquet::errors::{ParquetError, Result};
+use parquet::file::metadata::{ParquetMetaData, ParquetMetaDataReader};
+use std::ops::Range;
+use std::sync::Arc;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let operator = Operator::new(Memory::default()).map_err(to_parquet_err)?;
+    let path = "example.parquet";
+    let col = Arc::new(Int64Array::from_iter_values([1, 2, 3])) as ArrayRef;
+    let batch = RecordBatch::try_from_iter([("col", col)]).unwrap();
+
+    // Closing the Parquet writer also closes the OpenDAL writer, committing 
the object.
+    let writer = 
OpenDalWriter(operator.writer(path).await.map_err(to_parquet_err)?);
+    let mut writer = AsyncArrowWriter::try_new(writer, batch.schema(), None)?;
+    writer.write(&batch).await?;
+    writer.close().await?;
+
+    let reader = OpenDalReader::new(&operator, path).await?;
+    let builder = ParquetRecordBatchStreamBuilder::new(reader.clone()).await?;
+    let read: Vec<RecordBatch> = builder.build()?.try_collect().await?;
+    assert_eq!(read, vec![batch.clone()]);
+    println!("read {} rows", read[0].num_rows());
+
+    // The same adapter can perform I/O on a runtime separate from Parquet 
decoding.
+    let io_runtime = tokio::runtime::Builder::new_multi_thread()
+        .worker_threads(1)
+        .enable_all()
+        .build()
+        .expect("failed to build I/O runtime");
+    let reader = SpawnedReader::new(reader, io_runtime.handle().clone());
+    let builder = ParquetRecordBatchStreamBuilder::new(reader).await?;
+    let read: Vec<RecordBatch> = builder.build()?.try_collect().await?;
+    assert_eq!(read, vec![batch]);
+    println!("read {} rows via dedicated I/O runtime", read[0].num_rows());
+    io_runtime.shutdown_background();
+    Ok(())
+}
+
+fn to_parquet_err(error: opendal::Error) -> ParquetError {
+    ParquetError::External(Box::new(error))
+}
+
+/// Reads byte ranges through OpenDAL and uses the object size to locate the 
footer.
+#[derive(Clone)]
+struct OpenDalReader {
+    reader: Reader,
+    file_size: u64,
+}
+
+impl OpenDalReader {
+    async fn new(operator: &Operator, path: &str) -> Result<Self> {
+        let file_size = operator
+            .stat(path)
+            .await
+            .map_err(to_parquet_err)?
+            .content_length();
+        let reader = operator.reader(path).await.map_err(to_parquet_err)?;
+        Ok(Self { reader, file_size })
+    }
+}
+
+impl AsyncFileReader for OpenDalReader {
+    fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> 
{
+        async move {
+            self.reader
+                .read(range)
+                .await
+                .map(|buffer| buffer.to_bytes())
+                .map_err(to_parquet_err)
+        }
+        .boxed()
+    }
+
+    fn get_byte_ranges(&mut self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, 
Result<Vec<Bytes>>> {
+        async move {
+            self.reader
+                .fetch(ranges)
+                .await
+                .map(|buffers| {
+                    buffers
+                        .into_iter()
+                        .map(|buffer| buffer.to_bytes())
+                        .collect()
+                })
+                .map_err(to_parquet_err)
+        }
+        .boxed()
+    }
+
+    fn get_metadata<'a>(
+        &'a mut self,
+        options: Option<&'a ArrowReaderOptions>,
+    ) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
+        async move {
+            let file_size = self.file_size;
+            let metadata = ParquetMetaDataReader::new()
+                .with_arrow_reader_options(options)
+                .load_and_finish(self, file_size)
+                .await?;
+            Ok(Arc::new(metadata))
+        }
+        .boxed()
+    }
+}
+
+/// Passes owned bytes directly to OpenDAL without an AsyncWrite compatibility 
layer.
+struct OpenDalWriter(Writer);
+
+impl AsyncFileWriter for OpenDalWriter {
+    fn write(&mut self, bytes: Bytes) -> BoxFuture<'_, Result<()>> {
+        async move { self.0.write(bytes).await.map_err(to_parquet_err) 
}.boxed()
+    }
+
+    fn complete(&mut self) -> BoxFuture<'_, Result<()>> {
+        async move { self.0.close().await.map(|_| ()).map_err(to_parquet_err) 
}.boxed()
+    }
+}

Reply via email to