use async_trait::async_trait;
use saya_agent::{CancellationToken, Candidate, tally};
use saya_connectors::{fanout_probe, has_top_level_order_by};
use saya_types::{QueryResult, SqlDialect};
mod tie;
#[async_trait]
pub(crate) trait CandidateExecutor: Sync {
async fn run(&self, sql: &str) -> Option<QueryResult>;
}
pub(crate) struct Decision {
pub winner: Option<usize>,
pub votes: usize,
pub margin: usize,
pub tied: bool,
pub probe_broke_tie: bool,
pub fanout: Vec<Option<bool>>,
}
pub(crate) async fn decide(
nominated: &[Option<String>],
executor: &dyn CandidateExecutor,
dialect: SqlDialect,
cancellation: &CancellationToken,
) -> Decision {
let n = nominated.len();
let mut candidates: Vec<Candidate> = Vec::with_capacity(n);
for nominated_sql in nominated {
let Some(sql) = nominated_sql.as_deref() else {
candidates.push(Candidate {
sql: String::new(),
result: None,
});
continue;
};
if cancellation.is_cancelled() {
return cancelled(n);
}
let result = executor.run(sql).await;
candidates.push(Candidate {
sql: sql.to_string(),
result,
});
}
let ordered = candidates
.iter()
.any(|c| c.result.is_some() && has_top_level_order_by(&c.sql, dialect));
let consensus = tally(&candidates, ordered);
if !consensus.tied {
return Decision {
winner: consensus.winner,
votes: consensus.votes,
margin: consensus.margin,
tied: false,
probe_broke_tie: false,
fanout: vec![None; n],
};
}
let leaders = tie::tied_leaders(&candidates, ordered, consensus.votes);
let mut fanout = vec![None; n];
for &rep in &leaders {
let candidate = &candidates[rep];
if candidate.result.is_none() {
continue; }
let Some(probe) = fanout_probe(&candidate.sql, dialect) else {
continue; };
match tie::probe_one(executor, &probe, cancellation).await {
tie::ProbeOutcome::Cancelled => return cancelled(n),
tie::ProbeOutcome::NoSignal => {} tie::ProbeOutcome::Flagged(f) => fanout[rep] = Some(f),
}
}
let winner = tie::break_tie(&leaders, &fanout);
Decision {
winner,
votes: consensus.votes,
margin: consensus.margin,
tied: true,
probe_broke_tie: winner.is_some(),
fanout,
}
}
fn cancelled(n: usize) -> Decision {
Decision {
winner: None,
votes: 0,
margin: 0,
tied: false,
probe_broke_tie: false,
fanout: vec![None; n],
}
}
#[cfg(test)]
#[path = "decide_tests.rs"]
mod tests;