Skip to content

Commit 6e577c8

Browse files
fix(cross-repo): normalize URLs and match templated routes for cross-repo edges (#523)
cross-repo-intelligence returned 0 HTTP edges because match_http_routes could not link a consumer's HTTP_CALLS to a provider's Route: - Consumer url_path carried a full URL (scheme+host+port) while the provider Route is a bare path. cr_url_path() strips scheme+authority before the route-QN lookup. main's cbm_route_canon_path normalizes placeholder syntax but not scheme/host. - Concrete paths (/v2/orders/123) never matched templated routes (/v2/orders/{}). cr_path_matches_template() + find_route_handler_fuzzy() do segment-wise matching as a fallback when the exact QN lookup misses. - match_http_routes only searched HTTP_CALLS in the src project; a provider-initiated run has no outbound calls. Added the reverse direction so both orientations are covered. - insert_cross_edge / emit_cross_route_bidirectional are idempotent and return whether a row was inserted; match loops count real inserts, so a pair matched from both directions doesn't duplicate or inflate counts. The external-client HTTP_CALLS emission originally in this PR is dropped: main's beaa776 landed a broader variant (covers registry mis-resolution and both pipeline paths) that supersedes it. Signed-off-by: RithvikReddy0-0 <rithvikreddymukkara@gmail.com>
1 parent 09148ab commit 6e577c8

1 file changed

Lines changed: 177 additions & 27 deletions

File tree

src/pipeline/pass_cross_repo.c

Lines changed: 177 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,25 @@ static void delete_cross_edges(cbm_store_t *store, const char *project) {
115115
cbm_store_delete_edges_by_type(store, project, "CROSS_TRPC_CALLS");
116116
}
117117

118-
/* Insert a CROSS_* edge into a store. */
119-
static void insert_cross_edge(cbm_store_t *store, const char *project, int64_t from_id,
120-
int64_t to_id, const char *edge_type, const char *props) {
118+
/* Insert a CROSS_* edge, skipping if an identical (source, target, type) edge
119+
* already exists. The pass can reach the same caller/route pair from both
120+
* directions and emit_cross_route_bidirectional writes both DBs, so this guard
121+
* keeps emission idempotent. Returns 1 if a new row was inserted, else 0, so
122+
* callers count rows actually added rather than matches found. Issue #523. */
123+
static int insert_cross_edge(cbm_store_t *store, const char *project, int64_t from_id,
124+
int64_t to_id, const char *edge_type, const char *props) {
125+
cbm_edge_t *existing = NULL;
126+
int existing_count = 0;
127+
if (cbm_store_find_edges_by_source_type(store, from_id, edge_type, &existing,
128+
&existing_count) == 0) {
129+
for (int i = 0; i < existing_count; i++) {
130+
if (existing[i].target_id == to_id) {
131+
cbm_store_free_edges(existing, existing_count);
132+
return 0; /* already present - do not duplicate */
133+
}
134+
}
135+
cbm_store_free_edges(existing, existing_count);
136+
}
121137
cbm_edge_t edge = {
122138
.project = project,
123139
.source_id = from_id,
@@ -126,6 +142,22 @@ static void insert_cross_edge(cbm_store_t *store, const char *project, int64_t f
126142
.properties_json = props,
127143
};
128144
cbm_store_insert_edge(store, &edge);
145+
return 1;
146+
}
147+
148+
/* Strip scheme + authority from a URL, returning a pointer to the path.
149+
* cbm_route_canon_path handles placeholder syntax but not scheme/host, so a
150+
* consumer's full URL needs this before the route-QN lookup. Issue #523. */
151+
static const char *cr_url_path(const char *url) {
152+
if (!url) {
153+
return url;
154+
}
155+
const char *scheme_end = strstr(url, "://");
156+
if (!scheme_end) {
157+
return url;
158+
}
159+
const char *path_start = strchr(scheme_end + 3, '/');
160+
return path_start ? path_start : url;
129161
}
130162

131163
/* Look up a node's name and file_path by id. */
@@ -208,29 +240,135 @@ static int64_t find_route_handler(cbm_store_t *target_store, const char *route_q
208240
return handler_id;
209241
}
210242

243+
/* Segment-wise match of a concrete path against a route template path, where a
244+
* "{...}" segment in the template matches any single concrete segment. Both
245+
* inputs are bare paths (no method prefix). Returns true on a full match. */
246+
static bool cr_path_matches_template(const char *concrete, const char *templ) {
247+
const char *c = concrete;
248+
const char *t = templ;
249+
while (*c && *t) {
250+
if (*c == '/') {
251+
c++;
252+
}
253+
if (*t == '/') {
254+
t++;
255+
}
256+
const char *cseg = c;
257+
while (*c && *c != '/') {
258+
c++;
259+
}
260+
const char *tseg = t;
261+
while (*t && *t != '/') {
262+
t++;
263+
}
264+
size_t clen = (size_t)(c - cseg);
265+
size_t tlen = (size_t)(t - tseg);
266+
bool t_is_param = (tlen >= 2 && tseg[0] == '{' && tseg[tlen - 1] == '}');
267+
if (!t_is_param) {
268+
if (clen != tlen || strncmp(cseg, tseg, clen) != 0) {
269+
return false;
270+
}
271+
} else if (clen == 0) {
272+
return false;
273+
}
274+
}
275+
while (*c == '/') {
276+
c++;
277+
}
278+
while (*t == '/') {
279+
t++;
280+
}
281+
return *c == '\0' && *t == '\0';
282+
}
283+
284+
/* Fallback for when an exact route-QN lookup misses: a concrete client path
285+
* ("/v2/orders/123") will not exact-match a templated route QN
286+
* ("__route__GET__/v2/orders/{}"). Enumerate the target's Route nodes and
287+
* segment-match the concrete path against each template. On a match, copy the
288+
* route QN into out_qn and return the handler id. Returns 0 on no match. #523 */
289+
static int64_t find_route_handler_fuzzy(cbm_store_t *target_store, const char *concrete_path,
290+
const char *method, char *out_qn, size_t out_qn_sz,
291+
char *handler_name, size_t name_sz, char *handler_file,
292+
size_t file_sz) {
293+
struct sqlite3 *db = cbm_store_get_db(target_store);
294+
if (!db) {
295+
return 0;
296+
}
297+
sqlite3_stmt *s = NULL;
298+
if (sqlite3_prepare_v2(
299+
db, "SELECT qualified_name FROM nodes WHERE label = 'Route' AND qualified_name LIKE ?1",
300+
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
301+
return 0;
302+
}
303+
sqlite3_bind_text(s, SKIP_ONE, "__route__%", CBM_NOT_FOUND, SQLITE_STATIC);
304+
int64_t found = 0;
305+
char matched_qn[CR_QN_BUF] = {0};
306+
while (sqlite3_step(s) == SQLITE_ROW) {
307+
const char *qn = (const char *)sqlite3_column_text(s, 0);
308+
if (!qn || strncmp(qn, "__route__", 9) != 0) {
309+
continue;
310+
}
311+
const char *rest = qn + 9;
312+
const char *sep = strstr(rest, "__");
313+
if (!sep) {
314+
continue;
315+
}
316+
size_t mlen = (size_t)(sep - rest);
317+
const char *rpath = sep + 2;
318+
bool method_ok = true;
319+
if (method && method[0]) {
320+
method_ok = (strncmp(rest, method, mlen) == 0 && method[mlen] == '\0') ||
321+
(mlen == 3 && strncmp(rest, "ANY", 3) == 0);
322+
}
323+
if (!method_ok) {
324+
continue;
325+
}
326+
if (cr_path_matches_template(concrete_path, rpath)) {
327+
/* A path can match more than one stored template (e.g. both the
328+
* raw "{id}" route and its canonical "{}" form). Only accept a
329+
* match whose Route node actually has a HANDLES edge — the handler
330+
* is attached to the canonical node. Keep scanning otherwise. */
331+
int64_t hid =
332+
find_route_handler(target_store, qn, handler_name, name_sz, handler_file, file_sz);
333+
if (hid != 0) {
334+
snprintf(matched_qn, sizeof(matched_qn), "%s", qn);
335+
found = hid;
336+
break;
337+
}
338+
}
339+
}
340+
sqlite3_finalize(s);
341+
if (!found) {
342+
return 0;
343+
}
344+
snprintf(out_qn, out_qn_sz, "%s", matched_qn);
345+
return found;
346+
}
347+
211348
/* Emit CROSS_* edge for a route match: forward into source, reverse into target. */
212-
static void emit_cross_route_bidirectional(cbm_store_t *src_store, const char *src_project,
213-
struct sqlite3 *src_db, int64_t caller_id,
214-
int64_t local_route_id, cbm_store_t *tgt_store,
215-
const char *tgt_project, int64_t handler_id,
216-
const char *route_qn, const char *handler_name,
217-
const char *handler_file, const char *url_path,
218-
const char *method, const char *edge_type) {
349+
static int emit_cross_route_bidirectional(cbm_store_t *src_store, const char *src_project,
350+
struct sqlite3 *src_db, int64_t caller_id,
351+
int64_t local_route_id, cbm_store_t *tgt_store,
352+
const char *tgt_project, int64_t handler_id,
353+
const char *route_qn, const char *handler_name,
354+
const char *handler_file, const char *url_path,
355+
const char *method, const char *edge_type) {
219356
/* Forward: caller → local Route in source DB */
220357
char fwd[CR_PROPS_BUF];
221358
build_cross_props(fwd, sizeof(fwd), tgt_project, handler_name, handler_file, url_path,
222359
"url_path", method);
223-
insert_cross_edge(src_store, src_project, caller_id, local_route_id, edge_type, fwd);
360+
int inserted =
361+
insert_cross_edge(src_store, src_project, caller_id, local_route_id, edge_type, fwd);
224362

225363
/* Reverse: handler → Route in target DB */
226364
struct sqlite3 *tgt_db = cbm_store_get_db(tgt_store);
227365
if (!tgt_db) {
228-
return;
366+
return inserted;
229367
}
230368
sqlite3_stmt *rq = NULL;
231369
if (sqlite3_prepare_v2(tgt_db, "SELECT id FROM nodes WHERE qualified_name = ?1 LIMIT 1",
232370
CBM_NOT_FOUND, &rq, NULL) != SQLITE_OK) {
233-
return;
371+
return inserted;
234372
}
235373
sqlite3_bind_text(rq, SKIP_ONE, route_qn, CBM_NOT_FOUND, SQLITE_STATIC);
236374
int64_t tgt_route_id = 0;
@@ -239,7 +377,7 @@ static void emit_cross_route_bidirectional(cbm_store_t *src_store, const char *s
239377
}
240378
sqlite3_finalize(rq);
241379
if (tgt_route_id == 0) {
242-
return;
380+
return inserted;
243381
}
244382

245383
char caller_name[CBM_SZ_256] = {0};
@@ -251,6 +389,7 @@ static void emit_cross_route_bidirectional(cbm_store_t *src_store, const char *s
251389
build_cross_props(rev, sizeof(rev), src_project, caller_name, caller_file, url_path, "url_path",
252390
method);
253391
insert_cross_edge(tgt_store, tgt_project, handler_id, tgt_route_id, edge_type, rev);
392+
return inserted;
254393
}
255394

256395
static int match_http_routes(cbm_store_t *src_store, const char *src_project,
@@ -289,7 +428,7 @@ static int match_http_routes(cbm_store_t *src_store, const char *src_project,
289428
* placeholder syntax). */
290429
char route_qn[CR_QN_BUF];
291430
char cpath[CBM_SZ_256];
292-
const char *curl = cbm_route_canon_path(url_path, cpath, sizeof(cpath));
431+
const char *curl = cbm_route_canon_path(cr_url_path(url_path), cpath, sizeof(cpath));
293432
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method[0] ? method : "ANY", curl);
294433

295434
char handler_name[CBM_SZ_256] = {0};
@@ -303,15 +442,22 @@ static int match_http_routes(cbm_store_t *src_store, const char *src_project,
303442
handler_id = find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
304443
handler_file, sizeof(handler_file));
305444
}
445+
if (handler_id == 0) {
446+
/* Exact QN lookup missed. A concrete client path ("/v2/orders/123")
447+
* does not exact-match a templated route ("/v2/orders/{}"), so fall
448+
* back to segment-wise template matching. Issue #523. */
449+
handler_id = find_route_handler_fuzzy(
450+
tgt_store, cr_url_path(url_path), method[0] ? method : NULL, route_qn,
451+
sizeof(route_qn), handler_name, sizeof(handler_name), handler_file,
452+
sizeof(handler_file));
453+
}
306454
if (handler_id == 0) {
307455
continue;
308456
}
309457

310-
emit_cross_route_bidirectional(src_store, src_project, src_db, caller_id, route_id,
311-
tgt_store, tgt_project, handler_id, route_qn, handler_name,
312-
handler_file, url_path, method, "CROSS_HTTP_CALLS");
313-
314-
count++;
458+
count += emit_cross_route_bidirectional(
459+
src_store, src_project, src_db, caller_id, route_id, tgt_store, tgt_project, handler_id,
460+
route_qn, handler_name, handler_file, url_path, method, "CROSS_HTTP_CALLS");
315461
}
316462
sqlite3_finalize(s);
317463
return count;
@@ -365,9 +511,8 @@ static int match_async_routes(cbm_store_t *src_store, const char *src_project,
365511
char edge_props[CR_PROPS_BUF];
366512
build_cross_props(edge_props, sizeof(edge_props), tgt_project, handler_name, handler_file,
367513
url_path, "url_path", broker);
368-
insert_cross_edge(src_store, src_project, caller_id, route_id, "CROSS_ASYNC_CALLS",
369-
edge_props);
370-
count++;
514+
count += insert_cross_edge(src_store, src_project, caller_id, route_id, "CROSS_ASYNC_CALLS",
515+
edge_props);
371516
}
372517
sqlite3_finalize(s);
373518
return count;
@@ -543,10 +688,9 @@ static int match_typed_routes(cbm_store_t *src_store, const char *src_project,
543688
continue;
544689
}
545690

546-
emit_cross_route_bidirectional(src_store, src_project, src_db, caller_id, route_id,
547-
tgt_store, tgt_project, handler_id, route_qn, handler_name,
548-
handler_file, svc_val, svc_key, cross_edge_type);
549-
count++;
691+
count += emit_cross_route_bidirectional(
692+
src_store, src_project, src_db, caller_id, route_id, tgt_store, tgt_project, handler_id,
693+
route_qn, handler_name, handler_file, svc_val, svc_key, cross_edge_type);
550694
}
551695
sqlite3_finalize(s);
552696
return count;
@@ -655,6 +799,12 @@ cbm_cross_repo_result_t cbm_cross_repo_match(const char *project, const char **t
655799
}
656800

657801
result.http_edges += match_http_routes(src_store, project, tgt_store, tgt);
802+
/* Reverse direction: when this pass is run from the provider side, the
803+
* consumer's HTTP_CALLS live in tgt, not src — the forward pass above
804+
* finds nothing because the provider has no outbound calls. The dedup
805+
* guard in insert_cross_edge keeps a pair matched both ways from
806+
* inflating the count or duplicating rows. Issue #523. */
807+
result.http_edges += match_http_routes(tgt_store, tgt, src_store, project);
658808
result.async_edges += match_async_routes(src_store, project, tgt_store, tgt);
659809
result.channel_edges += match_channels(src_store, project, tgt_store, tgt);
660810
result.grpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "GRPC_CALLS",

0 commit comments

Comments
 (0)