Skip to main content

mz_repr/
stats.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Persist Stats for non-primitive types.
11//!
12//! For primitive types please see [`mz_persist_types::stats`].
13
14use std::borrow::Cow;
15use std::collections::BTreeMap;
16use std::fmt::{Debug, Formatter};
17
18use anyhow::Context;
19use arrow::array::{BinaryArray, FixedSizeBinaryArray};
20use chrono::{NaiveDateTime, NaiveTime};
21use dec::OrderedDecimal;
22use mz_ore::soft_panic_or_log;
23use mz_persist_types::columnar::FixedSizeCodec;
24use mz_persist_types::stats::bytes::{BytesStats, FixedSizeBytesStats, FixedSizeBytesStatsKind};
25use mz_persist_types::stats::json::{JsonMapElementStats, JsonStats};
26use mz_persist_types::stats::primitive::PrimitiveStats;
27use mz_persist_types::stats::{
28    AtomicBytesStats, ColumnNullStats, ColumnStatKinds, ColumnStats, ColumnarStats,
29    PrimitiveStatsVariants,
30};
31use ordered_float::OrderedFloat;
32use prost::Message;
33use serde::Deserializer;
34use serde::de::{DeserializeSeed, Error, MapAccess, SeqAccess, Visitor};
35use uuid::Uuid;
36
37use crate::adt::date::Date;
38use crate::adt::datetime::PackedNaiveTime;
39use crate::adt::interval::{Interval, PackedInterval};
40use crate::adt::jsonb::{KeyClass, KeyClassifier, NumberParser};
41use crate::adt::numeric::{Numeric, PackedNumeric};
42use crate::adt::timestamp::{CheckedTimestamp, PackedNaiveDateTime};
43use crate::row::ProtoDatum;
44use crate::{Datum, Row, RowArena, SqlScalarType};
45
46fn soft_expect_or_log<A, B: Debug>(result: Result<A, B>) -> Option<A> {
47    match result {
48        Ok(a) => Some(a),
49        Err(e) => {
50            soft_panic_or_log!("failed to decode stats: {e:?}");
51            None
52        }
53    }
54}
55
56/// Return the stats for a fixed-size bytes column, defaulting to an appropriate value if
57/// no values are present.
58pub fn fixed_stats_from_column(
59    col: &FixedSizeBinaryArray,
60    kind: FixedSizeBytesStatsKind,
61) -> ColumnStatKinds {
62    // Note: Ideally here we'd use the arrow compute kernels for getting
63    // the min and max of a column, but aren't yet implemented for a
64    // `FixedSizedBinaryArray`.
65    //
66    // See: <https://github.com/apache/arrow-rs/issues/5934>
67
68    let lower = col.into_iter().filter_map(|x| x).min();
69    let upper = col.into_iter().filter_map(|x| x).max();
70
71    // We use the default when all values are null, including when the input is empty...
72    // in which case any min/max are fine as long as they decode properly.
73    let default = || match kind {
74        FixedSizeBytesStatsKind::PackedTime => PackedNaiveTime::from_value(NaiveTime::default())
75            .as_bytes()
76            .to_vec(),
77        FixedSizeBytesStatsKind::PackedDateTime => {
78            PackedNaiveDateTime::from_value(NaiveDateTime::default())
79                .as_bytes()
80                .to_vec()
81        }
82        FixedSizeBytesStatsKind::PackedInterval => PackedInterval::from_value(Interval::default())
83            .as_bytes()
84            .to_vec(),
85        FixedSizeBytesStatsKind::PackedNumeric => {
86            unreachable!("Numeric is not stored in a fixed size byte array")
87        }
88        FixedSizeBytesStatsKind::Uuid => Uuid::default().as_bytes().to_vec(),
89    };
90
91    BytesStats::FixedSize(FixedSizeBytesStats {
92        lower: lower.map_or_else(default, Vec::from),
93        upper: upper.map_or_else(default, Vec::from),
94        kind,
95    })
96    .into()
97}
98
99/// Persist computes float column bounds in IEEE-754 total order, in which
100/// negative NaNs sort below -Infinity. `Datum` floats compare via
101/// `OrderedFloat`, which ranks every NaN (of either sign) above every other
102/// value. A lower bound that is a negative NaN therefore admits both NaNs
103/// (the largest values under `Datum` ordering) and ordinary values up to
104/// `upper`, a set no `Datum` interval can bound, so no bounds are returned
105/// and the column is treated as unconstrained. The one exception is an upper
106/// bound that is also a negative NaN, which under total order means every
107/// value in the column is a NaN.
108///
109/// NOTE: the widening `ResultSpec::value_between` does for inverted bounds is
110/// not sufficient here. A part holding both `-NaN` and `+NaN` decodes to the
111/// non-inverted bounds `(NaN, NaN)`, which would wrongly claim the part holds
112/// nothing but NaN. Only this layer still sees the NaN signs.
113fn float_bounds<F: num_traits::Float>(lower: F, upper: F) -> Option<(F, F)> {
114    if lower.is_nan() && lower.is_sign_negative() && !(upper.is_nan() && upper.is_sign_negative()) {
115        None
116    } else {
117        Some((lower, upper))
118    }
119}
120
121/// Returns a `(lower, upper)` bound from the provided [`ColumnStatKinds`], if applicable.
122pub fn col_values<'a>(
123    typ: &SqlScalarType,
124    stats: &'a ColumnStatKinds,
125    arena: &'a RowArena,
126) -> Option<(Datum<'a>, Datum<'a>)> {
127    use PrimitiveStatsVariants::*;
128
129    /// Helper method to map the lower and upper bounds of some stats to Datums.
130    fn map_stats<'a, T, F>(stats: &'a T, f: F) -> Option<(Datum<'a>, Datum<'a>)>
131    where
132        T: ColumnStats,
133        F: Fn(T::Ref<'a>) -> Datum<'a>,
134    {
135        Some((f(stats.lower()?), f(stats.upper()?)))
136    }
137
138    match (typ, stats) {
139        (SqlScalarType::Bool, ColumnStatKinds::Primitive(Bool(stats))) => {
140            let map_datum = |val| if val { Datum::True } else { Datum::False };
141            map_stats(stats, map_datum)
142        }
143        (SqlScalarType::PgLegacyChar, ColumnStatKinds::Primitive(U8(stats))) => {
144            map_stats(stats, Datum::UInt8)
145        }
146        (SqlScalarType::UInt16, ColumnStatKinds::Primitive(U16(stats))) => {
147            map_stats(stats, Datum::UInt16)
148        }
149        (
150            SqlScalarType::UInt32
151            | SqlScalarType::Oid
152            | SqlScalarType::RegClass
153            | SqlScalarType::RegProc
154            | SqlScalarType::RegType,
155            ColumnStatKinds::Primitive(U32(stats)),
156        ) => map_stats(stats, Datum::UInt32),
157        (SqlScalarType::UInt64, ColumnStatKinds::Primitive(U64(stats))) => {
158            map_stats(stats, Datum::UInt64)
159        }
160        (SqlScalarType::Int16, ColumnStatKinds::Primitive(I16(stats))) => {
161            map_stats(stats, Datum::Int16)
162        }
163        (SqlScalarType::Int32, ColumnStatKinds::Primitive(I32(stats))) => {
164            map_stats(stats, Datum::Int32)
165        }
166        (SqlScalarType::Int64, ColumnStatKinds::Primitive(I64(stats))) => {
167            map_stats(stats, Datum::Int64)
168        }
169        (SqlScalarType::Float32, ColumnStatKinds::Primitive(F32(stats))) => {
170            let (lower, upper) = float_bounds(stats.lower, stats.upper)?;
171            Some((
172                Datum::Float32(OrderedFloat(lower)),
173                Datum::Float32(OrderedFloat(upper)),
174            ))
175        }
176        (SqlScalarType::Float64, ColumnStatKinds::Primitive(F64(stats))) => {
177            let (lower, upper) = float_bounds(stats.lower, stats.upper)?;
178            Some((
179                Datum::Float64(OrderedFloat(lower)),
180                Datum::Float64(OrderedFloat(upper)),
181            ))
182        }
183        (
184            SqlScalarType::Numeric { .. },
185            ColumnStatKinds::Bytes(BytesStats::FixedSize(FixedSizeBytesStats {
186                lower,
187                upper,
188                kind: FixedSizeBytesStatsKind::PackedNumeric,
189            })),
190        ) => {
191            let lower = soft_expect_or_log(PackedNumeric::from_bytes(lower))?.into_value();
192            let upper = soft_expect_or_log(PackedNumeric::from_bytes(upper))?.into_value();
193            Some((
194                Datum::Numeric(OrderedDecimal(lower)),
195                Datum::Numeric(OrderedDecimal(upper)),
196            ))
197        }
198        (
199            SqlScalarType::String
200            | SqlScalarType::PgLegacyName
201            | SqlScalarType::Char { .. }
202            | SqlScalarType::VarChar { .. },
203            ColumnStatKinds::Primitive(String(stats)),
204        ) => map_stats(stats, Datum::String),
205        (SqlScalarType::Bytes, ColumnStatKinds::Bytes(BytesStats::Primitive(stats))) => {
206            Some((Datum::Bytes(&stats.lower), Datum::Bytes(&stats.upper)))
207        }
208        (SqlScalarType::Date, ColumnStatKinds::Primitive(I32(stats))) => {
209            let lower = soft_expect_or_log(Date::from_pg_epoch(stats.lower))?;
210            let upper = soft_expect_or_log(Date::from_pg_epoch(stats.upper))?;
211            Some((Datum::Date(lower), Datum::Date(upper)))
212        }
213        // NOTE: the `kind` field is checked in each fixed-size arm below
214        // because `from_bytes` validates length only, and PackedNaiveDateTime,
215        // PackedInterval, and Uuid are all 16 bytes: wrong-kind bytes would
216        // otherwise silently decode into garbage bounds. A mismatched kind
217        // falls through to the catch-all arm, which degrades to "no stats".
218        (
219            SqlScalarType::Time,
220            ColumnStatKinds::Bytes(BytesStats::FixedSize(
221                stats @ FixedSizeBytesStats {
222                    kind: FixedSizeBytesStatsKind::PackedTime,
223                    ..
224                },
225            )),
226        ) => {
227            let lower = soft_expect_or_log(PackedNaiveTime::from_bytes(&stats.lower))?.into_value();
228            let upper = soft_expect_or_log(PackedNaiveTime::from_bytes(&stats.upper))?.into_value();
229            Some((Datum::Time(lower), Datum::Time(upper)))
230        }
231        (
232            SqlScalarType::Timestamp { .. },
233            ColumnStatKinds::Bytes(BytesStats::FixedSize(
234                stats @ FixedSizeBytesStats {
235                    kind: FixedSizeBytesStatsKind::PackedDateTime,
236                    ..
237                },
238            )),
239        ) => {
240            let lower =
241                soft_expect_or_log(PackedNaiveDateTime::from_bytes(&stats.lower))?.into_value();
242            let lower = soft_expect_or_log(CheckedTimestamp::from_timestamplike(lower))?;
243            let upper =
244                soft_expect_or_log(PackedNaiveDateTime::from_bytes(&stats.upper))?.into_value();
245            let upper = soft_expect_or_log(CheckedTimestamp::from_timestamplike(upper))?;
246
247            Some((Datum::Timestamp(lower), Datum::Timestamp(upper)))
248        }
249        (
250            SqlScalarType::TimestampTz { .. },
251            ColumnStatKinds::Bytes(BytesStats::FixedSize(
252                stats @ FixedSizeBytesStats {
253                    kind: FixedSizeBytesStatsKind::PackedDateTime,
254                    ..
255                },
256            )),
257        ) => {
258            let lower = soft_expect_or_log(PackedNaiveDateTime::from_bytes(&stats.lower))?
259                .into_value()
260                .and_utc();
261            let lower = soft_expect_or_log(CheckedTimestamp::from_timestamplike(lower))?;
262            let upper = soft_expect_or_log(PackedNaiveDateTime::from_bytes(&stats.upper))?
263                .into_value()
264                .and_utc();
265            let upper = soft_expect_or_log(CheckedTimestamp::from_timestamplike(upper))?;
266
267            Some((Datum::TimestampTz(lower), Datum::TimestampTz(upper)))
268        }
269        (SqlScalarType::MzTimestamp, ColumnStatKinds::Primitive(U64(stats))) => {
270            map_stats(stats, |x| Datum::MzTimestamp(crate::Timestamp::from(x)))
271        }
272        (
273            SqlScalarType::Interval,
274            ColumnStatKinds::Bytes(BytesStats::FixedSize(
275                stats @ FixedSizeBytesStats {
276                    kind: FixedSizeBytesStatsKind::PackedInterval,
277                    ..
278                },
279            )),
280        ) => {
281            let lower = soft_expect_or_log(PackedInterval::from_bytes(&stats.lower))?.into_value();
282            let upper = soft_expect_or_log(PackedInterval::from_bytes(&stats.upper))?.into_value();
283            Some((Datum::Interval(lower), Datum::Interval(upper)))
284        }
285        (
286            SqlScalarType::Uuid,
287            ColumnStatKinds::Bytes(BytesStats::FixedSize(
288                stats @ FixedSizeBytesStats {
289                    kind: FixedSizeBytesStatsKind::Uuid,
290                    ..
291                },
292            )),
293        ) => {
294            let lower = soft_expect_or_log(Uuid::from_slice(&stats.lower))?;
295            let upper = soft_expect_or_log(Uuid::from_slice(&stats.upper))?;
296            Some((Datum::Uuid(lower), Datum::Uuid(upper)))
297        }
298        // JSON stats are handled elsewhere.
299        (SqlScalarType::Jsonb, ColumnStatKinds::Bytes(BytesStats::Json(_))) => None,
300        // We don't maintain stats on any of these types.
301        (
302            SqlScalarType::AclItem
303            | SqlScalarType::MzAclItem
304            | SqlScalarType::Range { .. }
305            | SqlScalarType::Array(_)
306            | SqlScalarType::Map { .. }
307            | SqlScalarType::List { .. }
308            | SqlScalarType::Record { .. }
309            | SqlScalarType::Int2Vector,
310            ColumnStatKinds::None,
311        ) => None,
312        // V0 Columnar Stat Types that differ from the above.
313        (
314            SqlScalarType::Numeric { .. }
315            | SqlScalarType::Time
316            | SqlScalarType::Timestamp { .. }
317            | SqlScalarType::TimestampTz { .. }
318            | SqlScalarType::Interval
319            | SqlScalarType::Uuid,
320            ColumnStatKinds::Bytes(BytesStats::Atomic(AtomicBytesStats { lower, upper })),
321        ) => {
322            // The V0 encoding carries no type tag, so a decoded bound has to
323            // be validated against the column type before it is used: a
324            // wrong-typed bound would produce a range that excludes every
325            // value of the column's actual type. Malformed or mismatched
326            // legacy bytes degrade to "no stats" instead of panicking.
327            fn decode_v0<'a>(
328                bytes: &[u8],
329                typ: &SqlScalarType,
330                arena: &'a RowArena,
331            ) -> Option<Datum<'a>> {
332                let proto = soft_expect_or_log(ProtoDatum::decode(bytes))?;
333                let mut row = Row::default();
334                soft_expect_or_log(row.packer().try_push_proto(&proto))?;
335                let datum = arena.push_unary_row(row);
336                let type_matches = matches!(
337                    (typ, datum),
338                    (SqlScalarType::Numeric { .. }, Datum::Numeric(_))
339                        | (SqlScalarType::Time, Datum::Time(_))
340                        | (SqlScalarType::Timestamp { .. }, Datum::Timestamp(_))
341                        | (SqlScalarType::TimestampTz { .. }, Datum::TimestampTz(_))
342                        | (SqlScalarType::Interval, Datum::Interval(_))
343                        | (SqlScalarType::Uuid, Datum::Uuid(_))
344                );
345                if !type_matches {
346                    soft_panic_or_log!("V0 stats bound {datum:?} does not match column {typ:?}");
347                    return None;
348                }
349                Some(datum)
350            }
351            let lower = decode_v0(lower.as_slice(), typ, arena)?;
352            let upper = decode_v0(upper.as_slice(), typ, arena)?;
353
354            Some((lower, upper))
355        }
356        (typ, stats) => {
357            mz_ore::soft_panic_or_log!("found unexpected {stats:?} for column {typ:?}");
358            None
359        }
360    }
361}
362
363/// Decodes the lower and upper bound from [`PrimitiveStats<Vec<u8>>`] as [`Numeric`]s.
364pub fn decode_numeric<'a>(
365    stats: &PrimitiveStats<Vec<u8>>,
366    arena: &'a RowArena,
367) -> Result<(Datum<'a>, Datum<'a>), anyhow::Error> {
368    fn decode<'a>(bytes: &[u8], arena: &'a RowArena) -> Result<Datum<'a>, anyhow::Error> {
369        let proto = ProtoDatum::decode(bytes)?;
370        let datum = arena.make_datum(|r| {
371            r.try_push_proto(&proto)
372                .expect("ProtoDatum should be valid Datum")
373        });
374        let Datum::Numeric(_) = &datum else {
375            anyhow::bail!("expected Numeric found {datum:?}");
376        };
377        Ok(datum)
378    }
379    let lower = decode(&stats.lower, arena).context("lower")?;
380    let upper = decode(&stats.upper, arena).context("upper")?;
381
382    Ok((lower, upper))
383}
384
385/// Take the smallest / largest numeric values for a numeric col.
386/// TODO: use the float data for this instead if it becomes a performance bottleneck.
387pub fn numeric_stats_from_column(col: &BinaryArray) -> ColumnStatKinds {
388    let mut lower = OrderedDecimal(Numeric::nan());
389    let mut upper = OrderedDecimal(-Numeric::infinity());
390
391    for val in col.iter() {
392        let Some(val) = val else {
393            continue;
394        };
395        let val = OrderedDecimal(
396            PackedNumeric::from_bytes(val)
397                .expect("failed to roundtrip Numeric")
398                .into_value(),
399        );
400        lower = val.min(lower);
401        upper = val.max(upper);
402    }
403
404    BytesStats::FixedSize(FixedSizeBytesStats {
405        lower: PackedNumeric::from_value(lower.0).as_bytes().to_vec(),
406        upper: PackedNumeric::from_value(upper.0).as_bytes().to_vec(),
407        kind: FixedSizeBytesStatsKind::PackedNumeric,
408    })
409    .into()
410}
411
412#[derive(Default)]
413struct JsonVisitor<'de> {
414    count: usize,
415    nulls: bool,
416    bools: Option<(bool, bool)>,
417    strings: Option<(Cow<'de, str>, Cow<'de, str>)>,
418    ints: Option<(i64, i64)>,
419    uints: Option<(u64, u64)>,
420    floats: Option<(f64, f64)>,
421    numerics: Option<(Numeric, Numeric)>,
422    lists: bool,
423    maps: bool,
424    fields: BTreeMap<Cow<'de, str>, JsonVisitor<'de>>,
425}
426
427impl<'de> JsonVisitor<'de> {
428    pub fn to_stats(self) -> JsonMapElementStats {
429        let mut context: dec::Context<Numeric> = Default::default();
430        let Self {
431            count,
432            nulls,
433            bools,
434            strings,
435            ints,
436            uints,
437            floats,
438            numerics,
439            lists,
440            maps,
441            fields,
442        } = self;
443        let min_numeric = [
444            numerics.map(|(n, _)| n),
445            ints.map(|(n, _)| n.into()),
446            uints.map(|(n, _)| n.into()),
447            floats.map(|(n, _)| n.into()),
448        ]
449        .into_iter()
450        .flatten()
451        .min_by(|a, b| context.total_cmp(a, b));
452        let max_numeric = [
453            numerics.map(|(_, n)| n),
454            ints.map(|(_, n)| n.into()),
455            uints.map(|(_, n)| n.into()),
456            floats.map(|(_, n)| n.into()),
457        ]
458        .into_iter()
459        .flatten()
460        .max_by(|a, b| context.total_cmp(a, b));
461
462        let stats = match (nulls, min_numeric, max_numeric, bools, strings, lists, maps) {
463            (false, None, None, None, None, false, false) => JsonStats::None,
464            (true, None, None, None, None, false, false) => JsonStats::JsonNulls,
465            (false, Some(min), Some(max), None, None, false, false) => {
466                JsonStats::Numerics(PrimitiveStats {
467                    lower: ProtoDatum::from(Datum::Numeric(OrderedDecimal(min))).encode_to_vec(),
468                    upper: ProtoDatum::from(Datum::Numeric(OrderedDecimal(max))).encode_to_vec(),
469                })
470            }
471            (false, None, None, Some((min, max)), None, false, false) => {
472                JsonStats::Bools(PrimitiveStats {
473                    lower: min,
474                    upper: max,
475                })
476            }
477            (false, None, None, None, Some((min, max)), false, false) => {
478                JsonStats::Strings(PrimitiveStats {
479                    lower: min.into_owned(),
480                    upper: max.into_owned(),
481                })
482            }
483            (false, None, None, None, None, true, false) => JsonStats::Lists,
484            (false, None, None, None, None, false, true) => JsonStats::Maps(
485                fields
486                    .into_iter()
487                    .map(|(k, v)| (k.into_owned(), v.to_stats()))
488                    .collect(),
489            ),
490            _ => JsonStats::Mixed,
491        };
492
493        JsonMapElementStats { len: count, stats }
494    }
495}
496
497impl<'a, 'de> Visitor<'de> for &'a mut JsonVisitor<'de> {
498    type Value = ();
499
500    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
501        write!(formatter, "json value")
502    }
503
504    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
505    where
506        E: Error,
507    {
508        self.count += 1;
509        let (min, max) = self.bools.get_or_insert((v, v));
510        *min = v.min(*min);
511        *max = v.max(*max);
512        Ok(())
513    }
514
515    fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
516    where
517        E: Error,
518    {
519        self.count += 1;
520        let (min, max) = self.ints.get_or_insert((v, v));
521        *min = v.min(*min);
522        *max = v.max(*max);
523        Ok(())
524    }
525
526    fn visit_u64<E>(self, v: u64) -> Result<(), E>
527    where
528        E: Error,
529    {
530        self.count += 1;
531        let (min, max) = self.uints.get_or_insert((v, v));
532        *min = v.min(*min);
533        *max = v.max(*max);
534        Ok(())
535    }
536
537    fn visit_f64<E>(self, v: f64) -> Result<(), E> {
538        self.count += 1;
539        let (min, max) = self.floats.get_or_insert((v, v));
540        *min = v.min(*min);
541        *max = v.max(*max);
542        Ok(())
543    }
544
545    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
546    where
547        E: Error,
548    {
549        self.count += 1;
550        match &mut self.strings {
551            None => {
552                self.strings = Some((v.to_owned().into(), v.to_owned().into()));
553            }
554            Some((min, max)) => {
555                if v < &**min {
556                    *min = v.to_owned().into();
557                } else if v > &**max {
558                    *max = v.to_owned().into();
559                }
560            }
561        }
562        Ok(())
563    }
564
565    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
566    where
567        E: Error,
568    {
569        self.count += 1;
570        match &mut self.strings {
571            None => {
572                self.strings = Some((v.into(), v.into()));
573            }
574            Some((min, max)) => {
575                if v < &**min {
576                    *min = v.into();
577                } else if v > &**max {
578                    *max = v.into();
579                }
580            }
581        }
582        Ok(())
583    }
584
585    fn visit_unit<E>(self) -> Result<Self::Value, E>
586    where
587        E: Error,
588    {
589        self.count += 1;
590        self.nulls = true;
591        Ok(())
592    }
593
594    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
595    where
596        A: SeqAccess<'de>,
597    {
598        self.count += 1;
599        self.lists = true;
600        while let Some(_) = seq.next_element::<serde::de::IgnoredAny>()? {}
601        Ok(())
602    }
603
604    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
605    where
606        A: MapAccess<'de>,
607    {
608        self.count += 1;
609        // serde_json gives us arbitrary-precision decimals as a specially shaped object.
610        // See crate::adt::jsonb for the details.
611        let mut normal_only = true;
612        while let Some(key) = map.next_key_seed(KeyClassifier)? {
613            match key {
614                KeyClass::Number => {
615                    let v = map.next_value_seed(NumberParser)?.0;
616                    let (min, max) = self.numerics.get_or_insert((v, v));
617                    if v < *min {
618                        *min = v;
619                    }
620                    if v > *max {
621                        *max = v;
622                    }
623                    normal_only = false;
624                }
625                KeyClass::MapKey(key) => {
626                    let field = self.fields.entry(key).or_default();
627                    map.next_value_seed(field)?;
628                }
629            }
630        }
631        if normal_only {
632            self.maps = true;
633        }
634
635        Ok(())
636    }
637}
638
639impl<'a, 'de> DeserializeSeed<'de> for &'a mut JsonVisitor<'de> {
640    type Value = ();
641
642    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
643    where
644        D: Deserializer<'de>,
645    {
646        deserializer.deserialize_any(self)
647    }
648}
649
650pub fn stats_for_json<'a>(jsons: impl IntoIterator<Item = Option<&'a str>>) -> ColumnarStats {
651    let mut visitor = JsonVisitor::default();
652    let mut nulls = 0;
653    for json in jsons {
654        match json {
655            None => {
656                nulls += 1;
657            }
658            Some(json) => {
659                let () = serde_json::Deserializer::from_str(json)
660                    .deserialize_any(&mut visitor)
661                    .unwrap_or_else(|e| panic!("error {e:?} on json: {json}"));
662            }
663        }
664    }
665
666    ColumnarStats {
667        nulls: Some(ColumnNullStats { count: nulls }),
668        values: ColumnStatKinds::Bytes(BytesStats::Json(visitor.to_stats().stats)),
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use arrow::array::AsArray;
675    use mz_persist_types::codec_impls::UnitSchema;
676    use mz_persist_types::columnar::{ColumnDecoder, Schema};
677    use mz_persist_types::part::PartBuilder;
678    use mz_persist_types::stats::{ProtoStructStats, StructStats, TrimStats};
679    use mz_proto::RustType;
680    use proptest::prelude::*;
681    use uuid::Uuid;
682
683    use crate::{Datum, RelationDesc, Row, RowArena, SqlScalarType};
684
685    fn datum_stats_roundtrip_trim<'a>(
686        schema: &RelationDesc,
687        datums: impl IntoIterator<Item = &'a Row>,
688    ) {
689        let mut builder = PartBuilder::new(schema, &UnitSchema);
690        for datum in datums {
691            builder.push(datum, &(), 1u64, 1i64);
692        }
693        let part = builder.finish();
694
695        let key_col = part.key.as_struct();
696        let decoder =
697            <RelationDesc as Schema<Row>>::decoder(schema, key_col.clone()).expect("success");
698        let mut actual: ProtoStructStats = RustType::into_proto(&decoder.stats());
699
700        // It's not particularly easy to give StructStats a PartialEq impl, but
701        // verifying that there weren't any panics gets us pretty far.
702
703        // Sanity check that trimming the stats doesn't cause them to be invalid
704        // (regression for a bug we had that caused panic at stats usage time).
705        actual.trim();
706        let actual: StructStats = RustType::from_proto(actual).unwrap();
707        let arena = RowArena::default();
708        for (name, typ) in schema.iter() {
709            let col_stats = actual.col(name).unwrap();
710            crate::stats::col_values(&typ.scalar_type, &col_stats.values, &arena);
711        }
712    }
713
714    fn scalar_type_stats_roundtrip_trim(scalar_type: SqlScalarType) {
715        let mut rows = Vec::new();
716        for datum in scalar_type.interesting_datums() {
717            rows.push(Row::pack(std::iter::once(datum)));
718        }
719
720        // Non-nullable version of the column.
721        let schema = RelationDesc::builder()
722            .with_column("col", scalar_type.clone().nullable(false))
723            .finish();
724        for row in rows.iter() {
725            datum_stats_roundtrip_trim(&schema, [row]);
726        }
727        datum_stats_roundtrip_trim(&schema, &rows[..]);
728
729        // Nullable version of the column.
730        let schema = RelationDesc::builder()
731            .with_column("col", scalar_type.nullable(true))
732            .finish();
733        rows.push(Row::pack(std::iter::once(Datum::Null)));
734        for row in rows.iter() {
735            datum_stats_roundtrip_trim(&schema, [row]);
736        }
737        datum_stats_roundtrip_trim(&schema, &rows[..]);
738    }
739
740    // Ideally, this test would live in persist-types next to the stats <->
741    // proto code, but it's much easier to proptest them from Datums.
742    #[mz_ore::test]
743    #[cfg_attr(miri, ignore)] // too slow
744    fn all_scalar_types_stats_roundtrip_trim() {
745        proptest!(|(scalar_type in any::<SqlScalarType>())| {
746            // The proptest! macro interferes with rustfmt.
747            scalar_type_stats_roundtrip_trim(scalar_type)
748        });
749    }
750
751    #[mz_ore::test]
752    #[cfg_attr(miri, ignore)] // slow
753    fn proptest_uuid_sort_order() {
754        fn test(mut og: Vec<Uuid>) {
755            let mut as_bytes: Vec<_> = og.iter().map(|u| u.as_bytes().clone()).collect();
756
757            og.sort();
758            as_bytes.sort();
759
760            let rnd: Vec<_> = as_bytes.into_iter().map(Uuid::from_bytes).collect();
761
762            assert_eq!(og, rnd);
763        }
764
765        let arb_uuid = any::<[u8; 16]>().prop_map(Uuid::from_bytes);
766        proptest!(|(uuids in proptest::collection::vec(arb_uuid, 0..128))| {
767            test(uuids);
768        });
769    }
770}