Inserts a batch of edges inside a single transaction. Edges whose source or target node does not yet exist are silently skipped (#58). They will be picked up on a future sync once the referenced file is indexed. `Contains` edges are denormalized into `nodes.parent_id` via UPDATE; they do not produce edge rows.
(&self, edges: &[Edge])
| 53 | /// referenced file is indexed. `Contains` edges are denormalized into |
| 54 | /// `nodes.parent_id` via UPDATE; they do not produce edge rows. |
| 55 | pub async fn insert_edges(&self, edges: &[Edge]) -> Result<()> { |
| 56 | if edges.is_empty() { |
| 57 | return Ok(()); |
| 58 | } |
| 59 | |
| 60 | self.with_batch_transaction("insert_edges", async { |
| 61 | // Conditional INSERT: only insert when both endpoints exist in |
| 62 | // `nodes`. This avoids FK violations during incremental sync |
| 63 | // when an edge references a node from a not-yet-indexed file. |
| 64 | let stmt = self |
| 65 | .conn() |
| 66 | .prepare( |
| 67 | "INSERT OR IGNORE INTO edges (source, target, kind, line) \ |
| 68 | SELECT ?1, ?2, ?3, ?4 \ |
| 69 | WHERE EXISTS (SELECT 1 FROM nodes WHERE id = ?1) \ |
| 70 | AND EXISTS (SELECT 1 FROM nodes WHERE id = ?2)", |
| 71 | ) |
| 72 | .await |
| 73 | .map_err(|e| TraceDecayError::Database { |
| 74 | message: format!("failed to prepare: {e}"), |
| 75 | operation: "insert_edges".to_string(), |
| 76 | })?; |
| 77 | |
| 78 | let parent_stmt = self |
| 79 | .conn() |
| 80 | .prepare("UPDATE nodes SET parent_id = ?1 WHERE id = ?2") |
| 81 | .await |
| 82 | .map_err(|e| TraceDecayError::Database { |
| 83 | message: format!("failed to prepare parent update: {e}"), |
| 84 | operation: "insert_edges".to_string(), |
| 85 | })?; |
| 86 | |
| 87 | for edge in edges { |
| 88 | if edge.kind == EdgeKind::Contains { |
| 89 | if let Err(e) = parent_stmt |
| 90 | .execute(params![edge.source.as_str(), edge.target.as_str()]) |
| 91 | .await |
| 92 | { |
| 93 | parent_stmt.reset(); |
| 94 | return Err(TraceDecayError::Database { |
| 95 | message: format!("failed to set parent_id: {e}"), |
| 96 | operation: "insert_edges".to_string(), |
| 97 | }); |
| 98 | } |
| 99 | parent_stmt.reset(); |
| 100 | continue; |
| 101 | } |
| 102 | if let Err(e) = stmt |
| 103 | .execute(params![ |
| 104 | edge.source.as_str(), |
| 105 | edge.target.as_str(), |
| 106 | edge.kind.as_str(), |
| 107 | edge.line.map(i64::from), |
| 108 | ]) |
| 109 | .await |
| 110 | { |
| 111 | stmt.reset(); |
| 112 | return Err(TraceDecayError::Database { |