use crate::{
core::{
error::{Error, ErrorCode, Result},
types::NullEquality,
},
frame::{dataframe::DataFrame, series::Series},
ops::join::{
ConditionalFullJoinSizeData, FilteredJoin, FilteredJoinOptions, FilteredJoinProbeState, HashJoin,
HashJoinOptions, MixedJoinSizeData, MixedSingleSideJoinSizeData, NullableJoin,
},
runtime::{DeviceId, ExecutionPlacement, execution_context::Exec},
};
const JOIN_GATHER_MAP_INDEX_SIZE_BYTES: usize = std::mem::size_of::<i32>();
const JOIN_GATHER_MAP_MAX_ROWS: usize = i32::MAX as usize;
const EQUALITY_JOIN_OUTPUT_SIZE_PREFLIGHT_CONTRACT: &str = "keyed_equality_join_output_size_preflight";
fn join_gather_map_reservation_size_bytes(
output_rows: usize,
map_count: usize,
operation: &'static str,
) -> Result<usize> {
if output_rows > JOIN_GATHER_MAP_MAX_ROWS {
return Err(Error::new_with(
ErrorCode::ValueOutOfRange,
operation,
"join gather-map row count exceeds INT32_MAX",
)
.with_detail("reason", "row_count_exceeds_cudf_size_type")
.with_detail("output_rows", output_rows.to_string())
.with_detail("max_output_rows", JOIN_GATHER_MAP_MAX_ROWS.to_string()));
}
Ok(output_rows * JOIN_GATHER_MAP_INDEX_SIZE_BYTES * map_count)
}
#[derive(Debug, Clone)]
pub struct DataFrameJoinOptions {
on: Option<Vec<String>>,
left_on: Option<Vec<String>>,
right_on: Option<Vec<String>>,
null_eq: NullEquality,
}
impl Default for DataFrameJoinOptions {
fn default() -> Self {
Self::new()
}
}
impl DataFrameJoinOptions {
pub fn new() -> Self {
Self { on: None, left_on: None, right_on: None, null_eq: NullEquality::Equal }
}
pub fn on<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.on = Some(keys.into_iter().map(|key| key.as_ref().to_string()).collect());
self
}
pub fn left_on<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.left_on = Some(keys.into_iter().map(|key| key.as_ref().to_string()).collect());
self
}
pub fn right_on<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.right_on = Some(keys.into_iter().map(|key| key.as_ref().to_string()).collect());
self
}
pub fn null_equality(mut self, null_eq: NullEquality) -> Self {
self.null_eq = null_eq;
self
}
pub fn on_keys(&self) -> Option<&[String]> {
self.on.as_deref()
}
pub fn left_on_keys(&self) -> Option<&[String]> {
self.left_on.as_deref()
}
pub fn right_on_keys(&self) -> Option<&[String]> {
self.right_on.as_deref()
}
pub fn null_eq(&self) -> NullEquality {
self.null_eq
}
}
#[derive(Debug, Clone)]
pub struct DataFrameCrossJoinOptions {
left_columns: Vec<String>,
right_columns: Vec<String>,
left_output_names: Vec<String>,
right_output_names: Vec<String>,
}
impl Default for DataFrameCrossJoinOptions {
fn default() -> Self {
Self::new()
}
}
impl DataFrameCrossJoinOptions {
pub fn new() -> Self {
Self {
left_columns: Vec::new(),
right_columns: Vec::new(),
left_output_names: Vec::new(),
right_output_names: Vec::new(),
}
}
pub fn left_columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.left_columns = columns.into_iter().map(|column| column.as_ref().to_string()).collect();
self.left_output_names = self.left_columns.clone();
self
}
pub fn right_columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.right_columns = columns.into_iter().map(|column| column.as_ref().to_string()).collect();
self.right_output_names = self.right_columns.clone();
self
}
pub fn left_projection<I, S, O>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = (S, O)>,
S: AsRef<str>,
O: AsRef<str>,
{
let (source, output): (Vec<_>, Vec<_>) = columns
.into_iter()
.map(|(source, output)| (source.as_ref().to_string(), output.as_ref().to_string()))
.unzip();
self.left_columns = source;
self.left_output_names = output;
self
}
pub fn right_projection<I, S, O>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = (S, O)>,
S: AsRef<str>,
O: AsRef<str>,
{
let (source, output): (Vec<_>, Vec<_>) = columns
.into_iter()
.map(|(source, output)| (source.as_ref().to_string(), output.as_ref().to_string()))
.unzip();
self.right_columns = source;
self.right_output_names = output;
self
}
pub fn left_column_names(&self) -> &[String] {
&self.left_columns
}
pub fn right_column_names(&self) -> &[String] {
&self.right_columns
}
pub fn left_output_names(&self) -> &[String] {
&self.left_output_names
}
pub fn right_output_names(&self) -> &[String] {
&self.right_output_names
}
}
#[derive(Debug, Clone)]
pub struct DataFrameProjectedCrossJoinPlan {
pub(crate) left: DataFrame,
pub(crate) right: DataFrame,
pub(crate) left_indices: Vec<usize>,
pub(crate) right_indices: Vec<usize>,
pub(crate) output_names: Vec<String>,
pub(crate) output_rows: usize,
}
#[derive(Debug, Clone)]
pub struct DataFramePredicateJoinOptions {
left_columns: Vec<String>,
right_columns: Vec<String>,
}
impl Default for DataFramePredicateJoinOptions {
fn default() -> Self {
Self::new()
}
}
impl DataFramePredicateJoinOptions {
pub fn new() -> Self {
Self { left_columns: Vec::new(), right_columns: Vec::new() }
}
pub fn left_columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.left_columns = columns.into_iter().map(|column| column.as_ref().to_string()).collect();
self
}
pub fn right_columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.right_columns = columns.into_iter().map(|column| column.as_ref().to_string()).collect();
self
}
pub fn left_column_names(&self) -> &[String] {
&self.left_columns
}
pub fn right_column_names(&self) -> &[String] {
&self.right_columns
}
}
#[derive(Debug, Clone)]
pub struct DataFrameMixedJoinOptions {
on: Option<Vec<String>>,
left_on: Option<Vec<String>>,
right_on: Option<Vec<String>>,
left_predicate_columns: Vec<String>,
right_predicate_columns: Vec<String>,
null_eq: NullEquality,
}
impl Default for DataFrameMixedJoinOptions {
fn default() -> Self {
Self::new()
}
}
impl DataFrameMixedJoinOptions {
pub fn new() -> Self {
Self {
on: None,
left_on: None,
right_on: None,
left_predicate_columns: Vec::new(),
right_predicate_columns: Vec::new(),
null_eq: NullEquality::Equal,
}
}
pub fn on<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.on = Some(keys.into_iter().map(|key| key.as_ref().to_string()).collect());
self
}
pub fn left_on<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.left_on = Some(keys.into_iter().map(|key| key.as_ref().to_string()).collect());
self
}
pub fn right_on<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.right_on = Some(keys.into_iter().map(|key| key.as_ref().to_string()).collect());
self
}
pub fn left_predicate_columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.left_predicate_columns = columns.into_iter().map(|column| column.as_ref().to_string()).collect();
self
}
pub fn right_predicate_columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.right_predicate_columns = columns.into_iter().map(|column| column.as_ref().to_string()).collect();
self
}
pub fn null_equality(mut self, null_eq: NullEquality) -> Self {
self.null_eq = null_eq;
self
}
pub fn on_keys(&self) -> Option<&[String]> {
self.on.as_deref()
}
pub fn left_on_keys(&self) -> Option<&[String]> {
self.left_on.as_deref()
}
pub fn right_on_keys(&self) -> Option<&[String]> {
self.right_on.as_deref()
}
pub fn left_predicate_column_names(&self) -> &[String] {
&self.left_predicate_columns
}
pub fn right_predicate_column_names(&self) -> &[String] {
&self.right_predicate_columns
}
pub fn null_eq(&self) -> NullEquality {
self.null_eq
}
}
#[derive(Debug, Clone, Copy)]
pub struct DataFrameHashJoinOptions {
null_eq: NullEquality,
nullable_join: NullableJoin,
load_factor: f64,
}
impl Default for DataFrameHashJoinOptions {
fn default() -> Self {
Self::new()
}
}
impl DataFrameHashJoinOptions {
pub fn new() -> Self {
Self { null_eq: NullEquality::Equal, nullable_join: NullableJoin::MaybeNull, load_factor: 0.5 }
}
pub fn null_equality(mut self, null_eq: NullEquality) -> Self {
self.null_eq = null_eq;
self
}
pub fn nullable_join(mut self, nullable_join: NullableJoin) -> Self {
self.nullable_join = nullable_join;
self
}
pub fn no_nulls(self) -> Self {
self.nullable_join(NullableJoin::NoNulls)
}
pub fn load_factor(mut self, load_factor: f64) -> Self {
self.load_factor = load_factor;
self
}
pub fn null_eq(&self) -> NullEquality {
self.null_eq
}
pub fn nullable_join_setting(&self) -> NullableJoin {
self.nullable_join
}
pub fn load_factor_value(&self) -> f64 {
self.load_factor
}
pub(crate) fn to_hash_join_options(self) -> HashJoinOptions {
HashJoinOptions::new()
.null_equality(self.null_eq)
.nullable_join(self.nullable_join)
.load_factor(self.load_factor)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DataFrameFilteredJoinOptions {
null_eq: NullEquality,
load_factor: f64,
}
impl Default for DataFrameFilteredJoinOptions {
fn default() -> Self {
Self::new()
}
}
impl DataFrameFilteredJoinOptions {
pub fn new() -> Self {
Self { null_eq: NullEquality::Equal, load_factor: 0.5 }
}
pub fn null_equality(mut self, null_eq: NullEquality) -> Self {
self.null_eq = null_eq;
self
}
pub fn load_factor(mut self, load_factor: f64) -> Self {
self.load_factor = load_factor;
self
}
pub fn null_eq(&self) -> NullEquality {
self.null_eq
}
pub fn load_factor_value(&self) -> f64 {
self.load_factor
}
pub(crate) fn to_filtered_join_options(self) -> FilteredJoinOptions {
FilteredJoinOptions::new().null_equality(self.null_eq).load_factor(self.load_factor)
}
}
#[derive(Debug, Clone)]
pub struct DataFrameJoinIndices {
indices: Series,
}
impl DataFrameJoinIndices {
pub(crate) fn new(indices: Series) -> Self {
Self { indices }
}
pub fn from_series(indices: Series) -> Self {
Self::new(indices)
}
pub fn indices(&self) -> &Series {
&self.indices
}
pub fn into_indices(self) -> Series {
self.indices
}
pub fn len(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
pub fn placement(&self) -> ExecutionPlacement {
self.indices.placement()
}
pub fn device(&self) -> DeviceId {
self.indices.device()
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
self.indices
.device_allocated_size_bytes()
.map_err(|error| error.reanchor("dataframe::join_indices::device_allocated_size_bytes"))
}
pub fn reservation_size_bytes_for_len(output_rows: usize) -> Result<usize> {
join_gather_map_reservation_size_bytes(
output_rows,
1,
"dataframe::join_indices::reservation_size_bytes_for_len",
)
}
}
#[derive(Debug, Clone)]
pub struct DataFrameJoinPairIndices {
left: DataFrameJoinIndices,
right: DataFrameJoinIndices,
}
impl DataFrameJoinPairIndices {
pub const OUTPUT_SIZE_PREFLIGHT_CONTRACT: &'static str = EQUALITY_JOIN_OUTPUT_SIZE_PREFLIGHT_CONTRACT;
pub(crate) fn new(left: Series, right: Series) -> Self {
Self { left: DataFrameJoinIndices::new(left), right: DataFrameJoinIndices::new(right) }
}
pub fn from_series(left: Series, right: Series) -> Self {
Self::new(left, right)
}
pub fn left(&self) -> &DataFrameJoinIndices {
&self.left
}
pub fn right(&self) -> &DataFrameJoinIndices {
&self.right
}
pub fn into_sides(self) -> (DataFrameJoinIndices, DataFrameJoinIndices) {
(self.left, self.right)
}
pub fn len(&self) -> usize {
self.left.len()
}
pub fn is_empty(&self) -> bool {
self.left.is_empty()
}
pub fn placement(&self) -> ExecutionPlacement {
self.left.placement()
}
pub fn device(&self) -> DeviceId {
self.left.device()
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
Ok(self.left.device_allocated_size_bytes()? + self.right.device_allocated_size_bytes()?)
}
pub fn reservation_size_bytes_for_len(output_rows: usize) -> Result<usize> {
join_gather_map_reservation_size_bytes(
output_rows,
2,
"dataframe::join_pair_indices::reservation_size_bytes_for_len",
)
}
}
#[derive(Debug, Clone)]
pub struct DataFrameHashJoinPairIndices {
probe: DataFrameJoinIndices,
build: DataFrameJoinIndices,
}
impl DataFrameHashJoinPairIndices {
pub(crate) fn new(probe: Series, build: Series) -> Self {
Self { probe: DataFrameJoinIndices::new(probe), build: DataFrameJoinIndices::new(build) }
}
pub fn probe(&self) -> &DataFrameJoinIndices {
&self.probe
}
pub fn build(&self) -> &DataFrameJoinIndices {
&self.build
}
pub fn len(&self) -> usize {
self.probe.len()
}
pub fn is_empty(&self) -> bool {
self.probe.is_empty()
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
Ok(self.probe.device_allocated_size_bytes()? + self.build.device_allocated_size_bytes()?)
}
pub fn reservation_size_bytes_for_len(output_rows: usize) -> Result<usize> {
join_gather_map_reservation_size_bytes(
output_rows,
2,
"dataframe::hash_join_pair_indices::reservation_size_bytes_for_len",
)
}
pub fn reservation_upper_bound_bytes_for_row_bound(output_row_bound: usize) -> usize {
output_row_bound.min(JOIN_GATHER_MAP_MAX_ROWS) * JOIN_GATHER_MAP_INDEX_SIZE_BYTES * 2
}
}
#[cfg(test)]
mod gather_map_bound_tests {
use super::*;
#[test]
fn hash_join_pair_bound_caps_at_the_largest_representable_output() {
let maximum =
DataFrameHashJoinPairIndices::reservation_upper_bound_bytes_for_row_bound(JOIN_GATHER_MAP_MAX_ROWS);
let above =
DataFrameHashJoinPairIndices::reservation_upper_bound_bytes_for_row_bound(JOIN_GATHER_MAP_MAX_ROWS + 1);
assert_eq!(above, maximum);
assert!(DataFrameHashJoinPairIndices::reservation_size_bytes_for_len(JOIN_GATHER_MAP_MAX_ROWS + 1).is_err());
}
}
#[derive(Debug, Clone)]
pub struct DataFrameHashJoinMaterialized {
pub probe: DataFrame,
pub build: DataFrame,
}
#[derive(Debug, Clone)]
pub struct DataFrameHashJoin {
pub(crate) join: HashJoin,
pub(crate) build: DataFrame,
pub(crate) build_keys: Vec<String>,
pub(crate) options: DataFrameHashJoinOptions,
}
#[derive(Debug, Clone)]
pub struct DataFrameFilteredJoin {
pub(crate) join: FilteredJoin,
pub(crate) build: DataFrame,
pub(crate) build_keys: Vec<String>,
pub(crate) options: DataFrameFilteredJoinOptions,
}
#[derive(Debug)]
pub struct DataFrameFilteredJoinProbeState {
pub(crate) state: FilteredJoinProbeState,
}
#[derive(Debug)]
pub struct DataFrameConditionalFullJoinSizeData<'a> {
pub(crate) state: ConditionalFullJoinSizeData<'a>,
pub(crate) _left: DataFrame,
pub(crate) _right: DataFrame,
}
#[derive(Debug)]
pub struct DataFrameMixedJoinSizeData<'a> {
pub(crate) state: MixedJoinSizeData<'a>,
pub(crate) _left: DataFrame,
pub(crate) _right: DataFrame,
}
#[derive(Debug)]
pub struct DataFrameMixedSingleSideJoinSizeData<'a> {
pub(crate) state: MixedSingleSideJoinSizeData<'a>,
pub(crate) _left: DataFrame,
pub(crate) _right: DataFrame,
}
impl DataFrameHashJoin {
pub fn build(&self) -> &DataFrame {
&self.build
}
pub fn build_keys(&self) -> &[String] {
&self.build_keys
}
pub fn options(&self) -> DataFrameHashJoinOptions {
self.options
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
self.join
.device_allocated_size_bytes()
.map_err(|error| error.reanchor("dataframe::hash_join::device_allocated_size_bytes"))
}
pub fn probe_gather_map_reservation_size_bytes(&self, output_rows: usize) -> Result<usize> {
DataFrameHashJoinPairIndices::reservation_size_bytes_for_len(output_rows)
.map_err(|error| error.reanchor("dataframe::hash_join::probe_gather_map_reservation_size_bytes"))
}
}
impl DataFrameFilteredJoin {
pub fn build(&self) -> &DataFrame {
&self.build
}
pub fn build_keys(&self) -> &[String] {
&self.build_keys
}
pub fn options(&self) -> DataFrameFilteredJoinOptions {
self.options
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
self.join
.device_allocated_size_bytes()
.map_err(|error| error.reanchor("dataframe::filtered_join::device_allocated_size_bytes"))
}
pub fn probe_gather_map_reservation_size_bytes(&self, output_rows: usize) -> Result<usize> {
DataFrameJoinIndices::reservation_size_bytes_for_len(output_rows)
.map_err(|error| error.reanchor("dataframe::filtered_join::probe_gather_map_reservation_size_bytes"))
}
}
impl DataFrameFilteredJoinProbeState {
pub fn output_size(&self) -> usize {
self.state.output_size()
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
self.state
.device_allocated_size_bytes()
.map_err(|error| error.reanchor("dataframe::filtered_join_probe_state::device_allocated_size_bytes"))
}
pub fn probe_gather_map_reservation_size_bytes(&self) -> Result<usize> {
DataFrameJoinIndices::reservation_size_bytes_for_len(self.output_size()).map_err(|error| {
error.reanchor("dataframe::filtered_join_probe_state::probe_gather_map_reservation_size_bytes")
})
}
pub fn materialize_indices(self, ctx: Exec<'_>) -> Result<DataFrameJoinIndices> {
let raw = self
.state
.materialize_indices_in(ctx)
.map_err(|error| error.reanchor("dataframe::filtered_join_probe_state::materialize_indices"))?;
Ok(DataFrameJoinIndices::new(Series::from_column(raw.indices)?))
}
}
impl DataFrameConditionalFullJoinSizeData<'_> {
pub fn output_size(&self) -> usize {
self.state.output_size()
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
self.state
.device_allocated_size_bytes()
.map_err(|error| error.reanchor("dataframe::conditional_full_join_size_data::device_allocated_size_bytes"))
}
pub fn gather_map_reservation_size_bytes(&self) -> Result<usize> {
self.state.gather_map_reservation_size_bytes().map_err(|error| {
error.reanchor("dataframe::conditional_full_join_size_data::gather_map_reservation_size_bytes")
})
}
pub fn materialize_indices(self, ctx: Exec<'_>) -> Result<DataFrameJoinPairIndices> {
let raw = self
.state
.materialize_indices_in(ctx)
.map_err(|error| error.reanchor("dataframe::conditional_full_join_size_data::materialize_indices"))?;
Ok(DataFrameJoinPairIndices::new(Series::from_column(raw.left)?, Series::from_column(raw.right)?))
}
}
impl DataFrameMixedJoinSizeData<'_> {
pub fn output_size(&self) -> usize {
self.state.output_size()
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
self.state
.device_allocated_size_bytes()
.map_err(|error| error.reanchor("dataframe::mixed_join_size_data::device_allocated_size_bytes"))
}
pub fn gather_map_reservation_size_bytes(&self) -> Result<usize> {
self.state
.gather_map_reservation_size_bytes()
.map_err(|error| error.reanchor("dataframe::mixed_join_size_data::gather_map_reservation_size_bytes"))
}
pub fn materialize_indices(self, ctx: Exec<'_>) -> Result<DataFrameJoinPairIndices> {
let raw = self
.state
.materialize_indices_in(ctx)
.map_err(|error| error.reanchor("dataframe::mixed_join_size_data::materialize_indices"))?;
Ok(DataFrameJoinPairIndices::new(Series::from_column(raw.left)?, Series::from_column(raw.right)?))
}
}
impl DataFrameMixedSingleSideJoinSizeData<'_> {
pub fn output_size(&self) -> usize {
self.state.output_size()
}
pub fn device_allocated_size_bytes(&self) -> Result<usize> {
self.state
.device_allocated_size_bytes()
.map_err(|error| error.reanchor("dataframe::mixed_single_side_join_size_data::device_allocated_size_bytes"))
}
pub fn gather_map_reservation_size_bytes(&self) -> Result<usize> {
self.state.gather_map_reservation_size_bytes().map_err(|error| {
error.reanchor("dataframe::mixed_single_side_join_size_data::gather_map_reservation_size_bytes")
})
}
pub fn materialize_indices(self, ctx: Exec<'_>) -> Result<DataFrameJoinIndices> {
let raw = self
.state
.materialize_indices_in(ctx)
.map_err(|error| error.reanchor("dataframe::mixed_single_side_join_size_data::materialize_indices"))?;
Ok(DataFrameJoinIndices::new(Series::from_column(raw.indices)?))
}
}