• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 16728097825

04 Aug 2025 04:00PM UTC coverage: 48.355% (-35.1%) from 83.429%
16728097825

Pull #4108

github

web-flow
Merge 1b2d27fd8 into 649ba9576
Pull Request #4108: perf[vortex-array]: use all_valid instead of `invalid_count() == 0`

1 of 1 new or added line in 1 file covered. (100.0%)

11596 existing lines in 378 files now uncovered.

18635 of 38538 relevant lines covered (48.35%)

151786.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.34
/vortex-btrblocks/src/integer.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
pub mod dictionary;
5
mod stats;
6

7
use std::fmt::Debug;
8
use std::hash::Hash;
9

10
pub use stats::IntegerStats;
11
use vortex_array::arrays::{ConstantArray, PrimitiveArray, PrimitiveVTable};
12
use vortex_array::compress::downscale_integer_array;
13
use vortex_array::{ArrayRef, IntoArray, ToCanonical};
14
use vortex_dict::DictArray;
15
use vortex_error::{VortexExpect, VortexResult, VortexUnwrap, vortex_bail, vortex_err};
16
use vortex_fastlanes::{FoRArray, bit_width_histogram, bitpack_encode, find_best_bit_width};
17
use vortex_runend::RunEndArray;
18
use vortex_runend::compress::runend_encode;
19
use vortex_scalar::Scalar;
20
use vortex_sequence::sequence_encode;
21
use vortex_sparse::{SparseArray, SparseVTable};
22
use vortex_zigzag::{ZigZagArray, zigzag_encode};
23

24
use crate::integer::dictionary::dictionary_encode;
25
use crate::patches::compress_patches;
26
use crate::{
27
    Compressor, CompressorStats, GenerateStatsOptions, Scheme,
28
    estimate_compression_ratio_with_sampling,
29
};
30

31
pub struct IntCompressor;
32

33
impl Compressor for IntCompressor {
34
    type ArrayVTable = PrimitiveVTable;
35
    type SchemeType = dyn IntegerScheme;
36
    type StatsType = IntegerStats;
37

38
    fn schemes() -> &'static [&'static dyn IntegerScheme] {
4,582✔
39
        &[
4,582✔
40
            &ConstantScheme,
4,582✔
41
            &FORScheme,
4,582✔
42
            &ZigZagScheme,
4,582✔
43
            &BitPackingScheme,
4,582✔
44
            &SparseScheme,
4,582✔
45
            &DictScheme,
4,582✔
46
            &RunEndScheme,
4,582✔
47
            &SequenceScheme,
4,582✔
48
        ]
4,582✔
49
    }
4,582✔
50

51
    fn default_scheme() -> &'static Self::SchemeType {
2,198✔
52
        &UncompressedScheme
2,198✔
53
    }
2,198✔
54

55
    fn dict_scheme_code() -> IntCode {
3,048✔
56
        DICT_SCHEME
3,048✔
57
    }
3,048✔
58
}
59

60
impl IntCompressor {
61
    pub fn compress_no_dict(
772✔
62
        array: &PrimitiveArray,
772✔
63
        is_sample: bool,
772✔
64
        allowed_cascading: usize,
772✔
65
        excludes: &[IntCode],
772✔
66
    ) -> VortexResult<ArrayRef> {
772✔
67
        let stats = IntegerStats::generate_opts(
772✔
68
            array,
772✔
69
            GenerateStatsOptions {
772✔
70
                count_distinct_values: false,
772✔
71
            },
772✔
72
        );
73

74
        let scheme = Self::choose_scheme(&stats, is_sample, allowed_cascading, excludes)?;
772✔
75
        let output = scheme.compress(&stats, is_sample, allowed_cascading, excludes)?;
772✔
76

77
        if output.nbytes() < array.nbytes() {
772✔
78
            Ok(output)
8✔
79
        } else {
80
            log::debug!("resulting tree too large: {}", output.display_tree());
764✔
81
            Ok(array.to_array())
764✔
82
        }
83
    }
772✔
84
}
85

86
pub trait IntegerScheme: Scheme<StatsType = IntegerStats, CodeType = IntCode> {}
87

88
// Auto-impl
89
impl<T> IntegerScheme for T where T: Scheme<StatsType = IntegerStats, CodeType = IntCode> {}
90

91
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
92
pub struct IntCode(u8);
93

