@@ -22,9 +22,13 @@ use std::{
2222 fmt,
2323 fmt:: { Debug , Formatter } ,
2424 fs:: File ,
25+ io:: Cursor ,
2526 sync:: Arc ,
2627} ;
2728
29+ use bytes:: Bytes ;
30+ use url:: Url ;
31+
2832use arrow:: datatypes:: { Schema , SchemaRef } ;
2933use arrow:: record_batch:: RecordBatch ;
3034use async_trait:: async_trait;
@@ -48,6 +52,71 @@ use parquet::{
4852} ;
4953
5054use crate :: execution:: shuffle:: CompressionCodec ;
55+ use crate :: parquet:: parquet_support:: write_to_hdfs_with_opendal_async;
56+
57+ /// Enum representing different types of Arrow writers based on storage backend
58+ enum ParquetWriter {
59+ /// Writer for local file system
60+ LocalFile ( ArrowWriter < File > ) ,
61+ /// Writer for HDFS or other remote storage (writes to in-memory buffer)
62+ /// Contains the writer and the destination HDFS path
63+ Remote ( ArrowWriter < Cursor < Vec < u8 > > > , String ) ,
64+ }
65+
66+ impl ParquetWriter {
67+ /// Write a RecordBatch to the underlying writer
68+ async fn write ( & mut self , batch : & RecordBatch ) -> std:: result:: Result < ( ) , parquet:: errors:: ParquetError > {
69+ match self {
70+ ParquetWriter :: LocalFile ( writer) => writer. write ( batch) ,
71+ ParquetWriter :: Remote ( writer, output_path) => {
72+ // Write batch to in-memory buffer
73+ writer. write ( batch) ?;
74+
75+ // Flush and get the current buffer content
76+ writer. flush ( ) ?;
77+ let cursor = writer. inner_mut ( ) ;
78+ let buffer = cursor. get_ref ( ) . clone ( ) ;
79+
80+ // Upload
81+ let url = Url :: parse ( output_path) . map_err ( |e| {
82+ parquet:: errors:: ParquetError :: General ( format ! (
83+ "Failed to parse URL '{}': {}" ,
84+ output_path, e
85+ ) )
86+ } ) ?;
87+
88+ write_to_hdfs_with_opendal_async ( & url, Bytes :: from ( buffer) )
89+ . await
90+ . map_err ( |e| {
91+ parquet:: errors:: ParquetError :: General ( format ! (
92+ "Failed to upload to '{}': {}" ,
93+ output_path, e
94+ ) )
95+ } ) ?;
96+
97+ // Clear the buffer after upload
98+ cursor. get_mut ( ) . clear ( ) ;
99+ cursor. set_position ( 0 ) ;
100+
101+ Ok ( ( ) )
102+ } ,
103+ }
104+ }
105+
106+ /// Close the writer and return the buffer for remote writers
107+ fn close ( self ) -> std:: result:: Result < Option < ( Vec < u8 > , String ) > , parquet:: errors:: ParquetError > {
108+ match self {
109+ ParquetWriter :: LocalFile ( writer) => {
110+ writer. close ( ) ?;
111+ Ok ( None )
112+ }
113+ ParquetWriter :: Remote ( writer, path) => {
114+ let cursor = writer. into_inner ( ) ?;
115+ Ok ( Some ( ( cursor. into_inner ( ) , path) ) )
116+ }
117+ }
118+ }
119+ }
51120
52121/// Parquet writer operator that writes input batches to a Parquet file
53122#[ derive( Debug ) ]
@@ -119,6 +188,59 @@ impl ParquetWriterExec {
119188 CompressionCodec :: Snappy => Ok ( Compression :: SNAPPY ) ,
120189 }
121190 }
191+
192+ /// Create an Arrow writer based on the storage scheme
193+ ///
194+ /// # Arguments
195+ /// * `storage_scheme` - The storage backend ("hdfs", "s3", or "local")
196+ /// * `output_file_path` - The full path to the output file
197+ /// * `schema` - The Arrow schema for the Parquet file
198+ /// * `props` - Writer properties including compression
199+ ///
200+ /// # Returns
201+ /// * `Ok(ParquetWriter)` - A writer appropriate for the storage scheme
202+ /// * `Err(DataFusionError)` - If writer creation fails
203+ fn create_arrow_writer (
204+ storage_scheme : & str ,
205+ output_file_path : & str ,
206+ schema : SchemaRef ,
207+ props : WriterProperties ,
208+ ) -> Result < ParquetWriter > {
209+ match storage_scheme {
210+ "hdfs" | "s3" => {
211+ // For remote storage (HDFS, S3), write to an in-memory buffer
212+ let buffer = Vec :: new ( ) ;
213+ let cursor = Cursor :: new ( buffer) ;
214+ let writer = ArrowWriter :: try_new ( cursor, schema, Some ( props) )
215+ . map_err ( |e| DataFusionError :: Execution ( format ! (
216+ "Failed to create {} writer: {}" , storage_scheme, e
217+ ) ) ) ?;
218+ Ok ( ParquetWriter :: Remote ( writer, output_file_path. to_string ( ) ) )
219+ }
220+ "local" => {
221+ // For local file system, write directly to file
222+ // Strip file:// or file: prefix if present
223+ let local_path = output_file_path
224+ . strip_prefix ( "file://" )
225+ . or_else ( || output_file_path. strip_prefix ( "file:" ) )
226+ . unwrap_or ( output_file_path) ;
227+
228+ let file = File :: create ( local_path) . map_err ( |e| {
229+ DataFusionError :: Execution ( format ! (
230+ "Failed to create output file '{}': {}" ,
231+ local_path, e
232+ ) )
233+ } ) ?;
234+
235+ let writer = ArrowWriter :: try_new ( file, schema, Some ( props) )
236+ . map_err ( |e| DataFusionError :: Execution ( format ! ( "Failed to create local file writer: {}" , e) ) ) ?;
237+ Ok ( ParquetWriter :: LocalFile ( writer) )
238+ }
239+ _ => Err ( DataFusionError :: Execution ( format ! (
240+ "Unsupported storage scheme: {}" , storage_scheme
241+ ) ) ) ,
242+ }
243+ }
122244}
123245
124246impl DisplayAs for ParquetWriterExec {
@@ -217,6 +339,15 @@ impl ExecutionPlan for ParquetWriterExec {
217339 . collect ( ) ;
218340 let output_schema = Arc :: new ( arrow:: datatypes:: Schema :: new ( fields) ) ;
219341
342+ // Determine storage scheme from work_dir
343+ let storage_scheme = if work_dir. starts_with ( "hdfs://" ) {
344+ "hdfs"
345+ } else if work_dir. starts_with ( "s3://" ) || work_dir. starts_with ( "s3a://" ) {
346+ "s3"
347+ } else {
348+ "local"
349+ } ;
350+
220351 // Strip file:// or file: prefix if present
221352 let local_path = work_dir
222353 . strip_prefix ( "file://" )
@@ -243,21 +374,12 @@ impl ExecutionPlan for ParquetWriterExec {
243374 format ! ( "{}/part-{:05}.parquet" , local_path, self . partition_id)
244375 } ;
245376
246- // Create the Parquet file
247- let file = File :: create ( & part_file) . map_err ( |e| {
248- DataFusionError :: Execution ( format ! (
249- "Failed to create output file '{}': {}" ,
250- part_file, e
251- ) )
252- } ) ?;
253-
254377 // Configure writer properties
255378 let props = WriterProperties :: builder ( )
256379 . set_compression ( compression)
257380 . build ( ) ;
258381
259- let mut writer = ArrowWriter :: try_new ( file, Arc :: clone ( & output_schema) , Some ( props) )
260- . map_err ( |e| DataFusionError :: Execution ( format ! ( "Failed to create writer: {}" , e) ) ) ?;
382+ let mut writer = Self :: create_arrow_writer ( storage_scheme, & part_file, Arc :: clone ( & output_schema) , props) ?;
261383
262384 // Clone schema for use in async closure
263385 let schema_for_write = Arc :: clone ( & output_schema) ;
@@ -286,7 +408,7 @@ impl ExecutionPlan for ParquetWriterExec {
286408 batch
287409 } ;
288410
289- writer. write ( & renamed_batch) . map_err ( |e| {
411+ writer. write ( & renamed_batch) . await . map_err ( |e| {
290412 DataFusionError :: Execution ( format ! ( "Failed to write batch: {}" , e) )
291413 } ) ?;
292414 }
0 commit comments