use super::{CandidateExecutor, decide};
use async_trait::async_trait;
use saya_agent::CancellationToken;
use saya_connectors::{FanoutProbe, fanout_probe};
use saya_types::{QueryResult, SqlDialect};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
const D: SqlDialect = SqlDialect::Postgres;
struct ScriptedExecutor {
results: HashMap<String, QueryResult>,
calls: Mutex<Vec<String>>,
cancel_after: Option<(usize, CancellationToken)>,
first_cancelled: AtomicBool,
}
impl ScriptedExecutor {
fn new(results: HashMap<String, QueryResult>) -> Self {
Self {
results,
calls: Mutex::new(Vec::new()),
cancel_after: None,
first_cancelled: AtomicBool::new(false),
}
}
fn cancel_after(mut self, n: usize, token: CancellationToken) -> Self {
self.cancel_after = Some((n, token));
self
}
fn calls(&self) -> Vec<String> {
self.calls.lock().unwrap().clone()
}
}
#[async_trait]
impl CandidateExecutor for ScriptedExecutor {
async fn run(&self, sql: &str) -> Option<QueryResult> {
let mut calls = self.calls.lock().unwrap();
calls.push(sql.to_string());
let count = calls.len();
drop(calls);
if let Some((n, token)) = &self.cancel_after
&& count == *n
&& !self.first_cancelled.swap(true, Ordering::SeqCst)
{
token.cancel();
}
self.results.get(sql).cloned()
}
}
fn rows_result(columns: &[&str], rows: Vec<Value>) -> QueryResult {
QueryResult {
columns: columns.iter().map(|s| s.to_string()).collect(),
rows,
row_count: 0,
truncated: false,
executed_sql: String::new(),
}
}
fn count_result(n: i64) -> QueryResult {
rows_result(&["n"], vec![json!([n])])
}
fn nominated(sql: &str) -> Option<String> {
Some(sql.to_string())
}
fn register_probe(
results: &mut HashMap<String, QueryResult>,
probe: &FanoutProbe,
joined: i64,
base: i64,
) {
results.insert(probe.joined_rows.clone(), count_result(joined));
results.insert(probe.base_rows.clone(), count_result(base));
}
#[tokio::test]
async fn execute_then_tally_picks_the_largest_group() {
let mut results = HashMap::new();
results.insert(
"SELECT a FROM t WHERE id = 1".to_string(),
rows_result(&["a"], vec![json!([1])]),
);
results.insert(
"SELECT a FROM t WHERE id = 1".to_string(),
rows_result(&["a"], vec![json!([1])]),
);
results.insert(
"SELECT a FROM t WHERE id = 2".to_string(),
rows_result(&["a"], vec![json!([2])]),
);
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[
nominated("SELECT a FROM t WHERE id = 1"),
nominated("SELECT a FROM t WHERE id = 1"),
nominated("SELECT a FROM t WHERE id = 2"),
],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, Some(0));
assert_eq!(decision.votes, 2);
assert_eq!(decision.margin, 1);
assert!(!decision.tied);
assert!(!decision.probe_broke_tie);
}
#[tokio::test]
async fn ordered_is_set_when_any_candidate_has_top_level_order_by() {
let mut results = HashMap::new();
results.insert(
"SELECT a FROM t ORDER BY a".to_string(),
rows_result(&["a"], vec![json!([1]), json!([2]), json!([3])]),
);
results.insert(
"SELECT a FROM t ORDER BY a".to_string(),
rows_result(&["a"], vec![json!([1]), json!([2]), json!([3])]),
);
results.insert(
"SELECT a FROM t ORDER BY a DESC".to_string(),
rows_result(&["a"], vec![json!([3]), json!([2]), json!([1])]),
);
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[
nominated("SELECT a FROM t ORDER BY a"),
nominated("SELECT a FROM t ORDER BY a"),
nominated("SELECT a FROM t ORDER BY a DESC"),
],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, Some(0));
assert_eq!(decision.votes, 2);
assert_eq!(decision.margin, 1);
}
#[tokio::test]
async fn ordered_is_not_set_when_order_by_is_only_in_a_subquery() {
let mut results = HashMap::new();
results.insert(
"SELECT * FROM (SELECT a FROM t ORDER BY a) sub".to_string(),
rows_result(&["a"], vec![json!([1]), json!([2])]),
);
results.insert(
"SELECT * FROM (SELECT a FROM t ORDER BY a DESC) sub".to_string(),
rows_result(&["a"], vec![json!([2]), json!([1])]),
);
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[
nominated("SELECT * FROM (SELECT a FROM t ORDER BY a) sub"),
nominated("SELECT * FROM (SELECT a FROM t ORDER BY a DESC) sub"),
],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, Some(0));
assert_eq!(decision.votes, 2);
assert!(!decision.tied);
}
#[tokio::test]
async fn ordered_is_not_set_by_order_by_inside_a_string_constant() {
let mut results = HashMap::new();
results.insert(
"SELECT 'order by' AS s, a FROM t".to_string(),
rows_result(
&["s", "a"],
vec![json!(["order by", 1]), json!(["order by", 2])],
),
);
results.insert(
"SELECT 'order by' AS s, a FROM t".to_string(),
rows_result(
&["s", "a"],
vec![json!(["order by", 2]), json!(["order by", 1])],
),
);
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[
nominated("SELECT 'order by' AS s, a FROM t"),
nominated("SELECT 'order by' AS s, a FROM t"),
],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, Some(0));
assert_eq!(decision.votes, 2);
assert!(!decision.tied);
}
#[tokio::test]
async fn no_tie_runs_zero_probe_statements() {
let mut results = HashMap::new();
results.insert(
"SELECT a FROM t WHERE id = 1".to_string(),
rows_result(&["a"], vec![json!([1])]),
);
results.insert(
"SELECT a FROM t WHERE id = 1".to_string(),
rows_result(&["a"], vec![json!([1])]),
);
results.insert(
"SELECT a FROM t WHERE id = 2".to_string(),
rows_result(&["a"], vec![json!([2])]),
);
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[
nominated("SELECT a FROM t WHERE id = 1"),
nominated("SELECT a FROM t WHERE id = 1"),
nominated("SELECT a FROM t WHERE id = 2"),
],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, Some(0));
assert!(!decision.tied);
assert_eq!(decision.fanout, vec![None, None, None]);
assert_eq!(executor.calls().len(), 3);
assert!(
!executor.calls().iter().any(|s| s.contains("COUNT(*)")),
"no probe statement should run when the vote is decisive"
);
}
fn tied_join_candidates() -> (String, String, FanoutProbe, FanoutProbe) {
let flagged =
"SELECT SUM(o.amount) AS total FROM orders o JOIN lines l ON l.order_id = o.id".to_string();
let cleared = "SELECT SUM(o.amount) AS total FROM orders o JOIN lines l ON l.order_id = o.id WHERE o.status = 'open'"
.to_string();
let probe_flagged = fanout_probe(&flagged, D).expect("flagged candidate yields a probe");
let probe_cleared = fanout_probe(&cleared, D).expect("cleared candidate yields a probe");
(flagged, cleared, probe_flagged, probe_cleared)
}
#[tokio::test]
async fn tie_broken_when_one_group_cleared_and_others_flagged() {
let (flagged, cleared, probe_flagged, probe_cleared) = tied_join_candidates();
let mut results = HashMap::new();
results.insert(flagged.clone(), rows_result(&["total"], vec![json!([100])]));
results.insert(cleared.clone(), rows_result(&["total"], vec![json!([200])]));
register_probe(&mut results, &probe_flagged, 10, 5);
register_probe(&mut results, &probe_cleared, 5, 5);
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[nominated(&flagged), nominated(&cleared)],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, Some(1));
assert!(decision.tied);
assert!(decision.probe_broke_tie);
assert_eq!(decision.fanout, vec![Some(true), Some(false)]);
}
#[tokio::test]
async fn tie_not_broken_when_no_group_is_cleared() {
let (flagged, cleared, probe_flagged, probe_cleared) = tied_join_candidates();
let mut results = HashMap::new();
results.insert(flagged.clone(), rows_result(&["total"], vec![json!([100])]));
results.insert(cleared.clone(), rows_result(&["total"], vec![json!([200])]));
register_probe(&mut results, &probe_flagged, 10, 5);
register_probe(&mut results, &probe_cleared, 9, 5); let executor = ScriptedExecutor::new(results);
let decision = decide(
&[nominated(&flagged), nominated(&cleared)],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, None);
assert!(decision.tied);
assert!(!decision.probe_broke_tie);
assert_eq!(decision.fanout, vec![Some(true), Some(true)]);
}
#[tokio::test]
async fn tie_not_broken_when_several_groups_are_cleared() {
let (flagged, cleared, probe_flagged, probe_cleared) = tied_join_candidates();
let mut results = HashMap::new();
results.insert(flagged.clone(), rows_result(&["total"], vec![json!([100])]));
results.insert(cleared.clone(), rows_result(&["total"], vec![json!([200])]));
register_probe(&mut results, &probe_flagged, 5, 5); register_probe(&mut results, &probe_cleared, 5, 5); let executor = ScriptedExecutor::new(results);
let decision = decide(
&[nominated(&flagged), nominated(&cleared)],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, None);
assert!(decision.tied);
assert!(!decision.probe_broke_tie);
assert_eq!(decision.fanout, vec![Some(false), Some(false)]);
}
#[tokio::test]
async fn tie_not_broken_when_a_probe_statement_fails() {
let (flagged, cleared, probe_flagged, probe_cleared) = tied_join_candidates();
let mut results = HashMap::new();
results.insert(flagged.clone(), rows_result(&["total"], vec![json!([100])]));
results.insert(cleared.clone(), rows_result(&["total"], vec![json!([200])]));
register_probe(&mut results, &probe_flagged, 10, 5); results.insert(probe_cleared.joined_rows.clone(), count_result(5));
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[nominated(&flagged), nominated(&cleared)],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.fanout, vec![Some(true), None]);
assert_eq!(decision.winner, None);
assert!(decision.tied);
assert!(!decision.probe_broke_tie);
}
#[tokio::test]
async fn tie_not_broken_when_probe_cannot_be_built() {
let join_sql =
"SELECT SUM(o.amount) AS total FROM orders o JOIN lines l ON l.order_id = o.id".to_string();
let plain_sql = "SELECT 1 AS total".to_string();
let probe_join = fanout_probe(&join_sql, D).expect("join candidate yields a probe");
assert!(
fanout_probe(&plain_sql, D).is_none(),
"plain aggregate has no probe"
);
let mut results = HashMap::new();
results.insert(
join_sql.clone(),
rows_result(&["total"], vec![json!([100])]),
);
results.insert(
plain_sql.clone(),
rows_result(&["total"], vec![json!([200])]),
);
register_probe(&mut results, &probe_join, 5, 5); let executor = ScriptedExecutor::new(results);
let decision = decide(
&[nominated(&join_sql), nominated(&plain_sql)],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.fanout, vec![Some(false), None]);
assert_eq!(decision.winner, None);
assert!(decision.tied);
assert!(!decision.probe_broke_tie);
}
#[tokio::test]
async fn cancelled_before_any_call_returns_no_winner() {
let token = CancellationToken::new();
token.cancel();
let executor = ScriptedExecutor::new(HashMap::new());
let decision = decide(
&[nominated("SELECT 1 AS a"), nominated("SELECT 2 AS a")],
&executor,
D,
&token,
)
.await;
assert_eq!(decision.winner, None);
assert!(!decision.tied);
assert!(executor.calls().is_empty());
}
#[tokio::test]
async fn cancelled_before_a_candidate_call_returns_no_winner() {
let token = CancellationToken::new();
let mut results = HashMap::new();
results.insert(
"SELECT 1 AS a".to_string(),
rows_result(&["a"], vec![json!([1])]),
);
let executor = ScriptedExecutor::new(results).cancel_after(1, token.clone());
let decision = decide(
&[nominated("SELECT 1 AS a"), nominated("SELECT 2 AS a")],
&executor,
D,
&token,
)
.await;
assert_eq!(decision.winner, None);
assert!(!decision.tied);
assert_eq!(executor.calls().len(), 1);
}
#[tokio::test]
async fn cancelled_during_probe_phase_returns_no_winner() {
let (flagged, cleared, probe_flagged, _probe_cleared) = tied_join_candidates();
let token = CancellationToken::new();
let mut results = HashMap::new();
results.insert(flagged.clone(), rows_result(&["total"], vec![json!([100])]));
results.insert(cleared.clone(), rows_result(&["total"], vec![json!([200])]));
results.insert(probe_flagged.joined_rows.clone(), count_result(10));
let executor = ScriptedExecutor::new(results).cancel_after(3, token.clone());
let decision = decide(
&[nominated(&flagged), nominated(&cleared)],
&executor,
D,
&token,
)
.await;
assert_eq!(decision.winner, None);
assert_eq!(executor.calls().len(), 3);
}
#[tokio::test]
async fn empty_slice_returns_without_panicking() {
let executor = ScriptedExecutor::new(HashMap::new());
let decision = decide(&[], &executor, D, &CancellationToken::new()).await;
assert_eq!(decision.winner, None);
assert_eq!(decision.votes, 0);
assert!(!decision.tied);
assert!(executor.calls().is_empty());
}
#[tokio::test]
async fn single_none_nomination_returns_no_winner() {
let executor = ScriptedExecutor::new(HashMap::new());
let decision = decide(&[None], &executor, D, &CancellationToken::new()).await;
assert_eq!(decision.winner, None);
assert_eq!(decision.votes, 0);
assert!(executor.calls().is_empty());
}
#[tokio::test]
async fn all_none_nominations_return_no_winner() {
let executor = ScriptedExecutor::new(HashMap::new());
let decision = decide(&[None, None, None], &executor, D, &CancellationToken::new()).await;
assert_eq!(decision.winner, None);
assert_eq!(decision.votes, 0);
assert!(executor.calls().is_empty());
}
#[tokio::test]
async fn single_successful_candidate_wins_without_a_probe() {
let mut results = HashMap::new();
results.insert(
"SELECT 1 AS a".to_string(),
rows_result(&["a"], vec![json!([1])]),
);
let executor = ScriptedExecutor::new(results);
let decision = decide(
&[nominated("SELECT 1 AS a")],
&executor,
D,
&CancellationToken::new(),
)
.await;
assert_eq!(decision.winner, Some(0));
assert_eq!(decision.votes, 1);
assert_eq!(decision.margin, 1);
assert!(!decision.tied);
assert_eq!(executor.calls().len(), 1);
assert_eq!(decision.fanout, vec![None]);
}