use petgraph::graph::NodeIndex;
use crate::datatypes::Value;
use crate::graph::embeddings::store_key;
use crate::graph::features::timeseries::{NodeTimeseries, TimeseriesConfig};
use crate::graph::schema::{DirGraph, EmbeddingStore};
use crate::graph::storage::GraphRead;
use crate::graph::wal::{EmbeddingWrite, MutationOp};
impl DirGraph {
#[inline]
pub(crate) fn records_payloads(&self) -> bool {
self.graph.is_wal_owner()
}
fn logical_key(&self, node_idx: NodeIndex) -> Option<(String, Value)> {
let _arena_guard = self.graph.begin_query();
let node = self.graph.node_view(node_idx)?;
Some((
node.node_type_str(&self.interner).to_string(),
node.id().into_owned(),
))
}
pub fn set_node_timeseries(&mut self, node_idx: NodeIndex, timeseries: NodeTimeseries) {
if self.records_payloads() {
if let Some((node_type, id)) = self.logical_key(node_idx) {
self.note_declaration(MutationOp::SetNodeTimeseries {
node_type,
id,
timeseries: timeseries.clone(),
});
}
}
self.install_node_timeseries(node_idx, timeseries);
}
pub(crate) fn install_node_timeseries(
&mut self,
node_idx: NodeIndex,
timeseries: NodeTimeseries,
) {
self.timeseries_store.insert(node_idx.index(), timeseries);
}
pub fn set_timeseries_config(&mut self, node_type: &str, config: TimeseriesConfig) {
if self.records_payloads() {
if let Ok(document) = serde_json::to_string(&config) {
self.note_declaration(MutationOp::SetTimeseriesConfig {
node_type: node_type.to_string(),
config: document,
});
}
}
self.install_timeseries_config(node_type, config);
}
pub(crate) fn install_timeseries_config(&mut self, node_type: &str, config: TimeseriesConfig) {
self.timeseries_configs
.insert(node_type.to_string(), config);
}
pub fn add_timeseries_channel(
&mut self,
node_idx: NodeIndex,
channel: String,
values: Vec<f64>,
) -> Result<(), String> {
let series = self
.timeseries_store
.get(&node_idx.index())
.ok_or("Node has no time index. Call set_time_index() first.")?;
crate::graph::features::timeseries::validate_channel_length(
series.keys.len(),
values.len(),
&channel,
)?;
let node_type = self
.logical_key(node_idx)
.map(|(node_type, _)| node_type)
.filter(|node_type| self.timeseries_configs.contains_key(node_type));
if let Some(node_type) = node_type {
let config = self
.timeseries_configs
.get(&node_type)
.expect("presence filtered immediately above");
if !config.channels.contains(&channel) {
let mut config = config.clone();
config.channels.push(channel.clone());
self.set_timeseries_config(&node_type, config);
}
}
let mut series = self
.timeseries_store
.get(&node_idx.index())
.expect("presence checked at entry")
.clone();
series.channels.insert(channel, values);
self.set_node_timeseries(node_idx, series);
Ok(())
}
pub fn set_embedding_store(
&mut self,
node_type: &str,
text_column: &str,
store: EmbeddingStore,
) {
self.install_embedding_store(node_type, text_column, store);
if self.records_payloads() {
let slots = self.embedding_slots(node_type, text_column);
self.note_embedding_write(node_type, text_column, EmbeddingWrite::Replace, &slots);
}
}
pub(crate) fn install_embedding_store(
&mut self,
node_type: &str,
text_column: &str,
store: EmbeddingStore,
) {
self.embeddings
.insert(store_key(node_type, text_column), store);
}
pub fn remove_embedding_store(&mut self, node_type: &str, text_column: &str) -> bool {
let removed = self.install_embedding_removal(node_type, text_column);
self.note_embedding_write(node_type, text_column, EmbeddingWrite::Withdraw, &[]);
removed
}
pub(crate) fn install_embedding_removal(&mut self, node_type: &str, text_column: &str) -> bool {
self.embeddings
.remove(&store_key(node_type, text_column))
.is_some()
}
pub(crate) fn embedding_slots(&self, node_type: &str, text_column: &str) -> Vec<usize> {
self.embeddings
.get(&store_key(node_type, text_column))
.map(|store| store.slot_to_node.clone())
.unwrap_or_default()
}
pub(crate) fn note_embedding_write(
&mut self,
node_type: &str,
text_column: &str,
mode: EmbeddingWrite,
slots: &[usize],
) {
if !self.records_payloads() {
return;
}
let op = self.embedding_op(node_type, text_column, mode, slots);
self.note_declaration(op);
}
fn embedding_op(
&self,
node_type: &str,
text_column: &str,
mode: EmbeddingWrite,
slots: &[usize],
) -> MutationOp {
let _arena_guard = self.graph.begin_query();
let store = self.embeddings.get(&store_key(node_type, text_column));
let mut entries = Vec::with_capacity(slots.len());
if let Some(store) = store {
for &slot in slots {
let (Some(vector), Some(node)) = (
store.get_embedding(slot),
self.graph.node_view(NodeIndex::new(slot)),
) else {
continue;
};
entries.push((
node.id().into_owned(),
vector.to_vec(),
store.text_hashes.get(&slot).copied(),
));
}
}
MutationOp::SetEmbeddings {
node_type: node_type.to_string(),
text_column: text_column.to_string(),
dimension: store.map_or(0, |store| store.dimension),
metric: store.and_then(|store| store.metric.clone()),
model_id: store.and_then(|store| store.model_id.clone()),
entries,
mode,
}
}
pub(crate) fn install_embedding_entries(
&mut self,
node_type: &str,
text_column: &str,
provenance: (usize, Option<String>, Option<String>),
entries: &[(Value, Vec<f32>, Option<u64>)],
) {
let (dimension, metric, model_id) = provenance;
self.build_id_index(node_type);
let mut store = EmbeddingStore::new(dimension);
store.metric = metric;
store.model_id = model_id;
for (id, vector, hash) in entries {
let Some(node_idx) = self.lookup_by_id_normalized(node_type, id) else {
continue;
};
store.set_embedding(node_idx.index(), vector);
if let Some(hash) = hash {
store.set_text_hash(node_idx.index(), *hash);
}
}
self.install_embedding_store(node_type, text_column, store);
}
}