| 3289 | } |
| 3290 | |
| 3291 | static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_t **bindings, |
| 3292 | int *bind_count, int *bind_cap, const char **var_name, |
| 3293 | bool is_optional) { |
| 3294 | for (int ri = 0; ri < pat->rel_count; ri++) { |
| 3295 | /* #601: stop expanding further hops once the wall-clock budget is spent |
| 3296 | * (an unbounded expansion is exactly what blows up here). */ |
| 3297 | if (cypher_deadline_exceeded()) { |
| 3298 | return; |
| 3299 | } |
| 3300 | cbm_rel_pattern_t *rel = &pat->rels[ri]; |
| 3301 | cbm_node_pattern_t *target_node = &pat->nodes[ri + SKIP_ONE]; |
| 3302 | const char *to_var = target_node->variable ? target_node->variable : "_n_t"; |
| 3303 | |
| 3304 | bool is_variable_length = (rel->min_hops != SKIP_ONE || rel->max_hops != SKIP_ONE); |
| 3305 | |
| 3306 | /* #1196: the hop's output buffer GROWS to hold every matched row — |
| 3307 | * the old bind_cap*10 ceiling silently dropped edges before WHERE and |
| 3308 | * aggregation, falsifying counts. binding_out_append handles growth |
| 3309 | * and the OPTIONAL fallback shares it, so no writer can run off the |
| 3310 | * end and no row class is dropped (the old fixed sizing had exactly |
| 3311 | * those two failure modes, CWE-787 and the OPTIONAL data loss). */ |
| 3312 | int new_cap = *bind_count > 0 ? *bind_count : CYP_GROWTH_10; |
| 3313 | binding_t *new_bindings = malloc((size_t)new_cap * sizeof(binding_t)); |
| 3314 | if (!new_bindings) { |
| 3315 | return; /* OOM: leave existing bindings untouched rather than corrupt */ |
| 3316 | } |
| 3317 | int new_count = 0; |
| 3318 | |
| 3319 | for (int bi = 0; bi < *bind_count; bi++) { |
| 3320 | if ((bi & CYPHER_DEADLINE_CHECK_MASK) == 0 && cypher_deadline_exceeded()) { |
| 3321 | break; |
| 3322 | } |
| 3323 | binding_t *b = &(*bindings)[bi]; |
| 3324 | cbm_node_t *src = binding_get(b, *var_name); |
| 3325 | if (!src) { |
| 3326 | continue; |
| 3327 | } |
| 3328 | |
| 3329 | int match_count = 0; |
| 3330 | |
| 3331 | if (is_variable_length) { |
| 3332 | expand_var_length(store, rel, target_node, b, src, to_var, &new_bindings, |
| 3333 | &new_count, &new_cap, &match_count); |
| 3334 | } else { |
| 3335 | expand_fixed_length(store, rel, target_node, b, src, to_var, &new_bindings, |
| 3336 | &new_count, &new_cap, &match_count); |
| 3337 | } |
| 3338 | |
| 3339 | /* OPTIONAL MATCH: no expansion for this source, so keep the binding |
| 3340 | * with the target unbound (projection renders it ""). The shared |
| 3341 | * growable append gives every fallback row a slot. */ |
| 3342 | if (is_optional && match_count == 0) { |
| 3343 | binding_t nb = {0}; |
| 3344 | binding_copy(&nb, b); |
| 3345 | /* Don't set to_var — it remains unbound; projection returns "" */ |
| 3346 | (void)binding_out_append(&new_bindings, &new_count, &new_cap, &nb); |
| 3347 | } |
| 3348 | } |
no test coverage detected