use xlog_core::RelId;
use xlog_ir::rir::{LookupPerm, ProjectExpr, VariableOrder};
use xlog_stats::StatsManager;
use crate::compiler_config::{CompilerConfig, WcojVarOrderingKind};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WcojCostGateParams {
pub wcoj_to_hash_cost_ratio_ceiling: f64,
}
pub const WCOJ_COST_GATE_PARAMS: WcojCostGateParams = WcojCostGateParams {
wcoj_to_hash_cost_ratio_ceiling: 1.0,
};
pub fn wcoj_cost_gate_predicts_wcoj(wcoj_cost: f64, hash_cost: f64) -> bool {
if !wcoj_cost.is_finite() || !hash_cost.is_finite() || hash_cost <= 0.0 {
return false;
}
wcoj_cost / hash_cost <= WCOJ_COST_GATE_PARAMS.wcoj_to_hash_cost_ratio_ceiling
}
pub trait WcojVariableOrderingModel {
fn pick_triangle_leader(
&self,
rel_ids: [RelId; 3],
stats: &StatsManager,
config: &CompilerConfig,
) -> Option<u8>;
fn pick_4cycle_leader(
&self,
rel_ids: [RelId; 4],
stats: &StatsManager,
config: &CompilerConfig,
) -> Option<u8>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LeaderCardinalityModel;
impl LeaderCardinalityModel {
fn card_of(stats: &StatsManager, rel: RelId) -> Option<u64> {
let s = stats.get_relation_stats(rel)?;
if s.cardinality == 0 {
None
} else {
Some(s.cardinality)
}
}
fn argmin_card<const N: usize>(
rel_ids: &[RelId; N],
stats: &StatsManager,
) -> Option<(u8, u64)> {
let mut best: Option<(u8, u64)> = None;
for (idx, rel) in rel_ids.iter().enumerate() {
let card = Self::card_of(stats, *rel)?;
best = match best {
None => Some((idx as u8, card)),
Some((_, b)) if card < b => Some((idx as u8, card)),
Some(prev) => Some(prev),
};
}
best
}
fn pick_leader<const N: usize>(
&self,
rel_ids: [RelId; N],
stats: &StatsManager,
config: &CompilerConfig,
) -> Option<u8> {
if config.wcoj_variable_ordering == WcojVarOrderingKind::Disabled {
return None;
}
let (min_idx, min_card) = Self::argmin_card(&rel_ids, stats)?;
if min_idx == 0 {
return None;
}
let default_card = Self::card_of(stats, rel_ids[0])?;
let ratio = min_card as f64 / default_card as f64;
let threshold = config.effective_wcoj_var_ordering_threshold();
if ratio <= threshold {
Some(min_idx)
} else {
None
}
}
}
impl WcojVariableOrderingModel for LeaderCardinalityModel {
fn pick_triangle_leader(
&self,
rel_ids: [RelId; 3],
stats: &StatsManager,
config: &CompilerConfig,
) -> Option<u8> {
self.pick_leader(rel_ids, stats, config)
}
fn pick_4cycle_leader(
&self,
rel_ids: [RelId; 4],
stats: &StatsManager,
config: &CompilerConfig,
) -> Option<u8> {
self.pick_leader(rel_ids, stats, config)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HeatAwareLeaderModel;
impl HeatAwareLeaderModel {
pub const HEAT_WEIGHT: f32 = 4.0;
pub const NO_OBSERVED_SEL: f64 = 1.0;
pub const SEL_FLOOR: f64 = 0.01;
fn card_of(stats: &StatsManager, rel: RelId) -> Option<u64> {
let s = stats.get_relation_stats(rel)?;
if s.cardinality == 0 {
None
} else {
Some(s.cardinality)
}
}
fn heat_of(stats: &StatsManager, rel: RelId) -> f32 {
stats.get_relation_stats(rel).map(|s| s.heat).unwrap_or(0.0)
}
fn observed_sel_or_one(
stats: &StatsManager,
rel_a: RelId,
rel_b: RelId,
candidate_left_keys: &[usize],
candidate_right_keys: &[usize],
) -> f64 {
let Some(js) = stats.get_join_selectivity(rel_a, rel_b) else {
return Self::NO_OBSERVED_SEL;
};
let (expected_left_keys, expected_right_keys) = if js.left_rel == rel_a {
(candidate_left_keys, candidate_right_keys)
} else {
(candidate_right_keys, candidate_left_keys)
};
if js.left_keys.as_slice() == expected_left_keys
&& js.right_keys.as_slice() == expected_right_keys
{
js.selectivity
} else {
Self::NO_OBSERVED_SEL
}
}
fn composite_score(
rel: RelId,
card: u64,
stats: &StatsManager,
incident_edges: &[(RelId, &[usize], &[usize])],
) -> f64 {
let heat = Self::heat_of(stats, rel);
let heat_factor = 1.0 + Self::HEAT_WEIGHT as f64 * heat as f64;
let sel_penalty: f64 = incident_edges
.iter()
.map(|(other, lk, rk)| {
let sel = Self::observed_sel_or_one(stats, rel, *other, lk, rk);
1.0 / sel.max(Self::SEL_FLOOR)
})
.sum();
card as f64 * heat_factor * sel_penalty
}
fn pick_leader_from_scores<const N: usize>(
rel_ids: [RelId; N],
scores: [f64; N],
config: &CompilerConfig,
) -> Option<u8> {
if config.wcoj_variable_ordering == WcojVarOrderingKind::Disabled {
return None;
}
if scores.iter().any(|s| !s.is_finite() || *s <= 0.0) {
return None;
}
let _ = rel_ids; let (min_idx, &min_score) = scores
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))?;
if min_idx == 0 {
return None;
}
let default_score = scores[0];
let ratio = min_score / default_score;
let threshold = config.effective_wcoj_var_ordering_threshold();
if ratio <= threshold {
Some(min_idx as u8)
} else {
None
}
}
}
impl WcojVariableOrderingModel for HeatAwareLeaderModel {
fn pick_triangle_leader(
&self,
rel_ids: [RelId; 3],
stats: &StatsManager,
config: &CompilerConfig,
) -> Option<u8> {
let cards: [u64; 3] = match (
Self::card_of(stats, rel_ids[0]),
Self::card_of(stats, rel_ids[1]),
Self::card_of(stats, rel_ids[2]),
) {
(Some(a), Some(b), Some(c)) => [a, b, c],
_ => return None, };
let scores: [f64; 3] = [
Self::composite_score(
rel_ids[0],
cards[0],
stats,
&[(rel_ids[1], &[1], &[0]), (rel_ids[2], &[0], &[0])],
),
Self::composite_score(
rel_ids[1],
cards[1],
stats,
&[(rel_ids[0], &[0], &[1]), (rel_ids[2], &[1], &[1])],
),
Self::composite_score(
rel_ids[2],
cards[2],
stats,
&[(rel_ids[0], &[0], &[0]), (rel_ids[1], &[1], &[1])],
),
];
Self::pick_leader_from_scores(rel_ids, scores, config)
}
fn pick_4cycle_leader(
&self,
rel_ids: [RelId; 4],
stats: &StatsManager,
config: &CompilerConfig,
) -> Option<u8> {
let cards: [u64; 4] = match (
Self::card_of(stats, rel_ids[0]),
Self::card_of(stats, rel_ids[1]),
Self::card_of(stats, rel_ids[2]),
Self::card_of(stats, rel_ids[3]),
) {
(Some(a), Some(b), Some(c), Some(d)) => [a, b, c, d],
_ => return None,
};
let scores: [f64; 4] = [
Self::composite_score(
rel_ids[0],
cards[0],
stats,
&[
(rel_ids[1], &[1], &[0]), (rel_ids[3], &[0], &[1]), ],
),
Self::composite_score(
rel_ids[1],
cards[1],
stats,
&[(rel_ids[0], &[0], &[1]), (rel_ids[2], &[1], &[0])],
),
Self::composite_score(
rel_ids[2],
cards[2],
stats,
&[(rel_ids[1], &[0], &[1]), (rel_ids[3], &[1], &[0])],
),
Self::composite_score(
rel_ids[3],
cards[3],
stats,
&[(rel_ids[2], &[0], &[1]), (rel_ids[0], &[1], &[0])],
),
];
Self::pick_leader_from_scores(rel_ids, scores, config)
}
}
pub fn triangle_lookup_perms(leader_idx: u8) -> Vec<LookupPerm> {
match leader_idx {
0 => vec![
LookupPerm {
input_idx: 1,
swap_cols: false,
},
LookupPerm {
input_idx: 2,
swap_cols: false,
},
],
1 => vec![
LookupPerm {
input_idx: 2,
swap_cols: true,
},
LookupPerm {
input_idx: 0,
swap_cols: true,
},
],
2 => vec![
LookupPerm {
input_idx: 1,
swap_cols: true,
},
LookupPerm {
input_idx: 0,
swap_cols: false,
},
],
_ => panic!("triangle leader_idx must be in [0, 3): got {leader_idx}"),
}
}
pub fn triangle_kernel_output_cols(leader_idx: u8) -> Vec<ProjectExpr> {
match leader_idx {
0 => vec![
ProjectExpr::Column(0),
ProjectExpr::Column(1),
ProjectExpr::Column(2),
],
1 => vec![
ProjectExpr::Column(2),
ProjectExpr::Column(0),
ProjectExpr::Column(1),
],
2 => vec![
ProjectExpr::Column(0),
ProjectExpr::Column(2),
ProjectExpr::Column(1),
],
_ => panic!("triangle leader_idx must be in [0, 3): got {leader_idx}"),
}
}
pub fn cycle4_kernel_output_cols(leader_idx: u8) -> Vec<ProjectExpr> {
match leader_idx {
0 => vec![
ProjectExpr::Column(0),
ProjectExpr::Column(1),
ProjectExpr::Column(2),
ProjectExpr::Column(3),
],
1 => vec![
ProjectExpr::Column(3),
ProjectExpr::Column(0),
ProjectExpr::Column(1),
ProjectExpr::Column(2),
],
2 => vec![
ProjectExpr::Column(2),
ProjectExpr::Column(3),
ProjectExpr::Column(0),
ProjectExpr::Column(1),
],
3 => vec![
ProjectExpr::Column(1),
ProjectExpr::Column(2),
ProjectExpr::Column(3),
ProjectExpr::Column(0),
],
_ => panic!("4-cycle leader_idx must be in [0, 4): got {leader_idx}"),
}
}
pub fn build_triangle_var_order(leader_idx: u8) -> VariableOrder {
VariableOrder::legacy(
leader_idx,
triangle_lookup_perms(leader_idx),
triangle_kernel_output_cols(leader_idx),
)
}
pub fn build_cycle4_var_order(leader_idx: u8) -> VariableOrder {
VariableOrder::legacy(
leader_idx,
cycle4_lookup_perms(leader_idx),
cycle4_kernel_output_cols(leader_idx),
)
}
pub fn cycle4_lookup_perms(leader_idx: u8) -> Vec<LookupPerm> {
if leader_idx >= 4 {
panic!("4-cycle leader_idx must be in [0, 4): got {leader_idx}");
}
(1..4)
.map(|k| LookupPerm {
input_idx: ((leader_idx as usize + k) % 4) as u8,
swap_cols: false,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use xlog_core::RelId;
fn stats_with(cards: &[(RelId, u64)]) -> StatsManager {
let mut mgr = StatsManager::new();
for (rel, card) in cards {
mgr.register_relation(*rel);
if *card > 0 {
mgr.update_cardinality(*rel, *card);
}
}
mgr
}
#[test]
fn disabled_config_returns_none_even_with_smaller_input() {
let stats = stats_with(&[(RelId(1), 1000), (RelId(2), 10), (RelId(3), 1000)]);
let config = CompilerConfig::default(); let m = LeaderCardinalityModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
None,
"Disabled config must short-circuit before the threshold gate"
);
}
#[test]
fn missing_stats_returns_none_safety_floor() {
let stats = stats_with(&[(RelId(1), 1000), (RelId(2), 0), (RelId(3), 1000)]);
let config = CompilerConfig {
wcoj_variable_ordering: WcojVarOrderingKind::LeaderCardinality,
..CompilerConfig::default()
};
let m = LeaderCardinalityModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
None,
"missing-stats safety floor: any None card → None decision"
);
}
#[test]
fn picks_min_when_clearly_under_threshold() {
let stats = stats_with(&[(RelId(1), 1000), (RelId(2), 100), (RelId(3), 800)]);
let config = CompilerConfig {
wcoj_variable_ordering: WcojVarOrderingKind::LeaderCardinality,
..CompilerConfig::default()
};
let m = LeaderCardinalityModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
Some(1)
);
}
#[test]
fn declines_when_min_is_default_leader() {
let stats = stats_with(&[(RelId(1), 100), (RelId(2), 1000), (RelId(3), 1000)]);
let config = CompilerConfig {
wcoj_variable_ordering: WcojVarOrderingKind::LeaderCardinality,
..CompilerConfig::default()
};
let m = LeaderCardinalityModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
None,
"min == default leader → no reorder needed → None"
);
}
#[test]
fn declines_marginal_above_threshold() {
let stats = stats_with(&[(RelId(1), 1000), (RelId(2), 700), (RelId(3), 900)]);
let config = CompilerConfig {
wcoj_variable_ordering: WcojVarOrderingKind::LeaderCardinality,
..CompilerConfig::default()
};
let m = LeaderCardinalityModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
None,
);
}
#[test]
fn picks_4cycle_min_under_threshold() {
let stats = stats_with(&[
(RelId(1), 1000),
(RelId(2), 1000),
(RelId(3), 100),
(RelId(4), 1000),
]);
let config = CompilerConfig {
wcoj_variable_ordering: WcojVarOrderingKind::LeaderCardinality,
..CompilerConfig::default()
};
let m = LeaderCardinalityModel;
assert_eq!(
m.pick_4cycle_leader([RelId(1), RelId(2), RelId(3), RelId(4)], &stats, &config),
Some(2)
);
}
#[test]
fn triangle_lookup_perms_default_leader_is_identity() {
let p = triangle_lookup_perms(0);
assert_eq!(p.len(), 2);
assert_eq!(p[0].input_idx, 1);
assert!(!p[0].swap_cols);
assert_eq!(p[1].input_idx, 2);
assert!(!p[1].swap_cols);
}
#[test]
fn triangle_lookup_perms_leader_yz_double_swap() {
let p = triangle_lookup_perms(1);
assert_eq!(p[0].input_idx, 2);
assert!(p[0].swap_cols);
assert_eq!(p[1].input_idx, 0);
assert!(p[1].swap_cols);
}
#[test]
fn triangle_lookup_perms_leader_xz_partial_swap() {
let p = triangle_lookup_perms(2);
assert_eq!(p[0].input_idx, 1);
assert!(p[0].swap_cols);
assert_eq!(p[1].input_idx, 0);
assert!(!p[1].swap_cols);
}
#[test]
fn cycle4_lookup_perms_default_is_identity_rotation() {
let p = cycle4_lookup_perms(0);
assert_eq!(
p.iter().map(|lp| lp.input_idx).collect::<Vec<_>>(),
vec![1, 2, 3]
);
assert!(p.iter().all(|lp| !lp.swap_cols));
}
#[test]
fn cycle4_lookup_perms_leader_xy_rotates_to_yz_zw_wx() {
let p = cycle4_lookup_perms(1);
assert_eq!(
p.iter().map(|lp| lp.input_idx).collect::<Vec<_>>(),
vec![2, 3, 0]
);
assert!(p.iter().all(|lp| !lp.swap_cols));
}
#[test]
fn cycle4_lookup_perms_leader_zw_rotates_to_wx_xy_yz() {
let p = cycle4_lookup_perms(3);
assert_eq!(
p.iter().map(|lp| lp.input_idx).collect::<Vec<_>>(),
vec![0, 1, 2]
);
assert!(p.iter().all(|lp| !lp.swap_cols));
}
fn stats_with_heat(seeded: &[(RelId, u64, f32)]) -> StatsManager {
let mut mgr = StatsManager::new();
for (rel, card, heat) in seeded {
mgr.register_relation(*rel);
if *card > 0 {
mgr.update_cardinality(*rel, *card);
}
if let Some(s) = mgr.get_relation_stats_mut(*rel) {
s.heat = *heat;
}
}
mgr
}
fn heat_aware_config() -> CompilerConfig {
CompilerConfig {
wcoj_variable_ordering: WcojVarOrderingKind::HeatAware,
..CompilerConfig::default()
}
}
#[test]
fn heat_aware_leader_picks_cold_when_hot_relation_at_default_idx() {
let stats = stats_with_heat(&[
(RelId(1), 100, 0.5),
(RelId(2), 100, 0.0),
(RelId(3), 100, 0.0),
]);
let config = heat_aware_config();
let m = HeatAwareLeaderModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
Some(1)
);
}
#[test]
fn heat_aware_leader_demotes_relation_in_tight_edge() {
let mut stats = stats_with_heat(&[
(RelId(1), 100, 0.0),
(RelId(2), 100, 0.0),
(RelId(3), 100, 0.0),
]);
stats.set_join_selectivity(RelId(1), RelId(2), vec![1], vec![0], 0.01);
let config = heat_aware_config();
let m = HeatAwareLeaderModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
Some(2)
);
}
#[test]
fn heat_aware_leader_returns_none_when_heat_too_low() {
let stats = stats_with_heat(&[
(RelId(1), 100, 0.1),
(RelId(2), 100, 0.0),
(RelId(3), 100, 0.0),
]);
let config = heat_aware_config();
let m = HeatAwareLeaderModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
None
);
}
#[test]
fn heat_aware_leader_disabled_short_circuit() {
let stats = stats_with_heat(&[
(RelId(1), 100, 0.9),
(RelId(2), 100, 0.0),
(RelId(3), 100, 0.0),
]);
let config = CompilerConfig::default(); let m = HeatAwareLeaderModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
None
);
}
#[test]
fn heat_aware_leader_missing_card_safety_floor() {
let stats = stats_with_heat(&[
(RelId(1), 100, 0.5),
(RelId(2), 0, 0.0), (RelId(3), 100, 0.0),
]);
let config = heat_aware_config();
let m = HeatAwareLeaderModel;
assert_eq!(
m.pick_triangle_leader([RelId(1), RelId(2), RelId(3)], &stats, &config),
None
);
}
}