How the Cypher Query Engine Handles openCypher Queries in codebase-memory-mcp
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 and 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 (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 JOINs. 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) 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, 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 (line 2912):
# Simple MATCH without LIMIT (triggers 100k default ceiling)
codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (n) RETURN n"}'
# Variable-length hop (unbounded pattern with safety limit)
codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (a)-[:CALL*]->(b) RETURN a,b"}'
# 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.candsrc/store/store.c. - Variable-length hops (
*) and omittedLIMITclauses trigger automatic 100,000-row ceilings to prevent runaway queries. - A custom SQLite authorizer blocks dangerous SQL operations like
ATTACHandDROPat the database layer. - Explicit error handling via
unsupported_clause_errorrejectsCREATE,MERGE, andCALLclauses before SQL generation. - The
handle_query_graphfunction insrc/mcp/mcp.cserves 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 (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, 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 (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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →