1use super::types::*;
4use crate::SqliteGraphError;
5use crate::backend::{EdgeSpec, NodeSpec};
6use crate::graph::GraphEntity;
7
8pub fn map_to_graph_error(err: NativeBackendError) -> SqliteGraphError {
10 match err {
11 NativeBackendError::Io(e) => SqliteGraphError::connection(e.to_string()),
12 NativeBackendError::InvalidNodeId { id, max_id } => {
13 SqliteGraphError::query(format!("Invalid node ID: {} (max: {})", id, max_id))
14 }
15 NativeBackendError::InvalidEdgeId { id, max_id } => {
16 SqliteGraphError::query(format!("Invalid edge ID: {} (max: {})", id, max_id))
17 }
18 NativeBackendError::CorruptNodeRecord { node_id, reason } => {
19 SqliteGraphError::connection(format!("Corrupt node record {}: {}", node_id, reason))
20 }
21 NativeBackendError::CorruptEdgeRecord { edge_id, reason } => {
22 SqliteGraphError::connection(format!("Corrupt edge record {}: {}", edge_id, reason))
23 }
24 NativeBackendError::FileTooSmall { size, min_size } => {
25 SqliteGraphError::connection(format!("File too small: {} < {}", size, min_size))
26 }
27 NativeBackendError::RecordTooLarge { size, max_size } => {
28 SqliteGraphError::connection(format!("Record too large: {} > {}", size, max_size))
29 }
30 NativeBackendError::InconsistentAdjacency {
31 node_id,
32 count,
33 direction,
34 file_count,
35 } => SqliteGraphError::connection(format!(
36 "Inconsistent adjacency for node {}: {} {} != {} in file",
37 node_id, direction, count, file_count
38 )),
39 NativeBackendError::InvalidMagic { expected, found } => {
40 SqliteGraphError::connection(format!(
41 "Invalid magic number: expected {:#x}, got {:#x}",
42 expected, found
43 ))
44 }
45 NativeBackendError::UnsupportedVersion {
46 version,
47 supported_version,
48 } => SqliteGraphError::connection(format!(
49 "Unsupported version: {} (supported: {})",
50 version, supported_version
51 )),
52 NativeBackendError::InvalidHeader { field, reason } => {
53 SqliteGraphError::connection(format!("Invalid header field '{}': {}", field, reason))
54 }
55 NativeBackendError::InvalidChecksum { expected, found } => {
56 SqliteGraphError::connection(format!(
57 "Invalid checksum: expected {:#x}, got {:#x}",
58 expected, found
59 ))
60 }
61 NativeBackendError::Utf8Error(e) => SqliteGraphError::connection(e.to_string()),
62 NativeBackendError::JsonError(e) => SqliteGraphError::connection(e.to_string()),
63 NativeBackendError::BincodeError(e) => SqliteGraphError::connection(e.to_string()),
64 NativeBackendError::InvalidUtf8(e) => SqliteGraphError::connection(e.to_string()),
65 NativeBackendError::BufferTooSmall { size, min_size } => {
66 SqliteGraphError::connection(format!("Buffer too small: {} < {}", size, min_size))
67 }
68 NativeBackendError::InvalidStringOffset { offset } => {
69 SqliteGraphError::connection(format!("Invalid string table offset: {}", offset))
70 }
71 NativeBackendError::CorruptStringTable { reason } => {
72 SqliteGraphError::connection(format!("Corrupt string table: {}", reason))
73 }
74 NativeBackendError::InvalidMagicBytes { found } => {
75 SqliteGraphError::connection(format!("Invalid magic bytes: {:?}", found))
76 }
77 NativeBackendError::ValidationFailed {
78 metric,
79 expected,
80 actual,
81 } => SqliteGraphError::connection(format!(
82 "Validation failed for {}: expected {}, got {}",
83 metric, expected, actual
84 )),
85 NativeBackendError::OutOfSpace => {
86 SqliteGraphError::connection("Out of space in file".to_string())
87 }
88 NativeBackendError::CorruptFreeSpace { reason } => {
89 SqliteGraphError::connection(format!("Corrupt free space: {}", reason))
90 }
91 NativeBackendError::TransactionRolledBack(reason) => {
92 SqliteGraphError::connection(format!("Transaction rolled back: {}", reason))
93 }
94 NativeBackendError::NodeNotFound { node_id, operation } => {
95 SqliteGraphError::query(format!("Node {} not found during {}", node_id, operation))
96 }
97 NativeBackendError::InvalidParameter { context, .. } => {
98 SqliteGraphError::query(format!("Invalid parameter: {}", context))
99 }
100 NativeBackendError::InvalidState { context, .. } => {
101 SqliteGraphError::connection(format!("Invalid state: {}", context))
102 }
103 NativeBackendError::CorruptionDetected { context, .. } => {
104 SqliteGraphError::connection(format!("Corruption detected: {}", context))
105 }
106 NativeBackendError::InvalidConfiguration { parameter, reason } => {
107 SqliteGraphError::InvalidInput(format!("Invalid {}: {}", parameter, reason))
108 }
109 NativeBackendError::VersionMismatch {
110 expected, found, ..
111 } => SqliteGraphError::connection(format!(
112 "Version mismatch: expected {}, found {}",
113 expected, found
114 )),
115 NativeBackendError::NodeExists { node_id } => {
117 SqliteGraphError::query(format!("Node {} already exists", node_id))
118 }
119 NativeBackendError::EdgeExists { edge_id } => {
120 SqliteGraphError::query(format!("Edge {} already exists", edge_id))
121 }
122 NativeBackendError::EdgeNotFound { edge_id } => {
123 SqliteGraphError::query(format!("Edge {} not found", edge_id))
124 }
125 NativeBackendError::TransactionNotFound { tx_id } => {
126 SqliteGraphError::connection(format!("Transaction {} not found", tx_id))
127 }
128 NativeBackendError::SavepointNotFound { savepoint_id } => {
129 SqliteGraphError::connection(format!("Savepoint {} not found", savepoint_id))
130 }
131 NativeBackendError::DeadlockDetected { tx_id, .. } => {
132 SqliteGraphError::connection(format!("Deadlock detected for transaction {}", tx_id))
133 }
134 NativeBackendError::InvalidTransaction { tx_id, reason } => {
135 SqliteGraphError::connection(format!("Invalid transaction {}: {}", tx_id, reason))
136 }
137 NativeBackendError::IoError { context, .. } => {
138 SqliteGraphError::connection(format!("I/O error: {}", context))
139 }
140 NativeBackendError::InvalidTransactionState { tx_id, state } => {
141 SqliteGraphError::connection(format!("Invalid transaction {} state: {}", tx_id, state))
142 }
143 NativeBackendError::Recovery(message) => {
144 SqliteGraphError::connection(format!("Recovery error: {}", message))
145 }
146 }
147}
148
149pub fn node_spec_to_record(spec: NodeSpec, node_id: NativeNodeId) -> NodeRecord {
151 NodeRecord::new(node_id, spec.kind, spec.name, spec.data)
152}
153
154pub fn node_record_to_entity(record: NodeRecord) -> GraphEntity {
156 GraphEntity {
157 id: record.id as i64,
158 kind: record.kind,
159 name: record.name,
160 file_path: None, data: record.data,
162 }
163}
164
165pub fn edge_spec_to_record(spec: EdgeSpec, edge_id: NativeEdgeId) -> EdgeRecord {
167 if std::env::var("EDGE_DEBUG").is_ok() {
169 println!(
170 "[EDGE_DEBUG] edge_spec_to_record: from={}, to={}, edge_type={}",
171 spec.from, spec.to, spec.edge_type
172 );
173 }
174
175 EdgeRecord::new(
176 edge_id,
177 spec.from as NativeNodeId,
178 spec.to as NativeNodeId,
179 spec.edge_type,
180 spec.data,
181 )
182}
183
184pub fn validate_node_exists(
186 graph_file: &mut super::graph_file::GraphFile,
187 node_id: NativeNodeId,
188) -> Result<(), NativeBackendError> {
189 let mut node_store = super::node_store::NodeStore::new(graph_file);
190
191 node_store.read_node(node_id)?;
193
194 Ok(())
195}
196
197pub fn validate_edge_exists(
199 graph_file: &mut super::graph_file::GraphFile,
200 edge_id: NativeEdgeId,
201) -> Result<(), NativeBackendError> {
202 let mut edge_store = super::edge_store::EdgeStore::new(graph_file);
203
204 edge_store.read_edge(edge_id)?;
206
207 Ok(())
208}
209
210pub fn validate_node_id_range(
212 graph_file: &super::graph_file::GraphFile,
213 node_id: NativeNodeId,
214) -> Result<(), NativeBackendError> {
215 let header = graph_file.persistent_header();
216
217 if node_id <= 0 {
219 return Err(NativeBackendError::InvalidNodeId {
220 id: node_id,
221 max_id: header.node_count as NativeNodeId,
222 });
223 }
224
225 let max_allowed = std::cmp::max(100_000, header.node_count + 1000);
228 if node_id > max_allowed as NativeNodeId {
229 return Err(NativeBackendError::InvalidNodeId {
230 id: node_id,
231 max_id: max_allowed as NativeNodeId,
232 });
233 }
234
235 Ok(())
236}
237
238pub fn validate_edge_id_range(
240 graph_file: &super::graph_file::GraphFile,
241 edge_id: NativeEdgeId,
242) -> Result<(), NativeBackendError> {
243 let header = graph_file.persistent_header();
244
245 if edge_id <= 0 || edge_id > header.edge_count as NativeEdgeId {
246 return Err(NativeBackendError::InvalidEdgeId {
247 id: edge_id,
248 max_id: header.edge_count as NativeEdgeId,
249 });
250 }
251
252 Ok(())
253}
254
255pub fn check_file_consistency(
257 graph_file: &super::graph_file::GraphFile,
258) -> Result<(), NativeBackendError> {
259 let header = graph_file.persistent_header();
260
261 if header.node_count < 0 || header.edge_count < 0 {
263 return Err(NativeBackendError::CorruptNodeRecord {
264 node_id: 0,
265 reason: "Negative counts in header".to_string(),
266 });
267 }
268
269 if header.node_count > 1_000_000 || header.edge_count > 10_000_000 {
271 return Err(NativeBackendError::CorruptNodeRecord {
272 node_id: 0,
273 reason: "Counts exceed reasonable limits".to_string(),
274 });
275 }
276
277 Ok(())
278}
279
280#[cfg(test)]
281mod tests {
282 use super::super::graph_file::GraphFile;
283 use super::*;
284 use tempfile::NamedTempFile;
285
286 #[test]
287 fn test_error_mapping() {
288 let node_error = NativeBackendError::InvalidNodeId { id: 0, max_id: 10 };
289 let mapped = map_to_graph_error(node_error);
290
291 match mapped {
292 SqliteGraphError::QueryError(msg) => {
293 assert!(msg.contains("Invalid node ID"));
294 assert!(msg.contains("0"));
295 assert!(msg.contains("10"));
296 }
297 _ => panic!("Expected QueryError"),
298 }
299 }
300
301 #[test]
302 fn test_node_spec_to_record() {
303 let spec = NodeSpec {
304 kind: "Test".to_string(),
305 name: "test_node".to_string(),
306 file_path: Some("/path/to/file".to_string()),
307 data: serde_json::json!({"key": "value"}),
308 };
309
310 let record = node_spec_to_record(spec, 5);
311 assert_eq!(record.id, 5);
312 assert_eq!(record.kind, "Test");
313 assert_eq!(record.name, "test_node");
314 assert_eq!(record.data, serde_json::json!({"key": "value"}));
315 }
316
317 #[test]
318 fn test_node_record_to_entity() {
319 let record = NodeRecord::new(
320 42,
321 "Test".to_string(),
322 "test_node".to_string(),
323 serde_json::json!({"key": "value"}),
324 );
325
326 let entity = node_record_to_entity(record);
327 assert_eq!(entity.id, 42);
328 assert_eq!(entity.kind, "Test");
329 assert_eq!(entity.name, "test_node");
330 assert_eq!(entity.data, serde_json::json!({"key": "value"}));
331 }
332
333 #[test]
334 fn test_validate_node_id_range() {
335 let temp_file = NamedTempFile::new().unwrap();
336 let path = temp_file.path();
337 let graph_file = GraphFile::create(path).unwrap();
338
339 assert!(validate_node_id_range(&graph_file, 1).is_ok());
341
342 assert!(validate_node_id_range(&graph_file, 0).is_err());
344 assert!(validate_node_id_range(&graph_file, -1).is_err());
345 assert!(validate_node_id_range(&graph_file, 1000000).is_err());
346 }
347
348 #[test]
349 fn test_check_file_consistency() {
350 let temp_file = NamedTempFile::new().unwrap();
351 let path = temp_file.path();
352 let graph_file = GraphFile::create(path).unwrap();
353
354 assert!(check_file_consistency(&graph_file).is_ok());
356 }
357}