mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #2764 from gaurav0107/fix/2742-cypher-return-zero-for-ungrouped-count-o
fix(cypher): return one row for an ungrouped aggregate over empty input
This commit is contained in:
2 files changed
+135
No files matched your search
@@ -5370,6 +5370,34 @@ static void ret_agg_emit_row(cbm_return_clause_t *ret, ret_agg_entry_t *agg, res
|
||||
rb_add_row(rb, row);
|
||||
}
|
||||
|
||||
/* True when every RETURN item is an aggregate, so the whole input is one group. */
|
||||
static bool ret_agg_is_ungrouped(const cbm_return_clause_t *ret) {
|
||||
for (int ci = 0; ci < ret->count; ci++) {
|
||||
if (!is_aggregate_func(ret->items[ci].func)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The one row an ungrouped aggregation answers when no binding reached it
|
||||
* (#2742): count and sum are 0, collect is an empty list, and avg/min/max have
|
||||
* no value, rendered as "" like any other null. */
|
||||
static void ret_agg_emit_empty_row(cbm_return_clause_t *ret, result_builder_t *rb) {
|
||||
const char *row[CBM_SZ_32];
|
||||
for (int ci = 0; ci < ret->count && ci < CBM_SZ_32; ci++) {
|
||||
const char *func = ret->items[ci].func;
|
||||
if (strcmp(func, "COUNT") == 0 || strcmp(func, "SUM") == 0) {
|
||||
row[ci] = "0";
|
||||
} else if (strcmp(func, "COLLECT") == 0) {
|
||||
row[ci] = "[]";
|
||||
} else {
|
||||
row[ci] = "";
|
||||
}
|
||||
}
|
||||
rb_add_row(rb, row);
|
||||
}
|
||||
|
||||
static void ret_sink_feed_agg(ret_sink_t *s, binding_t *bindings, int bind_count) {
|
||||
cbm_return_clause_t *ret = s->ret;
|
||||
for (int bi = 0; bi < bind_count && !s->done; bi++) {
|
||||
@@ -5586,6 +5614,11 @@ static void ret_sink_finish(ret_sink_t *s) {
|
||||
for (int a = 0; a < s->agg_count; a++) {
|
||||
ret_agg_emit_row(ret, &s->aggs[a], rb);
|
||||
}
|
||||
/* Without grouping keys the input is a single group even when it is
|
||||
* empty; a grouped aggregation over no rows has no group to report. */
|
||||
if (s->agg_count == 0 && ret_agg_is_ungrouped(ret)) {
|
||||
ret_agg_emit_empty_row(ret, rb);
|
||||
}
|
||||
}
|
||||
ret_sink_free(s);
|
||||
|
||||
|
||||
@@ -4958,6 +4958,107 @@ TEST(cypher_issue305_count_star_alias) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Runs one query and asserts it answers exactly one row with `want` in the
|
||||
* first column. */
|
||||
static int expect_one_row(cbm_store_t *s, const char *project, const char *query,
|
||||
const char *want) {
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s, query, project, 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_NULL(r.error);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], want);
|
||||
cbm_cypher_result_free(&r);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* #2742: an ungrouped aggregation is one row even when MATCH/WHERE produced no
|
||||
* input rows. count(*) over nothing is 0, not an empty result: a caller asking
|
||||
* "how many?" must not have to read "no rows" as "zero". */
|
||||
TEST(cypher_count_star_empty_input_returns_zero) {
|
||||
/* Empty graph: the project exists but holds no nodes. */
|
||||
cbm_store_t *empty = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(empty, "empty", "/tmp/empty");
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(empty, "MATCH (n) RETURN count(*) AS total", "empty", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_NULL(r.error);
|
||||
ASSERT_EQ(r.col_count, 1);
|
||||
ASSERT_STR_EQ(r.columns[0], "total");
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "0");
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(empty);
|
||||
|
||||
/* Nonempty graph, every row filtered out. */
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
ASSERT_EQ(expect_one_row(s, "test",
|
||||
"MATCH (f:Function) WHERE f.name = \"NoSuchFunction\" "
|
||||
"RETURN count(*) AS total",
|
||||
"0"),
|
||||
0);
|
||||
ASSERT_EQ(expect_one_row(s, "test",
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE b.name = \"NoSuchFunction\" "
|
||||
"RETURN count(*)",
|
||||
"0"),
|
||||
0);
|
||||
ASSERT_EQ(expect_one_row(s, "test", "MATCH (n:NoSuchLabel) RETURN count(*)", "0"), 0);
|
||||
ASSERT_EQ(expect_one_row(s, "test",
|
||||
"MATCH (f:Function) WHERE f.name = \"NoSuchFunction\" "
|
||||
"RETURN count(f) AS n",
|
||||
"0"),
|
||||
0);
|
||||
ASSERT_EQ(expect_one_row(s, "test",
|
||||
"MATCH (f:Function) WHERE f.name = \"NoSuchFunction\" "
|
||||
"RETURN count(DISTINCT f.name) AS n",
|
||||
"0"),
|
||||
0);
|
||||
|
||||
/* The other aggregates in the same row take their empty-input values:
|
||||
* sum 0, collect [], and avg/min/max null (rendered as ""). */
|
||||
rc = cbm_cypher_execute(s,
|
||||
"MATCH (f:Function) WHERE f.name = \"NoSuchFunction\" "
|
||||
"RETURN count(*) AS c, sum(f.start_line) AS s, "
|
||||
"avg(f.start_line) AS a, min(f.start_line) AS lo, "
|
||||
"max(f.start_line) AS hi, collect(f.name) AS names",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_NULL(r.error);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "0");
|
||||
ASSERT_STR_EQ(r.rows[0][1], "0");
|
||||
ASSERT_STR_EQ(r.rows[0][2], "");
|
||||
ASSERT_STR_EQ(r.rows[0][3], "");
|
||||
ASSERT_STR_EQ(r.rows[0][4], "");
|
||||
ASSERT_STR_EQ(r.rows[0][5], "[]");
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
/* A grouped aggregation has no group to report: still no rows. */
|
||||
rc = cbm_cypher_execute(s,
|
||||
"MATCH (f:Function) WHERE f.name = \"NoSuchFunction\" "
|
||||
"RETURN f.label, count(*)",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_NULL(r.error);
|
||||
ASSERT_EQ(r.row_count, 0);
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
/* SKIP still applies to the single aggregate row. */
|
||||
rc = cbm_cypher_execute(s,
|
||||
"MATCH (f:Function) WHERE f.name = \"NoSuchFunction\" "
|
||||
"RETURN count(*) SKIP 1",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_NULL(r.error);
|
||||
ASSERT_EQ(r.row_count, 0);
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
/* Nonempty input is unchanged. */
|
||||
ASSERT_EQ(expect_one_row(s, "test", "MATCH (f:Function) RETURN count(*) AS total", "4"), 0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Regression: projecting several computed/JSON properties in one row must yield
|
||||
* DISTINCT values. node_prop previously returned a single shared static buffer,
|
||||
* so every such column aliased the last property read — and because the search
|
||||
@@ -6260,6 +6361,7 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_issue873_distinct_order_skip_limit_dedupes_before_skip);
|
||||
RUN_TEST(cypher_issue252_tointeger);
|
||||
RUN_TEST(cypher_issue305_count_star_alias);
|
||||
RUN_TEST(cypher_count_star_empty_input_returns_zero);
|
||||
RUN_TEST(cypher_exec_where_eq);
|
||||
RUN_TEST(cypher_exec_unlabeled_where_beyond_result_limit_issue1196);
|
||||
RUN_TEST(cypher_exec_aggregate_sees_all_edges_beyond_expansion_cap_issue1196);
|
||||
|
||||
Reference in new issue
Block a user