94
const UNCOMPRESSED_SCHEME: IntCode = IntCode(0);
95
const CONSTANT_SCHEME: IntCode = IntCode(1);
96
const FOR_SCHEME: IntCode = IntCode(2);
97
const ZIGZAG_SCHEME: IntCode = IntCode(3);
98
const BITPACKING_SCHEME: IntCode = IntCode(4);
99
const SPARSE_SCHEME: IntCode = IntCode(5);
100
const DICT_SCHEME: IntCode = IntCode(6);
101
const RUNEND_SCHEME: IntCode = IntCode(7);
102
const SEQUENCE_SCHEME: IntCode = IntCode(8);
103

104
#[derive(Debug, Copy, Clone)]
105
pub struct UncompressedScheme;
106

107
#[derive(Debug, Copy, Clone)]
108
pub struct ConstantScheme;
109

110
#[derive(Debug, Copy, Clone)]
111
pub struct FORScheme;
112

113
#[derive(Debug, Copy, Clone)]
114
pub struct ZigZagScheme;
115

116
#[derive(Debug, Copy, Clone)]
117
pub struct BitPackingScheme;
118

119
#[derive(Debug, Copy, Clone)]
120
pub struct SparseScheme;
121

122
#[derive(Debug, Copy, Clone)]
123
pub struct DictScheme;
124

125
#[derive(Debug, Copy, Clone)]
126
pub struct RunEndScheme;
127

128
#[derive(Debug, Copy, Clone)]
129
pub struct SequenceScheme;
130

131
/// Threshold for the average run length in an array before we consider run-end encoding.
132
const RUN_END_THRESHOLD: u32 = 4;
133

134
impl Scheme for UncompressedScheme {
135
    type StatsType = IntegerStats;
136
    type CodeType = IntCode;
137

138
    fn code(&self) -> IntCode {
×
139
        UNCOMPRESSED_SCHEME
×
140
    }
×
141

142
    fn expected_compression_ratio(
×
143
        &self,
×
144
        _stats: &IntegerStats,
×
145
        _is_sample: bool,
×
146
        _allowed_cascading: usize,
×
147
        _excludes: &[IntCode],
×
148
    ) -> VortexResult<f64> {
×
149
        // no compression
150
        Ok(1.0)
×
151
    }
×
152

153
    fn compress(
2,198✔
154
        &self,
2,198✔
155
        stats: &IntegerStats,
2,198✔
156
        _is_sample: bool,
2,198✔
157
        _allowed_cascading: usize,
2,198✔
158
        _excludes: &[IntCode],
2,198✔
159
    ) -> VortexResult<ArrayRef> {
2,198✔
160
        Ok(stats.source().to_array())
2,198✔
161
    }
2,198✔
162
}
163

164
impl Scheme for ConstantScheme {
165
    type StatsType = IntegerStats;
166
    type CodeType = IntCode;
167

168
    fn code(&self) -> IntCode {
4,582✔
169
        CONSTANT_SCHEME
4,582✔
170
    }
4,582✔
171

172
    fn is_constant(&self) -> bool {
3,218✔
173
        true
3,218✔
174
    }
3,218✔
175

176
    fn expected_compression_ratio(
1,364✔
177
        &self,
1,364✔
178
        stats: &IntegerStats,
1,364✔
179
        is_sample: bool,
1,364✔
180
        _allowed_cascading: usize,
1,364✔
181
        _excludes: &[IntCode],
1,364✔
182
    ) -> VortexResult<f64> {
1,364✔
183
        // Never yield ConstantScheme for a sample, it could be a false-positive.
184
        if is_sample {
1,364✔
185
            return Ok(0.0);
×
186
        }
1,364✔
187

188
        // Only arrays with one distinct values can be constant compressed.
189
        if stats.distinct_values_count != 1 {
1,364✔
190
            return Ok(0.0);
1,076✔
191
        }
288✔
192

193
        // Cannot have mix of nulls and non-nulls
194
        if stats.null_count > 0 && stats.value_count > 0 {
288✔
UNCOV
195
            return Ok(0.0);
×
196
        }
288✔
197

198
        Ok(stats.value_count as f64)
288✔
199
    }
1,364✔
200

201
    fn compress(
214✔
202
        &self,
214✔
203
        stats: &IntegerStats,
214✔
204
        _is_sample: bool,
214✔
205
        _allowed_cascading: usize,
214✔
206
        _excludes: &[IntCode],
214✔
207
    ) -> VortexResult<ArrayRef> {
214✔
208
        // We only use Constant encoding if the entire array is constant, never if one of
209
        // the child arrays yields a constant value.
210
        let scalar = stats
214✔
211
            .source()
214✔
212
            .as_constant()
214✔
213
            .vortex_expect("constant array expected");
214✔
214

215
        Ok(ConstantArray::new(scalar, stats.src.len()).into_array())
214✔
216
    }
214✔
217
}
218

