1use std::sync::Arc;
12
13use datafusion::arrow::array::RecordBatch;
14use datafusion::arrow::datatypes::SchemaRef;
15use datafusion::dataframe::DataFrame;
16use datafusion::logical_expr::JoinType;
17use datafusion::physical_plan::displayable;
18use datafusion::prelude::{Expr, ParquetReadOptions, lit};
19use oxidelake_core::EngineError;
20use oxidelake_core::params::DistanceMetric;
21use oxidelake_runtime::OxideSession;
22use oxidelake_runtime::udf::{cosine_distance_udf, l2_distance_udf, query_literal};
23
24#[derive(Debug, Clone)]
26pub struct OxideFrame {
27 inner: DataFrame,
28}
29
30impl OxideFrame {
31 pub fn new(inner: DataFrame) -> Self {
33 Self { inner }
34 }
35
36 pub fn into_inner(self) -> DataFrame {
38 self.inner
39 }
40
41 pub fn schema(&self) -> SchemaRef {
43 Arc::new(self.inner.schema().as_arrow().clone())
44 }
45
46 pub fn filter(self, predicate: Expr) -> Result<Self, EngineError> {
48 Ok(Self::new(self.inner.filter(predicate)?))
49 }
50
51 pub fn select(self, columns: &[&str]) -> Result<Self, EngineError> {
53 Ok(Self::new(self.inner.select_columns(columns)?))
54 }
55
56 pub fn alias(self, name: &str) -> Result<Self, EngineError> {
58 Ok(Self::new(self.inner.alias(name)?))
59 }
60
61 pub fn join(
63 self,
64 right: OxideFrame,
65 left_key: &str,
66 right_key: &str,
67 ) -> Result<Self, EngineError> {
68 Ok(Self::new(self.inner.join(
69 right.inner,
70 JoinType::Inner,
71 &[left_key],
72 &[right_key],
73 None,
74 )?))
75 }
76
77 pub fn aggregate(
80 self,
81 group_by: Vec<Expr>,
82 aggregates: Vec<Expr>,
83 ) -> Result<Self, EngineError> {
84 Ok(Self::new(self.inner.aggregate(group_by, aggregates)?))
85 }
86
87 pub fn sort(self, exprs: Vec<datafusion::logical_expr::SortExpr>) -> Result<Self, EngineError> {
89 Ok(Self::new(self.inner.sort(exprs)?))
90 }
91
92 pub fn limit(self, n: usize) -> Result<Self, EngineError> {
94 Ok(Self::new(self.inner.limit(0, Some(n))?))
95 }
96
97 pub fn vector_distance(
102 self,
103 column: &str,
104 query: &[f32],
105 metric: DistanceMetric,
106 output: &str,
107 ) -> Result<Self, EngineError> {
108 let udf = match metric {
109 DistanceMetric::L2 => l2_distance_udf(),
110 DistanceMetric::Cosine => cosine_distance_udf(),
111 };
112 let call = udf.call(vec![
113 datafusion::prelude::col(column),
114 lit(query_literal(query)),
115 ]);
116 Ok(Self::new(self.inner.with_column(output, call)?))
117 }
118
119 pub async fn explain(&self) -> Result<String, EngineError> {
121 let plan = self.inner.clone().create_physical_plan().await?;
122 Ok(displayable(plan.as_ref()).indent(true).to_string())
123 }
124
125 pub async fn collect(self) -> Result<Vec<RecordBatch>, EngineError> {
127 Ok(self.inner.collect().await?)
128 }
129
130 pub async fn show(self) -> Result<(), EngineError> {
132 Ok(self.inner.show().await?)
133 }
134}
135
136impl From<DataFrame> for OxideFrame {
137 fn from(inner: DataFrame) -> Self {
138 Self::new(inner)
139 }
140}
141
142pub trait OxideSessionExt {
144 fn read_parquet(
146 &self,
147 path: &str,
148 ) -> impl Future<Output = Result<OxideFrame, EngineError>> + Send;
149
150 fn table(&self, name: &str) -> impl Future<Output = Result<OxideFrame, EngineError>> + Send;
152
153 fn sql_frame(
155 &self,
156 query: &str,
157 ) -> impl Future<Output = Result<OxideFrame, EngineError>> + Send;
158}
159
160impl OxideSessionExt for OxideSession {
161 async fn read_parquet(&self, path: &str) -> Result<OxideFrame, EngineError> {
162 Ok(OxideFrame::new(
163 self.ctx()
164 .read_parquet(path, ParquetReadOptions::default())
165 .await?,
166 ))
167 }
168
169 async fn table(&self, name: &str) -> Result<OxideFrame, EngineError> {
170 Ok(OxideFrame::new(self.ctx().table(name).await?))
171 }
172
173 async fn sql_frame(&self, query: &str) -> Result<OxideFrame, EngineError> {
174 Ok(OxideFrame::new(self.sql(query).await?))
175 }
176}