1use super::*;
2
3pub(super) type JoinIds = Vec<IdxSize>;
4pub type LeftJoinIds = (ChunkJoinIds, ChunkJoinOptIds);
5pub type InnerJoinIds = (JoinIds, JoinIds);
6
7#[cfg(feature = "chunked_ids")]
8pub(super) type ChunkJoinIds = Either<Vec<IdxSize>, Vec<ChunkId>>;
9#[cfg(feature = "chunked_ids")]
10pub type ChunkJoinOptIds = Either<Vec<NullableIdxSize>, Vec<ChunkId>>;
11
12#[cfg(not(feature = "chunked_ids"))]
13pub type ChunkJoinOptIds = Vec<NullableIdxSize>;
14
15#[cfg(not(feature = "chunked_ids"))]
16pub type ChunkJoinIds = Vec<IdxSize>;
17
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20use strum_macros::IntoStaticStr;
21
22#[derive(Clone, PartialEq, Debug, Hash)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
27pub enum JoinBuildSide {
28 PreferLeft,
31 ForceLeft,
33
34 PreferRight,
36 ForceRight,
37}
38
39#[derive(Clone, PartialEq, Debug, Hash, Default)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
42pub struct JoinArgs {
43 pub how: JoinType,
44 pub validation: JoinValidation,
45 pub suffix: Option<PlSmallStr>,
46 pub slice: Option<(i64, usize)>,
47 pub nulls_equal: bool,
48 pub coalesce: JoinCoalesce,
49 pub maintain_order: MaintainOrderJoin,
50 pub build_side: Option<JoinBuildSide>,
51}
52
53impl JoinArgs {
54 pub fn should_coalesce(&self) -> bool {
55 self.coalesce.coalesce(&self.how)
56 }
57}
58
59#[derive(Clone, PartialEq, Hash, Default, IntoStaticStr)]
60#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
62pub enum JoinType {
63 #[default]
64 Inner,
65 Left,
66 Right,
67 Full,
68 #[cfg(feature = "asof_join")]
70 AsOf(Box<AsOfOptions>),
71 #[cfg(feature = "semi_anti_join")]
72 Semi,
73 #[cfg(feature = "semi_anti_join")]
74 Anti,
75 #[cfg(feature = "iejoin")]
76 IEJoin,
79 #[cfg(feature = "iejoin")]
80 Range,
83 Cross,
85}
86
87#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Default, IntoStaticStr)]
88#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
89#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
90pub enum JoinCoalesce {
91 #[default]
92 JoinSpecific,
93 CoalesceColumns,
94 KeepColumns,
95}
96
97impl JoinCoalesce {
98 pub fn coalesce(&self, join_type: &JoinType) -> bool {
99 use JoinCoalesce::*;
100 use JoinType::*;
101 match join_type {
102 Left | Inner | Right => {
103 matches!(self, JoinSpecific | CoalesceColumns)
104 },
105 Full => {
106 matches!(self, CoalesceColumns)
107 },
108 #[cfg(feature = "asof_join")]
109 AsOf(_) => matches!(self, JoinSpecific | CoalesceColumns),
110 #[cfg(feature = "iejoin")]
111 IEJoin | Range => false,
112 Cross => false,
113 #[cfg(feature = "semi_anti_join")]
114 Semi | Anti => false,
115 }
116 }
117}
118
119#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Default, IntoStaticStr)]
120#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
121#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
122#[strum(serialize_all = "snake_case")]
123pub enum MaintainOrderJoin {
124 #[default]
125 None,
126 Left,
127 Right,
128 LeftRight,
129 RightLeft,
130}
131
132impl MaintainOrderJoin {
133 pub(super) fn flip(&self) -> Self {
134 match self {
135 MaintainOrderJoin::None => MaintainOrderJoin::None,
136 MaintainOrderJoin::Left => MaintainOrderJoin::Right,
137 MaintainOrderJoin::Right => MaintainOrderJoin::Left,
138 MaintainOrderJoin::LeftRight => MaintainOrderJoin::RightLeft,
139 MaintainOrderJoin::RightLeft => MaintainOrderJoin::LeftRight,
140 }
141 }
142}
143
144impl JoinArgs {
145 pub fn new(how: JoinType) -> Self {
146 Self {
147 how,
148 validation: Default::default(),
149 suffix: None,
150 slice: None,
151 nulls_equal: false,
152 coalesce: Default::default(),
153 maintain_order: Default::default(),
154 build_side: None,
155 }
156 }
157
158 pub fn with_coalesce(mut self, coalesce: JoinCoalesce) -> Self {
159 self.coalesce = coalesce;
160 self
161 }
162
163 pub fn with_maintain_order(mut self, maintain_order: MaintainOrderJoin) -> Self {
164 self.maintain_order = maintain_order;
165 self
166 }
167
168 pub fn with_suffix(mut self, suffix: Option<PlSmallStr>) -> Self {
169 self.suffix = suffix;
170 self
171 }
172
173 pub fn with_build_side(mut self, build_side: Option<JoinBuildSide>) -> Self {
174 self.build_side = build_side;
175 self
176 }
177
178 pub fn suffix(&self) -> &PlSmallStr {
179 const DEFAULT: &PlSmallStr = &PlSmallStr::from_static("_right");
180 self.suffix.as_ref().unwrap_or(DEFAULT)
181 }
182}
183
184impl From<JoinType> for JoinArgs {
185 fn from(value: JoinType) -> Self {
186 JoinArgs::new(value)
187 }
188}
189
190pub trait CrossJoinFilter: Send + Sync {
191 fn evaluate(&self, df: &DataFrame) -> PolarsResult<BooleanChunked>;
193
194 fn apply(&self, df: DataFrame) -> PolarsResult<DataFrame> {
195 let mask = self.evaluate(&df)?;
196 df.filter_seq(&mask)
197 }
198}
199
200impl<T> CrossJoinFilter for T
201where
202 T: Fn(&DataFrame) -> PolarsResult<BooleanChunked> + Send + Sync,
203{
204 fn evaluate(&self, df: &DataFrame) -> PolarsResult<BooleanChunked> {
205 self(df)
206 }
207}
208
209#[derive(Clone)]
210pub struct CrossJoinOptions {
211 pub predicate: Arc<dyn CrossJoinFilter>,
212}
213
214impl CrossJoinOptions {
215 fn as_ptr_ref(&self) -> *const dyn CrossJoinFilter {
216 Arc::as_ptr(&self.predicate)
217 }
218}
219
220impl Eq for CrossJoinOptions {}
221
222impl PartialEq for CrossJoinOptions {
223 fn eq(&self, other: &Self) -> bool {
224 std::ptr::addr_eq(self.as_ptr_ref(), other.as_ptr_ref())
225 }
226}
227
228impl Hash for CrossJoinOptions {
229 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
230 self.as_ptr_ref().hash(state);
231 }
232}
233
234impl Debug for CrossJoinOptions {
235 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
236 write!(f, "CrossJoinOptions",)
237 }
238}
239
240#[derive(Clone, PartialEq, Eq, Hash, IntoStaticStr, Debug)]
241#[strum(serialize_all = "snake_case")]
242pub enum JoinTypeOptions {
243 #[cfg(feature = "iejoin")]
244 IEJoin(IEJoinOptions),
245 Cross(CrossJoinOptions),
246}
247
248impl JoinTypeOptions {
249 pub fn is_iejoin(&self) -> bool {
250 match self {
251 #[cfg(feature = "iejoin")]
252 Self::IEJoin(_) => true,
253 _ => false,
254 }
255 }
256}
257
258impl Display for JoinType {
259 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
260 use JoinType::*;
261 let val = match self {
262 Left => "LEFT",
263 Right => "RIGHT",
264 Inner => "INNER",
265 Full => "FULL",
266 #[cfg(feature = "asof_join")]
267 AsOf(_) => "ASOF",
268 #[cfg(feature = "iejoin")]
269 IEJoin => "IEJOIN",
270 #[cfg(feature = "iejoin")]
271 Range => "RANGE",
272 Cross => "CROSS",
273 #[cfg(feature = "semi_anti_join")]
274 Semi => "SEMI",
275 #[cfg(feature = "semi_anti_join")]
276 Anti => "ANTI",
277 };
278 write!(f, "{val}")
279 }
280}
281
282impl Debug for JoinType {
283 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
284 write!(f, "{self}")
285 }
286}
287
288impl JoinType {
289 pub fn is_equi(&self) -> bool {
290 matches!(
291 self,
292 JoinType::Inner | JoinType::Left | JoinType::Right | JoinType::Full
293 )
294 }
295
296 pub fn is_semi_anti(&self) -> bool {
297 #[cfg(feature = "semi_anti_join")]
298 {
299 matches!(self, JoinType::Semi | JoinType::Anti)
300 }
301 #[cfg(not(feature = "semi_anti_join"))]
302 {
303 false
304 }
305 }
306
307 pub fn is_semi(&self) -> bool {
308 #[cfg(feature = "semi_anti_join")]
309 {
310 matches!(self, JoinType::Semi)
311 }
312 #[cfg(not(feature = "semi_anti_join"))]
313 {
314 false
315 }
316 }
317
318 pub fn is_anti(&self) -> bool {
319 #[cfg(feature = "semi_anti_join")]
320 {
321 matches!(self, JoinType::Anti)
322 }
323 #[cfg(not(feature = "semi_anti_join"))]
324 {
325 false
326 }
327 }
328
329 pub fn is_asof(&self) -> bool {
330 #[cfg(feature = "asof_join")]
331 {
332 matches!(self, JoinType::AsOf(_))
333 }
334 #[cfg(not(feature = "asof_join"))]
335 {
336 false
337 }
338 }
339
340 pub fn is_inner(&self) -> bool {
341 matches!(self, JoinType::Inner)
342 }
343
344 pub fn is_cross(&self) -> bool {
345 matches!(self, JoinType::Cross)
346 }
347
348 pub fn is_ie(&self) -> bool {
349 #[cfg(feature = "iejoin")]
350 {
351 matches!(self, JoinType::IEJoin)
352 }
353 #[cfg(not(feature = "iejoin"))]
354 {
355 false
356 }
357 }
358
359 pub fn is_range(&self) -> bool {
360 #[cfg(feature = "iejoin")]
361 {
362 matches!(self, JoinType::Range)
363 }
364 #[cfg(not(feature = "iejoin"))]
365 {
366 false
367 }
368 }
369
370 pub fn emits_unmatched_left(&self) -> bool {
372 #[cfg(feature = "semi_anti_join")]
373 {
374 matches!(self, JoinType::Left | JoinType::Full | JoinType::Anti)
375 }
376 #[cfg(not(feature = "semi_anti_join"))]
377 {
378 matches!(self, JoinType::Left | JoinType::Full)
379 }
380 }
381
382 pub fn emits_unmatched_right(&self) -> bool {
384 matches!(self, JoinType::Right | JoinType::Full)
385 }
386
387 pub fn supports_non_equi(&self) -> bool {
389 matches!(self, JoinType::Inner | JoinType::Left | JoinType::Right)
390 }
391
392 pub fn supports_non_equi_options(&self, options: &Option<JoinTypeOptions>) -> bool {
395 options.is_none()
396 || matches!(self, JoinType::Inner | JoinType::Cross)
397 || self.is_ie()
398 || self.is_range()
399 || (matches!(self, JoinType::Left | JoinType::Right)
400 && options.as_ref().map(|o| o.is_iejoin()).unwrap_or(false))
401 || (matches!(self, JoinType::Left)
402 && matches!(options, Some(JoinTypeOptions::Cross(_))))
403 }
404}
405
406#[derive(Copy, Clone, PartialEq, Eq, Default, Hash, IntoStaticStr)]
407#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
408#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
409pub enum JoinValidation {
410 #[default]
412 ManyToMany,
413 ManyToOne,
415 OneToMany,
417 OneToOne,
419}
420
421impl JoinValidation {
422 pub fn needs_checks(&self) -> bool {
423 !matches!(self, JoinValidation::ManyToMany)
424 }
425
426 fn swap(self, swap: bool) -> Self {
427 use JoinValidation::*;
428 if swap {
429 match self {
430 ManyToMany => ManyToMany,
431 ManyToOne => OneToMany,
432 OneToMany => ManyToOne,
433 OneToOne => OneToOne,
434 }
435 } else {
436 self
437 }
438 }
439
440 pub fn is_valid_join(&self, join_type: &JoinType) -> PolarsResult<()> {
441 if !self.needs_checks() {
442 return Ok(());
443 }
444 polars_ensure!(matches!(join_type, JoinType::Inner | JoinType::Full | JoinType::Left),
445 ComputeError: "{self} validation on a {join_type} join is not supported");
446 Ok(())
447 }
448
449 pub(super) fn validate_probe(
450 &self,
451 s_left: &Series,
452 s_right: &Series,
453 build_shortest_table: bool,
454 nulls_equal: bool,
455 ) -> PolarsResult<()> {
456 let should_swap = build_shortest_table && s_left.len() <= s_right.len();
464 let probe = if should_swap { s_right } else { s_left };
465
466 use JoinValidation::*;
467 let valid = match self.swap(should_swap) {
468 ManyToMany | ManyToOne => true,
471 OneToMany | OneToOne => {
472 if !nulls_equal && probe.null_count() > 0 {
473 probe.n_unique()? - 1 == probe.len() - probe.null_count()
474 } else {
475 probe.n_unique()? == probe.len()
476 }
477 },
478 };
479 polars_ensure!(valid, ComputeError: "join keys did not fulfill {} validation", self);
480 Ok(())
481 }
482
483 pub(super) fn validate_build(
484 &self,
485 build_size: usize,
486 expected_size: usize,
487 swapped: bool,
488 ) -> PolarsResult<()> {
489 use JoinValidation::*;
490
491 let valid = match self.swap(swapped) {
493 ManyToMany | OneToMany => true,
496 ManyToOne | OneToOne => build_size == expected_size,
497 };
498 polars_ensure!(valid, ComputeError: "join keys did not fulfill {} validation", self);
499 Ok(())
500 }
501}
502
503impl Display for JoinValidation {
504 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
505 let s = match self {
506 JoinValidation::ManyToMany => "m:m",
507 JoinValidation::ManyToOne => "m:1",
508 JoinValidation::OneToMany => "1:m",
509 JoinValidation::OneToOne => "1:1",
510 };
511 write!(f, "{s}")
512 }
513}
514
515impl Debug for JoinValidation {
516 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
517 write!(f, "JoinValidation: {self}")
518 }
519}