1use std::sync::atomic::{AtomicU64, Ordering};
2
3use polars_core::utils::{
4 _set_partition_size, CustomIterTools, NoNull, accumulate_dataframes_vertical_unchecked,
5 concat_df_unchecked, par_iter_bounded, split,
6};
7use polars_defs::join::{CrossJoinOptions, JoinType, MaintainOrderJoin};
8use polars_utils::pl_str::PlSmallStr;
9
10use super::*;
11
12fn slice_take(
13 total_rows: IdxSize,
14 n_rows_right: IdxSize,
15 slice: Option<(i64, usize)>,
16 inner: fn(IdxSize, IdxSize, IdxSize) -> IdxCa,
17) -> IdxCa {
18 match slice {
19 None => inner(0, total_rows, n_rows_right),
20 Some((offset, len)) => {
21 let (offset, len) = slice_offsets(offset, len, total_rows as usize);
22 inner(offset as IdxSize, (len + offset) as IdxSize, n_rows_right)
23 },
24 }
25}
26
27fn take_left(total_rows: IdxSize, n_rows_right: IdxSize, slice: Option<(i64, usize)>) -> IdxCa {
28 fn inner(offset: IdxSize, total_rows: IdxSize, n_rows_right: IdxSize) -> IdxCa {
29 let mut take: NoNull<IdxCa> = (offset..total_rows)
30 .map(|i| i / n_rows_right)
31 .collect_trusted();
32 take.set_sorted_flag(IsSorted::Ascending);
33 take.into_inner()
34 }
35 slice_take(total_rows, n_rows_right, slice, inner)
36}
37
38fn take_right(total_rows: IdxSize, n_rows_right: IdxSize, slice: Option<(i64, usize)>) -> IdxCa {
39 fn inner(offset: IdxSize, total_rows: IdxSize, n_rows_right: IdxSize) -> IdxCa {
40 let take: NoNull<IdxCa> = (offset..total_rows)
41 .map(|i| i % n_rows_right)
42 .collect_trusted();
43 take.into_inner()
44 }
45 slice_take(total_rows, n_rows_right, slice, inner)
46}
47
48pub trait CrossJoin: IntoDf {
49 fn cross_join(
51 &self,
52 other: &DataFrame,
53 suffix: Option<PlSmallStr>,
54 slice: Option<(i64, usize)>,
55 maintain_order: MaintainOrderJoin,
56 ) -> PolarsResult<DataFrame> {
57 let (l_df, r_df) = cross_join_dfs(self.to_df(), other, slice, true, maintain_order)?;
58
59 _finish_join(l_df, r_df, suffix)
60 }
61}
62
63impl CrossJoin for DataFrame {}
64
65fn cross_join_dfs<'a>(
66 mut df_self: &'a DataFrame,
67 mut other: &'a DataFrame,
68 slice: Option<(i64, usize)>,
69 parallel: bool,
70 maintain_order: MaintainOrderJoin,
71) -> PolarsResult<(DataFrame, DataFrame)> {
72 if df_self.height() == 0 || other.height() == 0 {
73 return Ok((df_self.clear(), other.clear()));
74 }
75
76 let left_is_primary = match maintain_order {
77 MaintainOrderJoin::None => true,
78 MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight => true,
79 MaintainOrderJoin::Right | MaintainOrderJoin::RightLeft => false,
80 };
81
82 if !left_is_primary {
83 core::mem::swap(&mut df_self, &mut other);
84 }
85
86 let n_rows_left = df_self.height() as IdxSize;
87 let n_rows_right = other.height() as IdxSize;
88 let Some(total_rows) = n_rows_left.checked_mul(n_rows_right) else {
89 polars_bail!(
90 ComputeError: "cross joins would produce more rows than fits into 2^32; \
91 consider compiling with polars-big-idx feature, or set 'streaming'"
92 );
93 };
94
95 let create_left_df = || {
104 unsafe {
107 df_self.take_unchecked_impl(&take_left(total_rows, n_rows_right, slice), parallel)
108 }
109 };
110
111 let create_right_df = || {
112 if n_rows_left > 100 || slice.is_some() {
116 unsafe {
119 other.take_unchecked_impl(&take_right(total_rows, n_rows_right, slice), parallel)
120 }
121 } else {
122 let iter = (0..n_rows_left).map(|_| other);
123 concat_df_unchecked(iter)
124 }
125 };
126 let (l_df, r_df) = if parallel {
127 try_raise_polars_abort();
128 RAYON.install(|| rayon::join(create_left_df, create_right_df))
129 } else {
130 (create_left_df(), create_right_df())
131 };
132 if left_is_primary {
133 Ok((l_df, r_df))
134 } else {
135 Ok((r_df, l_df))
136 }
137}
138
139pub(super) fn fused_cross_filter(
140 left: &DataFrame,
141 right: &DataFrame,
142 suffix: Option<PlSmallStr>,
143 cross_join_options: &CrossJoinOptions,
144 maintain_order: MaintainOrderJoin,
145 how: &JoinType,
146) -> PolarsResult<DataFrame> {
147 let emit_unmatched_left = how.emits_unmatched_left();
148 let emit_unmatched_right = how.emits_unmatched_right();
149 let unfiltered_size = (left.height() as u64).saturating_mul(right.height() as u64);
150 let chunk_size = (unfiltered_size / _set_partition_size() as u64).clamp(1, 100_000);
151 let num_chunks = (unfiltered_size / chunk_size).max(1) as usize;
152
153 let left_is_primary = match maintain_order {
154 MaintainOrderJoin::None => true,
155 MaintainOrderJoin::Left | MaintainOrderJoin::LeftRight => true,
156 MaintainOrderJoin::Right | MaintainOrderJoin::RightLeft => false,
157 };
158 polars_ensure!(
161 !(emit_unmatched_left || emit_unmatched_right) || left_is_primary,
162 InvalidOperation: "'maintain_order={:?}' is not supported for `join_where` with 'how'={}",
163 maintain_order,
164 how
165 );
166
167 let split_chunks;
168 let cartesian_prod = if left_is_primary {
169 split_chunks = split(left, num_chunks);
170 split_chunks.iter().map(|l| (l, right)).collect::<Vec<_>>()
171 } else {
172 split_chunks = split(right, num_chunks);
173 split_chunks.iter().map(|r| (left, r)).collect::<Vec<_>>()
174 };
175
176 let names = _finish_join(left.clear(), right.clear(), suffix)?;
177 let rename_names = names.get_column_names();
178 let rename_names = &rename_names[left.width()..];
179 let len_right = right.height();
180 let right_matched: Vec<AtomicU64> = if emit_unmatched_right {
182 (0..len_right.div_ceil(64))
183 .map(|_| AtomicU64::new(0))
184 .collect()
185 } else {
186 Vec::new()
187 };
188
189 let mut dfs = RAYON.install(|| {
190 par_iter_bounded(&cartesian_prod)
191 .map(|(left_chunk, right_chunk)| {
192 let (mut joined, right_taken) =
193 cross_join_dfs(left_chunk, right_chunk, None, false, maintain_order)?;
194 let mut right_columns = right_taken.into_columns();
195
196 for (c, name) in right_columns.iter_mut().zip(rename_names) {
197 c.rename((*name).clone());
198 }
199
200 unsafe { joined.hstack_mut_unchecked(&right_columns) };
201
202 if !emit_unmatched_left && !emit_unmatched_right {
203 cross_join_options.predicate.apply(joined, false)
204 } else {
205 let mask = cross_join_options
206 .predicate
207 .evaluate(&joined)?
208 .broadcast_owned_to(joined.height())?;
209
210 let len_left = left_chunk.height();
211 debug_assert_eq!(joined.height(), len_left * len_right);
212
213 let mask_arr = mask.rechunk();
216 let mask_arr = mask_arr.downcast_get(0).unwrap();
217 let match_bits = match mask_arr.validity() {
218 Some(validity) => mask_arr.values() & validity,
219 None => mask_arr.values().clone(),
220 };
221
222 let capacity = match_bits.set_bits() + len_left;
226 let mut left_idx: Vec<IdxSize> = Vec::with_capacity(capacity);
227 let mut right_idx: Vec<NullableIdxSize> = Vec::with_capacity(capacity);
228 for i in 0..len_left {
229 let run = match_bits.clone().sliced(i * len_right, len_right);
230 if run.unset_bits() == len_right {
231 if emit_unmatched_left {
232 left_idx.push(i as IdxSize);
233 right_idx.push(NullableIdxSize::null());
234 }
235 } else {
236 for j in run.true_idx_iter() {
237 left_idx.push(i as IdxSize);
238 right_idx.push(NullableIdxSize::from(j as IdxSize));
239 if emit_unmatched_right {
240 let (word, bit) = (&right_matched[j / 64], 1 << (j % 64));
241 if word.load(Ordering::Relaxed) & bit == 0 {
242 word.fetch_or(bit, Ordering::Relaxed);
243 }
244 }
245 }
246 }
247 }
248
249 let idx = unsafe { IdxCa::mmap_slice(PlSmallStr::EMPTY, &left_idx) };
250 let mut out = unsafe { left_chunk.take_unchecked(&idx) };
251 let mut right_columns = unsafe {
252 IdxCa::with_nullable_idx(&right_idx, |idx| right.take_unchecked(idx))
253 }
254 .into_columns();
255 for (c, name) in right_columns.iter_mut().zip(rename_names) {
256 c.rename((*name).clone());
257 }
258 unsafe { out.hstack_mut_unchecked(&right_columns) };
259
260 Ok(out)
261 }
262 })
263 .collect::<PolarsResult<Vec<_>>>()
264 })?;
265
266 if emit_unmatched_right {
267 let unmatched: Vec<IdxSize> = (0..len_right)
269 .filter(|&j| right_matched[j / 64].load(Ordering::Relaxed) & (1 << (j % 64)) == 0)
270 .map(|j| j as IdxSize)
271 .collect();
272 if !unmatched.is_empty() {
273 let mut out = DataFrame::full_null(left.schema(), unmatched.len());
274 let mut right_columns = if unmatched.len() == len_right {
275 right.clone()
276 } else {
277 let idx = unsafe { IdxCa::mmap_slice(PlSmallStr::EMPTY, &unmatched) };
278 unsafe { right.take_unchecked(&idx) }
279 }
280 .into_columns();
281 for (c, name) in right_columns.iter_mut().zip(rename_names) {
282 c.rename((*name).clone());
283 }
284 unsafe { out.hstack_mut_unchecked(&right_columns) };
285 dfs.push(out);
286 }
287 }
288
289 Ok(accumulate_dataframes_vertical_unchecked(dfs))
290}