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