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