219
impl Scheme for FORScheme {
220
    type StatsType = IntegerStats;
221
    type CodeType = IntCode;
222

223
    fn code(&self) -> IntCode {
4,582✔
224
        FOR_SCHEME
4,582✔
225
    }
4,582✔
226

227
    fn expected_compression_ratio(
4,582✔
228
        &self,
4,582✔
229
        stats: &IntegerStats,
4,582✔
230
        _is_sample: bool,
4,582✔
231
        allowed_cascading: usize,
4,582✔
232
        _excludes: &[IntCode],
4,582✔
233
    ) -> VortexResult<f64> {
4,582✔
234
        // Only apply if we are not at the leaf
235
        if allowed_cascading == 0 {
4,582✔
236
            return Ok(0.0);
×
237
        }
4,582✔
238

239
        // All-null cannot be FOR compressed.
240
        if stats.value_count == 0 {
4,582✔
241
            return Ok(0.0);
×
242
        }
4,582✔
243

244
        // Only apply when the min is not already zero.
245
        if stats.typed.min_is_zero() {
4,582✔
246
            return Ok(0.0);
1,794✔
247
        }
2,788✔
248

249
        // Difference between max and min
250
        let full_width: u32 = stats.src.ptype().bit_width().try_into().vortex_unwrap();
2,788✔
251
        let bw = match stats.typed.max_minus_min().checked_ilog2() {
2,788✔
252
            Some(l) => l + 1,
912✔
253
            // If max-min == 0, it we should use a different compression scheme
254
            // as we don't want to bitpack down to 0 bits.
255
            None => return Ok(0.0),
1,876✔
256
        };
257

258
        // If we're not saving at least 1 byte, don't bother with FOR
259
        if full_width - bw < 8 {
912✔
260
            return Ok(0.0);
48✔
261
        }
864✔
262

263
        Ok(full_width as f64 / bw as f64)
864✔
264
    }
4,582✔
265

266
    fn compress(
844✔
267
        &self,
844✔
268
        stats: &IntegerStats,
844✔
269
        is_sample: bool,
844✔
270
        _allowed_cascading: usize,
844✔
271
        excludes: &[IntCode],
844✔
272
    ) -> VortexResult<ArrayRef> {
844✔
273
        let for_array = FoRArray::encode(stats.src.clone())?;
844✔
274
        let biased = for_array.encoded().to_primitive()?;
844✔
275
        let biased_stats = IntegerStats::generate_opts(
844✔
276
            &biased,
844✔
277
            GenerateStatsOptions {
844✔
278
                count_distinct_values: false,
844✔
279
            },
844✔
280
        );
281

282
        // Immediately bitpack. If any other scheme was preferable, it would be chosen instead
283
        // of bitpacking.
284
        // NOTE: we could delegate in the future if we had another downstream codec that performs
285
        //  as well.
286
        let compressed = BitPackingScheme.compress(&biased_stats, is_sample, 0, excludes)?;
844✔
287

288
        Ok(FoRArray::try_new(compressed, for_array.reference_scalar().clone())?.into_array())
844✔
289
    }
844✔
290
}
291

