sqlitegraph/backend/native/
graph_validation.rs

1//! Validation and error mapping utilities for native graph backend.
2
3use super::types::*;
4use crate::SqliteGraphError;
5use crate::backend::{EdgeSpec, NodeSpec};
6use crate::graph::GraphEntity;
7
8/// Error mapping from NativeBackendError to SqliteGraphError
9pub 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        // New V2 WAL error variants
116        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
149/// Convert NodeSpec to NodeRecord for storage
150pub fn node_spec_to_record(spec: NodeSpec, node_id: NativeNodeId) -> NodeRecord {
151    NodeRecord::new(node_id, spec.kind, spec.name, spec.data)
152}
153
154/// Convert NodeRecord from storage to GraphEntity
155pub 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, // Native backend doesn't store file_path
161        data: record.data,
162    }
163}
164
165/// Convert EdgeSpec to EdgeRecord for storage
166pub fn edge_spec_to_record(spec: EdgeSpec, edge_id: NativeEdgeId) -> EdgeRecord {
167    // DEBUG: Print what EdgeSpec contains before conversion to EdgeRecord
168    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
184/// Validate node exists and is accessible
185pub 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    // Try to read the node - this will return an error if node doesn't exist
192    node_store.read_node(node_id)?;
193
194    Ok(())
195}
196
197/// Validate edge exists and is accessible
198pub 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    // Try to read the edge - this will return an error if edge doesn't exist
205    edge_store.read_edge(edge_id)?;
206
207    Ok(())
208}
209
210/// Validate node ID is in valid range
211pub 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    // Check lower bound (must be positive)
218    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    // For upper bound, allow both existing nodes and reasonable future allocation
226    // Allow up to 100,000 OR current node count + space for 1000 more nodes
227    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
238/// Validate edge ID is in valid range
239pub 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
255/// Check if file operations are in a consistent state
256pub fn check_file_consistency(
257    graph_file: &super::graph_file::GraphFile,
258) -> Result<(), NativeBackendError> {
259    let header = graph_file.persistent_header();
260
261    // Basic header validation
262    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    // Check for reasonable limits
270    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        // Valid node ID should pass (even though node doesn't exist yet)
340        assert!(validate_node_id_range(&graph_file, 1).is_ok());
341
342        // Invalid node IDs should fail
343        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        // Fresh file should be consistent
355        assert!(check_file_consistency(&graph_file).is_ok());
356    }
357}