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
53pub(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 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 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 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 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 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 let a = Series::full_null("".into(), 0, &DataType::Null);
376 (a.clone(), a)
377 } else {
378 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 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 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 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 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 || unsafe { left_df.take_unchecked(a.idx().unwrap()) },
651 || unsafe { other.take_unchecked(b.idx().unwrap()) },
652 )
653 } else {
654 RAYON.join(
655 || 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
668fn 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
708pub fn private_left_join_multiple_keys(
712 a: &[Column],
713 b: &[Column],
714 nulls_equal: bool,
715) -> PolarsResult<LeftJoinIds> {
716 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}