1use std::collections::HashSet;
22
23use crate::error::Result;
24use crate::unified::{ColumnEncoder, EncodedBlock};
25
26const DEFAULT_SAMPLE_SIZE: usize = 1024;
28
29const NUM_STRATA: usize = 4;
31
32#[derive(Debug, Clone)]
39pub struct AdaptiveSelector {
40 pub sample_size: usize,
42}
43
44impl Default for AdaptiveSelector {
45 fn default() -> Self {
46 Self {
47 sample_size: DEFAULT_SAMPLE_SIZE,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum FloatPattern {
55 Constant,
57 Periodic,
59 SlowlyVarying,
61 Random,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum StringPattern {
68 LowCardinality,
70 HighCardinality,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum IntegerPattern {
77 Constant,
79 RegularInterval,
81 NarrowRange,
83 Irregular,
85}
86
87impl AdaptiveSelector {
88 #[must_use]
90 pub fn new() -> Self {
91 Self::default()
92 }
93
94 fn stratified_sample<T: Copy>(values: &[T], budget: usize) -> Vec<T> {
98 let n = values.len();
99 if n <= budget {
100 return values.to_vec();
101 }
102 let per_stratum = budget / NUM_STRATA;
103 let stride = n / NUM_STRATA;
104 let mut out = Vec::with_capacity(budget);
105 for s in 0..NUM_STRATA {
106 let start = s * stride;
107 let end = (start + per_stratum).min(n);
108 out.extend_from_slice(&values[start..end]);
109 }
110 out
111 }
112
113 #[must_use]
115 pub fn analyze_floats(&self, values: &[f64]) -> FloatPattern {
116 if values.is_empty() {
117 return FloatPattern::Random;
118 }
119
120 let sample = Self::stratified_sample(values, self.sample_size);
121
122 let first = sample[0].to_bits();
124 if sample.iter().all(|v| v.to_bits() == first) {
125 return FloatPattern::Constant;
126 }
127
128 let mut total_leading: u64 = 0;
130 let mut prev = first;
131 for &v in &sample[1..] {
132 let bits = v.to_bits();
133 let xor = prev ^ bits;
134 total_leading += if xor == 0 {
135 64
136 } else {
137 xor.leading_zeros() as u64
138 };
139 prev = bits;
140 }
141
142 #[allow(clippy::cast_precision_loss)]
143 let avg_leading = total_leading as f64 / (sample.len() - 1) as f64;
144
145 if sample.len() > 128 {
149 let mut hits: usize = 0;
150 for i in 128..sample.len() {
151 let bits = sample[i].to_bits();
152 let window = &sample[i.saturating_sub(128)..i];
153 if window.iter().any(|v| v.to_bits() == bits) {
154 hits += 1;
155 }
156 }
157 #[allow(clippy::cast_precision_loss)]
158 let hit_rate = hits as f64 / (sample.len() - 128) as f64;
159 if hit_rate > 0.3 {
160 return FloatPattern::Periodic;
161 }
162 }
163
164 if avg_leading >= 16.0 {
165 FloatPattern::SlowlyVarying
166 } else {
167 FloatPattern::Random
168 }
169 }
170
171 #[must_use]
173 pub fn analyze_strings(&self, values: &[&str]) -> StringPattern {
174 if values.is_empty() {
175 return StringPattern::HighCardinality;
176 }
177
178 let sample = Self::stratified_sample(values, self.sample_size);
179 let distinct: HashSet<&str> = sample.iter().copied().collect();
180
181 if distinct.len() <= (sample.len() / 10).max(1) {
183 StringPattern::LowCardinality
184 } else {
185 StringPattern::HighCardinality
186 }
187 }
188
189 #[must_use]
191 pub fn analyze_integers(&self, values: &[i64]) -> IntegerPattern {
192 if values.is_empty() {
193 return IntegerPattern::Irregular;
194 }
195
196 let sample = Self::stratified_sample(values, self.sample_size);
197
198 let first = sample[0];
200 if sample.iter().all(|&v| v == first) {
201 return IntegerPattern::Constant;
202 }
203
204 if sample.len() >= 3 {
206 let interval = sample[1].wrapping_sub(sample[0]);
207 let is_regular = sample
208 .windows(2)
209 .all(|w| w[1].wrapping_sub(w[0]) == interval);
210 if is_regular {
211 return IntegerPattern::RegularInterval;
212 }
213 }
214
215 let min = sample.iter().copied().min().unwrap_or(0);
221 let max = sample.iter().copied().max().unwrap_or(0);
222 let range = (max as u128).wrapping_sub(min as u128);
223 if range <= u128::from(u16::MAX) {
224 return IntegerPattern::NarrowRange;
225 }
226
227 IntegerPattern::Irregular
228 }
229
230 pub fn encode_f64_adaptive(&self, values: &[f64]) -> Result<EncodedBlock> {
240 use crate::alp::AlpEncoder;
241 use crate::chimp::{Chimp128Encoder, ChimpEncoder};
242 use crate::gorilla::GorillaEncoder;
243 use crate::patas::PatasEncoder;
244 use crate::plain::PlainEncoder;
245 use crate::unified::{EncodingType, should_use_specialized};
246
247 let pattern = self.analyze_floats(values);
248
249 if matches!(pattern, FloatPattern::Constant) {
253 return self.encode_f64_direct(values, pattern);
254 }
255
256 if values.len() <= self.sample_size {
258 return self.encode_f64_direct(values, pattern);
259 }
260
261 let sample = Self::stratified_sample(values, self.sample_size);
263 let sample_raw_size = sample.len() * 8;
264
265 let candidates: &[EncodingType] = match pattern {
267 FloatPattern::Constant => &[
268 EncodingType::Rle,
269 EncodingType::Chimp,
270 EncodingType::Gorilla,
271 ],
272 FloatPattern::Periodic => &[
278 EncodingType::Pco,
279 EncodingType::Alp,
280 EncodingType::Chimp128,
281 EncodingType::Patas,
282 EncodingType::Chimp,
283 EncodingType::Gorilla,
284 ],
285 FloatPattern::SlowlyVarying => &[
286 EncodingType::Pco,
287 EncodingType::Alp,
288 EncodingType::Patas,
289 EncodingType::Chimp128,
290 EncodingType::Chimp,
291 EncodingType::Gorilla,
292 ],
293 FloatPattern::Random => &[
294 EncodingType::Pco,
295 EncodingType::Alp,
296 EncodingType::Gorilla,
297 EncodingType::Patas,
298 ],
299 };
300
301 let mut best: Option<(EncodingType, usize)> = None;
304 let mut best_any: Option<(EncodingType, usize)> = None;
305 for &enc in candidates {
306 let trial = match enc {
307 EncodingType::Rle => crate::rle::RleEncoder::encode_f64(&sample),
308 EncodingType::Pco => crate::pco::PcoEncoder::encode_f64(&sample),
309 EncodingType::Alp => AlpEncoder::encode(&sample),
310 EncodingType::Chimp128 => Chimp128Encoder::encode(&sample),
311 EncodingType::Patas => PatasEncoder::encode(&sample),
312 EncodingType::Chimp => ChimpEncoder::encode(&sample),
313 EncodingType::Gorilla => GorillaEncoder::encode(&sample),
314 _ => continue,
315 };
316 if let Ok(payload) = trial {
317 let len = payload.len();
318 if best_any.as_ref().is_none_or(|(_, bl)| len < *bl) {
320 best_any = Some((enc, len));
321 }
322 if should_use_specialized(&payload, sample_raw_size)
323 && best.as_ref().is_none_or(|(_, best_len)| len < *best_len)
324 {
325 best = Some((enc, len));
326 }
327 }
328 }
329
330 let winner = best
335 .or(best_any)
336 .map_or(EncodingType::PlainF64, |(enc, _)| enc);
337 let payload = match winner {
338 EncodingType::Rle => crate::rle::RleEncoder::encode_f64(values)?,
339 EncodingType::Pco => crate::pco::PcoEncoder::encode_f64(values)?,
340 EncodingType::Alp => AlpEncoder::encode(values)?,
341 EncodingType::Chimp128 => Chimp128Encoder::encode(values)?,
342 EncodingType::Patas => PatasEncoder::encode(values)?,
343 EncodingType::Chimp => ChimpEncoder::encode(values)?,
344 EncodingType::Gorilla => GorillaEncoder::encode(values)?,
345 _ => PlainEncoder::encode_f64(values)?,
346 };
347
348 let full_raw_size = values.len() * 8;
351 if winner != EncodingType::PlainF64 && !should_use_specialized(&payload, full_raw_size) {
352 return Ok(EncodedBlock {
353 encoding: EncodingType::PlainF64,
354 payload: PlainEncoder::encode_f64(values)?,
355 });
356 }
357
358 Ok(EncodedBlock {
359 encoding: winner,
360 payload,
361 })
362 }
363
364 fn encode_f64_direct(&self, values: &[f64], pattern: FloatPattern) -> Result<EncodedBlock> {
366 use crate::alp::AlpEncoder;
367 use crate::chimp::{Chimp128Encoder, ChimpEncoder};
368 use crate::gorilla::GorillaEncoder;
369 use crate::patas::PatasEncoder;
370 use crate::plain::PlainEncoder;
371 use crate::unified::{EncodedBlock, EncodingType, should_use_specialized};
372
373 let raw_size = values.len() * 8;
374 match pattern {
375 FloatPattern::Constant => {
376 let rle_payload = crate::rle::RleEncoder::encode_f64(values)?;
377 if should_use_specialized(&rle_payload, raw_size) {
378 return Ok(EncodedBlock {
379 encoding: EncodingType::Rle,
380 payload: rle_payload,
381 });
382 }
383 }
384 FloatPattern::Periodic | FloatPattern::SlowlyVarying => {
385 let pco_payload = crate::pco::PcoEncoder::encode_f64(values)?;
386 if should_use_specialized(&pco_payload, raw_size) {
387 return Ok(EncodedBlock {
388 encoding: EncodingType::Pco,
389 payload: pco_payload,
390 });
391 }
392 let chimp128_payload = Chimp128Encoder::encode(values)?;
393 if should_use_specialized(&chimp128_payload, raw_size) {
394 return Ok(EncodedBlock {
395 encoding: EncodingType::Chimp128,
396 payload: chimp128_payload,
397 });
398 }
399 let patas_payload = PatasEncoder::encode(values)?;
401 if should_use_specialized(&patas_payload, raw_size) {
402 return Ok(EncodedBlock {
403 encoding: EncodingType::Patas,
404 payload: patas_payload,
405 });
406 }
407 }
408 FloatPattern::Random => {}
409 }
410
411 let pco_payload = crate::pco::PcoEncoder::encode_f64(values)?;
414 if should_use_specialized(&pco_payload, raw_size) {
415 return Ok(EncodedBlock {
416 encoding: EncodingType::Pco,
417 payload: pco_payload,
418 });
419 }
420 let alp_payload = AlpEncoder::encode(values)?;
421 if should_use_specialized(&alp_payload, raw_size) {
422 return Ok(EncodedBlock {
423 encoding: EncodingType::Alp,
424 payload: alp_payload,
425 });
426 }
427 let chimp_payload = ChimpEncoder::encode(values)?;
428 if should_use_specialized(&chimp_payload, raw_size) {
429 return Ok(EncodedBlock {
430 encoding: EncodingType::Chimp,
431 payload: chimp_payload,
432 });
433 }
434 let patas_payload = PatasEncoder::encode(values)?;
435 if should_use_specialized(&patas_payload, raw_size) {
436 return Ok(EncodedBlock {
437 encoding: EncodingType::Patas,
438 payload: patas_payload,
439 });
440 }
441 let gorilla_payload = GorillaEncoder::encode(values)?;
442 if should_use_specialized(&gorilla_payload, raw_size) {
443 return Ok(EncodedBlock {
444 encoding: EncodingType::Gorilla,
445 payload: gorilla_payload,
446 });
447 }
448 Ok(EncodedBlock {
449 encoding: EncodingType::PlainF64,
450 payload: PlainEncoder::encode_f64(values)?,
451 })
452 }
453
454 pub fn encode_string_adaptive(&self, values: &[&str]) -> Result<EncodedBlock> {
460 let pattern = self.analyze_strings(values);
463 match pattern {
464 StringPattern::LowCardinality => ColumnEncoder::encode_string(values),
465 StringPattern::HighCardinality => {
466 ColumnEncoder::encode_string(values)
468 }
469 }
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use crate::unified::EncodingType;
477
478 #[test]
479 fn detects_constant_floats() {
480 let selector = AdaptiveSelector::new();
481 let values = vec![42.0_f64; 500];
482 assert_eq!(selector.analyze_floats(&values), FloatPattern::Constant);
483 }
484
485 #[test]
486 fn detects_slowly_varying_floats() {
487 let selector = AdaptiveSelector::new();
488 let values: Vec<f64> = (0..500).map(|i| 50.0 + (i as f64) * 0.01).collect();
489 assert_eq!(
490 selector.analyze_floats(&values),
491 FloatPattern::SlowlyVarying
492 );
493 }
494
495 #[test]
496 fn detects_random_floats() {
497 let selector = AdaptiveSelector::new();
498 let values: Vec<f64> = (0..500)
500 .map(|i| (i as f64 * 123.456).sin() * 1e15)
501 .collect();
502 assert_eq!(selector.analyze_floats(&values), FloatPattern::Random);
503 }
504
505 #[test]
506 fn encode_f64_adaptive_selects_pco_for_slowly_varying_decimals() {
507 let selector = AdaptiveSelector::new();
508 let values: Vec<f64> = (0..500).map(|i| 50.0 + (i as f64) * 0.01).collect();
511 let block = selector.encode_f64_adaptive(&values).unwrap();
512 assert_eq!(
513 block.encoding,
514 EncodingType::Pco,
515 "decimal data should select pco over ALP and the XOR codecs, got {}",
516 block.encoding,
517 );
518 let decoded = crate::unified::ColumnDecoder::decode(&block).unwrap();
520 if let crate::unified::DecodedColumn::F64(vals) = decoded {
521 assert_eq!(vals.len(), values.len());
522 for (a, b) in values.iter().zip(vals.iter()) {
523 assert_eq!(a.to_bits(), b.to_bits());
524 }
525 } else {
526 panic!("Expected F64 decoded column");
527 }
528 }
529
530 #[test]
531 fn detects_low_cardinality_strings() {
532 let selector = AdaptiveSelector::new();
533 let values = vec!["us-east-1"; 100];
534 assert_eq!(
535 selector.analyze_strings(&values),
536 StringPattern::LowCardinality
537 );
538 }
539
540 #[test]
541 fn detects_high_cardinality_strings() {
542 let selector = AdaptiveSelector::new();
543 let owned: Vec<String> = (0..100).map(|i| format!("uuid-{i}")).collect();
544 let refs: Vec<&str> = owned.iter().map(String::as_str).collect();
545 assert_eq!(
546 selector.analyze_strings(&refs),
547 StringPattern::HighCardinality
548 );
549 }
550
551 #[test]
552 fn detects_constant_integers() {
553 let selector = AdaptiveSelector::new();
554 let values = vec![42_i64; 500];
555 assert_eq!(selector.analyze_integers(&values), IntegerPattern::Constant);
556 }
557
558 #[test]
559 fn detects_regular_interval_integers() {
560 let selector = AdaptiveSelector::new();
561 let values: Vec<i64> = (0..500).map(|i| 1000 + i * 10).collect();
562 assert_eq!(
563 selector.analyze_integers(&values),
564 IntegerPattern::RegularInterval
565 );
566 }
567
568 #[test]
569 fn detects_irregular_integers() {
570 let selector = AdaptiveSelector::new();
571 let values = vec![1, 100, 3, 999, 42, 7, 88, 100_000];
573 assert_eq!(
574 selector.analyze_integers(&values),
575 IntegerPattern::Irregular
576 );
577 }
578
579 #[test]
580 fn detects_narrow_range_integers() {
581 let selector = AdaptiveSelector::new();
582 let values = vec![200, 201, 204, 301, 302, 400, 404, 500, 502, 503];
584 assert_eq!(
585 selector.analyze_integers(&values),
586 IntegerPattern::NarrowRange
587 );
588 }
589
590 #[test]
591 fn periodic_data_dispatches_pco() {
592 let selector = AdaptiveSelector::new();
593 let base: Vec<f64> = (0..64).map(|i| 20.0 + (i as f64) * 0.5).collect();
596 let values: Vec<f64> = base.iter().cycle().take(5000).copied().collect();
597
598 assert_eq!(selector.analyze_floats(&values), FloatPattern::Periodic);
599
600 let block = selector.encode_f64_adaptive(&values).unwrap();
601 assert_eq!(
602 block.encoding,
603 EncodingType::Pco,
604 "Periodic data should use pco, got {}",
605 block.encoding,
606 );
607
608 let decoded = crate::unified::ColumnDecoder::decode(&block).unwrap();
610 if let crate::unified::DecodedColumn::F64(vals) = decoded {
611 assert_eq!(vals.len(), values.len());
612 for (a, b) in values.iter().zip(vals.iter()) {
613 assert_eq!(a.to_bits(), b.to_bits());
614 }
615 } else {
616 panic!("Expected F64 decoded column");
617 }
618 }
619
620 #[test]
621 fn slowly_varying_picks_a_specialised_float_codec() {
622 let selector = AdaptiveSelector::new();
623 let values: Vec<f64> = (0..5000).map(|i| 50.0 + (i as f64) * 0.001).collect();
626
627 let block = selector.encode_f64_adaptive(&values).unwrap();
628 assert!(
629 block.encoding == EncodingType::Pco
630 || block.encoding == EncodingType::Alp
631 || block.encoding == EncodingType::Chimp128
632 || block.encoding == EncodingType::Chimp
633 || block.encoding == EncodingType::Gorilla,
634 "Unexpected encoding: {}",
635 block.encoding,
636 );
637
638 let decoded = crate::unified::ColumnDecoder::decode(&block).unwrap();
640 if let crate::unified::DecodedColumn::F64(vals) = decoded {
641 assert_eq!(vals.len(), values.len());
642 } else {
643 panic!("Expected F64 decoded column");
644 }
645 }
646}
647
648#[cfg(test)]
649mod proptests {
650 use super::*;
651 use crate::unified::{ColumnDecoder, DecodedColumn};
652 use proptest::prelude::*;
653
654 proptest! {
655 #[test]
656 fn roundtrip_f64_adaptive(values in proptest::collection::vec(any::<f64>(), 50..300)) {
657 let selector = AdaptiveSelector::new();
658 let block = selector.encode_f64_adaptive(&values).unwrap();
659 let decoded = ColumnDecoder::decode(&block).unwrap();
660 match decoded {
661 DecodedColumn::F64(vals) => {
662 prop_assert_eq!(values.len(), vals.len());
663 for (a, b) in values.iter().zip(vals.iter()) {
664 prop_assert_eq!(a.to_bits(), b.to_bits());
665 }
666 }
667 other => prop_assert!(false, "Expected F64, got {other:?}"),
668 }
669 }
670
671 #[test]
672 fn roundtrip_string_adaptive(values in proptest::collection::vec("[a-z]{0,8}", 1..100)) {
673 let selector = AdaptiveSelector::new();
674 let refs: Vec<&str> = values.iter().map(String::as_str).collect();
675 let block = selector.encode_string_adaptive(&refs).unwrap();
676 let decoded = ColumnDecoder::decode(&block).unwrap();
677 match decoded {
678 DecodedColumn::String(vals) => {
679 prop_assert_eq!(&values, &vals);
680 }
681 other => prop_assert!(false, "Expected String, got {other:?}"),
682 }
683 }
684 }
685}