292
impl Scheme for ZigZagScheme {
293
    type StatsType = IntegerStats;
294
    type CodeType = IntCode;
295

296
    fn code(&self) -> IntCode {
4,590✔
297
        ZIGZAG_SCHEME
4,590✔
298
    }
4,590✔
299

300
    fn expected_compression_ratio(
4,574✔
301
        &self,
4,574✔
302
        stats: &IntegerStats,
4,574✔
303
        is_sample: bool,
4,574✔
304
        allowed_cascading: usize,
4,574✔
305
        excludes: &[IntCode],
4,574✔
306
    ) -> VortexResult<f64> {
4,574✔
307
        // ZigZag is only useful when we cascade it with another encoding
308
        if allowed_cascading == 0 {
4,574✔
309
            return Ok(0.0);
×
310
        }
4,574✔
311

312
        // Don't try and compress all-null arrays
313
        if stats.value_count == 0 {
4,574✔
314
            return Ok(0.0);
×
315
        }
4,574✔
316

317
        // ZigZag is only useful when there are negative values.
318
        if !stats.typed.min_is_negative() {
4,574✔
319
            return Ok(0.0);
4,566✔
320
        }
8✔
321

322
        // Run compression on a sample to see how it performs.
323
        estimate_compression_ratio_with_sampling(
8✔
324
            self,
8✔
325
            stats,
8✔
326
            is_sample,
8✔
327
            allowed_cascading,
8✔
328
            excludes,
8✔
329
        )
330
    }
4,574✔
331

332
    fn compress(
8✔
333
        &self,
8✔
334
        stats: &IntegerStats,
8✔
335
        is_sample: bool,
8✔
336
        allowed_cascading: usize,
8✔
337
        excludes: &[IntCode],
8✔
338
    ) -> VortexResult<ArrayRef> {
8✔
339
        // Zigzag encode the values, then recursively compress the inner values.
340
        let zag = zigzag_encode(stats.src.clone())?;
8✔
341
        let encoded = zag.encoded().to_primitive()?;
8✔
342

343
        // ZigZag should be after Dict, RunEnd or Sparse.
344
        // We should only do these "container" style compressors once.
345
        let mut new_excludes = vec![
8✔
346
            ZigZagScheme.code(),
8✔
347
            DictScheme.code(),
8✔
348
            RunEndScheme.code(),
8✔
349
            SparseScheme.code(),
8✔
350
        ];
351
        new_excludes.extend_from_slice(excludes);
8✔
352

353
        let compressed =
8✔
354
            IntCompressor::compress(&encoded, is_sample, allowed_cascading - 1, &new_excludes)?;
8✔
355

356
        log::debug!("zigzag output: {}", compressed.display_tree());
8✔
357

358
        Ok(ZigZagArray::try_new(compressed)?.into_array())
8✔
359
    }
8✔
360
}
361

362
impl Scheme for BitPackingScheme {
363
    type StatsType = IntegerStats;
364
    type CodeType = IntCode;
365

366
    fn code(&self) -> IntCode {
4,582✔
367
        BITPACKING_SCHEME
4,582✔
368
    }
4,582✔
369

370
    #[allow(clippy::cast_possible_truncation)]
371
    fn expected_compression_ratio(
4,582✔
372
        &self,
4,582✔
373
        stats: &IntegerStats,
4,582✔
374
        is_sample: bool,
4,582✔
375
        allowed_cascading: usize,
4,582✔
376
        excludes: &[IntCode],
4,582✔
377
    ) -> VortexResult<f64> {
4,582✔
378
        // BitPacking only works for non-negative values
379
        if stats.typed.min_is_negative() {
4,582✔
380
            return Ok(0.0);
8✔
381
        }
4,574✔
382

383
        // Don't compress all-null arrays
384
        if stats.value_count == 0 {
4,574✔
385
            return Ok(0.0);
×
386
        }
4,574✔
387

388
        estimate_compression_ratio_with_sampling(
4,574✔
389
            self,
4,574✔
390
            stats,
4,574✔
391
            is_sample,
4,574✔
392
            allowed_cascading,
4,574✔
393
            excludes,
4,574✔
394
        )
395
    }
4,582✔
396

397
    #[allow(clippy::cast_possible_truncation)]
398
    fn compress(
6,300✔
399
        &self,
6,300✔
400
        stats: &IntegerStats,
6,300✔
401
        _is_sample: bool,
6,300✔
402
        _allowed_cascading: usize,
6,300✔
403
        _excludes: &[IntCode],
6,300✔
404
    ) -> VortexResult<ArrayRef> {
6,300✔
405
        let histogram = bit_width_histogram(stats.source())?;
6,300✔
406
        let bw = find_best_bit_width(stats.source().ptype(), &histogram)?;
6,300✔
407
        // If best bw is determined to be the current bit-width, return the original array.
408
        if bw as usize == stats.source().ptype().bit_width() {
6,300✔
409
            return Ok(stats.source().clone().into_array());
6✔
410
        }
6,294✔
411
        let mut packed = bitpack_encode(stats.source(), bw, Some(&histogram))?;
6,294✔
412

413
        let patches = packed.patches().map(compress_patches).transpose()?;
6,294✔
414
        packed.replace_patches(patches);
6,294✔
415

416
        Ok(packed.into_array())
6,294✔
417
    }
6,300✔
418
}
419

