use query_planning::triage::{TriageEvidenceRef, TriageSeverity, TriageSignal};
pub use query_planning::{
capability::Rejection,
correlation::{PlanNodeId, QueryId, TraceId},
plan::NativePlanCostReason,
triage::{TriageCode, TriageSummary, TriageVerdict},
};
use serde_json::{Value, json};
mod blocking;
pub use blocking::Blocking;
pub(crate) use blocking::{AggregateDefect, PlanBlocker, SinglePartitionOutcome, WindowDefect};
use crate::{capability::CapabilityReason, native::ExpressionRejection};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UnsupportedColumnDetail {
pub(crate) name: String,
pub(crate) data_type: String,
pub(crate) role: &'static str,
}
impl UnsupportedColumnDetail {
pub(crate) fn new(name: impl Into<String>, data_type: impl Into<String>, role: &'static str) -> Self {
Self { name: name.into(), data_type: data_type.into(), role }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NotSupportedDetail {
pub(crate) column: Option<UnsupportedColumnDetail>,
pub(crate) expression: Option<Box<ExpressionRejection>>,
pub(crate) blocker: Option<Box<PlanBlocker>>,
pub(crate) admission: Option<Rejection>,
pub(crate) single_partition: SinglePartitionOutcome,
}
impl NotSupportedDetail {
pub fn admission_rejection(&self) -> Option<&Rejection> {
self.admission.as_ref()
}
}
pub(crate) const UNSUPPORTED_NATIVE_PLAN_OPERATOR: &str = "UnsupportedNativePhysicalPlan";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CandidateShape {
operator: String,
inputs: Vec<CandidateShape>,
relation_input: bool,
}
impl CandidateShape {
pub fn leaf(operator: impl Into<String>) -> Self {
Self { operator: operator.into(), inputs: Vec::new(), relation_input: false }
}
pub fn new(operator: impl Into<String>, inputs: Vec<CandidateShape>) -> Self {
Self { operator: operator.into(), inputs, relation_input: false }
}
#[cfg(any(feature = "cugraph", test))]
pub(crate) fn relation_input(label: &str, shape: CandidateShape) -> Self {
Self { operator: label.to_string(), inputs: vec![shape], relation_input: true }
}
pub(crate) fn without_relation_input_label(&self) -> &CandidateShape {
match self.inputs.as_slice() {
[input] if self.relation_input => input,
_ => self,
}
}
pub fn operator(&self) -> &str {
&self.operator
}
pub fn inputs(&self) -> &[CandidateShape] {
&self.inputs
}
pub fn operator_count(&self) -> usize {
usize::from(!self.relation_input) + self.inputs.iter().map(Self::operator_count).sum::<usize>()
}
pub fn contains_operator(&self, operator: &str) -> bool {
self.operator == operator || self.inputs.iter().any(|input| input.contains_operator(operator))
}
}
impl std::fmt::Display for CandidateShape {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.operator)?;
if let Some((first, rest)) = self.inputs.split_first() {
write!(formatter, "({first}")?;
for input in rest {
write!(formatter, ",{input}")?;
}
formatter.write_str(")")?;
}
Ok(())
}
}
impl From<&str> for CandidateShape {
fn from(operator: &str) -> Self {
Self::leaf(operator)
}
}
impl From<String> for CandidateShape {
fn from(operator: String) -> Self {
Self::leaf(operator)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpuCandidate {
pub shape: CandidateShape,
pub plan_node_id: Option<PlanNodeId>,
pub output_schema_fields: usize,
}
impl GpuCandidate {
pub fn new(shape: impl Into<CandidateShape>, output_schema_fields: usize) -> Self {
Self { shape: shape.into(), plan_node_id: None, output_schema_fields }
}
pub fn operator_count(&self) -> usize {
self.shape.operator_count()
}
#[must_use]
pub fn with_plan_node_id(mut self, plan_node_id: impl Into<PlanNodeId>) -> Self {
self.plan_node_id = Some(plan_node_id.into());
self
}
#[must_use]
#[cfg(feature = "cugraph")]
pub(crate) fn with_relation_input_label(mut self, label: &str) -> Self {
self.shape = CandidateShape::relation_input(label, self.shape);
self
}
pub(crate) fn with_outer_operator(mut self, operator: &str) -> Self {
self.shape = CandidateShape::new(operator, vec![self.shape]);
self.plan_node_id = None;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CandidateOutcomeKind {
Selected,
NotSupported,
NotSelectedByCost,
}
impl CandidateOutcomeKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Selected => "selected",
Self::NotSupported => "not_supported",
Self::NotSelectedByCost => "not_selected_by_cost",
}
}
}
impl std::fmt::Display for CandidateOutcomeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CandidateOutcome {
Selected {
candidate: GpuCandidate,
},
NotSupported {
candidate: GpuCandidate,
capability_reason: CapabilityReason,
detail: NotSupportedDetail,
},
NotSelectedByCost {
candidate: GpuCandidate,
cost_reason: NativePlanCostReason,
},
}
impl CandidateOutcome {
pub const fn kind(&self) -> CandidateOutcomeKind {
match self {
Self::Selected { .. } => CandidateOutcomeKind::Selected,
Self::NotSupported { .. } => CandidateOutcomeKind::NotSupported,
Self::NotSelectedByCost { .. } => CandidateOutcomeKind::NotSelectedByCost,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct PlanningReport {
query_id: Option<QueryId>,
trace_id: Option<TraceId>,
outcomes: Vec<CandidateOutcome>,
}
#[derive(Debug, Clone, Copy)]
pub struct DataLayoutFixableNotSupported<'a> {
pub outcome_index: usize,
pub candidate: &'a GpuCandidate,
pub capability_reason: CapabilityReason,
pub property: &'static str,
}
impl PlanningReport {
pub fn new() -> Self {
Self::default()
}
pub fn from_outcomes(outcomes: impl IntoIterator<Item = CandidateOutcome>) -> Self {
Self { outcomes: outcomes.into_iter().collect(), ..Self::default() }
}
pub fn query_id(&self) -> Option<&QueryId> {
self.query_id.as_ref()
}
pub fn trace_id(&self) -> Option<&TraceId> {
self.trace_id.as_ref()
}
pub fn set_query_id(&mut self, query_id: impl Into<QueryId>) {
self.query_id = Some(query_id.into());
}
#[must_use]
pub fn with_query_id(mut self, query_id: impl Into<QueryId>) -> Self {
self.set_query_id(query_id);
self
}
pub fn set_trace_id(&mut self, trace_id: impl Into<TraceId>) {
self.trace_id = Some(trace_id.into());
}
#[must_use]
pub fn with_trace_id(mut self, trace_id: impl Into<TraceId>) -> Self {
self.set_trace_id(trace_id);
self
}
#[must_use]
pub fn with_correlation_from(mut self, other: &PlanningReport) -> Self {
self.query_id.clone_from(&other.query_id);
self.trace_id.clone_from(&other.trace_id);
self
}
pub(crate) fn anchor_selected_candidates_to_plan_node(&mut self, plan_node_id: impl Into<PlanNodeId>) {
let plan_node_id = plan_node_id.into();
for outcome in &mut self.outcomes {
match outcome {
CandidateOutcome::Selected { candidate } => {
candidate.plan_node_id = Some(plan_node_id.clone());
},
CandidateOutcome::NotSupported { .. } | CandidateOutcome::NotSelectedByCost { .. } => {},
}
}
}
pub(crate) fn wrap_selected_candidates_with_outer_operator(&mut self, operator: &str) {
for outcome in &mut self.outcomes {
match outcome {
CandidateOutcome::Selected { candidate } => {
*candidate = candidate.clone().with_outer_operator(operator);
},
CandidateOutcome::NotSupported { .. } | CandidateOutcome::NotSelectedByCost { .. } => {},
}
}
}
#[cfg(feature = "cugraph")]
pub(crate) fn label_relation_input(&mut self, label: &str) {
for outcome in &mut self.outcomes {
let (CandidateOutcome::Selected { candidate }
| CandidateOutcome::NotSupported { candidate, .. }
| CandidateOutcome::NotSelectedByCost { candidate, .. }) = outcome;
*candidate = candidate.clone().with_relation_input_label(label);
}
}
pub(crate) fn append(&mut self, other: PlanningReport) {
self.outcomes.extend(other.outcomes);
}
pub fn push_selected(&mut self, candidate: GpuCandidate) {
self.outcomes.push(CandidateOutcome::Selected { candidate });
}
pub fn push_not_supported(&mut self, candidate: GpuCandidate, capability_reason: CapabilityReason) {
self.push_not_supported_with_detail(candidate, capability_reason, NotSupportedDetail::default());
}
pub(crate) fn push_not_supported_with_detail(
&mut self,
candidate: GpuCandidate,
capability_reason: CapabilityReason,
detail: NotSupportedDetail,
) {
self.outcomes.push(CandidateOutcome::NotSupported { candidate, capability_reason, detail });
}
pub fn push_not_selected_by_cost(&mut self, candidate: GpuCandidate, cost_reason: NativePlanCostReason) {
self.outcomes.push(CandidateOutcome::NotSelectedByCost { candidate, cost_reason });
}
pub fn outcomes(&self) -> &[CandidateOutcome] {
&self.outcomes
}
pub fn selected_count(&self) -> usize {
self.outcomes.iter().filter(|outcome| matches!(outcome, CandidateOutcome::Selected { .. })).count()
}
pub fn single_selected_candidate(&self) -> Option<&GpuCandidate> {
let mut rewritten = self.outcomes.iter().filter_map(|outcome| match outcome {
CandidateOutcome::Selected { candidate } => Some(candidate),
CandidateOutcome::NotSupported { .. } | CandidateOutcome::NotSelectedByCost { .. } => None,
});
let candidate = rewritten.next()?;
rewritten.next().is_none().then_some(candidate)
}
pub fn not_selected_count(&self) -> usize {
self.not_supported_count() + self.not_selected_by_cost_count()
}
pub fn not_supported_count(&self) -> usize {
self.outcomes.iter().filter(|outcome| matches!(outcome, CandidateOutcome::NotSupported { .. })).count()
}
pub fn not_selected_by_cost_count(&self) -> usize {
self.outcomes.iter().filter(|outcome| matches!(outcome, CandidateOutcome::NotSelectedByCost { .. })).count()
}
pub fn data_layout_fixable_not_supported(&self) -> Option<DataLayoutFixableNotSupported<'_>> {
self.outcomes.iter().enumerate().find_map(|(outcome_index, outcome)| {
let CandidateOutcome::NotSupported { candidate, capability_reason, .. } = outcome else {
return None;
};
capability_reason.facts().layout_property.map(|property| DataLayoutFixableNotSupported {
outcome_index,
candidate,
capability_reason: *capability_reason,
property,
})
})
}
pub fn to_json(&self) -> Value {
json!({
"schema": "algeon_datafusion.planning_report",
"query_id": self.query_id.as_ref().map(QueryId::as_str),
"trace_id": self.trace_id.as_ref().map(TraceId::as_str),
"triage": self.triage_summary().to_json(),
"headline_facts": self.headline_facts_to_json(),
"selected_count": self.selected_count(),
"not_supported_count": self.not_supported_count(),
"not_selected_by_cost_count": self.not_selected_by_cost_count(),
"not_selected_count": self.not_selected_count(),
"outcomes": self.outcomes.iter().map(CandidateOutcome::to_json).collect::<Vec<_>>(),
})
}
fn headline_facts_to_json(&self) -> Value {
match self.data_layout_fixable_not_supported() {
Some(not_supported) => json!({
"data_layout_fixable_not_supported": true,
"data_layout_fixable_reason_code": not_supported.capability_reason.code(),
"data_layout_fixable_property": not_supported.property,
"data_layout_fixable_candidate_shape": not_supported.candidate.shape.to_string(),
"data_layout_fixable_outcome_index": not_supported.outcome_index,
}),
None => json!({
"data_layout_fixable_not_supported": false,
}),
}
}
pub fn triage_summary(&self) -> TriageSummary {
let signals = self
.outcomes
.iter()
.enumerate()
.filter_map(|(index, outcome)| match outcome {
CandidateOutcome::Selected { .. } => None,
CandidateOutcome::NotSupported { candidate, capability_reason, .. } => {
let mut evidence = TriageEvidenceRef::new("planning_report.outcomes").with_row_index(index);
if let Some(query_id) = self.query_id.clone() {
evidence = evidence.with_query_id(query_id);
}
if let Some(plan_node_id) = candidate.plan_node_id.clone() {
evidence = evidence.with_plan_node_id(plan_node_id);
}
Some(
TriageSignal::new(
TriageSeverity::Warning,
capability_reason.facts().triage,
format!(
"native candidate {} is not supported: {}",
candidate.shape,
capability_reason.code()
),
)
.with_evidence(evidence),
)
},
CandidateOutcome::NotSelectedByCost { candidate, cost_reason, .. } => {
let mut evidence = TriageEvidenceRef::new("planning_report.outcomes").with_row_index(index);
if let Some(query_id) = self.query_id.clone() {
evidence = evidence.with_query_id(query_id);
}
if let Some(plan_node_id) = candidate.plan_node_id.clone() {
evidence = evidence.with_plan_node_id(plan_node_id);
}
Some(
TriageSignal::new(
TriageSeverity::Warning,
TriageCode::NotSelectedByCost,
format!(
"native-capable candidate {} was not selected by cost: {}",
candidate.shape,
cost_reason.code()
),
)
.with_evidence(evidence),
)
},
})
.collect::<Vec<_>>();
let verdict = if signals.is_empty() {
TriageVerdict::Healthy
} else if self.selected_count() == 0 {
TriageVerdict::NotSelected
} else {
TriageVerdict::Degraded
};
TriageSummary::new(verdict, signals)
}
}
impl CandidateOutcome {
pub fn to_json(&self) -> Value {
match self {
Self::Selected { candidate } => json!({
"outcome": self.kind().as_str(),
"candidate": candidate.to_json(),
"capability_reason_code": null,
"cost_reason_code": null,
}),
Self::NotSupported { candidate, capability_reason, .. } => json!({
"outcome": self.kind().as_str(),
"candidate": candidate.to_json(),
"capability_reason_code": capability_reason.code(),
"cost_reason_code": null,
}),
Self::NotSelectedByCost { candidate, cost_reason } => json!({
"outcome": self.kind().as_str(),
"candidate": candidate.to_json(),
"capability_reason_code": null,
"cost_reason_code": cost_reason.code(),
}),
}
}
}
impl GpuCandidate {
pub fn to_json(&self) -> Value {
json!({
"shape": self.shape.to_string(),
"plan_node_id": self.plan_node_id.as_ref().map(PlanNodeId::as_str),
"output_schema_fields": self.output_schema_fields,
"operator_count": self.operator_count(),
})
}
}