mlt_core/decoder/property/model.rs
1use std::borrow::Cow;
2use std::ops::Deref;
3#[cfg(feature = "unstable-v2")]
4use std::rc::Rc;
5
6#[cfg(feature = "unstable-v2")]
7use bitvec::order::Lsb0;
8#[cfg(feature = "unstable-v2")]
9use bitvec::slice::BitSlice;
10#[cfg(feature = "unstable-v2")]
11use bitvec::vec::BitVec;
12use enum_dispatch::enum_dispatch;
13
14#[cfg(feature = "unstable-v2")]
15use crate::decoder::Alp;
16use crate::decoder::RawStream;
17use crate::utils::Presence;
18use crate::{DecodeState, Lazy, PropKind};
19
20/// Property column representation, parameterized by decode state.
21///
22/// - `Property<'a>` / `Property<'a, Lazy>` - either raw bytes or decoded, in an [`crate::LazyParsed`] enum.
23/// - `Property<'a, Parsed>` - decoded [`ParsedProperty`] directly (no enum wrapper).
24pub type Property<'a, S = Lazy> =
25 <S as DecodeState>::LazyOrParsed<RawProperty<'a>, ParsedProperty<'a>>;
26
27/// Raw scalar column (bool, integer, or float) as read directly from the tile.
28#[derive(Debug, Clone, PartialEq)]
29pub struct RawScalar<'a> {
30 pub(crate) name: &'a str,
31 pub(crate) presence: RawPresence<'a>,
32 pub(crate) data: RawStream<'a>,
33}
34
35impl<'a> RawScalar<'a> {
36 pub(crate) fn new(name: &'a str, presence: RawPresence<'a>, data: RawStream<'a>) -> Self {
37 Self {
38 name,
39 presence,
40 data,
41 }
42 }
43}
44
45/// Raw float column as read directly from the tile.
46/// Its encodings differ in how many streams they use, so it carries an encoding enum like [`RawStrings`] rather than one stream.
47#[derive(Debug, Clone, PartialEq)]
48pub struct RawFloats<'a> {
49 pub name: &'a str,
50 pub presence: RawPresence<'a>,
51 pub encoding: RawFloatsEncoding<'a>,
52}
53
54impl<'a> RawFloats<'a> {
55 /// A column stored as one data stream.
56 pub(crate) fn single(name: &'a str, presence: RawPresence<'a>, data: RawStream<'a>) -> Self {
57 Self {
58 name,
59 presence,
60 encoding: RawFloatsEncoding::Single(data),
61 }
62 }
63}
64
65/// Raw encoding payload for a float column.
66#[derive(Debug, Clone, PartialEq)]
67pub enum RawFloatsEncoding<'a> {
68 /// One data stream, its own logical encoding saying how to read it.
69 Single(RawStream<'a>),
70 /// Codes first, then the distinct values they index.
71 ///
72 /// Requires the `unstable-v2` feature.
73 #[cfg(feature = "unstable-v2")]
74 Dictionary {
75 codes: RawStream<'a>,
76 dictionary: RawStream<'a>,
77 },
78 /// Integers scaled by a power of ten, in one stream, with the parameters in its header.
79 ///
80 /// Requires the `unstable-v2` feature.
81 #[cfg(feature = "unstable-v2")]
82 Alp { params: Alp, data: RawStream<'a> },
83}
84
85/// Raw string column as read directly from the tile.
86#[derive(Debug, Clone, PartialEq)]
87pub struct RawStrings<'a> {
88 pub name: &'a str,
89 pub presence: RawPresence<'a>,
90 pub encoding: RawStringsEncoding<'a>,
91}
92
93/// Raw encoding payload for a string column (plain, dictionary, or FSST variants).
94///
95/// `RawStream` order matches the encoder: see `StringEncoder.encode()`.
96#[derive(Debug, Clone, PartialEq)]
97pub enum RawStringsEncoding<'a> {
98 /// Plain: length stream + data stream
99 Plain(RawPlainData<'a>),
100 /// Dictionary: lengths + offsets + dictionary data
101 Dictionary {
102 plain_data: RawPlainData<'a>,
103 offsets: RawStream<'a>,
104 dict: DictLayout,
105 },
106 /// FSST plain (4 streams): symbol lengths, symbol table, value lengths, compressed corpus. No offsets.
107 FsstPlain(RawFsstData<'a>),
108 /// FSST dictionary (5 streams): symbol lengths, symbol table, value lengths, compressed corpus, offsets.
109 FsstDictionary {
110 fsst_data: RawFsstData<'a>,
111 offsets: RawStream<'a>,
112 dict: DictLayout,
113 },
114}
115
116/// How a dictionary's entries sit in its blob.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, strum::EnumIter)]
118pub enum DictLayout {
119 /// Entries back to back, the lengths stream holding one length each.
120 Plain,
121 /// Entries with the prefix each shares with its predecessor factored out, so the blob holds
122 /// only suffixes and the lengths stream holds every prefix length then every suffix length.
123 #[cfg(feature = "unstable-v2")]
124 FrontCoded,
125}
126
127/// How a string column's values sit in the tile, as the extension bits of its
128/// leading stream name.
129///
130/// Decoding reconstructs the values and resolves this away, so it is readable
131/// only from a column that has not been decoded yet.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
133pub enum StringLayout {
134 /// Lengths, then the values' bytes.
135 Plain,
136 /// Codes, then the distinct values' lengths and bytes.
137 Dict,
138 /// Lengths, then the FSST symbol table and the compressed corpus.
139 Fsst,
140 /// Codes, then the distinct values' lengths, the FSST symbol table and the corpus.
141 FsstDict,
142}
143
144/// What a column declares on the wire, before decoding resolves it away.
145///
146/// Decoding reconstructs values and drops how they were stored: every id widens to
147/// `u64`, and a presence field is consumed into the values themselves. A caller
148/// reporting on a tile therefore has to read this off the parsed-but-not-decoded
149/// stage, which is the only one that still represents the data as written.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum ColumnDecl {
152 /// The layer's id column, 64-bit or 32-bit.
153 Id { wide: bool, optional: bool },
154 /// A property column, by the type its values decode to.
155 Value { kind: PropKind, optional: bool },
156}
157
158/// How a column stores its values, beyond what its streams' encodings already say.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
160pub struct ColumnStorage {
161 /// The layout of a string or shared-dictionary column; `None` for any other column.
162 pub string: Option<StringLayout>,
163 /// How the column's dictionary blob is laid out, when it carries one.
164 pub dictionary: Option<DictLayout>,
165}
166
167/// Raw encoding payload for a `SharedDict` column.
168///
169/// Unlike [`RawStringsEncoding`], shared dictionaries do NOT have their own offset stream.
170/// Instead, each child column has its own offset stream that references the shared dictionary.
171/// This is why only `Plain` and `FsstPlain` variants exist here.
172#[derive(Debug, Clone, PartialEq)]
173pub enum RawSharedDictEncoding<'a> {
174 /// Plain shared dict (2 streams): lengths + data.
175 Plain(RawPlainData<'a>),
176 /// FSST plain shared dict (4 streams): symbol lengths, symbol table, lengths, corpus.
177 FsstPlain(RawFsstData<'a>),
178}
179
180/// Raw shared-dictionary column as read directly from the tile.
181#[derive(Debug, Clone, PartialEq)]
182pub struct RawSharedDict<'a> {
183 pub name: &'a str,
184 pub encoding: RawSharedDictEncoding<'a>,
185 pub dict: DictLayout,
186 pub children: Vec<RawSharedDictItem<'a>>,
187}
188
189/// Raw property data as read directly from the tile.
190#[derive(Debug, PartialEq, Clone)]
191pub enum RawProperty<'a> {
192 Bool(RawScalar<'a>),
193 I8(RawScalar<'a>),
194 U8(RawScalar<'a>),
195 I32(RawScalar<'a>),
196 U32(RawScalar<'a>),
197 I64(RawScalar<'a>),
198 U64(RawScalar<'a>),
199 F32(RawFloats<'a>),
200 F64(RawFloats<'a>),
201 Str(RawStrings<'a>),
202 SharedDict(RawSharedDict<'a>),
203}
204
205/// Parsed property values in a typed enum form.
206#[derive(Clone, Debug, PartialEq, strum::IntoStaticStr)]
207#[strum(serialize_all = "snake_case")]
208#[enum_dispatch(Analyze)]
209pub enum ParsedProperty<'a> {
210 Bool(ParsedScalar<'a, bool>),
211 I8(ParsedScalar<'a, i8>),
212 U8(ParsedScalar<'a, u8>),
213 I32(ParsedScalar<'a, i32>),
214 U32(ParsedScalar<'a, u32>),
215 I64(ParsedScalar<'a, i64>),
216 U64(ParsedScalar<'a, u64>),
217 F32(ParsedScalar<'a, f32>),
218 F64(ParsedScalar<'a, f64>),
219 Str(ParsedStrings<'a>),
220 SharedDict(ParsedSharedDict<'a>),
221}
222
223macro_rules! impl_parsed_property_kind {
224 (
225 scalar { $($sv:ident),* $(,)? }
226 string { $($gv:ident),* $(,)? }
227 ) => {
228 impl ParsedProperty<'_> {
229 /// The column's data type. A shared dictionary is a way of storing strings
230 /// rather than a type of its own, so it reads as [`PropKind::Str`].
231 #[must_use]
232 pub fn kind(&self) -> PropKind {
233 match self {
234 $(Self::$sv(_) => PropKind::$sv,)*
235 $(Self::$gv(_) => PropKind::$gv,)*
236 Self::SharedDict(_) => PropKind::Str,
237 }
238 }
239 }
240 };
241}
242
243with_kinds!(impl_parsed_property_kind);
244
245/// Decoded scalar property column (bool, integer, or float).
246///
247/// `presence` carries both the optional bitvector and the dense values.
248/// For a non-optional column, `presence` is [`Presence::AllPresent`] with all
249/// values inline. For an optional column, `presence` is [`Presence::Bits`] with
250/// `bits.count_ones() == values.len()`.
251#[derive(Clone, Debug, PartialEq)]
252pub struct ParsedScalar<'a, T: Copy + PartialEq> {
253 pub(crate) name: &'a str,
254 pub(crate) presence: Presence<'a, T>,
255}
256impl<'a, T: Copy + PartialEq> Deref for ParsedScalar<'a, T> {
257 type Target = Presence<'a, T>;
258 #[inline]
259 fn deref(&self) -> &Self::Target {
260 &self.presence
261 }
262}
263
264/// Per-feature byte range into a shared dictionary corpus.
265///
266/// `start` and `end` are signed byte offsets into the corpus string.
267/// The sentinel value [`DictRange::NULL`] (`-1, -1`) indicates a NULL (absent) entry.
268/// Equal `start` and `end` indicate an empty string.
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
271pub struct DictRange {
272 pub start: i32,
273 pub end: i32,
274}
275impl DictRange {
276 /// Sentinel value indicating a NULL (absent) entry.
277 pub const NULL: Self = Self { start: -1, end: -1 };
278}
279
280/// A single sub-property within a shared dictionary parsed value.
281#[derive(Debug, Clone, PartialEq, Eq)]
282#[cfg_attr(all(not(test), feature = "arbitrary"), derive(arbitrary::Arbitrary))]
283pub struct ParsedSharedDictItem<'a> {
284 /// The suffix name of this sub-property (appended to parent struct name).
285 pub(crate) suffix: &'a str,
286 /// Per-feature byte ranges into the parsed shared corpus.
287 /// Non-null entries indicate a present string stored as
288 /// `shared_dict.corpus()[start..end]`.
289 /// [`DictRange::NULL`] indicates a NULL value.
290 /// Equal `start` and `end` indicate an empty string.
291 pub(crate) ranges: Vec<DictRange>,
292}
293
294/// Parsed string values for a single property.
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct ParsedStrings<'a> {
297 pub(crate) name: &'a str,
298 /// Per-feature cumulative end offsets into `data`.
299 /// Non-negative values indicate a present string and store its exclusive
300 /// end offset in `data`.
301 /// Negative values indicate NULL and encode the current offset as `-end-1`,
302 /// which is equivalent to `!end` in two's-complement form,
303 /// so the next item can still recover its start offset without scanning back
304 /// to the previous non-null value. This allows even the first item to be NULL.
305 /// In other words, if `lengths == [5, 5, -6, 8]`, then the strings are:
306 /// ```ignore
307 /// data[0..5], // 0th string
308 /// data[5..5], // 1st string is empty
309 /// NULL, // 2nd string, offset stays 5 because -6 == -5-1
310 /// data[5..8], // 3rd string
311 /// ```
312 pub(crate) lengths: Vec<i32>,
313 pub(crate) data: Cow<'a, str>,
314}
315
316/// Parsed shared dictionary payload shared by one or more child string properties.
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct ParsedSharedDict<'a> {
319 pub(crate) prefix: &'a str,
320 pub(crate) data: Cow<'a, str>,
321 pub(crate) items: Vec<ParsedSharedDictItem<'a>>,
322}
323
324/// A single child field within a `SharedDict` raw column
325#[derive(Clone, Debug, PartialEq)]
326pub struct RawSharedDictItem<'a> {
327 pub name: &'a str,
328 pub presence: RawPresence<'a>,
329 pub data: RawStream<'a>,
330}
331
332/// Raw plain data (length stream + data stream) borrowed from input bytes.
333#[derive(Debug, Clone, PartialEq)]
334pub struct RawPlainData<'a> {
335 pub lengths: RawStream<'a>,
336 pub data: RawStream<'a>,
337}
338
339/// Raw FSST-compressed data (4 streams) borrowed from input bytes.
340#[derive(Debug, Clone, PartialEq)]
341pub struct RawFsstData<'a> {
342 pub symbol_lengths: RawStream<'a>,
343 pub symbol_table: RawStream<'a>,
344 pub lengths: RawStream<'a>,
345 pub corpus: RawStream<'a>,
346}
347
348/// Raw presence/nullability data for a column.
349///
350/// `AllPresent` represents a non-optional column; other variants encode presence in a
351/// layer-format-specific way. Decode through [`RawPresence::decode_bits`]
352/// instead of matching on it.
353#[derive(Debug, Clone, PartialEq, Default)]
354pub enum RawPresence<'a> {
355 /// Non-optional column - every feature has a value; nothing is stored.
356 #[default]
357 AllPresent,
358 /// Tag `0x01`: bool-RLE presence stream with a full stream header.
359 Stream(RawStream<'a>),
360 /// Tag `0x02`: raw packed bitfield (LSB-first per byte, one bit per feature),
361 /// borrowed zero-copy from the tile bytes. No header precedes it.
362 /// Requires the `unstable-v2` feature.
363 #[cfg(feature = "unstable-v2")]
364 Bitfield(&'a BitSlice<u8, Lsb0>),
365 /// Tag `0x02`: presence that arrived run-coded or as a list of indices, so the
366 /// bits had to be built rather than borrowed.
367 ///
368 /// Behind an [`Rc`] because a shared field is read by every column that names it,
369 /// and one materialisation is enough for all of them: resolving a column's presence
370 /// costs a reference count either way, and only a reader that actually asks for the
371 /// bits of a field another column still holds pays to copy them.
372 /// Requires the `unstable-v2` feature.
373 #[cfg(feature = "unstable-v2")]
374 Decoded(Rc<BitVec<u8, Lsb0>>),
375}
376
377#[cfg(feature = "unstable-v2")]
378impl<'a> RawPresence<'a> {
379 /// Keep bits that were borrowed borrowed, and bits that were built built.
380 pub(crate) fn from_bits(bits: Cow<'a, BitSlice<u8, Lsb0>>) -> Self {
381 match bits {
382 Cow::Borrowed(bits) => Self::Bitfield(bits),
383 Cow::Owned(bits) => Self::Decoded(Rc::new(bits)),
384 }
385 }
386}
387
388impl RawPresence<'_> {
389 /// Whether this column carries presence data (some features may be null).
390 #[must_use]
391 pub(crate) fn is_optional(&self) -> bool {
392 !matches!(self, Self::AllPresent)
393 }
394}
395
396impl RawProperty<'_> {
397 /// How this column stores its values, or `None` when it has no layout to report.
398 #[must_use]
399 pub(crate) fn storage(&self) -> Option<ColumnStorage> {
400 let (string, dictionary) = match self {
401 Self::Str(column) => match &column.encoding {
402 RawStringsEncoding::Plain(_) => (StringLayout::Plain, None),
403 RawStringsEncoding::Dictionary { dict, .. } => (StringLayout::Dict, Some(*dict)),
404 RawStringsEncoding::FsstPlain(_) => (StringLayout::Fsst, None),
405 RawStringsEncoding::FsstDictionary { dict, .. } => {
406 (StringLayout::FsstDict, Some(*dict))
407 }
408 },
409 // A shared dictionary's children are dictionary-coded strings, so it reports
410 // the layout its corpus is stored with, as a lone string column would.
411 Self::SharedDict(column) => match &column.encoding {
412 RawSharedDictEncoding::Plain(_) => (StringLayout::Dict, Some(column.dict)),
413 RawSharedDictEncoding::FsstPlain(_) => (StringLayout::FsstDict, Some(column.dict)),
414 },
415 Self::Bool(_)
416 | Self::I8(_)
417 | Self::U8(_)
418 | Self::I32(_)
419 | Self::U32(_)
420 | Self::I64(_)
421 | Self::U64(_)
422 | Self::F32(_)
423 | Self::F64(_) => return None,
424 };
425 Some(ColumnStorage {
426 string: Some(string),
427 dictionary,
428 })
429 }
430}
431
432impl RawProperty<'_> {
433 /// Call `cb` with what this column declares. A shared dictionary stores strings, so
434 /// it reads as [`PropKind::Str`], as the decoded form does.
435 pub(crate) fn for_each_decl(&self, cb: &mut dyn FnMut(ColumnDecl)) {
436 let (kind, optional) = match self {
437 Self::Bool(c) => (PropKind::Bool, c.presence.is_optional()),
438 Self::I8(c) => (PropKind::I8, c.presence.is_optional()),
439 Self::U8(c) => (PropKind::U8, c.presence.is_optional()),
440 Self::I32(c) => (PropKind::I32, c.presence.is_optional()),
441 Self::U32(c) => (PropKind::U32, c.presence.is_optional()),
442 Self::I64(c) => (PropKind::I64, c.presence.is_optional()),
443 Self::U64(c) => (PropKind::U64, c.presence.is_optional()),
444 Self::F32(c) => (PropKind::F32, c.presence.is_optional()),
445 Self::F64(c) => (PropKind::F64, c.presence.is_optional()),
446 Self::Str(c) => (PropKind::Str, c.presence.is_optional()),
447 // A shared dictionary holds no values of its own and declares no presence:
448 // its children are the columns, and each carries its own.
449 Self::SharedDict(c) => {
450 for child in &c.children {
451 cb(ColumnDecl::Value {
452 kind: PropKind::Str,
453 optional: child.presence.is_optional(),
454 });
455 }
456 return;
457 }
458 };
459 cb(ColumnDecl::Value { kind, optional });
460 }
461}