420
impl Scheme for SparseScheme {
421
    type StatsType = IntegerStats;
422
    type CodeType = IntCode;
423

424
    fn code(&self) -> IntCode {
4,594✔
425
        SPARSE_SCHEME
4,594✔
426
    }
4,594✔
427

428
    // We can avoid asserting the encoding tree instead.
429
    fn expected_compression_ratio(
4,566✔
430
        &self,
4,566✔
431
        stats: &IntegerStats,
4,566✔
432
        _is_sample: bool,
4,566✔
433
        _allowed_cascading: usize,
4,566✔
434
        _excludes: &[IntCode],
4,566✔
435
    ) -> VortexResult<f64> {
4,566✔
436
        if stats.value_count == 0 {
4,566✔
437
            // All nulls should use ConstantScheme
438
            return Ok(0.0);
×
439
        }
4,566✔
440

441
        // If the majority is null, will compress well.
442
        if stats.null_count as f64 / stats.src.len() as f64 > 0.9 {
4,566✔
443
            return Ok(stats.src.len() as f64 / stats.value_count as f64);
×
444
        }
4,566✔
445

446
        // See if the top value accounts for >= 90% of the set values.
447
        let (_, top_count) = stats.typed.top_value_and_count();
4,566✔
448

449
        if top_count == stats.value_count {
4,566✔
450
            // top_value is the only value, should use ConstantScheme instead
451
            return Ok(0.0);
722✔
452
        }
3,844✔
453

454
        let freq = top_count as f64 / stats.value_count as f64;
3,844✔
455
        if freq >= 0.9 {
3,844✔
456
            // We only store the positions of the non-top values.
457
            return Ok(stats.value_count as f64 / (stats.value_count - top_count) as f64);
4✔
458
        }
3,840✔
459

460
        Ok(0.0)
3,840✔
461
    }
4,566✔
462

463
    fn compress(
4✔
464
        &self,
4✔
465
        stats: &IntegerStats,
4✔
466
        is_sample: bool,
4✔
467
        allowed_cascading: usize,
4✔
468
        excludes: &[IntCode],
4✔
469
    ) -> VortexResult<ArrayRef> {
4✔
470
        assert!(allowed_cascading > 0);
4✔
471
        let (top_pvalue, top_count) = stats.typed.top_value_and_count();
4✔
472
        if top_count as usize == stats.src.len() {
4✔
473
            // top_value is the only value, use ConstantScheme
474
            return Ok(ConstantArray::new(
×
475
                Scalar::primitive_value(
×
476
                    top_pvalue,
×
477
                    top_pvalue.ptype(),
×
478
                    stats.src.dtype().nullability(),
×
479
                ),
×
480
                stats.src.len(),
×
481
            )
×
482
            .into_array());
×
483
        }
4✔
484

485
        let sparse_encoded = SparseArray::encode(
4✔
486
            stats.src.as_ref(),
4✔
487
            Some(Scalar::primitive_value(
4✔
488
                top_pvalue,
4✔
489
                top_pvalue.ptype(),
4✔
490
                stats.src.dtype().nullability(),
4✔
491
            )),
4✔
492
        )?;
×
493

494
        if let Some(sparse) = sparse_encoded.as_opt::<SparseVTable>() {
4✔
495
            // Compress the values
496
            let mut new_excludes = vec![SparseScheme.code()];
4✔
497
            new_excludes.extend_from_slice(excludes);
4✔
498

499
            let compressed_values = IntCompressor::compress_no_dict(
4✔
500
                &sparse.patches().values().to_primitive()?,
4✔
501
                is_sample,
4✔
502
                allowed_cascading - 1,
4✔
503
                &new_excludes,
4✔
504
            )?;
×
505

506
            let indices =
4✔
507
                downscale_integer_array(sparse.patches().indices().clone())?.to_primitive()?;
4✔
508

509
            let compressed_indices = IntCompressor::compress_no_dict(
4✔
510
                &indices,
4✔
511
                is_sample,
4✔
512
                allowed_cascading - 1,
4✔
513
                &new_excludes,
4✔
514
            )?;
×
515

516
            SparseArray::try_new(
4✔
517
                compressed_indices,
4✔
518
                compressed_values,
4✔
519
                sparse.len(),
4✔
520
                sparse.fill_scalar().clone(),
4✔
521
            )
522
            .map(|a| a.into_array())
4✔
523
        } else {
524
            Ok(sparse_encoded)
×
525
        }
526
    }
4✔
527
}
528

