forgekit_core/storage/
mod.rs1mod ops;
33mod store;
34#[cfg(test)]
35mod tests;
36
37pub use sqlitegraph::backend::{EdgeSpec, NodeSpec};
38pub use sqlitegraph::config::{open_graph, BackendKind as SqliteGraphBackendKind, GraphConfig};
39pub use sqlitegraph::graph::{GraphEntity, SqliteGraph};
40
41pub use store::UnifiedGraphStore;
42
43use std::path::{Path, PathBuf};
44
45pub fn default_db_path(project_root: &Path) -> PathBuf {
55 if let Some(db) = lookup_registry(project_root) {
56 return db;
57 }
58
59 fallback_db_path(project_root)
60}
61
62fn fallback_db_path(project_root: &Path) -> PathBuf {
63 let stem = project_root
64 .file_name()
65 .and_then(|n| n.to_str())
66 .unwrap_or("graph");
67 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
68 PathBuf::from(home)
69 .join(".magellan")
70 .join(stem)
71 .join(format!("{}.db", stem))
72}
73
74fn lookup_registry(project_root: &Path) -> Option<PathBuf> {
75 let home = std::env::var("HOME").ok()?;
76 let registry_path = PathBuf::from(&home)
77 .join(".config")
78 .join("magellan")
79 .join("registry.toml");
80
81 let content = std::fs::read_to_string(®istry_path).ok()?;
82
83 let canonical_root = project_root
84 .canonicalize()
85 .ok()
86 .unwrap_or_else(|| project_root.to_path_buf());
87
88 for block in content.split("[[project]]") {
89 let mut name = None;
90 let mut root = None;
91 let mut db = None;
92
93 for line in block.lines() {
94 let trimmed = line.trim();
95 if let Some(rest) = trimmed.strip_prefix("name = ") {
96 name = parse_toml_string(rest);
97 } else if let Some(rest) = trimmed.strip_prefix("root = ") {
98 root = parse_toml_string(rest);
99 } else if let Some(rest) = trimmed.strip_prefix("db = ") {
100 db = parse_toml_string(rest);
101 }
102 }
103
104 if let (Some(proj_root), Some(proj_db)) = (root, db) {
105 let proj_root_path = Path::new(&proj_root);
106 if canonical_root.starts_with(proj_root_path)
107 || proj_root_path.starts_with(&canonical_root)
108 || paths_equal_after_src_strip(&canonical_root, proj_root_path)
109 {
110 return Some(PathBuf::from(proj_db));
111 }
112 }
113
114 let _ = name;
115 }
116
117 None
118}
119
120fn paths_equal_after_src_strip(a: &Path, b: &Path) -> bool {
121 let a_str = a.to_string_lossy();
122 let b_str = b.to_string_lossy();
123
124 if let Some(a_stripped) = a_str.strip_suffix("/src") {
125 if a_stripped == b_str {
126 return true;
127 }
128 }
129 if let Some(b_stripped) = b_str.strip_suffix("/src") {
130 if b_stripped == a_str {
131 return true;
132 }
133 }
134 false
135}
136
137fn parse_toml_string(s: &str) -> Option<String> {
138 s.trim()
139 .strip_prefix('"')
140 .and_then(|s| s.strip_suffix('"'))
141 .map(|s| s.to_string())
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
145pub enum BackendKind {
146 #[default]
147 SQLite,
148 NativeV3,
149}
150
151impl std::fmt::Display for BackendKind {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 match self {
154 Self::SQLite => write!(f, "SQLite"),
155 Self::NativeV3 => write!(f, "NativeV3"),
156 }
157 }
158}
159
160impl BackendKind {
161 #[cfg(test)]
162 fn to_sqlitegraph_kind(self) -> SqliteGraphBackendKind {
163 match self {
164 Self::SQLite => SqliteGraphBackendKind::SQLite,
165 Self::NativeV3 => SqliteGraphBackendKind::Native,
166 }
167 }
168
169 pub fn file_extension(&self) -> &str {
170 match self {
171 Self::SQLite => "db",
172 Self::NativeV3 => "v3",
173 }
174 }
175
176 pub fn default_filename(&self) -> &str {
177 match self {
178 Self::SQLite => "graph.db",
179 Self::NativeV3 => "graph.v3",
180 }
181 }
182}