#![allow(elided_lifetimes_in_paths)]
use super::{Executor, PhysicalOperator, TimeoutContext};
use crate::{Error, Result, Schema, Tuple};
use std::hash::Hash;
use std::sync::Arc;
pub struct NestedLoopJoinOperator {
left: Box<dyn PhysicalOperator>,
join_type: crate::sql::JoinType,
on_condition: Option<crate::sql::LogicalExpr>,
output_schema: Arc<Schema>,
evaluator: crate::sql::Evaluator,
left_tuple: Option<Tuple>,
right_tuples: Vec<Tuple>,
right_index: usize,
timeout_ctx: Option<TimeoutContext>,
left_column_count: usize,
right_column_count: usize,
left_matched: bool, right_matched: Vec<bool>, emitting_unmatched_right: bool, unmatched_right_index: usize, condition_pair_evaluable: bool,
}
struct PairView<'a> {
left: &'a Tuple,
right: &'a Tuple,
}
impl PairView<'_> {
fn get(&self, index: usize) -> Option<&crate::Value> {
let split = self.left.values.len();
if index < split {
self.left.get(index)
} else {
self.right.get(index - split)
}
}
}
fn pair_evaluable(expr: &crate::sql::LogicalExpr) -> bool {
use crate::sql::LogicalExpr;
match expr {
LogicalExpr::Literal(_) | LogicalExpr::BoundColumn { .. } => true,
LogicalExpr::BinaryExpr { left, op: _, right } => {
!matches!(left.as_ref(), LogicalExpr::Tuple { .. })
&& !matches!(right.as_ref(), LogicalExpr::Tuple { .. })
&& pair_evaluable(left)
&& pair_evaluable(right)
}
LogicalExpr::UnaryExpr { expr, .. } | LogicalExpr::IsNull { expr, .. } => pair_evaluable(expr),
_ => false,
}
}
fn eval_condition_on_pair(
evaluator: &crate::sql::Evaluator,
expr: &crate::sql::LogicalExpr,
pair: &PairView<'_>,
) -> Result<crate::Value> {
use crate::sql::{BinaryOperator, LogicalExpr};
use crate::Value;
match expr {
LogicalExpr::Literal(value) => Ok(value.clone()),
LogicalExpr::BoundColumn { index, .. } => pair
.get(*index)
.cloned()
.ok_or_else(|| Error::query_execution(format!("Column index {} out of bounds in tuple", index))),
LogicalExpr::BinaryExpr { left, op, right } => match op {
BinaryOperator::And => {
let left_val = eval_condition_on_pair(evaluator, left, pair)?;
match &left_val {
Value::Boolean(false) => Ok(Value::Boolean(false)),
Value::Boolean(true) => {
let right_val = eval_condition_on_pair(evaluator, right, pair)?;
match &right_val {
Value::Boolean(b) => Ok(Value::Boolean(*b)),
Value::Null => Ok(Value::Null),
_ => Err(Error::query_execution(format!(
"Cannot convert {:?} to boolean",
right_val
))),
}
}
Value::Null => {
let right_val = eval_condition_on_pair(evaluator, right, pair)?;
match &right_val {
Value::Boolean(false) => Ok(Value::Boolean(false)),
Value::Boolean(true) | Value::Null => Ok(Value::Null),
_ => Err(Error::query_execution(format!(
"Cannot convert {:?} to boolean",
right_val
))),
}
}
_ => Err(Error::query_execution(format!(
"Cannot convert {:?} to boolean",
left_val
))),
}
}
BinaryOperator::Or => {
let left_val = eval_condition_on_pair(evaluator, left, pair)?;
match &left_val {
Value::Boolean(true) => Ok(Value::Boolean(true)),
Value::Boolean(false) => {
let right_val = eval_condition_on_pair(evaluator, right, pair)?;
match &right_val {
Value::Boolean(b) => Ok(Value::Boolean(*b)),
Value::Null => Ok(Value::Null),
_ => Err(Error::query_execution(format!(
"Cannot convert {:?} to boolean",
right_val
))),
}
}
Value::Null => {
let right_val = eval_condition_on_pair(evaluator, right, pair)?;
match &right_val {
Value::Boolean(true) => Ok(Value::Boolean(true)),
Value::Boolean(false) | Value::Null => Ok(Value::Null),
_ => Err(Error::query_execution(format!(
"Cannot convert {:?} to boolean",
right_val
))),
}
}
_ => Err(Error::query_execution(format!(
"Cannot convert {:?} to boolean",
left_val
))),
}
}
_ => {
let left_val = eval_condition_on_pair(evaluator, left, pair)?;
let right_val = eval_condition_on_pair(evaluator, right, pair)?;
evaluator.evaluate_binary_op(&left_val, op, &right_val)
}
},
LogicalExpr::UnaryExpr { op, expr } => {
let val = eval_condition_on_pair(evaluator, expr, pair)?;
evaluator.evaluate_unary_op(op, &val)
}
LogicalExpr::IsNull { expr, is_null } => {
let val = eval_condition_on_pair(evaluator, expr, pair)?;
Ok(Value::Boolean(matches!(val, Value::Null) == *is_null))
}
other => Err(Error::query_execution(format!(
"Internal error: expression not pair-evaluable: {:?}",
other
))),
}
}
impl NestedLoopJoinOperator {
pub fn new(
left: Box<dyn PhysicalOperator>,
mut right: Box<dyn PhysicalOperator>,
join_type: crate::sql::JoinType,
on_condition: Option<crate::sql::LogicalExpr>,
timeout_ctx: Option<TimeoutContext>,
) -> Result<Self> {
let left_schema = left.schema();
let right_schema = right.schema();
let left_column_count = left_schema.columns.len();
let right_column_count = right_schema.columns.len();
let mut columns = left_schema.columns.clone();
columns.extend(right_schema.columns.clone());
let output_schema = Arc::new(Schema { columns });
let evaluator = crate::sql::Evaluator::new(output_schema.clone());
let on_condition = on_condition.map(|condition| evaluator.bind(condition));
let condition_pair_evaluable = on_condition.as_ref().map(pair_evaluable).unwrap_or(false);
let mut right_tuples = Vec::new();
while let Some(tuple) = right.next()? {
if let Some(ref ctx) = timeout_ctx {
ctx.check_timeout()?;
}
right_tuples.push(tuple);
}
let right_count = right_tuples.len();
Ok(Self {
left,
join_type,
on_condition,
output_schema,
evaluator,
left_tuple: None,
right_tuples,
right_index: 0,
timeout_ctx,
left_column_count,
right_column_count,
left_matched: false,
right_matched: vec![false; right_count],
emitting_unmatched_right: false,
unmatched_right_index: 0,
condition_pair_evaluable,
})
}
pub fn with_timeout(self, _timeout_ctx: Option<TimeoutContext>) -> Self {
self
}
}
impl PhysicalOperator for NestedLoopJoinOperator {
fn next(&mut self) -> Result<Option<Tuple>> {
use crate::sql::JoinType;
if self.emitting_unmatched_right {
return self.emit_unmatched_right();
}
loop {
if let Some(ref ctx) = self.timeout_ctx {
ctx.check_timeout()?;
}
if self.left_tuple.is_none() {
self.left_tuple = self.left.next()?;
if self.left_tuple.is_none() {
if matches!(self.join_type, JoinType::Right | JoinType::Full) {
self.emitting_unmatched_right = true;
return self.emit_unmatched_right();
}
return Ok(None);
}
self.right_index = 0;
self.left_matched = false;
}
while self.right_index < self.right_tuples.len() {
let right_idx = self.right_index;
let right_tuple = self
.right_tuples
.get(right_idx)
.ok_or_else(|| Error::query_execution("Right tuple index out of bounds"))?;
self.right_index += 1;
let left_tuple = self
.left_tuple
.as_ref()
.ok_or_else(|| Error::query_execution("Left tuple unexpectedly None"))?;
let mut materialized: Option<Tuple> = None;
let matches = if let Some(condition) = &self.on_condition {
let result = if self.condition_pair_evaluable {
let pair = PairView {
left: left_tuple,
right: right_tuple,
};
eval_condition_on_pair(&self.evaluator, condition, &pair)?
} else {
let mut combined_values = left_tuple.values.clone();
combined_values.extend(right_tuple.values.iter().cloned());
let combined_tuple = Tuple::new(combined_values);
let result = self.evaluator.evaluate(condition, &combined_tuple)?;
materialized = Some(combined_tuple);
result
};
match result {
crate::Value::Boolean(b) => b,
_ => false,
}
} else {
true
};
if matches {
let combined_tuple = match materialized {
Some(tuple) => tuple,
None => {
let mut combined_values =
Vec::with_capacity(left_tuple.values.len() + right_tuple.values.len());
combined_values.extend_from_slice(&left_tuple.values);
combined_values.extend_from_slice(&right_tuple.values);
Tuple::new(combined_values)
}
};
self.left_matched = true;
if matches!(self.join_type, JoinType::Right | JoinType::Full) {
if let Some(matched) = self.right_matched.get_mut(right_idx) {
*matched = true;
}
}
return Ok(Some(combined_tuple));
}
}
if !self.left_matched && matches!(self.join_type, JoinType::Left | JoinType::Full) {
let left_tuple = self
.left_tuple
.as_ref()
.ok_or_else(|| Error::query_execution("Left tuple unexpectedly None"))?;
let result = self.join_with_nulls_right(left_tuple);
self.left_tuple = None;
return Ok(Some(result));
}
self.left_tuple = None;
}
}
fn schema(&self) -> Arc<Schema> {
self.output_schema.clone()
}
}
impl NestedLoopJoinOperator {
fn emit_unmatched_right(&mut self) -> Result<Option<Tuple>> {
while self.unmatched_right_index < self.right_tuples.len() {
let idx = self.unmatched_right_index;
self.unmatched_right_index += 1;
if !self.right_matched.get(idx).copied().unwrap_or(false) {
let right_tuple = self
.right_tuples
.get(idx)
.ok_or_else(|| Error::query_execution("Right tuple index out of bounds"))?;
return Ok(Some(self.join_with_nulls_left(right_tuple)));
}
}
Ok(None)
}
fn join_with_nulls_right(&self, left: &Tuple) -> Tuple {
let mut values = left.values.clone();
values.extend(vec![crate::Value::Null; self.right_column_count]);
Tuple::new(values)
}
fn join_with_nulls_left(&self, right: &Tuple) -> Tuple {
let mut values = vec![crate::Value::Null; self.left_column_count];
values.extend(right.values.clone());
Tuple::new(values)
}
}
pub struct HashJoinOperator {
left: Box<dyn PhysicalOperator>,
join_type: crate::sql::JoinType,
on_condition: Option<crate::sql::LogicalExpr>,
hash_table: std::collections::HashMap<JoinKey, JoinBucket>,
output_schema: Arc<Schema>,
evaluator: crate::sql::Evaluator,
left_evaluator: crate::sql::Evaluator,
right_evaluator: crate::sql::Evaluator,
direct_key_indices: Option<DirectJoinKeyIndices>,
build_side: HashJoinBuildSide,
state: JoinState,
current_left_tuple: Option<Tuple>,
current_match_key: Option<JoinKey>,
current_matches: Vec<Tuple>,
match_index: usize,
pure_equi_join: bool,
output_projection: Option<Vec<usize>>,
matched_right_keys: std::collections::HashSet<JoinKey>,
unmatched_right_iter: Option<std::vec::IntoIter<(JoinKey, Vec<Tuple>)>>,
unmatched_right_current: Option<std::vec::IntoIter<Tuple>>,
memory_limit: usize,
memory_used: usize,
right_column_count: usize,
left_column_count: usize,
timeout_ctx: Option<TimeoutContext>,
}
#[derive(Debug, Clone)]
struct DirectJoinKeyIndices {
left: Vec<usize>,
right: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HashJoinBuildSide {
Left,
Right,
}
enum JoinBucket {
One(Tuple),
Many(Vec<Tuple>),
}
impl JoinBucket {
fn push(&mut self, tuple: Tuple) {
match self {
Self::One(existing) => {
let first = std::mem::replace(existing, Tuple::new(Vec::new()));
*self = Self::Many(vec![first, tuple]);
}
Self::Many(values) => values.push(tuple),
}
}
fn len(&self) -> usize {
match self {
Self::One(_) => 1,
Self::Many(values) => values.len(),
}
}
fn get(&self, index: usize) -> Option<&Tuple> {
match self {
Self::One(tuple) => (index == 0).then_some(tuple),
Self::Many(values) => values.get(index),
}
}
fn iter(&self) -> JoinBucketIter<'_> {
match self {
Self::One(tuple) => JoinBucketIter::One(Some(tuple)),
Self::Many(values) => JoinBucketIter::Many(values.iter()),
}
}
fn clone_tuples(&self) -> Vec<Tuple> {
match self {
Self::One(tuple) => vec![tuple.clone()],
Self::Many(values) => values.clone(),
}
}
}
enum JoinBucketIter<'a> {
One(Option<&'a Tuple>),
Many(std::slice::Iter<'a, Tuple>),
}
impl<'a> Iterator for JoinBucketIter<'a> {
type Item = &'a Tuple;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::One(value) => value.take(),
Self::Many(iter) => iter.next(),
}
}
}
#[derive(Debug, Clone)]
enum JoinKey {
Int(i64),
Single(crate::Value),
Composite(Vec<crate::Value>),
}
impl JoinKey {
fn from_values(mut values: Vec<crate::Value>) -> Self {
if values.len() == 1 {
Self::from_single_value(values.pop().expect("single value exists"))
} else {
Self::Composite(values)
}
}
fn from_single_value(value: crate::Value) -> Self {
match value {
crate::Value::Int2(value) => Self::Int(i64::from(value)),
crate::Value::Int4(value) => Self::Int(i64::from(value)),
crate::Value::Int8(value) => Self::Int(value),
value => Self::Single(value),
}
}
fn from_single_value_ref(value: &crate::Value) -> Self {
match value {
crate::Value::Int2(value) => Self::Int(i64::from(*value)),
crate::Value::Int4(value) => Self::Int(i64::from(*value)),
crate::Value::Int8(value) => Self::Int(*value),
value => Self::Single(value.clone()),
}
}
fn len(&self) -> usize {
match self {
Self::Int(_) => 1,
Self::Single(_) => 1,
Self::Composite(values) => values.len(),
}
}
}
impl PartialEq for JoinKey {
fn eq(&self, other: &Self) -> bool {
if self.len() != other.len() {
return false;
}
match (self, other) {
(Self::Int(a), Self::Int(b)) => a == b,
(Self::Int(a), Self::Single(b)) | (Self::Single(b), Self::Int(a)) => int_value_equal_for_join(*a, b),
(Self::Int(a), Self::Composite(values)) | (Self::Composite(values), Self::Int(a)) => {
values.first().is_some_and(|b| int_value_equal_for_join(*a, b))
}
(Self::Single(a), Self::Single(b)) => values_equal_for_join(a, b),
(Self::Single(a), Self::Composite(values)) | (Self::Composite(values), Self::Single(a)) => {
values.first().is_some_and(|b| values_equal_for_join(a, b))
}
(Self::Composite(left), Self::Composite(right)) => {
left.iter().zip(right).all(|(a, b)| values_equal_for_join(a, b))
}
}
}
}
impl Eq for JoinKey {}
impl std::hash::Hash for JoinKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.len().hash(state);
match self {
Self::Int(value) => {
2u8.hash(state);
value.hash(state);
}
Self::Single(value) => hash_value_for_join(value, state),
Self::Composite(values) => {
for value in values {
hash_value_for_join(value, state);
}
}
}
}
}
fn int_value_equal_for_join(left: i64, right: &crate::Value) -> bool {
use crate::Value;
match right {
Value::Int2(value) => left == i64::from(*value),
Value::Int4(value) => left == i64::from(*value),
Value::Int8(value) => left == *value,
Value::String(value) => value.parse::<i64>().map_or(false, |parsed| left == parsed),
_ => false,
}
}
fn hash_value_for_join<H: std::hash::Hasher>(value: &crate::Value, state: &mut H) {
use crate::Value;
match value {
Value::String(s) => {
if let Ok(n) = s.parse::<i64>() {
2u8.hash(state);
n.hash(state);
} else if let Ok(uuid) = uuid::Uuid::parse_str(s) {
10u8.hash(state);
uuid.hash(state);
} else {
value.hash(state);
}
}
Value::Uuid(uuid) => {
10u8.hash(state);
uuid.hash(state);
}
_ => value.hash(state),
}
}
fn string_equals_uuid(s: &str, uuid: &uuid::Uuid) -> bool {
uuid::Uuid::parse_str(s).is_ok_and(|parsed| parsed == *uuid)
}
fn values_equal_for_join(a: &crate::Value, b: &crate::Value) -> bool {
use crate::Value;
match (a, b) {
(Value::Null, _) | (_, Value::Null) => false,
(Value::Int2(x), Value::Int2(y)) => x == y,
(Value::Int4(x), Value::Int4(y)) => x == y,
(Value::Int8(x), Value::Int8(y)) => x == y,
(Value::Int2(x), Value::Int4(y)) | (Value::Int4(y), Value::Int2(x)) => i64::from(*x) == i64::from(*y),
(Value::Int2(x), Value::Int8(y)) | (Value::Int8(y), Value::Int2(x)) => i64::from(*x) == *y,
(Value::Int4(x), Value::Int8(y)) | (Value::Int8(y), Value::Int4(x)) => i64::from(*x) == *y,
(Value::Uuid(uuid), Value::String(s)) | (Value::String(s), Value::Uuid(uuid)) => string_equals_uuid(s, uuid),
(Value::String(x), Value::String(y)) => x == y,
(Value::String(s), Value::Int4(n)) | (Value::Int4(n), Value::String(s)) => {
s.parse::<i32>().map_or(false, |parsed| parsed == *n)
}
(Value::String(s), Value::Int8(n)) | (Value::Int8(n), Value::String(s)) => {
s.parse::<i64>().map_or(false, |parsed| parsed == *n)
}
(Value::String(s), Value::Int2(n)) | (Value::Int2(n), Value::String(s)) => {
s.parse::<i16>().map_or(false, |parsed| parsed == *n)
}
_ => a == b,
}
}
#[derive(Debug, Clone, Copy)]
enum DirectJoinKeySide {
Left(usize),
Right(usize),
}
fn build_direct_join_key_indices(
condition: &crate::sql::LogicalExpr,
left_schema: &Schema,
right_schema: &Schema,
) -> Option<DirectJoinKeyIndices> {
let mut pairs = Vec::new();
collect_direct_equi_pairs(condition, &mut pairs)?;
if pairs.is_empty() {
return None;
}
let mut left = Vec::with_capacity(pairs.len());
let mut right = Vec::with_capacity(pairs.len());
for (lhs, rhs) in pairs {
let lhs_side = direct_join_key_side(lhs, left_schema, right_schema)?;
let rhs_side = direct_join_key_side(rhs, left_schema, right_schema)?;
match (lhs_side, rhs_side) {
(DirectJoinKeySide::Left(left_idx), DirectJoinKeySide::Right(right_idx))
| (DirectJoinKeySide::Right(right_idx), DirectJoinKeySide::Left(left_idx)) => {
left.push(left_idx);
right.push(right_idx);
}
_ => return None,
}
}
Some(DirectJoinKeyIndices { left, right })
}
fn collect_direct_equi_pairs<'a>(
expr: &'a crate::sql::LogicalExpr,
pairs: &mut Vec<(&'a crate::sql::LogicalExpr, &'a crate::sql::LogicalExpr)>,
) -> Option<()> {
use crate::sql::{BinaryOperator, LogicalExpr};
match expr {
LogicalExpr::BinaryExpr {
left,
op: BinaryOperator::And,
right,
} => {
collect_direct_equi_pairs(left, pairs)?;
collect_direct_equi_pairs(right, pairs)
}
LogicalExpr::BinaryExpr {
left,
op: BinaryOperator::Eq,
right,
} => {
pairs.push((left, right));
Some(())
}
_ => None,
}
}
fn direct_join_key_side(
expr: &crate::sql::LogicalExpr,
left_schema: &Schema,
right_schema: &Schema,
) -> Option<DirectJoinKeySide> {
let crate::sql::LogicalExpr::Column { table, name } = expr else {
return None;
};
let left = resolve_direct_join_column(left_schema, table.as_deref(), name);
let right = resolve_direct_join_column(right_schema, table.as_deref(), name);
match (left, right) {
(Some(idx), None) => Some(DirectJoinKeySide::Left(idx)),
(None, Some(idx)) => Some(DirectJoinKeySide::Right(idx)),
_ => None,
}
}
fn resolve_direct_join_column(schema: &Schema, qualifier: Option<&str>, name: &str) -> Option<usize> {
let mut matches = schema.columns.iter().enumerate().filter(|(_, column)| {
if !column.name.eq_ignore_ascii_case(name) {
return false;
}
qualifier.map_or(true, |q| {
option_eq_ignore_ascii_case(column.source_table.as_deref(), q)
|| option_eq_ignore_ascii_case(column.source_table_name.as_deref(), q)
})
});
let (idx, _) = matches.next()?;
matches.next().is_none().then_some(idx)
}
fn option_eq_ignore_ascii_case(value: Option<&str>, expected: &str) -> bool {
value.is_some_and(|value| value.eq_ignore_ascii_case(expected))
}
fn join_input_leaf_info(plan: &crate::sql::LogicalPlan) -> Option<(&str, Option<&String>, &Schema, bool)> {
match plan {
crate::sql::LogicalPlan::Scan {
table_name,
alias,
schema,
projection,
..
}
| crate::sql::LogicalPlan::FilteredScan {
table_name,
alias,
schema,
projection,
..
} => Some((
table_name.as_str(),
alias.as_ref(),
schema.as_ref(),
projection.is_some(),
)),
crate::sql::LogicalPlan::Filter { input, .. } => join_input_leaf_info(input),
_ => None,
}
}
fn qualifier_matches_join_input(table_name: &str, alias: Option<&String>, qualifier: &str) -> bool {
qualifier.eq_ignore_ascii_case(table_name) || alias.is_some_and(|alias| qualifier.eq_ignore_ascii_case(alias))
}
fn collect_join_input_expr_columns(
input: &crate::sql::LogicalPlan,
expr: &crate::sql::LogicalExpr,
required: &mut std::collections::BTreeSet<usize>,
) -> Option<()> {
let (table_name, alias, schema, _) = join_input_leaf_info(input)?;
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)
}
fn collect_join_input_expr_columns_inner(
table_name: &str,
alias: Option<&String>,
schema: &Schema,
expr: &crate::sql::LogicalExpr,
required: &mut std::collections::BTreeSet<usize>,
) -> Option<()> {
use crate::sql::LogicalExpr;
match expr {
LogicalExpr::Column {
table: Some(table),
name,
} => {
if qualifier_matches_join_input(table_name, alias, table) {
let idx = schema
.columns
.iter()
.position(|column| column.name.eq_ignore_ascii_case(name))?;
required.insert(idx);
}
Some(())
}
LogicalExpr::Column { table: None, .. } | LogicalExpr::Wildcard => None,
LogicalExpr::BinaryExpr { left, right, .. } => {
collect_join_input_expr_columns_inner(table_name, alias, schema, left, required)?;
collect_join_input_expr_columns_inner(table_name, alias, schema, right, required)
}
LogicalExpr::UnaryExpr { expr, .. } | LogicalExpr::Cast { expr, .. } | LogicalExpr::IsNull { expr, .. } => {
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)
}
LogicalExpr::Between { expr, low, high, .. } => {
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)?;
collect_join_input_expr_columns_inner(table_name, alias, schema, low, required)?;
collect_join_input_expr_columns_inner(table_name, alias, schema, high, required)
}
LogicalExpr::InList { expr, list, .. } => {
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)?;
for item in list {
collect_join_input_expr_columns_inner(table_name, alias, schema, item, required)?;
}
Some(())
}
LogicalExpr::InSet { expr, .. } => {
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)
}
LogicalExpr::ArraySubscript { array, index } => {
collect_join_input_expr_columns_inner(table_name, alias, schema, array, required)?;
collect_join_input_expr_columns_inner(table_name, alias, schema, index, required)
}
LogicalExpr::Case {
expr,
when_then,
else_result,
} => {
if let Some(expr) = expr {
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)?;
}
for (when, then) in when_then {
collect_join_input_expr_columns_inner(table_name, alias, schema, when, required)?;
collect_join_input_expr_columns_inner(table_name, alias, schema, then, required)?;
}
if let Some(else_result) = else_result {
collect_join_input_expr_columns_inner(table_name, alias, schema, else_result, required)?;
}
Some(())
}
LogicalExpr::ScalarFunction { args, .. } | LogicalExpr::AggregateFunction { args, .. } => {
for arg in args {
collect_join_input_expr_columns_inner(table_name, alias, schema, arg, required)?;
}
Some(())
}
LogicalExpr::WindowFunction {
args,
partition_by,
order_by,
..
} => {
for arg in args {
collect_join_input_expr_columns_inner(table_name, alias, schema, arg, required)?;
}
for expr in partition_by {
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)?;
}
for (expr, _) in order_by {
collect_join_input_expr_columns_inner(table_name, alias, schema, expr, required)?;
}
Some(())
}
LogicalExpr::Tuple { items } => {
for item in items {
collect_join_input_expr_columns_inner(table_name, alias, schema, item, required)?;
}
Some(())
}
LogicalExpr::Literal(_) | LogicalExpr::Parameter { .. } | LogicalExpr::DefaultValue => Some(()),
LogicalExpr::ScalarSubquery { .. }
| LogicalExpr::InSubquery { .. }
| LogicalExpr::Exists { .. }
| LogicalExpr::NewRow { .. }
| LogicalExpr::OldRow { .. } => None,
LogicalExpr::BoundColumn { .. } => None,
}
}
fn collect_join_input_local_filter_columns(
input: &crate::sql::LogicalPlan,
required: &mut std::collections::BTreeSet<usize>,
) -> Option<()> {
match input {
crate::sql::LogicalPlan::Filter { predicate, .. } => {
collect_join_input_expr_columns(input, predicate, required)?;
if let crate::sql::LogicalPlan::Filter { input, .. } = input {
collect_join_input_local_filter_columns(input, required)?;
}
Some(())
}
crate::sql::LogicalPlan::FilteredScan { .. } => Some(()),
crate::sql::LogicalPlan::Scan { .. } => Some(()),
_ => None,
}
}
fn apply_join_input_projection(
input: &crate::sql::LogicalPlan,
required: &std::collections::BTreeSet<usize>,
) -> Option<(crate::sql::LogicalPlan, bool)> {
if required.is_empty() {
return None;
}
let (_, _, schema, already_projected) = join_input_leaf_info(input)?;
if already_projected {
return None;
}
let indices: Vec<usize> = required.iter().copied().collect();
if indices.iter().any(|&idx| idx >= schema.columns.len()) {
return None;
}
if indices.len() >= schema.columns.len() {
return Some((input.clone(), false));
}
match input {
crate::sql::LogicalPlan::Scan {
table_name,
alias,
schema,
as_of,
..
} => Some((
crate::sql::LogicalPlan::Scan {
table_name: table_name.clone(),
alias: alias.clone(),
schema: schema.clone(),
projection: Some(indices),
as_of: as_of.clone(),
},
true,
)),
crate::sql::LogicalPlan::FilteredScan {
table_name,
alias,
schema,
predicate,
as_of,
..
} => Some((
crate::sql::LogicalPlan::FilteredScan {
table_name: table_name.clone(),
alias: alias.clone(),
schema: schema.clone(),
projection: Some(indices),
predicate: predicate.clone(),
as_of: as_of.clone(),
},
true,
)),
crate::sql::LogicalPlan::Filter { input, predicate } => {
let (projected_input, changed) = apply_join_input_projection(input, required)?;
Some((
crate::sql::LogicalPlan::Filter {
input: Box::new(projected_input),
predicate: predicate.clone(),
},
changed,
))
}
_ => None,
}
}
fn compact_projected_join_inputs(
left: &crate::sql::LogicalPlan,
right: &crate::sql::LogicalPlan,
join_condition: &crate::sql::LogicalExpr,
post_join_predicate: Option<&crate::sql::LogicalExpr>,
project_exprs: &[crate::sql::LogicalExpr],
) -> Option<(crate::sql::LogicalPlan, crate::sql::LogicalPlan)> {
let mut left_required = std::collections::BTreeSet::new();
let mut right_required = std::collections::BTreeSet::new();
collect_join_input_expr_columns(left, join_condition, &mut left_required)?;
collect_join_input_expr_columns(right, join_condition, &mut right_required)?;
if let Some(predicate) = post_join_predicate {
collect_join_input_expr_columns(left, predicate, &mut left_required)?;
collect_join_input_expr_columns(right, predicate, &mut right_required)?;
}
for expr in project_exprs {
collect_join_input_expr_columns(left, expr, &mut left_required)?;
collect_join_input_expr_columns(right, expr, &mut right_required)?;
}
collect_join_input_local_filter_columns(left, &mut left_required)?;
collect_join_input_local_filter_columns(right, &mut right_required)?;
let (projected_left, left_changed) = apply_join_input_projection(left, &left_required)?;
let (projected_right, right_changed) = apply_join_input_projection(right, &right_required)?;
if left_changed || right_changed {
tracing::debug!(
left_cols = ?left_required,
right_cols = ?right_required,
"projected inner join inputs to compact scan columns"
);
Some((projected_left, projected_right))
} else {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JoinState {
Initial,
Probing,
EmittingUnmatched,
Exhausted,
}
impl HashJoinOperator {
const DEFAULT_MEMORY_LIMIT_MB: usize = 1024;
fn default_memory_limit() -> usize {
std::env::var("HELIOSDB_HASH_JOIN_MEM_MB")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.filter(|mb| *mb > 0)
.unwrap_or(Self::DEFAULT_MEMORY_LIMIT_MB)
.saturating_mul(1024 * 1024)
}
pub fn new(
left: Box<dyn PhysicalOperator>,
right: Box<dyn PhysicalOperator>,
join_type: crate::sql::JoinType,
on_condition: Option<crate::sql::LogicalExpr>,
timeout_ctx: Option<TimeoutContext>,
) -> Result<Self> {
Self::with_memory_limit(
left,
right,
join_type,
on_condition,
HashJoinBuildSide::Right,
Self::default_memory_limit(),
timeout_ctx,
)
}
pub fn new_build_left(
left: Box<dyn PhysicalOperator>,
right: Box<dyn PhysicalOperator>,
join_type: crate::sql::JoinType,
on_condition: Option<crate::sql::LogicalExpr>,
timeout_ctx: Option<TimeoutContext>,
) -> Result<Self> {
Self::with_memory_limit(
left,
right,
join_type,
on_condition,
HashJoinBuildSide::Left,
Self::default_memory_limit(),
timeout_ctx,
)
}
fn new_projected_inner(
left: Box<dyn PhysicalOperator>,
right: Box<dyn PhysicalOperator>,
on_condition: Option<crate::sql::LogicalExpr>,
projection: Vec<usize>,
output_schema: Arc<Schema>,
build_side: HashJoinBuildSide,
timeout_ctx: Option<TimeoutContext>,
) -> Result<Self> {
Self::with_memory_limit_projected(
left,
right,
crate::sql::JoinType::Inner,
on_condition,
build_side,
Self::default_memory_limit(),
timeout_ctx,
Some(projection),
Some(output_schema),
)
}
fn with_memory_limit(
left: Box<dyn PhysicalOperator>,
right: Box<dyn PhysicalOperator>,
join_type: crate::sql::JoinType,
on_condition: Option<crate::sql::LogicalExpr>,
build_side: HashJoinBuildSide,
memory_limit: usize,
timeout_ctx: Option<TimeoutContext>,
) -> Result<Self> {
Self::with_memory_limit_projected(
left,
right,
join_type,
on_condition,
build_side,
memory_limit,
timeout_ctx,
None,
None,
)
}
fn with_memory_limit_projected(
left: Box<dyn PhysicalOperator>,
right: Box<dyn PhysicalOperator>,
join_type: crate::sql::JoinType,
on_condition: Option<crate::sql::LogicalExpr>,
build_side: HashJoinBuildSide,
memory_limit: usize,
timeout_ctx: Option<TimeoutContext>,
output_projection: Option<Vec<usize>>,
projected_output_schema: Option<Arc<Schema>>,
) -> Result<Self> {
let left_schema = left.schema();
let right_schema = right.schema();
let left_column_count = left_schema.columns.len();
let right_column_count = right_schema.columns.len();
let mut columns = left_schema.columns.clone();
columns.extend(right_schema.columns.clone());
let combined_schema = Arc::new(Schema { columns });
let output_schema = projected_output_schema.unwrap_or_else(|| Arc::clone(&combined_schema));
let direct_key_indices = on_condition
.as_ref()
.and_then(|condition| build_direct_join_key_indices(condition, &left_schema, &right_schema));
let pure_equi_join = is_pure_equi_join(&on_condition);
let evaluator = crate::sql::Evaluator::new(combined_schema);
let left_evaluator = crate::sql::Evaluator::new(left_schema);
let right_evaluator = crate::sql::Evaluator::new(right_schema);
let (probe_input, mut build_input) = match build_side {
HashJoinBuildSide::Right => (left, right),
HashJoinBuildSide::Left => (right, left),
};
let mut operator = Self {
left: probe_input,
join_type,
on_condition,
hash_table: std::collections::HashMap::new(),
output_schema,
evaluator,
left_evaluator,
right_evaluator,
direct_key_indices,
build_side,
state: JoinState::Initial,
current_left_tuple: None,
current_match_key: None,
current_matches: Vec::new(),
match_index: 0,
pure_equi_join,
output_projection,
matched_right_keys: std::collections::HashSet::new(),
unmatched_right_iter: None,
unmatched_right_current: None,
memory_limit,
memory_used: 0,
right_column_count,
left_column_count,
timeout_ctx: timeout_ctx.clone(),
};
operator.build_phase(&mut build_input)?;
Ok(operator)
}
pub fn with_timeout(self, _timeout_ctx: Option<TimeoutContext>) -> Self {
self
}
fn build_phase(&mut self, right: &mut Box<dyn PhysicalOperator>) -> Result<()> {
tracing::debug!(
"HashJoin build_phase: right_schema columns = {:?}",
right.schema().columns.iter().map(|c| &c.name).collect::<Vec<_>>()
);
while let Some(tuple) = right.next()? {
tracing::debug!("HashJoin build: tuple = {:?}", tuple.values);
if let Some(ref ctx) = self.timeout_ctx {
ctx.check_timeout()?;
}
let key_opt = self.extract_join_key(&tuple, self.build_side == HashJoinBuildSide::Right)?;
tracing::debug!("HashJoin build: extracted key = {:?}", key_opt);
let key = match key_opt {
Some(k) => k,
None => continue, };
let tuple_size = Self::estimate_tuple_size(&tuple);
let key_size = Self::estimate_key_size(&key);
let entry_overhead = 24; let additional_memory = tuple_size + key_size + entry_overhead;
if self.memory_used + additional_memory > self.memory_limit {
return Err(Error::query_execution(format!(
"Hash join exceeds memory limit ({} MB). Raise it by setting the \
HELIOSDB_HASH_JOIN_MEM_MB environment variable, or rewrite the query \
(e.g. add a more selective filter or join key).",
self.memory_limit / (1024 * 1024)
)));
}
match self.hash_table.entry(key) {
std::collections::hash_map::Entry::Occupied(mut entry) => {
entry.get_mut().push(tuple);
}
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(JoinBucket::One(tuple));
}
}
self.memory_used += additional_memory;
}
self.state = JoinState::Probing;
Ok(())
}
fn extract_join_key(&self, tuple: &Tuple, is_right_side: bool) -> Result<Option<JoinKey>> {
if let Some(indices) = &self.direct_key_indices {
let key_indices = if is_right_side { &indices.right } else { &indices.left };
if key_indices.len() == 1 {
let value = tuple
.values
.get(key_indices[0])
.ok_or_else(|| Error::query_execution("Join key index out of bounds"))?;
if matches!(value, crate::Value::Null) {
return Ok(None);
}
return Ok(Some(JoinKey::from_single_value_ref(value)));
}
let mut key_values = Vec::with_capacity(key_indices.len());
for &idx in key_indices {
let value = tuple
.values
.get(idx)
.ok_or_else(|| Error::query_execution("Join key index out of bounds"))?
.clone();
if matches!(value, crate::Value::Null) {
return Ok(None);
}
key_values.push(value);
}
return Ok(Some(JoinKey::from_values(key_values)));
}
if let Some(condition) = &self.on_condition {
let key_values = self.extract_join_columns(condition, tuple, is_right_side)?;
if key_values.iter().any(|v| matches!(v, crate::Value::Null)) {
return Ok(None);
}
Ok(Some(JoinKey::from_values(key_values)))
} else {
Ok(Some(JoinKey::Composite(Vec::new())))
}
}
fn extract_join_columns(
&self,
condition: &crate::sql::LogicalExpr,
tuple: &Tuple,
is_right_side: bool,
) -> Result<Vec<crate::Value>> {
use crate::sql::{BinaryOperator, LogicalExpr};
let evaluator = if is_right_side {
&self.right_evaluator
} else {
&self.left_evaluator
};
match condition {
LogicalExpr::BinaryExpr { left, op, right } => {
match op {
BinaryOperator::Eq => {
let (primary, fallback) = if is_right_side { (right, left) } else { (left, right) };
match evaluator.evaluate(primary, tuple) {
Ok(value) => Ok(vec![value]),
Err(_) => {
let value = evaluator.evaluate(fallback, tuple)?;
Ok(vec![value])
}
}
}
BinaryOperator::And => {
let mut values = self.extract_join_columns(left, tuple, is_right_side)?;
values.extend(self.extract_join_columns(right, tuple, is_right_side)?);
Ok(values)
}
_ => {
Ok(vec![])
}
}
}
_ => {
Ok(vec![])
}
}
}
fn probe_phase(&mut self) -> Result<Option<Tuple>> {
loop {
if let Some(ref ctx) = self.timeout_ctx {
ctx.check_timeout()?;
}
if let Some(key) = self.current_match_key.as_ref() {
if let Some(matches) = self.hash_table.get(key) {
if self.match_index < matches.len() {
let right_tuple = matches
.get(self.match_index)
.ok_or_else(|| Error::query_execution("Match index out of bounds"))?;
let left_tuple = self
.current_left_tuple
.as_ref()
.ok_or_else(|| Error::query_execution("Missing left tuple"))?;
self.match_index += 1;
return Ok(Some(self.join_probe_with_build(left_tuple, right_tuple)));
}
}
self.current_match_key = None;
self.current_left_tuple = None;
self.match_index = 0;
}
if self.match_index < self.current_matches.len() {
let right_tuple = self
.current_matches
.get(self.match_index)
.ok_or_else(|| Error::query_execution("Match index out of bounds"))?;
self.match_index += 1;
let left_tuple = self
.current_left_tuple
.as_ref()
.ok_or_else(|| Error::query_execution("Missing left tuple"))?;
return Ok(Some(self.join_probe_with_build(left_tuple, right_tuple)));
}
match self.left.next()? {
None => {
if matches!(self.join_type, crate::sql::JoinType::Right | crate::sql::JoinType::Full) {
self.state = JoinState::EmittingUnmatched;
return self.emit_unmatched();
}
self.state = JoinState::Exhausted;
return Ok(None);
}
Some(left_tuple) => {
let key_opt = self.extract_join_key(&left_tuple, self.build_side == HashJoinBuildSide::Left)?;
tracing::debug!(
"HashJoin probe: left_tuple = {:?}, extracted key = {:?}",
left_tuple.values,
key_opt
);
let key = match key_opt {
Some(k) => k,
None => {
if matches!(self.join_type, crate::sql::JoinType::Left | crate::sql::JoinType::Full) {
return Ok(Some(self.join_with_nulls_right(&left_tuple)));
}
continue;
}
};
if let Some(matches) = self.hash_table.get(&key) {
if self.pure_equi_join {
if matches!(self.join_type, crate::sql::JoinType::Right | crate::sql::JoinType::Full) {
self.matched_right_keys.insert(key.clone());
}
if matches.len() == 1 {
let right_tuple = matches
.get(0)
.ok_or_else(|| Error::query_execution("Match index out of bounds"))?;
return Ok(Some(self.join_probe_with_build(&left_tuple, right_tuple)));
}
self.current_left_tuple = Some(left_tuple);
self.current_match_key = Some(key);
self.match_index = 0;
continue;
}
let filtered_matches: Vec<Tuple> = matches
.iter()
.filter(|right_tuple| {
self.evaluate_probe_build_condition(&left_tuple, right_tuple)
.unwrap_or(false)
})
.cloned()
.collect();
if !filtered_matches.is_empty() {
if matches!(self.join_type, crate::sql::JoinType::Right | crate::sql::JoinType::Full) {
self.matched_right_keys.insert(key);
}
self.current_left_tuple = Some(left_tuple);
self.current_matches = filtered_matches;
self.match_index = 0;
continue;
}
}
if matches!(self.join_type, crate::sql::JoinType::Left | crate::sql::JoinType::Full) {
return Ok(Some(self.join_with_nulls_right(&left_tuple)));
}
continue;
}
}
}
}
fn evaluate_join_condition(&self, left: &Tuple, right: &Tuple) -> Result<bool> {
if let Some(condition) = &self.on_condition {
if is_pure_equi_join(&self.on_condition) {
return Ok(true);
}
let combined = Self::join_tuples(left, right);
let result = self.evaluator.evaluate(condition, &combined)?;
match result {
crate::Value::Boolean(b) => Ok(b),
crate::Value::Null => Ok(false), _ => Ok(false),
}
} else {
Ok(true)
}
}
fn evaluate_probe_build_condition(&self, probe: &Tuple, build: &Tuple) -> Result<bool> {
match self.build_side {
HashJoinBuildSide::Right => self.evaluate_join_condition(probe, build),
HashJoinBuildSide::Left => self.evaluate_join_condition(build, probe),
}
}
fn emit_unmatched(&mut self) -> Result<Option<Tuple>> {
if self.unmatched_right_iter.is_none() {
let unmatched: Vec<_> = self
.hash_table
.iter()
.filter(|(key, _)| !self.matched_right_keys.contains(key))
.map(|(key, tuples)| (key.clone(), tuples.clone_tuples()))
.collect();
self.unmatched_right_iter = Some(unmatched.into_iter());
}
if let Some(ref mut current_iter) = self.unmatched_right_current {
if let Some(right_tuple) = current_iter.next() {
return Ok(Some(self.join_with_nulls_left(&right_tuple)));
}
}
if let Some(ref mut iter) = self.unmatched_right_iter {
if let Some((_, tuples)) = iter.next() {
self.unmatched_right_current = Some(tuples.into_iter());
return self.emit_unmatched();
}
}
self.state = JoinState::Exhausted;
Ok(None)
}
fn join_tuples(left: &Tuple, right: &Tuple) -> Tuple {
let mut values = Vec::with_capacity(left.values.len() + right.values.len());
values.extend_from_slice(&left.values);
values.extend_from_slice(&right.values);
Tuple::new(values)
}
fn join_tuples_projected(&self, left: &Tuple, right: &Tuple) -> Tuple {
let Some(indices) = &self.output_projection else {
return Self::join_tuples(left, right);
};
let mut values = Vec::with_capacity(indices.len());
for &idx in indices {
let value = if idx < self.left_column_count {
left.values.get(idx)
} else {
right.values.get(idx - self.left_column_count)
}
.cloned()
.unwrap_or(crate::Value::Null);
values.push(value);
}
Tuple::new(values)
}
fn join_probe_with_build(&self, probe: &Tuple, build: &Tuple) -> Tuple {
match self.build_side {
HashJoinBuildSide::Right => self.join_tuples_projected(probe, build),
HashJoinBuildSide::Left => self.join_tuples_projected(build, probe),
}
}
fn join_with_nulls_right(&self, left: &Tuple) -> Tuple {
let mut values = Vec::with_capacity(left.values.len() + self.right_column_count);
values.extend_from_slice(&left.values);
values.resize(values.len() + self.right_column_count, crate::Value::Null);
Tuple::new(values)
}
fn join_with_nulls_left(&self, right: &Tuple) -> Tuple {
let mut values = Vec::with_capacity(self.left_column_count + right.values.len());
values.resize(self.left_column_count, crate::Value::Null);
values.extend_from_slice(&right.values);
Tuple::new(values)
}
fn estimate_tuple_size(tuple: &Tuple) -> usize {
let base = 24; let values_size: usize = tuple.values.iter().map(|v| Self::estimate_value_size(v)).sum();
base + values_size
}
fn estimate_value_size(value: &crate::Value) -> usize {
use crate::Value;
match value {
Value::Null => 1,
Value::Boolean(_) => 1,
Value::Int2(_) => 2,
Value::Int4(_) => 4,
Value::Int8(_) => 8,
Value::Float4(_) => 4,
Value::Float8(_) => 8,
Value::Numeric(n) => 24 + n.len(),
Value::String(s) => 24 + s.len(),
Value::Bytes(b) => 24 + b.len(),
Value::Vector(v) => 24 + v.len() * 4,
Value::Array(arr) => 24 + arr.iter().map(Self::estimate_value_size).sum::<usize>(),
Value::Json(_) => 256, Value::Uuid(_) => 16,
Value::Timestamp(_) => 16,
Value::Date(_) => 4,
Value::Time(_) => 8,
Value::DictRef { .. } => 4,
Value::CasRef { .. } => 32,
Value::ColumnarRef => 1,
Value::Interval(_) => 16, }
}
fn estimate_key_size(key: &JoinKey) -> usize {
match key {
JoinKey::Int(_) => std::mem::size_of::<i64>(),
JoinKey::Single(value) => Self::estimate_value_size(value),
JoinKey::Composite(values) => 24 + values.iter().map(Self::estimate_value_size).sum::<usize>(),
}
}
}
impl PhysicalOperator for HashJoinOperator {
fn next(&mut self) -> Result<Option<Tuple>> {
match self.state {
JoinState::Probing => self.probe_phase(),
JoinState::EmittingUnmatched => self.emit_unmatched(),
JoinState::Exhausted => Ok(None),
JoinState::Initial => {
Err(Error::query_execution("HashJoinOperator in invalid initial state"))
}
}
}
fn schema(&self) -> Arc<Schema> {
self.output_schema.clone()
}
}
pub(super) fn handle_join(
executor: &mut Executor,
left: &crate::sql::LogicalPlan,
right: &crate::sql::LogicalPlan,
join_type: &crate::sql::JoinType,
on: &Option<crate::sql::LogicalExpr>,
lateral: bool,
) -> Result<Box<dyn PhysicalOperator>> {
if lateral {
let left_op = executor.plan_to_operator(left)?;
let right_op = executor.plan_to_operator(right)?;
let timeout_ctx = executor.timeout_ctx();
return Ok(Box::new(NestedLoopJoinOperator::new(
left_op,
right_op,
join_type.clone(),
on.clone(),
timeout_ctx,
)?));
}
let left_rows = estimate_hash_join_rows(executor, left);
let right_rows = estimate_hash_join_rows(executor, right);
let build_left_for_inner = matches!(join_type, crate::sql::JoinType::Inner)
&& std::env::var("HELIOS_HASHJOIN_BUILD_RIGHT").is_err()
&& should_build_left_for_inner(left_rows, right_rows);
if let Some(condition) = on {
let (equi_part, residual_part) = split_join_condition(condition);
let inlj_left_rows = if matches!(join_type, crate::sql::JoinType::Inner) {
estimate_index_nested_loop_probe_rows(executor, left).or(left_rows)
} else {
left_rows
};
if residual_part.is_none()
&& matches!(join_type, crate::sql::JoinType::Inner | crate::sql::JoinType::Left)
&& should_try_index_nested_loop_join(inlj_left_rows, right_rows)
&& is_plain_scan_like(right)
{
if let Some(join_op) = try_index_nested_loop_join(
executor,
left,
right,
join_type,
equi_part.as_ref().unwrap_or(condition),
)? {
return Ok(join_op);
}
}
}
let left_op = executor.plan_to_operator(left)?;
let right_op = executor.plan_to_operator(right)?;
let timeout_ctx = executor.timeout_ctx();
match on {
None => {
Ok(Box::new(HashJoinOperator::new(
left_op,
right_op,
join_type.clone(),
None,
timeout_ctx,
)?))
}
Some(condition) => {
let (equi_part, residual_part) = split_join_condition(condition);
if equi_part.is_some() {
let mut join_op: Box<dyn PhysicalOperator> = if build_left_for_inner {
Box::new(HashJoinOperator::new_build_left(
left_op,
right_op,
join_type.clone(),
equi_part,
timeout_ctx,
)?)
} else {
Box::new(HashJoinOperator::new(
left_op,
right_op,
join_type.clone(),
equi_part,
timeout_ctx,
)?)
};
if let Some(residual) = residual_part {
join_op = Box::new(super::filter::FilterOperator::new(join_op, residual, vec![]));
}
Ok(join_op)
} else {
Ok(Box::new(NestedLoopJoinOperator::new(
left_op,
right_op,
join_type.clone(),
on.clone(),
timeout_ctx,
)?))
}
}
}
}
pub(super) fn handle_projected_join(
executor: &mut Executor,
input: &crate::sql::LogicalPlan,
exprs: &[crate::sql::LogicalExpr],
aliases: &[String],
) -> Result<Option<Box<dyn PhysicalOperator>>> {
let (join_input, post_join_predicate) = match input {
crate::sql::LogicalPlan::Join { .. } => (input, None),
crate::sql::LogicalPlan::Filter { input, predicate }
if matches!(input.as_ref(), crate::sql::LogicalPlan::Join { .. }) =>
{
(input.as_ref(), Some(predicate))
}
_ => return Ok(None),
};
let crate::sql::LogicalPlan::Join {
left,
right,
join_type,
on,
lateral,
} = join_input
else {
return Ok(None);
};
if *lateral || !matches!(join_type, crate::sql::JoinType::Inner) {
return Ok(None);
}
let Some(condition) = on else {
return Ok(None);
};
let (equi_part, residual_part) = split_join_condition(condition);
if equi_part.is_none() || residual_part.is_some() {
return Ok(None);
}
for expr in exprs {
if !matches!(expr, crate::sql::LogicalExpr::Column { .. }) {
return Ok(None);
}
}
let left_rows = estimate_hash_join_rows(executor, left);
let right_rows = estimate_hash_join_rows(executor, right);
let build_left_for_inner =
std::env::var("HELIOS_HASHJOIN_BUILD_RIGHT").is_err() && should_build_left_for_inner(left_rows, right_rows);
let inlj_left_rows = estimate_index_nested_loop_probe_rows(executor, left).or(left_rows);
if post_join_predicate.is_none()
&& should_try_index_nested_loop_join(inlj_left_rows, right_rows)
&& is_plain_scan_like(right)
{
if let Some(join_op) = try_index_nested_loop_join(
executor,
left,
right,
join_type,
equi_part.as_ref().expect("checked above"),
)? {
let timeout_ctx = executor.timeout_ctx();
let project_op = super::project::ProjectOperator::new(
join_op,
exprs.to_vec(),
aliases.to_vec(),
false,
executor.parameters().to_vec(),
)
.with_timeout(timeout_ctx);
return Ok(Some(Box::new(project_op)));
}
}
let compact_plans = equi_part
.as_ref()
.and_then(|condition| compact_projected_join_inputs(left, right, condition, post_join_predicate, exprs));
if post_join_predicate.is_some() && compact_plans.is_none() {
return Ok(None);
}
let (left_plan, right_plan) =
compact_plans.unwrap_or_else(|| ((*left).as_ref().clone(), (*right).as_ref().clone()));
let left_op = executor.plan_to_operator(&left_plan)?;
let right_op = executor.plan_to_operator(&right_plan)?;
let left_schema = left_op.schema();
let right_schema = right_op.schema();
let mut combined_columns = left_schema.columns.clone();
combined_columns.extend(right_schema.columns.clone());
let combined_schema = Arc::new(Schema {
columns: combined_columns,
});
let mut projection = Vec::with_capacity(exprs.len());
for expr in exprs {
let crate::sql::LogicalExpr::Column { table, name } = expr else {
return Ok(None);
};
let Some(idx) = combined_schema.get_qualified_column_index(table.as_deref(), name) else {
return Ok(None);
};
projection.push(idx);
}
use crate::sql::TypeInference;
let output_schema = Arc::new(Schema {
columns: aliases
.iter()
.zip(exprs.iter())
.map(|(alias, expr)| expr.to_column(alias.clone(), &combined_schema))
.collect(),
});
let timeout_ctx = executor.timeout_ctx();
let build_side = if build_left_for_inner {
HashJoinBuildSide::Left
} else {
HashJoinBuildSide::Right
};
if let Some(predicate) = post_join_predicate {
let mut join_op: Box<dyn PhysicalOperator> = if build_left_for_inner {
Box::new(HashJoinOperator::new_build_left(
left_op,
right_op,
crate::sql::JoinType::Inner,
equi_part,
timeout_ctx.clone(),
)?)
} else {
Box::new(HashJoinOperator::new(
left_op,
right_op,
crate::sql::JoinType::Inner,
equi_part,
timeout_ctx.clone(),
)?)
};
let materialized_predicate = executor.materialize_subqueries(predicate)?;
join_op = Box::new(
super::filter::FilterOperator::new(join_op, materialized_predicate, executor.parameters().to_vec())
.with_timeout(timeout_ctx.clone()),
);
let project_op = super::project::ProjectOperator::new(
join_op,
exprs.to_vec(),
aliases.to_vec(),
false,
executor.parameters().to_vec(),
)
.with_timeout(timeout_ctx);
return Ok(Some(Box::new(project_op)));
}
let op = HashJoinOperator::new_projected_inner(
left_op,
right_op,
equi_part,
projection,
output_schema,
build_side,
timeout_ctx,
)?;
Ok(Some(Box::new(op)))
}
fn estimate_hash_join_rows(executor: &Executor<'_>, plan: &crate::sql::LogicalPlan) -> Option<usize> {
match plan {
crate::sql::LogicalPlan::Scan { table_name, .. } => executor.storage()?.count_table_rows(table_name).ok(),
crate::sql::LogicalPlan::FilteredScan {
table_name, predicate, ..
} => {
let rows = executor.storage()?.count_table_rows(table_name).ok()?;
if predicate.is_some() {
Some(estimate_filtered_rows(rows))
} else {
Some(rows)
}
}
crate::sql::LogicalPlan::Filter { input, .. } => {
estimate_hash_join_rows(executor, input).map(estimate_filtered_rows)
}
crate::sql::LogicalPlan::Project { input, .. } | crate::sql::LogicalPlan::Sort { input, .. } => {
estimate_hash_join_rows(executor, input)
}
crate::sql::LogicalPlan::Limit { input, limit, .. } => {
estimate_hash_join_rows(executor, input).map(|rows| rows.min(*limit))
}
_ => None,
}
}
fn estimate_index_nested_loop_probe_rows(executor: &Executor<'_>, plan: &crate::sql::LogicalPlan) -> Option<usize> {
match plan {
crate::sql::LogicalPlan::FilteredScan {
table_name,
alias,
schema,
predicate: Some(predicate),
..
} => indexed_equality_predicate_is_selective(executor, table_name, alias.as_deref(), schema, predicate)
.then_some(1),
crate::sql::LogicalPlan::Filter { input, .. } => {
estimate_index_nested_loop_probe_rows(executor, input).map(estimate_filtered_rows)
}
crate::sql::LogicalPlan::Project { input, .. } | crate::sql::LogicalPlan::Sort { input, .. } => {
estimate_index_nested_loop_probe_rows(executor, input)
}
crate::sql::LogicalPlan::Limit { input, limit, .. } => {
estimate_index_nested_loop_probe_rows(executor, input).map(|rows| rows.min(*limit))
}
crate::sql::LogicalPlan::Join {
left,
right,
join_type,
on: Some(condition),
lateral: false,
} if matches!(join_type, crate::sql::JoinType::Inner) => {
let left_rows = estimate_index_nested_loop_probe_rows(executor, left)?;
if left_rows > 128 || !is_plain_scan_like(right) || !right_join_key_has_index(executor, right, condition) {
return None;
}
Some(left_rows.saturating_mul(8).min(128))
}
_ => None,
}
}
fn indexed_equality_predicate_is_selective(
executor: &Executor<'_>,
table_name: &str,
alias: Option<&str>,
schema: &Schema,
predicate: &crate::sql::LogicalExpr,
) -> bool {
use crate::sql::{BinaryOperator, LogicalExpr};
let LogicalExpr::BinaryExpr {
left,
op: BinaryOperator::Eq,
right,
} = predicate
else {
return false;
};
let column = match (left.as_ref(), right.as_ref()) {
(LogicalExpr::Column { table, name }, LogicalExpr::Literal(_) | LogicalExpr::Parameter { .. })
| (LogicalExpr::Literal(_) | LogicalExpr::Parameter { .. }, LogicalExpr::Column { table, name }) => {
if table.as_deref().is_some_and(|qualifier| {
!qualifier.eq_ignore_ascii_case(table_name)
&& !alias.is_some_and(|alias| qualifier.eq_ignore_ascii_case(alias))
}) {
return false;
}
name
}
_ => return false,
};
if !schema.columns.iter().any(|col| col.name.eq_ignore_ascii_case(column)) {
return false;
}
executor
.storage()
.and_then(|storage| storage.art_indexes().find_column_index(table_name, column))
.is_some()
}
fn right_join_key_has_index(
executor: &Executor<'_>,
right: &crate::sql::LogicalPlan,
condition: &crate::sql::LogicalExpr,
) -> bool {
let Some(storage) = executor.storage() else {
return false;
};
let Some((right_table, right_alias, _right_schema)) = extract_scan_info(right) else {
return false;
};
let Some((left_col, right_col)) = extract_equi_columns(condition) else {
return false;
};
let right_join_col = if column_matches_table(&right_col, &right_table, right_alias.as_deref()) {
&right_col.1
} else if column_matches_table(&left_col, &right_table, right_alias.as_deref()) {
&left_col.1
} else {
return false;
};
storage
.art_indexes()
.find_column_index(&right_table, right_join_col)
.is_some()
}
fn should_build_left_for_inner(left_rows: Option<usize>, right_rows: Option<usize>) -> bool {
match (left_rows, right_rows) {
(Some(l), Some(r)) => l.saturating_mul(4) < r,
_ => false,
}
}
fn should_try_index_nested_loop_join(left_rows: Option<usize>, right_rows: Option<usize>) -> bool {
if std::env::var("HELIOS_INLJ_OFF").is_ok() {
return false;
}
match (left_rows, right_rows) {
(Some(l), Some(r)) => l <= 128 || l.saturating_mul(8) <= r,
(Some(l), None) => l <= 128,
_ => false,
}
}
fn is_plain_scan_like(plan: &crate::sql::LogicalPlan) -> bool {
match plan {
crate::sql::LogicalPlan::Scan { .. } => true,
crate::sql::LogicalPlan::Project { input, .. } => is_plain_scan_like(input),
_ => false,
}
}
fn estimate_filtered_rows(rows: usize) -> usize {
rows.saturating_mul(33).saturating_add(99).saturating_div(100).max(1)
}
fn try_index_nested_loop_join(
executor: &mut Executor,
left: &crate::sql::LogicalPlan,
right: &crate::sql::LogicalPlan,
join_type: &crate::sql::JoinType,
condition: &crate::sql::LogicalExpr,
) -> Result<Option<Box<dyn PhysicalOperator>>> {
use crate::storage::art_manager::ArtIndexManager;
let (right_table, right_schema, right_output_schema, index_name, left_join_col, right_join_col_type) = {
let storage = match executor.storage() {
Some(s) => s,
None => return Ok(None),
};
if storage.is_branch_active() {
return Ok(None);
}
let (right_table, right_alias, right_schema) = match extract_scan_info(right) {
Some(info) => info,
None => return Ok(None),
};
let right_source_name = right_alias.as_deref().unwrap_or(right_table.as_str());
let right_output_schema = Arc::new(super::scan::schema_with_source(
right_schema.as_ref(),
right_source_name,
&right_table,
));
let (left_col, right_col) = match extract_equi_columns(condition) {
Some(pair) => pair,
None => return Ok(None),
};
let right_join_col = if column_matches_table(&right_col, &right_table, right_alias.as_deref()) {
right_col.1.clone()
} else if column_matches_table(&left_col, &right_table, right_alias.as_deref()) {
left_col.1.clone()
} else {
return Ok(None);
};
let index_name = match storage.art_indexes().find_column_index(&right_table, &right_join_col) {
Some(name) => name,
None => return Ok(None),
};
let right_join_col_type = right_schema
.columns
.iter()
.find(|c| c.name.eq_ignore_ascii_case(&right_join_col))
.map(|c| c.data_type.clone());
let left_join_col = if column_matches_table(&right_col, &right_table, right_alias.as_deref()) {
left_col.clone()
} else {
right_col.clone()
};
(
right_table,
right_schema,
right_output_schema,
index_name,
left_join_col,
right_join_col_type,
)
};
if executor.txn_forces_slow_reads_for_table(&right_table) {
return Ok(None);
}
let mut left_op = executor.plan_to_operator(left)?;
let left_schema = left_op.schema();
let left_key_idx = match find_column_index(&left_schema, left_join_col.0.as_deref(), &left_join_col.1) {
Some(idx) => idx,
None => return Ok(None),
};
match (
left_schema.columns.get(left_key_idx).map(|c| &c.data_type),
right_join_col_type.as_ref(),
) {
(Some(lt), Some(rt)) if lt == rt => {}
_ => return Ok(None),
}
let mut output_columns = left_schema.columns.clone();
output_columns.extend(right_output_schema.columns.clone());
let output_schema = Arc::new(Schema {
columns: output_columns,
});
let right_col_count = right_schema.columns.len();
let is_left_join = matches!(join_type, crate::sql::JoinType::Left);
let storage = executor
.storage()
.ok_or_else(|| Error::query_execution("Storage unavailable for INLJ"))?;
let mut result_tuples = Vec::new();
while let Some(left_tuple) = left_op.next()? {
let key_value = match left_tuple.values.get(left_key_idx) {
Some(v) if !matches!(v, crate::Value::Null) => v.clone(),
_ => {
if is_left_join {
let mut combined_values = left_tuple.values.clone();
combined_values.resize(combined_values.len() + right_col_count, crate::Value::Null);
result_tuples.push(Tuple {
values: combined_values,
row_id: None,
branch_id: None,
});
}
continue;
}
};
let encoded_key = ArtIndexManager::encode_key(&[key_value]);
let matching_row_ids = storage.art_indexes().index_get_all(&index_name, &encoded_key);
if matching_row_ids.is_empty() {
if is_left_join {
let mut combined_values = left_tuple.values.clone();
combined_values.resize(combined_values.len() + right_col_count, crate::Value::Null);
result_tuples.push(Tuple {
values: combined_values,
row_id: None,
branch_id: None,
});
}
continue;
}
for row_id in matching_row_ids {
if let Some(right_tuple) = storage.get_row_by_id_arc(&right_table, row_id, &right_schema)? {
let mut combined_values = Vec::with_capacity(left_tuple.values.len() + right_tuple.values.len());
combined_values.extend_from_slice(&left_tuple.values);
combined_values.extend_from_slice(&right_tuple.values);
result_tuples.push(Tuple {
values: combined_values,
row_id: None,
branch_id: None,
});
}
}
}
Ok(Some(Box::new(super::MaterializedOperator::new(
result_tuples,
output_schema,
))))
}
fn extract_scan_info(plan: &crate::sql::LogicalPlan) -> Option<(String, Option<String>, Arc<Schema>)> {
match plan {
crate::sql::LogicalPlan::Scan {
table_name,
alias,
schema,
..
} => Some((table_name.clone(), alias.clone(), schema.clone())),
crate::sql::LogicalPlan::Filter { input, .. } => extract_scan_info(input),
crate::sql::LogicalPlan::Project { input, .. } => extract_scan_info(input),
_ => None,
}
}
fn extract_equi_columns(
condition: &crate::sql::LogicalExpr,
) -> Option<((Option<String>, String), (Option<String>, String))> {
use crate::sql::{BinaryOperator, LogicalExpr};
match condition {
LogicalExpr::BinaryExpr {
left,
op: BinaryOperator::Eq,
right,
} => match (left.as_ref(), right.as_ref()) {
(LogicalExpr::Column { table: lt, name: ln }, LogicalExpr::Column { table: rt, name: rn }) => {
Some(((lt.clone(), ln.clone()), (rt.clone(), rn.clone())))
}
_ => None,
},
LogicalExpr::BinaryExpr {
left,
op: BinaryOperator::And,
..
} => extract_equi_columns(left),
_ => None,
}
}
fn column_matches_table(col: &(Option<String>, String), table_name: &str, alias: Option<&str>) -> bool {
match &col.0 {
Some(qualifier) => qualifier == table_name || alias.is_some_and(|a| a == qualifier),
None => false,
}
}
fn find_column_index(schema: &Schema, table: Option<&str>, name: &str) -> Option<usize> {
if let Some(tbl) = table {
for (i, col) in schema.columns.iter().enumerate() {
if col.name == name {
if let Some(ref src) = col.source_table_name {
if src == tbl {
return Some(i);
}
}
}
}
}
schema.columns.iter().position(|c| c.name == name)
}
fn split_join_condition(
condition: &crate::sql::LogicalExpr,
) -> (Option<crate::sql::LogicalExpr>, Option<crate::sql::LogicalExpr>) {
let mut equi_parts = Vec::new();
let mut residual_parts = Vec::new();
collect_and_terms(condition, &mut equi_parts, &mut residual_parts);
let equi = combine_with_and(equi_parts);
let residual = combine_with_and(residual_parts);
(equi, residual)
}
fn is_pure_equi_join(condition: &Option<crate::sql::LogicalExpr>) -> bool {
use crate::sql::{BinaryOperator, LogicalExpr};
fn check(expr: &LogicalExpr) -> bool {
match expr {
LogicalExpr::BinaryExpr {
op: BinaryOperator::Eq, ..
} => true,
LogicalExpr::BinaryExpr {
left,
op: BinaryOperator::And,
right,
} => check(left) && check(right),
_ => false,
}
}
match condition {
None => true,
Some(expr) => check(expr),
}
}
fn collect_and_terms(
expr: &crate::sql::LogicalExpr,
equi: &mut Vec<crate::sql::LogicalExpr>,
residual: &mut Vec<crate::sql::LogicalExpr>,
) {
use crate::sql::{BinaryOperator, LogicalExpr};
match expr {
LogicalExpr::BinaryExpr {
left,
op: BinaryOperator::And,
right,
} => {
collect_and_terms(left, equi, residual);
collect_and_terms(right, equi, residual);
}
LogicalExpr::BinaryExpr {
op: BinaryOperator::Eq, ..
} => {
equi.push(expr.clone());
}
_ => {
residual.push(expr.clone());
}
}
}
fn combine_with_and(parts: Vec<crate::sql::LogicalExpr>) -> Option<crate::sql::LogicalExpr> {
use crate::sql::{BinaryOperator, LogicalExpr};
parts.into_iter().reduce(|left, right| LogicalExpr::BinaryExpr {
left: Box::new(left),
op: BinaryOperator::And,
right: Box::new(right),
})
}