529
impl Scheme for DictScheme {
530
    type StatsType = IntegerStats;
531
    type CodeType = IntCode;
532

533
    fn code(&self) -> IntCode {
5,576✔
534
        DICT_SCHEME
5,576✔
535
    }
5,576✔
536

537
    fn expected_compression_ratio(
2,824✔
538
        &self,
2,824✔
539
        stats: &IntegerStats,
2,824✔
540
        _is_sample: bool,
2,824✔
541
        allowed_cascading: usize,
2,824✔
542
        _excludes: &[IntCode],
2,824✔
543
    ) -> VortexResult<f64> {
2,824✔
544
        // Dict should not be terminal.
545
        if allowed_cascading == 0 {
2,824✔
546
            return Ok(0.0);
×
547
        }
2,824✔
548

549
        if stats.value_count == 0 {
2,824✔
550
            return Ok(0.0);
×
551
        }
2,824✔
552

553
        // If > 50% of the values are distinct, skip dict.
554
        if stats.distinct_values_count > stats.value_count / 2 {
2,824✔
555
            return Ok(0.0);
1,556✔
556
        }
1,268✔
557

558
        // Ignore nulls encoding for the estimate. We only focus on values.
559
        let values_size = stats.source().ptype().bit_width() * stats.distinct_values_count as usize;
1,268✔
560

561
        // Assume codes are compressed RLE + BitPacking.
562
        let codes_bw = usize::BITS - stats.distinct_values_count.leading_zeros();
1,268✔
563

564
        let n_runs = stats.value_count / stats.average_run_length;
1,268✔
565

566
        // Assume that codes will either be BitPack or RLE-BitPack
567
        let codes_size_bp = (codes_bw * stats.value_count) as usize;
1,268✔
568
        let codes_size_rle_bp = (codes_bw + 32) * n_runs;
1,268✔
569

570
        let codes_size = usize::min(codes_size_bp, codes_size_rle_bp as usize);
1,268✔
571

572
        let before = stats.value_count as usize * stats.source().ptype().bit_width();
1,268✔
573

574
        Ok(before as f64 / (values_size + codes_size) as f64)
1,268✔
575
    }
2,824✔
576

577
    fn compress(
2✔
578
        &self,
2✔
579
        stats: &IntegerStats,
2✔
580
        is_sample: bool,
2✔
581
        allowed_cascading: usize,
2✔
582
        excludes: &[IntCode],
2✔
583
    ) -> VortexResult<ArrayRef> {
2✔
584
        assert!(allowed_cascading > 0);
2✔
585

586
        // TODO(aduffy): we can be more prescriptive: we know that codes will EITHER be
587
        //    RLE or FOR + BP. Cascading probably wastes some time here.
588

589
        let dict = dictionary_encode(stats)?;
2✔
590

591
        // Cascade the codes child
592
        // Don't allow SequenceArray as the codes child as it merely adds extra indirection without actually compressing data.
593
        let mut new_excludes = vec![DICT_SCHEME, SEQUENCE_SCHEME];
2✔
594
        new_excludes.extend_from_slice(excludes);
2✔
595

596
        let compressed_codes = IntCompressor::compress_no_dict(
2✔
597
            &dict.codes().to_primitive()?,
2✔
598
            is_sample,
2✔
599
            allowed_cascading - 1,
2✔
600
            &new_excludes,
2✔
601
        )?;
×
602

603
        Ok(DictArray::try_new(compressed_codes, dict.values().clone())?.into_array())
2✔
604
    }
2✔
605
}
606

