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