Skip to content

Commit 9021c22

Browse files
testdLo999
andcommitted
feat: include property definitions per label in get_graph_schema
Adds per-label/type property key discovery via json_each() so users can see which properties are available for Cypher queries without trial and error. Base columns listed first, JSON keys appended (capped at 50). Closes #179. Based on #181 by dLo999. Co-Authored-By: Dustin Obrecht <dustin@kurtnoble.com>
1 parent a8406bd commit 9021c22

4 files changed

Lines changed: 94 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ codebase-memory-mcp cli --raw search_graph '{"label": "Function"}' | jq '.result
317317
| `trace_call_path` | BFS traversal — who calls a function and what it calls. Depth 1-5. |
318318
| `detect_changes` | Map git diff to affected symbols + blast radius with risk classification. |
319319
| `query_graph` | Execute Cypher-like graph queries (read-only). |
320-
| `get_graph_schema` | Node/edge counts, relationship patterns. Run this first. |
320+
| `get_graph_schema` | Node/edge counts, relationship patterns, property definitions per label. Run this first. |
321321
| `get_code_snippet` | Read source code for a function by qualified name. |
322322
| `get_architecture` | Codebase overview: languages, packages, routes, hotspots, clusters, ADR. |
323323
| `search_code` | Grep-like text search within indexed project files. |

src/mcp/mcp.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,11 @@ static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) {
994994
yyjson_mut_val *lbl = yyjson_mut_obj(doc);
995995
yyjson_mut_obj_add_str(doc, lbl, "label", schema.node_labels[i].label);
996996
yyjson_mut_obj_add_int(doc, lbl, "count", schema.node_labels[i].count);
997+
yyjson_mut_val *props = yyjson_mut_arr(doc);
998+
for (int j = 0; j < schema.node_labels[i].property_count; j++) {
999+
yyjson_mut_arr_add_str(doc, props, schema.node_labels[i].properties[j]);
1000+
}
1001+
yyjson_mut_obj_add_val(doc, lbl, "properties", props);
9971002
yyjson_mut_arr_add_val(labels, lbl);
9981003
}
9991004
yyjson_mut_obj_add_val(doc, root, "node_labels", labels);
@@ -1003,6 +1008,11 @@ static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) {
10031008
yyjson_mut_val *typ = yyjson_mut_obj(doc);
10041009
yyjson_mut_obj_add_str(doc, typ, "type", schema.edge_types[i].type);
10051010
yyjson_mut_obj_add_int(doc, typ, "count", schema.edge_types[i].count);
1011+
yyjson_mut_val *eprops = yyjson_mut_arr(doc);
1012+
for (int j = 0; j < schema.edge_types[i].property_count; j++) {
1013+
yyjson_mut_arr_add_str(doc, eprops, schema.edge_types[i].properties[j]);
1014+
}
1015+
yyjson_mut_obj_add_val(doc, typ, "properties", eprops);
10061016
yyjson_mut_arr_add_val(types, typ);
10071017
}
10081018
yyjson_mut_obj_add_val(doc, root, "edge_types", types);

src/store/store.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,6 +2696,36 @@ int cbm_deduplicate_hops(const cbm_node_hop_t *hops, int hop_count, cbm_node_hop
26962696

26972697
/* ── Schema ─────────────────────────────────────────────────────── */
26982698

2699+
enum { SCHEMA_MAX_JSON_KEYS = 50 };
2700+
2701+
/* Discover distinct JSON property keys for a table/column via json_each().
2702+
* Prepends base_cols, then appends up to SCHEMA_MAX_JSON_KEYS from the query.
2703+
* Caller must free the returned array and each string in it. */
2704+
static void schema_discover_props(sqlite3 *db, const char *sql, const char *project,
2705+
const char *filter, const char **base_cols, int base_col_count,
2706+
char ***out_props, int *out_count) {
2707+
int pcap = base_col_count + SCHEMA_MAX_JSON_KEYS;
2708+
char **props = malloc(pcap * sizeof(char *));
2709+
int pn = 0;
2710+
2711+
for (int b = 0; b < base_col_count; b++) {
2712+
props[pn++] = heap_strdup(base_cols[b]);
2713+
}
2714+
2715+
sqlite3_stmt *pstmt = NULL;
2716+
if (sqlite3_prepare_v2(db, sql, CBM_NOT_FOUND, &pstmt, NULL) == SQLITE_OK) {
2717+
bind_text(pstmt, SKIP_ONE, project);
2718+
bind_text(pstmt, PAIR_LEN, filter);
2719+
while (sqlite3_step(pstmt) == SQLITE_ROW && pn < pcap) {
2720+
props[pn++] = heap_strdup((const char *)sqlite3_column_text(pstmt, 0));
2721+
}
2722+
sqlite3_finalize(pstmt);
2723+
}
2724+
2725+
*out_props = props;
2726+
*out_count = pn;
2727+
}
2728+
26992729
int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t *out) {
27002730
memset(out, 0, sizeof(*out));
27012731
if (!s || !s->db) {
@@ -2720,13 +2750,34 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t
27202750
}
27212751
arr[n].label = heap_strdup((const char *)sqlite3_column_text(stmt, 0));
27222752
arr[n].count = sqlite3_column_int(stmt, SKIP_ONE);
2753+
arr[n].properties = NULL;
2754+
arr[n].property_count = 0;
27232755
n++;
27242756
}
27252757
sqlite3_finalize(stmt);
27262758
out->node_labels = arr;
27272759
out->node_label_count = n;
27282760
}
27292761

