vortex_datafusion/persistent/
sink.rs1use std::sync::Arc;
5
6use arrow_schema::SchemaRef;
7use async_trait::async_trait;
8use datafusion_common::DataFusionError;
9use datafusion_common::Result as DFResult;
10use datafusion_common::exec_datafusion_err;
11use datafusion_common_runtime::JoinSet;
12use datafusion_common_runtime::SpawnedTask;
13use datafusion_datasource::display::FileGroupDisplay;
14use datafusion_datasource::file_sink_config::FileSink;
15use datafusion_datasource::file_sink_config::FileSinkConfig;
16use datafusion_datasource::sink::DataSink;
17use datafusion_datasource::write::demux::DemuxedStreamReceiver;
18use datafusion_datasource::write::get_writer_schema;
19use datafusion_execution::SendableRecordBatchStream;
20use datafusion_execution::TaskContext;
21use datafusion_physical_plan::DisplayAs;
22use datafusion_physical_plan::DisplayFormatType;
23use datafusion_physical_plan::metrics::MetricsSet;
24use futures::StreamExt;
25use object_store::ObjectStore;
26use object_store::path::Path;
27use tokio_stream::wrappers::ReceiverStream;
28use vortex::array::arrow::ArrowSessionExt;
29use vortex::array::stream::ArrayStreamAdapter;
30use vortex::file::WriteOptionsSessionExt;
31use vortex::file::WriteSummary;
32use vortex::io::VortexWrite;
33use vortex::io::object_store::ObjectStoreWrite;
34use vortex::session::VortexSession;
35
36pub struct VortexSink {
38 config: FileSinkConfig,
39 schema: SchemaRef,
40 session: VortexSession,
41}
42
43impl VortexSink {
44 pub fn new(config: FileSinkConfig, schema: SchemaRef, session: VortexSession) -> Self {
46 Self {
47 config,
48 schema,
49 session,
50 }
51 }
52}
53
54impl std::fmt::Debug for VortexSink {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("VortexSink").finish()
57 }
58}
59
60impl DisplayAs for VortexSink {
61 fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match t {
63 DisplayFormatType::Default | DisplayFormatType::Verbose => {
64 write!(f, "VortexSink(file_groups=")?;
65 FileGroupDisplay(&self.config.file_group).fmt_as(t, f)?;
66 write!(f, ")")
67 }
68 DisplayFormatType::TreeRender => {
69 write!(f, "VortexSink")
70 }
71 }
72 }
73}
74
75#[async_trait]
76impl DataSink for VortexSink {
77 fn metrics(&self) -> Option<MetricsSet> {
78 None
79 }
80
81 fn schema(&self) -> &SchemaRef {
83 &self.schema
84 }
85
86 async fn write_all(
87 &self,
88 data: SendableRecordBatchStream,
89 context: &Arc<TaskContext>,
90 ) -> DFResult<u64> {
91 FileSink::write_all(self, data, context).await
92 }
93}
94
95#[async_trait]
96impl FileSink for VortexSink {
97 fn config(&self) -> &FileSinkConfig {
98 &self.config
99 }
100
101 async fn spawn_writer_tasks_and_join(
102 &self,
103 _context: &Arc<TaskContext>,
104 demux_task: SpawnedTask<DFResult<()>>,
105 mut file_stream_rx: DemuxedStreamReceiver,
106 object_store: Arc<dyn ObjectStore>,
107 ) -> DFResult<u64> {
108 let mut file_write_tasks: JoinSet<DFResult<(Path, WriteSummary)>> = JoinSet::new();
109 let writer_schema = get_writer_schema(&self.config);
110 let dtype = self
111 .session
112 .arrow()
113 .from_arrow_schema(&writer_schema)
114 .map_err(|e| {
115 exec_datafusion_err!("Failed to derive Vortex DType from writer schema: {e}")
116 })?;
117
118 while let Some((path, rx)) = file_stream_rx.recv().await {
121 let session = self.session.clone();
122 let object_store = Arc::clone(&object_store);
123
124 let arrow_session = session.clone();
127 let import_schema = Arc::clone(&writer_schema);
128 let dtype = dtype.clone();
129 file_write_tasks.spawn(async move {
130 let stream = ReceiverStream::new(rx).map(move |rb| {
131 arrow_session
132 .arrow()
133 .from_arrow_record_batch(rb, &import_schema)
134 });
135
136 let stream_adapter = ArrayStreamAdapter::new(dtype, stream);
137
138 let mut object_writer = ObjectStoreWrite::new(object_store, &path)
139 .await
140 .map_err(|e| exec_datafusion_err!("Failed to create ObjectStoreWrite: {e}"))?;
141
142 let summary = session
143 .write_options()
144 .write(&mut object_writer, stream_adapter)
145 .await
146 .map_err(|e| exec_datafusion_err!("Failed to write Vortex file: {e}"))?;
147
148 object_writer
149 .shutdown()
150 .await
151 .map_err(|e| exec_datafusion_err!("Failed to shutdown Vortex writer: {e}"))?;
152
153 Ok((path, summary))
154 });
155 }
156
157 let mut row_count = 0;
158
159 while let Some(result) = file_write_tasks.join_next().await {
160 match result {
161 Ok(r) => {
162 let (path, summary) = r?;
163
164 row_count += summary.row_count();
165
166 tracing::info!(path = %path, "Successfully written file");
167 }
168 Err(e) => {
169 if e.is_panic() {
170 std::panic::resume_unwind(e.into_panic());
171 } else {
172 unreachable!();
173 }
174 }
175 }
176 }
177
178 demux_task
179 .join_unwind()
180 .await
181 .map_err(|e| DataFusionError::ExecutionJoin(Box::new(e)))??;
182
183 Ok(row_count)
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use std::sync::Arc;
190
191 use arrow_schema::DataType;
192 use arrow_schema::Field;
193 use arrow_schema::Schema;
194 use datafusion::arrow::array::Int8Array;
195 use datafusion::arrow::array::Int64Array;
196 use datafusion::arrow::array::RecordBatch;
197 use datafusion::assert_batches_sorted_eq;
198 use datafusion::datasource::DefaultTableSource;
199 use datafusion::logical_expr::Expr;
200 use datafusion::logical_expr::LogicalPlan;
201 use datafusion::logical_expr::LogicalPlanBuilder;
202 use datafusion::logical_expr::Values;
203 use datafusion::logical_expr::dml::InsertOp;
204 use datafusion_common::ScalarValue;
205 use datafusion_datasource::TableSchema;
206 use datafusion_datasource::file_format::format_as_file_type;
207 use datafusion_datasource::file_groups::FileGroup;
208 use datafusion_datasource::file_sink_config::FileOutputMode;
209 use datafusion_datasource::sink::DataSinkExec;
210 use datafusion_execution::object_store::ObjectStoreUrl;
211 use datafusion_physical_plan::DefaultDisplay;
212 use datafusion_physical_plan::VerboseDisplay;
213 use datafusion_physical_plan::display::DisplayableExecutionPlan;
214 use datafusion_physical_plan::empty::EmptyExec;
215 use futures::TryStreamExt;
216 use rstest::rstest;
217 use vortex::file::VORTEX_FILE_EXTENSION;
218 use vortex::session::VortexSession;
219
220 use super::*;
221 use crate::common_tests::TestSessionContext;
222 use crate::persistent::VortexFormatFactory;
223
224 #[tokio::test]
225 async fn test_insert_into_sql() -> anyhow::Result<()> {
226 let ctx = TestSessionContext::default();
227
228 ctx.session
229 .sql(
230 "CREATE EXTERNAL TABLE my_tbl \
231 (c1 VARCHAR NOT NULL, c2 INT NOT NULL) \
232 STORED AS vortex \
233 LOCATION 'table/';",
234 )
235 .await?;
236
237 ctx.session
238 .sql("INSERT INTO my_tbl VALUES ('hello', 1), ('world', 2);")
239 .await?
240 .collect()
241 .await?;
242
243 let batches = ctx
244 .session
245 .sql("SELECT * from my_tbl")
246 .await?
247 .collect()
248 .await?;
249
250 assert_batches_sorted_eq!(
251 &[
252 "+-------+----+",
253 "| c1 | c2 |",
254 "+-------+----+",
255 "| hello | 1 |",
256 "| world | 2 |",
257 "+-------+----+",
258 ],
259 &batches
260 );
261
262 Ok(())
263 }
264
265 #[tokio::test]
266 async fn test_insert_into_logical_plan() -> anyhow::Result<()> {
267 let ctx = TestSessionContext::default();
268
269 ctx.session
270 .sql(
271 "CREATE EXTERNAL TABLE my_tbl \
272 (c1 VARCHAR NOT NULL, c2 INT NOT NULL) \
273 STORED AS vortex \
274 LOCATION 'table/';",
275 )
276 .await?;
277
278 let my_tbl = ctx.session.table("my_tbl").await?;
279
280 let values = Values {
282 schema: Arc::new(my_tbl.schema().clone()),
283 values: vec![vec![
284 Expr::Literal(ScalarValue::new_utf8view("hello"), None),
285 Expr::Literal(42_i32.into(), None),
286 ]],
287 };
288
289 let tbl_provider = ctx.session.table_provider("my_tbl").await?;
290
291 let logical_plan = LogicalPlanBuilder::insert_into(
292 LogicalPlan::Values(values.clone()),
293 "my_tbl",
294 Arc::new(DefaultTableSource::new(Arc::clone(&tbl_provider))),
295 InsertOp::Append,
296 )?
297 .build()?;
298
299 ctx.session
300 .execute_logical_plan(logical_plan)
301 .await?
302 .collect()
303 .await?;
304
305 let batches = ctx.session.read_table(tbl_provider)?.collect().await?;
306
307 assert_batches_sorted_eq!(
308 [
309 "+-------+----+",
310 "| c1 | c2 |",
311 "+-------+----+",
312 "| hello | 42 |",
313 "+-------+----+",
314 ],
315 &batches
316 );
317
318 Ok(())
319 }
320
321 #[rstest]
323 #[case(1000, 1)]
324 #[case(40_961, 4)]
325 #[case(1_000_000, 4)]
326 #[tokio::test]
327 async fn test_write_large_batch(
328 #[case] entries: usize,
329 #[case] expected_files: usize,
330 ) -> anyhow::Result<()> {
331 let ctx = TestSessionContext::default();
332
333 let data = ctx.session.read_batch(RecordBatch::try_new(
334 Arc::new(Schema::new(vec![Field::new("a", DataType::Int8, false)])),
335 vec![Arc::new(Int8Array::from(vec![0i8; entries]))],
336 )?)?;
337
338 let logical_plan = LogicalPlanBuilder::copy_to(
339 data.logical_plan().clone(),
340 "/table/".to_string(),
341 format_as_file_type(Arc::new(VortexFormatFactory::new())),
342 Default::default(),
343 vec![],
344 )?
345 .build()?;
346
347 ctx.session
348 .execute_logical_plan(logical_plan)
349 .await?
350 .collect()
351 .await?;
352
353 let result = ctx
354 .session
355 .sql("SELECT COUNT(*) as count FROM '/table/'")
356 .await?
357 .collect()
358 .await?;
359
360 assert_eq!(result.len(), 1);
361 let count_batch = &result[0];
362 assert_eq!(count_batch.num_rows(), 1);
363
364 let count_value = count_batch
365 .column(0)
366 .as_any()
367 .downcast_ref::<Int64Array>()
368 .unwrap()
369 .value(0);
370
371 assert_eq!(
372 count_value, entries as i64,
373 "Expected {} entries, but found {}",
374 entries, count_value
375 );
376
377 let all_data = ctx
378 .session
379 .sql("SELECT a FROM '/table/'")
380 .await?
381 .collect()
382 .await?;
383
384 let mut total_rows = 0;
385 for batch in all_data {
386 let col = batch
387 .column(0)
388 .as_any()
389 .downcast_ref::<Int8Array>()
390 .unwrap();
391
392 for i in 0..batch.num_rows() {
393 assert_eq!(
394 col.value(i),
395 0i8,
396 "Expected value 0 at row {}, but found {}",
397 total_rows + i,
398 col.value(i)
399 );
400 }
401 total_rows += batch.num_rows();
402 }
403
404 assert_eq!(
405 total_rows, entries,
406 "Total rows read ({}) doesn't match expected entries ({})",
407 total_rows, entries
408 );
409
410 let file_metas = ctx
411 .store
412 .list(Some(&"/table".into()))
413 .try_collect::<Vec<_>>()
414 .await?;
415
416 assert_eq!(
417 file_metas.len(),
418 expected_files,
419 "Expected {expected_files} files for {entries} values"
420 );
421
422 Ok(())
423 }
424
425 #[tokio::test]
426 async fn test_write_partitioned() -> anyhow::Result<()> {
427 let ctx = TestSessionContext::default();
428
429 let _unused = ctx
430 .session
431 .sql(
432 "CREATE EXTERNAL TABLE my_tbl \
433 (c1 VARCHAR NOT NULL, c2 INT NOT NULL) \
434 STORED AS vortex \
435 LOCATION 'table/' \
436 PARTITIONED BY (c1);",
437 )
438 .await?;
439
440 ctx.session
441 .sql("INSERT INTO my_tbl (c1, c2) VALUES ('world', 24), ('world', 25), ('hello', 42);")
442 .await?
443 .collect()
444 .await?;
445
446 let table = ctx.session.table("my_tbl").await?;
447 assert_eq!(table.count().await?, 3);
448
449 let location = Path::parse("table/")?;
450 let file_metas = ctx
451 .store
452 .list(Some(&location))
453 .try_collect::<Vec<_>>()
454 .await?;
455
456 for meta in file_metas.into_iter() {
457 let location = meta.location;
458 assert!(
459 location.prefix_matches(&"c1=hello".into())
460 || location.prefix_matches(&"c1=world".into())
461 );
462 }
463
464 Ok(())
465 }
466
467 #[test]
468 fn test_display_as() {
469 let session = VortexSession::empty();
470 let table_schema = TableSchema::new(Arc::new(Schema::empty()), Vec::new());
471
472 let config = FileSinkConfig {
473 original_url: "".to_owned(),
474 object_store_url: ObjectStoreUrl::local_filesystem(),
475 file_group: FileGroup::new(Vec::new()),
476 table_paths: Vec::new(),
477 output_schema: Arc::new(Schema::empty()),
478 table_partition_cols: Vec::new(),
479 insert_op: InsertOp::Overwrite,
480 keep_partition_by_columns: false,
481 file_extension: VORTEX_FILE_EXTENSION.to_owned(),
482 file_output_mode: FileOutputMode::SingleFile,
483 };
484
485 let get_sink = || VortexSink {
486 config: config.clone(),
487 schema: Arc::clone(table_schema.file_schema()),
488 session: session.clone(),
489 };
490
491 insta::assert_snapshot!(DefaultDisplay(get_sink()).to_string(), @"VortexSink(file_groups=[])");
492 insta::assert_snapshot!(VerboseDisplay(get_sink()).to_string(), @"VortexSink(file_groups=[])");
493
494 let plan = DataSinkExec::new(
495 Arc::new(EmptyExec::new(Arc::new(Schema::empty()))),
496 Arc::new(get_sink()),
497 None,
498 );
499
500 insta::assert_snapshot!(DisplayableExecutionPlan::new(&plan).tree_render().to_string(), @r"
501 ┌───────────────────────────┐
502 │ DataSinkExec │
503 │ -------------------- │
504 │ VortexSink │
505 └─────────────┬─────────────┘
506 ┌─────────────┴─────────────┐
507 │ EmptyExec │
508 └───────────────────────────┘
509 ");
510 }
511}