Skip to main content

vortex_array/aggregate_fn/fns/all_non_distinct/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod bool;
5mod decimal;
6mod extension;
7mod filter;
8mod fixed_size_list;
9mod list;
10mod map;
11mod primitive;
12mod struct_;
13#[cfg(test)]
14mod tests;
15mod varbin;
16mod variant;
17
18use std::sync::LazyLock;
19
20use vortex_error::VortexResult;
21use vortex_error::vortex_bail;
22use vortex_error::vortex_err;
23use vortex_session::registry::CachedId;
24
25use self::bool::check_bool_identical;
26use self::decimal::check_decimal_identical;
27use self::extension::check_extension_identical;
28use self::filter::shared_validity_mask;
29use self::fixed_size_list::check_fixed_size_list_identical;
30use self::list::check_list_identical;
31use self::map::check_map_identical;
32use self::primitive::check_primitive_identical;
33use self::struct_::check_struct_identical;
34use self::varbin::check_varbinview_identical;
35use crate::ArrayRef;
36use crate::Canonical;
37use crate::Columnar;
38use crate::ExecutionCtx;
39use crate::IntoArray;
40use crate::aggregate_fn::Accumulator;
41use crate::aggregate_fn::AggregateFnId;
42use crate::aggregate_fn::AggregateFnVTable;
43use crate::aggregate_fn::DynAccumulator;
44use crate::aggregate_fn::EmptyOptions;
45use crate::aggregate_fn::fns::all_non_distinct::variant::check_variant_identical;
46use crate::arrays::StructArray;
47use crate::arrays::struct_::StructArrayExt;
48use crate::dtype::DType;
49use crate::dtype::FieldNames;
50use crate::dtype::Nullability;
51use crate::scalar::Scalar;
52use crate::validity::Validity;
53
54/// Check if two arrays are element-wise non-distinct, treating null == null as true.
55///
56/// Returns `true` if and only if:
57/// - Both arrays have the same dtype and length
58/// - At every position, both are null or both are non-null with the same value
59/// - The arrays are empty, vacuously
60///
61/// This is a fused `bool_all(non_distinct(lhs, rhs))` aggregate that allows early
62/// termination via accumulator saturation as soon as a mismatch is found.
63pub fn all_non_distinct(a: &ArrayRef, b: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
64    if a.dtype() != b.dtype() {
65        vortex_bail!(
66            "all_non_distinct: dtype mismatch: {} vs {}",
67            a.dtype(),
68            b.dtype()
69        );
70    }
71
72    if a.len() != b.len() {
73        vortex_bail!(
74            "all_non_distinct: length mismatch: {} vs {}",
75            a.len(),
76            b.len()
77        );
78    }
79
80    if a.is_empty() {
81        return Ok(true);
82    }
83
84    let Some(shared_validity) = shared_validity_mask(a, b, ctx)? else {
85        return Ok(false);
86    };
87    if shared_validity.true_count() == 0 {
88        return Ok(true);
89    }
90
91    let validity = Validity::from_mask(shared_validity, a.dtype().nullability());
92    let batch = StructArray::try_new(NAMES.clone(), vec![a.clone(), b.clone()], a.len(), validity)?
93        .into_array();
94
95    let mut acc = Accumulator::try_new(AllNonDistinct, EmptyOptions, batch.dtype().clone())?;
96    acc.accumulate(&batch, ctx)?;
97    let result = acc.finish()?;
98
99    Ok(result.as_bool().value().unwrap_or(false))
100}
101
102static NAMES: LazyLock<FieldNames> = LazyLock::new(|| FieldNames::from(["lhs", "rhs"]));
103
104/// Fused `bool_all(non_distinct(lhs, rhs))` aggregate function.
105///
106/// This combines a pairwise non-distinct scalar comparison with a boolean-all reduction
107/// into a single aggregate, enabling early termination via accumulator saturation: as soon
108/// as the first distinct pair is found, the accumulator is saturated and remaining batches
109/// are skipped.
110///
111/// Like other `all` aggregates, this is vacuously true for empty input.
112///
113/// The input is a `Struct{lhs: T, rhs: T}` and the result is `Bool(NonNullable)`.
114#[derive(Clone, Debug)]
115pub struct AllNonDistinct;
116
117/// Partial accumulator state: just a bool tracking "all non-distinct so far".
118pub struct AllNonDistinctPartial {
119    all_non_distinct: bool,
120}
121
122impl AggregateFnVTable for AllNonDistinct {
123    type Options = EmptyOptions;
124    type Partial = AllNonDistinctPartial;
125
126    fn id(&self) -> AggregateFnId {
127        static ID: CachedId = CachedId::new("vortex.all_non_distinct");
128        *ID
129    }
130
131    fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
132        unimplemented!("AllNonDistinct is not yet serializable");
133    }
134
135    fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
136        match input_dtype {
137            DType::Struct(fields, _) if fields.nfields() == 2 => {
138                let lhs = fields.fields().next()?;
139                let rhs = fields.fields().nth(1)?;
140                (lhs == rhs).then(|| DType::Bool(Nullability::NonNullable))
141            }
142            _ => None,
143        }
144    }
145
146    fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
147        self.return_dtype(options, input_dtype)
148    }
149
150    fn empty_partial(
151        &self,
152        _options: &Self::Options,
153        _input_dtype: &DType,
154    ) -> VortexResult<Self::Partial> {
155        Ok(AllNonDistinctPartial {
156            all_non_distinct: true,
157        })
158    }
159
160    fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()> {
161        if !partial.all_non_distinct {
162            return Ok(());
163        }
164
165        if !other.as_bool().value().unwrap_or(false) {
166            partial.all_non_distinct = false;
167        }
168        Ok(())
169    }
170
171    fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
172        Ok(Scalar::bool(
173            partial.all_non_distinct,
174            Nullability::NonNullable,
175        ))
176    }
177
178    fn reset(&self, partial: &mut Self::Partial) {
179        partial.all_non_distinct = true;
180    }
181
182    #[inline]
183    fn is_saturated(&self, partial: &Self::Partial) -> bool {
184        !partial.all_non_distinct
185    }
186
187    fn accumulate(
188        &self,
189        partial: &mut Self::Partial,
190        batch: &Columnar,
191        ctx: &mut ExecutionCtx,
192    ) -> VortexResult<()> {
193        if !partial.all_non_distinct {
194            return Ok(());
195        }
196
197        match batch {
198            Columnar::Constant(c) => {
199                let _ = c;
200                Ok(())
201            }
202            Columnar::Canonical(c) => {
203                let Canonical::Struct(s) = c else {
204                    vortex_bail!(
205                        "AllNonDistinct expects a Struct canonical, got {:?}",
206                        c.dtype()
207                    );
208                };
209
210                // The struct-level validity represents the shared validity mask
211                // (positions where both lhs and rhs are non-null).
212                let struct_mask = s.validity()?.execute_mask(s.len(), ctx)?;
213                if struct_mask.true_count() == 0 {
214                    return Ok(());
215                }
216
217                let lhs = s.unmasked_field(0);
218                let rhs = s.unmasked_field(1);
219
220                // Filter to only valid rows if the struct has nulls.
221                let (lhs, rhs) = if struct_mask.true_count() == s.len() {
222                    (lhs.clone(), rhs.clone())
223                } else {
224                    (lhs.filter(struct_mask.clone())?, rhs.filter(struct_mask)?)
225                };
226
227                let lhs_canonical = lhs.execute::<Canonical>(ctx)?;
228                let rhs_canonical = rhs.execute::<Canonical>(ctx)?;
229
230                partial.all_non_distinct =
231                    check_canonical_identical(&lhs_canonical, &rhs_canonical, ctx)?;
232
233                Ok(())
234            }
235        }
236    }
237
238    fn finalize(&self, _partials: ArrayRef) -> VortexResult<ArrayRef> {
239        vortex_bail!("AllNonDistinct does not support array finalization");
240    }
241
242    fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar> {
243        Ok(Scalar::bool(
244            partial.all_non_distinct,
245            Nullability::NonNullable,
246        ))
247    }
248}
249
250fn check_canonical_identical(
251    lhs: &Canonical,
252    rhs: &Canonical,
253    ctx: &mut ExecutionCtx,
254) -> VortexResult<bool> {
255    match (lhs, rhs) {
256        (Canonical::Null(_), Canonical::Null(_)) => Ok(true),
257        (Canonical::Bool(lhs), Canonical::Bool(rhs)) => check_bool_identical(lhs, rhs),
258        (Canonical::Primitive(lhs), Canonical::Primitive(rhs)) => {
259            check_primitive_identical(lhs, rhs)
260        }
261        (Canonical::Decimal(lhs), Canonical::Decimal(rhs)) => check_decimal_identical(lhs, rhs),
262        (Canonical::VarBinView(lhs), Canonical::VarBinView(rhs)) => {
263            check_varbinview_identical(lhs, rhs)
264        }
265        (Canonical::Struct(lhs), Canonical::Struct(rhs)) => check_struct_identical(lhs, rhs, ctx),
266        (Canonical::List(lhs), Canonical::List(rhs)) => check_list_identical(lhs, rhs, ctx),
267        (Canonical::Map(lhs), Canonical::Map(rhs)) => check_map_identical(lhs, rhs, ctx),
268        (Canonical::FixedSizeList(lhs), Canonical::FixedSizeList(rhs)) => {
269            check_fixed_size_list_identical(lhs, rhs, ctx)
270        }
271        (Canonical::Extension(lhs), Canonical::Extension(rhs)) => {
272            check_extension_identical(lhs, rhs, ctx)
273        }
274        (Canonical::Variant(lhs), Canonical::Variant(rhs)) => {
275            check_variant_identical(lhs, rhs, ctx)
276        }
277        _ => Err(vortex_err!(
278            "Canonical type mismatch in AllNonDistinct: {:?} vs {:?}",
279            lhs.dtype(),
280            rhs.dtype()
281        )),
282    }
283}