1use mz_expr::{
13 ColumnSpecs, Interpretable, Interpreter, MapFilterProject, MfpPlan, OptimizableExpr,
14 ResultSpec, UnmaterializableFunc,
15};
16use mz_persist_types::stats::{
17 BytesStats, ColumnStatKinds, JsonStats, PartStats, PartStatsMetrics,
18};
19use mz_repr::{
20 ColumnIndex, Datum, RelationDesc, ReprRelationType, RowArena, SqlColumnType, SqlScalarType,
21};
22
23#[derive(Debug)]
26pub struct RelationPartStats<'a> {
27 pub(crate) name: &'a str,
28 pub(crate) metrics: &'a PartStatsMetrics,
29 pub(crate) desc: &'a RelationDesc,
30 pub(crate) stats: &'a PartStats,
31}
32
33impl<'a> RelationPartStats<'a> {
34 pub fn new(
35 name: &'a str,
36 metrics: &'a PartStatsMetrics,
37 desc: &'a RelationDesc,
38 stats: &'a PartStats,
39 ) -> Self {
40 Self {
41 name,
42 metrics,
43 desc,
44 stats,
45 }
46 }
47}
48
49impl RelationPartStats<'_> {
50 pub fn may_match_mfp<'a, X: Interpretable + OptimizableExpr>(
51 &'a self,
52 time_range: ResultSpec<'a>,
53 mfp: &MapFilterProject<X>,
54 ) -> bool {
55 self.may_match(time_range, |ranges| ranges.mfp_filter(mfp))
56 }
57
58 pub fn may_match_mfp_plan<'a, X: Interpretable + OptimizableExpr>(
61 &'a self,
62 time_range: ResultSpec<'a>,
63 plan: &MfpPlan<X>,
64 ) -> bool {
65 self.may_match(time_range, |ranges| ranges.mfp_plan_filter(plan))
66 }
67
68 fn may_match<'a>(
69 &'a self,
70 time_range: ResultSpec<'a>,
71 filter: impl for<'b> FnOnce(&ColumnSpecs<'b>) -> mz_expr::ColumnSpec<'b>,
72 ) -> bool {
73 let arena = RowArena::new();
74 let relation = ReprRelationType::from(self.desc.typ());
75 let mut ranges = ColumnSpecs::new(&relation, &arena);
76 ranges.push_unmaterializable(UnmaterializableFunc::MzNow, time_range);
77
78 if self.err_count().is_none_or(|count| count > 0) {
82 return true;
83 }
84
85 for (pos, (idx, _name, _typ)) in self.desc.iter_all().enumerate() {
86 let result_spec = self.col_stats(idx, &arena);
87 ranges.push_column(pos, result_spec);
88 }
89 let result = filter(&ranges).range;
90 result.may_contain(Datum::True) || result.may_fail()
91 }
92
93 fn json_spec<'a>(len: usize, stats: &'a JsonStats, arena: &'a RowArena) -> ResultSpec<'a> {
94 match stats {
95 JsonStats::JsonNulls => ResultSpec::value(Datum::JsonNull),
96 JsonStats::Bools(bools) => {
97 ResultSpec::value_between(bools.lower.into(), bools.upper.into())
98 }
99 JsonStats::Strings(strings) => ResultSpec::value_between(
100 Datum::String(strings.lower.as_str()),
101 Datum::String(strings.upper.as_str()),
102 ),
103 JsonStats::Numerics(numerics) => {
104 match mz_repr::stats::decode_numeric(numerics, arena) {
105 Ok((lower, upper)) => ResultSpec::value_between(lower, upper),
106 Err(err) => {
107 tracing::error!(%err, "failed to decode Json Numeric stats!");
108 ResultSpec::anything()
109 }
110 }
111 }
112 JsonStats::Maps(maps) => {
113 ResultSpec::map_spec(
114 maps.into_iter()
115 .map(|(k, v)| {
116 let mut v_spec = Self::json_spec(v.len, &v.stats, arena);
117 if v.len != len {
118 v_spec = v_spec.union(ResultSpec::null());
121 }
122 let key = arena.make_datum(|r| r.push(Datum::String(k.as_str())));
123 (key, v_spec)
124 })
125 .collect(),
126 )
127 }
128 JsonStats::None => ResultSpec::nothing(),
129 JsonStats::Lists | JsonStats::Mixed => ResultSpec::anything(),
130 }
131 }
132
133 pub fn col_stats<'a>(&'a self, idx: &ColumnIndex, arena: &'a RowArena) -> ResultSpec<'a> {
134 let value_range = match self.col_values(idx, arena) {
135 Some(spec) => spec,
136 None => ResultSpec::anything(),
137 };
138 let json_range = self
139 .col_json(idx, arena)
140 .unwrap_or_else(ResultSpec::anything);
141
142 value_range.intersect(json_range)
145 }
146
147 fn col_json<'a>(&'a self, idx: &ColumnIndex, arena: &'a RowArena) -> Option<ResultSpec<'a>> {
148 let name = self.desc.get_name_idx(idx);
149 let typ = &self.desc.get_type(idx);
150
151 let ok_stats = self.stats.key.col("ok")?;
152 let ok_stats = match ok_stats.try_as_optional_struct() {
156 Ok(ok_stats) => ok_stats,
157 Err(err) => {
158 self.metrics.mismatched_count.inc();
159 tracing::error!(
160 "expected nullable struct stats for the 'ok' column of {}: {err}",
161 self.name
162 );
163 return None;
164 }
165 };
166 let col_stats = ok_stats.some.cols.get(name.as_str())?;
167
168 if let SqlColumnType {
169 scalar_type: SqlScalarType::Jsonb,
170 nullable,
171 } = typ
172 {
173 let value_range = match &col_stats.values {
174 ColumnStatKinds::Bytes(BytesStats::Json(json_stats)) => {
175 Self::json_spec(ok_stats.some.len, json_stats, arena)
176 }
177 ColumnStatKinds::Bytes(
178 BytesStats::Primitive(_) | BytesStats::Atomic(_) | BytesStats::FixedSize(_),
179 ) => ResultSpec::anything(),
180 other => {
181 self.metrics.mismatched_count.inc();
182 tracing::error!(
183 "expected BytesStats for JSON column {}, found {other:?}",
184 self.name
185 );
186 return None;
187 }
188 };
189 let null_range = match (nullable, col_stats.nulls) {
190 (false, None) => ResultSpec::nothing(),
191 (true, Some(nulls)) if nulls.count == 0 => ResultSpec::nothing(),
192 (true, Some(_)) => ResultSpec::null(),
193 (col_null, stats_null) => {
194 self.metrics.mismatched_count.inc();
195 tracing::error!(
196 "JSON column nullability mismatch, col {} null: {col_null}, stats: {stats_null:?}",
197 self.name
198 );
199 return None;
200 }
201 };
202
203 Some(null_range.union(value_range))
204 } else {
205 None
206 }
207 }
208
209 pub fn len(&self) -> Option<usize> {
210 Some(self.stats.key.len)
211 }
212
213 pub fn ok_count(&self) -> Option<usize> {
214 let stats = self.stats.key.col("err")?.try_as_optional_bytes().ok()?;
219 Some(stats.none)
220 }
221
222 pub fn err_count(&self) -> Option<usize> {
223 let num_results = self.stats.key.len;
228 let num_oks = self.ok_count();
229 num_oks.and_then(|num_oks| num_results.checked_sub(num_oks))
232 }
233
234 fn col_values<'a>(&'a self, idx: &ColumnIndex, arena: &'a RowArena) -> Option<ResultSpec<'a>> {
235 let name = self.desc.get_name_idx(idx);
236 let typ = self.desc.get_type(idx);
237
238 let ok_stats = self.stats.key.cols.get("ok")?;
239 let ColumnStatKinds::Struct(ok_stats) = &ok_stats.values else {
242 self.metrics.mismatched_count.inc();
243 tracing::error!(
244 "expected struct stats for the 'ok' column of {}, found {:?}",
245 self.name,
246 ok_stats.values
247 );
248 return None;
249 };
250 let col_stats = ok_stats.cols.get(name.as_str())?;
251
252 let min_max = mz_repr::stats::col_values(&typ.scalar_type, &col_stats.values, arena);
253 let null_count = col_stats.nulls.as_ref().map_or(0, |nulls| nulls.count);
254 let total_count = self.len();
255
256 let values = match (total_count, min_max) {
257 (Some(total_count), _) if total_count == null_count => ResultSpec::nothing(),
258 (_, Some((min, max))) => ResultSpec::value_between(min, max),
259 _ => ResultSpec::value_all(),
260 };
261 let nulls = if null_count > 0 {
262 ResultSpec::null()
263 } else {
264 ResultSpec::nothing()
265 };
266
267 Some(values.union(nulls))
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use arrow::array::AsArray;
274 use mz_ore::metrics::MetricsRegistry;
275 use mz_persist_types::codec_impls::UnitSchema;
276 use mz_persist_types::columnar::{ColumnDecoder, Schema};
277 use mz_persist_types::part::PartBuilder;
278 use mz_persist_types::stats::{PartStats, ProtoStructStats, TrimStats, trim_to_budget};
279 use mz_proto::RustType;
280 use mz_repr::{Datum, RelationDesc, Row, RowArena, SqlColumnType, SqlScalarType};
281 use mz_repr::{SqlRelationType, arb_datum_for_column};
282 use proptest::prelude::*;
283 use proptest::strategy::ValueTree;
284
285 use super::*;
286 use crate::sources::SourceData;
287
288 fn validate_stats(column_type: &SqlColumnType, datums: &[Datum<'_>]) -> Result<(), String> {
289 let schema = RelationDesc::builder()
290 .with_column("col", column_type.clone())
291 .finish();
292
293 let mut builder = PartBuilder::new(&schema, &UnitSchema);
294 let mut row = SourceData(Ok(Row::default()));
295 for datum in datums {
296 row.as_mut().unwrap().packer().push(datum);
297 builder.push(&row, &(), 1u64, 1i64);
298 }
299 let part = builder.finish();
300
301 let key_col = part.key.as_struct();
302 let decoder = <RelationDesc as Schema<SourceData>>::decoder(&schema, key_col.clone())
303 .expect("success");
304 let key_stats = decoder.stats();
305
306 let proto: ProtoStructStats = RustType::into_proto(&key_stats);
313 let mut variants = vec![("collected".to_string(), key_stats)];
314 {
315 let mut trimmed = proto.clone();
316 trimmed.trim();
317 variants.push((
318 "trimmed".to_string(),
319 RustType::from_proto(trimmed).expect("valid proto"),
320 ));
321 }
322 let full = prost::Message::encoded_len(&proto);
323 for budget in [full / 2, full / 4, 16, 0] {
324 let mut trimmed = proto.clone();
325 trim_to_budget(&mut trimmed, budget, |col| col == "err");
326 variants.push((
327 format!("trim_to_budget({budget})"),
328 RustType::from_proto(trimmed).expect("valid proto"),
329 ));
330 }
331
332 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
333 for (label, key_stats) in variants {
334 let stats = RelationPartStats {
335 name: "test",
336 metrics: &metrics,
337 stats: &PartStats { key: key_stats },
338 desc: &schema,
339 };
340 let arena = RowArena::default();
341
342 for datum in datums {
344 let spec = stats.col_stats(&ColumnIndex::from_raw(0), &arena);
345 if !spec.may_contain(*datum) {
346 return Err(format!(
347 "{label} stats-derived spec claims {datum:?} is absent from a part that \
348 contains it (type: {:?}, part: {datums:?}, spec: {spec:?})",
349 column_type.scalar_type,
350 ));
351 }
352 }
353 }
354
355 Ok(())
356 }
357
358 fn scalar_type_stats_roundtrip(scalar_type: SqlScalarType) {
359 let column_type = scalar_type.clone().nullable(false);
361 for datum in scalar_type.interesting_datums() {
362 assert_eq!(validate_stats(&column_type, &[datum]), Ok(()));
363 }
364
365 let column_type = scalar_type.clone().nullable(true);
367 for datum in scalar_type.interesting_datums() {
368 assert_eq!(validate_stats(&column_type, &[datum]), Ok(()));
369 }
370 assert_eq!(validate_stats(&column_type, &[Datum::Null]), Ok(()));
371 }
372
373 #[mz_ore::test]
374 #[cfg_attr(miri, ignore)] fn all_scalar_types_stats_roundtrip() {
376 proptest!(|(scalar_type in any::<SqlScalarType>())| {
377 scalar_type_stats_roundtrip(scalar_type)
379 });
380 }
381
382 #[mz_ore::test]
392 #[cfg_attr(miri, ignore)] fn interesting_datum_combinations_stats_roundtrip() {
394 for scalar_type in SqlScalarType::enumerate() {
395 let datums: Vec<_> = scalar_type.interesting_datums().collect();
396 if datums.is_empty() {
397 continue;
398 }
399 for nullable in [false, true] {
400 let column_type = scalar_type.clone().nullable(nullable);
401 for (i, a) in datums.iter().enumerate() {
402 for b in &datums[i + 1..] {
403 assert_eq!(validate_stats(&column_type, &[*a, *b]), Ok(()));
404 }
405 if nullable {
406 assert_eq!(validate_stats(&column_type, &[*a, Datum::Null]), Ok(()));
407 }
408 }
409 let mut all = datums.clone();
410 if nullable {
411 all.push(Datum::Null);
412 }
413 assert_eq!(validate_stats(&column_type, &all[..]), Ok(()));
414 }
415 }
416 }
417
418 #[mz_ore::test]
419 #[cfg_attr(miri, ignore)] fn all_datums_produce_valid_stats() {
421 let datums = any::<SqlColumnType>().prop_flat_map(|ty| {
423 prop::collection::vec(arb_datum_for_column(ty.clone()), 0..128)
424 .prop_map(move |datums| (ty.clone(), datums))
425 });
426
427 proptest!(
428 ProptestConfig::with_cases(80),
429 |((ty, datums) in datums)| {
430 let datums: Vec<_> = datums.iter().map(Datum::from).collect();
432 prop_assert_eq!(validate_stats(&ty, &datums[..]), Ok(()));
433 }
434 )
435 }
436
437 #[mz_ore::test]
443 #[cfg_attr(miri, ignore)] fn may_match_mfp_missing_err_stats_keeps_part() {
445 use mz_expr::{BinaryFunc, EvalError, MirScalarExpr, func};
446 use mz_repr::ReprScalarType;
447
448 use crate::errors::DataflowError;
449
450 let schema = RelationDesc::builder()
451 .with_column("col", SqlScalarType::Int32.nullable(false))
452 .finish();
453 let mut builder = PartBuilder::new(&schema, &UnitSchema);
454 builder.push(
455 &SourceData(Ok(Row::pack_slice(&[Datum::Int32(1)]))),
456 &(),
457 1u64,
458 1i64,
459 );
460 builder.push(
461 &SourceData(Err(DataflowError::from(EvalError::DivisionByZero))),
462 &(),
463 1u64,
464 1i64,
465 );
466 let part = builder.finish();
467 let key_col = part.key.as_struct();
468 let decoder = <RelationDesc as Schema<SourceData>>::decoder(&schema, key_col.clone())
469 .expect("success");
470 let mut key_stats = decoder.stats();
471 key_stats.cols.remove("err").expect("err stats present");
473
474 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
475 let stats = RelationPartStats {
476 name: "test",
477 metrics: &metrics,
478 stats: &PartStats { key: key_stats },
479 desc: &schema,
480 };
481 let mfp = MapFilterProject::new(1).filter(std::iter::once(MirScalarExpr::CallBinary {
484 func: BinaryFunc::Eq(func::Eq),
485 expr1: Box::new(MirScalarExpr::column(0)),
486 expr2: Box::new(MirScalarExpr::literal_ok(
487 Datum::Int32(999),
488 ReprScalarType::Int32,
489 )),
490 }));
491 assert!(stats.may_match_mfp(ResultSpec::anything(), &mfp));
492 }
493
494 #[mz_ore::test]
498 #[cfg_attr(miri, ignore)] fn malformed_err_stats_fail_open() {
500 use mz_persist_types::stats::{ColumnNullStats, ColumnarStats, PrimitiveStats};
501
502 let schema = RelationDesc::builder()
503 .with_column("col", SqlScalarType::Int32.nullable(false))
504 .finish();
505 let mut builder = PartBuilder::new(&schema, &UnitSchema);
506 builder.push(
507 &SourceData(Ok(Row::pack_slice(&[Datum::Int32(1)]))),
508 &(),
509 1u64,
510 1i64,
511 );
512 let part = builder.finish();
513 let key_col = part.key.as_struct();
514 let decoder = <RelationDesc as Schema<SourceData>>::decoder(&schema, key_col.clone())
515 .expect("success");
516 let mut key_stats = decoder.stats();
517 key_stats.cols.insert(
519 "err".to_string(),
520 ColumnarStats {
521 nulls: Some(ColumnNullStats { count: 0 }),
522 values: PrimitiveStats {
523 lower: 0i32,
524 upper: 0i32,
525 }
526 .into(),
527 },
528 );
529
530 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
531 let stats = RelationPartStats {
532 name: "test",
533 metrics: &metrics,
534 stats: &PartStats { key: key_stats },
535 desc: &schema,
536 };
537 assert_eq!(stats.ok_count(), None);
538 assert_eq!(stats.err_count(), None);
539
540 let mut key_stats = decoder.stats();
543 match key_stats.cols.get_mut("err") {
544 Some(err_stats) => match &mut err_stats.values {
545 ColumnStatKinds::Bytes(BytesStats::Primitive(_)) => {
546 err_stats.nulls = Some(mz_persist_types::stats::ColumnNullStats {
547 count: key_stats.len + 1,
548 });
549 }
550 other => panic!("unexpected err stats {other:?}"),
551 },
552 None => panic!("err stats missing"),
553 }
554 let stats = RelationPartStats {
555 name: "test",
556 metrics: &metrics,
557 stats: &PartStats { key: key_stats },
558 desc: &schema,
559 };
560 assert_eq!(stats.err_count(), None);
561 }
562
563 #[mz_ore::test]
568 #[cfg_attr(miri, ignore)] fn malformed_ok_stats_fail_open() {
570 use mz_expr::{BinaryFunc, MirScalarExpr, func};
571 use mz_persist_types::stats::{ColumnNullStats, ColumnarStats, PrimitiveStats};
572 use mz_repr::ReprScalarType;
573
574 let schema = RelationDesc::builder()
575 .with_column("col", SqlScalarType::Int32.nullable(false))
576 .with_column("json", SqlScalarType::Jsonb.nullable(true))
577 .finish();
578 let mut builder = PartBuilder::new(&schema, &UnitSchema);
579 builder.push(
580 &SourceData(Ok(Row::pack_slice(&[Datum::Int32(1), Datum::JsonNull]))),
581 &(),
582 1u64,
583 1i64,
584 );
585 let part = builder.finish();
586 let key_col = part.key.as_struct();
587 let decoder = <RelationDesc as Schema<SourceData>>::decoder(&schema, key_col.clone())
588 .expect("success");
589
590 let json_idx = schema
591 .iter_all()
592 .map(|(idx, _name, _typ)| idx)
593 .nth(1)
594 .expect("two columns");
595 let mfp = MapFilterProject::new(2).filter(std::iter::once(MirScalarExpr::CallBinary {
598 func: BinaryFunc::Eq(func::Eq),
599 expr1: Box::new(MirScalarExpr::column(0)),
600 expr2: Box::new(MirScalarExpr::literal_ok(
601 Datum::Int32(999),
602 ReprScalarType::Int32,
603 )),
604 }));
605
606 let metrics = PartStatsMetrics::new(&MetricsRegistry::new());
607 let arena = RowArena::new();
608 let well_shaped = PartStats {
609 key: decoder.stats(),
610 };
611 let well_shaped = RelationPartStats::new("test", &metrics, &schema, &well_shaped);
612 assert!(!well_shaped.may_match_mfp(ResultSpec::anything(), &mfp));
613
614 let mut not_a_struct = decoder.stats();
617 not_a_struct.cols.insert(
618 "ok".to_string(),
619 ColumnarStats {
620 nulls: Some(ColumnNullStats { count: 0 }),
621 values: PrimitiveStats {
622 lower: 0i32,
623 upper: 0i32,
624 }
625 .into(),
626 },
627 );
628 let not_a_struct = PartStats { key: not_a_struct };
629 let not_a_struct = RelationPartStats::new("test", &metrics, &schema, ¬_a_struct);
630 for (idx, _name, _typ) in schema.iter_all() {
631 assert_eq!(not_a_struct.col_stats(idx, &arena), ResultSpec::anything());
632 }
633 assert!(not_a_struct.may_match_mfp(ResultSpec::anything(), &mfp));
634
635 let mut not_nullable = decoder.stats();
639 match not_nullable.cols.get_mut("ok") {
640 Some(ok_stats) => ok_stats.nulls = None,
641 None => panic!("ok stats missing"),
642 }
643 let not_nullable = PartStats { key: not_nullable };
644 let not_nullable = RelationPartStats::new("test", &metrics, &schema, ¬_nullable);
645 let other_json = Datum::String("a");
646 assert!(
647 !well_shaped
648 .col_stats(json_idx, &arena)
649 .may_contain(other_json)
650 );
651 assert!(
652 not_nullable
653 .col_stats(json_idx, &arena)
654 .may_contain(other_json)
655 );
656 }
657
658 #[mz_ore::test]
659 #[ignore] fn statistics_stability() {
661 const RNG_SEED: [u8; 32] = [
664 0xf4, 0x16, 0x16, 0x48, 0xc3, 0xac, 0x77, 0xac, 0x72, 0x20, 0x0b, 0xea, 0x99, 0x67,
665 0x2d, 0x6d, 0xca, 0x9f, 0x76, 0xaf, 0x1b, 0x09, 0x73, 0xa0, 0x59, 0x22, 0x6d, 0xc5,
666 0x46, 0x39, 0x1c, 0x4a,
667 ];
668
669 let rng = proptest::test_runner::TestRng::from_seed(
670 proptest::test_runner::RngAlgorithm::ChaCha,
671 &RNG_SEED,
672 );
673 let config = proptest::test_runner::Config {
675 cases: u32::MAX,
677 rng_algorithm: proptest::test_runner::RngAlgorithm::ChaCha,
678 ..Default::default()
679 };
680 let mut runner = proptest::test_runner::TestRunner::new_with_rng(config, rng);
681
682 let max_cols = 4;
683 let max_rows = 8;
684 let test_cases = 1000;
685
686 let strat = proptest::collection::vec(any::<SqlColumnType>(), 1..max_cols)
689 .prop_map(|cols| {
690 let col_names = (0..cols.len()).map(|i| i.to_string());
691 RelationDesc::new(SqlRelationType::new(cols), col_names)
692 })
693 .prop_flat_map(|desc| {
694 let rows = desc
695 .typ()
696 .columns()
697 .iter()
698 .cloned()
699 .map(arb_datum_for_column)
700 .collect::<Vec<_>>()
701 .prop_map(|datums| Row::pack(datums.iter().map(Datum::from)));
702 proptest::collection::vec(rows, 1..max_rows)
703 .prop_map(move |rows| (desc.clone(), rows))
704 });
705
706 let mut all_stats = Vec::new();
707 for _ in 0..test_cases {
708 let value_tree = strat.new_tree(&mut runner).unwrap();
709 let (desc, rows) = value_tree.current();
710
711 let mut builder = PartBuilder::new(&desc, &UnitSchema);
712 for row in &rows {
713 builder.push(&SourceData(Ok(row.clone())), &(), 1u64, 1i64);
714 }
715 let part = builder.finish();
716
717 let key_col = part.key.as_struct();
718 let decoder = <RelationDesc as Schema<SourceData>>::decoder(&desc, key_col.clone())
719 .expect("success");
720 let key_stats = decoder.stats();
721
722 all_stats.push(key_stats);
723 }
724
725 insta::assert_json_snapshot!(all_stats);
726 }
727}