{"record":{"id":"cc36d3499860073e","repo":"affaan-m/ECC","slug":"context-graph-relation-type-cannot-be-empty","errorCode":null,"errorMessage":"Context graph relation type cannot be empty","messagePattern":"Context graph relation type cannot be empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"ecc2/src/session/store.rs","lineNumber":3673,"sourceCode":"        Ok(ContextGraphCompactionStats {\n            entities_scanned,\n            duplicate_observations_deleted,\n            overflow_observations_deleted,\n            observations_retained,\n        })\n    }\n\n    pub fn upsert_context_relation(\n        &self,\n        session_id: Option<&str>,\n        from_entity_id: i64,\n        to_entity_id: i64,\n        relation_type: &str,\n        summary: &str,\n    ) -> Result<ContextGraphRelation> {\n        let relation_type = relation_type.trim();\n        if relation_type.is_empty() {\n            return Err(anyhow::anyhow!(\n                \"Context graph relation type cannot be empty\"\n            ));\n        }\n        let summary = summary.trim();\n        let timestamp = chrono::Utc::now().to_rfc3339();\n\n        self.conn.execute(\n            \"INSERT INTO context_graph_relations (\n                session_id, from_entity_id, to_entity_id, relation_type, summary, created_at\n             )\n             VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n             ON CONFLICT(from_entity_id, to_entity_id, relation_type) DO UPDATE SET\n                session_id = COALESCE(excluded.session_id, context_graph_relations.session_id),\n                summary = CASE\n                    WHEN excluded.summary <> '' THEN excluded.summary\n                    ELSE context_graph_relations.summary\n                END\",\n            rusqlite::params![","sourceCodeStart":3655,"sourceCodeEnd":3691,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/ecc2/src/session/store.rs#L3655-L3691","documentation":"Raised by upsert_context_relation in ecc2/src/session/store.rs:3673 when relation_type.trim().is_empty(). Relations are edges between context-graph entities with a unique constraint on (from_entity_id, to_entity_id, relation_type); an empty type would collide all relations between the same pair and lose semantic distinction, so it is rejected before the INSERT ... ON CONFLICT upsert.","triggerScenarios":"Passing relation_type = \"\"; importing edges where the type column is blank; programmatic relation creation where the type was supposed to be derived but the source was empty.","commonSituations":"Bulk import of relation data with sparse type columns; auto-derived relation types from a model that emitted nothing; UI forms that do not require a relation type.","solutions":["Validate relation_type is non-empty after trim at the call site; require a known vocabulary term.","During import, skip rows with empty types or map to a default like \"related_to\".","Co-locate validation so the store never receives empty types."],"exampleFix":"// before\nstore.upsert_context_relation(Some(sid), from_id, to_id, \"\", summary)?;\n\n// after: enforce a non-empty relation type\nlet relation_type = relation_type.trim();\nif relation_type.is_empty() {\n    anyhow::bail!(\"relation_type is required between {from_id} and {to_id}\");\n}\nstore.upsert_context_relation(Some(sid), from_id, to_id, relation_type, summary)?;","handlingStrategy":"validation","validationCode":"// Validating newtype for relation_type, optionally against a vocabulary.\n#[derive(Debug, Clone)]\npub struct RelationType(String);\n\nimpl RelationType {\n    pub fn new(raw: &str) -> anyhow::Result<Self> {\n        let trimmed = raw.trim();\n        if trimmed.is_empty() {\n            anyhow::bail!(\"relation type cannot be empty\");\n        }\n        Ok(Self(trimmed.to_string()))\n    }\n    pub fn as_str(&self) -> &str { &self.0 }\n}\n\nlet rtype = RelationType::new(raw)?;\nstore.upsert_context_relation(sid, from_id, to_id, rtype.as_str(), summary)?;","typeGuard":"// RelationType (above) is the type guard; it cannot represent an empty\n// string. Optionally add a vocabulary check in the constructor.","tryCatchPattern":"for edge in relation_import {\n    let rtype = match RelationType::new(&edge.relation_type) {\n        Ok(r) => r,\n        Err(_) => { tracing::warn!(\"skipping relation {} -> {} with empty type\", edge.from, edge.to); continue; }\n    };\n    store.upsert_context_relation(sid, edge.from, edge.to, rtype.as_str(), &edge.summary)?;\n}","preventionTips":["Wrap relation_type in a validating newtype; optionally enforce a controlled vocabulary.","Skip blank-type edges during import.","Require a relation type at the UI layer.","Co-locate validation so the store never receives empty types."],"tags":["database","context-graph","validation","relation","empty-string"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}