use std::fmt;
use kimberlite_query::QueryResult;
pub mod duckdb;
pub mod kimberlite;
pub use self::duckdb::DuckDbOracle;
pub use self::kimberlite::KimberliteOracle;
pub trait OracleRunner {
fn execute(&mut self, sql: &str) -> Result<QueryResult, OracleError>;
fn reset(&mut self) -> Result<(), OracleError>;
fn name(&self) -> &'static str;
}
#[derive(Debug, thiserror::Error)]
pub enum OracleError {
#[error("SQL syntax error: {0}")]
SyntaxError(String),
#[error("Semantic error: {0}")]
SemanticError(String),
#[error("Runtime error: {0}")]
RuntimeError(String),
#[error("Timeout after {0}ms")]
Timeout(u64),
#[error("Unsupported feature: {0}")]
Unsupported(String),
#[error("Internal error: {0}")]
Internal(String),
}
pub fn compare_results(
left: &QueryResult,
right: &QueryResult,
left_name: &str,
right_name: &str,
) -> Result<(), ResultMismatch> {
if left.columns.len() != right.columns.len() {
return Err(ResultMismatch::ColumnCountMismatch {
left: left.columns.len(),
right: right.columns.len(),
left_name: left_name.to_string(),
right_name: right_name.to_string(),
});
}
for (i, (left_col, right_col)) in left.columns.iter().zip(right.columns.iter()).enumerate() {
if left_col.as_str() != right_col.as_str() {
return Err(ResultMismatch::ColumnNameMismatch {
column_index: i,
left: left_col.as_str().to_string(),
right: right_col.as_str().to_string(),
left_name: left_name.to_string(),
right_name: right_name.to_string(),
});
}
}
if left.rows.len() != right.rows.len() {
return Err(ResultMismatch::RowCountMismatch {
left: left.rows.len(),
right: right.rows.len(),
left_name: left_name.to_string(),
right_name: right_name.to_string(),
});
}
for (row_idx, (left_row, right_row)) in left.rows.iter().zip(right.rows.iter()).enumerate() {
if left_row.len() != right_row.len() {
return Err(ResultMismatch::RowValueCountMismatch {
row_index: row_idx,
left: left_row.len(),
right: right_row.len(),
left_name: left_name.to_string(),
right_name: right_name.to_string(),
});
}
for (col_idx, (left_val, right_val)) in left_row.iter().zip(right_row.iter()).enumerate() {
if left_val != right_val {
return Err(ResultMismatch::ValueMismatch {
row_index: row_idx,
column_index: col_idx,
left: format!("{left_val:?}"),
right: format!("{right_val:?}"),
left_name: left_name.to_string(),
right_name: right_name.to_string(),
});
}
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub enum ResultMismatch {
ColumnCountMismatch {
left: usize,
right: usize,
left_name: String,
right_name: String,
},
ColumnNameMismatch {
column_index: usize,
left: String,
right: String,
left_name: String,
right_name: String,
},
RowCountMismatch {
left: usize,
right: usize,
left_name: String,
right_name: String,
},
RowValueCountMismatch {
row_index: usize,
left: usize,
right: usize,
left_name: String,
right_name: String,
},
ValueMismatch {
row_index: usize,
column_index: usize,
left: String,
right: String,
left_name: String,
right_name: String,
},
}
impl fmt::Display for ResultMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResultMismatch::ColumnCountMismatch {
left,
right,
left_name,
right_name,
} => {
write!(
f,
"Column count mismatch: {left_name}={left}, {right_name}={right}"
)
}
ResultMismatch::ColumnNameMismatch {
column_index,
left,
right,
left_name,
right_name,
} => {
write!(
f,
"Column name mismatch at index {column_index}: {left_name}='{left}', {right_name}='{right}'"
)
}
ResultMismatch::RowCountMismatch {
left,
right,
left_name,
right_name,
} => {
write!(
f,
"Row count mismatch: {left_name}={left}, {right_name}={right}"
)
}
ResultMismatch::RowValueCountMismatch {
row_index,
left,
right,
left_name,
right_name,
} => {
write!(
f,
"Row value count mismatch at row {row_index}: {left_name}={left}, {right_name}={right}"
)
}
ResultMismatch::ValueMismatch {
row_index,
column_index,
left,
right,
left_name,
right_name,
} => {
write!(
f,
"Value mismatch at row {row_index}, column {column_index}: {left_name}={left}, {right_name}={right}"
)
}
}
}
}
impl std::error::Error for ResultMismatch {}
#[cfg(test)]
mod tests {
use super::*;
use kimberlite_query::{ColumnName, Value};
#[test]
fn test_compare_results_identical() {
let result1 = QueryResult {
columns: vec![ColumnName::from("id"), ColumnName::from("name")],
rows: vec![
vec![Value::BigInt(1), Value::Text("Alice".to_string())],
vec![Value::BigInt(2), Value::Text("Bob".to_string())],
],
};
let result2 = result1.clone();
assert!(compare_results(&result1, &result2, "left", "right").is_ok());
}
#[test]
fn test_compare_results_column_count_mismatch() {
let result1 = QueryResult {
columns: vec![ColumnName::from("id"), ColumnName::from("name")],
rows: vec![],
};
let result2 = QueryResult {
columns: vec![ColumnName::from("id")],
rows: vec![],
};
let err = compare_results(&result1, &result2, "left", "right").unwrap_err();
assert!(matches!(err, ResultMismatch::ColumnCountMismatch { .. }));
}
#[test]
fn test_compare_results_column_name_mismatch() {
let result1 = QueryResult {
columns: vec![ColumnName::from("id"), ColumnName::from("name")],
rows: vec![],
};
let result2 = QueryResult {
columns: vec![ColumnName::from("id"), ColumnName::from("email")],
rows: vec![],
};
let err = compare_results(&result1, &result2, "left", "right").unwrap_err();
assert!(matches!(err, ResultMismatch::ColumnNameMismatch { .. }));
}
#[test]
fn test_compare_results_row_count_mismatch() {
let result1 = QueryResult {
columns: vec![ColumnName::from("id")],
rows: vec![vec![Value::BigInt(1)], vec![Value::BigInt(2)]],
};
let result2 = QueryResult {
columns: vec![ColumnName::from("id")],
rows: vec![vec![Value::BigInt(1)]],
};
let err = compare_results(&result1, &result2, "left", "right").unwrap_err();
assert!(matches!(err, ResultMismatch::RowCountMismatch { .. }));
}
#[test]
fn test_compare_results_value_mismatch() {
let result1 = QueryResult {
columns: vec![ColumnName::from("id")],
rows: vec![vec![Value::BigInt(1)]],
};
let result2 = QueryResult {
columns: vec![ColumnName::from("id")],
rows: vec![vec![Value::BigInt(2)]],
};
let err = compare_results(&result1, &result2, "left", "right").unwrap_err();
assert!(matches!(err, ResultMismatch::ValueMismatch { .. }));
}
}