607
impl Scheme for RunEndScheme {
608
    type StatsType = IntegerStats;
609
    type CodeType = IntCode;
610

611
    fn code(&self) -> IntCode {
5,352✔
612
        RUNEND_SCHEME
5,352✔
613
    }
5,352✔
614

615
    fn expected_compression_ratio(
3,050✔
616
        &self,
3,050✔
617
        stats: &IntegerStats,
3,050✔
618
        is_sample: bool,
3,050✔
619
        allowed_cascading: usize,
3,050✔
620
        excludes: &[IntCode],
3,050✔
621
    ) -> VortexResult<f64> {
3,050✔
622
        // If the run length is below the threshold, drop it.
623
        if stats.average_run_length < RUN_END_THRESHOLD {
3,050✔
624
            return Ok(0.0);
2,578✔
625
        }
472✔
626

627
        if allowed_cascading == 0 {
472✔
628
            return Ok(0.0);
×
629
        }
472✔
630

631
        // Run compression on a sample, see how it performs.
632
        estimate_compression_ratio_with_sampling(
472✔
633
            self,
472✔
634
            stats,
472✔
635
            is_sample,
472✔
636
            allowed_cascading,
472✔
637
            excludes,
472✔
638
        )
639
    }
3,050✔
640

641
    fn compress(
762✔
642
        &self,
762✔
643
        stats: &IntegerStats,
762✔
644
        is_sample: bool,
762✔
645
        allowed_cascading: usize,
762✔
646
        excludes: &[IntCode],
762✔
647
    ) -> VortexResult<ArrayRef> {
762✔
648
        assert!(allowed_cascading > 0);
762✔
649

650
        // run-end encode the ends
651
        let (ends, values) = runend_encode(&stats.src)?;
762✔
652

653
        let mut new_excludes = vec![RunEndScheme.code(), DictScheme.code()];
762✔
654
        new_excludes.extend_from_slice(excludes);
762✔
655

656
        let ends_stats = IntegerStats::generate_opts(
762✔
657
            &ends,
762✔
658
            GenerateStatsOptions {
762✔
659
                count_distinct_values: false,
762✔
660
            },
762✔
661
        );
662
        let ends_scheme = IntCompressor::choose_scheme(
762✔
663
            &ends_stats,
762✔
664
            is_sample,
762✔
665
            allowed_cascading - 1,
762✔
666
            &new_excludes,
762✔
667
        )?;
×
668
        let compressed_ends =
762✔
669
            ends_scheme.compress(&ends_stats, is_sample, allowed_cascading - 1, &new_excludes)?;
762✔
670

671
        let compressed_values = IntCompressor::compress_no_dict(
762✔
672
            &values.to_primitive()?,
762✔
673
            is_sample,
762✔
674
            allowed_cascading - 1,
762✔
675
            &new_excludes,
762✔
676
        )?;
×
677

678
        Ok(RunEndArray::try_new(compressed_ends, compressed_values)?.into_array())
762✔
679
    }
762✔
680
}
681

682
impl Scheme for SequenceScheme {
683
    type StatsType = IntegerStats;
684
    type CodeType = IntCode;
685

686
    fn code(&self) -> Self::CodeType {
4,806✔
687
        SEQUENCE_SCHEME
4,806✔
688
    }
4,806✔
689

690
    fn expected_compression_ratio(
4,124✔
691
        &self,
4,124✔
692
        stats: &Self::StatsType,
4,124✔
693
        _is_sample: bool,
4,124✔
694
        _allowed_cascading: usize,
4,124✔
695
        _excludes: &[Self::CodeType],
4,124✔
696
    ) -> VortexResult<f64> {
4,124✔
697
        if stats.null_count > 0 {
4,124✔
UNCOV
698
            return Ok(0.0);
×
699
        }
4,124✔
700
        // Since two values are required to store base and multiplier the
701
        // compression ratio is divided by 2.
702
        Ok(sequence_encode(&stats.src)?
4,124✔
703
            .map(|_| stats.src.len() as f64 / 2.0)
4,124✔
704
            .unwrap_or(0.0))
4,124✔
705
    }
4,124✔
706

707
    fn compress(
148✔
708
        &self,
148✔
709
        stats: &Self::StatsType,
148✔
710
        _is_sample: bool,
148✔
711
        _allowed_cascading: usize,
148✔
712
        _excludes: &[Self::CodeType],
148✔
713
    ) -> VortexResult<ArrayRef> {
148✔
714
        if stats.null_count > 0 {
148✔
715
            vortex_bail!("sequence encoding does not support nulls");
×
716
        }
148✔
717
        sequence_encode(&stats.src)?.ok_or_else(|| vortex_err!("cannot sequence encode array"))
148✔
718
    }
148✔
719
}
720

