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#[cfg(feature = "doublets")]
46pub struct DoubletsLinkStore {
47 path: PathBuf,
48 store: FileMappedDoubletsStore,
49}
50
51#[cfg(feature = "doublets")]
52impl DoubletsLinkStore {
53 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 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 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 #[must_use]
125 pub fn snapshot_path(path: impl AsRef<Path>) -> PathBuf {
126 snapshot_path(path.as_ref())
127 }
128
129 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 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}