Skip to main content

mlt_core/decoder/stream/
model.rs

1use std::fmt::{Display, Formatter, Result as FmtResult};
2
3use derive_debug::Dbg;
4use num_enum::TryFromPrimitive;
5
6use crate::errors::AsMltError as _;
7#[cfg(feature = "unstable-v2")]
8use crate::tile::ZStep;
9use crate::utils::formatter::{bytes_dbg, compact_dbg};
10use crate::{MltError, MltResult};
11
12/// Logical encoding technique used for a column, as stored in the tile
13///
14/// Variants are already shifted into the primary logical field of the v1 encoding byte (bits 7-5),
15/// so that field is matched with a mask rather than shifted down first.
16/// The secondary field (bits 4-2) holds the same patterns three bits lower.
17#[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)]
18#[repr(u8)]
19pub enum LogicalTechnique {
20    None = 0b0000_0000,
21    Delta = 0b0010_0000,
22    ComponentwiseDelta = 0b0100_0000,
23    Rle = 0b0110_0000,
24    Morton = 0b1000_0000,
25}
26
27/// The combinations of the two [`LogicalTechnique`] fields that are legal on the wire
28///
29/// Each variant is the whole logical part of the v1 encoding byte, i.e. the primary field
30/// (bits 7-5) or-ed with the secondary field (bits 4-2).
31/// Any other pairing of the two fields is rejected while parsing.
32#[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)]
33#[repr(u8)]
34pub enum LogicalCombination {
35    None = 0b0000_0000,
36    Delta = 0b0010_0000,
37    DeltaRle = 0b0010_1100,
38    ComponentwiseDelta = 0b0100_0000,
39    Rle = 0b0110_0000,
40    MortonDelta = 0b1000_0100,
41}
42
43/// Which RLE stream layout the encoder should produce.
44///
45/// A data-less selector chosen up front by the wire format (see
46/// [`WireVersion::rle_layout`](crate::encoder::WireVersion)); the realized
47/// per-stream metadata is [`RleMeta`], whose variants mirror these.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum RleLayout {
50    /// Tag `0x01`: all run lengths first, then all values.
51    Split,
52    /// Tag `0x02`: `(run_length, value)` pairs. Requires the `unstable-v2` feature.
53    #[cfg(feature = "unstable-v2")]
54    Interleaved,
55}
56
57/// Metadata for RLE decoding, one variant per [`RleLayout`].
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum RleMeta {
60    /// Tag `0x01`: physically-decoded words are `[run_len × runs][value × runs]`.
61    /// `runs` is the split point; `num_rle_values` is the expanded element count.
62    Split { runs: u32, num_rle_values: u32 },
63    /// Tag `0x02`: physically-decoded words are `(run_len, value)` pairs. The run
64    /// count is derived from the data length, so only the expanded element count
65    /// (`num_rle_values`, from the stream's count context) is carried.
66    /// Requires the `unstable-v2` feature.
67    #[cfg(feature = "unstable-v2")]
68    Interleaved { num_rle_values: u32 },
69}
70
71impl RleMeta {
72    /// The total expanded element count, common to both layouts.
73    #[cfg(feature = "unstable-v2")]
74    #[must_use]
75    pub(crate) fn num_rle_values(self) -> u32 {
76        match self {
77            Self::Split { num_rle_values, .. } => num_rle_values,
78            #[cfg(feature = "unstable-v2")]
79            Self::Interleaved { num_rle_values } => num_rle_values,
80        }
81    }
82}
83
84/// Metadata for Morton decoding
85#[derive(Debug, Clone, Copy, PartialEq)]
86pub struct Morton {
87    /// Number of bits used
88    pub(crate) bits: u32,
89    /// Coordinate shift
90    pub(crate) shift: u32,
91}
92
93impl Morton {
94    pub fn new(bits: u32, shift: u32) -> MltResult<Self> {
95        if bits <= 16 {
96            Ok(Self { bits, shift })
97        } else {
98            Err(MltError::InvalidMortonBits(bits))
99        }
100    }
101}
102
103/// The decimal scaling a Framed, Exception-Free ALP column uses: `i = round(v * 10^e / 10^f)`.
104/// Chosen before any value is seen, so it carries no frame of reference.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct AlpScale {
107    /// Decimal exponent the values were scaled by.
108    pub(crate) e: u8,
109    /// Factor dividing out the trailing zeros `e` introduced, never exceeding `e`.
110    pub(crate) f: u8,
111}
112
113/// Framed, Exception-Free ALP parameters: `v = (base + offset) * 10^f / 10^e`.
114/// Written as a scale byte and a base varint, the stream itself holding the unsigned offsets.
115#[derive(Clone, Copy, PartialEq, Eq)]
116pub struct Alp {
117    pub(crate) scale: AlpScale,
118    /// Frame of reference the offsets are measured from, the smallest scaled integer in the column.
119    pub(crate) base: i64,
120}
121
122#[cfg(feature = "unstable-v2")]
123impl AlpScale {
124    /// Largest exponent the codes can carry, past which `v * 10^e` leaves the `i64` range.
125    /// The codes themselves are bounded tighter, to `2^53 - 1`, by the encoder.
126    pub(crate) const MAX_EXPONENT: u8 = 18;
127
128    /// Net power of ten the codes carry, which fixes their magnitude and so their stored size.
129    /// The order [`candidates`](crate::codecs::alp::candidates) walks, and so what lets the
130    /// encoder stop at the first scale that fits; only the tests holding that invariant name it.
131    #[cfg(test)]
132    pub(crate) fn net(self) -> u8 {
133        self.e - self.f
134    }
135
136    /// The header byte naming this scale, the pairs with `f <= e` numbered row by row.
137    pub(crate) fn to_byte(self) -> u8 {
138        Self::row_start(self.e) + self.f
139    }
140
141    /// Read a scale back from its header byte, rejecting the codes past the last pair.
142    pub(crate) fn from_byte(byte: u8) -> MltResult<Self> {
143        (0..=Self::MAX_EXPONENT)
144            .find_map(|e| {
145                let f = byte.checked_sub(Self::row_start(e))?;
146                (f <= e).then_some(Self { e, f })
147            })
148            .ok_or(MltError::InvalidAlpScale(byte))
149    }
150
151    /// First byte of the row for `e`, which is `e * (e + 1) / 2`.
152    fn row_start(e: u8) -> u8 {
153        (1..=e).sum()
154    }
155}
156
157/// Flattened, since the nesting is an encoder concern and these appear in stream labels.
158impl std::fmt::Debug for Alp {
159    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
160        write!(
161            f,
162            "Alp {{ e: {}, f: {}, base: {} }}",
163            self.scale.e, self.scale.f, self.base
164        )
165    }
166}
167
168#[cfg(feature = "unstable-v2")]
169impl Alp {
170    #[cfg(test)]
171    pub(crate) fn new(e: u8, f: u8, base: i64) -> MltResult<Self> {
172        if e <= AlpScale::MAX_EXPONENT && f <= e {
173            Ok(Self {
174                scale: AlpScale { e, f },
175                base,
176            })
177        } else {
178            Err(MltError::InvalidAlpParams(e, f))
179        }
180    }
181
182    /// Measure a scaled integer from the frame of reference, giving the offset the stream stores.
183    /// Wrapping, so a foreign stream whose codes span the whole `i64` range still subtracts
184    /// exactly; our own encoder keeps codes within `2^53 - 1`, so the spread fits `u64` easily.
185    #[expect(
186        clippy::cast_sign_loss,
187        reason = "the bit pattern is the point; `code_at` casts it back"
188    )]
189    pub(crate) fn offset_of(self, code: i64) -> u64 {
190        (code as u64).wrapping_sub(self.base as u64)
191    }
192
193    /// Put a stored offset back on the frame of reference, inverting [`Self::offset_of`].
194    #[expect(
195        clippy::cast_possible_wrap,
196        clippy::cast_sign_loss,
197        reason = "the bit pattern is the point; inverts `offset_of` exactly"
198    )]
199    pub(crate) fn code_at(self, offset: u64) -> i64 {
200        (self.base as u64).wrapping_add(offset) as i64
201    }
202}
203
204/// What kind of values a stream holds, which fixes the encodings it can name.
205/// Neither wire format stores it: v1 reads it from the column type, v2 from the stream's context.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum ValueKind {
208    Int,
209    Bool,
210    Float,
211    Vertex,
212}
213
214/// Logical encoding of a stream of integer values.
215/// Covers the id columns, the integer property columns, and the geometry length and offset streams.
216#[derive(Debug, Clone, Copy, PartialEq)]
217pub enum IntLogical {
218    None,
219    Delta,
220    Rle(RleMeta),
221    DeltaRle(RleMeta),
222    /// Deltas of the deltas, both running from zero.
223    Delta2,
224}
225
226/// Logical encoding of a bool column's data stream or a presence bitfield.
227#[derive(Debug, Clone, Copy, PartialEq)]
228pub enum BoolLogical {
229    /// A raw packed bitmap, one bit per value.
230    None,
231    /// A byte-RLE compressed bitmap.
232    /// Its run parameters come from the stream's context rather than from its header.
233    ByteRle(RleMeta),
234}
235
236/// Logical encoding of a float column's data stream.
237#[derive(Debug, Clone, Copy, PartialEq)]
238pub enum FloatLogical {
239    /// Fixed-width little-endian values, one per element.
240    None,
241    /// A decimal split across two integer streams, which v1 can name but no decoder here implements.
242    /// One code per element, with the distinct values following as a second stream.
243    /// Only the tag `0x02` codec reads or writes it.
244    Dict,
245    /// Integers scaled by these parameters.
246    /// Only the tag `0x02` codec reads or writes it.
247    Alp(Alp),
248}
249
250/// Logical encoding of a geometry vertex stream, whose values are interleaved coordinate pairs.
251#[derive(Debug, Clone, Copy, PartialEq)]
252pub enum VertexLogical {
253    None,
254    Delta,
255    ComponentwiseDelta,
256    /// Componentwise deltas of the componentwise deltas.
257    ComponentwiseDelta2,
258    MortonDelta(Morton),
259    /// Interleaved `(x, y, z)` triples, whose z lie on the grid of the step.
260    #[cfg(feature = "unstable-v2")]
261    Xyz(ZStep, XyzLogical),
262}
263
264impl VertexLogical {
265    /// The 32-bit words one vertex spans, where a Morton code is one word.
266    #[must_use]
267    pub fn words_per_vertex(self) -> u32 {
268        match self {
269            Self::None | Self::Delta | Self::ComponentwiseDelta | Self::ComponentwiseDelta2 => 2,
270            Self::MortonDelta(_) => 1,
271            #[cfg(feature = "unstable-v2")]
272            Self::Xyz(..) => 3,
273        }
274    }
275}
276
277/// Logical encoding of an `(x, y, z)` vertex stream.
278///
279/// A Morton code spans only `x` and `y`, so there is no Morton member.
280#[cfg(feature = "unstable-v2")]
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub enum XyzLogical {
283    None,
284    Delta,
285    ComponentwiseDelta,
286}
287
288/// How should the stream be interpreted at the logical level (second pass of decoding)
289///
290/// Split per [`ValueKind`] so a stream can name only the encodings its values can have.
291#[derive(Debug, Clone, Copy, PartialEq)]
292pub enum LogicalEncoding {
293    Int(IntLogical),
294    Bool(BoolLogical),
295    Float(FloatLogical),
296    Vertex(VertexLogical),
297}
298
299impl LogicalEncoding {
300    /// The 32-bit words one counted value spans, which only a vertex stream makes more than one.
301    #[must_use]
302    pub(crate) fn words_per_value(self) -> u32 {
303        match self {
304            Self::Vertex(logical) => logical.words_per_vertex(),
305            Self::Int(_) | Self::Bool(_) | Self::Float(_) => 1,
306        }
307    }
308
309    /// The kind of values this encoding belongs to.
310    #[must_use]
311    pub fn kind(self) -> ValueKind {
312        match self {
313            Self::Int(_) => ValueKind::Int,
314            Self::Bool(_) => ValueKind::Bool,
315            Self::Float(_) => ValueKind::Float,
316            Self::Vertex(_) => ValueKind::Vertex,
317        }
318    }
319
320    /// The identity encoding for `kind`, i.e. values stored as they are.
321    #[must_use]
322    pub(crate) fn none(kind: ValueKind) -> Self {
323        match kind {
324            ValueKind::Int => Self::Int(IntLogical::None),
325            ValueKind::Bool => Self::Bool(BoolLogical::None),
326            ValueKind::Float => Self::Float(FloatLogical::None),
327            ValueKind::Vertex => Self::Vertex(VertexLogical::None),
328        }
329    }
330
331    /// Whether the stream's own logical pass is a no-op, so the physical words are already the output.
332    /// True for a float dictionary's codes, which the column turns back into floats.
333    /// Not true for Framed, Exception-Free ALP, whose offsets still need the frame of reference added back.
334    #[must_use]
335    pub(crate) fn is_identity(self) -> bool {
336        matches!(
337            self,
338            Self::Int(IntLogical::None)
339                | Self::Bool(BoolLogical::None)
340                | Self::Float(FloatLogical::None | FloatLogical::Dict)
341                | Self::Vertex(VertexLogical::None)
342        )
343    }
344}
345
346/// Carries the stream metadata needed to perform the logical decode pass.
347///
348/// Construct with [`LogicalValue::new`] after the physical decode pass fills a
349/// `&[u32]` or `&[u64]` buffer, then call the appropriate `decode_*` method,
350/// passing that slice as `data`.
351#[derive(Debug, PartialEq)]
352pub struct LogicalValue {
353    pub(crate) meta: StreamMeta,
354}
355
356// Physical encoding types
357
358/// Dictionary type used for a column, as stored in the tile
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
360#[repr(u8)]
361pub enum DictionaryType {
362    None = 0b0000_0000,
363    Single = 0b0000_0001,
364    Shared = 0b0000_0010,
365    Vertex = 0b0000_0011,
366    Morton = 0b0000_0100,
367    Fsst = 0b0000_0101,
368}
369
370/// Offset type used for a column, as stored in the tile
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
372#[repr(u8)]
373pub enum OffsetType {
374    Vertex = 0b0000_0000,
375    Index = 0b0000_0001,
376    String = 0b0000_0010,
377    Key = 0b0000_0011,
378}
379
380/// Length type used for a column, as stored in the tile
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
382#[repr(u8)]
383pub enum LengthType {
384    VarBinary = 0b0000_0000,
385    Geometries = 0b0000_0001,
386    Parts = 0b0000_0010,
387    Rings = 0b0000_0011,
388    Triangles = 0b0000_0100,
389    Symbol = 0b0000_0101,
390    Dictionary = 0b0000_0110,
391    /// A nested list or map node's lengths, one per present value of the node.
392    #[cfg(feature = "unstable-v2")]
393    Nested = 0b0000_0111,
394}
395
396/// How should the stream be interpreted at the physical level (first pass of decoding)
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
398pub enum StreamType {
399    Present,
400    Data(DictionaryType),
401    Offset(OffsetType),
402    Length(LengthType),
403}
404
405/// Physical encoding used for a column, as stored in the tile
406#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
407pub enum PhysicalEncoding {
408    None,
409    /// Preferred, tends to produce the best compression ratio and decoding performance.
410    /// But currently limited to 32-bit integer.
411    FastPFor(FastPForKind),
412    /// Can produce better results in combination with a heavyweight compression scheme like `Gzip`.
413    /// Simple compression scheme where the encoding is easier to implement compared to `FastPfor`.
414    VarInt,
415    /// Every value in the same number of bits, which a leading payload byte names.
416    /// Beats `VarInt` on the short, small-alphabet streams a dictionary's codes form,
417    /// where `FastPFor`'s 128-value blocks are too coarse to pay for themselves.
418    #[cfg(feature = "unstable-v2")]
419    BitPacked,
420}
421
422/// The `FastPFor` block size and word order a wire version codes its integer streams with.
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
424pub enum FastPForKind {
425    /// 256-value blocks over big-endian words
426    Block256Be,
427    /// 128-value blocks over little-endian words
428    #[cfg(feature = "unstable-v2")]
429    Block128Le,
430}
431
432// RawStream types
433
434#[derive(Debug, Clone, Copy, PartialEq)]
435pub struct IntEncoding {
436    pub logical: LogicalEncoding,
437    pub physical: PhysicalEncoding,
438}
439
440impl IntEncoding {
441    #[must_use]
442    pub(crate) const fn new(logical: LogicalEncoding, physical: PhysicalEncoding) -> Self {
443        Self { logical, physical }
444    }
445
446    #[must_use]
447    pub(crate) fn none(kind: ValueKind) -> Self {
448        Self::new(LogicalEncoding::none(kind), PhysicalEncoding::None)
449    }
450}
451
452/// Metadata about an encoded stream
453#[derive(Clone, Copy, Dbg, PartialEq)]
454pub struct StreamMeta {
455    #[dbg(formatter = "compact_dbg")]
456    pub stream_type: StreamType,
457    #[dbg(formatter = "compact_dbg")]
458    pub encoding: IntEncoding,
459    pub(crate) num_values: u32,
460}
461
462impl StreamMeta {
463    #[inline]
464    pub(crate) fn new(stream_type: StreamType, encoding: IntEncoding, num_values: u32) -> Self {
465        Self {
466            stream_type,
467            encoding,
468            num_values,
469        }
470    }
471
472    /// A meta for a stream of `num_words` physical words, which a vertex stream counts in vertices.
473    #[inline]
474    pub(crate) fn new2(
475        stream_type: StreamType,
476        logical: LogicalEncoding,
477        physical: PhysicalEncoding,
478        num_words: usize,
479    ) -> MltResult<Self> {
480        let enc = IntEncoding::new(logical, physical);
481        let num_words = u32::try_from(num_words)?;
482        let per_value = logical.words_per_value();
483        if !num_words.is_multiple_of(per_value) {
484            return Err(MltError::PartialVertex(num_words, per_value));
485        }
486        Ok(Self::new(stream_type, enc, num_words / per_value))
487    }
488
489    /// The physical words the stream's values span.
490    #[inline]
491    pub(crate) fn num_words(&self) -> MltResult<u32> {
492        self.num_values
493            .checked_mul(self.encoding.logical.words_per_value())
494            .or_overflow()
495    }
496
497    #[inline]
498    pub(crate) fn new_none(
499        stream_type: StreamType,
500        kind: ValueKind,
501        num_values: usize,
502    ) -> MltResult<Self> {
503        let enc = IntEncoding::none(kind);
504        Ok(Self::new(stream_type, enc, u32::try_from(num_values)?))
505    }
506}
507
508/// Representation of an encoded stream
509#[derive(Clone, Dbg, PartialEq)]
510pub struct RawStream<'a> {
511    pub meta: StreamMeta,
512    #[dbg(formatter = "bytes_dbg")]
513    pub(crate) data: &'a [u8],
514}
515
516impl<'a> RawStream<'a> {
517    #[must_use]
518    pub(crate) fn new(meta: StreamMeta, data: &'a [u8]) -> Self {
519        Self { meta, data }
520    }
521}
522
523// Display impls the annotated dump's JSON carries instead of serde derives on this
524// vocabulary, so the wire enums do not become frozen public JSON API.
525
526impl Display for StreamType {
527    /// `present`, `data`, `data[fsst]`, `offset[string]`, `length[rings]`.
528    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
529        match self {
530            Self::Present => f.write_str("present"),
531            Self::Data(dict) => {
532                let name = match dict {
533                    DictionaryType::None => return f.write_str("data"),
534                    DictionaryType::Single => "single",
535                    DictionaryType::Shared => "shared",
536                    DictionaryType::Vertex => "vertex",
537                    DictionaryType::Morton => "morton",
538                    DictionaryType::Fsst => "fsst",
539                };
540                write!(f, "data[{name}]")
541            }
542            Self::Offset(offset) => {
543                let name = match offset {
544                    OffsetType::Vertex => "vertex",
545                    OffsetType::Index => "index",
546                    OffsetType::String => "string",
547                    OffsetType::Key => "key",
548                };
549                write!(f, "offset[{name}]")
550            }
551            Self::Length(length) => {
552                let name = match length {
553                    LengthType::VarBinary => "var-binary",
554                    LengthType::Geometries => "geometries",
555                    LengthType::Parts => "parts",
556                    LengthType::Rings => "rings",
557                    LengthType::Triangles => "triangles",
558                    LengthType::Symbol => "symbol",
559                    LengthType::Dictionary => "dictionary",
560                    #[cfg(feature = "unstable-v2")]
561                    LengthType::Nested => "nested",
562                };
563                write!(f, "length[{name}]")
564            }
565        }
566    }
567}
568
569impl Display for LogicalEncoding {
570    /// The value kind, then its encoding: `int/rle`, `float/alp`, `vertex/morton-delta`.
571    ///
572    /// Run parameters are left out, they belong to the region's own annotation.
573    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
574        let (kind, enc) = match self {
575            Self::Int(int) => (
576                "int",
577                match int {
578                    IntLogical::None => "none",
579                    IntLogical::Delta => "delta",
580                    IntLogical::Rle(_) => "rle",
581                    IntLogical::DeltaRle(_) => "delta-rle",
582                    IntLogical::Delta2 => "delta2",
583                },
584            ),
585            Self::Bool(b) => (
586                "bool",
587                match b {
588                    BoolLogical::None => "none",
589                    BoolLogical::ByteRle(_) => "byte-rle",
590                },
591            ),
592            Self::Float(float) => (
593                "float",
594                match float {
595                    FloatLogical::None => "none",
596                    FloatLogical::Dict => "dict",
597                    FloatLogical::Alp(_) => "alp",
598                },
599            ),
600            Self::Vertex(vertex) => (
601                "vertex",
602                match vertex {
603                    VertexLogical::None => "none",
604                    VertexLogical::Delta => "delta",
605                    VertexLogical::ComponentwiseDelta => "componentwise-delta",
606                    VertexLogical::ComponentwiseDelta2 => "componentwise-delta2",
607                    VertexLogical::MortonDelta(_) => "morton-delta",
608                    #[cfg(feature = "unstable-v2")]
609                    VertexLogical::Xyz(_, XyzLogical::None) => "xyz-none",
610                    #[cfg(feature = "unstable-v2")]
611                    VertexLogical::Xyz(_, XyzLogical::Delta) => "xyz-delta",
612                    #[cfg(feature = "unstable-v2")]
613                    VertexLogical::Xyz(_, XyzLogical::ComponentwiseDelta) => {
614                        "xyz-componentwise-delta"
615                    }
616                },
617            ),
618        };
619        write!(f, "{kind}/{enc}")
620    }
621}
622
623impl Display for PhysicalEncoding {
624    /// `none`, `varint`, `fastpfor[256be]`, `bit-packed`.
625    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
626        match self {
627            Self::None => f.write_str("none"),
628            Self::VarInt => f.write_str("varint"),
629            Self::FastPFor(kind) => {
630                let name = match kind {
631                    FastPForKind::Block256Be => "256be",
632                    #[cfg(feature = "unstable-v2")]
633                    FastPForKind::Block128Le => "128le",
634                };
635                write!(f, "fastpfor[{name}]")
636            }
637            #[cfg(feature = "unstable-v2")]
638            Self::BitPacked => f.write_str("bit-packed"),
639        }
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use rstest::rstest;
646    use usize_cast::IntoUsize as _;
647
648    use super::*;
649
650    fn morton() -> Morton {
651        Morton { bits: 4, shift: 0 }
652    }
653
654    fn alp() -> Alp {
655        Alp {
656            scale: AlpScale { e: 3, f: 1 },
657            base: -5,
658        }
659    }
660
661    fn rle() -> RleMeta {
662        RleMeta::Split {
663            runs: 2,
664            num_rle_values: 5,
665        }
666    }
667
668    #[rstest]
669    #[case::present(StreamType::Present, "present")]
670    #[case::data_none(StreamType::Data(DictionaryType::None), "data")]
671    #[case::data_single(StreamType::Data(DictionaryType::Single), "data[single]")]
672    #[case::data_shared(StreamType::Data(DictionaryType::Shared), "data[shared]")]
673    #[case::data_vertex(StreamType::Data(DictionaryType::Vertex), "data[vertex]")]
674    #[case::data_morton(StreamType::Data(DictionaryType::Morton), "data[morton]")]
675    #[case::data_fsst(StreamType::Data(DictionaryType::Fsst), "data[fsst]")]
676    #[case::offset_vertex(StreamType::Offset(OffsetType::Vertex), "offset[vertex]")]
677    #[case::offset_index(StreamType::Offset(OffsetType::Index), "offset[index]")]
678    #[case::offset_string(StreamType::Offset(OffsetType::String), "offset[string]")]
679    #[case::offset_key(StreamType::Offset(OffsetType::Key), "offset[key]")]
680    #[case::length_var_binary(StreamType::Length(LengthType::VarBinary), "length[var-binary]")]
681    #[case::length_geometries(StreamType::Length(LengthType::Geometries), "length[geometries]")]
682    #[case::length_parts(StreamType::Length(LengthType::Parts), "length[parts]")]
683    #[case::length_rings(StreamType::Length(LengthType::Rings), "length[rings]")]
684    #[case::length_triangles(StreamType::Length(LengthType::Triangles), "length[triangles]")]
685    #[case::length_symbol(StreamType::Length(LengthType::Symbol), "length[symbol]")]
686    #[case::length_dictionary(StreamType::Length(LengthType::Dictionary), "length[dictionary]")]
687    fn every_stream_type_renders_its_wire_label(
688        #[case] stream_type: StreamType,
689        #[case] expected: &str,
690    ) {
691        assert_eq!(stream_type.to_string(), expected);
692    }
693
694    #[cfg(feature = "unstable-v2")]
695    #[test]
696    fn a_nested_length_stream_renders_its_wire_label() {
697        assert_eq!(
698            StreamType::Length(LengthType::Nested).to_string(),
699            "length[nested]"
700        );
701    }
702
703    #[rstest]
704    #[case::int_none(LogicalEncoding::Int(IntLogical::None), "int/none")]
705    #[case::int_delta(LogicalEncoding::Int(IntLogical::Delta), "int/delta")]
706    #[case::int_rle(LogicalEncoding::Int(IntLogical::Rle(rle())), "int/rle")]
707    #[case::int_delta_rle(LogicalEncoding::Int(IntLogical::DeltaRle(rle())), "int/delta-rle")]
708    #[case::bool_none(LogicalEncoding::Bool(BoolLogical::None), "bool/none")]
709    #[case::bool_byte_rle(LogicalEncoding::Bool(BoolLogical::ByteRle(rle())), "bool/byte-rle")]
710    #[case::float_none(LogicalEncoding::Float(FloatLogical::None), "float/none")]
711    #[case::float_dict(LogicalEncoding::Float(FloatLogical::Dict), "float/dict")]
712    #[case::float_alp(LogicalEncoding::Float(FloatLogical::Alp(alp())), "float/alp")]
713    #[case::vertex_none(LogicalEncoding::Vertex(VertexLogical::None), "vertex/none")]
714    #[case::vertex_delta(LogicalEncoding::Vertex(VertexLogical::Delta), "vertex/delta")]
715    #[case::vertex_componentwise_delta(
716        LogicalEncoding::Vertex(VertexLogical::ComponentwiseDelta),
717        "vertex/componentwise-delta"
718    )]
719    #[case::vertex_morton_delta(
720        LogicalEncoding::Vertex(VertexLogical::MortonDelta(morton())),
721        "vertex/morton-delta"
722    )]
723    fn every_logical_encoding_renders_kind_then_encoding(
724        #[case] encoding: LogicalEncoding,
725        #[case] expected: &str,
726    ) {
727        assert_eq!(encoding.to_string(), expected);
728    }
729
730    #[cfg(feature = "unstable-v2")]
731    #[rstest]
732    #[case::none(XyzLogical::None, "vertex/xyz-none")]
733    #[case::delta(XyzLogical::Delta, "vertex/xyz-delta")]
734    #[case::componentwise_delta(XyzLogical::ComponentwiseDelta, "vertex/xyz-componentwise-delta")]
735    fn every_xyz_encoding_renders_kind_then_encoding(
736        #[case] xyz: XyzLogical,
737        #[case] expected: &str,
738    ) {
739        let step = ZStep::new(0).unwrap();
740        assert_eq!(
741            LogicalEncoding::Vertex(VertexLogical::Xyz(step, xyz)).to_string(),
742            expected
743        );
744    }
745
746    #[rstest]
747    #[case::none(PhysicalEncoding::None, "none")]
748    #[case::varint(PhysicalEncoding::VarInt, "varint")]
749    #[case::fastpfor_256be(
750        PhysicalEncoding::FastPFor(FastPForKind::Block256Be),
751        "fastpfor[256be]"
752    )]
753    fn every_physical_encoding_renders_its_wire_label(
754        #[case] encoding: PhysicalEncoding,
755        #[case] expected: &str,
756    ) {
757        assert_eq!(encoding.to_string(), expected);
758    }
759
760    #[cfg(feature = "unstable-v2")]
761    #[rstest]
762    #[case::fastpfor_128le(
763        PhysicalEncoding::FastPFor(FastPForKind::Block128Le),
764        "fastpfor[128le]"
765    )]
766    #[case::bit_packed(PhysicalEncoding::BitPacked, "bit-packed")]
767    fn every_v2_physical_encoding_renders_its_wire_label(
768        #[case] encoding: PhysicalEncoding,
769        #[case] expected: &str,
770    ) {
771        assert_eq!(encoding.to_string(), expected);
772    }
773
774    #[rstest]
775    #[case::ints(LogicalEncoding::Int(IntLogical::Delta), 6, 6)]
776    #[case::pairs(LogicalEncoding::Vertex(VertexLogical::ComponentwiseDelta), 6, 3)]
777    #[case::morton_codes(LogicalEncoding::Vertex(VertexLogical::MortonDelta(morton())), 6, 6)]
778    #[cfg_attr(feature = "unstable-v2", case::triples(
779        LogicalEncoding::Vertex(VertexLogical::Xyz(ZStep::new(0).unwrap(), XyzLogical::Delta)),
780        6,
781        2
782    ))]
783    fn a_stream_counts_its_words_as_values(
784        #[case] logical: LogicalEncoding,
785        #[case] words: usize,
786        #[case] values: u32,
787    ) {
788        let meta = StreamMeta::new2(
789            StreamType::Present,
790            logical,
791            PhysicalEncoding::VarInt,
792            words,
793        )
794        .unwrap();
795        assert_eq!(meta.num_values, values);
796        assert_eq!(meta.num_words().unwrap().into_usize(), words);
797    }
798
799    #[rstest]
800    #[case::pairs(LogicalEncoding::Vertex(VertexLogical::Delta), 5, 2)]
801    #[cfg_attr(feature = "unstable-v2", case::triples(
802        LogicalEncoding::Vertex(VertexLogical::Xyz(ZStep::new(0).unwrap(), XyzLogical::Delta)),
803        4,
804        3
805    ))]
806    fn a_stream_of_partial_vertices_is_rejected(
807        #[case] logical: LogicalEncoding,
808        #[case] words: u32,
809        #[case] per_vertex: u32,
810    ) {
811        let err = StreamMeta::new2(
812            StreamType::Present,
813            logical,
814            PhysicalEncoding::VarInt,
815            words.into_usize(),
816        )
817        .unwrap_err();
818        assert!(
819            matches!(err, MltError::PartialVertex(w, p) if w == words && p == per_vertex),
820            "{err:?}"
821        );
822    }
823
824    #[rstest]
825    #[case::int(LogicalEncoding::Int(IntLogical::None), ValueKind::Int)]
826    #[case::bool(LogicalEncoding::Bool(BoolLogical::None), ValueKind::Bool)]
827    #[case::float(LogicalEncoding::Float(FloatLogical::None), ValueKind::Float)]
828    #[case::vertex(LogicalEncoding::Vertex(VertexLogical::None), ValueKind::Vertex)]
829    fn the_identity_encoding_of_a_kind_reports_that_kind_back(
830        #[case] expected: LogicalEncoding,
831        #[case] kind: ValueKind,
832    ) {
833        let none = LogicalEncoding::none(kind);
834        assert_eq!(none, expected);
835        assert_eq!(none.kind(), kind);
836        assert!(none.is_identity());
837    }
838}