Skip to main content

recall_echo/graph/
store.rs

1//! SurrealDB embedded store — open, schema init.
2
3use std::path::Path;
4
5use surrealdb::engine::local::SurrealKv;
6use surrealdb::Surreal;
7
8use super::error::GraphError;
9
10pub type Db = surrealdb::engine::local::Db;
11
12const NAMESPACE: &str = "recall";
13const DATABASE: &str = "graph";
14
15/// Open (or create) a SurrealDB embedded store at the given path.
16pub async fn open(path: &Path) -> Result<Surreal<Db>, GraphError> {
17    let surreal_path = path.join("surreal");
18    std::fs::create_dir_all(&surreal_path)?;
19
20    let db: Surreal<Db> = Surreal::new::<SurrealKv>(surreal_path.to_str().unwrap()).await?;
21    db.use_ns(NAMESPACE).use_db(DATABASE).await?;
22
23    Ok(db)
24}
25
26/// Initialize the graph schema. Idempotent — safe to call on every open.
27pub async fn init_schema(db: &Surreal<Db>) -> Result<(), GraphError> {
28    db.query(
29        r#"
30        DEFINE TABLE IF NOT EXISTS entity SCHEMAFULL;
31        DEFINE FIELD IF NOT EXISTS name         ON entity TYPE string;
32        DEFINE FIELD IF NOT EXISTS entity_type  ON entity TYPE string;
33        DEFINE FIELD IF NOT EXISTS abstract     ON entity TYPE string;
34        DEFINE FIELD IF NOT EXISTS overview     ON entity TYPE string;
35        DEFINE FIELD IF NOT EXISTS content      ON entity TYPE option<string>;
36        DEFINE FIELD IF NOT EXISTS attributes ON entity TYPE option<object> FLEXIBLE;
37        DEFINE FIELD IF NOT EXISTS embedding    ON entity TYPE option<array<float>>;
38        DEFINE FIELD IF NOT EXISTS mutable      ON entity TYPE bool DEFAULT true;
39        DEFINE FIELD IF NOT EXISTS access_count ON entity TYPE int DEFAULT 0;
40        DEFINE FIELD IF NOT EXISTS created_at   ON entity TYPE datetime DEFAULT time::now();
41        DEFINE FIELD IF NOT EXISTS updated_at   ON entity TYPE datetime DEFAULT time::now();
42        DEFINE FIELD IF NOT EXISTS source       ON entity TYPE option<string>;
43
44        DEFINE INDEX IF NOT EXISTS entity_name   ON entity FIELDS name;
45        DEFINE INDEX IF NOT EXISTS entity_type   ON entity FIELDS entity_type;
46        DEFINE INDEX IF NOT EXISTS entity_vector ON entity FIELDS embedding HNSW DIMENSION 384 DIST COSINE;
47
48        -- Pipeline attribute indexes
49        DEFINE INDEX IF NOT EXISTS entity_pipeline_stage  ON entity FIELDS attributes.pipeline_stage;
50        DEFINE INDEX IF NOT EXISTS entity_pipeline_status ON entity FIELDS attributes.pipeline_status;
51
52        DEFINE TABLE IF NOT EXISTS relates_to SCHEMAFULL TYPE RELATION;
53        DEFINE FIELD IF NOT EXISTS rel_type    ON relates_to TYPE string;
54        DEFINE FIELD IF NOT EXISTS description ON relates_to TYPE option<string>;
55        DEFINE FIELD IF NOT EXISTS valid_from  ON relates_to TYPE datetime DEFAULT time::now();
56        DEFINE FIELD IF NOT EXISTS valid_until ON relates_to TYPE option<datetime>;
57        DEFINE FIELD IF NOT EXISTS confidence  ON relates_to TYPE float DEFAULT 1.0;
58        DEFINE FIELD IF NOT EXISTS source      ON relates_to TYPE option<string>;
59
60        DEFINE INDEX IF NOT EXISTS rel_type_idx ON relates_to FIELDS rel_type;
61
62        DEFINE TABLE IF NOT EXISTS episode SCHEMAFULL;
63        DEFINE FIELD IF NOT EXISTS session_id  ON episode TYPE string;
64        DEFINE FIELD IF NOT EXISTS timestamp   ON episode TYPE datetime DEFAULT time::now();
65        DEFINE FIELD IF NOT EXISTS abstract    ON episode TYPE string;
66        DEFINE FIELD IF NOT EXISTS overview    ON episode TYPE option<string>;
67        DEFINE FIELD IF NOT EXISTS content     ON episode TYPE option<string>;
68        DEFINE FIELD IF NOT EXISTS embedding   ON episode TYPE option<array<float>>;
69        DEFINE FIELD IF NOT EXISTS log_number  ON episode TYPE option<int>;
70        DEFINE FIELD IF NOT EXISTS extracted  ON episode TYPE bool DEFAULT false;
71
72        -- Backfill: set extracted = false on episodes that predate the field
73        UPDATE episode SET extracted = false WHERE extracted IS NONE;
74
75        DEFINE INDEX IF NOT EXISTS episode_session ON episode FIELDS session_id;
76        DEFINE INDEX IF NOT EXISTS episode_time    ON episode FIELDS timestamp;
77        DEFINE INDEX IF NOT EXISTS episode_vector  ON episode FIELDS embedding HNSW DIMENSION 384 DIST COSINE;
78        "#,
79    )
80    .await?
81    .check()?;
82
83    Ok(())
84}