use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use super::*;
use crate::statistics::{ColumnStatistics, TableStatistics};
use crate::Row;
fn create_test_table_stats(row_count: usize) -> TableStatistics {
let schema = TableSchema::new(
"test_table".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
let rows: Vec<Row> =
(0..row_count).map(|i| Row::new(vec![SqlValue::Integer(i as i64)])).collect();
TableStatistics::compute(&rows, &schema)
}
#[test]
fn test_table_scan_cost() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let cost = estimator.estimate_table_scan(&table_stats);
assert!((cost - 20.0).abs() < 0.1);
}
#[test]
fn test_index_scan_high_selectivity() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let col_stats = table_stats.columns.get("id").unwrap();
let cost = estimator.estimate_index_scan(&table_stats, col_stats, 0.5);
assert!(cost > 100.0);
}
#[test]
fn test_index_scan_low_selectivity() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let col_stats = table_stats.columns.get("id").unwrap();
let cost = estimator.estimate_index_scan(&table_stats, col_stats, 0.01);
assert!(cost < 50.0);
}
#[test]
fn test_choose_access_method_favors_index_for_low_selectivity() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let col_stats = table_stats.columns.get("id").unwrap();
let method = estimator.choose_access_method(&table_stats, Some(col_stats), 0.001);
assert!(method.is_index_scan());
}
#[test]
fn test_choose_access_method_favors_table_scan_for_high_selectivity() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let col_stats = table_stats.columns.get("id").unwrap();
let method = estimator.choose_access_method(&table_stats, Some(col_stats), 0.9);
assert!(!method.is_index_scan());
}
#[test]
fn test_choose_access_method_no_index() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let method = estimator.choose_access_method(&table_stats, None, 0.1);
assert!(!method.is_index_scan());
}
#[test]
fn test_insert_cost_basic() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(1, 0, false, 0.0, 64);
let cost = estimator.estimate_insert(100, &table_stats, &index_info);
assert!(cost > 27.0 && cost < 29.0, "Insert cost was {}", cost);
}
#[test]
fn test_insert_cost_with_btree_indexes() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(1, 2, false, 0.0, 64);
let cost = estimator.estimate_insert(100, &table_stats, &index_info);
let cost_no_btree =
estimator.estimate_insert(100, &table_stats, &TableIndexInfo::new(1, 0, false, 0.0, 64));
assert!(cost > cost_no_btree, "B-tree indexes should increase cost");
}
#[test]
fn test_insert_cost_native_columnar() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let row_index_info = TableIndexInfo::new(1, 0, false, 0.0, 64);
let columnar_index_info = TableIndexInfo::new(1, 0, true, 0.0, 64);
let row_cost = estimator.estimate_insert(10, &table_stats, &row_index_info);
let columnar_cost = estimator.estimate_insert(10, &table_stats, &columnar_index_info);
assert!(
columnar_cost > row_cost,
"Columnar insert cost {} should be > row cost {}",
columnar_cost,
row_cost
);
}
#[test]
fn test_update_cost_basic() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(1, 1, false, 0.0, 64);
let full_cost = estimator.estimate_update(50, &table_stats, &index_info, 1.0);
let selective_cost = estimator.estimate_update(50, &table_stats, &index_info, 0.0);
assert!(
full_cost > selective_cost,
"Full update cost {} should be > selective update cost {}",
full_cost,
selective_cost
);
}
#[test]
fn test_update_cost_scales_with_affected_ratio() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(2, 3, false, 0.0, 64);
let cost_0 = estimator.estimate_update(100, &table_stats, &index_info, 0.0);
let cost_50 = estimator.estimate_update(100, &table_stats, &index_info, 0.5);
let cost_100 = estimator.estimate_update(100, &table_stats, &index_info, 1.0);
assert!(cost_50 > cost_0, "50% affected should cost more than 0%");
assert!(cost_100 > cost_50, "100% affected should cost more than 50%");
}
#[test]
fn test_delete_cost_basic() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(1, 1, false, 0.0, 64);
let cost = estimator.estimate_delete(100, &table_stats, &index_info);
assert!(cost > 0.0, "Delete cost should be positive");
assert!(cost < 100.0, "Delete cost should be reasonable");
}
#[test]
fn test_delete_cost_with_compaction() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info_40 = TableIndexInfo::new(1, 0, false, 0.0, 64);
let cost_40 = estimator.estimate_delete(400, &table_stats, &index_info_40);
let index_info_trigger = TableIndexInfo::new(1, 0, false, 0.45, 64);
let cost_trigger = estimator.estimate_delete(100, &table_stats, &index_info_trigger);
assert!(
cost_trigger > cost_40 * 0.1,
"Delete with compaction {} should have meaningful overhead vs large delete without {}",
cost_trigger,
cost_40
);
}
#[test]
fn test_delete_more_expensive_with_more_indexes() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let no_indexes = TableIndexInfo::new(0, 0, false, 0.0, 64);
let many_indexes = TableIndexInfo::new(2, 5, false, 0.0, 64);
let cost_no_indexes = estimator.estimate_delete(100, &table_stats, &no_indexes);
let cost_many_indexes = estimator.estimate_delete(100, &table_stats, &many_indexes);
assert!(
cost_many_indexes > cost_no_indexes,
"More indexes should increase delete cost: {} vs {}",
cost_many_indexes,
cost_no_indexes
);
}
#[test]
fn test_delete_cheaper_than_insert() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(1, 2, false, 0.0, 64);
let delete_cost = estimator.estimate_delete(100, &table_stats, &index_info);
let insert_cost = estimator.estimate_insert(100, &table_stats, &index_info);
assert!(
delete_cost < insert_cost,
"Delete {} should be cheaper than insert {} (without compaction)",
delete_cost,
insert_cost
);
}
#[test]
fn test_dml_costs_scale_with_row_count() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let index_info = TableIndexInfo::new(1, 1, false, 0.0, 64);
let insert_10 = estimator.estimate_insert(10, &table_stats, &index_info);
let insert_100 = estimator.estimate_insert(100, &table_stats, &index_info);
let delete_10 = estimator.estimate_delete(10, &table_stats, &index_info);
let delete_100 = estimator.estimate_delete(100, &table_stats, &index_info);
assert!(insert_100 > insert_10 * 5.0, "Insert should scale with rows");
assert!(delete_100 > delete_10 * 5.0, "Delete should scale with rows");
}
#[test]
fn test_wal_cost_included_in_insert() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(0, 0, false, 0.0, 64);
let cost = estimator.estimate_insert(100, &table_stats, &index_info);
assert!(cost > 22.0, "Insert cost should include WAL: {}", cost);
let tuple_plus_columnar = 100.0 * 0.1 + 0.1; let wal_cost = 100.0 * 0.12 + 0.5; assert!(
wal_cost > tuple_plus_columnar,
"WAL cost ({}) should exceed base tuple cost ({})",
wal_cost,
tuple_plus_columnar
);
}
#[test]
fn test_wal_cost_included_in_update() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(0, 0, false, 0.0, 64);
let cost = estimator.estimate_update(50, &table_stats, &index_info, 0.0);
assert!(cost > 10.0, "Update cost should include WAL: {}", cost);
}
#[test]
fn test_wal_cost_included_in_delete() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let index_info = TableIndexInfo::new(0, 0, false, 0.0, 64);
let cost = estimator.estimate_delete(100, &table_stats, &index_info);
assert!(cost > 17.0, "Delete cost should include WAL: {}", cost);
}
#[test]
fn test_wal_cost_dominant_in_delete() {
let estimator = CostEstimator::default();
let _table_stats = create_test_table_stats(1000);
let _index_info = TableIndexInfo::new(1, 0, false, 0.0, 64);
let rows = 100.0;
let tuple_cost = rows * estimator.delete_tuple_cost; let hash_cost = rows * 1.0 * estimator.hash_index_update_cost; let wal_cost = rows * estimator.wal_write_cost + estimator.wal_sync_cost;
let base_dml_cost = tuple_cost + hash_cost;
assert!(
wal_cost > base_dml_cost,
"WAL cost ({}) should exceed base DML cost ({}) per profiling data",
wal_cost,
base_dml_cost
);
}
#[test]
fn test_wal_sync_cost_amortized_for_batches() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let index_info = TableIndexInfo::new(1, 0, false, 0.0, 64);
let cost_1 = estimator.estimate_insert(1, &table_stats, &index_info);
let cost_100 = estimator.estimate_insert(100, &table_stats, &index_info);
let per_row_single = cost_1;
let per_row_batch = cost_100 / 100.0;
assert!(
per_row_batch < per_row_single,
"Batch insert per-row cost ({}) should be less than single-row cost ({}) due to amortized WAL sync",
per_row_batch,
per_row_single
);
}
#[test]
fn test_wal_cost_proportional_to_rows() {
let estimator = CostEstimator::default();
let wal_10 = 10.0 * estimator.wal_write_cost;
let wal_100 = 100.0 * estimator.wal_write_cost;
assert!(
(wal_100 - wal_10 * 10.0).abs() < 0.001,
"WAL write cost should scale linearly: 10x rows should be 10x cost"
);
}
#[test]
fn test_estimate_type_size_fixed_types() {
assert_eq!(estimate_type_size(&DataType::Boolean), 1);
assert_eq!(estimate_type_size(&DataType::Smallint), 2);
assert_eq!(estimate_type_size(&DataType::Integer), 4);
assert_eq!(estimate_type_size(&DataType::Bigint), 8);
assert_eq!(estimate_type_size(&DataType::Unsigned), 8);
assert_eq!(estimate_type_size(&DataType::Real), 4);
assert_eq!(estimate_type_size(&DataType::DoublePrecision), 8);
assert_eq!(estimate_type_size(&DataType::Float { precision: 24 }), 4);
assert_eq!(estimate_type_size(&DataType::Float { precision: 53 }), 8);
assert_eq!(estimate_type_size(&DataType::Date), 4);
assert_eq!(estimate_type_size(&DataType::Time { with_timezone: false }), 8);
assert_eq!(estimate_type_size(&DataType::Timestamp { with_timezone: false }), 8);
}
#[test]
fn test_estimate_type_size_variable_types() {
assert_eq!(
estimate_type_size(&DataType::Varchar { max_length: Some(100) }),
32 );
assert_eq!(
estimate_type_size(&DataType::Varchar { max_length: Some(20) }),
10 );
assert_eq!(
estimate_type_size(&DataType::Varchar { max_length: None }),
32 );
assert_eq!(estimate_type_size(&DataType::Character { length: 50 }), 50);
assert_eq!(estimate_type_size(&DataType::BinaryLargeObject), 128);
assert_eq!(estimate_type_size(&DataType::CharacterLargeObject), 64);
}
#[test]
fn test_estimate_type_size_vector() {
assert_eq!(estimate_type_size(&DataType::Vector { dimensions: 128 }), 128 * 8);
assert_eq!(estimate_type_size(&DataType::Vector { dimensions: 512 }), 512 * 8);
}
#[test]
fn test_estimate_row_size() {
let small_row = vec![DataType::Integer, DataType::Boolean];
let size = estimate_row_size(&small_row);
assert_eq!(size, 64);
let medium_row = vec![
DataType::Integer,
DataType::Bigint,
DataType::Varchar { max_length: Some(100) },
DataType::Timestamp { with_timezone: false },
DataType::Boolean,
];
let size = estimate_row_size(&medium_row);
assert_eq!(size, 64);
let large_row = vec![
DataType::Integer,
DataType::Bigint,
DataType::DoublePrecision,
DataType::Varchar { max_length: Some(200) },
DataType::Varchar { max_length: Some(200) },
DataType::Varchar { max_length: Some(200) },
DataType::Timestamp { with_timezone: false },
DataType::Decimal { precision: 18, scale: 2 },
DataType::Boolean,
DataType::Character { length: 100 },
];
let size = estimate_row_size(&large_row);
assert_eq!(size, 249);
}
#[test]
fn test_wal_size_factor_small_rows() {
let info = TableIndexInfo::new(1, 0, false, 0.0, 64);
assert!((info.wal_size_factor() - 1.0).abs() < 0.01);
let info = TableIndexInfo::new(1, 0, false, 0.0, 32);
assert!((info.wal_size_factor() - 1.0).abs() < 0.01);
}
#[test]
fn test_wal_size_factor_medium_rows() {
let info = TableIndexInfo::new(1, 0, false, 0.0, 128);
assert!((info.wal_size_factor() - 2.0).abs() < 0.01);
let info = TableIndexInfo::new(1, 0, false, 0.0, 256);
assert!((info.wal_size_factor() - 4.0).abs() < 0.01);
}
#[test]
fn test_wal_size_factor_large_rows_capped() {
let info = TableIndexInfo::new(1, 0, false, 0.0, 960); assert!((info.wal_size_factor() - 10.0).abs() < 0.01);
let info = TableIndexInfo::new(1, 0, false, 0.0, 10000);
assert!((info.wal_size_factor() - 10.0).abs() < 0.01);
}
#[test]
fn test_insert_wal_cost_scales_with_row_size() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let small_info = TableIndexInfo::new(1, 0, false, 0.0, 64);
let small_cost = estimator.estimate_insert(100, &table_stats, &small_info);
let large_info = TableIndexInfo::new(1, 0, false, 0.0, 256);
let large_cost = estimator.estimate_insert(100, &table_stats, &large_info);
assert!(
large_cost > small_cost,
"Large row insert cost ({}) should be higher than small row cost ({})",
large_cost,
small_cost
);
let cost_diff = large_cost - small_cost;
assert!(
cost_diff > 30.0 && cost_diff < 40.0,
"Cost difference ({}) should be approximately 3x WAL base cost",
cost_diff
);
}
#[test]
fn test_update_wal_cost_scales_with_row_size() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let small_info = TableIndexInfo::new(1, 0, false, 0.0, 64);
let small_cost = estimator.estimate_update(50, &table_stats, &small_info, 0.0);
let large_info = TableIndexInfo::new(1, 0, false, 0.0, 320);
let large_cost = estimator.estimate_update(50, &table_stats, &large_info, 0.0);
assert!(
large_cost > small_cost,
"Large row update cost ({}) should be higher than small row cost ({})",
large_cost,
small_cost
);
}
#[test]
fn test_delete_wal_cost_scales_with_row_size() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let small_info = TableIndexInfo::new(0, 0, false, 0.0, 64);
let small_cost = estimator.estimate_delete(100, &table_stats, &small_info);
let large_info = TableIndexInfo::new(0, 0, false, 0.0, 640);
let large_cost = estimator.estimate_delete(100, &table_stats, &large_info);
assert!(
large_cost > small_cost,
"Large row delete cost ({}) should be higher than small row cost ({})",
large_cost,
small_cost
);
}
#[test]
fn test_2_column_vs_50_column_wal_cost() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let small_row_size =
estimate_row_size(&[DataType::Integer, DataType::Varchar { max_length: Some(50) }]);
assert_eq!(small_row_size, 64);
let large_columns: Vec<DataType> = (0..10)
.map(|_| DataType::Integer)
.chain((0..10).map(|_| DataType::Bigint))
.chain((0..10).map(|_| DataType::DoublePrecision))
.chain((0..10).map(|_| DataType::Varchar { max_length: Some(100) }))
.chain((0..10).map(|_| DataType::Timestamp { with_timezone: false }))
.collect();
assert_eq!(large_columns.len(), 50);
let large_row_size = estimate_row_size(&large_columns);
assert!(large_row_size > 500, "Large row should be > 500 bytes, got {}", large_row_size);
let small_info = TableIndexInfo::new(1, 0, false, 0.0, small_row_size);
let large_info = TableIndexInfo::new(1, 0, false, 0.0, large_row_size);
let small_insert = estimator.estimate_insert(100, &table_stats, &small_info);
let large_insert = estimator.estimate_insert(100, &table_stats, &large_info);
assert!(
large_insert > small_insert,
"50-column table insert cost ({}) should exceed 2-column table cost ({})",
large_insert,
small_insert
);
let factor = large_info.wal_size_factor() / small_info.wal_size_factor();
assert!(factor >= 9.0, "WAL size factor ratio ({}) should be at least 9x", factor);
}
#[test]
fn test_skip_scan_cost_low_cardinality_prefix() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let prefix_stats = ColumnStatistics {
n_distinct: 10,
null_count: 0,
min_value: Some(SqlValue::Integer(1)),
max_value: Some(SqlValue::Integer(10)),
most_common_values: vec![],
histogram: None,
};
let cost = estimator.estimate_skip_scan_cost(&table_stats, &prefix_stats, 0.01);
let table_scan_cost = estimator.estimate_table_scan(&table_stats);
assert!(
cost < table_scan_cost,
"Skip-scan cost ({}) should be cheaper than table scan ({}) with low prefix cardinality",
cost,
table_scan_cost
);
}
#[test]
fn test_skip_scan_cost_high_cardinality_prefix() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let prefix_stats = ColumnStatistics {
n_distinct: 1000,
null_count: 0,
min_value: Some(SqlValue::Integer(1)),
max_value: Some(SqlValue::Integer(1000)),
most_common_values: vec![],
histogram: None,
};
let cost = estimator.estimate_skip_scan_cost(&table_stats, &prefix_stats, 0.01);
let table_scan_cost = estimator.estimate_table_scan(&table_stats);
assert!(
cost > table_scan_cost,
"Skip-scan cost ({}) should be more expensive than table scan ({}) with high prefix cardinality",
cost,
table_scan_cost
);
}
#[test]
fn test_skip_scan_cost_scales_with_prefix_cardinality() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let low_card_stats = ColumnStatistics {
n_distinct: 10,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let high_card_stats = ColumnStatistics {
n_distinct: 100,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let cost_low = estimator.estimate_skip_scan_cost(&table_stats, &low_card_stats, 0.01);
let cost_high = estimator.estimate_skip_scan_cost(&table_stats, &high_card_stats, 0.01);
assert!(
cost_high > cost_low,
"Skip-scan cost with high cardinality ({}) should exceed low cardinality cost ({})",
cost_high,
cost_low
);
}
#[test]
fn test_skip_scan_cost_scales_with_filter_selectivity() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let prefix_stats = ColumnStatistics {
n_distinct: 10,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let cost_selective = estimator.estimate_skip_scan_cost(&table_stats, &prefix_stats, 0.001);
let cost_broad = estimator.estimate_skip_scan_cost(&table_stats, &prefix_stats, 0.1);
assert!(
cost_broad > cost_selective,
"Skip-scan cost with broad filter ({}) should exceed selective filter cost ({})",
cost_broad,
cost_selective
);
}
#[test]
fn test_should_use_skip_scan_decision() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let low_card_stats = ColumnStatistics {
n_distinct: 5,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
assert!(
estimator.should_use_skip_scan(&table_stats, &low_card_stats, 0.01),
"Skip-scan should be chosen with low prefix cardinality and selective filter"
);
let high_card_stats = ColumnStatistics {
n_distinct: 5000,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
assert!(
!estimator.should_use_skip_scan(&table_stats, &high_card_stats, 0.01),
"Skip-scan should NOT be chosen with high prefix cardinality"
);
}
#[test]
fn test_skip_scan_break_even_point() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let selectivity = 0.1;
let mut threshold_cardinality = 0;
for cardinality in [5, 10, 25, 50, 100, 200, 500, 1000] {
let prefix_stats = ColumnStatistics {
n_distinct: cardinality,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
if estimator.should_use_skip_scan(&table_stats, &prefix_stats, selectivity) {
threshold_cardinality = cardinality;
} else {
break;
}
}
assert!(
threshold_cardinality > 0,
"Skip-scan should be beneficial for at least some low cardinalities"
);
assert!(
threshold_cardinality < 1000,
"Skip-scan should not be beneficial for very high cardinalities"
);
}
#[test]
fn test_multi_column_skip_scan_cost_single_column_delegates() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let prefix_stats = ColumnStatistics {
n_distinct: 10,
null_count: 0,
min_value: Some(SqlValue::Integer(1)),
max_value: Some(SqlValue::Integer(10)),
most_common_values: vec![],
histogram: None,
};
let single_col_cost = estimator.estimate_skip_scan_cost(&table_stats, &prefix_stats, 0.01);
let multi_col_cost =
estimator.estimate_skip_scan_cost_multi_column(&table_stats, &[&prefix_stats], 0.01);
assert!(
(single_col_cost - multi_col_cost).abs() < 0.001,
"Single-column and multi-column costs should match for single column: {} vs {}",
single_col_cost,
multi_col_cost
);
}
#[test]
fn test_multi_column_skip_scan_cost_increases_with_columns() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let col1_stats = ColumnStatistics {
n_distinct: 10,
null_count: 0,
min_value: Some(SqlValue::Integer(1)),
max_value: Some(SqlValue::Integer(10)),
most_common_values: vec![],
histogram: None,
};
let col2_stats = ColumnStatistics {
n_distinct: 20,
null_count: 0,
min_value: Some(SqlValue::Integer(1)),
max_value: Some(SqlValue::Integer(20)),
most_common_values: vec![],
histogram: None,
};
let col3_stats = ColumnStatistics {
n_distinct: 50,
null_count: 0,
min_value: Some(SqlValue::Integer(1)),
max_value: Some(SqlValue::Integer(50)),
most_common_values: vec![],
histogram: None,
};
let cost_1_col =
estimator.estimate_skip_scan_cost_multi_column(&table_stats, &[&col1_stats], 0.01);
let cost_2_col = estimator.estimate_skip_scan_cost_multi_column(
&table_stats,
&[&col1_stats, &col2_stats],
0.01,
);
let cost_3_col = estimator.estimate_skip_scan_cost_multi_column(
&table_stats,
&[&col1_stats, &col2_stats, &col3_stats],
0.01,
);
assert!(
cost_2_col > cost_1_col,
"2-column skip cost ({}) should exceed 1-column cost ({})",
cost_2_col,
cost_1_col
);
assert!(
cost_3_col > cost_2_col,
"3-column skip cost ({}) should exceed 2-column cost ({})",
cost_3_col,
cost_2_col
);
}
#[test]
fn test_multi_column_skip_scan_correlation_adjustment() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(1000);
let col1_stats = ColumnStatistics {
n_distinct: 10,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let col2_stats = ColumnStatistics {
n_distinct: 100,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let col3_stats = ColumnStatistics {
n_distinct: 500,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let cost = estimator.estimate_skip_scan_cost_multi_column(
&table_stats,
&[&col1_stats, &col2_stats, &col3_stats],
0.01,
);
assert!(cost.is_finite(), "Cost should be finite");
assert!(cost > 0.0, "Cost should be positive");
let table_scan_cost = estimator.estimate_table_scan(&table_stats);
assert!(
cost < 500_000.0 * 4.0,
"Correlation should prevent astronomical costs: {} vs max {}",
cost,
500_000.0 * 4.0
);
assert!(
cost > table_scan_cost,
"Skip-scan with high combined cardinality ({}) should cost more than table scan ({})",
cost,
table_scan_cost
);
}
#[test]
fn test_multi_column_skip_scan_empty_stats() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let cost_empty = estimator.estimate_skip_scan_cost_multi_column(&table_stats, &[], 0.01);
let table_scan_cost = estimator.estimate_table_scan(&table_stats);
assert!(
(cost_empty - table_scan_cost).abs() < 0.001,
"Empty prefix should return table scan cost: {} vs {}",
cost_empty,
table_scan_cost
);
}
#[test]
fn test_multi_column_skip_scan_vs_single_column_decision() {
let estimator = CostEstimator::default();
let table_stats = create_test_table_stats(10000);
let col1_high_card = ColumnStatistics {
n_distinct: 100,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let col2_low_card = ColumnStatistics {
n_distinct: 2,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let cost_1_col =
estimator.estimate_skip_scan_cost_multi_column(&table_stats, &[&col1_high_card], 0.01);
let cost_2_col = estimator.estimate_skip_scan_cost_multi_column(
&table_stats,
&[&col1_high_card, &col2_low_card],
0.01,
);
assert!(
cost_2_col >= cost_1_col,
"2-column skip cost ({}) should be >= 1-column cost ({}) with these stats",
cost_2_col,
cost_1_col
);
}
#[test]
fn test_combined_prefix_cardinality_estimation() {
let estimator = CostEstimator::default();
let total_rows = 10000.0;
let col1_stats = ColumnStatistics {
n_distinct: 10,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let cardinality_1 = estimator.estimate_combined_prefix_cardinality(&[&col1_stats], total_rows);
assert!(
(cardinality_1 - 10.0).abs() < 0.01,
"Single column cardinality should match n_distinct: {}",
cardinality_1
);
let col2_stats = ColumnStatistics {
n_distinct: 5,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let cardinality_2 =
estimator.estimate_combined_prefix_cardinality(&[&col1_stats, &col2_stats], total_rows);
assert!(
cardinality_2 > 40.0 && cardinality_2 < 60.0,
"Two-column cardinality should be close to product with low correlation: {}",
cardinality_2
);
let col_high_stats = ColumnStatistics {
n_distinct: 5000,
null_count: 0,
min_value: None,
max_value: None,
most_common_values: vec![],
histogram: None,
};
let cardinality_capped = estimator
.estimate_combined_prefix_cardinality(&[&col_high_stats, &col_high_stats], total_rows);
assert!(
cardinality_capped <= total_rows,
"Combined cardinality ({}) should be capped at total_rows ({})",
cardinality_capped,
total_rows
);
}