use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
sync::Arc,
};
use datafusion::{
common::{DataFusionError, config::ConfigOptions},
execution::{SessionState, SessionStateBuilder},
sql::{
parser::{DFParserBuilder, Statement as DfSqlStatement},
sqlparser::{ast::Statement as SqlStatement, dialect::dialect_from_str},
},
};
use query_planning::{execution_contracts::SourceChunkPlanningMode, mode::StatisticalAggregateExecutionMode};
use super::{
ALGEON_EXPLAIN_COVERAGE_FUNCTION, COVERAGE_VALIDATOR_RECURSION_REASON_CODE, GPU_COVERAGE_REJECTION_REASON_FACT,
model::{QueryGpuRejectionPhase, QueryGpuValidatedUnder},
recursion_guard::query_mentions_coverage_table_function,
};
use crate::{
error::{
DataFusionErrorExt, Error, ErrorCode, ErrorSourceDiagnostics, FailedStatement, Result, client::client_failure,
},
planner::AlgeonNativeOptimizerConfig,
session::installed_algeon_native_optimizer_config,
};
#[derive(Debug, Clone)]
pub(super) struct ValidationOptimizerConfig {
pub(super) optimizer_config_source: &'static str,
pub(super) effective_optimizer_config: AlgeonNativeOptimizerConfig,
}
#[derive(Debug)]
struct QueryGpuCoverageValidationInProgress;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct QueryGpuRejection {
pub(super) phase: QueryGpuRejectionPhase,
pub(super) reason_code: String,
pub(super) source_error_status: Option<&'static str>,
pub(super) detail: String,
pub(super) trusted_diagnostic: String,
}
impl QueryGpuRejection {
pub(super) fn new(
phase: QueryGpuRejectionPhase,
reason_code: impl Into<String>,
detail: impl Into<String>,
) -> Self {
let detail = detail.into();
Self {
phase,
reason_code: reason_code.into(),
source_error_status: None,
trusted_diagnostic: detail.clone(),
detail,
}
}
pub(super) fn from_datafusion_error(phase: QueryGpuRejectionPhase, error: &DataFusionError) -> Self {
let (code, detail) = client_failure(error, FailedStatement::Other);
let adapter_error = error.find_algeon_error();
let reason_code = adapter_error
.and_then(|adapter_error| adapter_error.fact_value(GPU_COVERAGE_REJECTION_REASON_FACT))
.unwrap_or_else(|| code.as_str())
.to_string();
let trusted_diagnostic = match (ErrorSourceDiagnostics::from_error(error).chain(), adapter_error) {
(Some(chain), _) => chain.to_string(),
(None, Some(adapter_error)) => adapter_error.message().to_string(),
(None, None) => error.message().to_string(),
};
Self {
phase,
reason_code,
source_error_status: adapter_error.map(|adapter_error| adapter_error.status().as_str()),
detail,
trusted_diagnostic,
}
}
}
pub(super) fn optimizer_config_for_validation(state: &SessionState) -> Result<ValidationOptimizerConfig> {
let config = match installed_algeon_native_optimizer_config(state) {
Some(config) => config,
None => {
return Err(Error::new(
ErrorCode::InvalidRequest,
"algeon_datafusion.gpu_coverage.validation_config",
"native optimizer rule is not installed on the validation session; install it via AlgeonGpuBackend::install_on or AlgeonSessionStateBuilderExt::with_algeon_native",
)
.with_fact("reason", "native_optimizer_rule_not_installed"));
},
};
Ok(ValidationOptimizerConfig {
optimizer_config_source: "installed_session_rule",
effective_optimizer_config: config,
})
}
#[derive(Debug)]
#[expect(dead_code, reason = "fields read through derived Debug format for fingerprint")]
struct StableNativeValidationPolicy {
max_source_chunk_bytes: u64,
max_row_groups_per_chunk: usize,
max_retry_attempts: usize,
max_source_reads_in_flight: usize,
source_chunk_planning_mode: SourceChunkPlanningMode,
statistical_aggregate_execution_mode: StatisticalAggregateExecutionMode,
}
pub(super) fn validated_under(state: &SessionState, config: &ValidationOptimizerConfig) -> QueryGpuValidatedUnder {
let rules = state.physical_optimizers();
let rule_names = rules.iter().map(|rule| rule.name()).collect::<Vec<_>>().join(",");
let optimizer_config = &config.effective_optimizer_config;
let execution = optimizer_config.native_execution_config();
let stable_policy = StableNativeValidationPolicy {
max_source_chunk_bytes: execution.source_chunking().max_source_chunk_bytes.get(),
max_row_groups_per_chunk: execution.source_chunking().max_row_groups_per_chunk.get(),
max_retry_attempts: execution.source_chunking().max_retry_attempts.get(),
max_source_reads_in_flight: execution.max_source_reads_in_flight(),
source_chunk_planning_mode: execution.source_chunk_planning_mode(),
statistical_aggregate_execution_mode: execution.statistical_aggregate_execution_mode(),
};
let final_plan_requirements = if optimizer_config.execution_mode().rejects_datafusion_cpu() {
vec!["no_datafusion_cpu".to_string()]
} else {
Vec::new()
};
let optimizer_fingerprint_input = format!(
"native_optimizer_rule=installed;{rule_names};session_options={stable_policy:?};native_optimizer_options={:?};final_plan_requirements={final_plan_requirements:?}",
optimizer_config.native_optimizer_options(),
);
QueryGpuValidatedUnder {
optimizer_config_source: config.optimizer_config_source.to_string(),
native_optimizer_rule_installed: true,
execution_mode: Some("bounded".to_string()),
final_plan_requirements,
session_config_fingerprint: fingerprint_session_config(state.config_options()),
optimizer_fingerprint: fingerprint_debug(&optimizer_fingerprint_input),
catalog_snapshot: "session_state_cloned_at_validation_call".to_string(),
}
}
pub(super) fn mark_coverage_validation_in_progress(builder: &mut SessionStateBuilder) {
builder.config().get_or_insert_with(Default::default).set_extension(Arc::new(QueryGpuCoverageValidationInProgress));
}
pub(crate) fn gpu_coverage_validation_in_progress(state: &SessionState) -> bool {
state.config().get_extension::<QueryGpuCoverageValidationInProgress>().is_some()
}
pub(super) fn coverage_validator_recursion_rejection() -> QueryGpuRejection {
QueryGpuRejection::new(
QueryGpuRejectionPhase::Parsing,
COVERAGE_VALIDATOR_RECURSION_REASON_CODE,
format!("{ALGEON_EXPLAIN_COVERAGE_FUNCTION} cannot validate a query that calls itself"),
)
}
pub(crate) fn coverage_validator_recursion_error() -> Error {
Error::new(
ErrorCode::DataFusionPlanning,
"algeon_datafusion.gpu_coverage.recursion",
format!("{ALGEON_EXPLAIN_COVERAGE_FUNCTION} cannot validate a query that calls itself"),
)
.with_fact(GPU_COVERAGE_REJECTION_REASON_FACT, COVERAGE_VALIDATOR_RECURSION_REASON_CODE)
}
pub(super) fn preflight_single_query(state: &SessionState, sql: &str) -> Option<QueryGpuRejection> {
let options = state.config().options();
let dialect_config = options.sql_parser.dialect;
let Some(dialect) = dialect_from_str(dialect_config) else {
return Some(QueryGpuRejection::new(
QueryGpuRejectionPhase::Parsing,
ErrorCode::DataFusionPlanning.as_str(),
format!("unsupported SQL dialect configured for validation: {dialect_config}"),
));
};
let statements = match DFParserBuilder::new(sql)
.with_dialect(dialect.as_ref())
.with_recursion_limit(options.sql_parser.recursion_limit.get())
.build()
.and_then(|mut parser| parser.parse_statements())
{
Ok(statements) => statements,
Err(error) => {
return Some(QueryGpuRejection::from_datafusion_error(QueryGpuRejectionPhase::Parsing, &error));
},
};
if statements.is_empty() {
return Some(QueryGpuRejection::new(
QueryGpuRejectionPhase::Parsing,
"empty_sql",
"validate_query expects a SQL query statement",
));
}
if statements.len() != 1 {
return Some(QueryGpuRejection::new(
QueryGpuRejectionPhase::Parsing,
"multi_statement_unsupported",
format!("validate_query expects exactly one query statement, got {}", statements.len()),
));
}
match statements.front() {
Some(DfSqlStatement::Statement(statement)) => match statement.as_ref() {
SqlStatement::Query(query) if query_mentions_coverage_table_function(query) => {
Some(coverage_validator_recursion_rejection())
},
SqlStatement::Query(_) => None,
_ => Some(QueryGpuRejection::new(
QueryGpuRejectionPhase::Parsing,
"non_query_statement_unsupported",
"validate_query v1 supports SELECT/query statements only",
)),
},
Some(_) => Some(QueryGpuRejection::new(
QueryGpuRejectionPhase::Parsing,
"non_query_statement_unsupported",
"validate_query v1 supports SELECT/query statements only",
)),
None => unreachable!("empty statements are handled before statement matching"),
}
}
pub(super) fn classify_physical_planning_error(error: &DataFusionError) -> QueryGpuRejectionPhase {
let Some(adapter_error) = error.find_algeon_error() else {
return QueryGpuRejectionPhase::PhysicalPlanning;
};
match adapter_error.code() {
ErrorCode::PlanningTimeout
| ErrorCode::SourceAccessDenied
| ErrorCode::SourceUnauthenticated
| ErrorCode::SourceObjectNotFound
| ErrorCode::SourceTemporaryFailure => QueryGpuRejectionPhase::PlanningIo,
_ => QueryGpuRejectionPhase::PhysicalPlanning,
}
}
pub(super) fn planning_did_io_for_physical_error(phase: QueryGpuRejectionPhase) -> bool {
phase == QueryGpuRejectionPhase::PlanningIo
}
fn fingerprint_debug(value: &impl std::fmt::Debug) -> String {
let mut hasher = DefaultHasher::new();
format!("{value:?}").hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
fn fingerprint_session_config(config: &ConfigOptions) -> String {
let mut entries = config.entries().into_iter().map(|entry| (entry.key, entry.value)).collect::<Vec<_>>();
entries.sort_unstable();
fingerprint_debug(&entries)
}