Merge pull request #2241 from lorenzozanee/fix/optional-match-count-unbound

fix(cypher): ignore unbound OPTIONAL MATCH values in COUNT
This commit is contained in:
Martin Vogel
2026-09-20 10:24:39 +02:00
committed by GitHub
2 changed files with 52 additions and 0 deletions
+22
View File
@@ -4450,11 +4450,33 @@ static void ret_agg_init_group(ret_agg_entry_t *entry, const char *key, int item
}
/* Accumulate a binding into RETURN aggregation */
static bool binding_has_value(binding_t *b, const char *var, const char *prop) {
if (!var || strcmp(var, "*") == 0) {
return true;
}
char full[CBM_SZ_256];
if (prop) {
snprintf(full, sizeof(full), "%s.%s", var, prop);
} else {
snprintf(full, sizeof(full), "%s", var);
}
for (int i = 0; i < b->var_count; i++) {
if (strcmp(b->var_names[i], full) == 0) {
return true;
}
}
return binding_get_edge(b, var) != NULL || binding_get(b, var) != NULL;
}
static void ret_agg_accumulate(ret_agg_entry_t *entry, cbm_return_clause_t *ret, binding_t *b) {
for (int ci = 0; ci < ret->count; ci++) {
if (!is_aggregate_func(ret->items[ci].func)) {
continue;
}
if (strcmp(ret->items[ci].func, "COUNT") == 0 &&
!binding_has_value(b, ret->items[ci].variable, ret->items[ci].property)) {
continue;
}
entry->counts[ci]++;
const char *raw = binding_get_virtual(b, ret->items[ci].variable, ret->items[ci].property);
double dv = strtod(raw, NULL);
+30
View File
@@ -1744,6 +1744,35 @@ TEST(cypher_exec_count) {
PASS();
}
TEST(cypher_exec_optional_bound_terminal_count_ignores_unbound) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) OPTIONAL MATCH (t)-[:CALLS]->(f) "
"RETURN f.name, COUNT(t) AS cnt",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 4);
for (int i = 0; i < r.row_count; i++) {
const char *name = r.rows[i][0];
const char *count = r.rows[i][1];
if (strcmp(name, "HandleOrder") == 0) {
ASSERT_STR_EQ(count, "0");
} else if (strcmp(name, "ValidateOrder") == 0 || strcmp(name, "SubmitOrder") == 0 ||
strcmp(name, "LogError") == 0) {
ASSERT_STR_EQ(count, "1");
} else {
ASSERT_TRUE(false);
}
}
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_limit) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
@@ -4578,6 +4607,7 @@ SUITE(cypher) {
RUN_TEST(cypher_exec_calls_with_where);
RUN_TEST(cypher_exec_inbound);
RUN_TEST(cypher_exec_count);
RUN_TEST(cypher_exec_optional_bound_terminal_count_ignores_unbound);
RUN_TEST(cypher_exec_limit);
RUN_TEST(cypher_exec_order_by);
RUN_TEST(cypher_exec_variable_length);