Multi-source BFS: one recursive CTE anchored on ALL seeds (via a temp * table — never a per-seed loop, which re-walks overlapping hub subgraphs * seed_count times). Semantics for impact analysis: * - shortest-path MIN(hop) across the whole seed set; * - SEEDS ARE EXCLUDED from the result: a seed reached from another seed * (changed files call each other constantly) is not "impact"; *
| 4684 | * - canonical (hop, id) order. |
| 4685 | * No root node and no edge collection — impact wants the reached set. */ |
| 4686 | int cbm_store_bfs_multi(cbm_store_t *s, const int64_t *seed_ids, int seed_count, |
| 4687 | const char *direction, const char **edge_types, int edge_type_count, |
| 4688 | int max_depth, int max_results, cbm_traverse_result_t *out, |
| 4689 | bool *truncated) { |
| 4690 | memset(out, 0, sizeof(*out)); |
| 4691 | if (truncated) { |
| 4692 | *truncated = false; |
| 4693 | } |
| 4694 | if (!s || !s->db || !seed_ids || seed_count <= 0) { |
| 4695 | return CBM_STORE_ERR; |
| 4696 | } |
| 4697 | |
| 4698 | if (sqlite3_exec(s->db, |
| 4699 | "CREATE TEMP TABLE IF NOT EXISTS bfs_seeds (id INTEGER PRIMARY KEY);" |
| 4700 | "DELETE FROM bfs_seeds;", |
| 4701 | NULL, NULL, NULL) != SQLITE_OK) { |
| 4702 | store_set_error_sqlite(s, "bfs_multi seeds"); |
| 4703 | return CBM_STORE_ERR; |
| 4704 | } |
| 4705 | sqlite3_stmt *ins = NULL; |
| 4706 | if (sqlite3_prepare_v2(s->db, "INSERT OR IGNORE INTO bfs_seeds(id) VALUES (?1)", CBM_NOT_FOUND, |
| 4707 | &ins, NULL) != SQLITE_OK) { |
| 4708 | store_set_error_sqlite(s, "bfs_multi seed insert"); |
| 4709 | return CBM_STORE_ERR; |
| 4710 | } |
| 4711 | for (int i = 0; i < seed_count; i++) { |
| 4712 | sqlite3_reset(ins); |
| 4713 | sqlite3_bind_int64(ins, SKIP_ONE, seed_ids[i]); |
| 4714 | (void)sqlite3_step(ins); |
| 4715 | } |
| 4716 | sqlite3_finalize(ins); |
| 4717 | |
| 4718 | char types_clause[CBM_SZ_512]; |
| 4719 | bfs_build_types_clause(edge_type_count, types_clause, (int)sizeof(types_clause)); |
| 4720 | |
| 4721 | const char *join_cond; |
| 4722 | const char *next_id; |
| 4723 | bool is_inbound = (direction != NULL) && (strcmp(direction, "inbound") == 0); |
| 4724 | if (is_inbound) { |
| 4725 | join_cond = "e.target_id = bfs.node_id"; |
| 4726 | next_id = "e.source_id"; |
| 4727 | } else { |
| 4728 | join_cond = "e.source_id = bfs.node_id"; |
| 4729 | next_id = "e.target_id"; |
| 4730 | } |
| 4731 | |
| 4732 | char sql[CBM_SZ_4K]; |
| 4733 | snprintf(sql, sizeof(sql), |
| 4734 | "WITH RECURSIVE bfs(node_id, hop) AS (" |
| 4735 | " SELECT id, 0 FROM bfs_seeds" |
| 4736 | " UNION" |
| 4737 | " SELECT %s, bfs.hop + 1" |
| 4738 | " FROM bfs" |
| 4739 | " JOIN edges e ON %s" |
| 4740 | " WHERE e.type IN (%s) AND bfs.hop < %d" |
| 4741 | ")" |
| 4742 | "SELECT n.id, n.project, n.label, n.name, n.qualified_name, " |
| 4743 | "n.file_path, n.start_line, n.end_line, n.properties, MIN(bfs.hop) AS hop " |
no test coverage detected