Files
deusdata--codebase-memory-mcp/tests/test_cypher.c
T
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

2998 lines
106 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* test_cypher.c — Tests for the Cypher query engine.
*
* Ported from internal/cypher/cypher_test.go (1016 LOC).
* Covers lexer, parser, and end-to-end execution.
*/
#include "test_framework.h"
#include <cypher/cypher.h>
#include <store/store.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#ifndef _WIN32
#include <sys/wait.h> /* fork/waitpid crash-isolation for the projection-width guard */
#include <unistd.h>
#endif
/* ══════════════════════════════════════════════════════════════════
* LEXER TESTS
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_lex_simple_match) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("MATCH (n:Function)", &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
/* MATCH ( n : Function ) EOF */
ASSERT_GTE(r.count, 6);
ASSERT_EQ(r.tokens[0].type, TOK_MATCH);
ASSERT_EQ(r.tokens[1].type, TOK_LPAREN);
ASSERT_EQ(r.tokens[2].type, TOK_IDENT);
ASSERT_STR_EQ(r.tokens[2].text, "n");
ASSERT_EQ(r.tokens[3].type, TOK_COLON);
ASSERT_EQ(r.tokens[4].type, TOK_IDENT);
ASSERT_STR_EQ(r.tokens[4].text, "Function");
ASSERT_EQ(r.tokens[5].type, TOK_RPAREN);
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_relationship) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("-[:CALLS]->", &r);
ASSERT_EQ(rc, 0);
/* - [ : CALLS ] - > EOF */
ASSERT_GTE(r.count, 7);
ASSERT_EQ(r.tokens[0].type, TOK_DASH);
ASSERT_EQ(r.tokens[1].type, TOK_LBRACKET);
ASSERT_EQ(r.tokens[2].type, TOK_COLON);
ASSERT_EQ(r.tokens[3].type, TOK_IDENT);
ASSERT_STR_EQ(r.tokens[3].text, "CALLS");
ASSERT_EQ(r.tokens[4].type, TOK_RBRACKET);
ASSERT_EQ(r.tokens[5].type, TOK_DASH);
ASSERT_EQ(r.tokens[6].type, TOK_GT);
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_string_literal) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("\"hello world\"", &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.count, 1);
ASSERT_EQ(r.tokens[0].type, TOK_STRING);
ASSERT_STR_EQ(r.tokens[0].text, "hello world");
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_single_quote_string) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("'hello'", &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.tokens[0].type, TOK_STRING);
ASSERT_STR_EQ(r.tokens[0].text, "hello");
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_string_overflow) {
/* Build a string literal longer than 4096 bytes to verify we don't
* overflow the stack buffer in lex_string_literal. */
const int big = 5000;
/* query: "AAAA...A" (quotes included) */
char *query = malloc(big + 3); /* quote + big chars + quote + NUL */
ASSERT_NOT_NULL(query);
query[0] = '"';
memset(query + 1, 'A', big);
query[big + 1] = '"';
query[big + 2] = '\0';
cbm_lex_result_t r = {0};
int rc = cbm_lex(query, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
ASSERT_GTE(r.count, 1);
ASSERT_EQ(r.tokens[0].type, TOK_STRING);
/* The string should be truncated to CBM_SZ_4K - 1 (4095) characters. */
ASSERT_EQ((int)strlen(r.tokens[0].text), 4095);
cbm_lex_free(&r);
free(query);
PASS();
}
TEST(cypher_lex_number) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("42 3.14", &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.count, 2);
ASSERT_EQ(r.tokens[0].type, TOK_NUMBER);
ASSERT_STR_EQ(r.tokens[0].text, "42");
ASSERT_EQ(r.tokens[1].type, TOK_NUMBER);
ASSERT_STR_EQ(r.tokens[1].text, "3.14");
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_operators) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("= =~ >= <= ..", &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.count, 5);
ASSERT_EQ(r.tokens[0].type, TOK_EQ);
ASSERT_EQ(r.tokens[1].type, TOK_EQTILDE);
ASSERT_EQ(r.tokens[2].type, TOK_GTE);
ASSERT_EQ(r.tokens[3].type, TOK_LTE);
ASSERT_EQ(r.tokens[4].type, TOK_DOTDOT);
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_keywords_case_insensitive) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("match WHERE Return limit", &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.tokens[0].type, TOK_MATCH);
ASSERT_EQ(r.tokens[1].type, TOK_WHERE);
ASSERT_EQ(r.tokens[2].type, TOK_RETURN);
ASSERT_EQ(r.tokens[3].type, TOK_LIMIT);
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_pipe_and_star) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("[:TYPE1|TYPE2*1..3]", &r);
ASSERT_EQ(rc, 0);
/* [ : TYPE1 | TYPE2 * 1 .. 3 ] */
ASSERT_GTE(r.count, 9);
ASSERT_EQ(r.tokens[3].type, TOK_PIPE);
ASSERT_EQ(r.tokens[5].type, TOK_STAR);
ASSERT_EQ(r.tokens[6].type, TOK_NUMBER);
ASSERT_STR_EQ(r.tokens[6].text, "1");
ASSERT_EQ(r.tokens[7].type, TOK_DOTDOT);
ASSERT_EQ(r.tokens[8].type, TOK_NUMBER);
ASSERT_STR_EQ(r.tokens[8].text, "3");
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_full_query) {
const char *q = "MATCH (f:Function)-[:CALLS]->(g:Function) "
"WHERE f.name =~ \".*Order.*\" "
"RETURN f.name, g.name LIMIT 10";
cbm_lex_result_t r = {0};
int rc = cbm_lex(q, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
/* Should have many tokens; just check it doesn't crash */
ASSERT_GT(r.count, 20);
cbm_lex_free(&r);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PARSER TESTS
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_parse_simple_node) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function)", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NULL(err);
ASSERT_NOT_NULL(q);
ASSERT_EQ(cbm_query_pattern(q).node_count, 1);
ASSERT_EQ(cbm_query_pattern(q).rel_count, 0);
ASSERT_STR_EQ(cbm_query_pattern(q).nodes[0].variable, "f");
ASSERT_STR_EQ(cbm_query_pattern(q).nodes[0].label, "Function");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_relationship_outbound) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function)-[:CALLS]->(g:Function)", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_EQ(cbm_query_pattern(q).node_count, 2);
ASSERT_EQ(cbm_query_pattern(q).rel_count, 1);
ASSERT_STR_EQ(cbm_query_pattern(q).rels[0].types[0], "CALLS");
ASSERT_STR_EQ(cbm_query_pattern(q).rels[0].direction, "outbound");
ASSERT_EQ(cbm_query_pattern(q).rels[0].min_hops, 1);
ASSERT_EQ(cbm_query_pattern(q).rels[0].max_hops, 1);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_relationship_inbound) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function)<-[:CALLS]-(g:Function)", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_STR_EQ(cbm_query_pattern(q).rels[0].direction, "inbound");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_relationship_any) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function)-[:CALLS]-(g:Function)", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_STR_EQ(cbm_query_pattern(q).rels[0].direction, "any");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_variable_length) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function)-[:CALLS*1..3]->(g:Function)", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_EQ(cbm_query_pattern(q).rels[0].min_hops, 1);
ASSERT_EQ(cbm_query_pattern(q).rels[0].max_hops, 3);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_variable_length_unbounded) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f)-[:CALLS*]->(g)", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_EQ(cbm_query_pattern(q).rels[0].min_hops, 1);
ASSERT_EQ(cbm_query_pattern(q).rels[0].max_hops, 0); /* 0 = unbounded */
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_multiple_edge_types) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f)-[:CALLS|HTTP_CALLS]->(g)", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_EQ(cbm_query_pattern(q).rels[0].type_count, 2);
ASSERT_STR_EQ(cbm_query_pattern(q).rels[0].types[0], "CALLS");
ASSERT_STR_EQ(cbm_query_pattern(q).rels[0].types[1], "HTTP_CALLS");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_where_clause) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function) WHERE f.name = \"Foo\"", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_NOT_NULL(q->where);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_CONDITION);
ASSERT_STR_EQ(q->where->root->cond.variable, "f");
ASSERT_STR_EQ(q->where->root->cond.property, "name");
ASSERT_STR_EQ(q->where->root->cond.op, "=");
ASSERT_STR_EQ(q->where->root->cond.value, "Foo");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_where_regex) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function) WHERE f.name =~ \".*Order.*\"", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_CONDITION);
ASSERT_STR_EQ(q->where->root->cond.op, "=~");
ASSERT_STR_EQ(q->where->root->cond.value, ".*Order.*");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_where_and) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function) WHERE f.name = \"A\" AND f.label = \"Function\"",
&q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_AND);
ASSERT_NOT_NULL(q->where->root->left);
ASSERT_NOT_NULL(q->where->root->right);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_return_simple) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function) RETURN f.name, f.qualified_name", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->ret);
ASSERT_EQ(q->ret->count, 2);
ASSERT_STR_EQ(q->ret->items[0].variable, "f");
ASSERT_STR_EQ(q->ret->items[0].property, "name");
ASSERT_STR_EQ(q->ret->items[1].property, "qualified_name");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_return_count) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f)-[:CALLS]->(g) RETURN f.name, COUNT(g) AS cnt", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_EQ(q->ret->count, 2);
ASSERT_NOT_NULL(q->ret->items[1].func);
ASSERT_STR_EQ(q->ret->items[1].func, "COUNT");
ASSERT_STR_EQ(q->ret->items[1].alias, "cnt");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_return_order_limit) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("MATCH (f:Function) RETURN f.name ORDER BY f.name DESC LIMIT 5", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->ret->order_by);
ASSERT_STR_EQ(q->ret->order_dir, "DESC");
ASSERT_EQ(q->ret->limit, 5);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_return_distinct) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function) RETURN DISTINCT f.label", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT(q->ret->distinct);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_inline_props) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function {name: \"Foo\"})", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_EQ(cbm_query_pattern(q).nodes[0].prop_count, 1);
ASSERT_STR_EQ(cbm_query_pattern(q).nodes[0].props[0].key, "name");
ASSERT_STR_EQ(cbm_query_pattern(q).nodes[0].props[0].value, "Foo");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_error) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("INVALID QUERY", &q, &err);
ASSERT_EQ(rc, -1);
ASSERT_NOT_NULL(err);
free(err);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* EXECUTION TESTS (end-to-end against store)
* ══════════════════════════════════════════════════════════════════ */
/* Helper: set up the standard test graph.
* Nodes: HandleOrder, ValidateOrder, SubmitOrder (Function), main (Module), LogError (Function)
* Edges: HandleOrder→ValidateOrder (CALLS), ValidateOrder→SubmitOrder (CALLS),
* HandleOrder→LogError (CALLS), main→HandleOrder (DEFINES)
*/
static cbm_store_t *setup_cypher_store(void) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t n1 = {.project = "test",
.label = "Function",
.name = "HandleOrder",
.qualified_name = "test.HandleOrder",
.file_path = "handler.go",
.start_line = 10,
.end_line = 30};
cbm_node_t n2 = {.project = "test",
.label = "Function",
.name = "ValidateOrder",
.qualified_name = "test.ValidateOrder",
.file_path = "validate.go",
.start_line = 5,
.end_line = 15};
cbm_node_t n3 = {.project = "test",
.label = "Function",
.name = "SubmitOrder",
.qualified_name = "test.SubmitOrder",
.file_path = "submit.go"};
cbm_node_t n4 = {
.project = "test", .label = "Module", .name = "main", .qualified_name = "test.main"};
cbm_node_t n5 = {.project = "test",
.label = "Function",
.name = "LogError",
.qualified_name = "test.LogError",
.file_path = "log.go"};
int64_t id1 = cbm_store_upsert_node(s, &n1);
int64_t id2 = cbm_store_upsert_node(s, &n2);
int64_t id3 = cbm_store_upsert_node(s, &n3);
int64_t id4 = cbm_store_upsert_node(s, &n4);
int64_t id5 = cbm_store_upsert_node(s, &n5);
cbm_edge_t e1 = {.project = "test", .source_id = id1, .target_id = id2, .type = "CALLS"};
cbm_edge_t e2 = {.project = "test", .source_id = id2, .target_id = id3, .type = "CALLS"};
cbm_edge_t e3 = {.project = "test", .source_id = id1, .target_id = id5, .type = "CALLS"};
cbm_edge_t e4 = {.project = "test", .source_id = id4, .target_id = id1, .type = "DEFINES"};
cbm_store_insert_edge(s, &e1);
cbm_store_insert_edge(s, &e2);
cbm_store_insert_edge(s, &e3);
cbm_store_insert_edge(s, &e4);
return s;
}
TEST(cypher_exec_match_all_functions) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function)", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 4); /* HandleOrder, ValidateOrder, SubmitOrder, LogError */
ASSERT_GT(r.col_count, 0);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_eq) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\"", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #874: coalesce(var.prop, literal) in WHERE — null-safe numeric filters
* for audit queries over OPTIONAL graph properties. The parser rejected the
* call outright ("unexpected operator"); RETURN-side coalesce already
* worked, so only the WHERE leaf needs it. Semantics: when the property is
* missing/empty, the literal default is compared instead. */
/* #797: variable-length / repeated-variable path semantics. Fixture:
* loopy has a SELF-LOOP as one of its outbound CALLS edges plus a real
* 2-chain loopy->mid->leaf. Correct openCypher semantics:
* - a repeated node variable must unify: (a)-[:CALLS]->(a) matches ONLY
* the self-loop, not every edge;
* - relationship uniqueness within a path: the self-loop cannot be
* traversed repeatedly, so no *k..k path exists beyond the real chain;
* - the engine hop cap must not fabricate or silently truncate results. */
TEST(cypher_exec_varlength_path_semantics_issue797) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t loopy = {.project = "test",
.label = "Function",
.name = "loopy",
.qualified_name = "test.mod.loopy",
.file_path = "mod.go",
.start_line = 1,
.end_line = 2};
cbm_node_t mid = {.project = "test",
.label = "Function",
.name = "mid",
.qualified_name = "test.mod.mid",
.file_path = "mod.go",
.start_line = 3,
.end_line = 4};
cbm_node_t leaf = {.project = "test",
.label = "Function",
.name = "leaf",
.qualified_name = "test.mod.leaf",
.file_path = "mod.go",
.start_line = 5,
.end_line = 6};
int64_t id_loopy = cbm_store_upsert_node(s, &loopy);
int64_t id_mid = cbm_store_upsert_node(s, &mid);
int64_t id_leaf = cbm_store_upsert_node(s, &leaf);
ASSERT_GT(id_loopy, 0);
cbm_edge_t self_loop = {
.project = "test", .source_id = id_loopy, .target_id = id_loopy, .type = "CALLS"};
cbm_edge_t e1 = {
.project = "test", .source_id = id_loopy, .target_id = id_mid, .type = "CALLS"};
cbm_edge_t e2 = {.project = "test", .source_id = id_mid, .target_id = id_leaf, .type = "CALLS"};
cbm_store_insert_edge(s, &self_loop);
cbm_store_insert_edge(s, &e1);
cbm_store_insert_edge(s, &e2);
/* Bug 1: repeated variable must unify — only the self-loop matches. */
cbm_cypher_result_t r1 = {0};
ASSERT_EQ(cbm_cypher_execute(s, "MATCH (a)-[:CALLS]->(a) RETURN a.name", "test", 0, &r1), 0);
ASSERT_EQ(r1.row_count, 1);
cbm_cypher_result_free(&r1);
/* Bug 2: *2..2 from loopy — only the REAL 2-chain (leaf); the self-loop
* must not be reused to pad paths (relationship uniqueness). */
cbm_cypher_result_t r2 = {0};
ASSERT_EQ(cbm_cypher_execute(s,
"MATCH (a {name: \"loopy\"})-[:CALLS*2..2]->(b) "
"RETURN DISTINCT b.name",
"test", 0, &r2),
0);
ASSERT_EQ(r2.row_count, 1); /* leaf only */
cbm_cypher_result_free(&r2);
/* Bug 2 amplifier: no directed path of length 5 exists at all. */
cbm_cypher_result_t r3 = {0};
ASSERT_EQ(cbm_cypher_execute(s,
"MATCH (a {name: \"loopy\"})-[:CALLS*5..5]->(b) "
"RETURN b.name",
"test", 0, &r3),
0);
ASSERT_EQ(r3.row_count, 0);
cbm_cypher_result_free(&r3);
/* Bug 3: a hop range beyond the engine ceiling must be an ADVERTISED
* clamp, not silently indistinguishable from "no such path". */
cbm_cypher_result_t r4 = {0};
ASSERT_EQ(
cbm_cypher_execute(s, "MATCH (a)-[:CALLS*150..150]->(b) RETURN b.name", "test", 0, &r4), 0);
ASSERT_EQ(r4.row_count, 0);
ASSERT_NOT_NULL(r4.warning);
ASSERT_NOT_NULL(strstr(r4.warning, "clamped"));
cbm_cypher_result_free(&r4);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_coalesce_issue874) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t a = {.project = "test",
.label = "Function",
.name = "deep_a",
.qualified_name = "test.mod.deep_a",
.file_path = "mod.py",
.start_line = 1,
.end_line = 2,
.properties_json = "{\"transitive_loop_depth\":3}"};
cbm_node_t b = {.project = "test",
.label = "Function",
.name = "deep_b",
.qualified_name = "test.mod.deep_b",
.file_path = "mod.py",
.start_line = 3,
.end_line = 4,
.properties_json = "{\"transitive_loop_depth\":1}"};
cbm_node_t c = {.project = "test",
.label = "Function",
.name = "plain_c",
.qualified_name = "test.mod.plain_c",
.file_path = "mod.py",
.start_line = 5,
.end_line = 6};
ASSERT_GT(cbm_store_upsert_node(s, &a), 0);
ASSERT_GT(cbm_store_upsert_node(s, &b), 0);
ASSERT_GT(cbm_store_upsert_node(s, &c), 0);
/* Default FAILS the predicate: only the node with depth 3 matches. */
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE "
"coalesce(f.transitive_loop_depth, 0) >= 2 "
"RETURN f.qualified_name LIMIT 10",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
/* Default PASSES: the property-less node is included via the default. */
cbm_cypher_result_t r2 = {0};
rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE "
"coalesce(f.transitive_loop_depth, 9) >= 2 "
"RETURN f.qualified_name LIMIT 10",
"test", 0, &r2);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r2.row_count, 2); /* deep_a (3) + plain_c (default 9) */
cbm_cypher_result_free(&r2);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_regex) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.name =~ \".*Order.*\"", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 3); /* HandleOrder, ValidateOrder, SubmitOrder */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_contains) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.name CONTAINS \"Order\"", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 3);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_starts_with) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.name STARTS WITH \"Handle\"", "test",
0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_return_properties) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" "
"RETURN f.name, f.qualified_name, f.file_path",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_EQ(r.col_count, 3);
/* Columns should be f.name, f.qualified_name, f.file_path */
ASSERT_STR_EQ(r.columns[0], "f.name");
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
ASSERT_STR_EQ(r.rows[0][1], "test.HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* ── Scalar / introspection functions (full-suite Tier 1) ──────── */
TEST(cypher_func_labels) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN labels(f)", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "[\"Function\"]");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_func_type) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function)-[r:CALLS]->(g:Function) RETURN type(r) LIMIT 1", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "CALLS");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_func_id) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN id(f)",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
/* id is a non-empty numeric string */
ASSERT_TRUE(r.rows[0][0][0] >= '0' && r.rows[0][0][0] <= '9');
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_func_keys) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN keys(f)", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_TRUE(strstr(r.rows[0][0], "\"name\"") != NULL);
ASSERT_TRUE(strstr(r.rows[0][0], "\"qualified_name\"") != NULL);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_func_properties) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN properties(f)", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_EQ(r.rows[0][0][0], '{'); /* a JSON object */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_func_tointeger_tofloat) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" "
"RETURN toInteger(f.start_line), toFloat(f.start_line)",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "10"); /* start_line = 10 */
ASSERT_STR_EQ(r.rows[0][1], "10");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_func_size_reverse) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"LogError\" "
"RETURN size(f.name), length(f.name), reverse(f.name)",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "8"); /* "LogError" has 8 chars */
ASSERT_STR_EQ(r.rows[0][1], "8");
ASSERT_STR_EQ(r.rows[0][2], "rorrEgoL");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_func_multiarg) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" "
"RETURN substring(f.name, 0, 6), left(f.name, 6), "
"right(f.name, 5), replace(f.name, \"Order\", \"Req\"), "
"coalesce(f.missing, \"fallback\")",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "Handle"); /* substring("HandleOrder",0,6) */
ASSERT_STR_EQ(r.rows[0][1], "Handle"); /* left(...,6) */
ASSERT_STR_EQ(r.rows[0][2], "Order"); /* right("HandleOrder",5) */
ASSERT_STR_EQ(r.rows[0][3], "HandleReq"); /* replace Order->Req */
ASSERT_STR_EQ(r.rows[0][4], "fallback"); /* coalesce: f.missing empty -> literal */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exists_no_callers) {
/* NOT EXISTS { (f)<-[:CALLS]-() } → functions with no CALLS caller.
* HandleOrder has only an incoming DEFINES edge (not CALLS), so it is the
* sole match — proving EXISTS is edge-type-specific (in_degree=1 here). */
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exists_has_outgoing_calls) {
/* EXISTS { (f)-[:CALLS]->() } → functions that call something.
* HandleOrder (→ValidateOrder, →LogError) and ValidateOrder (→SubmitOrder). */
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE EXISTS { (f)-[:CALLS]->() } RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_calls_relationship) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"RETURN f.name, g.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* HandleOrder→ValidateOrder, HandleOrder→LogError, ValidateOrder→SubmitOrder */
ASSERT_EQ(r.row_count, 3);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_calls_with_where) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"WHERE f.name = \"HandleOrder\" "
"RETURN f.name, g.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2); /* →ValidateOrder, →LogError */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_inbound) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)<-[:CALLS]-(g:Function) "
"WHERE f.name = \"ValidateOrder\" "
"RETURN g.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1); /* HandleOrder calls ValidateOrder */
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_count) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"RETURN f.name, COUNT(g) AS cnt",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* HandleOrder→2, ValidateOrder→1 */
ASSERT_EQ(r.row_count, 2);
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};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.name LIMIT 2", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_order_by) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.name ORDER BY f.name ASC", "test",
0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 4);
/* Alphabetical: HandleOrder, LogError, SubmitOrder, ValidateOrder */
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
ASSERT_STR_EQ(r.rows[1][0], "LogError");
ASSERT_STR_EQ(r.rows[2][0], "SubmitOrder");
ASSERT_STR_EQ(r.rows[3][0], "ValidateOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_variable_length) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* HandleOrder →CALLS→ ValidateOrder →CALLS→ SubmitOrder (2 hops) */
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS*1..3]->(g:Function) "
"WHERE f.name = \"HandleOrder\" "
"RETURN g.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* Should find: ValidateOrder (1 hop), SubmitOrder (2 hops), LogError (1 hop) */
ASSERT_GTE(r.row_count, 3);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* Reproduce-first (#887): an EXPLICIT variable-length upper bound must still be
* capped at the engine ceiling (cbm_cypher_max_depth(), default 10). On
* origin/main, expand_var_length honoured an explicit `*1..N` verbatim (only the
* unbounded `*` / `*..m` forms were capped), so `[:CALLS*1..N]` passed N straight
* to cbm_store_bfs — an unbounded traversal (a DoS on cyclic graphs). RED before
* the clamp: a *1..12 walk over a 13-node chain
* returns all 12 hops (N01..N12). GREEN after: it stops at the depth-10 ceiling
* (N01..N10); N11/N12 are never emitted. max_rows=64 keeps the binding-expansion
* cap (bind_cap*10) well above the hop count, so DEPTH — not the binding cap — is
* the bound under test. */
TEST(cypher_exec_var_length_explicit_bound_capped) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
/* Linear chain N00 -CALLS-> N01 -> ... -> N12 (13 nodes, one node per hop). */
int64_t ids[13];
for (int i = 0; i < 13; i++) {
char name[8];
char qn[24];
snprintf(name, sizeof(name), "N%02d", i);
snprintf(qn, sizeof(qn), "test.N%02d", i);
cbm_node_t n = {.project = "test",
.label = "Function",
.name = name,
.qualified_name = qn,
.file_path = "chain.go"};
ids[i] = cbm_store_upsert_node(s, &n);
}
for (int i = 0; i < 12; i++) {
cbm_edge_t e = {
.project = "test", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"};
cbm_store_insert_edge(s, &e);
}
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a:Function {name: \"N00\"})-[:CALLS*1..12]->"
"(x:Function) RETURN x.name",
"test", 64, &r);
ASSERT_EQ(rc, 0);
/* Capped at 10 hops → exactly N01..N10; N11/N12 are beyond the ceiling. */
ASSERT_EQ(r.row_count, 10);
bool saw_n10 = false;
bool saw_n11 = false;
bool saw_n12 = false;
for (int i = 0; i < r.row_count; i++) {
const char *v = r.rows[i][0];
if (v && strcmp(v, "N10") == 0) {
saw_n10 = true;
}
if (v && strcmp(v, "N11") == 0) {
saw_n11 = true;
}
if (v && strcmp(v, "N12") == 0) {
saw_n12 = true;
}
}
ASSERT_TRUE(saw_n10); /* within the ceiling — proves the traversal really ran */
ASSERT_FALSE(saw_n11); /* clamped away */
ASSERT_FALSE(saw_n12);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_defines_edge) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (m:Module)-[:DEFINES]->(f:Function) "
"RETURN m.name, f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "main");
ASSERT_STR_EQ(r.rows[0][1], "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_no_results) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.name = \"NonExistent\"", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 0);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_numeric) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.start_line > \"8\" "
"RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* HandleOrder starts at 10 */
ASSERT_GTE(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* --- Ported from cypher_test.go: TestExecuteDistinct --- */
TEST(cypher_exec_distinct) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN DISTINCT f.label", "test", 0, &r);
ASSERT_EQ(rc, 0);
/* All 4 Function nodes share label "Function" → 1 distinct row */
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* issue #238: WITH DISTINCT must deduplicate projected rows (previously the
* DISTINCT keyword on WITH was parsed but silently ignored). */
TEST(cypher_exec_with_distinct_issue238) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* 4 Function nodes all share label "Function" → WITH DISTINCT collapses to
* one row; without dedup this returned 4. */
int rc = cbm_cypher_execute(s, "MATCH (f:Function) WITH DISTINCT f.label AS lbl RETURN lbl",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
/* Control: without DISTINCT, all 4 rows flow through. */
cbm_cypher_result_t r2 = {0};
rc = cbm_cypher_execute(s, "MATCH (f:Function) WITH f.label AS lbl RETURN lbl", "test", 0, &r2);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r2.row_count, 4);
cbm_cypher_result_free(&r2);
cbm_store_close(s);
PASS();
}
/* issue #241: label tests in WHERE clauses (openCypher `WHERE n:Label`) —
* previously a parse error. */
TEST(cypher_exec_where_label_test_issue241) {
cbm_store_t *s = setup_cypher_store();
/* f:Function is true for all 4 Function nodes. */
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) WHERE f:Function RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 4);
cbm_cypher_result_free(&r);
/* f:Class matches none of the functions. */
cbm_cypher_result_t r2 = {0};
rc = cbm_cypher_execute(s, "MATCH (f:Function) WHERE f:Class RETURN f.name", "test", 0, &r2);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r2.row_count, 0);
cbm_cypher_result_free(&r2);
/* Negated label test: NOT f:Class is always true. */
cbm_cypher_result_t r3 = {0};
rc =
cbm_cypher_execute(s, "MATCH (f:Function) WHERE NOT f:Class RETURN f.name", "test", 0, &r3);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r3.row_count, 4);
cbm_cypher_result_free(&r3);
cbm_store_close(s);
PASS();
}
/* issue #239: COUNT(DISTINCT x) — previously a parse error. */
TEST(cypher_exec_count_distinct_issue239) {
cbm_store_t *s = setup_cypher_store();
/* 4 functions all share label "Function" → COUNT(DISTINCT f.label) = 1. */
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) RETURN count(DISTINCT f.label)", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "1");
cbm_cypher_result_free(&r);
/* Non-distinct COUNT counts all 4 occurrences. */
cbm_cypher_result_t r2 = {0};
rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN count(f.label)", "test", 0, &r2);
ASSERT_EQ(rc, 0);
ASSERT_STR_EQ(r2.rows[0][0], "4");
cbm_cypher_result_free(&r2);
/* DISTINCT over the 4 unique function names = 4. */
cbm_cypher_result_t r3 = {0};
rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN count(DISTINCT f.name)", "test", 0, &r3);
ASSERT_EQ(rc, 0);
ASSERT_STR_EQ(r3.rows[0][0], "4");
cbm_cypher_result_free(&r3);
cbm_store_close(s);
PASS();
}
/* issue #373: an unsupported computed expression in WITH/RETURN (an unknown
* function like split(...) or list indexing [..]) must FAIL LOUDLY with a clear
* "unsupported function" error rather than silently projecting an empty column
* (which looks like a valid-but-blank result and hides the real problem). */
TEST(cypher_exec_unsupported_func_errors_issue373) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WITH split(f.name)[0] AS top, count(*) AS c RETURN top, c", "test",
0, &r);
ASSERT_TRUE(rc != 0); /* unsupported function now fails loudly */
ASSERT_NOT_NULL(r.error);
ASSERT_TRUE(strstr(r.error, "unsupported") != NULL);
ASSERT_TRUE(strstr(r.error, "split") != NULL);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* A recognised function still works, and an unknown one in plain RETURN errors. */
TEST(cypher_exec_unknown_func_return_errors) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN nosuchfunc(f.name)", "test", 0, &r);
ASSERT_TRUE(rc != 0);
ASSERT_NOT_NULL(r.error);
ASSERT_TRUE(strstr(r.error, "unsupported function") != NULL);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* issue #242: openCypher label alternation in MATCH — (n:A|B). */
TEST(cypher_exec_label_alternation_issue242) {
cbm_store_t *s = setup_cypher_store();
/* Store has 4 Function + 1 Module node → alternation seeds all 5. */
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (n:Function|Module) RETURN n.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 5);
cbm_cypher_result_free(&r);
/* Alternation with a non-existent label still returns the existing one. */
cbm_cypher_result_t r2 = {0};
rc = cbm_cypher_execute(s, "MATCH (n:Function|Class) RETURN n.name", "test", 0, &r2);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r2.row_count, 4);
cbm_cypher_result_free(&r2);
cbm_store_close(s);
PASS();
}
/* --- Ported from cypher_test.go: TestExecuteInlinePropertyFilter --- */
TEST(cypher_exec_inline_props) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function {name: \"SubmitOrder\"}) "
"RETURN f.name, f.qualified_name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* --- Ported from cypher_test.go: TestParseWhereStartsWith --- */
TEST(cypher_parse_where_starts_with) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("MATCH (f:Function) WHERE f.name STARTS WITH \"Send\" RETURN f", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_NOT_NULL(q->where);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_CONDITION);
ASSERT_STR_EQ(q->where->root->cond.op, "STARTS WITH");
ASSERT_STR_EQ(q->where->root->cond.value, "Send");
cbm_query_free(q);
PASS();
}
/* --- Ported from cypher_test.go: TestParseWhereContains --- */
TEST(cypher_parse_where_contains) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("MATCH (f:Function) WHERE f.name CONTAINS \"Handler\" RETURN f", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_NOT_NULL(q->where);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_CONDITION);
ASSERT_STR_EQ(q->where->root->cond.op, "CONTAINS");
ASSERT_STR_EQ(q->where->root->cond.value, "Handler");
cbm_query_free(q);
PASS();
}
/* --- Ported from cypher_test.go: TestParseWhereNumericComparison --- */
TEST(cypher_parse_where_numeric) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function) WHERE f.start_line > 10 RETURN f", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q);
ASSERT_NOT_NULL(q->where);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_CONDITION);
ASSERT_STR_EQ(q->where->root->cond.op, ">");
ASSERT_STR_EQ(q->where->root->cond.value, "10");
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* EDGE PROPERTY TESTS (ported from cypher_test.go Feature 2)
* ══════════════════════════════════════════════════════════════════ */
/* Helper: set up store with HTTP_CALLS edge having properties.
* Creates same graph as setup_cypher_store + one HTTP_CALLS edge. */
static cbm_store_t *setup_cypher_http_store(void) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t n1 = {.project = "test",
.label = "Function",
.name = "HandleOrder",
.qualified_name = "test.main.HandleOrder",
.file_path = "main.go",
.start_line = 10,
.end_line = 30};
cbm_node_t n2 = {.project = "test",
.label = "Function",
.name = "ValidateOrder",
.qualified_name = "test.service.ValidateOrder",
.file_path = "service.go",
.start_line = 5,
.end_line = 20};
cbm_node_t n3 = {.project = "test",
.label = "Function",
.name = "SubmitOrder",
.qualified_name = "test.service.SubmitOrder",
.file_path = "service.go",
.start_line = 25,
.end_line = 50};
int64_t id1 = cbm_store_upsert_node(s, &n1);
cbm_store_upsert_node(s, &n2);
int64_t id3 = cbm_store_upsert_node(s, &n3);
cbm_edge_t http = {
.project = "test",
.source_id = id1,
.target_id = id3,
.type = "HTTP_CALLS",
.properties_json =
"{\"url_path\":\"/api/orders\",\"confidence\":0.85,\"method\":\"POST\"}"};
cbm_store_insert_edge(s, &http);
return s;
}
/* Helper: set up store with TWO HTTP_CALLS edges for filtering tests. */
static cbm_store_t *setup_cypher_multi_edge_store(void) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "testproj", "/tmp/test");
cbm_node_t n1 = {.project = "testproj",
.label = "Function",
.name = "SendOrder",
.qualified_name = "testproj.caller.SendOrder",
.file_path = "caller/client.go"};
cbm_node_t n2 = {.project = "testproj",
.label = "Function",
.name = "HandleOrder",
.qualified_name = "testproj.handler.HandleOrder",
.file_path = "handler/routes.go"};
cbm_node_t n3 = {.project = "testproj",
.label = "Function",
.name = "HandleHealth",
.qualified_name = "testproj.handler.HandleHealth",
.file_path = "handler/health.go"};
int64_t id1 = cbm_store_upsert_node(s, &n1);
int64_t id2 = cbm_store_upsert_node(s, &n2);
int64_t id3 = cbm_store_upsert_node(s, &n3);
cbm_edge_t e1 = {.project = "testproj",
.source_id = id1,
.target_id = id2,
.type = "HTTP_CALLS",
.properties_json =
"{\"url_path\":\"/api/orders\",\"confidence\":0.85,\"method\":\"POST\"}"};
cbm_edge_t e2 = {.project = "testproj",
.source_id = id1,
.target_id = id3,
.type = "HTTP_CALLS",
.properties_json = "{\"url_path\":\"/health\",\"confidence\":0.45}"};
cbm_store_insert_edge(s, &e1);
cbm_store_insert_edge(s, &e2);
return s;
}
/* Helper: find a column value in a cypher result row */
static const char *cypher_get_col(const cbm_cypher_result_t *r, int row, const char *col) {
for (int c = 0; c < r->col_count; c++) {
if (strcmp(r->columns[c], col) == 0)
return r->rows[row][c];
}
return NULL;
}
/* Helper: check if any row has a column matching a value */
static bool cypher_has_row_with(const cbm_cypher_result_t *r, const char *col, const char *val) {
int ci = -1;
for (int c = 0; c < r->col_count; c++) {
if (strcmp(r->columns[c], col) == 0) {
ci = c;
break;
}
}
if (ci < 0)
return false;
for (int row = 0; row < r->row_count; row++) {
if (strcmp(r->rows[row][ci], val) == 0)
return true;
}
return false;
}
TEST(cypher_edge_prop_access) {
cbm_store_t *s = setup_cypher_http_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a:Function)-[r:HTTP_CALLS]->(b:Function) "
"RETURN a.name, b.name, r.url_path, r.confidence",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "a.name"), "HandleOrder");
ASSERT_STR_EQ(cypher_get_col(&r, 0, "b.name"), "SubmitOrder");
ASSERT_STR_EQ(cypher_get_col(&r, 0, "r.url_path"), "/api/orders");
ASSERT_STR_EQ(cypher_get_col(&r, 0, "r.confidence"), "0.85");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_prop_in_where) {
cbm_store_t *s = setup_cypher_http_store();
cbm_cypher_result_t r = {0};
/* confidence > 0.8 → should match */
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.confidence > 0.8 "
"RETURN a.name, b.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
/* confidence > 0.9 → should NOT match */
memset(&r, 0, sizeof(r));
rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.confidence > 0.9 "
"RETURN a.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 0);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_type_prop) {
cbm_store_t *s = setup_cypher_http_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (a)-[r:HTTP_CALLS]->(b) RETURN r.type", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "r.type"), "HTTP_CALLS");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_filter_contains) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.url_path CONTAINS 'orders' "
"RETURN a.name, b.name, r.url_path",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "a.name"), "SendOrder");
ASSERT_STR_EQ(cypher_get_col(&r, 0, "b.name"), "HandleOrder");
ASSERT_STR_EQ(cypher_get_col(&r, 0, "r.url_path"), "/api/orders");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_filter_numeric_gte) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.confidence >= 0.6 "
"RETURN a.name, b.name, r.confidence LIMIT 20",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "b.name"), "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_bare_edge_return_exposes_properties_json) {
/* `RETURN r` on an edge variable, with no property accessor, should
* surface the edge's full properties JSON (or "{}"). Before the fix,
* binding_get_virtual returned an empty string, which made bare edge
* returns useless for callers that wanted to inspect timestamps,
* weights, etc. without naming each property up front. */
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.method = 'POST' RETURN r",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
const char *r_val = cypher_get_col(&r, 0, "r");
ASSERT_NOT_NULL(r_val);
/* Expect JSON object content rather than the previous empty string. */
ASSERT_NOT_NULL(strstr(r_val, "url_path"));
ASSERT_NOT_NULL(strstr(r_val, "/api/orders"));
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_return_without_filter) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) "
"RETURN a.name, b.name, r.url_path, r.confidence LIMIT 20",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.row_count, 2);
ASSERT(cypher_has_row_with(&r, "r.url_path", "/api/orders"));
ASSERT(cypher_has_row_with(&r, "r.url_path", "/health"));
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_filter_equals) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.method = 'POST' "
"RETURN a.name, b.name",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "b.name"), "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_filter_starts_with) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.url_path STARTS WITH '/api' "
"RETURN a.name, b.name, r.url_path",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "r.url_path"), "/api/orders");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_combined_node_and_edge_filter) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a:Function)-[r:HTTP_CALLS]->(b:Function) "
"WHERE a.name = 'SendOrder' AND r.confidence >= 0.6 "
"RETURN b.name, r.url_path",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "b.name"), "HandleOrder");
ASSERT_STR_EQ(cypher_get_col(&r, 0, "r.url_path"), "/api/orders");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_filter_no_match) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
/* No edge has method = 'DELETE' */
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.method = 'DELETE' "
"RETURN a.name",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 0);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_filter_numeric_lt) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
/* Only health edge (0.45) should match confidence < 0.5 */
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.confidence < 0.5 "
"RETURN b.name, r.confidence",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "b.name"), "HandleHealth");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_filter_regex) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r:HTTP_CALLS]->(b) WHERE r.url_path =~ \"/api/.*\" "
"RETURN b.name",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(cypher_get_col(&r, 0, "b.name"), "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_edge_builtin_type_filter) {
cbm_store_t *s = setup_cypher_multi_edge_store();
cbm_cypher_result_t r = {0};
/* Untyped rel [r] — filter on r.type in WHERE */
int rc = cbm_cypher_execute(s,
"MATCH (a)-[r]->(b) WHERE r.type = 'HTTP_CALLS' "
"RETURN a.name, b.name LIMIT 20",
"testproj", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2); /* Both HTTP_CALLS edges */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* Ported from cypher_test.go: TestApplyLimitRespectsExplicit */
TEST(cypher_apply_limit) {
/* Create store with many nodes */
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "lim", "/tmp/lim");
for (int i = 0; i < 50; i++) {
char name[32], qn[64];
snprintf(name, sizeof(name), "func%d", i);
snprintf(qn, sizeof(qn), "lim.func%d", i);
cbm_node_t n = {.project = "lim",
.label = "Function",
.name = name,
.qualified_name = qn,
.file_path = "test.go"};
cbm_store_upsert_node(s, &n);
}
/* LIMIT 5 → 5 rows */
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.name LIMIT 5", "lim", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 5);
cbm_cypher_result_free(&r);
/* No LIMIT, max_rows=10 → capped at 10 */
memset(&r, 0, sizeof(r));
rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.name", "lim", 10, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 10);
cbm_cypher_result_free(&r);
/* LIMIT above max_rows → explicit limit wins */
memset(&r, 0, sizeof(r));
rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.name LIMIT 30", "lim", 10, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 30);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 1: SIMPLE OPERATORS
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_lex_neq_operators) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("<> !=", &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.count, 2);
ASSERT_EQ(r.tokens[0].type, TOK_NEQ);
ASSERT_EQ(r.tokens[1].type, TOK_NEQ);
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_ends_keyword) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("ENDS WITH", &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.count, 2);
ASSERT_EQ(r.tokens[0].type, TOK_ENDS);
ASSERT_EQ(r.tokens[1].type, TOK_WITH);
cbm_lex_free(&r);
PASS();
}
TEST(cypher_lex_in_is_null) {
cbm_lex_result_t r = {0};
int rc = cbm_lex("IN IS NULL", &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.count, 3);
ASSERT_EQ(r.tokens[0].type, TOK_IN);
ASSERT_EQ(r.tokens[1].type, TOK_IS);
ASSERT_EQ(r.tokens[2].type, TOK_NULL_KW);
cbm_lex_free(&r);
PASS();
}
TEST(cypher_exec_where_neq) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name <> \"HandleOrder\" RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 3); /* ValidateOrder, SubmitOrder, LogError */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_neq_bang) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name != \"HandleOrder\" RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 3);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_ends_with) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name ENDS WITH \"Order\" RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
/* HandleOrder, ValidateOrder, SubmitOrder */
ASSERT_EQ(r.row_count, 3);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_not) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE NOT f.name = \"HandleOrder\" RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 3);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_in) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f) WHERE f.label IN [\"Function\", \"Module\"] RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 5); /* 4 Functions + 1 Module */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_not_in) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f) WHERE NOT f.label IN [\"Module\"] RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 4); /* 4 Functions only */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_is_null) {
/* SubmitOrder has no start_line (defaults to 0, so start_line prop = "0") */
/* But file_path is set for all. Use a node with missing data. */
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t n1 = {.project = "test",
.label = "Function",
.name = "WithFile",
.qualified_name = "test.WithFile",
.file_path = "a.go"};
cbm_node_t n2 = {.project = "test",
.label = "Function",
.name = "NoFile",
.qualified_name = "test.NoFile",
.file_path = NULL};
cbm_store_upsert_node(s, &n1);
cbm_store_upsert_node(s, &n2);
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.file_path IS NULL RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1); /* NoFile has NULL file_path */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_is_not_null) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t n1 = {.project = "test",
.label = "Function",
.name = "WithFile",
.qualified_name = "test.WithFile",
.file_path = "a.go"};
cbm_node_t n2 = {.project = "test",
.label = "Function",
.name = "NoFile",
.qualified_name = "test.NoFile",
.file_path = NULL};
cbm_store_upsert_node(s, &n1);
cbm_store_upsert_node(s, &n2);
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) WHERE f.file_path IS NOT NULL RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1); /* WithFile */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_return_star) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN * LIMIT 3", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 3);
/* Should have columns: f.name, f.qualified_name, f.label, f.file_path */
ASSERT_EQ(r.col_count, 4);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_parse_neq) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f:Function) WHERE f.name <> \"X\"", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->where);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_CONDITION);
ASSERT_STR_EQ(q->where->root->cond.op, "<>");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_in) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f) WHERE f.label IN [\"Function\", \"Module\"]", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_CONDITION);
ASSERT_STR_EQ(q->where->root->cond.op, "IN");
ASSERT_EQ(q->where->root->cond.in_value_count, 2);
ASSERT_STR_EQ(q->where->root->cond.in_values[0], "Function");
ASSERT_STR_EQ(q->where->root->cond.in_values[1], "Module");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_is_null) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f) WHERE f.file_path IS NULL", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->where->root);
ASSERT_STR_EQ(q->where->root->cond.op, "IS NULL");
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 2: EXPRESSION TREE
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_exec_where_or) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" OR f.name = \"LogError\" RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_complex_bool) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* (name CONTAINS "Order" OR name = "LogError") AND label = "Function" */
int rc = cbm_cypher_execute(s,
"MATCH (f) WHERE (f.name CONTAINS \"Order\" OR f.name = "
"\"LogError\") AND f.label = \"Function\" "
"RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 4); /* HandleOrder, ValidateOrder, SubmitOrder, LogError */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_xor) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* name CONTAINS "Handle" XOR name CONTAINS "Order" → XOR = true when exactly one is true
* HandleOrder: both true → false
* ValidateOrder: false, true → true
* SubmitOrder: false, true → true
* LogError: false, false → false */
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name CONTAINS \"Handle\" XOR f.name "
"CONTAINS \"Order\" RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2); /* ValidateOrder, SubmitOrder */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_where_not_prefix) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE NOT (f.name CONTAINS \"Order\") RETURN f.name", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1); /* LogError */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_parse_expr_tree_and_or) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("MATCH (f) WHERE f.a = \"1\" AND f.b = \"2\" OR f.c = \"3\"", &q, &err);
ASSERT_EQ(rc, 0);
/* Precedence: AND binds tighter than OR → root is OR */
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_OR);
ASSERT_EQ(q->where->root->left->type, EXPR_AND);
ASSERT_EQ(q->where->root->right->type, EXPR_CONDITION);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_expr_tree_nested) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("MATCH (f) WHERE (f.a = \"1\" OR f.b = \"2\") AND f.c = \"3\"", &q, &err);
ASSERT_EQ(rc, 0);
/* Parens override precedence: root is AND, left is OR */
ASSERT_NOT_NULL(q->where->root);
ASSERT_EQ(q->where->root->type, EXPR_AND);
ASSERT_EQ(q->where->root->left->type, EXPR_OR);
ASSERT_EQ(q->where->root->right->type, EXPR_CONDITION);
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 3: UNSUPPORTED KEYWORD ERRORS
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_error_create) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("CREATE (n:Node {name: \"X\"})", &q, &err);
ASSERT_EQ(rc, -1);
ASSERT_NOT_NULL(err);
ASSERT(strstr(err, "CREATE") != NULL);
free(err);
PASS();
}
TEST(cypher_error_delete) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("DELETE n", &q, &err);
ASSERT_EQ(rc, -1);
ASSERT_NOT_NULL(err);
ASSERT(strstr(err, "DELETE") != NULL);
free(err);
PASS();
}
TEST(cypher_error_set) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("SET n.name = \"X\"", &q, &err);
ASSERT_EQ(rc, -1);
ASSERT_NOT_NULL(err);
ASSERT(strstr(err, "SET") != NULL);
free(err);
PASS();
}
TEST(cypher_error_merge) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MERGE (n:Node)", &q, &err);
ASSERT_EQ(rc, -1);
ASSERT_NOT_NULL(err);
ASSERT(strstr(err, "MERGE") != NULL);
free(err);
PASS();
}
TEST(cypher_error_call) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("CALL db.labels()", &q, &err);
ASSERT_EQ(rc, -1);
ASSERT_NOT_NULL(err);
ASSERT(strstr(err, "CALL") != NULL);
free(err);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 4: SKIP + GENERALIZED AGGREGATION
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_exec_skip) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.name ORDER BY f.name ASC SKIP 2",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* 4 functions ordered: HandleOrder, LogError, SubmitOrder, ValidateOrder → skip 2 = 2 */
ASSERT_EQ(r.row_count, 2);
ASSERT_STR_EQ(r.rows[0][0], "SubmitOrder");
ASSERT_STR_EQ(r.rows[1][0], "ValidateOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_skip_limit) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) RETURN f.name ORDER BY f.name ASC SKIP 1 LIMIT 2", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2);
ASSERT_STR_EQ(r.rows[0][0], "LogError");
ASSERT_STR_EQ(r.rows[1][0], "SubmitOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_sum) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* start_lines: HandleOrder=10, ValidateOrder=5, SubmitOrder=0, LogError=0 → sum=15 */
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN SUM(f.start_line) AS total", "test",
0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "15");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_avg) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* start_lines: 10, 5, 0, 0 → avg = 3.75 */
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN AVG(f.start_line) AS avg_line",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "3.75");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_min) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* Among functions with nonzero: HandleOrder=10, ValidateOrder=5 → but MIN is 0 from others */
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) RETURN MIN(f.start_line) AS mn", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "0");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_max) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (f:Function) RETURN MAX(f.start_line) AS mx", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "10");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_collect) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"WHERE f.name = \"HandleOrder\" "
"RETURN f.name, COLLECT(g.name) AS callees",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
/* Should be a JSON array like ["ValidateOrder","LogError"] */
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
ASSERT(strstr(r.rows[0][1], "ValidateOrder") != NULL);
ASSERT(strstr(r.rows[0][1], "LogError") != NULL);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_count_star) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN COUNT(*) AS n", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "4");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_parse_skip) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f) RETURN f.name SKIP 5 LIMIT 10", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->ret);
ASSERT_EQ(q->ret->skip, 5);
ASSERT_EQ(q->ret->limit, 10);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_sum_avg) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f) RETURN SUM(f.x) AS s, AVG(f.y) AS a", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_EQ(q->ret->count, 2);
ASSERT_STR_EQ(q->ret->items[0].func, "SUM");
ASSERT_STR_EQ(q->ret->items[0].alias, "s");
ASSERT_STR_EQ(q->ret->items[1].func, "AVG");
ASSERT_STR_EQ(q->ret->items[1].alias, "a");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_collect) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f)-[:CALLS]->(g) RETURN f.name, COLLECT(g.name) AS names", &q,
&err);
ASSERT_EQ(rc, 0);
ASSERT_EQ(q->ret->count, 2);
ASSERT_STR_EQ(q->ret->items[1].func, "COLLECT");
ASSERT_STR_EQ(q->ret->items[1].alias, "names");
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 5: STRING FUNCTIONS + CASE
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_exec_tolower) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN toLower(f.name) AS lower_name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "handleorder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_toupper) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN toUpper(f.name) AS upper_name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "HANDLEORDER");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_tostring) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN toString(f.start_line) AS sl",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "10");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_case) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" "
"RETURN CASE WHEN f.start_line > \"5\" THEN \"high\" ELSE \"low\" END AS pos",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "high");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_parse_tolower) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f) RETURN toLower(f.name) AS n", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_STR_EQ(q->ret->items[0].func, "toLower");
ASSERT_STR_EQ(q->ret->items[0].alias, "n");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_case) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse(
"MATCH (f) RETURN CASE WHEN f.x = \"1\" THEN \"a\" ELSE \"b\" END AS val", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->ret->items[0].kase);
ASSERT_EQ(q->ret->items[0].kase->branch_count, 1);
ASSERT_STR_EQ(q->ret->items[0].kase->branches[0].then_val, "a");
ASSERT_STR_EQ(q->ret->items[0].kase->else_val, "b");
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 6: WITH CLAUSE
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_exec_with_rename) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" "
"WITH f.name AS fname RETURN fname",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_with_count) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"WITH f.name AS caller, COUNT(g) AS cnt "
"RETURN caller, cnt ORDER BY cnt DESC",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_GTE(r.row_count, 1);
/* HandleOrder calls 2 (ValidateOrder, LogError), ValidateOrder calls 1 (SubmitOrder) */
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
ASSERT_STR_EQ(r.rows[0][1], "2");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* Regression: a bare node group-var carried through WITH aggregation must project
* its real properties (not blank). Pre-fix, the carried var held only the node
* name, so RETURN g.file_path returned "". */
TEST(cypher_exec_with_node_groupvar_prop) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"WHERE g.name = \"ValidateOrder\" "
"WITH g, COUNT(*) AS c "
"RETURN g.file_path, g.name, c",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "validate.go"); /* was "" before the fix */
ASSERT_STR_EQ(r.rows[0][1], "ValidateOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_with_where) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"WITH f.name AS caller, COUNT(g) AS cnt "
"WHERE cnt > \"1\" "
"RETURN caller, cnt",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* Only HandleOrder has cnt > 1 (cnt=2) */
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_with_orderby_limit) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function)-[:CALLS]->(g:Function) "
"WITH f.name AS caller, COUNT(g) AS cnt "
"ORDER BY cnt DESC LIMIT 1 "
"RETURN caller, cnt",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "HandleOrder");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_parse_with) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse(
"MATCH (f)-[:CALLS]->(g) WITH f.name AS caller, COUNT(g) AS cnt RETURN caller, cnt", &q,
&err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->with_clause);
ASSERT_EQ(q->with_clause->count, 2);
ASSERT_STR_EQ(q->with_clause->items[0].alias, "caller");
ASSERT_STR_EQ(q->with_clause->items[1].func, "COUNT");
ASSERT_NOT_NULL(q->ret);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_with_where) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (f)-[:CALLS]->(g) WITH f.name AS caller, COUNT(g) AS cnt "
"WHERE cnt > \"1\" RETURN caller",
&q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->with_clause);
ASSERT_NOT_NULL(q->post_with_where);
ASSERT_NOT_NULL(q->post_with_where->root);
ASSERT_NOT_NULL(q->ret);
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 7: OPTIONAL MATCH + MULTIPLE MATCH
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_exec_optional_match_no_result) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* LogError has no CALLS outbound edges → OPTIONAL MATCH keeps binding with empty target */
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"LogError\" "
"OPTIONAL MATCH (f)-[:CALLS]->(g:Function) "
"RETURN f.name, g.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "LogError");
/* g.name should be empty since OPTIONAL MATCH found nothing */
ASSERT_STR_EQ(r.rows[0][1], "");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_optional_match_has_result) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" "
"OPTIONAL MATCH (f)-[:CALLS]->(g:Function) "
"RETURN f.name, g.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 2); /* ValidateOrder, LogError */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_multi_match) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
/* Two MATCH clauses: first finds a module, second finds functions */
int rc =
cbm_cypher_execute(s,
"MATCH (m:Module) MATCH (f:Function) WHERE f.name CONTAINS \"Order\" "
"RETURN m.name, f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* 1 module × 3 *Order functions = 3 */
ASSERT_EQ(r.row_count, 3);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_parse_optional_match) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse(
"MATCH (f:Function) OPTIONAL MATCH (f)-[:CALLS]->(g) RETURN f.name, g.name", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_EQ(q->pattern_count, 2);
ASSERT(!q->pattern_optional[0]);
ASSERT(q->pattern_optional[1]);
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_multi_match) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("MATCH (a:Module) MATCH (b:Function) RETURN a.name, b.name", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_EQ(q->pattern_count, 2);
ASSERT(!q->pattern_optional[0]);
ASSERT(!q->pattern_optional[1]);
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 8: UNION / UNION ALL
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_exec_union) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN f.name "
"UNION "
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* UNION deduplicates → 1 row */
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_exec_union_all) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN f.name "
"UNION ALL "
"MATCH (f:Function) WHERE f.name = \"HandleOrder\" RETURN f.name",
"test", 0, &r);
ASSERT_EQ(rc, 0);
/* UNION ALL keeps duplicates → 2 rows */
ASSERT_EQ(r.row_count, 2);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
TEST(cypher_parse_union) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("MATCH (f) RETURN f.name UNION ALL MATCH (g) RETURN g.name", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT(q->union_all);
ASSERT_NOT_NULL(q->union_next);
cbm_query_free(q);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* PHASE 9: UNWIND
* ══════════════════════════════════════════════════════════════════ */
TEST(cypher_parse_unwind) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc =
cbm_cypher_parse("UNWIND [\"a\", \"b\", \"c\"] AS x MATCH (f) RETURN f.name", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_NOT_NULL(q->unwind_expr);
ASSERT_STR_EQ(q->unwind_alias, "x");
cbm_query_free(q);
PASS();
}
TEST(cypher_parse_unwind_var) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("UNWIND items AS item MATCH (f) RETURN f.name", &q, &err);
ASSERT_EQ(rc, 0);
ASSERT_STR_EQ(q->unwind_expr, "items");
ASSERT_STR_EQ(q->unwind_alias, "item");
cbm_query_free(q);
PASS();
}
/* ── Issue #389 group: Cypher feature reproductions ─────────────────
* Each asserts the CORRECT behavior; a failure reproduces the bug. */
/* #240: labels() function */
TEST(cypher_issue240_labels_function) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (n:Module) RETURN labels(n) AS lbl", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #237: DISTINCT applied before ORDER BY + LIMIT */
TEST(cypher_issue237_distinct_order_limit) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (f:Function) RETURN DISTINCT f.label AS l ORDER BY l LIMIT 10", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #873: duplicate projected rows must be deduped before ORDER BY + LIMIT */
TEST(cypher_issue873_distinct_order_limit_dedupes_before_limit) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (n) RETURN DISTINCT n.label AS label ORDER BY label LIMIT 2", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
ASSERT_EQ(r.row_count, 2);
ASSERT_STR_EQ(r.rows[0][0], "Function");
ASSERT_STR_EQ(r.rows[1][0], "Module");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #873: early LIMIT must not truncate rows before DISTINCT for simple RETURN */
TEST(cypher_issue873_distinct_limit_dedupes_before_limit) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc =
cbm_cypher_execute(s, "MATCH (n) RETURN DISTINCT n.label AS label LIMIT 2", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
ASSERT_EQ(r.row_count, 2);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #873: SKIP is applied after DISTINCT and ORDER BY, not before dedupe */
TEST(cypher_issue873_distinct_order_skip_limit_dedupes_before_skip) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(
s, "MATCH (n) RETURN DISTINCT n.label AS label ORDER BY label SKIP 1 LIMIT 1", "test", 0,
&r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
ASSERT_EQ(r.row_count, 1);
ASSERT_STR_EQ(r.rows[0][0], "Module");
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #252: toInteger() */
TEST(cypher_issue252_tointeger) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN toInteger(f.start_line) AS ln",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #305: count(*) + AS alias */
TEST(cypher_issue305_count_star_alias) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, "MATCH (n) RETURN count(*) AS total", "test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_NULL(r.error);
ASSERT_EQ(r.row_count, 1);
cbm_cypher_result_free(&r);
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
* key is matched in the JSON, `loop_depth` must not be confused with its suffix
* `transitive_loop_depth`. Exercises the bottleneck metrics end-to-end. */
TEST(cypher_multi_prop_projection_no_alias) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t n = {.project = "test",
.label = "Function",
.name = "Hot",
.qualified_name = "test.Hot",
.file_path = "hot.go",
.start_line = 10,
.end_line = 42,
.properties_json = "{\"complexity\":3,\"cognitive\":7,\"loop_count\":2,"
"\"loop_depth\":1,\"self_recursive\":false,"
"\"transitive_loop_depth\":5,\"recursive\":true}"};
cbm_store_upsert_node(s, &n);
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s,
"MATCH (f:Function) RETURN f.loop_depth, f.transitive_loop_depth, "
"f.cognitive, f.complexity, f.start_line, f.end_line",
"test", 0, &r);
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, 1);
ASSERT_EQ(r.col_count, 6);
ASSERT_STR_EQ(r.rows[0][0], "1"); /* loop_depth — NOT the suffix transitive_loop_depth */
ASSERT_STR_EQ(r.rows[0][1], "5"); /* transitive_loop_depth */
ASSERT_STR_EQ(r.rows[0][2], "7"); /* cognitive */
ASSERT_STR_EQ(r.rows[0][3], "3"); /* complexity */
ASSERT_STR_EQ(r.rows[0][4], "10"); /* start_line (computed) */
ASSERT_STR_EQ(r.rows[0][5], "42"); /* end_line (computed) — distinct from start_line */
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* Result projection writes into fixed-width per-row stack arrays
* (vals[CBM_SZ_32] / func_bufs[CBM_SZ_32][…] in execute_return_simple and its
* siblings), indexed by the parsed RETURN item count. The parser must bound
* that count to the array width; an over-wide RETURN has to be rejected, not
* allowed to run and write past the arrays. Drive a >32-column RETURN in a
* forked child so a stack overwrite (ASan abort, or a raw segfault) shows up
* as a killing signal instead of taking down the whole runner; the bounded
* path returns an ordinary error and the child exits cleanly. */
TEST(cypher_wide_return_projection_bounded) {
#ifdef _WIN32
SKIP_PLATFORM("fork crash-isolation is POSIX-only; the parse-time bound is platform-agnostic");
#else
char query[4096];
int off = snprintf(query, sizeof(query), "MATCH (f:Function) RETURN ");
for (int i = 0; i < 48; i++) { /* 48 > CBM_SZ_32 (32) fixed columns */
off += snprintf(query + off, sizeof(query) - (size_t)off, "%sf.p%d", i ? ", " : "", i);
}
fflush(NULL);
pid_t pid = fork();
if (pid == 0) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
(void)cbm_cypher_execute(s, query, "test", 0, &r);
cbm_cypher_result_free(&r);
cbm_store_close(s);
_exit(0); /* reached only when execution did NOT overflow */
}
ASSERT_TRUE(pid > 0);
int status = 0;
(void)waitpid(pid, &status, 0);
if (WIFSIGNALED(status)) {
char msg[96];
snprintf(msg, sizeof(msg), "wide RETURN killed by signal %d — projection stack overflow",
WTERMSIG(status));
FAIL(msg);
}
ASSERT_TRUE(WIFEXITED(status));
PASS();
#endif
}
/* ══════════════════════════════════════════════════════════════════ */
SUITE(cypher) {
/* Lexer */
RUN_TEST(cypher_lex_simple_match);
RUN_TEST(cypher_lex_relationship);
RUN_TEST(cypher_lex_string_literal);
RUN_TEST(cypher_lex_single_quote_string);
RUN_TEST(cypher_lex_string_overflow);
RUN_TEST(cypher_lex_number);
RUN_TEST(cypher_lex_operators);
RUN_TEST(cypher_lex_keywords_case_insensitive);
RUN_TEST(cypher_lex_pipe_and_star);
RUN_TEST(cypher_lex_full_query);
/* Parser */
RUN_TEST(cypher_parse_simple_node);
RUN_TEST(cypher_parse_relationship_outbound);
RUN_TEST(cypher_parse_relationship_inbound);
RUN_TEST(cypher_parse_relationship_any);
RUN_TEST(cypher_parse_variable_length);
RUN_TEST(cypher_parse_variable_length_unbounded);
RUN_TEST(cypher_parse_multiple_edge_types);
RUN_TEST(cypher_parse_where_clause);
RUN_TEST(cypher_parse_where_regex);
RUN_TEST(cypher_parse_where_and);
RUN_TEST(cypher_parse_return_simple);
RUN_TEST(cypher_parse_return_count);
RUN_TEST(cypher_parse_return_order_limit);
RUN_TEST(cypher_parse_return_distinct);
RUN_TEST(cypher_parse_inline_props);
RUN_TEST(cypher_parse_error);
/* Execution */
RUN_TEST(cypher_exec_match_all_functions);
RUN_TEST(cypher_issue240_labels_function);
RUN_TEST(cypher_issue237_distinct_order_limit);
RUN_TEST(cypher_issue873_distinct_order_limit_dedupes_before_limit);
RUN_TEST(cypher_issue873_distinct_limit_dedupes_before_limit);
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_exec_where_eq);
RUN_TEST(cypher_exec_varlength_path_semantics_issue797);
RUN_TEST(cypher_exec_where_coalesce_issue874);
RUN_TEST(cypher_exec_where_regex);
RUN_TEST(cypher_exec_where_contains);
RUN_TEST(cypher_exec_where_starts_with);
RUN_TEST(cypher_exec_return_properties);
RUN_TEST(cypher_func_labels);
RUN_TEST(cypher_func_type);
RUN_TEST(cypher_func_id);
RUN_TEST(cypher_func_keys);
RUN_TEST(cypher_func_properties);
RUN_TEST(cypher_func_tointeger_tofloat);
RUN_TEST(cypher_func_size_reverse);
RUN_TEST(cypher_func_multiarg);
RUN_TEST(cypher_multi_prop_projection_no_alias);
RUN_TEST(cypher_exists_no_callers);
RUN_TEST(cypher_exists_has_outgoing_calls);
RUN_TEST(cypher_exec_calls_relationship);
RUN_TEST(cypher_exec_calls_with_where);
RUN_TEST(cypher_exec_inbound);
RUN_TEST(cypher_exec_count);
RUN_TEST(cypher_exec_limit);
RUN_TEST(cypher_exec_order_by);
RUN_TEST(cypher_exec_variable_length);
RUN_TEST(cypher_exec_var_length_explicit_bound_capped);
RUN_TEST(cypher_exec_defines_edge);
RUN_TEST(cypher_exec_no_results);
RUN_TEST(cypher_exec_where_numeric);
/* Go test ports */
RUN_TEST(cypher_exec_distinct);
RUN_TEST(cypher_exec_with_distinct_issue238);
RUN_TEST(cypher_exec_where_label_test_issue241);
RUN_TEST(cypher_exec_label_alternation_issue242);
RUN_TEST(cypher_exec_count_distinct_issue239);
RUN_TEST(cypher_exec_unsupported_func_errors_issue373);
RUN_TEST(cypher_exec_unknown_func_return_errors);
RUN_TEST(cypher_exec_inline_props);
RUN_TEST(cypher_parse_where_starts_with);
RUN_TEST(cypher_parse_where_contains);
RUN_TEST(cypher_parse_where_numeric);
/* Edge property tests (ported from cypher_test.go Feature 2) */
RUN_TEST(cypher_edge_prop_access);
RUN_TEST(cypher_edge_prop_in_where);
RUN_TEST(cypher_edge_type_prop);
RUN_TEST(cypher_edge_filter_contains);
RUN_TEST(cypher_edge_filter_numeric_gte);
RUN_TEST(cypher_bare_edge_return_exposes_properties_json);
RUN_TEST(cypher_edge_return_without_filter);
RUN_TEST(cypher_edge_filter_equals);
RUN_TEST(cypher_edge_filter_starts_with);
RUN_TEST(cypher_edge_combined_node_and_edge_filter);
RUN_TEST(cypher_edge_filter_no_match);
RUN_TEST(cypher_edge_filter_numeric_lt);
RUN_TEST(cypher_edge_filter_regex);
RUN_TEST(cypher_edge_builtin_type_filter);
RUN_TEST(cypher_apply_limit);
/* Phase 1: Simple operators */
RUN_TEST(cypher_lex_neq_operators);
RUN_TEST(cypher_lex_ends_keyword);
RUN_TEST(cypher_lex_in_is_null);
RUN_TEST(cypher_exec_where_neq);
RUN_TEST(cypher_exec_where_neq_bang);
RUN_TEST(cypher_exec_where_ends_with);
RUN_TEST(cypher_exec_where_not);
RUN_TEST(cypher_exec_where_in);
RUN_TEST(cypher_exec_where_not_in);
RUN_TEST(cypher_exec_where_is_null);
RUN_TEST(cypher_exec_where_is_not_null);
RUN_TEST(cypher_exec_return_star);
RUN_TEST(cypher_parse_neq);
RUN_TEST(cypher_parse_in);
RUN_TEST(cypher_parse_is_null);
/* Phase 2: Expression tree */
RUN_TEST(cypher_exec_where_or);
RUN_TEST(cypher_exec_where_complex_bool);
RUN_TEST(cypher_exec_where_xor);
RUN_TEST(cypher_exec_where_not_prefix);
RUN_TEST(cypher_parse_expr_tree_and_or);
RUN_TEST(cypher_parse_expr_tree_nested);
/* Phase 3: Unsupported keyword errors */
RUN_TEST(cypher_error_create);
RUN_TEST(cypher_error_delete);
RUN_TEST(cypher_error_set);
RUN_TEST(cypher_error_merge);
RUN_TEST(cypher_error_call);
/* Phase 4: SKIP + aggregation */
RUN_TEST(cypher_exec_skip);
RUN_TEST(cypher_exec_skip_limit);
RUN_TEST(cypher_exec_sum);
RUN_TEST(cypher_exec_avg);
RUN_TEST(cypher_exec_min);
RUN_TEST(cypher_exec_max);
RUN_TEST(cypher_exec_collect);
RUN_TEST(cypher_exec_count_star);
RUN_TEST(cypher_parse_skip);
RUN_TEST(cypher_parse_sum_avg);
RUN_TEST(cypher_parse_collect);
/* Phase 5: String functions + CASE */
RUN_TEST(cypher_exec_tolower);
RUN_TEST(cypher_exec_toupper);
RUN_TEST(cypher_exec_tostring);
RUN_TEST(cypher_exec_case);
RUN_TEST(cypher_parse_tolower);
RUN_TEST(cypher_parse_case);
/* Phase 6: WITH clause */
RUN_TEST(cypher_exec_with_rename);
RUN_TEST(cypher_exec_with_count);
RUN_TEST(cypher_exec_with_node_groupvar_prop);
RUN_TEST(cypher_exec_with_where);
RUN_TEST(cypher_exec_with_orderby_limit);
RUN_TEST(cypher_parse_with);
RUN_TEST(cypher_parse_with_where);
/* Phase 7: OPTIONAL MATCH + multiple MATCH */
RUN_TEST(cypher_exec_optional_match_no_result);
RUN_TEST(cypher_exec_optional_match_has_result);
RUN_TEST(cypher_exec_multi_match);
RUN_TEST(cypher_parse_optional_match);
RUN_TEST(cypher_parse_multi_match);
/* Phase 8: UNION */
RUN_TEST(cypher_exec_union);
RUN_TEST(cypher_exec_union_all);
RUN_TEST(cypher_parse_union);
/* Phase 9: UNWIND */
RUN_TEST(cypher_parse_unwind);
RUN_TEST(cypher_parse_unwind_var);
RUN_TEST(cypher_wide_return_projection_bounded);
}