use anyhow::{bail, Context, Result};
use rusqlite::{params, Connection};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphNodeKind {
Memory,
Entity,
Fact,
Episode,
State,
Topic,
File,
}
impl GraphNodeKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Memory => "memory",
Self::Entity => "entity",
Self::Fact => "fact",
Self::Episode => "episode",
Self::State => "state",
Self::Topic => "topic",
Self::File => "file",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GraphNodeRef {
pub kind: GraphNodeKind,
pub id: i64,
}
impl GraphNodeRef {
pub fn new(kind: GraphNodeKind, id: i64) -> Result<Self> {
if id <= 0 {
bail!("graph node id must be positive");
}
Ok(Self { kind, id })
}
pub fn memory(id: i64) -> Result<Self> {
Self::new(GraphNodeKind::Memory, id)
}
pub fn entity(id: i64) -> Result<Self> {
Self::new(GraphNodeKind::Entity, id)
}
pub fn fact(id: i64) -> Result<Self> {
Self::new(GraphNodeKind::Fact, id)
}
pub fn episode(id: i64) -> Result<Self> {
Self::new(GraphNodeKind::Episode, id)
}
pub fn state(id: i64) -> Result<Self> {
Self::new(GraphNodeKind::State, id)
}
pub fn topic(id: i64) -> Result<Self> {
Self::new(GraphNodeKind::Topic, id)
}
pub fn file(id: i64) -> Result<Self> {
Self::new(GraphNodeKind::File, id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphEdgeTrust {
Trusted,
DiagnosticHint,
}
impl GraphEdgeTrust {
pub const fn as_str(self) -> &'static str {
match self {
Self::Trusted => "trusted",
Self::DiagnosticHint => "diagnostic_hint",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphEdgeType {
Supersedes,
Duplicates,
Conflicts,
DerivedFrom,
MergedInto,
SplitFrom,
ExtractedFrom,
Mentions,
TouchesFile,
HasState,
HasTopic,
SimilarTo,
CandidateHint,
CoOccursWith,
}
impl GraphEdgeType {
pub const fn as_str(self) -> &'static str {
match self {
Self::Supersedes => "supersedes",
Self::Duplicates => "duplicates",
Self::Conflicts => "conflicts",
Self::DerivedFrom => "derived_from",
Self::MergedInto => "merged_into",
Self::SplitFrom => "split_from",
Self::ExtractedFrom => "extracted_from",
Self::Mentions => "mentions",
Self::TouchesFile => "touches_file",
Self::HasState => "has_state",
Self::HasTopic => "has_topic",
Self::SimilarTo => "similar_to",
Self::CandidateHint => "candidate_hint",
Self::CoOccursWith => "co_occurs_with",
}
}
pub const fn trust(self) -> GraphEdgeTrust {
match self {
Self::SimilarTo | Self::CandidateHint | Self::CoOccursWith => {
GraphEdgeTrust::DiagnosticHint
}
Self::Supersedes
| Self::Duplicates
| Self::Conflicts
| Self::DerivedFrom
| Self::MergedInto
| Self::SplitFrom
| Self::ExtractedFrom
| Self::Mentions
| Self::TouchesFile
| Self::HasState
| Self::HasTopic => GraphEdgeTrust::Trusted,
}
}
pub const fn allows_endpoints(self, from: GraphNodeKind, to: GraphNodeKind) -> bool {
match self {
Self::Supersedes
| Self::Duplicates
| Self::Conflicts
| Self::DerivedFrom
| Self::MergedInto
| Self::SplitFrom
| Self::SimilarTo
| Self::CandidateHint
| Self::CoOccursWith => same_graph_node_kind(from, to),
Self::ExtractedFrom => {
matches!(
from,
GraphNodeKind::Entity
| GraphNodeKind::Fact
| GraphNodeKind::State
| GraphNodeKind::Topic
) && matches!(to, GraphNodeKind::Episode)
}
Self::Mentions => {
matches!(from, GraphNodeKind::Memory | GraphNodeKind::Episode)
&& matches!(to, GraphNodeKind::Entity)
}
Self::TouchesFile => {
matches!(from, GraphNodeKind::Memory | GraphNodeKind::Episode)
&& matches!(to, GraphNodeKind::File)
}
Self::HasState => {
matches!(from, GraphNodeKind::Memory) && matches!(to, GraphNodeKind::State)
}
Self::HasTopic => {
matches!(from, GraphNodeKind::Memory | GraphNodeKind::Episode)
&& matches!(to, GraphNodeKind::Topic)
}
}
}
}
const fn same_graph_node_kind(left: GraphNodeKind, right: GraphNodeKind) -> bool {
matches!(
(left, right),
(GraphNodeKind::Memory, GraphNodeKind::Memory)
| (GraphNodeKind::Entity, GraphNodeKind::Entity)
| (GraphNodeKind::Fact, GraphNodeKind::Fact)
| (GraphNodeKind::Episode, GraphNodeKind::Episode)
| (GraphNodeKind::State, GraphNodeKind::State)
| (GraphNodeKind::Topic, GraphNodeKind::Topic)
| (GraphNodeKind::File, GraphNodeKind::File)
)
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct GraphEdgeProvenance<'a> {
pub source_event_ids: &'a [i64],
pub source_candidate_id: Option<i64>,
pub source_operation_id: Option<i64>,
pub confidence: Option<f64>,
pub reason: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct GraphEdgeInput<'a> {
pub edge_type: GraphEdgeType,
pub from_node: GraphNodeRef,
pub to_node: GraphNodeRef,
pub provenance: GraphEdgeProvenance<'a>,
pub valid_from_epoch: Option<i64>,
pub valid_to_epoch: Option<i64>,
}
pub fn insert_graph_edge(conn: &Connection, input: &GraphEdgeInput<'_>) -> Result<i64> {
validate_graph_edge(conn, input)?;
let now = chrono::Utc::now().timestamp();
let source_event_ids = serde_json::to_string(input.provenance.source_event_ids)
.context("serialize graph edge source event ids")?;
conn.execute(
"INSERT INTO graph_edges
(edge_type, edge_trust, from_node_kind, from_node_id, to_node_kind, to_node_id,
source_event_ids, source_candidate_id, source_operation_id, confidence, reason,
valid_from_epoch, valid_to_epoch, created_at_epoch)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
params![
input.edge_type.as_str(),
input.edge_type.trust().as_str(),
input.from_node.kind.as_str(),
input.from_node.id,
input.to_node.kind.as_str(),
input.to_node.id,
source_event_ids,
input.provenance.source_candidate_id,
input.provenance.source_operation_id,
input.provenance.confidence,
input.provenance.reason,
input.valid_from_epoch,
input.valid_to_epoch,
now
],
)
.context("insert graph edge")?;
Ok(conn.last_insert_rowid())
}
fn validate_graph_edge(conn: &Connection, input: &GraphEdgeInput<'_>) -> Result<()> {
if input.from_node == input.to_node {
bail!("graph edge cannot link a node to itself");
}
if !input
.edge_type
.allows_endpoints(input.from_node.kind, input.to_node.kind)
{
bail!(
"graph edge type {} does not allow {} to {} endpoints",
input.edge_type.as_str(),
input.from_node.kind.as_str(),
input.to_node.kind.as_str()
);
}
if let (Some(valid_from), Some(valid_to)) = (input.valid_from_epoch, input.valid_to_epoch) {
if valid_to < valid_from {
bail!("graph edge valid_to_epoch cannot be before valid_from_epoch");
}
}
if let Some(confidence) = input.provenance.confidence {
if !(0.0..=1.0).contains(&confidence) {
bail!("graph edge confidence out of range");
}
}
if input.edge_type.trust() == GraphEdgeTrust::Trusted {
validate_trusted_provenance(conn, input.provenance)?;
}
Ok(())
}
fn validate_trusted_provenance(
conn: &Connection,
provenance: GraphEdgeProvenance<'_>,
) -> Result<()> {
if provenance.source_event_ids.is_empty() {
bail!("trusted graph edge requires source event ids");
}
if provenance.source_event_ids.iter().any(|id| *id <= 0) {
bail!("trusted graph edge source event ids must be positive");
}
for event_id in provenance.source_event_ids {
let exists: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM captured_events WHERE id = ?1)",
[event_id],
|row| row.get(0),
)
.with_context(|| format!("validate graph edge source event id {event_id}"))?;
if !exists {
bail!("trusted graph edge source event id {event_id} does not exist");
}
}
crate::memory::graph_provenance::validate_trusted_source_candidate(
conn,
provenance.source_candidate_id,
provenance.source_operation_id,
)?;
if provenance.confidence.is_none() {
bail!("trusted graph edge requires confidence");
}
let reason = provenance.reason.unwrap_or_default().trim();
if reason.is_empty() {
bail!("trusted graph edge requires reason");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::facts::{insert_temporal_fact, FactPredicate, TemporalFactInput};
struct GraphFixture {
conn: Connection,
memory_id: i64,
second_memory_id: i64,
entity_id: i64,
second_entity_id: i64,
fact_id: i64,
episode_id: i64,
state_id: i64,
topic_id: i64,
candidate_id: i64,
operation_id: i64,
}
fn fixture() -> Result<GraphFixture> {
let mut conn = Connection::open_in_memory()?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
crate::migrate::run_migrations(&conn)?;
let now = 1_700_000_000_i64;
let host_id: i64 =
conn.query_row("SELECT id FROM hosts WHERE name = 'codex-cli'", [], |row| {
row.get(0)
})?;
conn.execute(
"INSERT INTO workspaces(root_path, git_remote, git_branch, created_at_epoch, updated_at_epoch)
VALUES ('/tmp/remem-graph', 'origin', 'main', ?1, ?1)",
[now],
)?;
let workspace_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO projects(workspace_id, project_path, project_key, created_at_epoch, updated_at_epoch)
VALUES (?1, '/tmp/remem-graph', 'tmp-remem-graph', ?2, ?2)",
params![workspace_id, now],
)?;
let project_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO sessions(host_id, workspace_id, project_id, session_id, started_at_epoch,
last_seen_at_epoch, status)
VALUES (?1, ?2, ?3, 'session-a', ?4, ?4, 'active')",
params![host_id, workspace_id, project_id, now],
)?;
let session_row_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO captured_events(host_id, workspace_id, project_id, session_row_id,
session_id, event_id, event_type, content_hash,
retention_class, created_at_epoch, inserted_at_epoch)
VALUES (?1, ?2, ?3, ?4, 'session-a', 'event-a', 'message',
'hash-a', 'default', ?5, ?5)",
params![host_id, workspace_id, project_id, session_row_id, now],
)?;
let episode_id = conn.last_insert_rowid();
let memory_id = crate::memory::insert_memory(
&conn,
Some("session-a"),
"/tmp/remem-graph",
Some("graph-contract"),
"Graph contract",
"Typed graph refs are available.",
"decision",
None,
)?;
let second_memory_id = crate::memory::insert_memory(
&conn,
Some("session-a"),
"/tmp/remem-graph",
Some("graph-contract-2"),
"Graph contract follow-up",
"Typed graph refs keep endpoints constrained.",
"decision",
None,
)?;
conn.execute(
"INSERT INTO entities(canonical_name, entity_type, mention_count, created_at_epoch)
VALUES ('Graph API', 'concept', 1, ?1)",
[now],
)?;
let entity_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO entities(canonical_name, entity_type, mention_count, created_at_epoch)
VALUES ('Graph API Review', 'concept', 1, ?1)",
[now],
)?;
let second_entity_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO memory_state_keys(owner_scope, owner_key, memory_type, state_key,
state_label, current_memory_id,
created_at_epoch, updated_at_epoch)
VALUES ('project', '/tmp/remem-graph', 'decision', 'graph-contract',
'graph contract', ?1, ?2, ?2)",
params![memory_id, now],
)?;
let state_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO topic_segments(host_id, project_id, session_row_id, project, topic_key,
title, summary, status, segment_index,
covered_from_event_id, covered_to_event_id,
evidence_event_ids, confidence,
created_at_epoch, updated_at_epoch)
VALUES (?1, ?2, ?3, '/tmp/remem-graph', 'graph-contract',
'Graph contract', 'Typed graph refs.', 'resolved', 0,
?4, ?4, ?5, 0.9, ?6, ?6)",
params![
host_id,
project_id,
session_row_id,
episode_id,
format!("[{episode_id}]"),
now
],
)?;
let topic_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO memory_candidates(project_id, scope, memory_type, topic_key, text,
evidence_event_ids, confidence, risk_class,
review_status, created_at_epoch, updated_at_epoch)
VALUES (?1, 'project', 'decision', 'graph-contract', 'Typed graph refs.',
?2, 0.9, 'low', 'accepted', ?3, ?3)",
params![project_id, format!("[{episode_id}]"), now],
)?;
let candidate_id = conn.last_insert_rowid();
conn.execute(
"INSERT INTO memory_operation_log(operation, planner_version, actor, source,
owner_scope, owner_key, memory_type, state_key,
source_candidate_id, result_memory_id,
superseded_ids, conflicting_ids, confidence,
reason, created_at_epoch)
VALUES ('add', 'graph-contract-test', 'test', 'memory_candidate',
'project', '/tmp/remem-graph', 'decision', 'graph-contract',
?1, ?2, '[]', '[]', 0.9, 'test provenance', ?3)",
params![candidate_id, memory_id, now],
)?;
let operation_id = conn.last_insert_rowid();
let fact_id = insert_temporal_fact(
&mut conn,
&TemporalFactInput {
project: "/tmp/remem-graph",
subject: "graph-contract",
predicate: FactPredicate::VerifiedBy,
object: "cargo test memory::graph",
valid_from_epoch: Some(now),
valid_to_epoch: None,
learned_at_epoch: Some(now),
source_memory_id: Some(memory_id),
source_observation_id: None,
source_event_ids: &[episode_id],
confidence: 0.9,
supersedes_fact_id: None,
},
)?;
Ok(GraphFixture {
conn,
memory_id,
second_memory_id,
entity_id,
second_entity_id,
fact_id,
episode_id,
state_id,
topic_id,
candidate_id,
operation_id,
})
}
fn trusted_provenance(fixture: &GraphFixture) -> GraphEdgeProvenance<'_> {
GraphEdgeProvenance {
source_event_ids: std::slice::from_ref(&fixture.episode_id),
source_candidate_id: Some(fixture.candidate_id),
source_operation_id: Some(fixture.operation_id),
confidence: Some(0.9),
reason: Some("test provenance"),
}
}
fn insert_raw_trusted_mention(
fixture: &GraphFixture,
source_event_ids: &str,
) -> rusqlite::Result<usize> {
fixture.conn.execute(
"INSERT INTO graph_edges
(edge_type, edge_trust, from_node_kind, from_node_id, to_node_kind, to_node_id,
source_event_ids, source_candidate_id, source_operation_id, confidence, reason,
created_at_epoch)
VALUES ('mentions', 'trusted', 'memory', ?1, 'entity', ?2,
?3, ?4, ?5, 0.9, 'raw trusted provenance', 1)",
params![
fixture.memory_id,
fixture.entity_id,
source_event_ids,
fixture.candidate_id,
fixture.operation_id
],
)
}
#[test]
fn graph_node_refs_use_stable_db_values() -> Result<()> {
assert_eq!(GraphNodeRef::memory(1)?.kind.as_str(), "memory");
assert_eq!(GraphNodeRef::entity(1)?.kind.as_str(), "entity");
assert_eq!(GraphNodeRef::fact(1)?.kind.as_str(), "fact");
assert_eq!(GraphNodeRef::episode(1)?.kind.as_str(), "episode");
assert_eq!(GraphNodeRef::state(1)?.kind.as_str(), "state");
assert_eq!(GraphNodeRef::topic(1)?.kind.as_str(), "topic");
assert!(GraphNodeRef::memory(0).is_err());
Ok(())
}
#[test]
fn trusted_edge_requires_complete_provenance() -> Result<()> {
let fixture = fixture()?;
let err = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type: GraphEdgeType::Mentions,
from_node: GraphNodeRef::memory(fixture.memory_id)?,
to_node: GraphNodeRef::entity(fixture.entity_id)?,
provenance: GraphEdgeProvenance::default(),
valid_from_epoch: None,
valid_to_epoch: None,
},
)
.expect_err("trusted graph edge without provenance must fail");
assert!(err.to_string().contains("source event ids"));
Ok(())
}
#[test]
fn inserts_typed_graph_edges_for_supported_node_refs() -> Result<()> {
let fixture = fixture()?;
let edges = [
(
GraphEdgeType::Mentions,
GraphNodeRef::memory(fixture.memory_id)?,
GraphNodeRef::entity(fixture.entity_id)?,
),
(
GraphEdgeType::ExtractedFrom,
GraphNodeRef::fact(fixture.fact_id)?,
GraphNodeRef::episode(fixture.episode_id)?,
),
(
GraphEdgeType::HasState,
GraphNodeRef::memory(fixture.memory_id)?,
GraphNodeRef::state(fixture.state_id)?,
),
(
GraphEdgeType::HasTopic,
GraphNodeRef::memory(fixture.memory_id)?,
GraphNodeRef::topic(fixture.topic_id)?,
),
];
for (edge_type, from_node, to_node) in edges {
let id = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type,
from_node,
to_node,
provenance: trusted_provenance(&fixture),
valid_from_epoch: Some(1_700_000_000),
valid_to_epoch: None,
},
)?;
assert!(id > 0);
}
let count: i64 = fixture
.conn
.query_row("SELECT COUNT(*) FROM graph_edges", [], |row| row.get(0))?;
assert_eq!(count, 4);
Ok(())
}
#[test]
fn diagnostic_hint_can_be_written_without_trusted_provenance() -> Result<()> {
let fixture = fixture()?;
let id = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type: GraphEdgeType::SimilarTo,
from_node: GraphNodeRef::memory(fixture.memory_id)?,
to_node: GraphNodeRef::memory(fixture.second_memory_id)?,
provenance: GraphEdgeProvenance::default(),
valid_from_epoch: None,
valid_to_epoch: None,
},
)?;
let trust: String = fixture.conn.query_row(
"SELECT edge_trust FROM graph_edges WHERE id = ?1",
[id],
|row| row.get(0),
)?;
assert_eq!(trust, "diagnostic_hint");
Ok(())
}
#[test]
fn missing_target_node_fails_closed() -> Result<()> {
let fixture = fixture()?;
let err = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type: GraphEdgeType::SimilarTo,
from_node: GraphNodeRef::memory(fixture.memory_id)?,
to_node: GraphNodeRef::memory(99_999)?,
provenance: GraphEdgeProvenance::default(),
valid_from_epoch: None,
valid_to_epoch: None,
},
)
.expect_err("missing graph target must fail");
assert!(err.to_string().contains("insert graph edge"));
let chain = format!("{err:#}");
assert!(chain.contains("graph_edges to memory node missing"));
Ok(())
}
#[test]
fn trusted_edge_requires_existing_source_events() -> Result<()> {
let fixture = fixture()?;
let missing_event_id = 99_999_i64;
let source_event_ids = [missing_event_id];
let err = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type: GraphEdgeType::Mentions,
from_node: GraphNodeRef::memory(fixture.memory_id)?,
to_node: GraphNodeRef::entity(fixture.entity_id)?,
provenance: GraphEdgeProvenance {
source_event_ids: &source_event_ids,
source_candidate_id: Some(fixture.candidate_id),
source_operation_id: Some(fixture.operation_id),
confidence: Some(0.9),
reason: Some("missing event must fail"),
},
valid_from_epoch: None,
valid_to_epoch: None,
},
)
.expect_err("trusted graph edge with missing evidence must fail");
assert!(err
.to_string()
.contains("source event id 99999 does not exist"));
Ok(())
}
#[test]
fn invalid_edge_type_fails_closed_at_schema_boundary() -> Result<()> {
let fixture = fixture()?;
let err = fixture
.conn
.execute(
"INSERT INTO graph_edges
(edge_type, edge_trust, from_node_kind, from_node_id, to_node_kind, to_node_id,
source_event_ids, source_candidate_id, source_operation_id, confidence, reason,
created_at_epoch)
VALUES ('made_up', 'trusted', 'memory', ?1, 'entity', ?2,
?3, ?4, ?5, 0.9, 'invalid edge type', 1)",
params![
fixture.memory_id,
fixture.entity_id,
format!("[{}]", fixture.episode_id),
fixture.candidate_id,
fixture.operation_id
],
)
.expect_err("invalid graph edge type must fail");
assert!(err.to_string().contains("CHECK constraint failed"));
Ok(())
}
#[test]
fn invalid_source_event_ids_fail_closed_at_schema_boundary() -> Result<()> {
let fixture = fixture()?;
for source_event_ids in ["[ ]", "not-json", "{\"id\":1}", "[\"event-a\"]", "[99999]"] {
assert!(
insert_raw_trusted_mention(&fixture, source_event_ids).is_err(),
"invalid source_event_ids {source_event_ids:?} must fail"
);
}
Ok(())
}
#[test]
fn edge_type_rejects_invalid_endpoint_kinds() -> Result<()> {
let fixture = fixture()?;
let err = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type: GraphEdgeType::HasState,
from_node: GraphNodeRef::entity(fixture.entity_id)?,
to_node: GraphNodeRef::memory(fixture.memory_id)?,
provenance: trusted_provenance(&fixture),
valid_from_epoch: None,
valid_to_epoch: None,
},
)
.expect_err("has_state with entity to memory endpoints must fail");
assert!(err
.to_string()
.contains("graph edge type has_state does not allow entity to memory endpoints"));
let err = fixture
.conn
.execute(
"INSERT INTO graph_edges
(edge_type, edge_trust, from_node_kind, from_node_id, to_node_kind, to_node_id,
source_event_ids, source_candidate_id, source_operation_id, confidence, reason,
created_at_epoch)
VALUES ('has_state', 'trusted', 'entity', ?1, 'memory', ?2,
?3, ?4, ?5, 0.9, 'invalid endpoints', 1)",
params![
fixture.entity_id,
fixture.memory_id,
format!("[{}]", fixture.episode_id),
fixture.candidate_id,
fixture.operation_id
],
)
.expect_err("raw SQL invalid graph endpoints must fail");
assert!(err.to_string().contains("CHECK constraint failed"));
Ok(())
}
#[test]
fn parent_node_deletes_remove_graph_edges() -> Result<()> {
let fixture = fixture()?;
let entity_edge_id = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type: GraphEdgeType::Mentions,
from_node: GraphNodeRef::memory(fixture.memory_id)?,
to_node: GraphNodeRef::entity(fixture.second_entity_id)?,
provenance: trusted_provenance(&fixture),
valid_from_epoch: None,
valid_to_epoch: None,
},
)?;
let state_edge_id = insert_graph_edge(
&fixture.conn,
&GraphEdgeInput {
edge_type: GraphEdgeType::HasState,
from_node: GraphNodeRef::memory(fixture.memory_id)?,
to_node: GraphNodeRef::state(fixture.state_id)?,
provenance: trusted_provenance(&fixture),
valid_from_epoch: None,
valid_to_epoch: None,
},
)?;
fixture.conn.execute(
"DELETE FROM entities WHERE id = ?1",
[fixture.second_entity_id],
)?;
let entity_edge_count: i64 = fixture.conn.query_row(
"SELECT COUNT(*) FROM graph_edges WHERE id = ?1",
[entity_edge_id],
|row| row.get(0),
)?;
assert_eq!(entity_edge_count, 0);
fixture.conn.execute(
"DELETE FROM memory_state_keys WHERE id = ?1",
[fixture.state_id],
)?;
let state_edge_count: i64 = fixture.conn.query_row(
"SELECT COUNT(*) FROM graph_edges WHERE id = ?1",
[state_edge_id],
|row| row.get(0),
)?;
assert_eq!(state_edge_count, 0);
Ok(())
}
}