mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
fix(cypher): defer unbound relationship predicates
execute_single previously evaluated the complete WHERE tree while only the first MATCH alias was bound. A target-alias predicate such as NOT callee.file_path CONTAINS "tests/" therefore rejected every seed before CALLS expansion and returned zero rows. Add allocation-free three-valued partial evaluation in src/cypher/cypher.c: bound leaves prune early, unbound node or edge aliases remain unknown, and AND/OR/NOT/XOR preserve boolean semantics and safe short-circuiting. Register eval_expr_partial in src/foundation/recursion_whitelist.h. Add four relationship-target and mixed-alias regressions in tests/test_cypher.c. Verified 159/159 Cypher tests under ASan/UBSan, source-safety and whitelist checks, an -O2 production build, and the formerly empty hotspot query returning 20 bounded rows. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
+82
-1
@@ -2619,6 +2619,87 @@ static bool eval_where(const cbm_where_clause_t *w, binding_t *b) {
|
|||||||
return is_and;
|
return is_and;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef enum { CYP_PARTIAL_FALSE = 0, CYP_PARTIAL_TRUE, CYP_PARTIAL_UNKNOWN } cypher_partial_bool_t;
|
||||||
|
|
||||||
|
static cypher_partial_bool_t partial_and(cypher_partial_bool_t left, cypher_partial_bool_t right) {
|
||||||
|
if (left == CYP_PARTIAL_FALSE || right == CYP_PARTIAL_FALSE) {
|
||||||
|
return CYP_PARTIAL_FALSE;
|
||||||
|
}
|
||||||
|
return left == CYP_PARTIAL_TRUE && right == CYP_PARTIAL_TRUE ? CYP_PARTIAL_TRUE
|
||||||
|
: CYP_PARTIAL_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cypher_partial_bool_t partial_or(cypher_partial_bool_t left, cypher_partial_bool_t right) {
|
||||||
|
if (left == CYP_PARTIAL_TRUE || right == CYP_PARTIAL_TRUE) {
|
||||||
|
return CYP_PARTIAL_TRUE;
|
||||||
|
}
|
||||||
|
return left == CYP_PARTIAL_FALSE && right == CYP_PARTIAL_FALSE ? CYP_PARTIAL_FALSE
|
||||||
|
: CYP_PARTIAL_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Evaluate the portion of a WHERE expression whose aliases are already
|
||||||
|
* bound. Unknown leaves keep the seed; definitively false source predicates
|
||||||
|
* still prune before relationship expansion. Per seed this is O(expression
|
||||||
|
* nodes) time, O(expression depth) stack, and allocation-free; AND/OR retain
|
||||||
|
* the full evaluator's safe short-circuit behavior. */
|
||||||
|
static cypher_partial_bool_t eval_expr_partial(const cbm_expr_t *e, // NOLINT(misc-no-recursion)
|
||||||
|
binding_t *b) {
|
||||||
|
if (!e) {
|
||||||
|
return CYP_PARTIAL_TRUE;
|
||||||
|
}
|
||||||
|
if (e->type == EXPR_CONDITION) {
|
||||||
|
if (!binding_get(b, e->cond.variable) && !binding_get_edge(b, e->cond.variable)) {
|
||||||
|
return CYP_PARTIAL_UNKNOWN;
|
||||||
|
}
|
||||||
|
return eval_condition(&e->cond, b) ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
cypher_partial_bool_t left = eval_expr_partial(e->left, b);
|
||||||
|
if (e->type == EXPR_NOT) {
|
||||||
|
return left == CYP_PARTIAL_UNKNOWN
|
||||||
|
? CYP_PARTIAL_UNKNOWN
|
||||||
|
: (left == CYP_PARTIAL_TRUE ? CYP_PARTIAL_FALSE : CYP_PARTIAL_TRUE);
|
||||||
|
}
|
||||||
|
if (e->type == EXPR_AND && left == CYP_PARTIAL_FALSE) {
|
||||||
|
return CYP_PARTIAL_FALSE;
|
||||||
|
}
|
||||||
|
if (e->type == EXPR_OR && left == CYP_PARTIAL_TRUE) {
|
||||||
|
return CYP_PARTIAL_TRUE;
|
||||||
|
}
|
||||||
|
cypher_partial_bool_t right = eval_expr_partial(e->right, b);
|
||||||
|
if (e->type == EXPR_AND) {
|
||||||
|
return partial_and(left, right);
|
||||||
|
}
|
||||||
|
if (e->type == EXPR_OR) {
|
||||||
|
return partial_or(left, right);
|
||||||
|
}
|
||||||
|
if (left == CYP_PARTIAL_UNKNOWN || right == CYP_PARTIAL_UNKNOWN) {
|
||||||
|
return CYP_PARTIAL_UNKNOWN;
|
||||||
|
}
|
||||||
|
return left != right ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cypher_partial_bool_t eval_where_partial(const cbm_where_clause_t *where, binding_t *b) {
|
||||||
|
if (!where) {
|
||||||
|
return CYP_PARTIAL_TRUE;
|
||||||
|
}
|
||||||
|
if (where->root) {
|
||||||
|
return eval_expr_partial(where->root, b);
|
||||||
|
}
|
||||||
|
cypher_partial_bool_t result =
|
||||||
|
where->op && strcmp(where->op, "AND") == 0 ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE;
|
||||||
|
for (int i = 0; i < where->count; i++) {
|
||||||
|
cypher_partial_bool_t item =
|
||||||
|
(!binding_get(b, where->conditions[i].variable) &&
|
||||||
|
!binding_get_edge(b, where->conditions[i].variable))
|
||||||
|
? CYP_PARTIAL_UNKNOWN
|
||||||
|
: (eval_condition(&where->conditions[i], b) ? CYP_PARTIAL_TRUE : CYP_PARTIAL_FALSE);
|
||||||
|
result = where->op && strcmp(where->op, "AND") == 0 ? partial_and(result, item)
|
||||||
|
: partial_or(result, item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/* Check if a string value looks like a regex pattern. */
|
/* Check if a string value looks like a regex pattern. */
|
||||||
static bool looks_like_regex(const char *s) {
|
static bool looks_like_regex(const char *s) {
|
||||||
if (!s) {
|
if (!s) {
|
||||||
@@ -4668,7 +4749,7 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec
|
|||||||
b.project = project;
|
b.project = project;
|
||||||
b.use_active_overlay_edges = scan_mode == CYP_NODE_SCAN_ACTIVE_OVERLAY;
|
b.use_active_overlay_edges = scan_mode == CYP_NODE_SCAN_ACTIVE_OVERLAY;
|
||||||
binding_set(&b, var_name, &scanned[i]);
|
binding_set(&b, var_name, &scanned[i]);
|
||||||
bool pass = !q->where || eval_where(q->where, &b);
|
bool pass = eval_where_partial(q->where, &b) != CYP_PARTIAL_FALSE;
|
||||||
if (pass) {
|
if (pass) {
|
||||||
bindings[bind_count++] = b;
|
bindings[bind_count++] = b;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
* - parse_or_expr, parse_xor_expr, parse_and_expr, parse_not_expr
|
* - parse_or_expr, parse_xor_expr, parse_and_expr, parse_not_expr
|
||||||
* - parse_atom_expr, parse_post_where, cbm_parse
|
* - parse_atom_expr, parse_post_where, cbm_parse
|
||||||
*
|
*
|
||||||
* Cypher expression evaluator (bounded by WHERE clause depth ~5):
|
* Cypher expression traversal (bounded by WHERE clause depth ~5):
|
||||||
* - eval_expr
|
* - eval_expr, eval_expr_partial
|
||||||
*
|
*
|
||||||
* Glob pattern matcher (bounded by pattern nesting ~3):
|
* Glob pattern matcher (bounded by pattern nesting ~3):
|
||||||
* - glob_match, glob_match_star, glob_match_doublestar
|
* - glob_match, glob_match_star, glob_match_doublestar
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
*/
|
*/
|
||||||
#define CBM_RECURSION_WHITELIST \
|
#define CBM_RECURSION_WHITELIST \
|
||||||
"parse_or_expr", "parse_xor_expr", "parse_and_expr", "parse_not_expr", "parse_atom_expr", \
|
"parse_or_expr", "parse_xor_expr", "parse_and_expr", "parse_not_expr", "parse_atom_expr", \
|
||||||
"parse_post_where", "cbm_parse", "eval_expr", "glob_match", "glob_match_star", \
|
"parse_post_where", "cbm_parse", "eval_expr", "eval_expr_partial", "glob_match", \
|
||||||
"glob_match_doublestar", "glob_match_doublestar_slash", "glob_match_doublestar_any", \
|
"glob_match_star", "glob_match_doublestar", "glob_match_doublestar_slash", \
|
||||||
"parse_bool_expr", "parse_bool_atom", "r_collect_imports", \
|
"glob_match_doublestar_any", "parse_bool_expr", "parse_bool_atom", "r_collect_imports", \
|
||||||
"find_first_descendant_by_kind", "find_first_descendant_of"
|
"find_first_descendant_by_kind", "find_first_descendant_of"
|
||||||
|
|||||||
@@ -1929,6 +1929,69 @@ TEST(cypher_exec_where_not) {
|
|||||||
PASS();
|
PASS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(cypher_exec_where_not_on_relationship_target) {
|
||||||
|
cbm_store_t *s = setup_cypher_store();
|
||||||
|
cbm_cypher_result_t r = {0};
|
||||||
|
int rc = cbm_cypher_execute(s,
|
||||||
|
"MATCH (a:Function)-[:CALLS]->(b:Function) "
|
||||||
|
"WHERE NOT b.name CONTAINS \"Order\" RETURN b.name",
|
||||||
|
"test", 0, &r);
|
||||||
|
ASSERT_EQ(rc, 0);
|
||||||
|
ASSERT_EQ(r.row_count, 1);
|
||||||
|
ASSERT_STR_EQ(r.rows[0][0], "LogError");
|
||||||
|
cbm_cypher_result_free(&r);
|
||||||
|
cbm_store_close(s);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(cypher_exec_where_mixed_alias_and) {
|
||||||
|
cbm_store_t *s = setup_cypher_store();
|
||||||
|
cbm_cypher_result_t r = {0};
|
||||||
|
int rc = cbm_cypher_execute(
|
||||||
|
s,
|
||||||
|
"MATCH (a:Function)-[:CALLS]->(b:Function) "
|
||||||
|
"WHERE a.name = \"HandleOrder\" AND NOT b.name CONTAINS \"Order\" RETURN b.name",
|
||||||
|
"test", 0, &r);
|
||||||
|
ASSERT_EQ(rc, 0);
|
||||||
|
ASSERT_EQ(r.row_count, 1);
|
||||||
|
ASSERT_STR_EQ(r.rows[0][0], "LogError");
|
||||||
|
cbm_cypher_result_free(&r);
|
||||||
|
cbm_store_close(s);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(cypher_exec_where_mixed_alias_or) {
|
||||||
|
cbm_store_t *s = setup_cypher_store();
|
||||||
|
cbm_cypher_result_t r = {0};
|
||||||
|
int rc = cbm_cypher_execute(
|
||||||
|
s,
|
||||||
|
"MATCH (a:Function)-[:CALLS]->(b:Function) "
|
||||||
|
"WHERE a.name = \"NoSuchFunction\" OR NOT b.name CONTAINS \"Order\" RETURN b.name",
|
||||||
|
"test", 0, &r);
|
||||||
|
ASSERT_EQ(rc, 0);
|
||||||
|
ASSERT_EQ(r.row_count, 1);
|
||||||
|
ASSERT_STR_EQ(r.rows[0][0], "LogError");
|
||||||
|
cbm_cypher_result_free(&r);
|
||||||
|
cbm_store_close(s);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(cypher_exec_where_mixed_alias_xor) {
|
||||||
|
cbm_store_t *s = setup_cypher_store();
|
||||||
|
cbm_cypher_result_t r = {0};
|
||||||
|
int rc =
|
||||||
|
cbm_cypher_execute(s,
|
||||||
|
"MATCH (a:Function)-[:CALLS]->(b:Function) "
|
||||||
|
"WHERE a.name = \"HandleOrder\" XOR b.name = \"LogError\" RETURN b.name",
|
||||||
|
"test", 0, &r);
|
||||||
|
ASSERT_EQ(rc, 0);
|
||||||
|
ASSERT_EQ(r.row_count, 1);
|
||||||
|
ASSERT_STR_EQ(r.rows[0][0], "ValidateOrder");
|
||||||
|
cbm_cypher_result_free(&r);
|
||||||
|
cbm_store_close(s);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
TEST(cypher_exec_where_in) {
|
TEST(cypher_exec_where_in) {
|
||||||
cbm_store_t *s = setup_cypher_store();
|
cbm_store_t *s = setup_cypher_store();
|
||||||
cbm_cypher_result_t r = {0};
|
cbm_cypher_result_t r = {0};
|
||||||
@@ -3073,6 +3136,10 @@ SUITE(cypher) {
|
|||||||
RUN_TEST(cypher_exec_where_neq_bang);
|
RUN_TEST(cypher_exec_where_neq_bang);
|
||||||
RUN_TEST(cypher_exec_where_ends_with);
|
RUN_TEST(cypher_exec_where_ends_with);
|
||||||
RUN_TEST(cypher_exec_where_not);
|
RUN_TEST(cypher_exec_where_not);
|
||||||
|
RUN_TEST(cypher_exec_where_not_on_relationship_target);
|
||||||
|
RUN_TEST(cypher_exec_where_mixed_alias_and);
|
||||||
|
RUN_TEST(cypher_exec_where_mixed_alias_or);
|
||||||
|
RUN_TEST(cypher_exec_where_mixed_alias_xor);
|
||||||
RUN_TEST(cypher_exec_where_in);
|
RUN_TEST(cypher_exec_where_in);
|
||||||
RUN_TEST(cypher_exec_where_not_in);
|
RUN_TEST(cypher_exec_where_not_in);
|
||||||
RUN_TEST(cypher_exec_where_is_null);
|
RUN_TEST(cypher_exec_where_is_null);
|
||||||
|
|||||||
Reference in New Issue
Block a user