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
54pub(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 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 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 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 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 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 let a = Series::full_null("".into(), 0, &DataType::Null);
368 (a.clone(), a)
369 } else {
370 (
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 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 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 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 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 || unsafe { left_df.take_unchecked(a.idx().unwrap()) },
646 || unsafe { other.take_unchecked(b.idx().unwrap()) },
647 )
648 } else {
649 RAYON.join(
650 || 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
685pub fn private_left_join_multiple_keys(
687 a: &[Column],
688 b: &[Column],
689 nulls_equal: bool,
690) -> PolarsResult<LeftJoinIds> {
691 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}