# How Memgraph Stores and Queries Code Graph Relationships in code-graph-rag

> Discover how Memgraph stores and queries code graph relationships using labeled property graphs and efficient Cypher writes. Explore direct query capabilities with the CLI.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: internals
- Published: 2026-08-15

---

**Memgraph persists code relationships as labeled property graphs, with `MemgraphIngestor` handling batched Cypher writes and the CLI exposing direct query capabilities.**

The **code-graph-rag** repository transforms source code into queryable graph structures for retrieval-augmented generation (RAG) workflows. At its core, Memgraph serves as the primary storage engine for code entities—files, functions, classes, imports, and their relationships—enabling fast traversal of complex dependency and call graphs.

## Connecting to Memgraph: The `connect_memgraph` Factory

Before any data flows, the system establishes a driver connection through a centralized factory. In [`codebase_rag/main.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/main.py), the `connect_memgraph` function instantiates `MemgraphIngestor` with credentials sourced from environment or config:

```python
from codebase_rag.main import connect_memgraph

# Batch size controls transaction frequency for bulk inserts

with connect_memgraph(batch_size=500) as ingestor:
    # ingestor ready for node/edge operations

    pass

```

**Configuration parameters** (defined in [`codebase_rag/config.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/config.py)):
- `MEMGRAPH_HOST` — database hostname
- `MEMGRAPH_PORT` — Bolt protocol port (default 7687)
- `MEMGRAPH_USERNAME` / `MEMGRAPH_PASSWORD` — optional authentication

---

## How Memgraph Stores Code Graph Relationships

### The `MemgraphIngestor` Wrapper

The core storage abstraction lives in [`codebase_rag/services/graph_service.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/graph_service.py). The `MemgraphIngestor` class wraps the official `mgclient` driver and exposes three critical operations:

| Method | Purpose | Cypher Pattern |
|--------|---------|--------------|
| `add_node(label, properties)` | Create vertices | `CREATE (n:Label {props})` |
| `add_edge(from_node, to_node, rel_type, properties)` | Create relationships | `MATCH (a), (b) CREATE (a)-[r:TYPE {props}]->(b)` |
| `run_cypher(query)` | Execute arbitrary queries | User-supplied Cypher |

### Node and Relationship Schema

**Nodes** represent code artifacts with consistent property sets:

- **`File`** — `path`, `line_start`, `line_end`
- **`Function`** / **`Method`** — `name`, `fqn` (fully qualified name), `signature`, `line_start`
- **`Class`** — `name`, `fqn`, `line_start`, `line_end`
- **`Variable`** — `name`, `type_hint` (when available)

**Relationships** encode semantic connections:

- `CALLS` — function-to-function invocations
- `IMPORTS` — file-to-file module dependencies
- `DECLARES` — container-to-member definitions (class→method, file→function)
- `EXTENDS` — inheritance hierarchies

### Batched Insertion for Performance

The ingestor accumulates operations in memory and commits transactions when the batch threshold is reached. This minimizes network round-trips during large codebase ingestion:

```python

# From trace ingestion workflow

with connect_memgraph(batch_size=1000) as ingestor:
    for record in parsed_trace:
        ingestor.add_node("Function", {
            "name": record.func_name,
            "fqn": record.fqn,
            "line_start": record.lineno
        })
        ingestor.add_edge(
            from_node={"fqn": record.caller_fqn},
            to_node={"fqn": record.callee_fqn},
            rel_type="CALLS",
            properties={"line": record.call_site}
        )
    # Final commit on context exit

```

---

## How Memgraph Queries Code Graph Relationships

### CLI Query Interface

The `cgr graph query` command in [`codebase_rag/cli.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/cli.py) provides direct Cypher access:

```bash

# Find all functions calling a specific target

cgr graph query 'MATCH (f:Function)-[:CALLS]->(t:Function {name:"process_data"}) RETURN f.fqn'

```

**Implementation** (lines 298-306):

```python
with connect_memgraph(batch_size=1) as ingestor:
    result = ingestor.run_cypher(user_query)
    for row in result:
        print(row)

```

### Programmatic Query Patterns

#### 1. Call Graph Traversal

```cypher
// Direct callers of a function
MATCH (caller:Function)-[:CALLS]->(callee:Function {fqn: "mymod.utils.process"})
RETURN caller.fqn AS upstream_function, caller.path AS source_file

```

#### 2. Import Dependency Tree

