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