use crate::datatypes::values::{FilterCondition, Value};
pub use crate::graph::storage::interner::{InternedKey, StringInterner};
pub(crate) use crate::graph::storage::interner::{
SerdeDeserializeGuard, SerdeSerializeGuard, StripPropertiesGuard,
};
use crate::graph::storage::GraphRead;
pub(crate) use crate::graph::storage::property_storage::{ColumnarRow, PropertyStorage};
pub use crate::graph::dir_graph::DirGraph;
use crate::graph::dir_graph::NodeRemap;
pub use crate::graph::storage::backend::GraphBackend;
#[allow(unused_imports)]
pub use crate::graph::storage::{MappedGraph, MemoryGraph};
use petgraph::graph::NodeIndex;
use rustc_hash::FxHashMap;
pub const RESERVED_PROVENANCE_KEYS: &[&str] = &["updated_at", "git_sha", "modified_by"];
#[inline]
pub fn is_reserved_provenance_key(key: &str) -> bool {
RESERVED_PROVENANCE_KEYS.contains(&key)
}
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
pub const PROVISIONAL_KEY: &str = "_provisional";
#[derive(Debug, Clone)]
pub struct TypeSchema {
pub(crate) slots: Vec<InternedKey>,
key_to_slot: FxHashMap<InternedKey, u16>,
}
impl TypeSchema {
pub fn new() -> Self {
TypeSchema {
slots: Vec::new(),
key_to_slot: FxHashMap::default(),
}
}
pub fn from_keys(keys: impl IntoIterator<Item = InternedKey>) -> Self {
let mut schema = TypeSchema::new();
for key in keys {
if !schema.key_to_slot.contains_key(&key) {
let slot = schema.slots.len() as u16;
schema.slots.push(key);
schema.key_to_slot.insert(key, slot);
}
}
schema
}
#[inline]
pub fn slot(&self, key: InternedKey) -> Option<u16> {
self.key_to_slot.get(&key).copied()
}
#[inline]
pub fn len(&self) -> usize {
self.slots.len()
}
pub fn merge(&self, other: &TypeSchema) -> TypeSchema {
let mut merged = self.clone();
for &key in &other.slots {
merged.add_key(key);
}
merged
}
pub fn add_key(&mut self, key: InternedKey) -> u16 {
if let Some(&slot) = self.key_to_slot.get(&key) {
slot
} else {
let slot = self.slots.len() as u16;
self.slots.push(key);
self.key_to_slot.insert(key, slot);
slot
}
}
pub fn iter(&self) -> impl Iterator<Item = (u16, InternedKey)> + '_ {
self.slots.iter().enumerate().map(|(i, &k)| (i as u16, k))
}
}
pub(crate) fn serialize_sorted_map<K, V, S>(
map: &HashMap<K, V>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
K: Ord + Serialize + std::hash::Hash + Eq,
V: Serialize,
S: Serializer,
{
let mut entries: Vec<(&K, &V)> = map.iter().collect();
entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
serializer.collect_map(entries)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct SpatialConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<(String, String)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub geometry: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub points: HashMap<String, (String, String)>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub shapes: HashMap<String, String>,
}
pub type SpatialColumnParseResult = (Option<SpatialConfig>, Vec<(String, String)>);
pub fn parse_spatial_column_types_from_pairs(
pairs: Vec<(String, String)>,
) -> Result<SpatialColumnParseResult, String> {
let mut cleaned: Vec<(String, String)> = Vec::with_capacity(pairs.len());
let mut config = SpatialConfig::default();
let mut has_spatial = false;
let mut location_lat: Option<String> = None;
let mut location_lon: Option<String> = None;
let mut point_lats: HashMap<String, String> = HashMap::new();
let mut point_lons: HashMap<String, String> = HashMap::new();
for (col_name, type_str) in pairs {
let type_lower = type_str.to_lowercase();
match type_lower.as_str() {
"location.lat" => {
location_lat = Some(col_name.clone());
cleaned.push((col_name, "float".to_string()));
has_spatial = true;
}
"location.lon" => {
location_lon = Some(col_name.clone());
cleaned.push((col_name, "float".to_string()));
has_spatial = true;
}
"geometry" => {
config.geometry = Some(col_name.clone());
cleaned.push((col_name, "str".to_string()));
has_spatial = true;
}
_ if type_lower.starts_with("point.") => {
let parts: Vec<&str> = type_lower.splitn(3, '.').collect();
if parts.len() == 3 {
let name = parts[1].to_string();
match parts[2] {
"lat" => {
point_lats.insert(name, col_name.clone());
}
"lon" => {
point_lons.insert(name, col_name.clone());
}
_ => {
return Err(format!(
"Invalid spatial type '{}' for column '{}'. \
Expected 'point.<name>.lat' or 'point.<name>.lon'.",
type_str, col_name
));
}
}
cleaned.push((col_name, "float".to_string()));
has_spatial = true;
} else {
return Err(format!(
"Invalid spatial type '{}' for column '{}'. \
Expected 'point.<name>.lat' or 'point.<name>.lon'.",
type_str, col_name
));
}
}
_ if type_lower.starts_with("shape.") => {
let parts: Vec<&str> = type_lower.splitn(2, '.').collect();
if parts.len() == 2 {
let name = parts[1].to_string();
config.shapes.insert(name, col_name.clone());
cleaned.push((col_name, "str".to_string()));
has_spatial = true;
} else {
return Err(format!(
"Invalid spatial type '{}' for column '{}'.",
type_str, col_name
));
}
}
_ => {
cleaned.push((col_name, type_str));
}
}
}
if !has_spatial {
return Ok((None, cleaned));
}
match (location_lat, location_lon) {
(Some(lat), Some(lon)) => config.location = Some((lat, lon)),
(Some(_), None) | (None, Some(_)) => {
return Err(
"Incomplete location: both 'location.lat' and 'location.lon' must be specified."
.to_string(),
);
}
(None, None) => {}
}
let all_point_names: std::collections::HashSet<&String> =
point_lats.keys().chain(point_lons.keys()).collect();
for name in all_point_names {
match (point_lats.get(name), point_lons.get(name)) {
(Some(lat), Some(lon)) => {
config
.points
.insert(name.clone(), (lat.clone(), lon.clone()));
}
_ => {
return Err(format!(
"Incomplete point '{}': both 'point.{}.lat' and 'point.{}.lon' must be specified.",
name, name, name
));
}
}
}
Ok((Some(config), cleaned))
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct TemporalConfig {
pub valid_from: String,
pub valid_to: String,
}
pub type TemporalColumnParseResult = (Option<TemporalConfig>, Vec<(String, String)>);
pub fn parse_temporal_column_types_from_pairs(
pairs: Vec<(String, String)>,
) -> Result<TemporalColumnParseResult, String> {
let mut cleaned: Vec<(String, String)> = Vec::with_capacity(pairs.len());
let mut valid_from_col: Option<String> = None;
let mut valid_to_col: Option<String> = None;
for (col_name, type_str) in pairs {
let type_lower = type_str.to_lowercase();
match type_lower.as_str() {
"validfrom" => {
valid_from_col = Some(col_name.clone());
cleaned.push((col_name, "datetime".to_string()));
}
"validto" => {
valid_to_col = Some(col_name.clone());
cleaned.push((col_name, "datetime".to_string()));
}
_ => {
cleaned.push((col_name, type_str));
}
}
}
match (valid_from_col, valid_to_col) {
(Some(from), Some(to)) => Ok((
Some(TemporalConfig {
valid_from: from,
valid_to: to,
}),
cleaned,
)),
(Some(_), None) | (None, Some(_)) => Err(
"Incomplete temporal config: both 'validFrom' and 'validTo' column types must be specified."
.to_string(),
),
(None, None) => Ok((None, cleaned)),
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum TypeIdIndex {
Integer(FxHashMap<u32, NodeIndex>),
General(FxHashMap<Value, NodeIndex>),
}
impl TypeIdIndex {
pub fn get(&self, id: &Value) -> Option<NodeIndex> {
match self {
TypeIdIndex::Integer(map) => match id {
Value::UniqueId(u) => map.get(u).copied(),
Value::Int64(i) => {
if *i >= 0 && *i <= u32::MAX as i64 {
map.get(&(*i as u32)).copied()
} else {
None
}
}
Value::Float64(f) => {
if f.fract() == 0.0 {
let i = *f as i64;
if i >= 0 && i <= u32::MAX as i64 {
map.get(&(i as u32)).copied()
} else {
None
}
} else {
None
}
}
_ => None,
},
TypeIdIndex::General(map) => {
if let Some(&idx) = map.get(id) {
return Some(idx);
}
match id {
Value::Int64(i) => {
if let Some(&idx) = map.get(&Value::Float64(*i as f64)) {
return Some(idx);
}
if *i >= 0 && *i <= u32::MAX as i64 {
map.get(&Value::UniqueId(*i as u32)).copied()
} else {
None
}
}
Value::UniqueId(u) => {
if let Some(&idx) = map.get(&Value::Int64(*u as i64)) {
return Some(idx);
}
map.get(&Value::Float64(*u as f64)).copied()
}
Value::Float64(f) => {
if f.fract() == 0.0 {
let i = *f as i64;
if let Some(&idx) = map.get(&Value::Int64(i)) {
return Some(idx);
}
if i >= 0 && i <= u32::MAX as i64 {
return map.get(&Value::UniqueId(i as u32)).copied();
}
}
None
}
_ => None,
}
}
}
}
pub fn insert(&mut self, id: Value, idx: NodeIndex) {
match self {
TypeIdIndex::Integer(map) => {
if let Value::UniqueId(u) = id {
map.insert(u, idx);
} else {
let mut general: FxHashMap<Value, NodeIndex> =
map.drain().map(|(k, v)| (Value::UniqueId(k), v)).collect();
general.insert(id, idx);
*self = TypeIdIndex::General(general);
}
}
TypeIdIndex::General(map) => {
map.insert(id, idx);
}
}
}
pub fn len(&self) -> usize {
match self {
TypeIdIndex::Integer(map) => map.len(),
TypeIdIndex::General(map) => map.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn remove_matching(&mut self, id: &Value, idx: NodeIndex) -> bool {
if self.get(id) != Some(idx) {
return false;
}
match self {
TypeIdIndex::Integer(map) => {
let key = match id {
Value::UniqueId(u) => Some(*u),
Value::Int64(i) if *i >= 0 && *i <= u32::MAX as i64 => Some(*i as u32),
Value::Float64(f) if f.fract() == 0.0 => {
let i = *f as i64;
if i >= 0 && i <= u32::MAX as i64 {
Some(i as u32)
} else {
None
}
}
_ => None,
};
key.is_some_and(|k| map.remove(&k).is_some())
}
TypeIdIndex::General(map) => {
if map.remove(id).is_some() {
return true;
}
let coerced = match id {
Value::Int64(i) if *i >= 0 && *i <= u32::MAX as i64 => {
Some(Value::UniqueId(*i as u32))
}
Value::UniqueId(u) => Some(Value::Int64(*u as i64)),
Value::Float64(f) if f.fract() == 0.0 => {
let i = *f as i64;
if map.contains_key(&Value::Int64(i)) {
Some(Value::Int64(i))
} else if i >= 0 && i <= u32::MAX as i64 {
Some(Value::UniqueId(i as u32))
} else {
None
}
}
_ => None,
};
coerced.is_some_and(|k| map.remove(&k).is_some())
}
}
}
pub fn iter(&self) -> Box<dyn Iterator<Item = (Value, NodeIndex)> + '_> {
match self {
TypeIdIndex::Integer(map) => {
Box::new(map.iter().map(|(&k, &v)| (Value::UniqueId(k), v)))
}
TypeIdIndex::General(map) => Box::new(map.iter().map(|(k, &v)| (k.clone(), v))),
}
}
}
impl Default for TypeIdIndex {
fn default() -> Self {
TypeIdIndex::General(FxHashMap::default())
}
}
#[derive(Clone, Debug)]
pub struct NodeInfo {
pub id: Value,
pub title: Value,
pub node_type: String,
pub properties: HashMap<String, Value>,
}
#[derive(Clone, Debug)]
pub enum SelectionOperation {
Filter(HashMap<String, FilterCondition>),
Sort(Vec<(String, bool)>), Traverse {
connection_type: String,
direction: Option<String>,
max_nodes: Option<usize>,
},
Custom(String), }
#[derive(Clone, Debug)]
pub struct SelectionLevel {
pub selections: HashMap<Option<NodeIndex>, Vec<NodeIndex>>, pub operations: Vec<SelectionOperation>,
}
impl SelectionLevel {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
SelectionLevel {
selections: HashMap::new(),
operations: Vec::new(),
}
}
pub fn add_selection(&mut self, parent: Option<NodeIndex>, children: Vec<NodeIndex>) {
self.selections.insert(parent, children);
}
pub fn get_all_nodes(&self) -> Vec<NodeIndex> {
self.selections
.values()
.flat_map(|children| children.iter().copied())
.collect()
}
pub fn is_empty(&self) -> bool {
self.selections.is_empty()
}
pub fn iter_groups(&self) -> impl Iterator<Item = (&Option<NodeIndex>, &Vec<NodeIndex>)> {
self.selections.iter()
}
pub fn iter_node_indices(&self) -> impl Iterator<Item = NodeIndex> + '_ {
self.selections
.values()
.flat_map(|children| children.iter().copied())
}
pub fn node_count(&self) -> usize {
self.selections.values().map(|v| v.len()).sum()
}
pub fn remap_indices(&mut self, remap: &NodeRemap) {
let mut remapped: HashMap<Option<NodeIndex>, Vec<NodeIndex>> =
HashMap::with_capacity(self.selections.len());
for (parent, children) in self.selections.drain() {
let new_parent = match parent {
None => None,
Some(p) => match remap.get(p) {
Some(new_p) => Some(new_p),
None => continue,
},
};
remapped.insert(
new_parent,
children.into_iter().filter_map(|c| remap.get(c)).collect(),
);
}
self.selections = remapped;
}
}
#[derive(Clone, Debug)]
pub struct PlanStep {
pub operation: String,
pub node_type: Option<String>,
pub estimated_rows: usize,
pub actual_rows: Option<usize>,
}
impl PlanStep {
pub fn new(operation: &str, node_type: Option<&str>, estimated_rows: usize) -> Self {
PlanStep {
operation: operation.to_string(),
node_type: node_type.map(|s| s.to_string()),
estimated_rows,
actual_rows: None,
}
}
pub fn with_actual_rows(mut self, actual: usize) -> Self {
self.actual_rows = Some(actual);
self
}
}
#[derive(Clone, Default)]
pub struct CurrentSelection {
levels: Vec<SelectionLevel>,
current_level: usize,
execution_plan: Vec<PlanStep>,
}
impl CurrentSelection {
pub fn new() -> Self {
let mut selection = CurrentSelection {
levels: Vec::new(),
current_level: 0,
execution_plan: Vec::new(),
};
selection.add_level(); selection
}
pub fn add_level(&mut self) {
self.levels.push(SelectionLevel::new());
self.current_level = self.levels.len() - 1;
}
pub fn clear(&mut self) {
self.levels.clear();
self.current_level = 0;
self.execution_plan.clear();
self.add_level(); }
pub fn add_plan_step(&mut self, step: PlanStep) {
self.execution_plan.push(step);
}
pub fn get_execution_plan(&self) -> &[PlanStep] {
&self.execution_plan
}
pub fn clear_execution_plan(&mut self) {
self.execution_plan.clear();
}
pub fn get_level_count(&self) -> usize {
self.levels.len()
}
pub fn get_level(&self, index: usize) -> Option<&SelectionLevel> {
self.levels.get(index)
}
pub fn get_level_mut(&mut self, index: usize) -> Option<&mut SelectionLevel> {
self.levels.get_mut(index)
}
pub fn current_node_count(&self) -> usize {
self.levels.last().map(|l| l.node_count()).unwrap_or(0)
}
pub fn has_active_selection(&self) -> bool {
self.levels
.last()
.map(|l| !l.operations.is_empty())
.unwrap_or(false)
}
pub fn current_node_indices(&self) -> impl Iterator<Item = NodeIndex> + '_ {
self.levels
.last()
.into_iter()
.flat_map(|l| l.iter_node_indices())
}
pub fn remap_indices(&mut self, remap: &NodeRemap) {
if !remap.describes_rebuild() {
return;
}
for level in &mut self.levels {
level.remap_indices(remap);
}
}
pub fn first_node_type(&self, graph: &DirGraph) -> Option<String> {
let _arena_guard = graph.graph.begin_query();
self.current_node_indices()
.next()
.and_then(|idx| graph.graph.node_view(idx))
.map(|node| node.node_type_str(&graph.interner).to_string())
}
}
#[derive(Clone, Default)]
pub struct CowSelection {
inner: Arc<CurrentSelection>,
}
impl CowSelection {
pub fn new() -> Self {
CowSelection {
inner: Arc::new(CurrentSelection::new()),
}
}
}
impl std::ops::Deref for CowSelection {
type Target = CurrentSelection;
#[inline]
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for CowSelection {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
Arc::make_mut(&mut self.inner)
}
}
pub type IndexKey = (String, String);
pub type CompositeIndexKey = (String, Vec<String>);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CompositeValue(pub Vec<Value>);
pub const KGL_FORMAT_VERSION: u32 = 2;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SaveMetadata {
pub format_version: u32,
pub library_version: String,
}
impl SaveMetadata {
pub fn current() -> Self {
SaveMetadata {
format_version: KGL_FORMAT_VERSION,
library_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivityTriple {
pub src: String,
pub conn: String,
pub tgt: String,
pub count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct ConnectionTypeInfo {
pub source_types: HashSet<String>,
pub target_types: HashSet<String>,
pub property_types: HashMap<String, String>,
}
impl Serialize for ConnectionTypeInfo {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut sorted_sources: Vec<&String> = self.source_types.iter().collect();
sorted_sources.sort();
let mut sorted_targets: Vec<&String> = self.target_types.iter().collect();
sorted_targets.sort();
let mut sorted_props: Vec<(&String, &String)> = self.property_types.iter().collect();
sorted_props.sort_by(|a, b| a.0.cmp(b.0));
let property_types: std::collections::BTreeMap<&String, &String> =
sorted_props.into_iter().collect();
let mut state = serializer.serialize_struct("ConnectionTypeInfo", 3)?;
state.serialize_field("source_types", &sorted_sources)?;
state.serialize_field("target_types", &sorted_targets)?;
state.serialize_field("property_types", &property_types)?;
state.end()
}
}
impl<'de> Deserialize<'de> for ConnectionTypeInfo {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Legacy {
source_type: Option<String>,
target_type: Option<String>,
#[serde(default)]
source_types: Option<HashSet<String>>,
#[serde(default)]
target_types: Option<HashSet<String>>,
#[serde(default)]
property_types: HashMap<String, String>,
}
let legacy = Legacy::deserialize(deserializer)?;
let source_types = legacy.source_types.unwrap_or_else(|| {
legacy
.source_type
.map(|s| HashSet::from([s]))
.unwrap_or_default()
});
let target_types = legacy.target_types.unwrap_or_else(|| {
legacy
.target_type
.map(|s| HashSet::from([s]))
.unwrap_or_default()
});
Ok(ConnectionTypeInfo {
source_types,
target_types,
property_types: legacy.property_types,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EmbeddingStore {
pub dimension: usize,
pub data: Vec<f32>,
#[serde(serialize_with = "serialize_sorted_map")]
pub node_to_slot: HashMap<usize, usize>,
pub slot_to_node: Vec<usize>,
#[serde(default)]
pub metric: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default, serialize_with = "serialize_sorted_map")]
pub text_hashes: HashMap<usize, u64>,
#[serde(skip)]
pub norms: Vec<f32>,
#[serde(skip)]
pub index: Option<crate::graph::algorithms::hnsw::HnswIndex>,
}
#[inline]
fn l2_norm_sq(v: &[f32]) -> f32 {
let (mut s0, mut s1, mut s2, mut s3) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
let chunks = v.chunks_exact(8);
let rem = chunks.remainder();
for c in chunks {
s0 += c[0] * c[0];
s1 += c[1] * c[1];
s2 += c[2] * c[2];
s3 += c[3] * c[3];
s0 += c[4] * c[4];
s1 += c[5] * c[5];
s2 += c[6] * c[6];
s3 += c[7] * c[7];
}
for &x in rem {
s0 += x * x;
}
(s0 + s1) + (s2 + s3)
}
impl EmbeddingStore {
pub fn new(dimension: usize) -> Self {
EmbeddingStore {
dimension,
data: Vec::new(),
node_to_slot: HashMap::new(),
slot_to_node: Vec::new(),
metric: None,
model_id: None,
text_hashes: HashMap::new(),
norms: Vec::new(),
index: None,
}
}
pub fn with_metric(dimension: usize, metric: &str) -> Self {
EmbeddingStore {
dimension,
data: Vec::new(),
node_to_slot: HashMap::new(),
slot_to_node: Vec::new(),
metric: Some(metric.to_string()),
model_id: None,
text_hashes: HashMap::new(),
norms: Vec::new(),
index: None,
}
}
pub fn text_hash(text: &str) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut h = FNV_OFFSET;
for &byte in text.as_bytes() {
h ^= byte as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h
}
pub fn set_text_hash(&mut self, node_index: usize, hash: u64) {
self.text_hashes.insert(node_index, hash);
}
pub fn is_stale(&self, node_index: usize, current_hash: u64) -> bool {
if !self.node_to_slot.contains_key(&node_index) {
return true;
}
match self.text_hashes.get(&node_index) {
Some(&stored) => stored != current_hash,
None => true,
}
}
pub fn set_embedding(&mut self, node_index: usize, embedding: &[f32]) -> usize {
self.index = None;
let norm = l2_norm_sq(embedding).sqrt();
if let Some(&slot) = self.node_to_slot.get(&node_index) {
let start = slot * self.dimension;
self.data[start..start + self.dimension].copy_from_slice(embedding);
self.norms[slot] = norm;
slot
} else {
let slot = self.slot_to_node.len();
self.node_to_slot.insert(node_index, slot);
self.slot_to_node.push(node_index);
self.data.extend_from_slice(embedding);
self.norms.push(norm);
slot
}
}
#[inline]
pub fn get_embedding(&self, node_index: usize) -> Option<&[f32]> {
self.node_to_slot.get(&node_index).map(|&slot| {
let start = slot * self.dimension;
&self.data[start..start + self.dimension]
})
}
#[inline]
pub fn get_embedding_with_norm(&self, node_index: usize) -> Option<(&[f32], f32)> {
self.node_to_slot.get(&node_index).map(|&slot| {
let start = slot * self.dimension;
(&self.data[start..start + self.dimension], self.norms[slot])
})
}
#[inline]
pub fn invalidate_index(&mut self) {
self.index = None;
}
#[inline]
pub fn has_index(&self) -> bool {
self.index.is_some()
}
pub fn build_index(
&mut self,
metric: crate::graph::algorithms::vector::DistanceMetric,
params: crate::graph::algorithms::hnsw::HnswParams,
seed: u64,
) -> Result<(), String> {
let hm = crate::graph::algorithms::hnsw::HnswMetric::from_distance(metric).ok_or_else(
|| "HNSW does not support the Poincaré metric; it stays on the exact (brute-force) path.".to_string(),
)?;
if self.dimension == 0 {
return Err(
"HNSW index construction requires a non-zero embedding dimension".to_string(),
);
}
params.validate().map_err(str::to_string)?;
self.validate_shape().map_err(str::to_string)?;
if self.norms.len() != self.slot_to_node.len() {
self.rebuild_norms();
}
self.index = Some(crate::graph::algorithms::hnsw::HnswIndex::build(
&self.data,
&self.norms,
self.dimension,
hm,
params,
seed,
));
Ok(())
}
pub(crate) fn validate_shape(&self) -> Result<(), &'static str> {
let expected_data_len = self
.slot_to_node
.len()
.checked_mul(self.dimension)
.ok_or("embedding data cardinality overflows usize")?;
if self.data.len() != expected_data_len {
return Err("embedding data cardinality does not match its slot count");
}
if self.node_to_slot.len() != self.slot_to_node.len() {
return Err("embedding node/slot maps have different cardinalities");
}
for (slot, &node) in self.slot_to_node.iter().enumerate() {
if self.node_to_slot.get(&node) != Some(&slot) {
return Err("embedding node/slot maps are not a bijection");
}
}
Ok(())
}
pub fn rebuild_norms(&mut self) {
let n = self.slot_to_node.len();
self.norms.clear();
self.norms.reserve(n);
for slot in 0..n {
let start = slot * self.dimension;
self.norms
.push(l2_norm_sq(&self.data[start..start + self.dimension]).sqrt());
}
}
#[inline]
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.slot_to_node.len()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SoftAliasFallback {
Title,
TypeString,
}
pub const SOFT_ALIAS_NAMES: [&str; 4] = ["name", "type", "node_type", "label"];
#[inline]
pub fn soft_alias_fallback(resolved: &str) -> Option<SoftAliasFallback> {
match resolved {
"name" => Some(SoftAliasFallback::Title),
"type" | "node_type" | "label" => Some(SoftAliasFallback::TypeString),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NodeData {
pub(crate) id: Value,
pub(crate) title: Value,
pub node_type: InternedKey,
pub(crate) properties: PropertyStorage,
}
impl NodeData {
pub fn new(
id: Value,
title: Value,
node_type: String,
properties: HashMap<String, Value>,
interner: &mut StringInterner,
) -> Self {
let type_key = interner.get_or_intern(&node_type);
let interned_props = properties
.into_iter()
.map(|(k, v)| {
let key = interner.get_or_intern(&k);
(key, v)
})
.collect();
NodeData {
id,
title,
node_type: type_key,
properties: PropertyStorage::Map(interned_props),
}
}
pub fn new_preinterned(
id: Value,
title: Value,
node_type: InternedKey,
properties: Vec<(InternedKey, Value)>,
) -> Self {
let map: HashMap<InternedKey, Value> = properties.into_iter().collect();
NodeData {
id,
title,
node_type,
properties: PropertyStorage::Map(map),
}
}
#[inline]
pub fn id(&self) -> Cow<'_, Value> {
Cow::Borrowed(&self.id)
}
#[inline]
pub fn title(&self) -> Cow<'_, Value> {
Cow::Borrowed(&self.title)
}
#[inline]
pub fn node_type_str<'a>(&self, interner: &'a StringInterner) -> &'a str {
interner.resolve(self.node_type)
}
}
pub struct EdgeData {
pub connection_type: InternedKey,
pub properties: Vec<(InternedKey, Value)>,
}
impl Serialize for EdgeData {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("EdgeData", 2)?;
s.serialize_field("connection_type", &self.connection_type)?;
let props_map: BTreeMap<&InternedKey, &Value> =
self.properties.iter().map(|(k, v)| (k, v)).collect();
s.serialize_field("properties", &props_map)?;
s.end()
}
}
impl<'de> Deserialize<'de> for EdgeData {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct EdgeDataHelper {
connection_type: InternedKey,
#[serde(default)]
properties: HashMap<InternedKey, Value>,
}
let helper = EdgeDataHelper::deserialize(deserializer)?;
Ok(EdgeData {
connection_type: helper.connection_type,
properties: helper.properties.into_iter().collect(),
})
}
}
impl std::fmt::Debug for EdgeData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EdgeData")
.field("connection_type", &self.connection_type)
.field("properties", &self.properties)
.finish()
}
}
impl Clone for EdgeData {
fn clone(&self) -> Self {
EdgeData {
connection_type: self.connection_type,
properties: self.properties.clone(),
}
}
}
impl EdgeData {
pub fn new(
connection_type: String,
properties: HashMap<String, Value>,
interner: &mut StringInterner,
) -> Self {
let ct_key = interner.get_or_intern(&connection_type);
let interned_props: Vec<(InternedKey, Value)> = properties
.into_iter()
.map(|(k, v)| {
let key = interner.get_or_intern(&k);
(key, v)
})
.collect();
EdgeData {
connection_type: ct_key,
properties: interned_props,
}
}
pub fn new_interned(
connection_type: InternedKey,
properties: Vec<(InternedKey, Value)>,
) -> Self {
EdgeData {
connection_type,
properties,
}
}
#[inline]
pub fn connection_type_str<'a>(&self, interner: &'a StringInterner) -> &'a str {
interner.resolve(self.connection_type)
}
#[inline]
pub fn get_property(&self, key: &str) -> Option<&Value> {
let ik = InternedKey::from_str(key);
self.properties
.iter()
.find(|(k, _)| *k == ik)
.map(|(_, v)| v)
}
#[inline]
pub fn property_keys<'a>(
&'a self,
interner: &'a StringInterner,
) -> impl Iterator<Item = &'a str> {
self.properties
.iter()
.map(move |(k, _)| interner.resolve(*k))
}
#[inline]
pub fn property_iter<'a>(
&'a self,
interner: &'a StringInterner,
) -> impl Iterator<Item = (&'a str, &'a Value)> {
self.properties
.iter()
.map(move |(k, v)| (interner.resolve(*k), v))
}
#[inline]
pub fn property_count(&self) -> usize {
self.properties.len()
}
#[inline]
pub fn properties_cloned(&self, interner: &StringInterner) -> HashMap<String, Value> {
self.properties
.iter()
.map(|(k, v)| (interner.resolve(*k).to_string(), v.clone()))
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NodeSchemaDefinition {
pub required_fields: Vec<String>,
pub optional_fields: Vec<String>,
pub field_types: HashMap<String, String>,
#[serde(default)]
pub primary_key: Option<String>,
#[serde(default)]
pub unique: Option<Vec<Vec<String>>>,
#[serde(default)]
pub layer: Option<String>,
#[serde(default)]
pub auto_timestamp: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionSchemaDefinition {
pub source_type: String,
pub target_type: String,
pub cardinality: Option<String>,
pub required_properties: Vec<String>,
pub property_types: HashMap<String, String>,
#[serde(default)]
pub auto_timestamp: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaDefinition {
pub node_schemas: HashMap<String, NodeSchemaDefinition>,
pub connection_schemas: HashMap<String, ConnectionSchemaDefinition>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SchemaInstall {
#[default]
Merge,
Replace,
}
impl SchemaDefinition {
pub fn new() -> Self {
SchemaDefinition {
node_schemas: HashMap::new(),
connection_schemas: HashMap::new(),
}
}
pub fn merged_with(mut self, incoming: SchemaDefinition) -> Self {
self.node_schemas.extend(incoming.node_schemas);
self.connection_schemas.extend(incoming.connection_schemas);
self
}
pub fn add_node_schema(&mut self, node_type: String, schema: NodeSchemaDefinition) {
self.node_schemas.insert(node_type, schema);
}
pub fn add_connection_schema(
&mut self,
connection_type: String,
schema: ConnectionSchemaDefinition,
) {
self.connection_schemas.insert(connection_type, schema);
}
}
#[derive(Debug, Clone)]
pub enum ValidationError {
MissingRequiredField {
node_type: String,
node_title: String,
field: String,
},
TypeMismatch {
node_type: String,
node_title: String,
field: String,
expected_type: String,
actual_type: String,
},
InvalidConnectionEndpoint {
connection_type: String,
expected_source: String,
expected_target: String,
actual_source: String,
actual_target: String,
},
MissingConnectionProperty {
connection_type: String,
source_title: String,
target_title: String,
property: String,
},
UndefinedNodeType { node_type: String, count: usize },
UndefinedConnectionType {
connection_type: String,
count: usize,
},
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::MissingRequiredField {
node_type,
node_title,
field,
} => {
write!(
f,
"Missing required field '{}' on {} node '{}'",
field, node_type, node_title
)
}
ValidationError::TypeMismatch {
node_type,
node_title,
field,
expected_type,
actual_type,
} => {
write!(
f,
"Type mismatch on {} node '{}': field '{}' expected {}, got {}",
node_type, node_title, field, expected_type, actual_type
)
}
ValidationError::InvalidConnectionEndpoint {
connection_type,
expected_source,
expected_target,
actual_source,
actual_target,
} => {
write!(
f,
"Invalid connection '{}': expected {}->{} but found {}->{}",
connection_type, expected_source, expected_target, actual_source, actual_target
)
}
ValidationError::MissingConnectionProperty {
connection_type,
source_title,
target_title,
property,
} => {
write!(
f,
"Missing required property '{}' on {} connection from '{}' to '{}'",
property, connection_type, source_title, target_title
)
}
ValidationError::UndefinedNodeType { node_type, count } => {
write!(
f,
"Node type '{}' ({} nodes) exists in graph but not defined in schema",
node_type, count
)
}
ValidationError::UndefinedConnectionType {
connection_type,
count,
} => {
write!(f, "Connection type '{}' ({} connections) exists in graph but not defined in schema", connection_type, count)
}
}
}
}
#[cfg(test)]
#[path = "schema_tests.rs"]
mod schema_tests;