use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
sync::Arc,
};
use datafusion::{
common::{DFSchema, TableReference},
datasource::listing::ListingTableUrl,
error::{DataFusionError, Result as DataFusionResult},
execution::{SessionState, SessionStateBuilder},
logical_expr::{CreateExternalTable, DdlStatement, LogicalPlan},
prelude::{SessionConfig, SessionContext},
sql::{parser::DFParserBuilder, sqlparser::dialect::dialect_from_str},
};
use futures::StreamExt;
use url::Url;
use crate::server::{
adapter_error::Error as AdapterError,
adapter_session::AlgeonSessionStateBuilderExt,
backend::AlgeonGpuBackend,
config::{CugraphServerConfig, ServerConfig},
cugraph_sql::CugraphSqlConfig,
cuml_sql::CumlSqlConfig,
error::{Result as ServerResult, ServerError},
flight::{reject_drop_object_type_mismatch, verify_update_ddl},
observability::RedactedSql,
};
pub async fn build_session_context_with_backend(
config: &ServerConfig,
backend: &AlgeonGpuBackend,
) -> ServerResult<SessionContext> {
let mut effective_config = config.clone();
effective_config.native_optimizer_config = backend.optimizer_config().clone();
let session_config = build_session_config(&effective_config)?;
let builder = SessionStateBuilder::new_with_default_features().with_config(session_config);
let builder = backend
.install_on(builder)
.map_err(|error| ServerError::session("failed to install backend native session", error))?;
let builder = builder
.try_with_cuvs_sql()
.map_err(|error| ServerError::session("failed to install cuVS SQL family", error))?
.try_with_cuml_sql_config(CumlSqlConfig { model_store: effective_config.models.clone() })
.map_err(|error| ServerError::session("failed to install cuML SQL family", error))?;
let builder = match &effective_config.cugraph {
Some(cugraph) => builder
.try_with_cugraph_sql(cugraph_sql_config(cugraph))
.map_err(|error| ServerError::session("failed to install cuGraph SQL family", error))?,
None => builder,
};
Ok(SessionContext::new_with_state(builder.build()))
}
pub(crate) async fn prepare_shared_session(ctx: &SessionContext, config: &ServerConfig) -> ServerResult<()> {
if let Some(root) = &config.data_root {
register_data_root(ctx, root).await?;
}
if let Some(path) = &config.init_sql {
run_init_sql(ctx, path).await?;
}
Ok(())
}
const PARQUET_FILE_TYPE: &str = "PARQUET";
const PARQUET_FOLDER_GLOB: &str = "*.parquet";
struct DataRootEntry {
name: String,
path: PathBuf,
location: Url,
is_dir: bool,
}
pub(crate) async fn register_data_root(ctx: &SessionContext, root: &Path) -> ServerResult<()> {
let state = ctx.state();
let entries = data_root_entries(root)?;
let mut tables = BTreeMap::<String, PathBuf>::new();
for entry in entries {
if entry.is_dir {
match folder_lists_parquet(&state, &entry.location).await {
Ok(true) => {},
Ok(false) => continue,
Err(error) => {
tracing::warn!(
path = %entry.path.display(),
error = %error,
"skipping data root directory whose listing failed before a Parquet file was found"
);
continue;
},
}
}
if let Some(first) = tables.get(&entry.name) {
return Err(ServerError::invalid_config(
"data.root",
format!(
"has two entries for table `{}`: `{}` and `{}`; rename one of them",
entry.name,
first.display(),
entry.path.display()
),
)
.with_fact("reason", "data_root_duplicate_table")
.with_fact("table", &entry.name));
}
register_data_root_table(ctx, &state, &entry).await?;
tracing::info!(table = %entry.name, path = %entry.path.display(), "registered data root table");
tables.insert(entry.name, entry.path);
}
tracing::info!(root = %root.display(), table_count = tables.len(), "registered data root tables");
Ok(())
}
fn data_root_entries(root: &Path) -> ServerResult<Vec<DataRootEntry>> {
let unreadable = |error: std::io::Error| {
ServerError::invalid_config_with_source("data.root", format!("`{}` cannot be read", root.display()), error)
};
let root = std::path::absolute(root).map_err(unreadable)?;
let mut dir_entries =
fs::read_dir(&root).and_then(|entries| entries.collect::<std::io::Result<Vec<_>>>()).map_err(unreadable)?;
dir_entries.sort_by_key(fs::DirEntry::file_name);
let mut entries = Vec::new();
for dir_entry in dir_entries {
let file_name = dir_entry.file_name();
if matches!(file_name.as_encoded_bytes().first(), Some(b'.' | b'_')) {
continue;
}
let path = dir_entry.path();
let metadata = match fs::metadata(&path) {
Ok(metadata) => metadata,
Err(error) => {
tracing::warn!(path = %path.display(), error = %error, "skipping data root entry that cannot be inspected");
continue;
},
};
let (name, location) = if metadata.is_dir() {
(path.file_name(), Url::from_directory_path(&path))
} else if metadata.is_file()
&& path.extension().is_some_and(|extension| extension.eq_ignore_ascii_case("parquet"))
{
(path.file_stem(), Url::from_file_path(&path))
} else {
continue;
};
let Some(name) = name.and_then(|name| name.to_str()) else {
tracing::warn!(path = %path.display(), "skipping data root entry whose name is not valid UTF-8");
continue;
};
let location = location.map_err(|()| {
ServerError::invalid_config("data.root", format!("`{}` cannot be written as a file URL", path.display()))
})?;
entries.push(DataRootEntry { name: name.to_string(), path, location, is_dir: metadata.is_dir() });
}
Ok(entries)
}
async fn folder_lists_parquet(state: &SessionState, location: &Url) -> DataFusionResult<bool> {
let url = ListingTableUrl::parse(location.as_str())?.with_glob(PARQUET_FOLDER_GLOB)?;
let ignore_subdirectory = state.config_options().execution.listing_table_ignore_subdirectory;
let store = state.runtime_env().object_store(url.object_store())?;
let mut files = store.list(Some(url.prefix()));
while let Some(file) = files.next().await {
if url.contains(&file?.location, ignore_subdirectory) {
return Ok(true);
}
}
Ok(false)
}
async fn register_data_root_table(
ctx: &SessionContext,
state: &SessionState,
entry: &DataRootEntry,
) -> ServerResult<()> {
let failed = |error: DataFusionError| {
ServerError::invalid_config_with_source(
"data.root",
format!("`{}` cannot be registered as table `{}`: {error}", entry.path.display(), entry.name),
error,
)
.with_fact("table", &entry.name)
};
let name = TableReference::bare(entry.name.as_str());
let command = CreateExternalTable::builder(
name.clone(),
entry.location.as_str(),
PARQUET_FILE_TYPE,
Arc::new(DFSchema::empty()),
)
.build();
let factory = state.table_factories().get(PARQUET_FILE_TYPE).ok_or_else(|| {
ServerError::invalid_config(
"data.root",
format!("cannot be registered because the session has no `{PARQUET_FILE_TYPE}` table factory"),
)
})?;
let provider = factory.create(state, &command).await.map_err(failed)?;
ctx.register_table(name, provider).map_err(failed)?;
Ok(())
}
pub(crate) async fn run_init_sql(ctx: &SessionContext, path: &Path) -> ServerResult<()> {
let sql = fs::read_to_string(path).map_err(|error| {
ServerError::invalid_config_with_source("init_sql", format!("`{}` cannot be read", path.display()), error)
})?;
let state = ctx.state();
let statements = {
let options = state.config_options();
let dialect = dialect_from_str(options.sql_parser.dialect).ok_or_else(|| {
ServerError::invalid_config(
"init_sql",
format!("cannot be parsed with the configured SQL dialect `{}`", options.sql_parser.dialect),
)
})?;
DFParserBuilder::new(sql.as_str())
.with_dialect(dialect.as_ref())
.with_recursion_limit(options.sql_parser.recursion_limit.get())
.build()
.and_then(|mut parser| parser.parse_statements())
.map_err(|error| {
ServerError::invalid_config_with_source(
"init_sql",
format!("`{}` is not valid SQL: {error}", path.display()),
error,
)
})?
};
for (index, statement) in statements.into_iter().enumerate() {
let index = index + 1;
let text = statement.to_string();
let excerpt = RedactedSql(&text).to_string().chars().take(120).collect::<String>();
let statement_error = |reason: String| {
ServerError::invalid_config("init_sql", format!("statement {index} ({excerpt}) {reason}"))
.with_fact("statement_index", index)
};
let plan = state
.statement_to_plan(statement)
.await
.map_err(|error| statement_error(format!("failed to plan: {error}")))?;
let LogicalPlan::Ddl(ddl) = &plan else {
return Err(statement_error(
"is not DDL; init_sql accepts DDL such as CREATE EXTERNAL TABLE or CREATE VIEW only".to_string(),
)
.with_fact("reason", "init_sql_statement_not_ddl"));
};
let rejected = |error: AdapterError| {
statement_error(format!("is not accepted: {}", error.message()))
.with_fact("reason", "init_sql_statement_rejected")
};
verify_update_ddl(ddl, "algeon_datafusion.server.init_sql").map_err(rejected)?;
reject_drop_object_type_mismatch(ctx, &plan, "algeon_datafusion.server.init_sql").await.map_err(rejected)?;
let kind = ddl.name().to_string();
let object = ddl_object_name(ddl);
let frame =
ctx.execute_logical_plan(plan).await.map_err(|error| statement_error(format!("failed: {error}")))?;
frame.collect().await.map_err(|error| statement_error(format!("failed: {error}")))?;
tracing::info!(statement_index = index, statement_kind = %kind, object = object.as_deref(), "ran init_sql statement");
}
Ok(())
}
fn ddl_object_name(ddl: &DdlStatement) -> Option<String> {
match ddl {
DdlStatement::CreateExternalTable(table) => Some(table.name.to_string()),
DdlStatement::CreateMemoryTable(table) => Some(table.name.to_string()),
DdlStatement::CreateView(view) => Some(view.name.to_string()),
DdlStatement::CreateCatalogSchema(schema) => Some(schema.schema_name.clone()),
DdlStatement::CreateCatalog(catalog) => Some(catalog.catalog_name.clone()),
DdlStatement::CreateFunction(function) => Some(function.name.clone()),
DdlStatement::DropTable(table) => Some(table.name.to_string()),
DdlStatement::DropView(view) => Some(view.name.to_string()),
DdlStatement::DropFunction(function) => Some(function.name.clone()),
DdlStatement::CreateIndex(index) => index.name.clone(),
DdlStatement::DropCatalogSchema(_) => None,
}
}
pub(crate) fn cugraph_sql_config(config: &CugraphServerConfig) -> CugraphSqlConfig {
CugraphSqlConfig {
default_graph_options: config.default_graph_options.clone(),
host_boundary_config: config.host_boundary_config,
}
}
pub(crate) fn build_session_config(config: &ServerConfig) -> ServerResult<SessionConfig> {
let gpu_only = config.native_optimizer_config.execution_mode().rejects_datafusion_cpu();
SessionConfig::from_env()
.map(enable_session_discovery)
.map(|session_config| {
if gpu_only { session_config.with_target_partitions(1) } else { session_config }
})
.map_err(|error| ServerError::session("failed to build DataFusion session config from environment", error))
}
pub(super) fn enable_session_discovery(config: SessionConfig) -> SessionConfig {
config.with_information_schema(true)
}
#[cfg(all(test, feature = "cugraph"))]
mod tests {
use super::*;
use crate::server::planner::AlgeonGpuExecutionMode;
fn server_config_with_execution_mode(mode: &str) -> ServerConfig {
ServerConfig::from_vars([("ALGEON_SERVER_NATIVE_EXECUTION_MODE", mode)]).expect("load server config")
}
#[test]
fn execution_mode_env_maps_to_functions_only_and_preserves_partitions() {
let server = server_config_with_execution_mode("functions_only");
assert_eq!(server.native_optimizer_config.execution_mode(), AlgeonGpuExecutionMode::FunctionsOnly);
let session = build_session_config(&server).expect("session config");
let baseline = SessionConfig::from_env().expect("baseline session config");
assert_eq!(
session.options().execution.target_partitions,
baseline.options().execution.target_partitions,
"functions-only must preserve DataFusion's configured parallelism"
);
}
#[test]
fn execution_mode_env_maps_native_required_to_single_partition() {
let server = server_config_with_execution_mode("native_required");
let session = build_session_config(&server).expect("session config");
assert_eq!(session.options().execution.target_partitions, 1);
}
}