```cypher
// Transitive imports from an entry point (limited depth)
MATCH (entry:File {path: "/src/main.py"})-[:IMPORTS*1..3]->(dep:File)
RETURN entry.path AS root, dep.path AS dependency, length(path) AS depth

```

#### 3. Class Hierarchy Analysis

```cypher
// Full inheritance chain
MATCH (subclass:Class)-[:EXTENDS*]->(base:Class)
WITH subclass, base ORDER BY length(path)
RETURN subclass.fqn AS class_name, collect(base.fqn) AS inherits_from

```

#### 4. Symbol Definition Lookup

```cypher
// Where is this function declared?
MATCH (f:Function {name: "handle_request"})
MATCH (parent)-[:DECLARES]->(f)
RETURN parent.path AS defined_in, f.line_start AS line_number

```

### Exporting Graph Data

The `export_graph_to_dict()` method serializes the entire graph for external processing:

```python
with connect_memgraph(batch_size=1000) as ingestor:
    graph_data = ingestor.export_graph_to_dict()
    # Returns: {"nodes": [...], "relationships": [...]}

    # Suitable for JSON export or visualization tools

```

---

## Trace Ingestion: From Runtime Data to Stored Graph

The `cgr trace ingest` command bridges runtime profiling and graph storage. Located in [`codebase_rag/trace/cli.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/trace/cli.py) (lines 41-56), it:

1. Parses profiler output (execution traces with call stacks)
2. Resolves symbols to source file locations
3. Streams normalized records to `MemgraphIngestor`

```python

# Simplified trace ingestion flow

from codebase_rag.trace.ingest import ingest_trace
from pathlib import Path

trace_file = Path("profiling_output.trace")
repo_root = Path("/path/to/analyzed/repo")

with connect_memgraph(batch_size=500) as ingestor:
    ingest_trace(
        trace_path=trace_file,
        ingestor=ingestor,
        repo_path=repo_root,
        project_name="my_service"
    )

```

This populates `CALLS` relationships with actual runtime frequency data, augmenting static analysis graphs with dynamic execution patterns.

---

## Configuration and Deployment

Environment variables control Memgraph connectivity without code changes:

```bash
export MEMGRAPH_HOST=memgraph.internal
export MEMGRAPH_PORT=7687
export MEMGRAPH_USERNAME=codegraph
export MEMGRAPH_PASSWORD=secure_token

# Now all CLI commands use these credentials

cgr trace ingest --trace data.trace --repo ./src

```

For local development, the default `localhost:7687` with no authentication applies.

---

## Summary

- **`MemgraphIngestor`** in [`codebase_rag/services/graph_service.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/graph_service.py) abstracts all Memgraph interactions—connection management, batched writes, and query execution.
- **Code entities** map to labeled nodes (`File`, `Function`, `Class`, `Variable`) with `fqn` and location properties.
- **Relationships** use directed edges (`CALLS`, `IMPORTS`, `DECLARES`, `EXTENDS`) with optional metadata.
- **Batching** via `batch_size` parameter optimizes bulk ingestion throughput.
- **CLI entry points** (`cgr trace ingest`, `cgr graph query`) and programmatic APIs provide flexible access patterns.
- **Configuration** through [`codebase_rag/config.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/config.py) or environment variables enables deployment portability.

---

## Frequently Asked Questions

### What Cypher statements does `MemgraphIngestor` generate internally?

The ingestor produces standard Cypher `CREATE` and `MERGE` statements. Nodes use parameterized `CREATE (n:Label {props})` with unique `fqn` values as identifiers. Relationships execute as `MATCH` followed by `CREATE` to link resolved node references, ensuring idempotent inserts when `MERGE` is specified.

### How does batching affect transaction boundaries?

Each batch accumulates in memory until reaching `batch_size`, then commits as a single transaction. Larger batches improve throughput but increase memory usage; smaller batches provide more frequent persistence points. The context manager ensures final commit on normal exit and rollback on exceptions.

### Can I query Memgraph directly without the Python wrapper?

Yes. The database exposes Bolt protocol access on the configured port. You can connect with any Cypher client (mgconsole, Neo4j Browser, or language drivers) using the same host/port/credentials specified in [`codebase_rag/config.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/config.py). The `export_graph_to_dict()` method provides a bridge for external tools expecting JSON graph formats.

### What distinguishes `CALLS` relationships from `DECLARES`?

`CALLS` captures dynamic invocation at runtime or static analysis call sites—function A executes function B. `DECLARES` captures static containment—file or class B contains definition of member A. Call graphs answer "what executes," while declaration trees answer "where defined."