Architecture
This document explains how GraphQLite is structured and how queries flow through the system.
High-Level Overview
┌─────────────────────────────────────────────────────────────┐
│ SQLite Extension │
├─────────────────────────────────────────────────────────────┤
│ cypher() function │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Parser │───▶│ Transform │───▶│ Executor │ │
│ └─────────┘ └───────────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Cypher AST SQL Query Results │
└─────────────────────────────────────────────────────────────┘
Components
Parser
The parser converts Cypher query text into an Abstract Syntax Tree (AST).
Implementation: Flex (lexer) + Bison (parser)
src/backend/parser/cypher_scanner.l- Tokenizersrc/backend/parser/cypher_gram.y- Grammarsrc/backend/parser/cypher_ast.c- AST construction
Transformer
The transformer converts the Cypher AST into SQL that can be executed against the graph schema.
Key files:
src/backend/transform/cypher_transform.c- Main entry pointsrc/backend/transform/transform_match.c- MATCH clause handlingsrc/backend/transform/transform_return.c- RETURN clause handlingsrc/backend/transform/sql_builder.c- SQL construction utilities
Executor
The executor runs the generated SQL and handles special cases like graph algorithms.
Key files:
src/backend/executor/cypher_executor.c- Main entry pointsrc/backend/executor/query_dispatch.c- Pattern-based routingsrc/backend/executor/graph_algorithms.c- Algorithm implementations
Query Flow
1. Entry Point
The cypher() SQL function receives the query:
// In extension.c
static void graphqlite_cypher_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
const char *query = (const char *)sqlite3_value_text(argv[0]);
// ...
}
2. Parsing
The query is tokenized and parsed:
cypher_parse_result *parse_result = parse_cypher_query_ext(query);
ast_node *ast = parse_result->root;
3. Pattern Dispatch
Instead of a giant if-else chain, queries are matched against patterns:
clause_flags flags = analyze_query_clauses(ast);
const query_pattern *pattern = find_matching_pattern(flags);
return pattern->handler(executor, ast, result, flags);
4. Transformation
The AST is converted to SQL using the unified SQL builder:
cypher_transform_context *ctx = create_transform_context(db);
transform_query(ctx, ast);
char *sql = sql_builder_to_string(ctx->unified_builder);
5. Execution
The SQL is executed against SQLite:
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
while (sqlite3_step(stmt) == SQLITE_ROW) {
// Process results
}
Design Decisions
Why SQLite?
- Zero configuration - single file, no server
- Ubiquitous - available everywhere
- Well-tested - decades of production use
- Extensible - clean extension API
Why Transform to SQL?
Rather than implementing our own storage engine, we transform Cypher to SQL:
- Leverage SQLite's query optimizer
- Benefit from SQLite's transaction handling
- Interop with regular SQL tables
- Simpler implementation
Why Pattern Dispatch?
Replacing if-else chains with table-driven dispatch:
- Easier to add new query patterns
- Clear priority ordering
- Better testability
- Reduced cyclomatic complexity
Extension Loading
When the extension loads:
- Register the
cypher()function - Create schema tables if they don't exist
- Create indexes for efficient lookups
int sqlite3_graphqlite_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
) {
SQLITE_EXTENSION_INIT2(pApi);
create_graph_schema(db);
sqlite3_create_function(db, "cypher", -1, SQLITE_UTF8, 0,
graphqlite_cypher_func, 0, 0);
return SQLITE_OK;
}