Skip to main content

graph_r/
key.rs

1// Author: spicyzhug@gmail.com
2// Copyright: lituus-io, all rights reserved
3//
4//! Identity types. The durable foreign key everywhere is [`UrlKey`] — the
5//! 64-bit xxh3 of a canonical URL, byte-compatible with link-r's key of the
6//! same name — because link-r's internal document ids are unstable across
7//! removals. [`NodeId`]s here are dense per-snapshot indices assigned at
8//! compaction; they are stable *within* one base generation and never
9//! persisted across it, exactly link-r's `DocId` philosophy.
10
11use crate::hash::xxh3_64;
12
13/// Durable identity of a document node: xxh3 of its canonical URL.
14///
15/// When the `bridge` feature is enabled this is produced from link-r's
16/// canonicalization; standalone callers must pass an already-canonical URL
17/// string to [`UrlKey::of`] (graph-r deliberately does not re-implement URL
18/// canonicalization — one recipe, one owner).
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct UrlKey(pub u64);
21
22impl UrlKey {
23    /// Key a canonical URL string.
24    #[must_use]
25    pub fn of(canonical_url: &str) -> Self {
26        Self(xxh3_64(canonical_url.as_bytes()))
27    }
28
29    /// The raw 64-bit key.
30    #[must_use]
31    pub fn raw(self) -> u64 {
32        self.0
33    }
34}
35
36/// Durable identity of a sub-document segment: xxh3 of the owning key plus
37/// the segment's heading path, so a re-crawl that keeps a heading keeps the
38/// segment's identity (and any enrichment attached to it).
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct SegKey(pub u64);
41
42impl SegKey {
43    /// Derive the segment key for `heading_path` under `owner`.
44    #[must_use]
45    pub fn derive(owner: UrlKey, heading_path: &str) -> Self {
46        let mut buf = Vec::with_capacity(8 + 1 + heading_path.len());
47        buf.extend_from_slice(&owner.0.to_le_bytes());
48        buf.push(0);
49        buf.extend_from_slice(heading_path.as_bytes());
50        Self(xxh3_64(&buf))
51    }
52}
53
54/// Dense per-snapshot node index (position in the base `Nodes` section).
55/// Invalidated by compaction; cross-snapshot APIs use [`UrlKey`].
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
57pub struct NodeId(pub u32);
58
59/// Typed edges. The discriminants are burned into the on-disk format; new
60/// kinds are additive (the enum is non-exhaustive and unknown values decode
61/// to [`EdgeType::Unknown`] rather than failing the file).
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
63#[non_exhaustive]
64pub enum EdgeType {
65    /// An outbound hyperlink observed in the crawled document (crawl tier).
66    Link,
67    /// A semantic-similarity edge proposed from dense vectors or enrichment.
68    Related,
69    /// A redirect observed at fetch time.
70    Redirect,
71    /// Structural containment: a table contains its columns.
72    Contains,
73    /// Value lineage: the source computes from the destination (a column
74    /// from the columns it is calculated from). With `eflags::EXACT`, an
75    /// unchanged copy along which a description carries over verbatim.
76    Derives,
77    /// Row lineage: the destination decides which rows the source holds (a
78    /// filter, join key, grouping or match condition of its producing SQL).
79    Conditions,
80    /// Forward-compatibility catch-all for tags this build does not know.
81    Unknown(u8),
82}
83
84impl EdgeType {
85    /// On-disk tag.
86    #[must_use]
87    pub fn as_tag(self) -> u8 {
88        match self {
89            Self::Link => 0,
90            Self::Related => 2,
91            Self::Redirect => 3,
92            Self::Contains => 4,
93            Self::Derives => 5,
94            Self::Conditions => 6,
95            Self::Unknown(t) => t,
96        }
97    }
98
99    /// Decode an on-disk tag (never fails; unknown tags are preserved).
100    #[must_use]
101    pub fn from_tag(tag: u8) -> Self {
102        match tag {
103            0 => Self::Link,
104            2 => Self::Related,
105            3 => Self::Redirect,
106            4 => Self::Contains,
107            5 => Self::Derives,
108            6 => Self::Conditions,
109            t => Self::Unknown(t),
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn url_key_matches_xxh3_of_string() {
120        let k = UrlKey::of("https://example.com/a");
121        assert_eq!(k.raw(), xxh3_64(b"https://example.com/a"));
122    }
123
124    #[test]
125    fn seg_key_is_stable_and_distinct() {
126        let owner = UrlKey::of("https://example.com/a");
127        let a = SegKey::derive(owner, "Install > Linux");
128        let b = SegKey::derive(owner, "Install > Linux");
129        let c = SegKey::derive(owner, "Install > macOS");
130        assert_eq!(a, b);
131        assert_ne!(a, c);
132        assert_ne!(a, SegKey::derive(UrlKey::of("https://example.com/b"), "Install > Linux"));
133    }
134
135    #[test]
136    fn edge_type_tags_round_trip() {
137        for t in [EdgeType::Link, EdgeType::Related, EdgeType::Redirect, EdgeType::Unknown(9)] {
138            assert_eq!(EdgeType::from_tag(t.as_tag()), t);
139        }
140    }
141}