Process edges: look up target node, filter by label/props, add binding. * `inbound` controls which end of the edge is the target id. */
| 3119 | /* Process edges: look up target node, filter by label/props, add binding. |
| 3120 | * `inbound` controls which end of the edge is the target id. */ |
| 3121 | static void process_edges(cbm_store_t *store, cbm_edge_t *edges, int edge_count, bool inbound, |
| 3122 | const cbm_node_pattern_t *target_node, binding_t *b, const char *to_var, |
| 3123 | const char *rel_var, binding_t *new_bindings, int *new_count, int max_new, |
| 3124 | int *match_count) { |
| 3125 | /* When the terminal node variable is ALREADY bound (e.g. the second pattern |
| 3126 | * `(c)-[:CALLS]->(f)` where `f` came from an earlier MATCH), we must FILTER |
| 3127 | * to edges that actually reach the bound node — not overwrite the caller's |
| 3128 | * `f` binding with whatever node the edge leads to. Overwriting corrupted |
| 3129 | * the result of dead-code queries and produced wrong rows (#627). */ |
| 3130 | cbm_node_t *bound_to = binding_get(b, to_var); |
| 3131 | int64_t bound_to_id = bound_to ? bound_to->id : 0; |
| 3132 | /* The budget caps MATERIALISATION, not detection: `match_count` must stay |
| 3133 | * truthful even after `new_count` hits `max_new`. Gating the loop itself |
| 3134 | * (`ei < edge_count && *new_count < max_new`) stopped fetching entirely, so |
| 3135 | * a saturated source reported `match_count == 0` despite having neighbours — |
| 3136 | * and the OPTIONAL fallback in expand_pattern_rels then emitted an unbound |
| 3137 | * "no match" row for it. `WHERE x IS NULL` reads those rows as "nothing |
| 3138 | * points here", i.e. it reported live code as dead. Losing rows is |
| 3139 | * recoverable; asserting a match does not exist when it does is not. */ |
| 3140 | for (int ei = 0; ei < edge_count; ei++) { |
| 3141 | int64_t tid = inbound ? edges[ei].source_id : edges[ei].target_id; |
| 3142 | if (bound_to && tid != bound_to_id) { |
| 3143 | continue; /* edge does not reach the already-bound terminal node */ |
| 3144 | } |
| 3145 | cbm_node_t found = {0}; |
| 3146 | if (cbm_store_find_node_by_id(store, tid, &found) != CBM_STORE_OK) { |
| 3147 | continue; |
| 3148 | } |
| 3149 | if (target_node->label && !label_alt_matches(found.label, target_node->label)) { |
| 3150 | node_fields_free(&found); |
| 3151 | continue; |
| 3152 | } |
| 3153 | if (!check_inline_props(&found, target_node->props, target_node->prop_count, store)) { |
| 3154 | node_fields_free(&found); |
| 3155 | continue; |
| 3156 | } |
| 3157 | (*match_count)++; /* a real neighbour exists, budget or not */ |
| 3158 | if (*new_count < max_new) { |
| 3159 | binding_t nb = {0}; |
| 3160 | binding_copy(&nb, b); |
| 3161 | binding_set(&nb, to_var, &found); |
| 3162 | if (rel_var) { |
| 3163 | binding_set_edge(&nb, rel_var, &edges[ei]); |
| 3164 | } |
| 3165 | new_bindings[(*new_count)++] = nb; |
| 3166 | } |
| 3167 | node_fields_free(&found); |
| 3168 | } |
| 3169 | } |
| 3170 | |
| 3171 | /* Expand variable-length relationship via BFS */ |
| 3172 | /* Set when a variable-length hop range is clamped to the engine ceiling |
no test coverage detected