2762+
/* Node label property keys: base columns + distinct JSON property keys per label */
2763+
{
2764+
static const char *node_base_cols[] = {"name", "qualified_name", "file_path", "start_line",
2765+
"end_line"};
2766+
const char *prop_sql = "SELECT DISTINCT je.key "
2767+
"FROM nodes, json_each(nodes.properties) AS je "
2768+
"WHERE nodes.project = ?1 AND nodes.label = ?2 "
2769+
" AND nodes.properties != '{}' "
2770+
"ORDER BY je.key "
2771+
"LIMIT 50;";
2772+
2773+
for (int i = 0; i < out->node_label_count; i++) {
2774+
schema_discover_props(
2775+
s->db, prop_sql, project, out->node_labels[i].label, node_base_cols,
2776+
(int)(sizeof(node_base_cols) / sizeof(node_base_cols[0])),
2777+
&out->node_labels[i].properties, &out->node_labels[i].property_count);
2778+
}
2779+
}
2780+
27302781
/* Edge types */
27312782
{
27322783
const char *sql = "SELECT type, COUNT(*) FROM edges WHERE project = ?1 GROUP BY type ORDER "
@@ -2745,13 +2796,33 @@ int cbm_store_get_schema(cbm_store_t *s, const char *project, cbm_schema_info_t
27452796
}
27462797
arr[n].type = heap_strdup((const char *)sqlite3_column_text(stmt, 0));
27472798
arr[n].count = sqlite3_column_int(stmt, SKIP_ONE);
2799+
arr[n].properties = NULL;
2800+
arr[n].property_count = 0;
27482801
n++;
27492802
}
27502803
sqlite3_finalize(stmt);
27512804
out->edge_types = arr;
27522805
out->edge_type_count = n;
27532806
}
27542807

2808+
/* Edge type property keys: base columns + distinct JSON property keys per type */
2809+
{
2810+
static const char *edge_base_cols[] = {"source_id", "target_id"};
2811+
const char *prop_sql = "SELECT DISTINCT je.key "
2812+
"FROM edges, json_each(edges.properties) AS je "
2813+
"WHERE edges.project = ?1 AND edges.type = ?2 "
2814+
" AND edges.properties != '{}' "
2815+
"ORDER BY je.key "
2816+
"LIMIT 50;";
2817+
2818+
for (int i = 0; i < out->edge_type_count; i++) {
2819+
schema_discover_props(s->db, prop_sql, project, out->edge_types[i].type, edge_base_cols,
2820+
(int)(sizeof(edge_base_cols) / sizeof(edge_base_cols[0])),
2821+
&out->edge_types[i].properties,
2822+
&out->edge_types[i].property_count);
2823+
}
2824+
}
2825+
27552826
return CBM_STORE_OK;
27562827
}
27572828

@@ -2761,11 +2832,19 @@ void cbm_store_schema_free(cbm_schema_info_t *out) {
27612832
}
27622833
for (int i = 0; i < out->node_label_count; i++) {
27632834
free((void *)out->node_labels[i].label);
2835+
for (int j = 0; j < out->node_labels[i].property_count; j++) {
2836+
free(out->node_labels[i].properties[j]);
2837+
}
2838+
free(out->node_labels[i].properties);
27642839
}
27652840
free(out->node_labels);
27662841

27672842
for (int i = 0; i < out->edge_type_count; i++) {
27682843
free((void *)out->edge_types[i].type);
2844+
for (int j = 0; j < out->edge_types[i].property_count; j++) {
2845+
free(out->edge_types[i].properties[j]);
2846+
}
2847+
free(out->edge_types[i].properties);
27692848
}
27702849
free(out->edge_types);
27712850

src/store/store.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,15 @@ typedef struct {
163163
typedef struct {
164164
const char *label;
165165
int count;
166+
char **properties; /* distinct property keys for this label (base + JSON) */
167+
int property_count;
166168
} cbm_label_count_t;
167169

168170
typedef struct {
169171
const char *type;
170172
int count;
173+
char **properties; /* distinct property keys for this edge type (base + JSON) */
174+
int property_count;
171175
} cbm_type_count_t;
172176

173177
typedef struct {

0 commit comments

Comments
 (0)