use nodedb_sql::planner::bitmap_emit::predicate::BitmapHint;
use nodedb_sql::types::SqlPlan;
use crate::bridge::envelope::PhysicalPlan;
use crate::types::{DatabaseId, VShardId};
use nodedb_physical::physical_plan::*;
use super::super::aggregate::{
extract_join_projection_specs, extract_scan_alias, serialize_join_computed_projection,
};
use super::super::convert::convert_one;
use super::super::filter::{expr_filter_qualified, serialize_join_post_filters};
use super::super::scan_params::JoinPlanParams;
use super::super::value::sql_value_to_string;
use nodedb_physical::physical_task::{PhysicalTask, PostSetOp};
fn serialize_join_condition(
condition: &Option<nodedb_sql::types::SqlExpr>,
) -> crate::Result<Vec<u8>> {
let Some(condition) = condition else {
return Ok(Vec::new());
};
zerompk::to_msgpack_vec(&vec![expr_filter_qualified(condition)]).map_err(|e| {
crate::Error::Serialization {
format: "msgpack".into(),
detail: format!("join condition serialization: {e}"),
}
})
}
fn shuffle_supports_join_tail(
projection: &[JoinProjection],
computed_projection: &[u8],
join_filters: &[u8],
post_filters: &[u8],
) -> bool {
projection.is_empty()
&& computed_projection.is_empty()
&& join_filters.is_empty()
&& post_filters.is_empty()
}
fn bitmap_hint_to_plan(hint: &BitmapHint, database_id: DatabaseId) -> Option<Box<PhysicalPlan>> {
if !hint.extra_values.is_empty() {
return None;
}
let collection = super::super::convert::db_qualified(database_id, &hint.collection);
let value_str = sql_value_to_string(&hint.primary_value);
Some(Box::new(PhysicalPlan::Document(DocumentOp::IndexedFetch {
collection,
path: hint.field.clone(),
value: value_str,
filters: Vec::new(),
projection: Vec::new(),
limit: 10_000,
offset: 0,
})))
}
pub(in crate::control::planner::sql_plan_convert) fn convert_join(
p: JoinPlanParams<'_>,
) -> crate::Result<Vec<PhysicalTask>> {
let JoinPlanParams {
left,
right,
on,
join_type,
condition,
limit,
projection,
filters,
tenant_id,
ctx,
} = p;
let mut left_collection =
super::super::aggregate::join_side_collection(left, p.ctx.database_id);
let mut right_collection =
super::super::aggregate::join_side_collection(right, p.ctx.database_id);
let mut left_raw = super::super::aggregate::extract_collection_name(left);
let mut right_raw = super::super::aggregate::extract_collection_name(right);
let mut left_alias = extract_scan_alias(left);
let mut right_alias = extract_scan_alias(right);
let join_projection = extract_join_projection_specs(projection);
let computed_projection = serialize_join_computed_projection(projection)?;
let join_filter_bytes = serialize_join_condition(condition)?;
let filter_bytes = serialize_join_post_filters(filters)?;
let left_input = if matches!(left, SqlPlan::Join { .. }) {
let inner_tasks = convert_one(left, tenant_id, ctx)?;
inner_tasks.into_iter().next().map(|t| {
let plan = t.plan;
if plan.is_sharded_source() {
Box::new(PhysicalPlan::Query(QueryOp::Exchange(ExchangeOp {
child: Box::new(plan),
mode: ExchangeMode::Broadcast,
})))
} else {
Box::new(plan)
}
})
} else {
super::super::aggregate::inline_join_side(left, tenant_id, ctx)?
};
let right_input = super::super::aggregate::inline_join_side(right, tenant_id, ctx)?;
let mut on_keys = on.to_vec();
let mut left_input = left_input;
let mut right_input = right_input;
let effective_join_type = if join_type.as_str() == "right" {
std::mem::swap(&mut left_collection, &mut right_collection);
std::mem::swap(&mut left_raw, &mut right_raw);
std::mem::swap(&mut left_alias, &mut right_alias);
std::mem::swap(&mut left_input, &mut right_input);
on_keys = on_keys.into_iter().map(|(l, r)| (r, l)).collect();
"left".to_string()
} else {
join_type.as_str().to_string()
};
let bitmap_hints = nodedb_sql::planner::bitmap_emit::hashjoin::analyze_join_sides(left, right);
let (mut raw_left_bm, mut raw_right_bm) = (bitmap_hints.left, bitmap_hints.right);
if join_type.as_str() == "right" {
std::mem::swap(&mut raw_left_bm, &mut raw_right_bm);
}
let db_id = p.ctx.database_id;
let left_bitmap = raw_left_bm.and_then(|h| bitmap_hint_to_plan(&h, db_id));
let right_bitmap = raw_right_bm.and_then(|h| bitmap_hint_to_plan(&h, db_id));
let vshard = VShardId::from_collection_in_database(p.ctx.database_id, &left_collection);
let structurally_shufflable = p.ctx.cluster_enabled
&& !on_keys.is_empty()
&& left_input.is_none()
&& right_input.is_none()
&& shuffle_supports_join_tail(
&join_projection,
&computed_projection,
&join_filter_bytes,
&filter_bytes,
);
let shuffle_eligible = structurally_shufflable
&& (p.ctx.force_shuffle_join
|| super::join_cost::cost_model_picks_shuffle(p.ctx, &left_raw, &right_raw));
let shuffle_keys = on_keys.clone();
let hash_join = PhysicalPlan::Query(QueryOp::HashJoin {
left_collection,
right_collection,
left_alias,
right_alias,
on: on_keys,
join_type: effective_join_type,
limit: limit.unwrap_or(usize::MAX),
post_group_by: Vec::new(),
post_aggregates: Vec::new(),
projection: join_projection,
computed_projection,
join_filters: join_filter_bytes,
post_filters: filter_bytes,
left_input,
right_input,
left_bitmap,
right_bitmap,
});
let plan = if shuffle_eligible {
PhysicalPlan::Query(QueryOp::Exchange(ExchangeOp {
child: Box::new(hash_join),
mode: ExchangeMode::Shuffle {
keys: shuffle_keys,
num_parts: p.ctx.shuffle_num_parts,
},
}))
} else {
hash_join
};
Ok(vec![PhysicalTask {
tenant_id,
vshard_id: vshard,
database_id: p.ctx.database_id,
plan,
post_set_op: PostSetOp::None,
txn_id: None,
}])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shuffle_rejects_join_tail_semantics() {
assert!(shuffle_supports_join_tail(&[], &[], &[], &[]));
assert!(!shuffle_supports_join_tail(
&[JoinProjection {
source: "left.id".into(),
output: "id".into(),
}],
&[],
&[],
&[],
));
assert!(!shuffle_supports_join_tail(&[], &[1], &[], &[]));
assert!(!shuffle_supports_join_tail(&[], &[], &[1], &[]));
assert!(!shuffle_supports_join_tail(&[], &[], &[], &[1]));
}
}