use std::{fmt::Display, hash::Hash, sync::Arc};
use arrow::{
array::{ArrayRef, UInt64Array},
datatypes::{DataType, Schema},
record_batch::RecordBatch,
};
use datafusion_common::Result;
use datafusion_common::hash_utils::RandomState;
use datafusion_common::hash_utils::{create_hashes, with_hashes};
#[cfg(feature = "proto")]
use datafusion_common::internal_err;
use datafusion_expr::ColumnarValue;
use datafusion_physical_expr_common::physical_expr::{
DynHash, PhysicalExpr, PhysicalExprRef,
};
use crate::joins::Map;
#[derive(Clone, Debug)]
pub struct SeededRandomState {
random_state: RandomState,
seed: u64,
}
impl SeededRandomState {
pub const fn with_seed(k: u64) -> Self {
Self {
random_state: RandomState::with_seed(k),
seed: k,
}
}
pub fn random_state(&self) -> &RandomState {
&self.random_state
}
pub fn seed(&self) -> u64 {
self.seed
}
}
pub struct HashExpr {
on_columns: Vec<PhysicalExprRef>,
random_state: SeededRandomState,
description: String,
}
impl HashExpr {
pub fn new(
on_columns: Vec<PhysicalExprRef>,
random_state: SeededRandomState,
description: String,
) -> Self {
Self {
on_columns,
random_state,
description,
}
}
pub fn on_columns(&self) -> &[PhysicalExprRef] {
&self.on_columns
}
pub fn seed(&self) -> u64 {
self.random_state.seed()
}
pub fn description(&self) -> &str {
&self.description
}
}
impl std::fmt::Debug for HashExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let cols = self
.on_columns
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", ");
let seed = self.seed();
write!(f, "{}({cols}, [{seed}])", self.description)
}
}
impl Hash for HashExpr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.on_columns.dyn_hash(state);
self.description.hash(state);
self.seed().hash(state);
}
}
impl PartialEq for HashExpr {
fn eq(&self, other: &Self) -> bool {
self.on_columns == other.on_columns
&& self.description == other.description
&& self.seed() == other.seed()
}
}
impl Eq for HashExpr {}
impl Display for HashExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
}
impl PhysicalExpr for HashExpr {
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
self.on_columns.iter().collect()
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> Result<Arc<dyn PhysicalExpr>> {
Ok(Arc::new(HashExpr::new(
children,
self.random_state.clone(),
self.description.clone(),
)))
}
fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
Ok(DataType::UInt64)
}
fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
Ok(false)
}
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
let num_rows = batch.num_rows();
let keys_values = evaluate_columns(&self.on_columns, batch)?;
let mut hashes_buffer = vec![0; num_rows];
create_hashes(
&keys_values,
self.random_state.random_state(),
&mut hashes_buffer,
)?;
Ok(ColumnarValue::Array(Arc::new(UInt64Array::from(
hashes_buffer,
))))
}
fn fmt_sql(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
#[cfg(feature = "proto")]
fn try_to_proto(
&self,
ctx: &datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx<'_>,
) -> Result<Option<datafusion_proto_models::protobuf::PhysicalExprNode>> {
use datafusion_proto_models::protobuf;
let on_columns = ctx.encode_children_expressions(&self.on_columns)?;
Ok(Some(protobuf::PhysicalExprNode {
expr_id: None,
expr_type: Some(protobuf::physical_expr_node::ExprType::HashExpr(
protobuf::PhysicalHashExprNode {
on_columns,
seed0: self.seed(),
description: self.description.clone(),
},
)),
}))
}
}
#[cfg(feature = "proto")]
impl HashExpr {
pub fn try_from_proto(
node: &datafusion_proto_models::protobuf::PhysicalExprNode,
ctx: &datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx<'_>,
) -> Result<Arc<dyn PhysicalExpr>> {
use datafusion_proto_models::protobuf;
let hash_expr = match &node.expr_type {
Some(protobuf::physical_expr_node::ExprType::HashExpr(h)) => h,
_ => return internal_err!("PhysicalExprNode is not a HashExpr"),
};
let on_columns = ctx.decode_children_expressions(&hash_expr.on_columns)?;
Ok(Arc::new(HashExpr::new(
on_columns,
SeededRandomState::with_seed(hash_expr.seed0),
hash_expr.description.clone(),
)))
}
}
pub struct HashTableLookupExpr {
on_columns: Vec<PhysicalExprRef>,
random_state: SeededRandomState,
map: Arc<Map>,
description: String,
}
impl HashTableLookupExpr {
pub fn new(
on_columns: Vec<PhysicalExprRef>,
random_state: SeededRandomState,
map: Arc<Map>,
description: String,
) -> Self {
Self {
on_columns,
random_state,
map,
description,
}
}
}
impl std::fmt::Debug for HashTableLookupExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let cols = self
.on_columns
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", ");
let seed = self.random_state.seed();
write!(f, "{}({cols}, [{seed}])", self.description)
}
}
impl Hash for HashTableLookupExpr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.on_columns.dyn_hash(state);
self.description.hash(state);
self.random_state.seed().hash(state);
Arc::as_ptr(&self.map).hash(state);
}
}
impl PartialEq for HashTableLookupExpr {
fn eq(&self, other: &Self) -> bool {
self.on_columns == other.on_columns
&& self.description == other.description
&& self.random_state.seed() == other.random_state.seed()
&& Arc::ptr_eq(&self.map, &other.map)
}
}
impl Eq for HashTableLookupExpr {}
impl Display for HashTableLookupExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
}
impl PhysicalExpr for HashTableLookupExpr {
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
self.on_columns.iter().collect()
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> Result<Arc<dyn PhysicalExpr>> {
Ok(Arc::new(HashTableLookupExpr::new(
children,
self.random_state.clone(),
Arc::clone(&self.map),
self.description.clone(),
)))
}
fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
Ok(DataType::Boolean)
}
fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
Ok(false)
}
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
let join_keys = evaluate_columns(&self.on_columns, batch)?;
match self.map.as_ref() {
Map::HashMap(map) => {
with_hashes(&join_keys, self.random_state.random_state(), |hashes| {
let array = map.contain_hashes(hashes);
Ok(ColumnarValue::Array(Arc::new(array)))
})
}
Map::ArrayMap(map) => {
let array = map.contain_keys(&join_keys)?;
Ok(ColumnarValue::Array(Arc::new(array)))
}
}
}
#[cfg(feature = "proto")]
fn try_to_proto(
&self,
_ctx: &datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx<'_>,
) -> Result<Option<datafusion_proto_models::protobuf::PhysicalExprNode>> {
use datafusion_proto_models::protobuf;
use datafusion_proto_models::protobuf::physical_expr_node::ExprType;
let value = datafusion_proto_common::ScalarValue {
value: Some(datafusion_proto_common::scalar_value::Value::BoolValue(
true,
)),
};
Ok(Some(protobuf::PhysicalExprNode {
expr_id: None,
expr_type: Some(ExprType::Literal(value)),
}))
}
fn fmt_sql(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
}
fn evaluate_columns(
columns: &[PhysicalExprRef],
batch: &RecordBatch,
) -> Result<Vec<ArrayRef>> {
let num_rows = batch.num_rows();
columns
.iter()
.map(|c| c.evaluate(batch)?.into_array(num_rows))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::joins::join_hash_map::JoinHashMapU32;
use datafusion_physical_expr::expressions::Column;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
fn compute_hash<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
#[test]
fn test_hash_expr_eq_same() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let col_b: PhysicalExprRef = Arc::new(Column::new("b", 1));
let expr1 = HashExpr::new(
vec![Arc::clone(&col_a), Arc::clone(&col_b)],
SeededRandomState::with_seed(1),
"test_hash".to_string(),
);
let expr2 = HashExpr::new(
vec![Arc::clone(&col_a), Arc::clone(&col_b)],
SeededRandomState::with_seed(1),
"test_hash".to_string(),
);
assert_eq!(expr1, expr2);
}
#[test]
fn test_hash_expr_eq_different_columns() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let col_b: PhysicalExprRef = Arc::new(Column::new("b", 1));
let col_c: PhysicalExprRef = Arc::new(Column::new("c", 2));
let expr1 = HashExpr::new(
vec![Arc::clone(&col_a), Arc::clone(&col_b)],
SeededRandomState::with_seed(1),
"test_hash".to_string(),
);
let expr2 = HashExpr::new(
vec![Arc::clone(&col_a), Arc::clone(&col_c)],
SeededRandomState::with_seed(1),
"test_hash".to_string(),
);
assert_ne!(expr1, expr2);
}
#[test]
fn test_hash_expr_eq_different_description() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let expr1 = HashExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
"hash_one".to_string(),
);
let expr2 = HashExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
"hash_two".to_string(),
);
assert_ne!(expr1, expr2);
}
#[test]
fn test_hash_expr_eq_different_seeds() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let expr1 = HashExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
"test_hash".to_string(),
);
let expr2 = HashExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(5),
"test_hash".to_string(),
);
assert_ne!(expr1, expr2);
}
#[test]
fn test_hash_expr_hash_consistency() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let col_b: PhysicalExprRef = Arc::new(Column::new("b", 1));
let expr1 = HashExpr::new(
vec![Arc::clone(&col_a), Arc::clone(&col_b)],
SeededRandomState::with_seed(1),
"test_hash".to_string(),
);
let expr2 = HashExpr::new(
vec![Arc::clone(&col_a), Arc::clone(&col_b)],
SeededRandomState::with_seed(1),
"test_hash".to_string(),
);
assert_eq!(expr1, expr2);
assert_eq!(compute_hash(&expr1), compute_hash(&expr2));
}
#[cfg(feature = "proto")]
mod proto_tests {
use super::*;
use arrow::datatypes::{DataType, Field};
use datafusion_common::internal_datafusion_err;
use datafusion_physical_expr_common::physical_expr::proto_decode::{
PhysicalExprDecode, PhysicalExprDecodeCtx,
};
use datafusion_physical_expr_common::physical_expr::proto_encode::{
PhysicalExprEncode, PhysicalExprEncodeCtx,
};
use datafusion_proto_models::protobuf;
struct TestEncoder;
impl PhysicalExprEncode for TestEncoder {
fn encode(
&self,
expr: &Arc<dyn PhysicalExpr>,
) -> Result<protobuf::PhysicalExprNode> {
let ctx = PhysicalExprEncodeCtx::new(self);
expr.try_to_proto(&ctx)?.ok_or_else(|| {
internal_datafusion_err!("test encoder cannot encode {expr:?}")
})
}
}
struct TestDecoder;
impl PhysicalExprDecode for TestDecoder {
fn decode(
&self,
node: &protobuf::PhysicalExprNode,
schema: &Schema,
) -> Result<Arc<dyn PhysicalExpr>> {
let ctx = PhysicalExprDecodeCtx::new(schema, self);
match &node.expr_type {
Some(protobuf::physical_expr_node::ExprType::Column(_)) => {
Column::try_from_proto(node, &ctx)
}
_ => internal_err!("test decoder cannot decode {node:?}"),
}
}
}
fn test_decode_ctx<'a>(
schema: &'a Schema,
decoder: &'a TestDecoder,
) -> PhysicalExprDecodeCtx<'a> {
PhysicalExprDecodeCtx::new(schema, decoder)
}
#[test]
fn hash_expr_try_to_proto() {
let expr = HashExpr::new(
vec![Arc::new(Column::new("a", 0)), Arc::new(Column::new("b", 1))],
SeededRandomState::with_seed(42),
"hash_join".to_string(),
);
let encoder = TestEncoder;
let ctx = PhysicalExprEncodeCtx::new(&encoder);
let proto = expr.try_to_proto(&ctx).unwrap().unwrap();
assert_eq!(proto.expr_id, None);
let hash_expr = match proto.expr_type.unwrap() {
protobuf::physical_expr_node::ExprType::HashExpr(hash_expr) => hash_expr,
other => panic!("expected HashExpr, got {other:?}"),
};
assert_eq!(hash_expr.seed0, 42);
assert_eq!(hash_expr.description, "hash_join");
assert_eq!(hash_expr.on_columns.len(), 2);
assert!(
hash_expr
.on_columns
.iter()
.all(|expr| expr.expr_id.is_none())
);
}
#[test]
fn hash_expr_try_from_proto() {
let schema = Schema::new(vec![
Field::new("a", DataType::Int32, false),
Field::new("b", DataType::Utf8, true),
]);
let decoder = TestDecoder;
let ctx = test_decode_ctx(&schema, &decoder);
let proto = protobuf::PhysicalExprNode {
expr_id: None,
expr_type: Some(protobuf::physical_expr_node::ExprType::HashExpr(
protobuf::PhysicalHashExprNode {
on_columns: vec![
protobuf::PhysicalExprNode {
expr_id: None,
expr_type: Some(
protobuf::physical_expr_node::ExprType::Column(
protobuf::PhysicalColumn {
name: "a".to_string(),
index: 0,
},
),
),
},
protobuf::PhysicalExprNode {
expr_id: None,
expr_type: Some(
protobuf::physical_expr_node::ExprType::Column(
protobuf::PhysicalColumn {
name: "b".to_string(),
index: 1,
},
),
),
},
],
seed0: 42,
description: "hash_join".to_string(),
},
)),
};
let expr = HashExpr::try_from_proto(&proto, &ctx).unwrap();
let expr = expr.downcast_ref::<HashExpr>().unwrap();
assert_eq!(expr.seed(), 42);
assert_eq!(expr.description(), "hash_join");
assert_eq!(expr.on_columns().len(), 2);
assert_eq!(
expr.on_columns()[0]
.downcast_ref::<Column>()
.map(|col| (col.name(), col.index())),
Some(("a", 0))
);
assert_eq!(
expr.on_columns()[1]
.downcast_ref::<Column>()
.map(|col| (col.name(), col.index())),
Some(("b", 1))
);
}
#[test]
fn hash_expr_try_from_proto_rejects_wrong_node_type() {
let schema = Schema::empty();
let decoder = TestDecoder;
let ctx = test_decode_ctx(&schema, &decoder);
let proto = protobuf::PhysicalExprNode {
expr_id: None,
expr_type: Some(protobuf::physical_expr_node::ExprType::Column(
protobuf::PhysicalColumn {
name: "a".to_string(),
index: 0,
},
)),
};
let err = HashExpr::try_from_proto(&proto, &ctx).unwrap_err();
assert!(
err.to_string()
.contains("PhysicalExprNode is not a HashExpr"),
"{err}"
);
}
}
#[test]
fn test_hash_table_lookup_expr_eq_same() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let hash_map =
Arc::new(Map::HashMap(Box::new(JoinHashMapU32::with_capacity(10))));
let expr1 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup".to_string(),
);
let expr2 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup".to_string(),
);
assert_eq!(expr1, expr2);
}
#[test]
fn test_hash_table_lookup_expr_eq_different_columns() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let col_b: PhysicalExprRef = Arc::new(Column::new("b", 1));
let hash_map =
Arc::new(Map::HashMap(Box::new(JoinHashMapU32::with_capacity(10))));
let expr1 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup".to_string(),
);
let expr2 = HashTableLookupExpr::new(
vec![Arc::clone(&col_b)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup".to_string(),
);
assert_ne!(expr1, expr2);
}
#[test]
fn test_hash_table_lookup_expr_eq_different_description() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let hash_map =
Arc::new(Map::HashMap(Box::new(JoinHashMapU32::with_capacity(10))));
let expr1 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup_one".to_string(),
);
let expr2 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup_two".to_string(),
);
assert_ne!(expr1, expr2);
}
#[test]
fn test_hash_table_lookup_expr_eq_different_hash_map() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let hash_map1 =
Arc::new(Map::HashMap(Box::new(JoinHashMapU32::with_capacity(10))));
let hash_map2 =
Arc::new(Map::HashMap(Box::new(JoinHashMapU32::with_capacity(10))));
let expr1 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
hash_map1,
"lookup".to_string(),
);
let expr2 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
hash_map2,
"lookup".to_string(),
);
assert_ne!(expr1, expr2);
}
#[test]
fn test_hash_table_lookup_expr_hash_consistency() {
let col_a: PhysicalExprRef = Arc::new(Column::new("a", 0));
let hash_map =
Arc::new(Map::HashMap(Box::new(JoinHashMapU32::with_capacity(10))));
let expr1 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup".to_string(),
);
let expr2 = HashTableLookupExpr::new(
vec![Arc::clone(&col_a)],
SeededRandomState::with_seed(1),
Arc::clone(&hash_map),
"lookup".to_string(),
);
assert_eq!(expr1, expr2);
assert_eq!(compute_hash(&expr1), compute_hash(&expr2));
}
}