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
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.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 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 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 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 let a = Series::full_null("".into(), 0, &DataType::Null);
374 (a.clone(), a)
375 } else {
376 (
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 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 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 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 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 || unsafe { left_df.take_unchecked(a.idx().unwrap()) },
652 || unsafe { other.take_unchecked(b.idx().unwrap()) },
653 )
654 } else {
655 RAYON.join(
656 || 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
691pub fn private_left_join_multiple_keys(
693 a: &[Column],
694 b: &[Column],
695 nulls_equal: bool,
696) -> PolarsResult<LeftJoinIds> {
697 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}