Skip to main content

polars_ops/frame/join/
args.rs

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/// Parameters for which side to use as the build side in a join. Currently only
23/// respected by the streaming engine.
24#[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    /// Unless there's a very good reason to believe that the right side is
29    /// smaller, use the left side.
30    PreferLeft,
31    /// Regardless of other heuristics, use the left side as build side.
32    ForceLeft,
33
34    // Similar to above.
35    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    // Box is okay because this is inside a `Arc<JoinOptionsIR>`
69    #[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    /// Inequality join with two arbitrary predicates
77    // Options are set by optimizer/planner in Options
78    IEJoin,
79    #[cfg(feature = "iejoin")]
80    /// Inequality join with col ∈ [lo, hi] predicate
81    // Options are set by optimizer/planner in Options
82    Range,
83    // Options are set by optimizer/planner in Options
84    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    /// Evaluates the filter predicate on `df`, returning a boolean mask.
192    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    /// Unmatched rows of the left input appear in the output.
371    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    /// Unmatched rows of the right input appear in the output.
383    pub fn emits_unmatched_right(&self) -> bool {
384        matches!(self, JoinType::Right | JoinType::Full)
385    }
386
387    /// Joins supported in join where with non-equi conditions
388    pub fn supports_non_equi(&self) -> bool {
389        matches!(self, JoinType::Inner | JoinType::Left | JoinType::Right)
390    }
391
392    /// Whether the physical join implementations can execute this `how` with the given
393    /// (already-resolved) match-condition algorithm without silently dropping it.
394    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    /// No unique checks
411    #[default]
412    ManyToMany,
413    /// Check if join keys are unique in right dataset.
414    ManyToOne,
415    /// Check if join keys are unique in left dataset.
416    OneToMany,
417    /// Check if join keys are unique in both left and right datasets
418    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        // In default, probe is the left series.
457        //
458        // In inner join and outer join, the shortest relation will be used to create a hash table.
459        // In left join, always use the right side to create.
460        //
461        // If `build_shortest_table` and left is shorter, swap. Then rhs will be the probe.
462        // If left == right, swap too. (apply the same logic as `det_hash_prone_order`)
463        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            // Only check the `build` side.
469            // The other side use `validate_build` to check
470            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        // In default, build is in rhs.
492        let valid = match self.swap(swapped) {
493            // Only check the `build` side.
494            // The other side use `validate_prone` to check
495            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}