Skip to main content

kyma_memory/
rows.rs

1//! Build JSON rows for the columnar memory tables. Rows are serialized to
2//! NDJSON and coerced to Arrow by `kyma_ingest_core::parse_ndjson`, so the
3//! `embedding` value is a plain JSON array of floats.
4
5use serde_json::{json, Value};
6use uuid::Uuid;
7
8use crate::types::CreateMemory;
9
10/// Max length of the stored content preview.
11const PREVIEW_CHARS: usize = 280;
12
13/// Canonical node id for a memory uuid.
14pub fn node_id(id: &Uuid) -> String {
15    format!("memory:{id}")
16}
17
18/// Deterministic edge id, matching the `{src}->{dst}:{type}` convention used
19/// elsewhere so dedup keys stay stable.
20pub fn edge_id(src: &str, dst: &str, rel_type: &str) -> String {
21    format!("{src}->{dst}:{rel_type}")
22}
23
24pub fn preview(content: &str) -> String {
25    let trimmed = content.trim();
26    if trimmed.chars().count() <= PREVIEW_CHARS {
27        return trimmed.to_string();
28    }
29    let cut: String = trimmed.chars().take(PREVIEW_CHARS).collect();
30    format!("{cut}…")
31}
32
33/// Build a `memory_nodes` row. `now` is an RFC3339 timestamp.
34pub fn node_row(id: &Uuid, m: &CreateMemory, embedding: &[f32], now: &str) -> Value {
35    json!({
36        "id": node_id(id),
37        "labels": "Memory",
38        "realm": m.realm,
39        "memory_type": m.memory_type.as_str(),
40        "title": m.title,
41        "content": m.content,
42        "content_preview": preview(&m.content),
43        "tags": m.tags.join(","),
44        "importance": m.importance as f64,
45        "status": "active",
46        "source_session_id": m.source_session_id.map(|u| u.to_string()),
47        "source_run_id": m.source_run_id.map(|u| u.to_string()),
48        "embedding": embedding,
49        "created_at": now,
50        "updated_at": now,
51        "valid_at": m.valid_at.as_deref().unwrap_or(now),
52        "invalid_at": Value::Null,
53        "superseded_by": Value::Null,
54        "provenance": m.provenance.as_ref().map(|p| p.to_string()),
55        "topic_key": m.topic_key,
56    })
57}
58
59/// Build a `memory_edges` row.
60pub fn edge_row(
61    src: &str,
62    dst: &str,
63    rel_type: &str,
64    realm: &str,
65    target_namespace: Option<&str>,
66    props: Option<&Value>,
67    now: &str,
68) -> Value {
69    json!({
70        "id": edge_id(src, dst, rel_type),
71        "src": src,
72        "dst": dst,
73        "type": rel_type,
74        "realm": realm,
75        "target_namespace": target_namespace,
76        "props": props.map(|p| p.to_string()),
77        "created_at": now,
78    })
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::types::{CreateMemory, MemoryType};
85
86    #[test]
87    fn preview_truncates_long_content() {
88        let long = "x".repeat(1000);
89        let p = preview(&long);
90        assert!(p.chars().count() <= PREVIEW_CHARS + 1);
91        assert!(p.ends_with('…'));
92    }
93
94    #[test]
95    fn node_row_carries_embedding_array_and_ids() {
96        let id = Uuid::nil();
97        let mut m = CreateMemory::new("hello world");
98        m.memory_type = MemoryType::Decision;
99        m.tags = vec!["a".into(), "b".into()];
100        // Use f32-exact values so the JSON round-trip compares cleanly.
101        let row = node_row(&id, &m, &[0.5, 0.25, 0.125], "2026-05-31T00:00:00Z");
102        assert_eq!(row["id"], json!("memory:00000000-0000-0000-0000-000000000000"));
103        assert_eq!(row["memory_type"], json!("decision"));
104        assert_eq!(row["tags"], json!("a,b"));
105        assert_eq!(row["embedding"], json!([0.5, 0.25, 0.125]));
106        assert_eq!(row["status"], json!("active"));
107    }
108
109    #[test]
110    fn edge_id_is_stable() {
111        assert_eq!(
112            edge_id("memory:1", "default::repo:x", "REFERENCES"),
113            "memory:1->default::repo:x:REFERENCES"
114        );
115    }
116}