Skip to main content

meta_language/storage/
doublets.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use ::doublets::{Doublets, DoubletsExt, Links};
6use platform_mem::FileMapped;
7
8use crate::link_flags::LinkFlags;
9use crate::link_network::{Link, LinkId, LinkMetadata, LinkNetwork, LinkType};
10use crate::source::{ByteRange, Point, SourceSpan};
11
12use super::{LinkStore, LinkStoreQuery, StorageError};
13
14impl From<::doublets::Error<u64>> for StorageError {
15    fn from(error: ::doublets::Error<u64>) -> Self {
16        Self::Doublets(error.to_string())
17    }
18}
19
20#[cfg(feature = "doublets")]
21type FileMappedDoubletsStore =
22    ::doublets::unit::Store<u64, FileMapped<::doublets::parts::LinkPart<u64>>>;
23
24#[cfg(feature = "doublets")]
25const TAG_HEADER: u64 = u64::MAX - 1_024;
26#[cfg(feature = "doublets")]
27const TAG_REFERENCE: u64 = u64::MAX - 1_025;
28#[cfg(feature = "doublets")]
29const TAG_METADATA_BYTE: u64 = u64::MAX - 1_026;
30#[cfg(feature = "doublets")]
31const METADATA_VERSION: u8 = 1;
32#[cfg(feature = "doublets")]
33const SNAPSHOT_MAGIC: &[u8; 8] = b"MLDSNP01";
34
35#[cfg(feature = "doublets")]
36#[derive(Debug)]
37struct StoredLinkRecord {
38    sequence: u64,
39    link: Link,
40    registered_term: bool,
41    deleted: bool,
42}
43
44/// File-mapped binary storage backed by `doublets-rs`.
45#[cfg(feature = "doublets")]
46pub struct DoubletsLinkStore {
47    path: PathBuf,
48    store: FileMappedDoubletsStore,
49}
50
51#[cfg(feature = "doublets")]
52impl DoubletsLinkStore {
53    /// Creates a new empty file-mapped doublets store at `path`.
54    ///
55    /// Any existing file at `path` is removed first.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`StorageError`] when the file cannot be created or initialized.
60    pub fn create_file(path: impl AsRef<Path>) -> Result<Self, StorageError> {
61        let path = path.as_ref();
62        if path.exists() {
63            fs::remove_file(path)?;
64        }
65        let snapshot_path = snapshot_path(path);
66        if snapshot_path.exists() {
67            fs::remove_file(snapshot_path)?;
68        }
69        Self::open_file(path)
70    }
71
72    /// Opens an existing file-mapped doublets store, creating the file when it
73    /// does not yet exist.
74    ///
75    /// # Errors
76    ///
77    /// Returns [`StorageError`] when the file cannot be opened or the doublets
78    /// store cannot be initialized.
79    pub fn open_file(path: impl AsRef<Path>) -> Result<Self, StorageError> {
80        let path = path.as_ref().to_path_buf();
81        let mem = FileMapped::from_path(&path)?;
82        let store = ::doublets::unit::Store::<u64, _>::new(mem)?;
83        let mut this = Self { path, store };
84        for (link, registered_term) in read_snapshot(&this.path)? {
85            this.append_record(
86                link.id(),
87                link.references(),
88                link.metadata(),
89                registered_term,
90                false,
91            )?;
92        }
93        Ok(this)
94    }
95
96    /// Replaces the logical binary contents with a lossless encoding of
97    /// `network`.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`StorageError`] when the file-mapped doublets store cannot be
102    /// updated.
103    pub fn replace_with_network(&mut self, network: &LinkNetwork) -> Result<(), StorageError> {
104        for record in self.active_records()? {
105            self.append_record(record.link.id(), &[], &LinkMetadata::new(), false, true)?;
106        }
107        for link in network.links() {
108            let registered_term = link
109                .metadata()
110                .term()
111                .is_some_and(|term| network.find_term(term) == Some(link.id()));
112            self.append_record(
113                link.id(),
114                link.references(),
115                link.metadata(),
116                registered_term,
117                false,
118            )?;
119        }
120        self.persist_snapshot()
121    }
122
123    /// Returns the durable companion snapshot path for a doublets store path.
124    #[must_use]
125    pub fn snapshot_path(path: impl AsRef<Path>) -> PathBuf {
126        snapshot_path(path.as_ref())
127    }
128
129    /// Reconstructs a [`LinkNetwork`] from this binary store.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`StorageError`] when the binary data is malformed.
134    pub fn to_network(&self) -> Result<LinkNetwork, StorageError> {
135        let mut links = self
136            .active_records()?
137            .into_iter()
138            .map(|record| (record.link, record.registered_term))
139            .collect::<Vec<_>>();
140        links.sort_by_key(|(link, _registered)| link.id());
141        Ok(super::network_from_stored_links(links))
142    }
143
144    /// Creates a logical link using an explicit id.
145    ///
146    /// This is used by network import paths to preserve text and binary id
147    /// equivalence. Normal callers can use [`LinkStore::create`].
148    ///
149    /// # Errors
150    ///
151    /// Returns [`StorageError`] when the id already exists or the backend
152    /// cannot write the physical records.
153    pub fn create_with_id(
154        &mut self,
155        id: LinkId,
156        references: &[LinkId],
157        metadata: &LinkMetadata,
158        registered_term: bool,
159    ) -> Result<(), StorageError> {
160        if self.latest_record(id)?.is_some() {
161            return Err(StorageError::Corrupt(format!(
162                "link id {id} already exists in doublets store"
163            )));
164        }
165        self.append_record(id, references, metadata, registered_term, false)?;
166        self.persist_snapshot()
167    }
168
169    fn persist_snapshot(&self) -> Result<(), StorageError> {
170        write_snapshot(&self.path, &self.active_records()?)
171    }
172
173    fn active_records(&self) -> Result<Vec<StoredLinkRecord>, StorageError> {
174        let mut latest = BTreeMap::<LinkId, StoredLinkRecord>::new();
175        for record in self.decode_all_records()? {
176            let replace = latest
177                .get(&record.link.id())
178                .map_or(true, |existing| existing.sequence < record.sequence);
179            if replace {
180                latest.insert(record.link.id(), record);
181            }
182        }
183        Ok(latest
184            .into_values()
185            .filter(|record| !record.deleted)
186            .collect())
187    }
188
189    fn latest_record(&self, id: LinkId) -> Result<Option<StoredLinkRecord>, StorageError> {
190        let latest = self
191            .decode_records_for_id(id)?
192            .into_iter()
193            .max_by_key(|record| record.sequence);
194        Ok(latest.filter(|record| !record.deleted))
195    }
196
197    fn decode_records_for_id(&self, id: LinkId) -> Result<Vec<StoredLinkRecord>, StorageError> {
198        Ok(self
199            .decode_all_records()?
200            .into_iter()
201            .filter(|record| record.link.id() == id)
202            .collect())
203    }
204
205    fn append_record(
206        &mut self,
207        id: LinkId,
208        references: &[LinkId],
209        metadata: &LinkMetadata,
210        registered_term: bool,
211        deleted: bool,
212    ) -> Result<(), StorageError> {
213        self.encode_record(id, references, metadata, registered_term, deleted)
214    }
215
216    fn next_logical_id(&self) -> Result<LinkId, StorageError> {
217        let max_id = self
218            .decode_all_records()?
219            .into_iter()
220            .map(|record| record.link.id().as_u64())
221            .max()
222            .unwrap_or(0);
223        let next = max_id
224            .checked_add(1)
225            .ok_or_else(|| StorageError::Corrupt("link id space is exhausted".to_string()))?;
226        Ok(LinkId::from_u64(next))
227    }
228
229    fn headers(&self) -> Vec<::doublets::Link<u64>> {
230        let any = self.store.constants().any;
231        self.store
232            .each_iter([any, TAG_HEADER, any])
233            .collect::<Vec<_>>()
234    }
235
236    fn encode_record(
237        &mut self,
238        id: LinkId,
239        references: &[LinkId],
240        metadata: &LinkMetadata,
241        registered_term: bool,
242        deleted: bool,
243    ) -> Result<(), StorageError> {
244        let nonce = self.store.create_point()?;
245        let id_link = self.store.create_link(nonce, id.as_u64())?;
246        let header = self.store.create_link(TAG_HEADER, id_link)?;
247        for (position, reference) in references.iter().enumerate() {
248            let position = stored_position(position)?;
249            let entry = self.store.create_link(TAG_REFERENCE, position)?;
250            let value = self.store.create_link(entry, reference.as_u64())?;
251            self.store.create_link(header, value)?;
252        }
253
254        let metadata = encode_metadata(metadata, registered_term, deleted)?;
255        for (position, byte) in metadata.iter().enumerate() {
256            let position = stored_position(position)?;
257            let entry = self.store.create_link(TAG_METADATA_BYTE, position)?;
258            let value = self.store.create_link(entry, u64::from(*byte) + 1)?;
259            self.store.create_link(header, value)?;
260        }
261        Ok(())
262    }
263
264    fn decode_all_records(&self) -> Result<Vec<StoredLinkRecord>, StorageError> {
265        self.headers()
266            .into_iter()
267            .map(|header| self.decode_header(&header))
268            .collect()
269    }
270
271    fn decode_header(
272        &self,
273        header: &::doublets::Link<u64>,
274    ) -> Result<StoredLinkRecord, StorageError> {
275        let mut references = BTreeMap::new();
276        let mut metadata_bytes = BTreeMap::new();
277        let any = self.store.constants().any;
278        let id_link = self.store.get_link(header.target).ok_or_else(|| {
279            StorageError::Corrupt(format!(
280                "record header {} references missing id link {}",
281                header.index, header.target
282            ))
283        })?;
284        let logical_id = LinkId::from_u64(id_link.target);
285
286        for association in self.store.each_iter([any, header.index, any]) {
287            let value = self.store.get_link(association.target).ok_or_else(|| {
288                StorageError::Corrupt(format!(
289                    "record {logical_id} references missing value link {}",
290                    association.target
291                ))
292            })?;
293            let entry = self.store.get_link(value.source).ok_or_else(|| {
294                StorageError::Corrupt(format!(
295                    "record {logical_id} references missing entry link {}",
296                    value.source
297                ))
298            })?;
299            match entry.source {
300                TAG_REFERENCE => {
301                    references.insert(entry.target, LinkId::from_u64(value.target));
302                }
303                TAG_METADATA_BYTE => {
304                    let byte = value.target.checked_sub(1).ok_or_else(|| {
305                        StorageError::Corrupt("metadata byte cannot be zero".to_string())
306                    })?;
307                    let byte = u8::try_from(byte).map_err(|_| {
308                        StorageError::Corrupt(format!("metadata byte out of range: {byte}"))
309                    })?;
310                    metadata_bytes.insert(entry.target, byte);
311                }
312                other => {
313                    return Err(StorageError::Corrupt(format!(
314                        "unknown doublets field tag {other}"
315                    )));
316                }
317            }
318        }
319
320        let references = ordered_values(references, "reference")?;
321        let metadata_bytes = ordered_values(metadata_bytes, "metadata byte")?;
322        let (metadata, registered_term, deleted) = decode_metadata(&metadata_bytes)?;
323        let link = Link {
324            id: logical_id,
325            references: std::sync::Arc::from(references),
326            metadata,
327        };
328        Ok(StoredLinkRecord {
329            sequence: header.index,
330            link,
331            registered_term,
332            deleted,
333        })
334    }
335}
336
337#[cfg(feature = "doublets")]
338impl LinkStore for DoubletsLinkStore {
339    fn create(
340        &mut self,
341        references: &[LinkId],
342        metadata: LinkMetadata,
343    ) -> Result<LinkId, StorageError> {
344        let id = self.next_logical_id()?;
345        self.append_record(id, references, &metadata, true, false)?;
346        self.persist_snapshot()?;
347        Ok(id)
348    }
349
350    fn read(&self, id: LinkId) -> Result<Option<Link>, StorageError> {
351        Ok(self.latest_record(id)?.map(|record| record.link))
352    }
353
354    fn update(
355        &mut self,
356        id: LinkId,
357        references: &[LinkId],
358        metadata: LinkMetadata,
359    ) -> Result<bool, StorageError> {
360        if self.latest_record(id)?.is_none() {
361            return Ok(false);
362        }
363        self.append_record(id, references, &metadata, true, false)?;
364        self.persist_snapshot()?;
365        Ok(true)
366    }
367
368    fn delete(&mut self, id: LinkId) -> Result<bool, StorageError> {
369        if self.latest_record(id)?.is_none() {
370            return Ok(false);
371        }
372        self.append_record(id, &[], &LinkMetadata::new(), false, true)?;
373        self.persist_snapshot()?;
374        Ok(true)
375    }
376
377    fn search(&self, query: &LinkStoreQuery) -> Result<Vec<Link>, StorageError> {
378        Ok(self
379            .active_records()?
380            .into_iter()
381            .map(|record| record.link)
382            .filter(|link| query.matches(link))
383            .collect())
384    }
385}
386
387#[cfg(feature = "doublets")]
388fn snapshot_path(path: &Path) -> PathBuf {
389    let mut snapshot = path.as_os_str().to_os_string();
390    snapshot.push(".snapshot");
391    PathBuf::from(snapshot)
392}
393
394#[cfg(feature = "doublets")]
395fn write_snapshot(path: &Path, records: &[StoredLinkRecord]) -> Result<(), StorageError> {
396    let mut output = Vec::new();
397    output.extend_from_slice(SNAPSHOT_MAGIC);
398    write_len(&mut output, records.len())?;
399    for record in records {
400        write_u64(&mut output, record.link.id().as_u64());
401        write_len(&mut output, record.link.references().len())?;
402        for reference in record.link.references() {
403            write_u64(&mut output, reference.as_u64());
404        }
405        let metadata = encode_metadata(record.link.metadata(), record.registered_term, false)?;
406        write_len(&mut output, metadata.len())?;
407        output.extend_from_slice(&metadata);
408    }
409    fs::write(snapshot_path(path), output)?;
410    Ok(())
411}
412
413#[cfg(feature = "doublets")]
414fn read_snapshot(path: &Path) -> Result<Vec<(Link, bool)>, StorageError> {
415    let snapshot_path = snapshot_path(path);
416    if !snapshot_path.exists() {
417        return Ok(Vec::new());
418    }
419
420    let bytes = fs::read(snapshot_path)?;
421    let mut cursor = 0;
422    if read_bytes(&bytes, &mut cursor, SNAPSHOT_MAGIC.len())? != SNAPSHOT_MAGIC {
423        return Err(StorageError::Corrupt(
424            "doublets snapshot has an invalid header".to_string(),
425        ));
426    }
427
428    let record_count = read_len(&bytes, &mut cursor)?;
429    let mut records = Vec::with_capacity(record_count);
430    for _ in 0..record_count {
431        let id = LinkId::from_u64(read_u64(&bytes, &mut cursor)?);
432        let reference_count = read_len(&bytes, &mut cursor)?;
433        let mut references = Vec::with_capacity(reference_count);
434        for _ in 0..reference_count {
435            references.push(LinkId::from_u64(read_u64(&bytes, &mut cursor)?));
436        }
437        let metadata_len = read_len(&bytes, &mut cursor)?;
438        let metadata_bytes = read_bytes(&bytes, &mut cursor, metadata_len)?;
439        let (metadata, registered_term, deleted) = decode_metadata(metadata_bytes)?;
440        if deleted {
441            return Err(StorageError::Corrupt(
442                "doublets snapshot cannot contain tombstones".to_string(),
443            ));
444        }
445        let link = Link {
446            id,
447            references: std::sync::Arc::from(references),
448            metadata,
449        };
450        records.push((link, registered_term));
451    }
452
453    if cursor != bytes.len() {
454        return Err(StorageError::Corrupt(
455            "doublets snapshot has trailing bytes".to_string(),
456        ));
457    }
458    Ok(records)
459}
460
461#[cfg(feature = "doublets")]
462fn stored_position(position: usize) -> Result<u64, StorageError> {
463    u64::try_from(position)
464        .ok()
465        .and_then(|position| position.checked_add(1))
466        .ok_or_else(|| StorageError::Corrupt("position does not fit in u64".to_string()))
467}
468
469#[cfg(feature = "doublets")]
470fn ordered_values<T: Copy>(
471    values: BTreeMap<u64, T>,
472    label: &'static str,
473) -> Result<Vec<T>, StorageError> {
474    let mut ordered = Vec::with_capacity(values.len());
475    for (expected, (position, value)) in (1_u64..).zip(values) {
476        if position != expected {
477            return Err(StorageError::Corrupt(format!(
478                "{label} positions must be contiguous; expected {expected}, found {position}"
479            )));
480        }
481        ordered.push(value);
482    }
483    Ok(ordered)
484}
485
486#[cfg(feature = "doublets")]
487fn encode_metadata(
488    metadata: &LinkMetadata,
489    registered_term: bool,
490    deleted: bool,
491) -> Result<Vec<u8>, StorageError> {
492    let mut output = vec![
493        METADATA_VERSION,
494        link_type_code(metadata.link_type()),
495        u8::from(metadata.is_named()),
496        flag_bits(metadata.flags()),
497        u8::from(registered_term),
498        u8::from(deleted),
499    ];
500    write_optional_string(&mut output, metadata.term())?;
501    write_optional_string(&mut output, metadata.definition())?;
502    write_optional_string(&mut output, metadata.language())?;
503    write_optional_span(&mut output, metadata.span())?;
504    Ok(output)
505}
506
507#[cfg(feature = "doublets")]
508fn decode_metadata(bytes: &[u8]) -> Result<(LinkMetadata, bool, bool), StorageError> {
509    let mut cursor = 0;
510    let version = read_u8(bytes, &mut cursor)?;
511    if version != METADATA_VERSION {
512        return Err(StorageError::Corrupt(format!(
513            "unsupported metadata version {version}"
514        )));
515    }
516    let mut metadata = LinkMetadata::new();
517    if let Some(link_type) = parse_link_type_code(read_u8(bytes, &mut cursor)?)? {
518        metadata = metadata.with_link_type(link_type);
519    }
520    metadata = metadata.with_named(read_u8(bytes, &mut cursor)? != 0);
521    let flags = read_u8(bytes, &mut cursor)?;
522    let registered_term = read_u8(bytes, &mut cursor)? != 0;
523    let deleted = read_u8(bytes, &mut cursor)? != 0;
524
525    if let Some(term) = read_optional_string(bytes, &mut cursor)? {
526        metadata = metadata.with_term(term);
527    }
528    if let Some(definition) = read_optional_string(bytes, &mut cursor)? {
529        metadata = metadata.with_definition(definition);
530    }
531    if let Some(language) = read_optional_string(bytes, &mut cursor)? {
532        metadata = metadata.with_language(language);
533    }
534    if let Some(span) = read_optional_span(bytes, &mut cursor)? {
535        metadata = metadata.with_span(span);
536    }
537    if flags != 0 {
538        metadata = metadata.with_flags(parse_flags(flags));
539    }
540    if cursor != bytes.len() {
541        return Err(StorageError::Corrupt(
542            "metadata has trailing bytes".to_string(),
543        ));
544    }
545    Ok((metadata, registered_term, deleted))
546}
547
548#[cfg(feature = "doublets")]
549fn write_optional_string(output: &mut Vec<u8>, value: Option<&str>) -> Result<(), StorageError> {
550    let Some(value) = value else {
551        output.push(0);
552        return Ok(());
553    };
554    output.push(1);
555    write_len(output, value.len())?;
556    output.extend_from_slice(value.as_bytes());
557    Ok(())
558}
559
560#[cfg(feature = "doublets")]
561fn read_optional_string(bytes: &[u8], cursor: &mut usize) -> Result<Option<String>, StorageError> {
562    if read_u8(bytes, cursor)? == 0 {
563        return Ok(None);
564    }
565    let len = read_len(bytes, cursor)?;
566    let value = read_bytes(bytes, cursor, len)?;
567    let value = String::from_utf8(value.to_vec())
568        .map_err(|_| StorageError::Corrupt("metadata string is not UTF-8".to_string()))?;
569    Ok(Some(value))
570}
571
572#[cfg(feature = "doublets")]
573fn write_optional_span(output: &mut Vec<u8>, span: Option<SourceSpan>) -> Result<(), StorageError> {
574    let Some(span) = span else {
575        output.push(0);
576        return Ok(());
577    };
578    output.push(1);
579    let byte_range = span.byte_range();
580    let start = span.start_point();
581    let end = span.end_point();
582    for value in [
583        byte_range.start(),
584        byte_range.end(),
585        start.row(),
586        start.column(),
587        end.row(),
588        end.column(),
589    ] {
590        write_usize(output, value)?;
591    }
592    Ok(())
593}
594
595#[cfg(feature = "doublets")]
596fn read_optional_span(
597    bytes: &[u8],
598    cursor: &mut usize,
599) -> Result<Option<SourceSpan>, StorageError> {
600    if read_u8(bytes, cursor)? == 0 {
601        return Ok(None);
602    }
603    let values = [
604        read_usize(bytes, cursor)?,
605        read_usize(bytes, cursor)?,
606        read_usize(bytes, cursor)?,
607        read_usize(bytes, cursor)?,
608        read_usize(bytes, cursor)?,
609        read_usize(bytes, cursor)?,
610    ];
611    Ok(Some(SourceSpan::new(
612        ByteRange::new(values[0], values[1]),
613        Point::new(values[2], values[3]),
614        Point::new(values[4], values[5]),
615    )))
616}
617
618#[cfg(feature = "doublets")]
619fn write_len(output: &mut Vec<u8>, len: usize) -> Result<(), StorageError> {
620    write_usize(output, len)
621}
622
623#[cfg(feature = "doublets")]
624fn read_len(bytes: &[u8], cursor: &mut usize) -> Result<usize, StorageError> {
625    read_usize(bytes, cursor)
626}
627
628#[cfg(feature = "doublets")]
629fn write_usize(output: &mut Vec<u8>, value: usize) -> Result<(), StorageError> {
630    let value = u64::try_from(value)
631        .map_err(|_| StorageError::Corrupt("usize does not fit in u64".to_string()))?;
632    write_u64(output, value);
633    Ok(())
634}
635
636#[cfg(feature = "doublets")]
637fn read_usize(bytes: &[u8], cursor: &mut usize) -> Result<usize, StorageError> {
638    let value = read_u64(bytes, cursor)?;
639    usize::try_from(value)
640        .map_err(|_| StorageError::Corrupt("u64 does not fit in usize".to_string()))
641}
642
643#[cfg(feature = "doublets")]
644fn write_u64(output: &mut Vec<u8>, value: u64) {
645    output.extend_from_slice(&value.to_le_bytes());
646}
647
648#[cfg(feature = "doublets")]
649fn read_u64(bytes: &[u8], cursor: &mut usize) -> Result<u64, StorageError> {
650    let mut value = [0_u8; 8];
651    value.copy_from_slice(read_bytes(bytes, cursor, 8)?);
652    Ok(u64::from_le_bytes(value))
653}
654
655#[cfg(feature = "doublets")]
656fn read_u8(bytes: &[u8], cursor: &mut usize) -> Result<u8, StorageError> {
657    let value = *read_bytes(bytes, cursor, 1)?
658        .first()
659        .expect("one byte was requested");
660    Ok(value)
661}
662
663#[cfg(feature = "doublets")]
664fn read_bytes<'a>(
665    bytes: &'a [u8],
666    cursor: &mut usize,
667    len: usize,
668) -> Result<&'a [u8], StorageError> {
669    let end = cursor
670        .checked_add(len)
671        .ok_or_else(|| StorageError::Corrupt("metadata cursor overflow".to_string()))?;
672    let value = bytes.get(*cursor..end).ok_or_else(|| {
673        StorageError::Corrupt("metadata ended before expected length".to_string())
674    })?;
675    *cursor = end;
676    Ok(value)
677}
678
679#[cfg(feature = "doublets")]
680fn flag_bits(flags: LinkFlags) -> u8 {
681    u8::from(flags.is_error())
682        | (u8::from(flags.has_error()) << 1)
683        | (u8::from(flags.is_missing()) << 2)
684        | (u8::from(flags.is_extra()) << 3)
685}
686
687#[cfg(feature = "doublets")]
688const fn parse_flags(bits: u8) -> LinkFlags {
689    let mut flags = LinkFlags::clean();
690    if bits & 0b0001 != 0 {
691        flags = flags.with_error();
692    }
693    if bits & 0b0010 != 0 {
694        flags = flags.with_containing_error();
695    }
696    if bits & 0b0100 != 0 {
697        flags = flags.with_missing();
698    }
699    if bits & 0b1000 != 0 {
700        flags = flags.with_extra();
701    }
702    flags
703}
704
705#[cfg(feature = "doublets")]
706const fn link_type_code(link_type: Option<LinkType>) -> u8 {
707    match link_type {
708        None => 0,
709        Some(LinkType::Link) => 1,
710        Some(LinkType::Reference) => 2,
711        Some(LinkType::Relation) => 3,
712        Some(LinkType::Language) => 4,
713        Some(LinkType::Grammar) => 5,
714        Some(LinkType::Type) => 6,
715        Some(LinkType::Concept) => 7,
716        Some(LinkType::Syntax) => 8,
717        Some(LinkType::Field) => 9,
718        Some(LinkType::Trivia) => 10,
719        Some(LinkType::Token) => 11,
720        Some(LinkType::Document) => 12,
721        Some(LinkType::Semantic) => 13,
722        Some(LinkType::Region) => 14,
723        Some(LinkType::Object) => 15,
724    }
725}
726
727#[cfg(feature = "doublets")]
728fn parse_link_type_code(code: u8) -> Result<Option<LinkType>, StorageError> {
729    Ok(Some(match code {
730        0 => return Ok(None),
731        1 => LinkType::Link,
732        2 => LinkType::Reference,
733        3 => LinkType::Relation,
734        4 => LinkType::Language,
735        5 => LinkType::Grammar,
736        6 => LinkType::Type,
737        7 => LinkType::Concept,
738        8 => LinkType::Syntax,
739        9 => LinkType::Field,
740        10 => LinkType::Trivia,
741        11 => LinkType::Token,
742        12 => LinkType::Document,
743        13 => LinkType::Semantic,
744        14 => LinkType::Region,
745        15 => LinkType::Object,
746        other => {
747            return Err(StorageError::Corrupt(format!(
748                "unknown link type code {other}"
749            )))
750        }
751    }))
752}