Skip to main content

vortex_datafusion/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Integrations between [`Vortex`] and [DataFusion].
5//!
6//! The crate exposes two main entry points:
7//!
8//! - [`VortexFormatFactory`] for the file-based integration used by SQL,
9//!   `CREATE EXTERNAL TABLE`, and
10//!   [`ListingTable`].
11//! - [`v2`] for direct integration from an existing Vortex
12//!   [`DataSourceRef`].
13//!
14//! # Registering The File Format
15//!
16//! Most applications register [`VortexFormatFactory`] with a DataFusion
17//! [`SessionContext`] and then let DataFusion create [`VortexFormat`] and
18//! [`VortexSource`] instances as queries are planned:
19//!
20//! ```no_run
21//! use std::sync::Arc;
22//!
23//! use datafusion::datasource::provider::DefaultTableFactory;
24//! use datafusion::execution::SessionStateBuilder;
25//! use datafusion::prelude::SessionContext;
26//! use datafusion_common::GetExt;
27//! use vortex_datafusion::VortexFormatFactory;
28//!
29//! # #[tokio::main]
30//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! let factory = Arc::new(VortexFormatFactory::new());
32//! let mut state_builder = SessionStateBuilder::new()
33//!     .with_default_features()
34//!     .with_table_factory(
35//!         factory.get_ext().to_uppercase(),
36//!         Arc::new(DefaultTableFactory::new()),
37//!     );
38//!
39//! if let Some(file_formats) = state_builder.file_formats() {
40//!     file_formats.push(factory.clone() as _);
41//! }
42//!
43//! let ctx = SessionContext::new_with_state(state_builder.build()).enable_url_table();
44//! ctx.sql(
45//!     "CREATE EXTERNAL TABLE metrics (service VARCHAR, value BIGINT) \
46//!      STORED AS vortex LOCATION 'file:///tmp/metrics/'",
47//! )
48//! .await?;
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! # Registering An Existing Vortex Data Source
54//!
55//! If your application already has a Vortex [`DataSourceRef`], use
56//! [`v2::VortexTable`] to register it directly with DataFusion:
57//!
58//! ```no_run
59//! use std::sync::Arc;
60//!
61//! use arrow_schema::Schema;
62//! use datafusion::prelude::SessionContext;
63//! use vortex::VortexSessionDefault;
64//! use vortex::scan::DataSourceRef;
65//! use vortex::session::VortexSession;
66//! use vortex_datafusion::v2::VortexTable;
67//!
68//! # let data_source: DataSourceRef = todo!();
69//! let table = Arc::new(VortexTable::new(
70//!     data_source,
71//!     VortexSession::default(),
72//!     Arc::new(Schema::empty()),
73//! ));
74//!
75//! let ctx = SessionContext::new();
76//! ctx.register_table("vortex_data", table)?;
77//! # Ok::<(), datafusion_common::DataFusionError>(())
78//! ```
79//!
80//! [`Vortex`]: https://docs.rs/crate/vortex/latest
81//! [DataFusion]: https://docs.rs/datafusion/latest/datafusion/
82//! [`ListingTable`]: https://docs.rs/datafusion/latest/datafusion/datasource/listing/struct.ListingTable.html
83//! [`DataSourceRef`]: vortex::scan::DataSourceRef
84//! [`SessionContext`]: https://docs.rs/datafusion/latest/datafusion/prelude/struct.SessionContext.html
85#![deny(missing_docs)]
86use std::fmt::Debug;
87
88use datafusion_common::stats::Precision as DFPrecision;
89use vortex::expr::stats::Precision;
90
91pub mod convert;
92mod persistent;
93pub mod v2;
94
95#[cfg(test)]
96mod tests;
97
98pub use persistent::*;
99
100/// Extension trait to convert our [`Precision`] to DataFusion's
101/// [`DataFusionPrecision`].
102///
103/// [`Precision`]: vortex::expr::stats::Precision
104/// [`DataFusionPrecision`]: datafusion_common::stats::Precision
105trait PrecisionExt<T>
106where
107    T: Debug + Clone + PartialEq + Eq + PartialOrd,
108{
109    /// Convert `Precision` to the datafusion equivalent.
110    fn to_df(self) -> DFPrecision<T>;
111}
112
113impl<T> PrecisionExt<T> for Precision<T>
114where
115    T: Debug + Clone + PartialEq + Eq + PartialOrd,
116{
117    fn to_df(self) -> DFPrecision<T> {
118        match self {
119            Precision::Exact(v) => DFPrecision::Exact(v),
120            Precision::Inexact(v) => DFPrecision::Inexact(v),
121            Precision::Absent => DFPrecision::Absent,
122        }
123    }
124}
125
126#[cfg(test)]
127mod common_tests {
128    use std::sync::Arc;
129    use std::sync::LazyLock;
130
131    use datafusion::arrow::array::RecordBatch;
132    use datafusion::datasource::provider::DefaultTableFactory;
133    use datafusion::execution::SessionStateBuilder;
134    use datafusion::prelude::SessionContext;
135    use datafusion_catalog::TableProvider;
136    use datafusion_common::DFSchema;
137    use datafusion_common::GetExt;
138    use datafusion_expr::CreateExternalTable;
139    use object_store::ObjectStore;
140    use object_store::memory::InMemory;
141    use url::Url;
142    use vortex::VortexSessionDefault;
143    use vortex::array::ArrayRef;
144    use vortex::array::arrow::FromArrowArray;
145    use vortex::file::WriteOptionsSessionExt;
146    use vortex::io::VortexWrite;
147    use vortex::io::object_store::ObjectStoreWrite;
148    use vortex::session::VortexSession;
149
150    use crate::VortexFormatFactory;
151    use crate::VortexTableOptions;
152
153    static VX_SESSION: LazyLock<VortexSession> = LazyLock::new(VortexSession::default);
154
155    pub struct TestSessionContext {
156        pub store: Arc<dyn ObjectStore>,
157        pub session: SessionContext,
158    }
159
160    impl Default for TestSessionContext {
161        fn default() -> Self {
162            Self::new(false)
163        }
164    }
165
166    impl TestSessionContext {
167        /// Create a new test session context with the given projection pushdown setting.
168        pub fn new(projection_pushdown: bool) -> Self {
169            let store = Arc::new(InMemory::new());
170            let opts = VortexTableOptions {
171                projection_pushdown,
172                ..Default::default()
173            };
174            let factory = Arc::new(VortexFormatFactory::new().with_options(opts));
175            let mut session_state_builder = SessionStateBuilder::new()
176                .with_default_features()
177                .with_table_factory(
178                    factory.get_ext().to_uppercase(),
179                    Arc::new(DefaultTableFactory::new()),
180                )
181                .with_object_store(
182                    &Url::try_from("file://").unwrap(),
183                    Arc::<InMemory>::clone(&store),
184                );
185
186            if let Some(file_formats) = session_state_builder.file_formats() {
187                file_formats.push(factory as _);
188            }
189
190            let session: SessionContext =
191                SessionContext::new_with_state(session_state_builder.build()).enable_url_table();
192
193            Self { store, session }
194        }
195
196        // Write arrow data into a vortex file.
197        pub async fn write_arrow_batch<P>(&self, path: P, batch: &RecordBatch) -> anyhow::Result<()>
198        where
199            P: Into<object_store::path::Path>,
200        {
201            let array = ArrayRef::from_arrow(batch, false)?;
202            let mut write = ObjectStoreWrite::new(Arc::clone(&self.store), &path.into()).await?;
203            VX_SESSION
204                .write_options()
205                .write(&mut write, array.to_array_stream())
206                .await?;
207            write.shutdown().await?;
208
209            Ok(())
210        }
211
212        /// Creates a ListingTable provider targeted at the provided path
213        pub async fn table_provider<S>(
214            &self,
215            name: &str,
216            location: impl Into<String>,
217            schema: S,
218        ) -> anyhow::Result<Arc<dyn TableProvider>>
219        where
220            DFSchema: TryFrom<S>,
221            anyhow::Error: From<<S as TryInto<DFSchema>>::Error>,
222        {
223            let factory = self.session.table_factory("VORTEX").unwrap();
224
225            let cmd = CreateExternalTable::builder(
226                name,
227                location.into(),
228                "vortex",
229                DFSchema::try_from(schema)?.into(),
230            )
231            .build();
232
233            let table = factory.create(&self.session.state(), &cmd).await?;
234
235            Ok(table)
236        }
237    }
238}