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;
32use polars_core::chunked_array::ops::row_encode::{
33    encode_rows_with_layout_par, get_binary_offset_row_encoding_layout,
34    get_optimal_row_encoding_layout,
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;
41#[allow(unused_imports)]
42use polars_core::utils::slice_slice;
43use polars_defs::join::{
44    JoinArgs, JoinCoalesce, JoinType, JoinTypeOptions, JoinValidation, MaintainOrderJoin,
45};
46use polars_utils::hashing::BytesHash;
47use polars_utils::slice::slice_offsets;
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,
168            );
169        }
170        #[cfg(feature = "cross_join")]
171        if let JoinType::Cross = args.how {
172            return left_df.cross_join(other, args.suffix.clone(), args.slice, args.maintain_order);
173        }
174
175        // Clear literals if a frame is empty. Otherwise we could get an oob
176        fn clear(s: &mut [Series]) {
177            for s in s.iter_mut() {
178                if s.len() == 1 {
179                    *s = s.clear()
180                }
181            }
182        }
183        if left_df.height() == 0 {
184            clear(&mut selected_left);
185        }
186        if other.height() == 0 {
187            clear(&mut selected_right);
188        }
189
190        let should_coalesce = args.should_coalesce();
191        assert_eq!(selected_left.len(), selected_right.len());
192
193        #[cfg(feature = "chunked_ids")]
194        {
195            // a left join create chunked-ids
196            // the others not yet.
197            // TODO! change this to other join types once they support chunked-id joins
198            if _check_rechunk
199                && !(matches!(args.how, JoinType::Left)
200                    || std::env::var("POLARS_NO_CHUNKED_JOIN").is_ok())
201            {
202                let mut left = Cow::Borrowed(left_df);
203                let mut right = Cow::Borrowed(other);
204                if left_df.should_rechunk() {
205                    if _verbose {
206                        eprintln!(
207                            "{:?} join triggered a rechunk of the left DataFrame: {} columns are affected",
208                            args.how,
209                            left_df.width()
210                        );
211                    }
212
213                    let mut tmp_left = left_df.clone();
214                    tmp_left.rechunk_mut_par();
215                    left = Cow::Owned(tmp_left);
216                }
217                if other.should_rechunk() {
218                    if _verbose {
219                        eprintln!(
220                            "{:?} join triggered a rechunk of the right DataFrame: {} columns are affected",
221                            args.how,
222                            other.width()
223                        );
224                    }
225                    let mut tmp_right = other.clone();
226                    tmp_right.rechunk_mut_par();
227                    right = Cow::Owned(tmp_right);
228                }
229                return left._join_impl(
230                    &right,
231                    selected_left,
232                    selected_right,
233                    args,
234                    options,
235                    false,
236                    _verbose,
237                );
238            }
239        }
240
241        if let Some((l, r)) = selected_left
242            .iter()
243            .zip(&selected_right)
244            .find(|(l, r)| l.dtype() != r.dtype())
245        {
246            polars_bail!(
247                ComputeError:
248                    "datatypes of join keys don't match - `{}`: {} on left does not match `{}`: {} on right",
249                    l.name(), l.dtype().pretty_format(), r.name(), r.dtype().pretty_format()
250            );
251        };
252
253        #[cfg(feature = "iejoin")]
254        if let Some(JoinTypeOptions::IEJoin(ie_options)) = options {
255            let func = if RAYON.current_num_threads() > 1
256                && !left_df.shape_has_zero()
257                && !other.shape_has_zero()
258            {
259                iejoin::iejoin_par
260            } else {
261                iejoin::iejoin
262            };
263            let emit_unmatched =
264                if args.how.emits_unmatched_left() && args.how.emits_unmatched_right() {
265                    iejoin::EmitUnmatched::Both
266                } else if args.how.emits_unmatched_left() {
267                    iejoin::EmitUnmatched::Left
268                } else if args.how.emits_unmatched_right() {
269                    iejoin::EmitUnmatched::Right
270                } else {
271                    iejoin::EmitUnmatched::None
272                };
273            return func(
274                left_df,
275                other,
276                selected_left,
277                selected_right,
278                &ie_options,
279                args.suffix,
280                args.slice,
281                emit_unmatched,
282            );
283        }
284
285        // Single keys.
286        if selected_left.len() == 1 {
287            let s_left = &selected_left[0];
288            let s_right = &selected_right[0];
289            let drop_names: Option<Vec<PlSmallStr>> =
290                if should_coalesce { None } else { Some(vec![]) };
291            return match args.how {
292                JoinType::Inner => left_df
293                    ._inner_join_from_series(other, s_left, s_right, args, _verbose, drop_names),
294                JoinType::Left => dispatch_left_right::left_join_from_series(
295                    self.to_df().clone(),
296                    other,
297                    s_left,
298                    s_right,
299                    args,
300                    _verbose,
301                    drop_names,
302                ),
303                JoinType::Right => dispatch_left_right::right_join_from_series(
304                    self.to_df(),
305                    other.clone(),
306                    s_left,
307                    s_right,
308                    args,
309                    _verbose,
310                    drop_names,
311                ),
312                JoinType::Full => left_df._full_join_from_series(other, s_left, s_right, args),
313                #[cfg(feature = "semi_anti_join")]
314                JoinType::Anti => left_df._semi_anti_join_from_series(
315                    s_left,
316                    s_right,
317                    args.slice,
318                    true,
319                    args.nulls_equal,
320                ),
321                #[cfg(feature = "semi_anti_join")]
322                JoinType::Semi => left_df._semi_anti_join_from_series(
323                    s_left,
324                    s_right,
325                    args.slice,
326                    false,
327                    args.nulls_equal,
328                ),
329                #[cfg(feature = "asof_join")]
330                JoinType::AsOf(options) => match (options.left_by, options.right_by) {
331                    (Some(left_by), Some(right_by)) => left_df._join_asof_by(
332                        other,
333                        s_left,
334                        s_right,
335                        left_by,
336                        right_by,
337                        options.strategy,
338                        options.tolerance.map(|v| v.into_value()),
339                        args.suffix.clone(),
340                        args.slice,
341                        should_coalesce,
342                        options.allow_eq,
343                        options.check_sortedness,
344                    ),
345                    (None, None) => left_df._join_asof(
346                        other,
347                        s_left,
348                        s_right,
349                        options.strategy,
350                        options.tolerance.map(|v| v.into_value()),
351                        args.suffix,
352                        args.slice,
353                        should_coalesce,
354                        options.allow_eq,
355                        options.check_sortedness,
356                    ),
357                    _ => {
358                        panic!("expected by arguments on both sides")
359                    },
360                },
361                #[cfg(feature = "iejoin")]
362                JoinType::IEJoin | JoinType::Range => {
363                    unreachable!()
364                },
365                JoinType::Cross => {
366                    unreachable!()
367                },
368            };
369        }
370        let (lhs_keys, rhs_keys) = if (left_df.height() == 0 || other.height() == 0)
371            && matches!(&args.how, JoinType::Inner)
372        {
373            // Fast path for empty inner joins.
374            // Return 2 dummies so that we don't row-encode.
375            let a = Series::full_null("".into(), 0, &DataType::Null);
376            (a.clone(), a)
377        } else {
378            // Row encode the keys.
379            prepare_keys_multiple(&selected_left, &selected_right, args.nulls_equal)?
380        };
381
382        let drop_names = if should_coalesce {
383            if args.how == JoinType::Right {
384                selected_left
385                    .iter()
386                    .map(|s| s.name().clone())
387                    .collect::<Vec<_>>()
388            } else {
389                selected_right
390                    .iter()
391                    .map(|s| s.name().clone())
392                    .collect::<Vec<_>>()
393            }
394        } else {
395            vec![]
396        };
397
398        // Multiple keys.
399        match args.how {
400            #[cfg(feature = "asof_join")]
401            JoinType::AsOf(_) => polars_bail!(
402                ComputeError: "asof join not supported for join on multiple keys"
403            ),
404            #[cfg(feature = "iejoin")]
405            JoinType::IEJoin | JoinType::Range => {
406                unreachable!()
407            },
408            JoinType::Cross => {
409                unreachable!()
410            },
411            JoinType::Full => {
412                let names_left = selected_left
413                    .iter()
414                    .map(|s| s.name().clone())
415                    .collect::<Vec<_>>();
416                args.coalesce = JoinCoalesce::KeepColumns;
417                let suffix = args.suffix.clone();
418                let out = left_df._full_join_from_series(other, &lhs_keys, &rhs_keys, args);
419
420                if should_coalesce {
421                    Ok(_coalesce_full_join(
422                        out?,
423                        names_left.as_slice(),
424                        drop_names.as_slice(),
425                        suffix,
426                        left_df,
427                    ))
428                } else {
429                    out
430                }
431            },
432            JoinType::Inner => left_df._inner_join_from_series(
433                other,
434                &lhs_keys,
435                &rhs_keys,
436                args,
437                _verbose,
438                Some(drop_names),
439            ),
440            JoinType::Left => dispatch_left_right::left_join_from_series(
441                left_df.clone(),
442                other,
443                &lhs_keys,
444                &rhs_keys,
445                args,
446                _verbose,
447                Some(drop_names),
448            ),
449            JoinType::Right => dispatch_left_right::right_join_from_series(
450                left_df,
451                other.clone(),
452                &lhs_keys,
453                &rhs_keys,
454                args,
455                _verbose,
456                Some(drop_names),
457            ),
458            #[cfg(feature = "semi_anti_join")]
459            JoinType::Anti | JoinType::Semi => self._join_impl(
460                other,
461                vec![lhs_keys],
462                vec![rhs_keys],
463                args,
464                options,
465                _check_rechunk,
466                _verbose,
467            ),
468        }
469    }
470
471    /// Perform an inner join on two DataFrames.
472    ///
473    /// # Example
474    ///
475    /// ```
476    /// # use polars_core::prelude::*;
477    /// # use polars_ops::prelude::*;
478    /// fn join_dfs(left: &DataFrame, right: &DataFrame) -> PolarsResult<DataFrame> {
479    ///     left.inner_join(right, ["join_column_left"], ["join_column_right"])
480    /// }
481    /// ```
482    fn inner_join(
483        &self,
484        other: &DataFrame,
485        left_on: impl IntoIterator<Item = impl AsRef<str>>,
486        right_on: impl IntoIterator<Item = impl AsRef<str>>,
487    ) -> PolarsResult<DataFrame> {
488        self.join(
489            other,
490            left_on,
491            right_on,
492            JoinArgs::new(JoinType::Inner),
493            None,
494        )
495    }
496
497    /// Perform a left outer join on two DataFrames
498    /// # Example
499    ///
500    /// ```no_run
501    /// # use polars_core::prelude::*;
502    /// # use polars_ops::prelude::*;
503    /// let df1: DataFrame = df!("Wavelength (nm)" => &[480.0, 650.0, 577.0, 1201.0, 100.0])?;
504    /// let df2: DataFrame = df!("Color" => &["Blue", "Yellow", "Red"],
505    ///                          "Wavelength nm" => &[480.0, 577.0, 650.0])?;
506    ///
507    /// let df3: DataFrame = df1.left_join(&df2, ["Wavelength (nm)"], ["Wavelength nm"])?;
508    /// println!("{:?}", df3);
509    /// # Ok::<(), PolarsError>(())
510    /// ```
511    ///
512    /// Output:
513    ///
514    /// ```text
515    /// shape: (5, 2)
516    /// +-----------------+--------+
517    /// | Wavelength (nm) | Color  |
518    /// | ---             | ---    |
519    /// | f64             | str    |
520    /// +=================+========+
521    /// | 480             | Blue   |
522    /// +-----------------+--------+
523    /// | 650             | Red    |
524    /// +-----------------+--------+
525    /// | 577             | Yellow |
526    /// +-----------------+--------+
527    /// | 1201            | null   |
528    /// +-----------------+--------+
529    /// | 100             | null   |
530    /// +-----------------+--------+
531    /// ```
532    fn left_join(
533        &self,
534        other: &DataFrame,
535        left_on: impl IntoIterator<Item = impl AsRef<str>>,
536        right_on: impl IntoIterator<Item = impl AsRef<str>>,
537    ) -> PolarsResult<DataFrame> {
538        self.join(
539            other,
540            left_on,
541            right_on,
542            JoinArgs::new(JoinType::Left),
543            None,
544        )
545    }
546
547    /// Perform a full outer join on two DataFrames
548    /// # Example
549    ///
550    /// ```
551    /// # use polars_core::prelude::*;
552    /// # use polars_ops::prelude::*;
553    /// fn join_dfs(left: &DataFrame, right: &DataFrame) -> PolarsResult<DataFrame> {
554    ///     left.full_join(right, ["join_column_left"], ["join_column_right"])
555    /// }
556    /// ```
557    fn full_join(
558        &self,
559        other: &DataFrame,
560        left_on: impl IntoIterator<Item = impl AsRef<str>>,
561        right_on: impl IntoIterator<Item = impl AsRef<str>>,
562    ) -> PolarsResult<DataFrame> {
563        self.join(
564            other,
565            left_on,
566            right_on,
567            JoinArgs::new(JoinType::Full),
568            None,
569        )
570    }
571}
572
573trait DataFrameJoinOpsPrivate: IntoDf {
574    fn _inner_join_from_series(
575        &self,
576        other: &DataFrame,
577        s_left: &Series,
578        s_right: &Series,
579        args: JoinArgs,
580        verbose: bool,
581        drop_names: Option<Vec<PlSmallStr>>,
582    ) -> PolarsResult<DataFrame> {
583        let left_df = self.to_df();
584        let ((join_tuples_left, join_tuples_right), sorted) =
585            _sort_or_hash_inner(s_left, s_right, verbose, args.validation, args.nulls_equal)?;
586
587        let mut join_tuples_left = &*join_tuples_left;
588        let mut join_tuples_right = &*join_tuples_right;
589
590        let already_left_sorted = sorted
591            && matches!(
592                args.maintain_order,
593                MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight
594            );
595        let need_sort = args.maintain_order != MaintainOrderJoin::None && !already_left_sorted;
596
597        if !need_sort && let Some((offset, len)) = args.slice {
598            join_tuples_left = slice_slice(join_tuples_left, offset, len);
599            join_tuples_right = slice_slice(join_tuples_right, offset, len);
600        }
601
602        let other = if let Some(drop_names) = drop_names {
603            other.drop_many(drop_names)
604        } else {
605            other.drop(s_right.name()).unwrap()
606        };
607
608        let mut left = unsafe { IdxCa::mmap_slice("a".into(), join_tuples_left) };
609        if sorted {
610            left.set_sorted_flag(IsSorted::Ascending);
611        }
612        let right = unsafe { IdxCa::mmap_slice("b".into(), join_tuples_right) };
613
614        try_raise_polars_abort();
615
616        let (df_left, df_right) = if need_sort {
617            let mut df = unsafe {
618                DataFrame::new_unchecked_infer_height(vec![
619                    left.into_series().into(),
620                    right.into_series().into(),
621                ])
622            };
623
624            let columns = match args.maintain_order {
625                MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight => vec!["a"],
626                MaintainOrderJoin::Right | MaintainOrderJoin::RightLeft => vec!["b"],
627                _ => unreachable!(),
628            };
629
630            let options = SortMultipleOptions::new()
631                .with_order_descending(false)
632                .with_maintain_order(true);
633
634            df.sort_in_place(columns, options)?;
635
636            if let Some((offset, len)) = args.slice {
637                df = df.slice(offset, len);
638            }
639
640            let [mut a, b]: [Column; 2] = df.into_columns().try_into().unwrap();
641            if matches!(
642                args.maintain_order,
643                MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight
644            ) {
645                a.set_sorted_flag(IsSorted::Ascending);
646            }
647
648            RAYON.join(
649                // SAFETY: join indices are known to be in bounds
650                || unsafe { left_df.take_unchecked(a.idx().unwrap()) },
651                || unsafe { other.take_unchecked(b.idx().unwrap()) },
652            )
653        } else {
654            RAYON.join(
655                // SAFETY: join indices are known to be in bounds
656                || unsafe { left_df.take_unchecked(left.into_series().idx().unwrap()) },
657                || unsafe { other.take_unchecked(right.into_series().idx().unwrap()) },
658            )
659        };
660
661        _finish_join(df_left, df_right, args.suffix)
662    }
663}
664
665impl DataFrameJoinOps for DataFrame {}
666impl DataFrameJoinOpsPrivate for DataFrame {}
667
668/// Row encodes the keys of both sides.
669fn prepare_keys_multiple(
670    left: &[Series],
671    right: &[Series],
672    nulls_equal: bool,
673) -> PolarsResult<(Series, Series)> {
674    let to_keys = |s: &[Series]| {
675        s.iter()
676            .map(|s| {
677                let phys = s.to_physical_repr();
678                match phys.dtype() {
679                    #[cfg(feature = "dtype-f16")]
680                    DataType::Float16 => phys.f16().unwrap().to_canonical().into_column(),
681                    DataType::Float32 => phys.f32().unwrap().to_canonical().into_column(),
682                    DataType::Float64 => phys.f64().unwrap().to_canonical().into_column(),
683                    _ => phys.into_owned().into_column(),
684                }
685            })
686            .collect::<Vec<_>>()
687    };
688    let (left, right) = (to_keys(left), to_keys(right));
689
690    let opts = vec![RowEncodingOptions::new_unsorted(); left.len()];
691    let same_dtypes = left.iter().zip(&right).all(|(l, r)| l.dtype() == r.dtype());
692    let propagate_nulls = !nulls_equal;
693    let (left_layout, right_layout) = if same_dtypes {
694        let layout = get_optimal_row_encoding_layout(&left, &[&right], &opts);
695        (layout.clone(), layout)
696    } else {
697        (
698            get_binary_offset_row_encoding_layout(&left, &opts),
699            get_binary_offset_row_encoding_layout(&right, &opts),
700        )
701    };
702    Ok((
703        encode_rows_with_layout_par(&left, &left_layout, propagate_nulls)?,
704        encode_rows_with_layout_par(&right, &right_layout, propagate_nulls)?,
705    ))
706}
707
708/// The returned right ids are plain row indices into `b`.
709///
710/// Duplicate column names are allowed.
711pub fn private_left_join_multiple_keys(
712    a: &[Column],
713    b: &[Column],
714    nulls_equal: bool,
715) -> PolarsResult<LeftJoinIds> {
716    // @scalar-opt
717    let a_cols = a
718        .iter()
719        .map(|c| c.as_materialized_series().clone())
720        .collect::<Vec<_>>();
721    let b_cols = b
722        .iter()
723        .map(|c| c.as_materialized_series().clone())
724        .collect::<Vec<_>>();
725
726    let (a, b) = prepare_keys_multiple(&a_cols, &b_cols, nulls_equal)?;
727    sort_or_hash_left(
728        &a,
729        &b.rechunk(),
730        false,
731        JoinValidation::ManyToMany,
732        nulls_equal,
733    )
734}