# How the Cypher Query Engine Handles openCypher Queries in codebase-memory-mcp

> Discover how the Cypher query engine in codebase-memory-mcp translates openCypher queries into SQL. Learn about its four-stage pipeline and safety features.

- Repository: [Martin Vogel/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)
- Tags: internals
- Published: 2026-07-17

---

**The Cypher query engine translates openCypher queries into parameterized SQLite SQL through a four-stage pipeline that tokenizes input, builds an abstract syntax tree, generates SQL joins, and enforces a hard-coded 100,000-row safety ceiling on unbounded patterns.**

The DeusData/codebase-memory-mcp repository implements a lightweight openCypher subset for querying internal knowledge graphs. This engine converts high-level graph pattern matching into efficient SQLite operations while protecting against runaway queries. Understanding how the Cypher query engine handles openCypher queries reveals the safety mechanisms and translation strategies that make graph traversal both flexible and secure.

## The Four-Stage Processing Pipeline

The engine processes every query through tightly coupled stages defined in [`src/cypher/cypher.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/cypher/cypher.c) and [`src/store/store.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/store/store.c).

### Lexing and Tokenization

The **hand-written lexer** scans raw query strings into tokens including identifiers, literals, operators, and brackets. Located in [`src/cypher/cypher.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/cypher/cypher.c) (lines 45–120), the lexer handles string escaping, number literals, comments, and two-character operators such as `!=` and `..` through functions like `lex_push`, `lex_string_literal`, and `lex_try_number`.

### Recursive-Descent Parsing

A **recursive-descent parser** constructs an abstract syntax tree (AST) representing node patterns, relationship patterns, optional matches, `WHERE` clauses, and return items. Core entry points include `parse_node`, `parse_rel`, and `parse_where`, with expression builders such as `parse_or_expr` and `parse_and_expr` handling logical combinations (lines 445–575). The parser explicitly rejects unsupported constructs like `CREATE`, `MERGE`, and `CALL`.

### AST-to-SQL Translation

The **planner** walks the AST to produce parameterized SQLite queries. Each `MATCH` pattern becomes a series of joins on the `node` and `edge` tables. Labels and relationship-type alternations encode as `IN` predicates, while optional matches render as `LEFT OUTER JOIN`s. The translation logic handles pagination through `LIMIT` and `SKIP` clauses, and aggregates like `COUNT` and `SUM`. Functions such as `parse_rel` and `parse_hop_range` convert patterns into SQL fragments before handing the final string to the store layer.

### Execution with Safety Controls

The **store module** ([`src/store/store.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/store/store.c)) executes the generated SQL using SQLite. The `cbm_query_graph` function (lines 350–420) handles query execution, while `cbm_db_open` (line 639) installs a custom authorizer via `sqlite3_set_authorizer`. This authorizer blocks disallowed statements including `ATTACH` and `DROP`, preventing SQL injection even when the Cypher parser tolerates open syntax. Result rows marshal back into JSON for the client.

## Handling Open and Unbounded Queries

The engine implements specific safeguards for **open queries**—those omitting explicit termination or containing variable-length hops.

### Variable-Length Hop Constraints

The parser recognizes the `*` token and `parse_hop_range` (lines 998–1020) extracts minimum and maximum hop counts. When the upper bound is omitted (`*` alone), the planner emits an unbounded join with a **hard-coded safety ceiling of 100,000 rows**. This limit protects the server from exponential path explosions while still allowing flexible traversal patterns.

### Default Pagination Limits

When queries lack an explicit `LIMIT` clause, the planner automatically injects a **default ceiling of 100,000 rows** via `WHERE rowid < 100000`. This behavior is documented in the CLI usage text (`query_graph` help in [`src/cli/cli.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/cli/cli.c), lines 590–595) and ensures that forgotten constraints cannot crash the system.

### SQL Injection Protection

All generated SQL passes through the SQLite authorizer registered in `cbm_db_open` (line 639). This layer rejects any SQL attempting to escape the intended query shape, providing defense-in-depth against injection attacks even when processing permissive openCypher syntax.

### Explicit Error Handling for Unsupported Clauses

If the parser encounters unsupported constructs like `CREATE`, `MERGE`, or `CALL`, it returns a clear error string via `unsupported_clause_error` (lines 813–838). The CLI propagates this message immediately, preventing malformed queries from reaching the SQL execution layer.

## Practical Code Examples

Execute openCypher queries through the `query_graph` tool dispatched by `handle_query_graph` in [`src/mcp/mcp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/mcp/mcp.c) (line 2912):

```bash

# Simple MATCH without LIMIT (triggers 100k default ceiling)

codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (n) RETURN n"}'

```

```bash

# Variable-length hop (unbounded pattern with safety limit)

codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (a)-[:CALL*]->(b) RETURN a,b"}'

```

```bash

# Explicit LIMIT to override default ceiling

codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (n) RETURN n LIMIT 50"}'

```

## Summary

- The **four-stage pipeline** (lexing, parsing, planning, execution) converts openCypher queries into parameterized SQLite SQL in [`src/cypher/cypher.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/cypher/cypher.c) and [`src/store/store.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/store/store.c).
- **Variable-length hops** (`*`) and omitted `LIMIT` clauses trigger automatic 100,000-row ceilings to prevent runaway queries.
- A **custom SQLite authorizer** blocks dangerous SQL operations like `ATTACH` and `DROP` at the database layer.
- **Explicit error handling** via `unsupported_clause_error` rejects `CREATE`, `MERGE`, and `CALL` clauses before SQL generation.
- The `handle_query_graph` function in [`src/mcp/mcp.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/mcp/mcp.c) serves as the entry point for all graph queries, coordinating the entire pipeline.

## Frequently Asked Questions

### What happens if I send a CREATE or MERGE clause to the engine?

The parser rejects these clauses immediately. The `unsupported_clause_error` function in [`src/cypher/cypher.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/cypher/cypher.c) (lines 813–838) returns a clear error message before any SQL generation occurs, as the engine currently supports only read-only `MATCH` queries.

### How does the engine prevent runaway queries with infinite hops?

For variable-length patterns like `*..` or unbounded `*`, the `parse_hop_range` function extracts hop limits and the planner enforces a hard-coded ceiling of 100,000 rows. This safety limit applies even when the Cypher syntax omits explicit bounds.

### Can malicious Cypher syntax execute arbitrary SQL commands?

No. While the Cypher parser is permissive, all generated SQL passes through a custom authorizer installed in `cbm_db_open` ([`src/store/store.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/store/store.c), line 639). This authorizer blocks `ATTACH`, `DROP`, and other dangerous statements, ensuring the query stays within the intended node/edge table joins.

### Where is the 100,000-row limit documented and enforced?

The default ceiling appears in the CLI help text for `query_graph` in [`src/cli/cli.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/cli/cli.c) (lines 590–595). The planner enforces it by injecting `WHERE rowid < 100000` into generated SQL when no explicit `LIMIT` is present, and the SQLite query planner respects this boundary during execution.