Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/knut

crates/knut-graphdb/src/types.rs

//! The backend-neutral result + input types every [`crate::GraphDb`] op speaks in.

/// One node in a returned [`SubGraph`]. Backend-neutral: a stable `id`, a `label`,
/// the **raw** natural key the node carried (a Vessel's `mmsi`, an Aircraft's
/// `icao24`, a Company's `org_nr`), if any, and the node's **place** on the earth if
/// it had one. The consumer resolves that raw key to an object identity — this crate
/// does not depend on any ontology (that would be a cycle), so the resolution stays
/// on the consumer's side.
///
/// `Eq` is deliberately absent: `place` is a pair of `f64`. `PartialEq` is what every
/// consumer and test actually uses.
#[derive(Clone, Debug, PartialEq)]
pub struct GraphNode {
    /// The backend's node id (FalkorDB `id()`, or the Cozo `id` column).
    pub id: i64,
    /// The node's primary label (`"Person"`, `"Vessel"`, …), or `""` if unlabelled.
    pub label: String,
    /// The raw natural-key string this node carried, pre-resolution (`None` if it
    /// carried none of the known natural-key properties).
    pub natural_key: Option<String>,
    /// **Where this node IS** — `[lon, lat]` in WGS84 degrees, read off the node's own
    /// `lon`/`lat` properties by [`crate::falkor::parse_batch`]. `None` for the vast
    /// majority of nodes, which are not anywhere: a Person has no coordinate, a road
    /// junction does.
    ///
    /// Deliberately raw degrees and NOT a projected point: this crate must not own a
    /// map projection, or a consumer's map and its graph would be projected by two
    /// different pieces of code. The consumer projects with the one its map uses.
    pub place: Option<[f64; 2]>,
}

impl GraphNode {
    pub fn new(id: i64, label: impl Into<String>) -> Self {
        Self {
            id,
            label: label.into(),
            natural_key: None,
            place: None,
        }
    }

    #[must_use]
    pub fn with_key(mut self, key: Option<String>) -> Self {
        self.natural_key = key;
        self
    }

    /// Attach the node's `[lon, lat]` place (WGS84 degrees).
    #[must_use]
    pub fn with_place(mut self, place: Option<[f64; 2]>) -> Self {
        self.place = place;
        self
    }
}

/// One edge in a returned [`SubGraph`], referencing node **ids** (not indices), so
/// the result is order-independent — the consumer builds its own index mapping.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GraphEdge {
    pub src_id: i64,
    pub dst_id: i64,
    pub rel: String,
}

/// A sub-graph returned by any [`crate::GraphDb`] op — nodes plus id-referencing
/// edges. Deliberately not de-duplicated or sorted here; the consumer merges /
/// orders it for its own render layer.
///
/// `Eq` is absent because [`GraphNode::place`] is floating point; `PartialEq` is what
/// consumers and tests compare with.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SubGraph {
    pub nodes: Vec<GraphNode>,
    pub edges: Vec<GraphEdge>,
}

/// A node to upsert (the load entry point). `key` is an optional natural key stored
/// as a property so a later scene can return it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UpsertNode {
    pub id: i64,
    pub label: String,
    pub key: Option<String>,
}

impl UpsertNode {
    pub fn new(id: i64, label: impl Into<String>) -> Self {
        Self {
            id,
            label: label.into(),
            key: None,
        }
    }

    #[must_use]
    pub fn with_key(mut self, key: impl Into<String>) -> Self {
        self.key = Some(key.into());
        self
    }
}

/// An edge to upsert, referencing node ids.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UpsertEdge {
    pub src_id: i64,
    pub dst_id: i64,
    pub rel: String,
}

impl UpsertEdge {
    pub fn new(src_id: i64, dst_id: i64, rel: impl Into<String>) -> Self {
        Self {
            src_id,
            dst_id,
            rel: rel.into(),
        }
    }
}

/// One graph's **catalogue + size**: its distinct node **labels**, **relationship
/// types**, **property keys**, and its total node/edge **counts**.
///
/// This is what [`crate::GraphDb::open`] answers — the "what is in here?" surface a
/// picker fills a schema pane from before anyone writes a query. It is
/// backend-neutral and carries no colours: colour is a RENDERER's concern and lives
/// with the renderer (see the crate docs).
///
/// A graph that does not exist is [`crate::GraphError::NotFound`], **never** an empty
/// `GraphSchema` — an empty catalogue means an empty graph, and the two must stay
/// distinguishable.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct GraphSchema {
    /// The graph this catalogue was read from.
    pub graph: String,
    /// Distinct node labels, sorted and de-duplicated.
    pub labels: Vec<String>,
    /// Distinct relationship types, sorted and de-duplicated.
    pub relationship_types: Vec<String>,
    /// Distinct property keys, sorted and de-duplicated. May be empty for a backend
    /// with no property catalogue — that is not a failure.
    pub property_keys: Vec<String>,
    pub node_count: u64,
    pub edge_count: u64,
}

impl GraphSchema {
    /// Sort + de-duplicate every catalogue list, so the schema is deterministic
    /// regardless of the order the engine returned its catalogue in.
    #[must_use]
    pub fn normalised(mut self) -> Self {
        for v in [
            &mut self.labels,
            &mut self.relationship_types,
            &mut self.property_keys,
        ] {
            v.sort();
            v.dedup();
        }
        self
    }

    /// Derive a catalogue from an already-read [`SubGraph`] — the honest schema a
    /// backend with no catalogue procedures can still answer. Property keys are
    /// unknowable from a rendered scene, so they stay empty.
    #[must_use]
    pub fn from_subgraph(graph: impl Into<String>, sub: &SubGraph) -> Self {
        let labels: std::collections::BTreeSet<String> = sub
            .nodes
            .iter()
            .filter(|n| !n.label.is_empty())
            .map(|n| n.label.clone())
            .collect();
        let rels: std::collections::BTreeSet<String> = sub
            .edges
            .iter()
            .filter(|e| !e.rel.is_empty())
            .map(|e| e.rel.clone())
            .collect();
        Self {
            graph: graph.into(),
            labels: labels.into_iter().collect(),
            relationship_types: rels.into_iter().collect(),
            property_keys: Vec::new(),
            node_count: sub.nodes.len() as u64,
            edge_count: sub.edges.len() as u64,
        }
    }
}