mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-03 08:03:38 +08:00
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # tests/test_lang_contract.c
This commit is contained in:
+250
-70
@@ -760,6 +760,13 @@ static void expr_free(cbm_expr_t *e) {
|
||||
safe_str_free(&cur->cond.in_values[i]);
|
||||
}
|
||||
free(cur->cond.in_values);
|
||||
safe_str_free(&cur->cond.func);
|
||||
for (int i = 0; i < cur->cond.arg_count; i++) {
|
||||
safe_str_free(&cur->cond.args[i].variable);
|
||||
safe_str_free(&cur->cond.args[i].property);
|
||||
safe_str_free(&cur->cond.args[i].literal);
|
||||
}
|
||||
free(cur->cond.args);
|
||||
}
|
||||
if (cur->right) {
|
||||
if (top < EXPR_FREE_STACK) {
|
||||
@@ -838,6 +845,37 @@ static const char *unsupported_clause_error(cbm_token_type_t type) {
|
||||
|
||||
/* Forward declarations for recursive descent */
|
||||
static cbm_expr_t *parse_or_expr(parser_t *p);
|
||||
/* Multi-arg scalar function support, shared with the RETURN-item parser (#874) */
|
||||
static bool is_multiarg_func_call(parser_t *p);
|
||||
static int parse_multiarg_func_item(parser_t *p, cbm_return_item_t *item);
|
||||
|
||||
/* Free a multi-arg function argument array. */
|
||||
static void func_args_free(cbm_func_arg_t *args, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
safe_str_free(&args[i].variable);
|
||||
safe_str_free(&args[i].property);
|
||||
safe_str_free(&args[i].literal);
|
||||
}
|
||||
free(args);
|
||||
}
|
||||
|
||||
/* Free the fields of a partially-parsed multi-arg function item. */
|
||||
static void func_item_fields_free(cbm_return_item_t *item) {
|
||||
safe_str_free(&item->variable);
|
||||
safe_str_free(&item->property);
|
||||
safe_str_free(&item->func);
|
||||
func_args_free(item->args, item->arg_count);
|
||||
item->args = NULL;
|
||||
item->arg_count = 0;
|
||||
}
|
||||
|
||||
/* Free the function-call fields of a WHERE condition (#874). */
|
||||
static void cond_func_fields_free(cbm_condition_t *c) {
|
||||
safe_str_free(&c->func);
|
||||
func_args_free(c->args, c->arg_count);
|
||||
c->args = NULL;
|
||||
c->arg_count = 0;
|
||||
}
|
||||
|
||||
/* Parse IN [val, val, ...] list. Returns expr_leaf or NULL on error. */
|
||||
static cbm_expr_t *parse_in_list(parser_t *p, cbm_condition_t *c) {
|
||||
@@ -1008,11 +1046,9 @@ static cbm_expr_t *parse_exists_predicate(parser_t *p, bool negated) {
|
||||
return expr_leaf(c);
|
||||
}
|
||||
|
||||
static bool cyp_ci_eq(const char *a, const char *b);
|
||||
|
||||
/* Parse the operator + value tail shared by every condition subject
|
||||
* (var[.prop] and coalesce(var.prop, literal)): IS [NOT] NULL, IN [...],
|
||||
* or a comparison operator with a literal value. Consumes/frees *c. */
|
||||
* (var[.prop] and multi-arg functions like coalesce(...)): IS [NOT] NULL,
|
||||
* IN [...], or a comparison operator with a literal value. */
|
||||
static cbm_expr_t *parse_condition_op(parser_t *p, cbm_condition_t *c) {
|
||||
/* IS NULL / IS NOT NULL */
|
||||
if (check(p, TOK_IS)) {
|
||||
@@ -1029,13 +1065,18 @@ static cbm_expr_t *parse_condition_op(parser_t *p, cbm_condition_t *c) {
|
||||
|
||||
/* IN [...] */
|
||||
if (check(p, TOK_IN)) {
|
||||
return parse_in_list(p, c);
|
||||
cbm_expr_t *e = parse_in_list(p, c);
|
||||
if (!e) {
|
||||
cond_func_fields_free(c);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
/* Standard operators */
|
||||
c->op = parse_comparison_op(p);
|
||||
if (!c->op) {
|
||||
snprintf(p->error, sizeof(p->error), "unexpected operator at pos %d", peek(p)->pos);
|
||||
cond_func_fields_free(c);
|
||||
safe_str_free(&c->variable);
|
||||
safe_str_free(&c->property);
|
||||
safe_str_free(&c->coalesce_default);
|
||||
@@ -1053,6 +1094,7 @@ static cbm_expr_t *parse_condition_op(parser_t *p, cbm_condition_t *c) {
|
||||
c->value = heap_strdup("false");
|
||||
} else {
|
||||
snprintf(p->error, sizeof(p->error), "expected value at pos %d", peek(p)->pos);
|
||||
cond_func_fields_free(c);
|
||||
safe_str_free(&c->variable);
|
||||
safe_str_free(&c->property);
|
||||
safe_str_free(&c->op);
|
||||
@@ -1063,6 +1105,75 @@ static cbm_expr_t *parse_condition_op(parser_t *p, cbm_condition_t *c) {
|
||||
return expr_leaf(*c);
|
||||
}
|
||||
|
||||
/* parse_condition_lhs result: the label-test form is a complete condition. */
|
||||
enum { COND_LHS_COMPLETE = 1 };
|
||||
|
||||
/* Parse the left-hand side of a WHERE condition into c.
|
||||
* Returns CBM_NOT_FOUND on error, 0 when an operator/value should follow, and
|
||||
* COND_LHS_COMPLETE when the condition is already complete (label test). */
|
||||
static int parse_condition_lhs(parser_t *p, cbm_condition_t *c) {
|
||||
if (is_multiarg_func_call(p)) {
|
||||
/* Multi-arg scalar function LHS: coalesce(f.depth, 0) >= 2 (#874).
|
||||
* Reuse the RETURN-item parser, then move ownership into the condition. */
|
||||
cbm_return_item_t fitem;
|
||||
memset(&fitem, 0, sizeof(fitem));
|
||||
if (parse_multiarg_func_item(p, &fitem) < 0) {
|
||||
func_item_fields_free(&fitem);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
c->variable = fitem.variable;
|
||||
c->property = fitem.property;
|
||||
c->func = fitem.func;
|
||||
c->args = fitem.args;
|
||||
c->arg_count = fitem.arg_count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (check(p, TOK_IDENT) && p->pos + SKIP_ONE < p->count &&
|
||||
p->tokens[p->pos + SKIP_ONE].type == TOK_LPAREN) {
|
||||
/* Unrecognised function call in WHERE — fail loudly with the supported
|
||||
* set instead of the misleading "unexpected operator" (#874). */
|
||||
snprintf(p->error, sizeof(p->error),
|
||||
"unsupported function '%s' in WHERE (supported: coalesce, substring, replace, "
|
||||
"left, right)",
|
||||
peek(p)->text);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
const cbm_token_t *var = expect(p, TOK_IDENT);
|
||||
if (!var) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Label test: WHERE n:Label (openCypher, #241). Modelled as a leaf with
|
||||
* op="HAS_LABEL" and value=Label, evaluated against the bound node's label. */
|
||||
if (check(p, TOK_COLON)) {
|
||||
advance(p);
|
||||
const cbm_token_t *lbl = expect(p, TOK_IDENT);
|
||||
if (!lbl) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
c->variable = heap_strdup(var->text);
|
||||
c->op = heap_strdup("HAS_LABEL");
|
||||
c->value = heap_strdup(lbl->text);
|
||||
return COND_LHS_COMPLETE;
|
||||
}
|
||||
|
||||
if (match(p, TOK_DOT)) {
|
||||
const cbm_token_t *prop = expect(p, TOK_IDENT);
|
||||
if (!prop) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
c->variable = heap_strdup(var->text);
|
||||
c->property = heap_strdup(prop->text);
|
||||
} else {
|
||||
/* No dot: bare alias (e.g. post-WITH variable like "cnt") */
|
||||
c->variable = heap_strdup(var->text);
|
||||
c->property = NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cbm_expr_t *parse_condition_expr(parser_t *p) {
|
||||
/* Check for NOT prefix at condition level (e.g. NOT n.name CONTAINS "x") */
|
||||
bool negated = match(p, TOK_NOT);
|
||||
@@ -1072,73 +1183,16 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) {
|
||||
return parse_exists_predicate(p, negated);
|
||||
}
|
||||
|
||||
/* coalesce(var.prop, <literal>) as a null-safe condition subject (#874).
|
||||
* The default literal substitutes a missing/empty property at eval time;
|
||||
* any comparison operator may follow, exactly as with a bare var.prop. */
|
||||
if (check(p, TOK_IDENT) && cyp_ci_eq(peek(p)->text, "coalesce") &&
|
||||
p->pos + SKIP_ONE < p->count && p->tokens[p->pos + SKIP_ONE].type == TOK_LPAREN) {
|
||||
advance(p); /* coalesce */
|
||||
advance(p); /* ( */
|
||||
const cbm_token_t *cvar = expect(p, TOK_IDENT);
|
||||
if (!cvar || !expect(p, TOK_DOT)) {
|
||||
return NULL;
|
||||
}
|
||||
const cbm_token_t *cprop = expect(p, TOK_IDENT);
|
||||
if (!cprop || !expect(p, TOK_COMMA)) {
|
||||
return NULL;
|
||||
}
|
||||
if (!check(p, TOK_STRING) && !check(p, TOK_NUMBER)) {
|
||||
return NULL; /* supported form: coalesce(var.prop, literal) */
|
||||
}
|
||||
const cbm_token_t *cdef = peek(p);
|
||||
cbm_condition_t cc = {0};
|
||||
cc.negated = negated;
|
||||
cc.variable = heap_strdup(cvar->text);
|
||||
cc.property = heap_strdup(cprop->text);
|
||||
cc.coalesce_default = heap_strdup(cdef->text);
|
||||
advance(p);
|
||||
if (!expect(p, TOK_RPAREN)) {
|
||||
safe_str_free(&cc.variable);
|
||||
safe_str_free(&cc.property);
|
||||
safe_str_free(&cc.coalesce_default);
|
||||
return NULL;
|
||||
}
|
||||
return parse_condition_op(p, &cc);
|
||||
}
|
||||
|
||||
const cbm_token_t *var = expect(p, TOK_IDENT);
|
||||
if (!var) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cbm_condition_t c = {0};
|
||||
c.negated = negated;
|
||||
|
||||
/* Label test: WHERE n:Label (openCypher, #241). Modelled as a leaf with
|
||||
* op="HAS_LABEL" and value=Label, evaluated against the bound node's label. */
|
||||
if (check(p, TOK_COLON)) {
|
||||
advance(p);
|
||||
const cbm_token_t *lbl = expect(p, TOK_IDENT);
|
||||
if (!lbl) {
|
||||
return NULL;
|
||||
}
|
||||
c.variable = heap_strdup(var->text);
|
||||
c.op = heap_strdup("HAS_LABEL");
|
||||
c.value = heap_strdup(lbl->text);
|
||||
return expr_leaf(c);
|
||||
int lhs_rc = parse_condition_lhs(p, &c);
|
||||
if (lhs_rc < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (match(p, TOK_DOT)) {
|
||||
const cbm_token_t *prop = expect(p, TOK_IDENT);
|
||||
if (!prop) {
|
||||
return NULL;
|
||||
}
|
||||
c.variable = heap_strdup(var->text);
|
||||
c.property = heap_strdup(prop->text);
|
||||
} else {
|
||||
/* No dot: bare alias (e.g. post-WITH variable like "cnt") */
|
||||
c.variable = heap_strdup(var->text);
|
||||
c.property = NULL;
|
||||
if (lhs_rc > 0) {
|
||||
/* HAS_LABEL leaf — complete condition, no operator follows */
|
||||
return expr_leaf(c);
|
||||
}
|
||||
|
||||
return parse_condition_op(p, &c);
|
||||
@@ -1995,6 +2049,8 @@ static void free_where(cbm_where_clause_t *w) {
|
||||
safe_str_free(&w->conditions[i].in_values[j]);
|
||||
}
|
||||
free(w->conditions[i].in_values);
|
||||
safe_str_free(&w->conditions[i].func);
|
||||
func_args_free(w->conditions[i].args, w->conditions[i].arg_count);
|
||||
}
|
||||
free(w->conditions);
|
||||
safe_str_free(&w->op);
|
||||
@@ -2230,15 +2286,49 @@ static const char *json_extract_prop(const char *json, const char *key, char *bu
|
||||
p++;
|
||||
}
|
||||
if (*p == '"') {
|
||||
/* String value */
|
||||
/* String value — honor backslash escapes: without this, an embedded \"
|
||||
* cuts the value short at the first escaped quote. */
|
||||
p++;
|
||||
size_t i = 0;
|
||||
while (*p && *p != '"' && i < buf_sz - SKIP_ONE) {
|
||||
if (*p == '\\' && p[SKIP_ONE] && i + SKIP_ONE < buf_sz - SKIP_ONE) {
|
||||
buf[i++] = *p++; /* keep the escape pair intact */
|
||||
}
|
||||
buf[i++] = *p++;
|
||||
}
|
||||
buf[i] = '\0';
|
||||
} else if (*p == '[' || *p == '{') {
|
||||
/* Array/object value — copy the whole balanced construct. A scan-to-comma
|
||||
* truncates at the first comma INSIDE the value: e.g. a decorators array
|
||||
* ["@Roles('OWNER', 'ADMIN')","@Get()"] came back as ["@Roles('OWNER'. */
|
||||
char open = *p;
|
||||
char close = (open == '[') ? ']' : '}';
|
||||
int depth = 0;
|
||||
int in_str = 0;
|
||||
size_t i = 0;
|
||||
while (*p && i < buf_sz - SKIP_ONE) {
|
||||
char c = *p;
|
||||
if (in_str) {
|
||||
if (c == '\\' && p[SKIP_ONE] && i + SKIP_ONE < buf_sz - SKIP_ONE) {
|
||||
buf[i++] = *p++; /* escape pair stays intact */
|
||||
} else if (c == '"') {
|
||||
in_str = 0;
|
||||
}
|
||||
} else if (c == '"') {
|
||||
in_str = 1;
|
||||
} else if (c == open) {
|
||||
depth++;
|
||||
} else if (c == close) {
|
||||
depth--;
|
||||
}
|
||||
buf[i++] = *p++;
|
||||
if (!in_str && depth == 0) {
|
||||
break; /* outer bracket closed */
|
||||
}
|
||||
}
|
||||
buf[i] = '\0';
|
||||
} else {
|
||||
/* Numeric or other value */
|
||||
/* Numeric or other scalar value */
|
||||
size_t i = 0;
|
||||
while (*p && *p != ',' && *p != '}' && *p != ' ' && i < buf_sz - SKIP_ONE) {
|
||||
buf[i++] = *p++;
|
||||
@@ -2384,8 +2474,26 @@ static void binding_set(binding_t *b, const char *var, const cbm_node_t *node) {
|
||||
b->var_count++;
|
||||
}
|
||||
|
||||
static const char *eval_multiarg_func(binding_t *b, const cbm_return_item_t *item, char *buf,
|
||||
size_t bufsz);
|
||||
|
||||
/* Resolve the actual property value for a condition from a binding */
|
||||
static const char *resolve_condition_value(const cbm_condition_t *c, binding_t *b) {
|
||||
/* Multi-arg scalar function LHS: coalesce(f.depth, 0) >= 2 (#874).
|
||||
* Evaluated through the same code path as RETURN projections. The value is
|
||||
* consumed by eval_condition before any other condition is resolved, so a
|
||||
* single thread-local buffer per call is safe. */
|
||||
if (c->func) {
|
||||
static _Thread_local char func_buf[CBM_SZ_512];
|
||||
cbm_return_item_t item = {0};
|
||||
item.variable = c->variable;
|
||||
item.property = c->property;
|
||||
item.func = c->func;
|
||||
item.args = c->args;
|
||||
item.arg_count = c->arg_count;
|
||||
return eval_multiarg_func(b, &item, func_buf, sizeof(func_buf));
|
||||
}
|
||||
|
||||
cbm_edge_t *e = binding_get_edge(b, c->variable);
|
||||
if (e) {
|
||||
return edge_prop(e, c->property);
|
||||
@@ -2650,6 +2758,49 @@ static void rb_add_row(result_builder_t *rb, const char **values) {
|
||||
* Prevents accidental multi-GB JSON payloads from unbounded MATCH (n) RETURN n. */
|
||||
#define CYPHER_RESULT_CEILING 100000
|
||||
|
||||
/* Wall-clock execution deadline (#601). The row ceiling above only fires once
|
||||
* rows exist, but an unbounded `OPTIONAL MATCH` over the full node set (or a
|
||||
* GROUP BY with ~one group per node) does O(bindings x groups) work and can run
|
||||
* for minutes — exhausting RAM/CPU — before a single row is produced, so the
|
||||
* ceiling never trips and the caller just hangs. A monotonic deadline aborts
|
||||
* such runaway queries with a clear, actionable error. Checked (throttled) in
|
||||
* the scan, expansion and aggregation hot loops. */
|
||||
#define CYPHER_DEADLINE_BUDGET_MS 30000 /* 30s: generous for legit heavy queries */
|
||||
#define CYPHER_DEADLINE_CHECK_MASK 0x3FF /* sample the clock every 1024 iterations */
|
||||
|
||||
static _Thread_local uint64_t g_cypher_deadline_ms = 0; /* absolute; 0 = disarmed */
|
||||
static _Thread_local bool g_cypher_timed_out = false;
|
||||
static _Thread_local int64_t g_cypher_deadline_override_ms = -1; /* test hook; <0 = default */
|
||||
|
||||
static void cypher_deadline_arm(void) {
|
||||
g_cypher_timed_out = false;
|
||||
int64_t budget = g_cypher_deadline_override_ms >= 0 ? g_cypher_deadline_override_ms
|
||||
: CYPHER_DEADLINE_BUDGET_MS;
|
||||
g_cypher_deadline_ms = cbm_now_ms() + (uint64_t)budget;
|
||||
}
|
||||
|
||||
/* True once the query has run past its wall-clock budget. Sticky: after the
|
||||
* first trip every subsequent call returns true, so later loops short-circuit. */
|
||||
static bool cypher_deadline_exceeded(void) {
|
||||
if (g_cypher_timed_out) {
|
||||
return true;
|
||||
}
|
||||
if (g_cypher_deadline_ms == 0) {
|
||||
return false;
|
||||
}
|
||||
if (cbm_now_ms() >= g_cypher_deadline_ms) {
|
||||
g_cypher_timed_out = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Test-only: force the execution budget (ms) for subsequent queries on this
|
||||
* thread. 0 = trip on the first hot-loop check; <0 restores the default. */
|
||||
void cbm_cypher_test_set_deadline_ms(int64_t budget_ms) {
|
||||
g_cypher_deadline_override_ms = budget_ms;
|
||||
}
|
||||
|
||||
/* ── Binding virtual variables (for WITH clause) ──────────────── */
|
||||
|
||||
static const char *binding_get_virtual(binding_t *b, const char *var, const char *prop) {
|
||||
@@ -3054,6 +3205,11 @@ static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_
|
||||
int *bind_count, const int *bind_cap, const char **var_name,
|
||||
bool is_optional) {
|
||||
for (int ri = 0; ri < pat->rel_count; ri++) {
|
||||
/* #601: stop expanding further hops once the wall-clock budget is spent
|
||||
* (an unbounded expansion is exactly what blows up here). */
|
||||
if (cypher_deadline_exceeded()) {
|
||||
return;
|
||||
}
|
||||
cbm_rel_pattern_t *rel = &pat->rels[ri];
|
||||
cbm_node_pattern_t *target_node = &pat->nodes[ri + SKIP_ONE];
|
||||
const char *to_var = target_node->variable ? target_node->variable : "_n_t";
|
||||
@@ -3068,6 +3224,9 @@ static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_
|
||||
int new_count = 0;
|
||||
|
||||
for (int bi = 0; bi < *bind_count; bi++) {
|
||||
if ((bi & CYPHER_DEADLINE_CHECK_MASK) == 0 && cypher_deadline_exceeded()) {
|
||||
break;
|
||||
}
|
||||
binding_t *b = &(*bindings)[bi];
|
||||
cbm_node_t *src = binding_get(b, *var_name);
|
||||
if (!src) {
|
||||
@@ -4111,6 +4270,11 @@ static void execute_return_agg(cbm_return_clause_t *ret, binding_t *bindings, in
|
||||
int agg_count = 0;
|
||||
|
||||
for (int bi = 0; bi < bind_count; bi++) {
|
||||
/* #601: grouping is O(bindings x groups) — the dominant cost on a
|
||||
* whole-graph GROUP BY. Abort if we blow the wall-clock budget. */
|
||||
if ((bi & CYPHER_DEADLINE_CHECK_MASK) == 0 && cypher_deadline_exceeded()) {
|
||||
break;
|
||||
}
|
||||
char key[CBM_SZ_1K] = "";
|
||||
const char *vals[CBM_SZ_32];
|
||||
char valbufs[CBM_SZ_32][CBM_SZ_512];
|
||||
@@ -4479,6 +4643,9 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec
|
||||
const char *var_name = pat0->nodes[0].variable ? pat0->nodes[0].variable : "_n0";
|
||||
|
||||
for (int i = 0; i < scan_count && bind_count < bind_cap; i++) {
|
||||
if ((i & CYPHER_DEADLINE_CHECK_MASK) == 0 && cypher_deadline_exceeded()) {
|
||||
break;
|
||||
}
|
||||
binding_t b = {0};
|
||||
b.store = store;
|
||||
binding_set(&b, var_name, &scanned[i]);
|
||||
@@ -4527,6 +4694,7 @@ int cbm_cypher_execute(cbm_store_t *store, const char *query, const char *projec
|
||||
cbm_cypher_result_t *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
g_cypher_depth_clamped = 0;
|
||||
cypher_deadline_arm(); /* #601: start the wall-clock budget for this query */
|
||||
if (max_rows <= 0) {
|
||||
max_rows = CYPHER_RESULT_CEILING;
|
||||
}
|
||||
@@ -4570,6 +4738,18 @@ int cbm_cypher_execute(cbm_store_t *store, const char *query, const char *projec
|
||||
rb_apply_distinct(&rb);
|
||||
}
|
||||
|
||||
/* #601: abort a runaway query that blew the wall-clock budget before it can
|
||||
* return a misleading partial result. Checked before the row ceiling. */
|
||||
if (g_cypher_timed_out) {
|
||||
rb_free(&rb);
|
||||
cbm_query_free(q);
|
||||
out->error =
|
||||
heap_strdup("query exceeded the execution time limit — narrow the pattern with a WHERE "
|
||||
"filter, use a directed MATCH instead of an unbounded OPTIONAL MATCH, or "
|
||||
"add LIMIT");
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Check ceiling */
|
||||
if (rb.row_count >= CYPHER_RESULT_CEILING) {
|
||||
rb_free(&rb);
|
||||
|
||||
+17
-7
@@ -181,6 +181,13 @@ typedef struct {
|
||||
int rel_count;
|
||||
} cbm_pattern_t;
|
||||
|
||||
/* One argument to a multi-argument scalar function (coalesce, substring, ...). */
|
||||
typedef struct {
|
||||
const char *variable; /* variable reference (NULL if a literal) */
|
||||
const char *property; /* property of the variable (NULL if whole var / literal) */
|
||||
const char *literal; /* literal string/number text (NULL if a variable ref) */
|
||||
} cbm_func_arg_t;
|
||||
|
||||
/* WHERE condition */
|
||||
typedef struct {
|
||||
const char *variable;
|
||||
@@ -198,6 +205,11 @@ typedef struct {
|
||||
* anchor, `value` the edge type (NULL = any), `exists_dir` the direction
|
||||
* (0 = outbound, 1 = inbound, 2 = any). */
|
||||
int exists_dir;
|
||||
/* Multi-arg scalar function on the LHS, e.g. coalesce(f.depth, 0) >= 2
|
||||
* (#874). NULL func = plain variable/property LHS. */
|
||||
const char *func;
|
||||
cbm_func_arg_t *args;
|
||||
int arg_count;
|
||||
} cbm_condition_t;
|
||||
|
||||
/* Expression tree for WHERE clause */
|
||||
@@ -237,13 +249,6 @@ typedef struct {
|
||||
const char *else_val; /* NULL if no ELSE */
|
||||
} cbm_case_expr_t;
|
||||
|
||||
/* One argument to a multi-argument scalar function (coalesce, substring, ...). */
|
||||
typedef struct {
|
||||
const char *variable; /* variable reference (NULL if a literal) */
|
||||
const char *property; /* property of the variable (NULL if whole var / literal) */
|
||||
const char *literal; /* literal string/number text (NULL if a variable ref) */
|
||||
} cbm_func_arg_t;
|
||||
|
||||
/* RETURN item */
|
||||
typedef struct {
|
||||
const char *variable;
|
||||
@@ -332,4 +337,9 @@ int cbm_cypher_parse(const char *query, cbm_query_t **out, char **error);
|
||||
/* Free a query AST. */
|
||||
void cbm_query_free(cbm_query_t *q);
|
||||
|
||||
/* Test-only (#601): force the wall-clock execution budget in milliseconds for
|
||||
* subsequent queries on the calling thread. 0 = trip on the first hot-loop
|
||||
* check; a negative value restores the default budget. */
|
||||
void cbm_cypher_test_set_deadline_ms(int64_t budget_ms);
|
||||
|
||||
#endif /* CBM_CYPHER_H */
|
||||
|
||||
@@ -95,7 +95,7 @@ static cbm_system_info_t detect_system_bsd(void) {
|
||||
info.total_cores = nprocs > 0 ? (int)nprocs : 1;
|
||||
info.perf_cores = info.total_cores;
|
||||
|
||||
#if defined(__OpenBSD__)
|
||||
#if defined(__OpenBSD__) || defined(__FreeBSD__)
|
||||
int mib[2] = {CTL_HW, HW_PHYSMEM};
|
||||
#else
|
||||
int mib[2] = {CTL_HW, HW_PHYSMEM64};
|
||||
|
||||
+13
-1
@@ -117,7 +117,19 @@ char *cbm_pipeline_fqn_compute(const char *project, const char *rel_path, const
|
||||
|
||||
char *path = strdup(rel_path ? rel_path : "");
|
||||
cbm_normalize_path_sep(path);
|
||||
strip_file_extension(path);
|
||||
/* #1077/#964: File-node QNs (name=="__file__") must preserve the full
|
||||
* filename so sibling files sharing a stem get DISTINCT nodes — e.g.
|
||||
* .env / .env.local / .env.production (which all strip to ".env" and
|
||||
* collide, so only one survives per directory), and a C/C++ header vs
|
||||
* its same-stem .cpp. Extension stripping stays for MODULE/symbol QNs
|
||||
* (name==NULL or a symbol): that stem unification is load-bearing for
|
||||
* C/C++ declaration↔definition cross-file resolution. All 26 __file__
|
||||
* QN sites route through here, so creation and every lookup stay
|
||||
* consistent. */
|
||||
bool is_file_qn = name && strcmp(name, "__file__") == 0;
|
||||
if (!is_file_qn) {
|
||||
strip_file_extension(path);
|
||||
}
|
||||
|
||||
const char *segments[CBM_SZ_256];
|
||||
int seg_count = 0;
|
||||
|
||||
+43
-1
@@ -615,6 +615,47 @@ static bool is_infra_file(const char *fp) {
|
||||
strstr(fp, ".tf") != NULL || strstr(fp, ".hcl") != NULL || strstr(fp, ".toml") != NULL);
|
||||
}
|
||||
|
||||
/* CI/tooling configs describe the development TOOLCHAIN — their URLs are
|
||||
* repository/action/registry references, never endpoints this service
|
||||
* exposes. Minting infra Route nodes from them lets the route matcher's
|
||||
* root-service heuristic attach every handler of an ambiguous "/" route to
|
||||
* each tooling URL (junk HANDLES churn on plain pallets/flask, #999).
|
||||
* Deny by file identity, not URL shape: deployment configs (Cloud
|
||||
* Scheduler, compose) keep minting their genuine endpoints. */
|
||||
static bool is_ci_tooling_config(const char *fp) {
|
||||
if (!fp) {
|
||||
return false;
|
||||
}
|
||||
if (strstr(fp, ".github/") != NULL || strstr(fp, ".gitlab/") != NULL ||
|
||||
strstr(fp, ".circleci/") != NULL) {
|
||||
return true;
|
||||
}
|
||||
const char *slash = strrchr(fp, '/');
|
||||
const char *base = slash ? slash + 1 : fp;
|
||||
static const char *const tooling[] = {".pre-commit-config.yaml",
|
||||
".pre-commit-hooks.yaml",
|
||||
".gitlab-ci.yml",
|
||||
".travis.yml",
|
||||
"azure-pipelines.yml",
|
||||
"appveyor.yml",
|
||||
"bitbucket-pipelines.yml",
|
||||
".readthedocs.yaml",
|
||||
".readthedocs.yml",
|
||||
"codecov.yml",
|
||||
".codecov.yml",
|
||||
".goreleaser.yaml",
|
||||
".goreleaser.yml",
|
||||
".golangci.yml",
|
||||
".golangci.yaml",
|
||||
NULL};
|
||||
for (int i = 0; tooling[i]; i++) {
|
||||
if (strcmp(base, tooling[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* True when a YAML key path denotes an UPSTREAM dependency, CONFIG value, or
|
||||
* HEALTHCHECK target rather than an endpoint this service exposes. Such URLs
|
||||
* (auth JWKS, downstream service base URLs, package-registry URLs, healthcheck
|
||||
@@ -683,7 +724,8 @@ static void cbm_pipeline_extract_infra_routes(cbm_gbuf_t *gbuf, const cbm_file_i
|
||||
CBMHashTable *denied = cbm_ht_create(16);
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (!result_cache[i] || !is_infra_file(files[i].rel_path)) {
|
||||
if (!result_cache[i] || !is_infra_file(files[i].rel_path) ||
|
||||
is_ci_tooling_config(files[i].rel_path)) {
|
||||
continue;
|
||||
}
|
||||
for (int si = 0; si < result_cache[i]->string_refs.count; si++) {
|
||||
|
||||
@@ -60,26 +60,36 @@ static int r964_file_node_exists(cbm_store_t *store, const char *project, const
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Count inbound IMPORTS edges whose TARGET node name is exactly `name`. */
|
||||
static int r964_inbound_imports(cbm_store_t *store, const char *project, const char *name) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int n = 0;
|
||||
if (cbm_store_find_edges_by_type(store, project, "IMPORTS", &edges, &n) != CBM_STORE_OK)
|
||||
return -1;
|
||||
int hits = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
cbm_node_t tgt;
|
||||
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
|
||||
/* Count INBOUND edges (any of the reporter's connection types) landing on the
|
||||
* File node named `name` — the exact disconnection metric from the issue's
|
||||
* query `NOT EXISTS { (f)<-[:IMPORTS|CALLS|CONTAINS_FILE|DEFINES|USAGE]-() }`.
|
||||
* A header that used to have no node at all (extension-stripped QN collision)
|
||||
* now has its own File node AND is connected (CONTAINS_FILE from its folder,
|
||||
* plus DEFINES of the class it declares); `#include` itself resolves to the
|
||||
* declared Class node via #983, not to the header File. */
|
||||
static int r964_file_inbound(cbm_store_t *store, const char *project, const char *name) {
|
||||
static const char *const types[] = {"IMPORTS", "CALLS", "CONTAINS_FILE",
|
||||
"DEFINES", "USAGE", NULL};
|
||||
int total = 0;
|
||||
for (int t = 0; types[t]; t++) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int n = 0;
|
||||
if (cbm_store_find_edges_by_type(store, project, types[t], &edges, &n) != CBM_STORE_OK)
|
||||
continue;
|
||||
if (tgt.name && strcmp(tgt.name, name) == 0)
|
||||
hits++;
|
||||
cbm_node_free_fields(&tgt);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cbm_node_t tgt;
|
||||
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
|
||||
continue;
|
||||
if (tgt.name && strcmp(tgt.name, name) == 0)
|
||||
total++;
|
||||
cbm_node_free_fields(&tgt);
|
||||
}
|
||||
cbm_store_free_edges(edges, n);
|
||||
}
|
||||
cbm_store_free_edges(edges, n);
|
||||
return hits;
|
||||
return total;
|
||||
}
|
||||
|
||||
TEST(repro_issue964_header_has_node_and_inbound_import) {
|
||||
TEST(repro_issue964_header_has_node_and_is_connected) {
|
||||
static const RFile files[] = {
|
||||
{"NodeController.h", "#pragma once\n"
|
||||
"class NodeController {\n"
|
||||
@@ -102,20 +112,20 @@ TEST(repro_issue964_header_has_node_and_inbound_import) {
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
int header_node = r964_file_node_exists(store, lp.project, "NodeController.h");
|
||||
int header_inbound = r964_inbound_imports(store, lp.project, "NodeController.h");
|
||||
int header_inbound = r964_file_inbound(store, lp.project, "NodeController.h");
|
||||
if (!header_node || header_inbound < 1) {
|
||||
fprintf(stderr,
|
||||
" [964] FAIL header_file_node=%d inbound_imports=%d (header merged into "
|
||||
"same-stem .cpp by extension-stripped QN collision)\n",
|
||||
" [964] FAIL header_file_node=%d inbound_edges=%d (header merged into "
|
||||
"same-stem .cpp by extension-stripped QN collision, or left disconnected)\n",
|
||||
header_node, header_inbound);
|
||||
}
|
||||
ASSERT_TRUE(header_node); /* header must keep its own File node */
|
||||
ASSERT_TRUE(header_inbound >= 1); /* #include must land an edge on it */
|
||||
ASSERT_TRUE(header_node); /* header keeps its own File node (not merged) */
|
||||
ASSERT_TRUE(header_inbound >= 1); /* and is connected (not the reporter's zero-inbound) */
|
||||
|
||||
rh_cleanup(&lp, store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_issue964) {
|
||||
RUN_TEST(repro_issue964_header_has_node_and_inbound_import);
|
||||
RUN_TEST(repro_issue964_header_has_node_and_is_connected);
|
||||
}
|
||||
|
||||
@@ -836,6 +836,123 @@ TEST(cypher_func_multiarg) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* issue #874: coalesce() in WHERE — null-safe numeric filter over an optional
|
||||
* JSON property. Exact repro shape from the issue: nodes lacking the property
|
||||
* fall back to the literal default instead of failing to parse. */
|
||||
TEST(cypher_issue874_where_coalesce_numeric) {
|
||||
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 = "DeepLoop",
|
||||
.qualified_name = "test.DeepLoop",
|
||||
.file_path = "deep.go",
|
||||
.properties_json = "{\"transitive_loop_depth\":5}"};
|
||||
cbm_node_t n2 = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "NoMetrics",
|
||||
.qualified_name = "test.NoMetrics",
|
||||
.file_path = "flat.go"};
|
||||
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 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); /* only DeepLoop; NoMetrics coalesces to 0 */
|
||||
ASSERT_STR_EQ(r.rows[0][0], "test.DeepLoop");
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* issue #874: coalesce() in WHERE with a string fallback and first-arg-wins. */
|
||||
TEST(cypher_issue874_where_coalesce_string) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
|
||||
/* Missing property on every node → fallback literal matches all 4 Functions */
|
||||
int rc = cbm_cypher_execute(
|
||||
s,
|
||||
"MATCH (f:Function) WHERE coalesce(f.missing, \"fallback\") = \"fallback\" "
|
||||
"RETURN f.name",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 4);
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
/* Present first arg wins over the fallback */
|
||||
cbm_cypher_result_t r2 = {0};
|
||||
rc = cbm_cypher_execute(
|
||||
s, "MATCH (f:Function) WHERE coalesce(f.name, \"zz\") = \"HandleOrder\" RETURN f.name",
|
||||
"test", 0, &r2);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r2.row_count, 1);
|
||||
ASSERT_STR_EQ(r2.rows[0][0], "HandleOrder");
|
||||
cbm_cypher_result_free(&r2);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* issue #874: function LHS composes with NOT and AND like any other condition. */
|
||||
TEST(cypher_issue874_where_coalesce_not_and) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
|
||||
int rc = cbm_cypher_execute(
|
||||
s, "MATCH (f:Function) WHERE NOT coalesce(f.missing, \"x\") = \"x\" RETURN f.name", "test",
|
||||
0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 0); /* every node coalesces to "x" — NOT filters all */
|
||||
cbm_cypher_result_free(&r);
|
||||
|
||||
cbm_cypher_result_t r2 = {0};
|
||||
rc = cbm_cypher_execute(s,
|
||||
"MATCH (f:Function) WHERE coalesce(f.missing, \"1\") = \"1\" "
|
||||
"AND f.name CONTAINS \"Order\" RETURN f.name",
|
||||
"test", 0, &r2);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r2.row_count, 3); /* HandleOrder, ValidateOrder, SubmitOrder */
|
||||
cbm_cypher_result_free(&r2);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* issue #874: the other multi-arg scalar functions work in WHERE too. */
|
||||
TEST(cypher_issue874_where_substring) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(
|
||||
s, "MATCH (f:Function) WHERE substring(f.name, 0, 6) = \"Handle\" 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();
|
||||
}
|
||||
|
||||
/* issue #874: an unrecognised function in WHERE must fail loudly with the
|
||||
* supported set, not the misleading "unexpected operator". */
|
||||
TEST(cypher_issue874_where_unsupported_func_error) {
|
||||
cbm_query_t *q = NULL;
|
||||
char *err = NULL;
|
||||
int rc =
|
||||
cbm_cypher_parse("MATCH (f:Function) WHERE foo(f.name) = \"x\" RETURN f.name", &q, &err);
|
||||
ASSERT_EQ(rc, -1);
|
||||
ASSERT_NOT_NULL(err);
|
||||
ASSERT_TRUE(strstr(err, "unsupported function 'foo'") != NULL);
|
||||
free(err);
|
||||
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
|
||||
@@ -2794,6 +2911,59 @@ TEST(cypher_multi_prop_projection_no_alias) {
|
||||
* 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. */
|
||||
/* Property projection must return the WHOLE value of composite properties.
|
||||
* json_extract_prop() scanned a non-string value up to the first ',' — so an
|
||||
* array/object property was truncated at its first INTERNAL comma. Real-world
|
||||
* hit: a NestJS handler's decorators
|
||||
* ["@Roles('OWNER', 'ADMIN')","@Get()"]
|
||||
* projected as ["@Roles('OWNER' — unusable for route/authz queries. */
|
||||
TEST(cypher_exec_prop_array_with_internal_commas) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "findAll",
|
||||
.qualified_name = "test.PacienteController.findAll",
|
||||
.file_path = "paciente.controller.ts",
|
||||
.properties_json =
|
||||
"{\"decorators\":[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"],\"lines\":3}"};
|
||||
cbm_store_upsert_node(s, &n);
|
||||
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s, "MATCH (m:Method) RETURN m.decorators, m.lines", "test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
/* whole array, commas and all — was ["@Roles('OWNER' before the fix */
|
||||
ASSERT_STR_EQ(r.rows[0][0], "[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"]");
|
||||
ASSERT_STR_EQ(r.rows[0][1], "3"); /* scalar sibling still parses */
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A string property must not end at an ESCAPED quote: the scan stopped at the
|
||||
* first '"' regardless of a preceding backslash, cutting the value short. */
|
||||
TEST(cypher_exec_prop_string_with_escaped_quote) {
|
||||
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 = "parse",
|
||||
.qualified_name = "test.parse",
|
||||
.file_path = "p.ts",
|
||||
.properties_json = "{\"signature\":\"(sep: \\\"a,b\\\") => void\"}"};
|
||||
cbm_store_upsert_node(s, &n);
|
||||
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.signature", "test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "(sep: \\\"a,b\\\") => void"); /* was: (sep: \ */
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_wide_return_projection_bounded) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("fork crash-isolation is POSIX-only; the parse-time bound is platform-agnostic");
|
||||
@@ -2827,6 +2997,54 @@ TEST(cypher_wide_return_projection_bounded) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* #601: an unbounded whole-graph OPTIONAL MATCH / GROUP BY does
|
||||
* O(bindings x groups) work and can run for minutes with no wall-clock guard —
|
||||
* the 100k row ceiling never fires because no rows are produced, so query_graph
|
||||
* just hangs. With the execution deadline armed to trip immediately (budget 0),
|
||||
* the runaway query must abort with a clear error instead of returning a
|
||||
* (misleading, possibly partial) result.
|
||||
*
|
||||
* RED on unfixed code: no deadline exists, so the query completes and returns
|
||||
* rc==0 with rows and no error — the assertions below fail. */
|
||||
TEST(cypher_exec_deadline_aborts_runaway_query_issue601) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
|
||||
cbm_cypher_test_set_deadline_ms(0); /* trip on the first hot-loop check */
|
||||
int rc = cbm_cypher_execute(
|
||||
s, "MATCH (a) OPTIONAL MATCH (a)-[:CALLS]->(b) RETURN a.qualified_name, count(b)", "test",
|
||||
0, &r);
|
||||
cbm_cypher_test_set_deadline_ms(-1); /* restore default before asserting (thread-local) */
|
||||
|
||||
ASSERT_TRUE(rc != 0); /* CBM_NOT_FOUND (-1) — query aborted, not success */
|
||||
ASSERT_NOT_NULL(r.error);
|
||||
ASSERT_TRUE(strstr(r.error, "time limit") != NULL);
|
||||
ASSERT_EQ(r.row_count, 0);
|
||||
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #601 companion: the default (ample) budget must NOT false-positive on a
|
||||
* normal small query — it still returns its rows. */
|
||||
TEST(cypher_exec_deadline_allows_normal_query_issue601) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
cbm_cypher_result_t r = {0};
|
||||
|
||||
int rc = cbm_cypher_execute(
|
||||
s, "MATCH (a) OPTIONAL MATCH (a)-[:CALLS]->(b) RETURN a.qualified_name, count(b)", "test",
|
||||
0, &r);
|
||||
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_TRUE(r.error == NULL);
|
||||
ASSERT_GT(r.row_count, 0);
|
||||
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
SUITE(cypher) {
|
||||
@@ -2859,6 +3077,8 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_parse_inline_props);
|
||||
RUN_TEST(cypher_parse_error);
|
||||
/* Execution */
|
||||
RUN_TEST(cypher_exec_deadline_aborts_runaway_query_issue601);
|
||||
RUN_TEST(cypher_exec_deadline_allows_normal_query_issue601);
|
||||
RUN_TEST(cypher_exec_match_all_functions);
|
||||
RUN_TEST(cypher_issue240_labels_function);
|
||||
RUN_TEST(cypher_issue237_distinct_order_limit);
|
||||
@@ -2882,6 +3102,11 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_func_tointeger_tofloat);
|
||||
RUN_TEST(cypher_func_size_reverse);
|
||||
RUN_TEST(cypher_func_multiarg);
|
||||
RUN_TEST(cypher_issue874_where_coalesce_numeric);
|
||||
RUN_TEST(cypher_issue874_where_coalesce_string);
|
||||
RUN_TEST(cypher_issue874_where_coalesce_not_and);
|
||||
RUN_TEST(cypher_issue874_where_substring);
|
||||
RUN_TEST(cypher_issue874_where_unsupported_func_error);
|
||||
RUN_TEST(cypher_multi_prop_projection_no_alias);
|
||||
RUN_TEST(cypher_exists_no_callers);
|
||||
RUN_TEST(cypher_exists_has_outgoing_calls);
|
||||
@@ -2994,4 +3219,7 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_parse_unwind);
|
||||
RUN_TEST(cypher_parse_unwind_var);
|
||||
RUN_TEST(cypher_wide_return_projection_bounded);
|
||||
/* Composite property projection (arrays/objects, escaped quotes) */
|
||||
RUN_TEST(cypher_exec_prop_array_with_internal_commas);
|
||||
RUN_TEST(cypher_exec_prop_string_with_escaped_quote);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,38 @@ TEST(fqn_compute_basic_c) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1077/#964: File-node QNs (name=="__file__") must preserve the FULL filename
|
||||
* so sibling files sharing a stem get DISTINCT nodes. Extension stripping is
|
||||
* kept for module/symbol QNs (name!=__file__) — load-bearing for C/C++
|
||||
* declaration↔definition resolution. */
|
||||
TEST(fqn_file_qn_preserves_dotfile_variants_issue1077) {
|
||||
/* .env / .env.local / .env.production all stripped to ".env" before,
|
||||
* colliding so only one File node survived per directory. */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env", "__file__"), "proj..env.__file__");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env.local", "__file__"),
|
||||
"proj..env.local.__file__");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env.production", "__file__"),
|
||||
"proj..env.production.__file__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964) {
|
||||
/* NodeController.h and NodeController.cpp both stripped to
|
||||
* "NodeController", so the header's File node was merged into the .cpp's. */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "NodeController.h", "__file__"),
|
||||
"proj.NodeController.h.__file__");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "NodeController.cpp", "__file__"),
|
||||
"proj.NodeController.cpp.__file__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_qn_still_strips_extension) {
|
||||
/* The MODULE/symbol QN keeps stripping — unchanged by the File-QN fix. */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "core.c", "init"), "proj.core.init");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "NodeController.cpp"), "proj.NodeController");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_rs) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "lib.rs", "new"), "proj.lib.new");
|
||||
PASS();
|
||||
@@ -559,6 +591,9 @@ SUITE(fqn) {
|
||||
RUN_TEST(fqn_compute_basic_ts);
|
||||
RUN_TEST(fqn_compute_basic_js);
|
||||
RUN_TEST(fqn_compute_basic_c);
|
||||
RUN_TEST(fqn_file_qn_preserves_dotfile_variants_issue1077);
|
||||
RUN_TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964);
|
||||
RUN_TEST(fqn_module_qn_still_strips_extension);
|
||||
RUN_TEST(fqn_compute_basic_rs);
|
||||
|
||||
/* fqn_compute: nested paths */
|
||||
|
||||
@@ -1242,6 +1242,100 @@ static int calls_edge_between(cbm_store_t *store, const char *project, const cha
|
||||
return found;
|
||||
}
|
||||
|
||||
/* #999: URLs in CI/tooling configs (.pre-commit-config.yaml, .github/
|
||||
* workflows) are repository references for TOOLING, not endpoints this
|
||||
* service exposes. They must not mint __route__infra__ Route nodes — the
|
||||
* route matcher's root-service heuristic otherwise attaches every handler
|
||||
* of an ambiguous "/" route to each junk URL (HANDLES churn on plain
|
||||
* pallets/flask). */
|
||||
static int count_infra_routes_matching(cbm_store_t *store, const char *project,
|
||||
const char *substr) {
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_store_find_nodes_by_label(store, project, "Route", &nodes, &count) != CBM_STORE_OK)
|
||||
return -1;
|
||||
int hits = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *qn = nodes[i].qualified_name;
|
||||
if (qn && strncmp(qn, "__route__infra__", (sizeof("__route__infra__") - 1)) == 0 &&
|
||||
strstr(qn, substr))
|
||||
hits++;
|
||||
}
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
return hits;
|
||||
}
|
||||
|
||||
TEST(contract_edge_no_infra_routes_from_ci_configs_issue999) {
|
||||
LangProj lp;
|
||||
static const LangFile f[] = {
|
||||
{"app.py", "from flask import Flask\n"
|
||||
"app = Flask(__name__)\n"
|
||||
"\n"
|
||||
"@app.route(\"/\")\n"
|
||||
"def index():\n"
|
||||
" return \"hi\"\n"
|
||||
"\n"
|
||||
"@app.route(\"/auth/login\")\n"
|
||||
"def login():\n"
|
||||
" return \"login\"\n"},
|
||||
{".pre-commit-config.yaml", "repos:\n"
|
||||
" - repo: https://github.com/astral-sh/ruff-pre-commit\n"
|
||||
" rev: v0.1.0\n"
|
||||
" hooks:\n"
|
||||
" - id: ruff\n"
|
||||
" - repo: https://github.com/pre-commit/pre-commit-hooks\n"
|
||||
" rev: v4.5.0\n"
|
||||
" hooks:\n"
|
||||
" - id: trailing-whitespace\n"},
|
||||
{".github/workflows/ci.yml", "name: CI\n"
|
||||
"on: [push]\n"
|
||||
"jobs:\n"
|
||||
" build:\n"
|
||||
" runs-on: ubuntu-latest\n"
|
||||
" steps:\n"
|
||||
" - uses: actions/checkout@v4\n"
|
||||
" - run: curl https://coverage.example.io/upload\n"}};
|
||||
cbm_store_t *store = lang_index_files(&lp, f, 3);
|
||||
ASSERT_NOT_NULL(store);
|
||||
/* No tooling URL may materialize as an infra Route node. */
|
||||
int gh_routes = count_infra_routes_matching(store, lp.project, "github.com");
|
||||
int wf_routes = count_infra_routes_matching(store, lp.project, "coverage.example.io");
|
||||
if (gh_routes != 0 || wf_routes != 0) {
|
||||
fprintf(stderr,
|
||||
" [999] FAIL infra routes from CI configs: github.com=%d workflows=%d "
|
||||
"(expected 0/0)\n",
|
||||
gh_routes, wf_routes);
|
||||
}
|
||||
ASSERT_EQ(gh_routes, 0);
|
||||
ASSERT_EQ(wf_routes, 0);
|
||||
lang_cleanup(&lp, store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #999 inverse guard: genuine infra endpoint URLs (Cloud Scheduler push
|
||||
* targets and similar deployment configs) must STILL mint infra Route
|
||||
* nodes — the CI/tooling deny must be file-scoped, not a blanket URL kill. */
|
||||
TEST(contract_edge_infra_routes_from_deploy_configs_still_minted) {
|
||||
LangProj lp;
|
||||
static const LangFile f[] = {
|
||||
{"scheduler.yaml",
|
||||
"jobs:\n"
|
||||
" - name: nightly-sync\n"
|
||||
" schedule: \"0 3 * * *\"\n"
|
||||
" push_endpoint: https://sync.internal.example/api/v1/sync\n"}};
|
||||
cbm_store_t *store = lang_index_files(&lp, f, 1);
|
||||
ASSERT_NOT_NULL(store);
|
||||
int routes = count_infra_routes_matching(store, lp.project, "sync.internal.example");
|
||||
if (routes < 1) {
|
||||
fprintf(stderr, " [999] FAIL deploy-config endpoint minted %d infra routes "
|
||||
"(expected >=1)\n",
|
||||
routes);
|
||||
}
|
||||
ASSERT_TRUE(routes >= 1);
|
||||
lang_cleanup(&lp, store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* True if some CALLS edge's TARGET node carries `label` and a QN ending with
|
||||
* `qn_suffix` — i.e. the call resolved to that specific definition. */
|
||||
static int calls_edge_targets(cbm_store_t *store, const char *project, const char *label,
|
||||
@@ -1563,6 +1657,8 @@ SUITE(lang_contract) {
|
||||
RUN_TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767);
|
||||
RUN_TEST(contract_edge_imports_alias_resolves_real_file_issue767);
|
||||
RUN_TEST(contract_edge_python_aliased_import_call_resolves_issue988);
|
||||
RUN_TEST(contract_edge_no_infra_routes_from_ci_configs_issue999);
|
||||
RUN_TEST(contract_edge_infra_routes_from_deploy_configs_still_minted);
|
||||
RUN_TEST(contract_edge_commonjs_require_call_resolves_issue871);
|
||||
RUN_TEST(contract_edge_depends_on);
|
||||
RUN_TEST(contract_edge_parallel_service_edges);
|
||||
|
||||
@@ -132,6 +132,13 @@ TEST(platform_default_workers_env_unset) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(platform_system_info) {
|
||||
cbm_system_info_t info = cbm_system_info();
|
||||
ASSERT_GT(info.total_cores, 0);
|
||||
ASSERT_GT(info.total_ram, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── cgroup-aware detection (Linux only) ─────────────────────────── */
|
||||
|
||||
#ifdef __linux__
|
||||
@@ -316,6 +323,7 @@ SUITE(platform) {
|
||||
RUN_TEST(platform_default_workers_env_override);
|
||||
RUN_TEST(platform_default_workers_env_invalid);
|
||||
RUN_TEST(platform_default_workers_env_unset);
|
||||
RUN_TEST(platform_system_info);
|
||||
#ifdef __linux__
|
||||
RUN_TEST(cgroup_v2_cpu_quota);
|
||||
RUN_TEST(cgroup_v2_cpu_quota_rounds_up);
|
||||
|
||||
Reference in New Issue
Block a user