use std::sync::Arc;
use ballista::prelude::SessionContextExt;
use ballista_core::extension::SessionConfigExt;
use datafusion::dataframe::DataFrame;
use datafusion::execution::SessionStateBuilder;
use datafusion::physical_plan::displayable;
use datafusion::prelude::{SessionConfig, SessionContext};
use oxidelake_compute::{local_backend, oxide_udfs};
use oxidelake_core::telemetry::TelemetryHub;
use oxidelake_core::{BackendKind, EngineError};
use oxidelake_planner::{HardwarePlacementRule, physical_optimizer_rules};
use oxidelake_storage::{
default_object_store, register_local_store, register_parquet_table, with_gpu_batch_size,
with_pruning,
};
use crate::cluster;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionMode {
Embedded {
target: BackendKind,
},
Cluster {
scheduler_url: String,
},
}
pub struct OxideSession {
ctx: SessionContext,
mode: SessionMode,
telemetry: Arc<TelemetryHub>,
}
impl OxideSession {
pub fn local() -> Result<Self, EngineError> {
Self::local_with_target(local_backend()?.kind())
}
pub fn local_with_target(target: BackendKind) -> Result<Self, EngineError> {
let telemetry = TelemetryHub::new();
let rule = HardwarePlacementRule::new(target).with_telemetry(Arc::clone(&telemetry));
let mut config = with_pruning(SessionConfig::new());
if target.is_gpu() {
config = with_gpu_batch_size(config);
}
let state = SessionStateBuilder::new()
.with_default_features()
.with_config(config)
.with_physical_optimizer_rules(physical_optimizer_rules(rule))
.build();
let ctx = SessionContext::new_with_state(state);
register_local_store(&ctx, default_object_store());
for udf in oxide_udfs() {
ctx.register_udf(udf.as_ref().clone());
}
Ok(Self {
ctx,
mode: SessionMode::Embedded { target },
telemetry,
})
}
pub async fn connect(scheduler_url: &str) -> Result<Self, EngineError> {
let config = with_pruning(SessionConfig::new_with_ballista())
.with_ballista_physical_extension_codec(cluster::oxide_codec());
let state = SessionStateBuilder::new()
.with_default_features()
.with_config(config)
.build();
let ctx = SessionContext::remote_with_state(scheduler_url, state).await?;
for udf in oxide_udfs() {
ctx.register_udf(udf.as_ref().clone());
}
Ok(Self {
ctx,
mode: SessionMode::Cluster {
scheduler_url: scheduler_url.to_owned(),
},
telemetry: TelemetryHub::new(),
})
}
pub fn mode(&self) -> &SessionMode {
&self.mode
}
pub fn ctx(&self) -> &SessionContext {
&self.ctx
}
pub fn telemetry(&self) -> &Arc<TelemetryHub> {
&self.telemetry
}
pub async fn sql(&self, query: &str) -> Result<DataFrame, EngineError> {
Ok(self.ctx.sql(query).await?)
}
pub async fn register_parquet(&self, name: &str, path: &str) -> Result<(), EngineError> {
register_parquet_table(&self.ctx, name, path).await
}
pub async fn explain(&self, query: &str) -> Result<String, EngineError> {
let plan = self.ctx.sql(query).await?.create_physical_plan().await?;
Ok(displayable(plan.as_ref()).indent(true).to_string())
}
}
impl std::fmt::Debug for OxideSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OxideSession")
.field("mode", &self.mode)
.field("session_id", &self.ctx.session_id())
.finish_non_exhaustive()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[tokio::test]
async fn embedded_session_runs_sql_and_reports_mode() {
let session = OxideSession::local().unwrap();
assert!(matches!(session.mode(), SessionMode::Embedded { .. }));
let batches = session
.sql("SELECT 1 + 1 AS two")
.await
.unwrap()
.collect()
.await
.unwrap();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0].num_rows(), 1);
let text = session.explain("SELECT 1 + 1 AS two").await.unwrap();
assert!(text.contains("ProjectionExec"), "{text}");
}
}