721
#[cfg(test)]
722
mod tests {
723
    use itertools::Itertools;
724
    use log::LevelFilter;
725
    use rand::rngs::StdRng;
726
    use rand::{RngCore, SeedableRng};
727
    use vortex_array::arrays::PrimitiveArray;
728
    use vortex_array::validity::Validity;
729
    use vortex_array::vtable::ValidityHelper;
730
    use vortex_array::{Array, IntoArray, ToCanonical};
731
    use vortex_buffer::{Buffer, BufferMut, buffer, buffer_mut};
732
    use vortex_dict::DictEncoding;
733
    use vortex_sequence::SequenceEncoding;
734
    use vortex_sparse::SparseEncoding;
735
    use vortex_utils::aliases::hash_set::HashSet;
736

737
    use crate::integer::{IntCompressor, IntegerStats, SequenceScheme, SparseScheme};
738
    use crate::{Compressor, CompressorStats, Scheme};
739

740
    #[test]
741
    fn test_empty() {
742
        // Make sure empty array compression does not fail
743
        let result = IntCompressor::compress(
744
            &PrimitiveArray::new(Buffer::<i32>::empty(), Validity::NonNullable),
745
            false,
746
            3,
747
            &[],
748
        )
749
        .unwrap();
750

751
        assert!(result.is_empty());
752
    }
753

754
    #[test]
755
    fn test_dict_encodable() {
756
        let mut codes = BufferMut::<i32>::with_capacity(65_535);
757
        // Write some runs of length 3 of a handful of different values. Interrupted by some
758
        // one-off values.
759

760
        let numbers = [0, 10, 50, 100, 1000, 3000]
761
            .into_iter()
762
            .map(|i| 1234 * i)
763
            .collect_vec();
764

765
        let mut rng = StdRng::seed_from_u64(1u64);
766
        while codes.len() < 64000 {
767
            let run_length = rng.next_u32() % 5;
768
            let value = numbers[rng.next_u32() as usize % numbers.len()];
769
            for _ in 0..run_length {
770
                codes.push(value);
771
            }
772
        }
773

774
        let primitive = codes.freeze().into_array().to_primitive().unwrap();
775
        let compressed = IntCompressor::compress(&primitive, false, 3, &[]).unwrap();
776
        assert_eq!(compressed.encoding_id(), DictEncoding.id());
777
    }
778

779
    #[test]
780
    fn test_window_name() {
781
        env_logger::builder()
782
            .filter(None, LevelFilter::Debug)
783
            .try_init()
784
            .ok();
785

786
        // A test that's meant to mirror the WindowName column from ClickBench.
787
        let mut values = buffer_mut![-1i32; 1_000_000];
788
        let mut visited = HashSet::new();
789
        let mut rng = StdRng::seed_from_u64(1u64);
790
        while visited.len() < 223 {
791
            let random = (rng.next_u32() as usize) % 1_000_000;
792
            if visited.contains(&random) {
793
                continue;
794
            }
795
            visited.insert(random);
796
            // Pick 100 random values to insert.
797
            values[random] = 5 * (rng.next_u64() % 100) as i32;
798
        }
799

800
        let array = values.freeze().into_array().to_primitive().unwrap();
801
        let compressed = IntCompressor::compress(&array, false, 3, &[]).unwrap();
802
        log::info!("WindowName compressed: {}", compressed.display_tree());
803
    }
804

805
    #[test]
806
    fn sparse_with_nulls() {
807
        let array = PrimitiveArray::new(
808
            buffer![189u8, 189, 189, 0, 46],
809
            Validity::from_iter(vec![true, true, true, true, false]),
810
        );
811
        let compressed = SparseScheme
812
            .compress(&IntegerStats::generate(&array), false, 3, &[])
813
            .unwrap();
814
        assert_eq!(compressed.encoding_id(), SparseEncoding.id());
815
        let decoded = compressed.to_primitive().unwrap();
816
        let expected = [189u8, 189, 189, 0, 0];
817
        assert_eq!(decoded.as_slice::<u8>(), &expected);
818
        assert_eq!(decoded.validity(), array.validity());
819
    }
820

821
    #[test]
822
    fn sparse_mostly_nulls() {
823
        let array = PrimitiveArray::new(
824
            buffer![189u8, 189, 189, 189, 189, 189, 189, 189, 189, 0, 46],
825
            Validity::from_iter(vec![
826
                false, false, false, false, false, false, false, false, false, false, true,
827
            ]),
828
        );
829
        let compressed = SparseScheme
830
            .compress(&IntegerStats::generate(&array), false, 3, &[])
831
            .unwrap();
832
        assert_eq!(compressed.encoding_id(), SparseEncoding.id());
833
        let decoded = compressed.to_primitive().unwrap();
834
        let expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46];
835
        assert_eq!(decoded.as_slice::<u8>(), &expected);
836
        assert_eq!(decoded.validity(), array.validity());
837
    }
838

839
    #[test]
840
    fn nullable_sequence() {
841
        let values = (0i32..20).step_by(7).collect_vec();
842
        let array = PrimitiveArray::from_option_iter(values.clone().into_iter().map(Some));
843
        let compressed = SequenceScheme
844
            .compress(&IntegerStats::generate(&array), false, 3, &[])
845
            .unwrap();
846
        assert_eq!(compressed.encoding_id(), SequenceEncoding.id());
847
        let decoded = compressed.to_primitive().unwrap();
848
        assert_eq!(decoded.as_slice::<i32>(), values.as_slice());
849
    }
850
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc