Skip to main content

polars_ops/frame/join/
mod.rs

1mod args;
2#[cfg(feature = "asof_join")]
3mod asof;
4mod cross_join;
5mod dispatch_left_right;
6mod general;
7mod hash_join;
8#[cfg(feature = "iejoin")]
9mod iejoin;
10pub mod merge_join;
11#[cfg(feature = "merge_sorted")]
12mod merge_sorted;
13mod validation;
14
15use std::borrow::Cow;
16use std::hash::Hash;
17
18pub use args::*;
19#[cfg(feature = "asof_join")]
20pub use asof::{_check_asof_columns, _join_asof_dispatch, AsofJoin, AsofJoinBy};
21pub use cross_join::CrossJoin;
22#[cfg(feature = "chunked_ids")]
23use either::Either;
24#[cfg(feature = "chunked_ids")]
25use general::create_chunked_index_mapping;
26pub use general::{_coalesce_full_join, _finish_join, _join_suffix_name};
27pub use hash_join::*;
28use hashbrown::hash_map::{Entry, RawEntryMut};
29#[cfg(feature = "merge_sorted")]
30pub use merge_sorted::_merge_sorted_dfs;
31use polars_arrow::trusted_len::TrustedLen;
32#[allow(unused_imports)]
33use polars_core::chunked_array::ops::row_encode::{
34    encode_rows_vertical_par_unordered, encode_rows_vertical_par_unordered_broadcast_nulls,
35};
36use polars_core::datatypes::DataType;
37use polars_core::hashing::_HASHMAP_INIT_SIZE;
38use polars_core::prelude::*;
39use polars_core::runtime::RAYON;
40pub(super) use polars_core::series::IsSorted;
41use polars_core::utils::slice_offsets;
42#[allow(unused_imports)]
43use polars_core::utils::slice_slice;
44use polars_defs::join::{
45    JoinArgs, JoinCoalesce, JoinType, JoinTypeOptions, JoinValidation, MaintainOrderJoin,
46};
47use polars_utils::hashing::BytesHash;
48use rayon::prelude::*;
49
50use self::cross_join::fused_cross_filter;
51use super::IntoDf;
52
53/// Reduces monomorphization: rayon plumbing is instantiated per `R`, not per closure.
54pub(crate) fn par_map_collect<R: Send>(n: usize, f: &(dyn Fn(usize) -> R + Sync)) -> Vec<R> {
55    RAYON.install(|| (0..n).into_par_iter().map(f).collect())
56}
57
58pub trait DataFrameJoinOps: IntoDf {
59    /// Generic join method. Can be used to join on multiple columns.
60    ///
61    /// # Example
62    ///
63    /// ```no_run
64    /// # use polars_core::prelude::*;
65    /// # use polars_defs::join::{JoinArgs, JoinType};
66    /// # use polars_ops::prelude::*;
67    /// let df1: DataFrame = df!("Fruit" => &["Apple", "Banana", "Pear"],
68    ///                          "Phosphorus (mg/100g)" => &[11, 22, 12])?;
69    /// let df2: DataFrame = df!("Name" => &["Apple", "Banana", "Pear"],
70    ///                          "Potassium (mg/100g)" => &[107, 358, 115])?;
71    ///
72    /// let df3: DataFrame = df1.join(&df2, ["Fruit"], ["Name"], JoinArgs::new(JoinType::Inner),
73    /// None)?;
74    /// assert_eq!(df3.shape(), (3, 3));
75    /// println!("{}", df3);
76    /// # Ok::<(), PolarsError>(())
77    /// ```
78    ///
79    /// Output:
80    ///
81    /// ```text
82    /// shape: (3, 3)
83    /// +--------+----------------------+---------------------+
84    /// | Fruit  | Phosphorus (mg/100g) | Potassium (mg/100g) |
85    /// | ---    | ---                  | ---                 |
86    /// | str    | i32                  | i32                 |
87    /// +========+======================+=====================+
88    /// | Apple  | 11                   | 107                 |
89    /// +--------+----------------------+---------------------+
90    /// | Banana | 22                   | 358                 |
91    /// +--------+----------------------+---------------------+
92    /// | Pear   | 12                   | 115                 |
93    /// +--------+----------------------+---------------------+
94    /// ```
95    fn join(
96        &self,
97        other: &DataFrame,
98        left_on: impl IntoIterator<Item = impl AsRef<str>>,
99        right_on: impl IntoIterator<Item = impl AsRef<str>>,
100        args: JoinArgs,
101        options: Option<JoinTypeOptions>,
102    ) -> PolarsResult<DataFrame> {
103        let df_left = self.to_df();
104        let selected_left = df_left.select_to_vec(left_on)?;
105        let selected_right = other.select_to_vec(right_on)?;
106
107        let selected_left = selected_left
108            .into_iter()
109            .map(Column::take_materialized_series)
110            .collect::<Vec<_>>();
111        let selected_right = selected_right
112            .into_iter()
113            .map(Column::take_materialized_series)
114            .collect::<Vec<_>>();
115
116        self._join_impl(
117            other,
118            selected_left,
119            selected_right,
120            args,
121            options,
122            true,
123            false,
124        )
125    }
126
127    #[doc(hidden)]
128    #[allow(clippy::too_many_arguments)]
129    #[allow(unused_mut)]
130    fn _join_impl(
131        &self,
132        other: &DataFrame,
133        mut selected_left: Vec<Series>,
134        mut selected_right: Vec<Series>,
135        mut args: JoinArgs,
136        options: Option<JoinTypeOptions>,
137        _check_rechunk: bool,
138        _verbose: bool,
139    ) -> PolarsResult<DataFrame> {
140        let left_df = self.to_df();
141
142        // This join has no per-candidate match condition, so it filters after the fact.
143        if let Some(JoinTypeOptions::FusedPredicate(fused_options)) = &options {
144            debug_assert!(args.slice.is_none());
145            let predicate = fused_options.predicate.clone();
146            let joined = self._join_impl(
147                other,
148                selected_left,
149                selected_right,
150                args,
151                None,
152                _check_rechunk,
153                _verbose,
154            )?;
155            return predicate.apply(joined, true);
156        }
157
158        #[cfg(feature = "cross_join")]
159        if let Some(JoinTypeOptions::Cross(cross_options)) = &options {
160            assert!(args.slice.is_none());
161            return fused_cross_filter(
162                left_df,
163                other,
164                args.suffix.clone(),
165                cross_options,
166                args.maintain_order,
167                args.how.emits_unmatched_left(),
168                &args.how,
169            );
170        }
171        #[cfg(feature = "cross_join")]
172        if let JoinType::Cross = args.how {
173            return left_df.cross_join(other, args.suffix.clone(), args.slice, args.maintain_order);
174        }
175
176        // Clear literals if a frame is empty. Otherwise we could get an oob
177        fn clear(s: &mut [Series]) {
178            for s in s.iter_mut() {
179                if s.len() == 1 {
180                    *s = s.clear()
181                }
182            }
183        }
184        if left_df.height() == 0 {
185            clear(&mut selected_left);
186        }
187        if other.height() == 0 {
188            clear(&mut selected_right);
189        }
190
191        let should_coalesce = args.should_coalesce();
192        assert_eq!(selected_left.len(), selected_right.len());
193
194        #[cfg(feature = "chunked_ids")]
195        {
196            // a left join create chunked-ids
197            // the others not yet.
198            // TODO! change this to other join types once they support chunked-id joins
199            if _check_rechunk
200                && !(matches!(args.how, JoinType::Left)
201                    || std::env::var("POLARS_NO_CHUNKED_JOIN").is_ok())
202            {
203                let mut left = Cow::Borrowed(left_df);
204                let mut right = Cow::Borrowed(other);
205                if left_df.should_rechunk() {
206                    if _verbose {
207                        eprintln!(
208                            "{:?} join triggered a rechunk of the left DataFrame: {} columns are affected",
209                            args.how,
210                            left_df.width()
211                        );
212                    }
213
214                    let mut tmp_left = left_df.clone();
215                    tmp_left.rechunk_mut_par();
216                    left = Cow::Owned(tmp_left);
217                }
218                if other.should_rechunk() {
219                    if _verbose {
220                        eprintln!(
221                            "{:?} join triggered a rechunk of the right DataFrame: {} columns are affected",
222                            args.how,
223                            other.width()
224                        );
225                    }
226                    let mut tmp_right = other.clone();
227                    tmp_right.rechunk_mut_par();
228                    right = Cow::Owned(tmp_right);
229                }
230                return left._join_impl(
231                    &right,
232                    selected_left,
233                    selected_right,
234                    args,
235                    options,
236                    false,
237                    _verbose,
238                );
239            }
240        }
241
242        if let Some((l, r)) = selected_left
243            .iter()
244            .zip(&selected_right)
245            .find(|(l, r)| l.dtype() != r.dtype())
246        {
247            polars_bail!(
248                ComputeError:
249                    "datatypes of join keys don't match - `{}`: {} on left does not match `{}`: {} on right",
250                    l.name(), l.dtype().pretty_format(), r.name(), r.dtype().pretty_format()
251            );
252        };
253
254        #[cfg(feature = "iejoin")]
255        if let Some(JoinTypeOptions::IEJoin(ie_options)) = options {
256            let func = if RAYON.current_num_threads() > 1
257                && !left_df.shape_has_zero()
258                && !other.shape_has_zero()
259            {
260                iejoin::iejoin_par
261            } else {
262                iejoin::iejoin
263            };
264            let emit_unmatched = if args.how.emits_unmatched_left() {
265                iejoin::EmitUnmatched::Left
266            } else if args.how.emits_unmatched_right() {
267                iejoin::EmitUnmatched::Right
268            } else {
269                iejoin::EmitUnmatched::None
270            };
271            return func(
272                left_df,
273                other,
274                selected_left,
275                selected_right,
276                &ie_options,
277                args.suffix,
278                args.slice,
279                emit_unmatched,
280            );
281        }
282
283        // Single keys.
284        if selected_left.len() == 1 {
285            let s_left = &selected_left[0];
286            let s_right = &selected_right[0];
287            let drop_names: Option<Vec<PlSmallStr>> =
288                if should_coalesce { None } else { Some(vec![]) };
289            return match args.how {
290                JoinType::Inner => left_df
291                    ._inner_join_from_series(other, s_left, s_right, args, _verbose, drop_names),
292                JoinType::Left => dispatch_left_right::left_join_from_series(
293                    self.to_df().clone(),
294                    other,
295                    s_left,
296                    s_right,
297                    args,
298                    _verbose,
299                    drop_names,
300                ),
301                JoinType::Right => dispatch_left_right::right_join_from_series(
302                    self.to_df(),
303                    other.clone(),
304                    s_left,
305                    s_right,
306                    args,
307                    _verbose,
308                    drop_names,
309                ),
310                JoinType::Full => left_df._full_join_from_series(other, s_left, s_right, args),
311                #[cfg(feature = "semi_anti_join")]
312                JoinType::Anti => left_df._semi_anti_join_from_series(
313                    s_left,
314                    s_right,
315                    args.slice,
316                    true,
317                    args.nulls_equal,
318                ),
319                #[cfg(feature = "semi_anti_join")]
320                JoinType::Semi => left_df._semi_anti_join_from_series(
321                    s_left,
322                    s_right,
323                    args.slice,
324                    false,
325                    args.nulls_equal,
326                ),
327                #[cfg(feature = "asof_join")]
328                JoinType::AsOf(options) => match (options.left_by, options.right_by) {
329                    (Some(left_by), Some(right_by)) => left_df._join_asof_by(
330                        other,
331                        s_left,
332                        s_right,
333                        left_by,
334                        right_by,
335                        options.strategy,
336                        options.tolerance.map(|v| v.into_value()),
337                        args.suffix.clone(),
338                        args.slice,
339                        should_coalesce,
340                        options.allow_eq,
341                        options.check_sortedness,
342                    ),
343                    (None, None) => left_df._join_asof(
344                        other,
345                        s_left,
346                        s_right,
347                        options.strategy,
348                        options.tolerance.map(|v| v.into_value()),
349                        args.suffix,
350                        args.slice,
351                        should_coalesce,
352                        options.allow_eq,
353                        options.check_sortedness,
354                    ),
355                    _ => {
356                        panic!("expected by arguments on both sides")
357                    },
358                },
359                #[cfg(feature = "iejoin")]
360                JoinType::IEJoin | JoinType::Range => {
361                    unreachable!()
362                },
363                JoinType::Cross => {
364                    unreachable!()
365                },
366            };
367        }
368        let (lhs_keys, rhs_keys) = if (left_df.height() == 0 || other.height() == 0)
369            && matches!(&args.how, JoinType::Inner)
370        {
371            // Fast path for empty inner joins.
372            // Return 2 dummies so that we don't row-encode.
373            let a = Series::full_null("".into(), 0, &DataType::Null);
374            (a.clone(), a)
375        } else {
376            // Row encode the keys.
377            (
378                prepare_keys_multiple(&selected_left, args.nulls_equal)?.into_series(),
379                prepare_keys_multiple(&selected_right, args.nulls_equal)?.into_series(),
380            )
381        };
382
383        let drop_names = if should_coalesce {
384            if args.how == JoinType::Right {
385                selected_left
386                    .iter()
387                    .map(|s| s.name().clone())
388                    .collect::<Vec<_>>()
389            } else {
390                selected_right
391                    .iter()
392                    .map(|s| s.name().clone())
393                    .collect::<Vec<_>>()
394            }
395        } else {
396            vec![]
397        };
398
399        // Multiple keys.
400        match args.how {
401            #[cfg(feature = "asof_join")]
402            JoinType::AsOf(_) => polars_bail!(
403                ComputeError: "asof join not supported for join on multiple keys"
404            ),
405            #[cfg(feature = "iejoin")]
406            JoinType::IEJoin | JoinType::Range => {
407                unreachable!()
408            },
409            JoinType::Cross => {
410                unreachable!()
411            },
412            JoinType::Full => {
413                let names_left = selected_left
414                    .iter()
415                    .map(|s| s.name().clone())
416                    .collect::<Vec<_>>();
417                args.coalesce = JoinCoalesce::KeepColumns;
418                let suffix = args.suffix.clone();
419                let out = left_df._full_join_from_series(other, &lhs_keys, &rhs_keys, args);
420
421                if should_coalesce {
422                    Ok(_coalesce_full_join(
423                        out?,
424                        names_left.as_slice(),
425                        drop_names.as_slice(),
426                        suffix,
427                        left_df,
428                    ))
429                } else {
430                    out
431                }
432            },
433            JoinType::Inner => left_df._inner_join_from_series(
434                other,
435                &lhs_keys,
436                &rhs_keys,
437                args,
438                _verbose,
439                Some(drop_names),
440            ),
441            JoinType::Left => dispatch_left_right::left_join_from_series(
442                left_df.clone(),
443                other,
444                &lhs_keys,
445                &rhs_keys,
446                args,
447                _verbose,
448                Some(drop_names),
449            ),
450            JoinType::Right => dispatch_left_right::right_join_from_series(
451                left_df,
452                other.clone(),
453                &lhs_keys,
454                &rhs_keys,
455                args,
456                _verbose,
457                Some(drop_names),
458            ),
459            #[cfg(feature = "semi_anti_join")]
460            JoinType::Anti | JoinType::Semi => self._join_impl(
461                other,
462                vec![lhs_keys],
463                vec![rhs_keys],
464                args,
465                options,
466                _check_rechunk,
467                _verbose,
468            ),
469        }
470    }
471
472    /// Perform an inner join on two DataFrames.
473    ///
474    /// # Example
475    ///
476    /// ```
477    /// # use polars_core::prelude::*;
478    /// # use polars_ops::prelude::*;
479    /// fn join_dfs(left: &DataFrame, right: &DataFrame) -> PolarsResult<DataFrame> {
480    ///     left.inner_join(right, ["join_column_left"], ["join_column_right"])
481    /// }
482    /// ```
483    fn inner_join(
484        &self,
485        other: &DataFrame,
486        left_on: impl IntoIterator<Item = impl AsRef<str>>,
487        right_on: impl IntoIterator<Item = impl AsRef<str>>,
488    ) -> PolarsResult<DataFrame> {
489        self.join(
490            other,
491            left_on,
492            right_on,
493            JoinArgs::new(JoinType::Inner),
494            None,
495        )
496    }
497
498    /// Perform a left outer join on two DataFrames
499    /// # Example
500    ///
501    /// ```no_run
502    /// # use polars_core::prelude::*;
503    /// # use polars_ops::prelude::*;
504    /// let df1: DataFrame = df!("Wavelength (nm)" => &[480.0, 650.0, 577.0, 1201.0, 100.0])?;
505    /// let df2: DataFrame = df!("Color" => &["Blue", "Yellow", "Red"],
506    ///                          "Wavelength nm" => &[480.0, 577.0, 650.0])?;
507    ///
508    /// let df3: DataFrame = df1.left_join(&df2, ["Wavelength (nm)"], ["Wavelength nm"])?;
509    /// println!("{:?}", df3);
510    /// # Ok::<(), PolarsError>(())
511    /// ```
512    ///
513    /// Output:
514    ///
515    /// ```text
516    /// shape: (5, 2)
517    /// +-----------------+--------+
518    /// | Wavelength (nm) | Color  |
519    /// | ---             | ---    |
520    /// | f64             | str    |
521    /// +=================+========+
522    /// | 480             | Blue   |
523    /// +-----------------+--------+
524    /// | 650             | Red    |
525    /// +-----------------+--------+
526    /// | 577             | Yellow |
527    /// +-----------------+--------+
528    /// | 1201            | null   |
529    /// +-----------------+--------+
530    /// | 100             | null   |
531    /// +-----------------+--------+
532    /// ```
533    fn left_join(
534        &self,
535        other: &DataFrame,
536        left_on: impl IntoIterator<Item = impl AsRef<str>>,
537        right_on: impl IntoIterator<Item = impl AsRef<str>>,
538    ) -> PolarsResult<DataFrame> {
539        self.join(
540            other,
541            left_on,
542            right_on,
543            JoinArgs::new(JoinType::Left),
544            None,
545        )
546    }
547
548    /// Perform a full outer join on two DataFrames
549    /// # Example
550    ///
551    /// ```
552    /// # use polars_core::prelude::*;
553    /// # use polars_ops::prelude::*;
554    /// fn join_dfs(left: &DataFrame, right: &DataFrame) -> PolarsResult<DataFrame> {
555    ///     left.full_join(right, ["join_column_left"], ["join_column_right"])
556    /// }
557    /// ```
558    fn full_join(
559        &self,
560        other: &DataFrame,
561        left_on: impl IntoIterator<Item = impl AsRef<str>>,
562        right_on: impl IntoIterator<Item = impl AsRef<str>>,
563    ) -> PolarsResult<DataFrame> {
564        self.join(
565            other,
566            left_on,
567            right_on,
568            JoinArgs::new(JoinType::Full),
569            None,
570        )
571    }
572}
573
574trait DataFrameJoinOpsPrivate: IntoDf {
575    fn _inner_join_from_series(
576        &self,
577        other: &DataFrame,
578        s_left: &Series,
579        s_right: &Series,
580        args: JoinArgs,
581        verbose: bool,
582        drop_names: Option<Vec<PlSmallStr>>,
583    ) -> PolarsResult<DataFrame> {
584        let left_df = self.to_df();
585        let ((join_tuples_left, join_tuples_right), sorted) =
586            _sort_or_hash_inner(s_left, s_right, verbose, args.validation, args.nulls_equal)?;
587
588        let mut join_tuples_left = &*join_tuples_left;
589        let mut join_tuples_right = &*join_tuples_right;
590
591        let already_left_sorted = sorted
592            && matches!(
593                args.maintain_order,
594                MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight
595            );
596        let need_sort = args.maintain_order != MaintainOrderJoin::None && !already_left_sorted;
597
598        if !need_sort && let Some((offset, len)) = args.slice {
599            join_tuples_left = slice_slice(join_tuples_left, offset, len);
600            join_tuples_right = slice_slice(join_tuples_right, offset, len);
601        }
602
603        let other = if let Some(drop_names) = drop_names {
604            other.drop_many(drop_names)
605        } else {
606            other.drop(s_right.name()).unwrap()
607        };
608
609        let mut left = unsafe { IdxCa::mmap_slice("a".into(), join_tuples_left) };
610        if sorted {
611            left.set_sorted_flag(IsSorted::Ascending);
612        }
613        let right = unsafe { IdxCa::mmap_slice("b".into(), join_tuples_right) };
614
615        try_raise_polars_abort();
616
617        let (df_left, df_right) = if need_sort {
618            let mut df = unsafe {
619                DataFrame::new_unchecked_infer_height(vec![
620                    left.into_series().into(),
621                    right.into_series().into(),
622                ])
623            };
624
625            let columns = match args.maintain_order {
626                MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight => vec!["a"],
627                MaintainOrderJoin::Right | MaintainOrderJoin::RightLeft => vec!["b"],
628                _ => unreachable!(),
629            };
630
631            let options = SortMultipleOptions::new()
632                .with_order_descending(false)
633                .with_maintain_order(true);
634
635            df.sort_in_place(columns, options)?;
636
637            if let Some((offset, len)) = args.slice {
638                df = df.slice(offset, len);
639            }
640
641            let [mut a, b]: [Column; 2] = df.into_columns().try_into().unwrap();
642            if matches!(
643                args.maintain_order,
644                MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight
645            ) {
646                a.set_sorted_flag(IsSorted::Ascending);
647            }
648
649            RAYON.join(
650                // SAFETY: join indices are known to be in bounds
651                || unsafe { left_df.take_unchecked(a.idx().unwrap()) },
652                || unsafe { other.take_unchecked(b.idx().unwrap()) },
653            )
654        } else {
655            RAYON.join(
656                // SAFETY: join indices are known to be in bounds
657                || unsafe { left_df.take_unchecked(left.into_series().idx().unwrap()) },
658                || unsafe { other.take_unchecked(right.into_series().idx().unwrap()) },
659            )
660        };
661
662        _finish_join(df_left, df_right, args.suffix)
663    }
664}
665
666impl DataFrameJoinOps for DataFrame {}
667impl DataFrameJoinOpsPrivate for DataFrame {}
668
669fn prepare_keys_multiple(s: &[Series], nulls_equal: bool) -> PolarsResult<BinaryOffsetChunked> {
670    let keys = s
671        .iter()
672        .map(|s| {
673            let phys = s.to_physical_repr();
674            match phys.dtype() {
675                #[cfg(feature = "dtype-f16")]
676                DataType::Float16 => phys.f16().unwrap().to_canonical().into_column(),
677                DataType::Float32 => phys.f32().unwrap().to_canonical().into_column(),
678                DataType::Float64 => phys.f64().unwrap().to_canonical().into_column(),
679                _ => phys.into_owned().into_column(),
680            }
681        })
682        .collect::<Vec<_>>();
683
684    if nulls_equal {
685        encode_rows_vertical_par_unordered(&keys)
686    } else {
687        encode_rows_vertical_par_unordered_broadcast_nulls(&keys)
688    }
689}
690
691// Duplicate column names are allowed
692pub fn private_left_join_multiple_keys(
693    a: &[Column],
694    b: &[Column],
695    nulls_equal: bool,
696) -> PolarsResult<LeftJoinIds> {
697    // @scalar-opt
698    let a_cols = a
699        .iter()
700        .map(|c| c.as_materialized_series().clone())
701        .collect::<Vec<_>>();
702    let b_cols = b
703        .iter()
704        .map(|c| c.as_materialized_series().clone())
705        .collect::<Vec<_>>();
706
707    let a = prepare_keys_multiple(&a_cols, nulls_equal)?.into_series();
708    let b = prepare_keys_multiple(&b_cols, nulls_equal)?.into_series();
709    sort_or_hash_left(&a, &b, false, JoinValidation::ManyToMany, nulls_equal)
710}