use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
pub struct ExecutionStats {
pub estimated_rows: u64,
pub actual_rows: u64,
pub estimated_cost: f64,
pub actual_cost: f64,
}
impl ExecutionStats {
pub fn new(estimated_rows: u64, estimated_cost: f64) -> Self {
ExecutionStats {
estimated_rows,
actual_rows: 0,
estimated_cost,
actual_cost: 0.0,
}
}
pub fn estimation_error(&self) -> f64 {
if self.estimated_rows == 0 {
return 0.0;
}
self.actual_rows as f64 / self.estimated_rows as f64
}
pub fn cost_overrun(&self) -> f64 {
if self.estimated_cost == 0.0 {
return 0.0;
}
self.actual_cost / self.estimated_cost
}
pub fn estimates_wrong(&self) -> bool {
let error = self.estimation_error();
error > 2.0 || error < 0.5
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionHint {
Continue,
Adapt,
Replan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinExecutionStrategy {
NestedLoop,
Hash,
SortMerge,
GraceHash,
}
pub struct AdaptiveExecutionContext {
pub stats: Arc<RwLock<ExecutionStats>>,
pub current_strategy: JoinExecutionStrategy,
pub rows_processed: u64,
pub adaptation_threshold: f64,
}
impl AdaptiveExecutionContext {
pub fn new(estimated_rows: u64, estimated_cost: f64) -> Self {
AdaptiveExecutionContext {
stats: Arc::new(RwLock::new(ExecutionStats::new(estimated_rows, estimated_cost))),
current_strategy: JoinExecutionStrategy::Hash,
rows_processed: 0,
adaptation_threshold: 0.5, }
}
pub fn record_row(&mut self) -> Result<(), String> {
self.rows_processed += 1;
let mut stats = self
.stats
.write()
.map_err(|e| format!("Failed to acquire write lock: {}", e))?;
stats.actual_rows += 1;
Ok(())
}
pub fn record_cost(&mut self, cost: f64) -> Result<(), String> {
let mut stats = self
.stats
.write()
.map_err(|e| format!("Failed to acquire write lock: {}", e))?;
stats.actual_cost += cost;
Ok(())
}
pub fn should_adapt(&self) -> Result<ExecutionHint, String> {
let stats = self
.stats
.read()
.map_err(|e| format!("Failed to acquire read lock: {}", e))?;
if stats.estimates_wrong() {
let error = stats.estimation_error();
if error > 10.0 || error < 0.1 {
Ok(ExecutionHint::Replan)
} else {
Ok(ExecutionHint::Adapt)
}
} else {
Ok(ExecutionHint::Continue)
}
}
pub fn choose_adaptive_join_strategy(&self, left_rows: u64, right_rows: u64) -> JoinExecutionStrategy {
let smaller = left_rows.min(right_rows);
let larger = left_rows.max(right_rows);
if larger < 1000 {
return JoinExecutionStrategy::NestedLoop;
}
if smaller < 100_000 {
return JoinExecutionStrategy::Hash;
}
if smaller < 1_000_000 {
return JoinExecutionStrategy::GraceHash;
}
JoinExecutionStrategy::SortMerge
}
pub fn update_strategy(&mut self, hint: ExecutionHint) {
match hint {
ExecutionHint::Continue => {
}
ExecutionHint::Adapt => {
self.current_strategy = match self.current_strategy {
JoinExecutionStrategy::Hash => JoinExecutionStrategy::SortMerge,
JoinExecutionStrategy::NestedLoop => JoinExecutionStrategy::Hash,
JoinExecutionStrategy::SortMerge => JoinExecutionStrategy::NestedLoop,
JoinExecutionStrategy::GraceHash => JoinExecutionStrategy::SortMerge,
};
}
ExecutionHint::Replan => {
self.current_strategy = JoinExecutionStrategy::SortMerge;
}
}
}
}
pub fn execute_join_adaptive(
left_rows: u64,
right_rows: u64,
context: &mut AdaptiveExecutionContext,
) -> Result<u64, String> {
let mut result_count = 0;
loop {
match context.should_adapt()? {
ExecutionHint::Continue => {
break;
}
ExecutionHint::Adapt => {
context.update_strategy(ExecutionHint::Adapt);
}
ExecutionHint::Replan => {
return Err("Query plan needs replanning".to_string());
}
}
}
match context.current_strategy {
JoinExecutionStrategy::NestedLoop => {
result_count = (left_rows * right_rows) / 10; }
JoinExecutionStrategy::Hash => {
result_count = (left_rows * right_rows) / 10;
}
JoinExecutionStrategy::SortMerge => {
result_count = (left_rows * right_rows) / 10;
}
JoinExecutionStrategy::GraceHash => {
result_count = (left_rows * right_rows) / 10;
}
}
for _ in 0..result_count {
context.record_row()?;
}
Ok(result_count)
}
pub struct AdaptiveQueryPipeline {
pub current_stage: usize,
pub stage_contexts: Vec<AdaptiveExecutionContext>,
}
impl AdaptiveQueryPipeline {
pub fn new(num_stages: usize) -> Self {
AdaptiveQueryPipeline {
current_stage: 0,
stage_contexts: Vec::with_capacity(num_stages),
}
}
pub fn add_stage(&mut self, estimated_rows: u64, estimated_cost: f64) {
self.stage_contexts
.push(AdaptiveExecutionContext::new(estimated_rows, estimated_cost));
}
pub fn execute_next_stage(&mut self) -> Result<ExecutionHint, String> {
if self.current_stage >= self.stage_contexts.len() {
return Err("No more stages to execute".to_string());
}
let context = &self.stage_contexts[self.current_stage];
let hint = context.should_adapt()?;
self.current_stage += 1;
Ok(hint)
}
pub fn get_stats(&self) -> Result<Vec<ExecutionStats>, String> {
let mut all_stats = Vec::new();
for context in &self.stage_contexts {
let stats = context
.stats
.read()
.map_err(|e| format!("Failed to read stats: {}", e))?;
all_stats.push(stats.clone());
}
Ok(all_stats)
}
pub fn needs_replanning(&self) -> Result<bool, String> {
for context in &self.stage_contexts {
match context.should_adapt()? {
ExecutionHint::Replan => return Ok(true),
_ => {}
}
}
Ok(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execution_stats_creation() {
let stats = ExecutionStats::new(1000, 100.0);
assert_eq!(stats.estimated_rows, 1000);
assert_eq!(stats.estimated_cost, 100.0);
assert_eq!(stats.actual_rows, 0);
}
#[test]
fn test_estimation_error() {
let mut stats = ExecutionStats::new(1000, 100.0);
stats.actual_rows = 2000;
assert_eq!(stats.estimation_error(), 2.0);
}
#[test]
fn test_estimates_wrong_too_high() {
let mut stats = ExecutionStats::new(1000, 100.0);
stats.actual_rows = 100;
assert!(stats.estimates_wrong());
}
#[test]
fn test_estimates_wrong_too_low() {
let mut stats = ExecutionStats::new(1000, 100.0);
stats.actual_rows = 3000;
assert!(stats.estimates_wrong());
}
#[test]
fn test_adaptive_context_creation() {
let ctx = AdaptiveExecutionContext::new(1000, 100.0);
assert_eq!(ctx.rows_processed, 0);
assert_eq!(ctx.current_strategy, JoinExecutionStrategy::Hash);
}
#[test]
fn test_record_row() {
let mut ctx = AdaptiveExecutionContext::new(1000, 100.0);
let result = ctx.record_row();
assert!(result.is_ok());
assert_eq!(ctx.rows_processed, 1);
}
#[test]
fn test_choose_adaptive_join_strategy_small() {
let ctx = AdaptiveExecutionContext::new(1000, 100.0);
let strategy = ctx.choose_adaptive_join_strategy(100, 200);
assert_eq!(strategy, JoinExecutionStrategy::NestedLoop);
}
#[test]
fn test_choose_adaptive_join_strategy_medium() {
let ctx = AdaptiveExecutionContext::new(1000, 100.0);
let strategy = ctx.choose_adaptive_join_strategy(10000, 50000);
assert_eq!(strategy, JoinExecutionStrategy::Hash);
}
#[test]
fn test_choose_adaptive_join_strategy_large() {
let ctx = AdaptiveExecutionContext::new(1000, 100.0);
let strategy = ctx.choose_adaptive_join_strategy(10_000_000, 10_000_000);
assert_eq!(strategy, JoinExecutionStrategy::SortMerge);
}
#[test]
fn test_should_adapt_no_error() {
let mut ctx = AdaptiveExecutionContext::new(1000, 100.0);
for _ in 0..1000 {
ctx.record_row().unwrap();
}
ctx.record_cost(100.0).unwrap();
let hint = ctx.should_adapt().unwrap();
assert_eq!(hint, ExecutionHint::Continue);
}
#[test]
fn test_update_strategy() {
let mut ctx = AdaptiveExecutionContext::new(1000, 100.0);
let initial = ctx.current_strategy;
ctx.update_strategy(ExecutionHint::Adapt);
let after_adapt = ctx.current_strategy;
assert_ne!(initial, after_adapt);
}
#[test]
fn test_adaptive_pipeline_creation() {
let pipeline = AdaptiveQueryPipeline::new(3);
assert_eq!(pipeline.current_stage, 0);
assert_eq!(pipeline.stage_contexts.len(), 0);
}
#[test]
fn test_adaptive_pipeline_add_stages() {
let mut pipeline = AdaptiveQueryPipeline::new(3);
pipeline.add_stage(1000, 100.0);
pipeline.add_stage(500, 50.0);
pipeline.add_stage(100, 10.0);
assert_eq!(pipeline.stage_contexts.len(), 3);
}
#[test]
fn test_execute_join_adaptive() {
let mut ctx = AdaptiveExecutionContext::new(100, 10.0);
for _ in 0..100 {
ctx.record_row().unwrap();
}
ctx.record_cost(10.0).unwrap();
let result = execute_join_adaptive(100, 100, &mut ctx);
assert!(result.is_ok());
assert!(result.unwrap() > 0);
}
}