1use std::collections::HashSet;
19
20use arrow::array::Array;
21use arrow::datatypes::DataType;
22use arrow::record_batch::RecordBatch;
23
24use crate::catalog::ColumnStatistics;
25
26pub const EXACT_NDV_CAP: usize = 1_000_000;
34
35pub fn analyze_batch(batch: &RecordBatch) -> ColumnStatistics {
43 analyze_record_batches(std::iter::once(batch))
44}
45
46pub fn analyze_record_batches<'a, I>(batches: I) -> ColumnStatistics
53where
54 I: IntoIterator<Item = &'a RecordBatch>,
55{
56 let mut row_count: u64 = 0;
57 let mut null_count: u64 = 0;
58 let mut min_value: Option<Extremum> = None;
59 let mut max_value: Option<Extremum> = None;
60 let mut distinct: HashSet<String> = HashSet::new();
61 let mut hit_cap = false;
62
63 for batch in batches {
64 row_count = row_count.saturating_add(batch.num_rows() as u64);
65 for col_idx in 0..batch.num_columns() {
71 let array = batch.column(col_idx);
72 null_count = null_count.saturating_add(array.null_count() as u64);
73 if let Some((batch_min, batch_max)) = min_max_string(array) {
74 update_min(&mut min_value, batch_min);
75 update_max(&mut max_value, batch_max);
76 }
77 if !hit_cap {
78 for value in string_values(array) {
79 if distinct.len() >= EXACT_NDV_CAP {
80 hit_cap = true;
81 distinct.clear();
82 break;
83 }
84 distinct.insert(value);
85 }
86 }
87 }
88 }
89
90 let now_secs = std::time::SystemTime::now()
91 .duration_since(std::time::UNIX_EPOCH)
92 .map(|d| d.as_secs())
93 .unwrap_or(0);
94
95 let mut stats = ColumnStatistics::new()
96 .with_row_count(row_count)
97 .with_null_count(null_count)
98 .with_collected_at_secs(now_secs);
99 if let Some(m) = min_value {
100 stats = stats.with_min(m.text);
101 }
102 if let Some(m) = max_value {
103 stats = stats.with_max(m.text);
104 }
105 if !hit_cap {
106 stats = stats.with_distinct_count(distinct.len() as u64);
107 }
108 stats
109}
110
111pub fn analyze_batch_per_column(batch: &RecordBatch) -> Vec<ColumnStatistics> {
116 let now_secs = std::time::SystemTime::now()
117 .duration_since(std::time::UNIX_EPOCH)
118 .map(|d| d.as_secs())
119 .unwrap_or(0);
120 (0..batch.num_columns())
121 .map(|col_idx| {
122 let array = batch.column(col_idx);
123 let mut stats = ColumnStatistics::new()
124 .with_row_count(batch.num_rows() as u64)
125 .with_null_count(array.null_count() as u64)
126 .with_collected_at_secs(now_secs);
127 if let Some((min, max)) = min_max_string(array) {
128 stats = stats.with_min(min.text).with_max(max.text);
129 }
130 if array.len() <= EXACT_NDV_CAP {
131 let distinct: HashSet<String> = string_values(array).collect();
132 stats = stats.with_distinct_count(distinct.len() as u64);
133 }
134 stats
135 })
136 .collect()
137}
138
139#[derive(Clone)]
148struct Extremum {
149 key: Option<Vec<u8>>,
150 text: String,
151 dt: DataType,
152}
153
154fn less(a: &Extremum, b: &Extremum) -> bool {
161 match (&a.key, &b.key) {
162 (Some(ak), Some(bk)) if a.dt == b.dt => ak < bk,
163 _ => a.text < b.text,
164 }
165}
166
167fn update_min(slot: &mut Option<Extremum>, candidate: Extremum) {
168 match slot {
169 Some(existing) if !less(&candidate, existing) => {}
170 _ => *slot = Some(candidate),
171 }
172}
173
174fn update_max(slot: &mut Option<Extremum>, candidate: Extremum) {
175 match slot {
176 Some(existing) if !less(existing, &candidate) => {}
177 _ => *slot = Some(candidate),
178 }
179}
180
181fn sort_keys(array: &dyn Array) -> Option<Vec<Vec<u8>>> {
186 use arrow::row::{RowConverter, SortField};
187 let field = SortField::new(array.data_type().clone());
188 let converter = RowConverter::new(vec![field]).ok()?;
189 let rows = converter
190 .convert_columns(&[arrow::array::make_array(array.to_data())])
191 .ok()?;
192 Some((0..rows.num_rows()).map(|i| rows.row(i).as_ref().to_vec()).collect())
193}
194
195fn value_texts(array: &dyn Array) -> Vec<Option<String>> {
204 use arrow::util::display::{ArrayFormatter, FormatOptions};
205 match ArrayFormatter::try_new(array, &FormatOptions::default()) {
206 Ok(formatter) => (0..array.len())
207 .map(|i| {
208 if array.is_null(i) {
209 None
210 } else {
211 Some(formatter.value(i).to_string())
212 }
213 })
214 .collect(),
215 Err(_) => vec![None; array.len()],
218 }
219}
220
221fn min_max_string(array: &dyn Array) -> Option<(Extremum, Extremum)> {
223 let mut min_v: Option<Extremum> = None;
224 let mut max_v: Option<Extremum> = None;
225 for value in extrema_candidates(array) {
226 update_min(&mut min_v, value.clone());
227 update_max(&mut max_v, value);
228 }
229 match (min_v, max_v) {
230 (Some(lo), Some(hi)) => Some((lo, hi)),
231 _ => None,
232 }
233}
234
235fn extrema_candidates(array: &dyn Array) -> Vec<Extremum> {
237 let keys = sort_keys(array);
238 let dt = array.data_type().clone();
239 value_texts(array)
240 .into_iter()
241 .enumerate()
242 .filter_map(|(i, text)| {
243 text.map(|text| Extremum {
244 key: keys.as_ref().and_then(|k| k.get(i).cloned()),
245 text,
246 dt: dt.clone(),
247 })
248 })
249 .collect()
250}
251
252fn string_values(array: &dyn Array) -> impl Iterator<Item = String> + '_ {
254 value_texts(array).into_iter().flatten()
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use arrow::array::{Int32Array, StringArray};
261 use arrow::datatypes::{Field, Schema};
262 use std::sync::Arc;
263
264 fn batch_int(values: Vec<Option<i32>>) -> RecordBatch {
265 let schema = Arc::new(Schema::new(vec![Field::new("k", DataType::Int32, true)]));
266 RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(values))]).unwrap()
267 }
268
269 fn batch_str(values: Vec<Option<&str>>) -> RecordBatch {
270 let schema = Arc::new(Schema::new(vec![Field::new("name", DataType::Utf8, true)]));
271 RecordBatch::try_new(schema, vec![Arc::new(StringArray::from(values))]).unwrap()
272 }
273
274 #[test]
275 fn analyze_batch_records_row_and_null_counts() {
276 let batch = batch_int(vec![Some(1), None, Some(2), Some(3)]);
277 let stats = analyze_batch(&batch);
278 assert_eq!(stats.row_count, Some(4));
279 assert_eq!(stats.null_count, Some(1));
280 }
281
282 #[test]
283 fn analyze_batch_records_min_and_max_stringified() {
284 let batch = batch_int(vec![Some(3), Some(1), Some(2)]);
285 let stats = analyze_batch(&batch);
286 assert_eq!(stats.min_value.as_deref(), Some("1"));
287 assert_eq!(stats.max_value.as_deref(), Some("3"));
288 }
289
290 #[test]
291 fn analyze_batch_counts_distinct_values() {
292 let batch = batch_int(vec![Some(1), Some(1), Some(2), Some(3)]);
293 let stats = analyze_batch(&batch);
294 assert_eq!(stats.distinct_count, Some(3));
295 }
296
297 #[test]
298 fn analyze_batch_handles_all_nulls() {
299 let batch = batch_int(vec![None, None]);
300 let stats = analyze_batch(&batch);
301 assert_eq!(stats.row_count, Some(2));
302 assert_eq!(stats.null_count, Some(2));
303 assert_eq!(stats.min_value, None);
304 assert_eq!(stats.distinct_count, Some(0));
305 }
306
307 #[test]
308 fn analyze_batch_works_on_string_columns() {
309 let batch = batch_str(vec![Some("b"), Some("a"), Some("a")]);
310 let stats = analyze_batch(&batch);
311 assert_eq!(stats.row_count, Some(3));
312 assert_eq!(stats.distinct_count, Some(2));
313 assert_eq!(stats.min_value.as_deref(), Some("a"));
314 assert_eq!(stats.max_value.as_deref(), Some("b"));
315 }
316
317 #[test]
318 fn analyze_record_batches_aggregates_across_batches() {
319 let b1 = batch_int(vec![Some(1), Some(2)]);
320 let b2 = batch_int(vec![Some(3), None, Some(2)]);
321 let stats = analyze_record_batches([&b1, &b2]);
322 assert_eq!(stats.row_count, Some(5));
323 assert_eq!(stats.null_count, Some(1));
324 assert_eq!(stats.distinct_count, Some(3));
325 assert_eq!(stats.min_value.as_deref(), Some("1"));
326 assert_eq!(stats.max_value.as_deref(), Some("3"));
327 }
328
329 #[test]
330 fn analyze_batch_per_column_returns_one_entry_per_field() {
331 let schema = Arc::new(Schema::new(vec![
332 Field::new("k", DataType::Int32, true),
333 Field::new("v", DataType::Utf8, true),
334 ]));
335 let batch = RecordBatch::try_new(
336 schema,
337 vec![
338 Arc::new(Int32Array::from(vec![Some(1), Some(1), Some(2)])),
339 Arc::new(StringArray::from(vec![Some("a"), Some("b"), Some("a")])),
340 ],
341 )
342 .unwrap();
343 let per_col = analyze_batch_per_column(&batch);
344 assert_eq!(per_col.len(), 2);
345 assert_eq!(per_col[0].row_count, Some(3));
346 assert_eq!(per_col[0].distinct_count, Some(2));
347 assert_eq!(per_col[1].distinct_count, Some(2));
348 }
349
350 #[test]
357 fn min_and_max_order_numerically_not_lexicographically() {
358 let batch = batch_int(vec![Some(9), Some(10), Some(100), Some(2)]);
359 let stats = analyze_batch(&batch);
360 assert_eq!(stats.min_value.as_deref(), Some("2"));
361 assert_eq!(stats.max_value.as_deref(), Some("100"));
362 }
363
364 #[test]
366 fn min_and_max_handle_negative_numbers() {
367 let batch = batch_int(vec![Some(5), Some(-40), Some(-3)]);
368 let stats = analyze_batch(&batch);
369 assert_eq!(stats.min_value.as_deref(), Some("-40"));
370 assert_eq!(stats.max_value.as_deref(), Some("5"));
371 }
372
373 #[test]
376 fn min_and_max_stay_numeric_across_batches() {
377 let b1 = batch_int(vec![Some(9)]);
378 let b2 = batch_int(vec![Some(10)]);
379 let stats = analyze_record_batches([&b1, &b2]);
380 assert_eq!(stats.min_value.as_deref(), Some("9"));
381 assert_eq!(stats.max_value.as_deref(), Some("10"));
382 }
383
384 #[test]
389 fn a_date_column_produces_real_values_not_debug_blobs() {
390 use arrow::array::Date32Array;
391 let schema = Arc::new(Schema::new(vec![Field::new("d", DataType::Date32, true)]));
392 let batch = RecordBatch::try_new(
393 schema,
394 vec![Arc::new(Date32Array::from(vec![Some(19000), Some(18000)]))],
395 )
396 .unwrap();
397 let stats = analyze_batch(&batch);
398 let min = stats.min_value.expect("a date column must produce a min");
399 assert!(
400 !min.contains('\n') && !min.contains("PrimitiveArray"),
401 "min is a Debug blob, not a value: {min:?}"
402 );
403 assert_eq!(min, "2019-04-14");
404 assert_eq!(stats.distinct_count, Some(2));
405 }
406
407 #[test]
409 fn a_decimal_column_orders_by_value() {
410 use arrow::array::Decimal128Array;
411 let array = Decimal128Array::from(vec![Some(925i128), Some(1050), Some(30)])
412 .with_precision_and_scale(10, 2)
413 .unwrap();
414 let schema = Arc::new(Schema::new(vec![Field::new(
415 "p",
416 DataType::Decimal128(10, 2),
417 true,
418 )]));
419 let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
420 let stats = analyze_batch(&batch);
421 assert_eq!(stats.min_value.as_deref(), Some("0.30"));
422 assert_eq!(stats.max_value.as_deref(), Some("10.50"));
423 assert_eq!(stats.distinct_count, Some(3));
424 }
425
426 #[test]
427 fn column_statistics_equality_selectivity_uses_ndv() {
428 let s = ColumnStatistics::new().with_distinct_count(10);
429 let sel = s.equality_selectivity().unwrap();
430 assert!((sel - 0.1).abs() < 1e-9);
431 }
432
433 #[test]
434 fn column_statistics_equality_selectivity_handles_zero_ndv() {
435 let s = ColumnStatistics::new().with_distinct_count(0);
436 assert_eq!(s.equality_selectivity(), Some(0.0));
437 }
438
439 #[test]
440 fn column_statistics_equality_selectivity_returns_none_without_ndv() {
441 let s = ColumnStatistics::new();
442 assert_eq!(s.equality_selectivity(), None);
443 }
444
445 #[test]
446 fn column_statistics_freshness_with_no_timestamp_is_fresh() {
447 let s = ColumnStatistics::new();
448 assert!(s.is_fresh(1_000, 60));
449 }
450
451 #[test]
452 fn column_statistics_freshness_detects_stale_stats() {
453 let s = ColumnStatistics::new().with_collected_at_secs(100);
454 assert!(!s.is_fresh(200, 60));
456 assert!(s.is_fresh(200, 200));
458 }
459}