CLANGSQL Agent Guide

July 9, 2026 · View on GitHub

SQL interface for C/C++ source code analysis using Clang AST.

Key Concepts

Why Filter System Headers?

Every C++ file includes system headers (<vector>, <string>, etc.). These add thousands of symbols to the database. Almost always, you want user code only:

WHERE is_system = 0

Declaration vs Definition

C++ separates declaration (announces existence) from definition (provides implementation):

  • A header declares void foo();
  • A source file defines void foo() { ... }

Use is_definition = 1 to find implementations:

SELECT name FROM functions WHERE is_definition = 1 AND is_system = 0;

USR (Unified Symbol Resolution)

Each symbol has a unique usr string that's consistent across translation units. Use it to:

  • Match the same symbol across files
  • Join calls to functions: calls.callee_usr = functions.usr
  • Track inheritance: inheritance.base_usr = classes.usr

Use id for simple same-table foreign keys (e.g., methods.class_id = classes.id).

Translation Units and Schemas

Each source file is a translation unit. When analyzing multiple files:

clangsql file1.cpp:a file2.cpp:b -i

Tables become prefixed: a_functions, b_functions. Use USR to correlate symbols across schemas.

CLI Usage

clangsql main.cpp -e "SELECT name FROM functions"      # Single query
clangsql main.cpp -i                                    # Interactive REPL
clangsql main.cpp --http 8080                           # HTTP server mode
clangsql main.cpp --mcp                                 # MCP server mode (SSE)
clangsql main.cpp -- -std=c++17 -I./include            # Pass Clang flags

Project Mode

Analyze entire codebases with a unified schema. Files are deduplicated by USR, giving you a single view across all translation units.

# Analyze all .cpp files in a directory
clangsql --project ./src -i

# Custom file patterns
clangsql --project ./src --pattern "*.cpp" --pattern "*.cxx" -i

# Exclude directories
clangsql --project ./src --exclude build --exclude third_party -i

# With Clang flags
clangsql --project ./src -- -std=c++17 -I./include -DDEBUG

AST Caching (Speed Up Re-parses)

Enable caching to avoid re-parsing unchanged files. Cache validates source AND all include file mtimes.

# Enable caching (disabled by default)
clangsql --project ./src --cache -i

# With verbose output (shows cache hits/misses)
clangsql --project ./src --cache --cache-verbose -i

# Custom cache directory
clangsql --project ./src --cache-dir C:/my/cache -i

# Clear all cached files
clangsql --clear-cache

Cache location: %LOCALAPPDATA%\clangsql\cache (Windows) or ~/.cache/clangsql (Linux/macOS)

When to use: Enable --cache for repeated analysis of the same project. First parse is slightly slower (writes cache), but subsequent runs are much faster for unchanged files.

How Deduplication Works

  • Functions, classes, methods, variables, enums: Deduplicated by USR (same symbol = one row)
  • Files: Deduplicated by normalized path
  • Calls, inheritance, string_literals: NOT deduplicated (each occurrence preserved with file_id)

Project Mode Queries

-- Files in the project
SELECT path FROM files WHERE is_system = 0;

-- Functions with their source files
SELECT f.name, fi.path
FROM functions f
JOIN files fi ON f.file_id = fi.id
WHERE f.is_system = 0;

-- Calls by source file
SELECT fi.path, c.callee_name, COUNT(*) as call_count
FROM calls c
JOIN files fi ON c.file_id = fi.id
WHERE c.is_system = 0
GROUP BY fi.path, c.callee_name
ORDER BY call_count DESC;

-- String literals across the project
SELECT value, COUNT(*) as occurrences
FROM string_literals
WHERE is_system = 0
GROUP BY value
ORDER BY occurrences DESC LIMIT 20;

-- Functions per file
SELECT fi.path, COUNT(f.id) as func_count
FROM files fi
LEFT JOIN functions f ON f.file_id = fi.id
WHERE fi.is_system = 0
GROUP BY fi.id
ORDER BY func_count DESC;

Tables (12 total)

All tables have is_system column (1=system header, 0=user code).

files

ColumnTypeDescription
idINTPrimary key
pathTEXTFull file path
is_main_fileINT1=main TU file
mtimeINTFile modification time (Unix epoch seconds)
is_headerINT1=header file
is_systemINT1=system include

functions

ColumnTypeDescription
idINTPrimary key
usrTEXTUnique symbol ID (for cross-TU matching)
nameTEXTFunction name
qualified_nameTEXTFull namespace::name
return_typeTEXTReturn type
file_idINTFK to files
line, columnINTStart position
end_line, end_columnINTEnd position
is_definitionINT1=definition, 0=declaration
is_inlineINT1=inline
is_variadicINT1=has ...
is_staticINT1=static linkage

classes

ColumnTypeDescription
idINTPrimary key
usrTEXTUnique symbol ID
nameTEXTClass name
qualified_nameTEXTFull name
kindTEXT"class", "struct", "union"
file_idINTFK to files
lineINTLine number
is_definitionINT1=definition
is_abstractINT1=has pure virtual
namespace_idINTFK to enclosing namespace (0 = global)

methods

ColumnTypeDescription
idINTPrimary key
usrTEXTUnique symbol ID
class_idINTFK to classes
nameTEXTMethod name
qualified_nameTEXTFull name
return_typeTEXTReturn type
accessTEXT"public", "protected", "private"
is_virtualINT1=virtual
is_pure_virtualINT1=pure virtual (= 0)
is_staticINT1=static method
is_constINT1=const method
is_overrideINT1=marked override
is_finalINT1=marked final
line, column, end_line, end_columnINTPosition

fields

ColumnTypeDescription
idINTPrimary key
class_idINTFK to classes
nameTEXTField name
typeTEXTField type
accessTEXTAccess level
is_staticINT1=static member
is_mutableINT1=mutable
bit_widthINTBit field width (0=normal)
offset_bitsINTOffset from class start

variables

ColumnTypeDescription
idINTPrimary key
usrTEXTUnique symbol ID
nameTEXTVariable name
typeTEXTType
file_idINTFK to files
lineINTLine number
function_idINTFK to functions (0 when parent is a method or when global)
method_idINTFK to methods (0 when parent is a free function or when global)
namespace_idINTFK to enclosing namespace (0 = global / non-namespace scope)
scope_kindTEXT"global", "local", "static_local"
storage_classTEXT"static", "extern", "none"
is_constINT1=const
is_inlineINT1=inline (inline keyword)

Exactly one of function_id / method_id is non-zero for locals; both are 0 for globals.

parameters

ColumnTypeDescription
idINTPrimary key
function_idINTFK to functions (0 when owner is a method)
method_idINTFK to methods (0 when owner is a free function)
nameTEXTParameter name
typeTEXTParameter type
index_INT0-based position
has_defaultINT1=has default value

Exactly one of function_id / method_id is non-zero per parameter. Free-function parameters join functions; method/constructor/destructor parameters join methods.

enums

ColumnTypeDescription
idINTPrimary key
usrTEXTUnique symbol ID
nameTEXTEnum name
underlying_typeTEXTInteger type
is_scopedINT1=enum class
file_idINTFK to files
lineINTLine number

enum_values

ColumnTypeDescription
idINTPrimary key
enum_idINTFK to enums
nameTEXTConstant name
valueINTInteger value

calls

Function call graph.

ColumnTypeDescription
idINTPrimary key
caller_usrTEXTCalling function USR
callee_usrTEXTCalled function USR
callee_nameTEXTCalled function name
line, columnINTCall site position
is_virtualINT1=virtual method call
file_idINTFK to files (project mode)

inheritance

Class hierarchy.

ColumnTypeDescription
idINTPrimary key
derived_usrTEXTDerived class USR
derived_nameTEXTDerived class name
base_usrTEXTBase class USR
base_nameTEXTBase class name
accessTEXT"public", "protected", "private"
is_virtualINT1=virtual inheritance
file_idINTFK to files (project mode)

string_literals

String constants found in code, with direct FKs to the enclosing callable for fast queries.

ColumnTypeDescription
idINTPrimary key
contentTEXTString content (unescaped)
file_idINTFK to files
lineINTLine number
columnINTColumn number
function_usrTEXTEnclosing callable USR (empty if global)
func_idINTFK to functions (0 when enclosing is a method or global)
method_idINTFK to methods (0 when enclosing is a free function or global)
is_wideINT1=wide string (L"...")
is_systemINT1=system header

Exactly one of func_id / method_id is non-zero for strings inside a callable; both are 0 for global strings. Join the appropriate parent table based on which column is set.

Fast String Queries:

-- All strings in a specific free function (O(1) lookup)
SELECT content FROM string_literals WHERE func_id = 42;

-- All strings in a specific method (O(1) lookup)
SELECT content FROM string_literals WHERE method_id = 7;

-- Which free function has the most string literals? (fast GROUP BY)
SELECT f.name, COUNT(*) as count
FROM string_literals s
JOIN functions f ON s.func_id = f.id
WHERE s.is_system = 0 AND f.is_system = 0
GROUP BY s.func_id
ORDER BY count DESC LIMIT 10;

-- Which method has the most string literals?
SELECT m.qualified_name, COUNT(*) as count
FROM string_literals s
JOIN methods m ON s.method_id = m.id
WHERE s.is_system = 0 AND m.is_system = 0
GROUP BY s.method_id
ORDER BY count DESC LIMIT 10;

-- Free functions with hardcoded "password" strings
SELECT DISTINCT f.name
FROM string_literals s
JOIN functions f ON s.func_id = f.id
WHERE s.content LIKE '%password%' AND s.is_system = 0;

-- Methods with hardcoded "password" strings
SELECT DISTINCT m.qualified_name
FROM string_literals s
JOIN methods m ON s.method_id = m.id
WHERE s.content LIKE '%password%' AND s.is_system = 0;

-- All callables (free functions and methods) with "password"
SELECT 'function' AS kind, f.name FROM string_literals s
  JOIN functions f ON s.func_id = f.id
  WHERE s.content LIKE '%password%' AND s.is_system = 0
UNION ALL
SELECT 'method' AS kind, m.qualified_name FROM string_literals s
  JOIN methods m ON s.method_id = m.id
  WHERE s.content LIKE '%password%' AND s.is_system = 0;

-- Global strings (not inside any callable)
SELECT content FROM string_literals
WHERE func_id = 0 AND method_id = 0 AND is_system = 0;

Aggregates

blob_concat(value) concatenates BLOB inputs and INTEGER 0-255 values into one BLOB. NULL inputs are skipped; TEXT or out-of-range INTs error. Use over an ordered row source.

There are no custom views or extra scalar functions — query the 12 tables above directly, along with the standard SQLite built-ins.

Common Queries

Basic Queries

-- User functions only (exclude system headers)
SELECT name, return_type FROM functions WHERE is_system = 0;

-- Classes with method counts
SELECT c.name, COUNT(m.id) as methods
FROM classes c LEFT JOIN methods m ON m.class_id = c.id
WHERE c.is_system = 0 GROUP BY c.id ORDER BY methods DESC;

-- Virtual methods
SELECT c.name, m.name FROM methods m
JOIN classes c ON m.class_id = c.id
WHERE m.is_virtual = 1 AND m.is_system = 0;

-- Function size (line count)
SELECT name, end_line - line + 1 as lines
FROM functions WHERE is_definition = 1
ORDER BY lines DESC LIMIT 10;

Call Graph Analysis

-- Who calls what
SELECT f.name as caller, c.callee_name
FROM calls c JOIN functions f ON c.caller_usr = f.usr
WHERE c.is_system = 0;

-- Most called functions
SELECT callee_name, COUNT(*) as calls
FROM calls WHERE is_system = 0
GROUP BY callee_name ORDER BY calls DESC LIMIT 10;

-- Functions calling a specific function
SELECT DISTINCT f.name
FROM functions f
JOIN calls c ON c.caller_usr = f.usr
WHERE c.callee_name = 'malloc';

-- Leaf functions (call nothing)
SELECT f.name FROM functions f
LEFT JOIN calls c ON c.caller_usr = f.usr
WHERE c.id IS NULL AND f.is_system = 0 AND f.is_definition = 1;

-- Call graph depth with recursive CTE
WITH RECURSIVE callgraph AS (
    SELECT usr, name, 0 as depth
    FROM functions WHERE name = 'main'
    UNION ALL
    SELECT f.usr, f.name, cg.depth + 1
    FROM callgraph cg
    JOIN calls c ON c.caller_usr = cg.usr
    JOIN functions f ON f.usr = c.callee_usr
    WHERE cg.depth < 5
)
SELECT DISTINCT name, MIN(depth) as min_depth
FROM callgraph GROUP BY name ORDER BY min_depth;

Class Hierarchy

-- Inheritance relationships
SELECT derived_name, base_name, access
FROM inheritance WHERE is_system = 0;

-- Find all subclasses of a class
SELECT derived_name FROM inheritance WHERE base_name = 'BaseClass';

-- Classes with no base class (root classes)
SELECT c.name FROM classes c
LEFT JOIN inheritance i ON i.derived_usr = c.usr
WHERE i.id IS NULL AND c.is_system = 0;

-- Full hierarchy with recursive CTE
WITH RECURSIVE hierarchy AS (
    SELECT usr, name, 0 as depth
    FROM classes WHERE name = 'BaseClass'
    UNION ALL
    SELECT c.usr, c.name, h.depth + 1
    FROM hierarchy h
    JOIN inheritance i ON i.base_usr = h.usr
    JOIN classes c ON c.usr = i.derived_usr
)
SELECT name, depth FROM hierarchy ORDER BY depth, name;

-- Abstract classes (have pure virtual methods)
SELECT name FROM classes WHERE is_abstract = 1 AND is_system = 0;

API Surface Analysis

-- Public methods (the API)
SELECT c.name, m.name, m.return_type
FROM methods m JOIN classes c ON m.class_id = c.id
WHERE m.access = 'public' AND c.is_system = 0
ORDER BY c.name, m.name;

-- Private methods (implementation details)
SELECT c.name, m.name
FROM methods m JOIN classes c ON m.class_id = c.id
WHERE m.access = 'private' AND c.is_system = 0;

-- Class fields layout with offsets
SELECT c.name, f.name, f.type, f.offset_bits
FROM fields f JOIN classes c ON f.class_id = c.id
WHERE c.is_system = 0 ORDER BY c.name, f.offset_bits;

-- Const methods (don't modify state)
SELECT c.name, m.name FROM methods m
JOIN classes c ON m.class_id = c.id
WHERE m.is_const = 1 AND c.is_system = 0;

Code Metrics

-- Free functions by parameter count
SELECT f.name, COUNT(p.id) as param_count
FROM functions f
LEFT JOIN parameters p ON p.function_id = f.id
WHERE f.is_system = 0
GROUP BY f.id ORDER BY param_count DESC LIMIT 20;

-- Methods by parameter count
SELECT m.qualified_name, COUNT(p.id) as param_count
FROM methods m
LEFT JOIN parameters p ON p.method_id = m.id
WHERE m.is_system = 0
GROUP BY m.id ORDER BY param_count DESC LIMIT 20;

-- Classes by size (field count)
SELECT c.name, COUNT(f.id) as field_count
FROM classes c LEFT JOIN fields f ON f.class_id = c.id
WHERE c.is_system = 0
GROUP BY c.id ORDER BY field_count DESC;

-- Files by function count
SELECT fi.path, COUNT(fu.id) as func_count
FROM files fi
LEFT JOIN functions fu ON fu.file_id = fi.id
WHERE fi.is_system = 0
GROUP BY fi.id ORDER BY func_count DESC;

-- Return type distribution
SELECT return_type, COUNT(*) as count
FROM functions WHERE is_system = 0
GROUP BY return_type ORDER BY count DESC LIMIT 10;

Static Analysis Patterns

-- Unused parameters (unnamed) on free functions
SELECT f.name as function, p.type as param_type
FROM parameters p
JOIN functions f ON p.function_id = f.id
WHERE (p.name = '' OR p.name IS NULL) AND f.is_system = 0;

-- Unused parameters (unnamed) on methods
SELECT m.qualified_name as method, p.type as param_type
FROM parameters p
JOIN methods m ON p.method_id = m.id
WHERE (p.name = '' OR p.name IS NULL) AND m.is_system = 0;

-- Methods that might be const (non-const, non-setter)
SELECT c.name, m.name
FROM methods m JOIN classes c ON m.class_id = c.id
WHERE m.is_const = 0 AND m.is_static = 0
  AND m.name NOT LIKE 'set%'
  AND m.name NOT LIKE 'operator%'
  AND c.is_system = 0;

-- Static variables (potential globals)
SELECT name, type, scope_kind
FROM variables WHERE storage_class = 'static' AND is_system = 0;

-- Large enums (many values)
SELECT e.name, COUNT(ev.id) as value_count
FROM enums e JOIN enum_values ev ON ev.enum_id = e.id
WHERE e.is_system = 0
GROUP BY e.id ORDER BY value_count DESC;

Cross-File Analysis

When multiple files are attached with schema prefixes:

-- Attach files
-- clangsql file1.cpp:f1 file2.cpp:f2 -i

-- Find functions defined in both files (ODR candidates)
SELECT f1.name
FROM f1_functions f1
JOIN f2_functions f2 ON f1.name = f2.name
WHERE f1.is_definition = 1 AND f2.is_definition = 1;

-- Cross-file calls
SELECT f1.name as caller, c.callee_name
FROM f1_calls c
JOIN f1_functions f1 ON c.caller_usr = f1.usr
JOIN f2_functions f2 ON c.callee_usr = f2.usr;

Tips

  • Always filter system headers: WHERE is_system = 0 unless you specifically want STL/OS symbols
  • Use USR for cross-table matching: calls.caller_usr = functions.usr
  • Use id for parent-child: methods.class_id = classes.id
  • Pattern matching: LIKE 'get%', LIKE '%Handler%', LIKE '%_t'
  • Count first: Before complex queries, SELECT COUNT(*) FROM table WHERE is_system = 0

Common Mistakes

-- WRONG: Missing system filter (returns thousands of STL symbols)
SELECT name FROM functions;

-- RIGHT: Filter to user code
SELECT name FROM functions WHERE is_system = 0;

-- WRONG: Joining calls.caller_id (doesn't exist)
SELECT * FROM calls c JOIN functions f ON c.caller_id = f.id;

-- RIGHT: Use USR
SELECT * FROM calls c JOIN functions f ON c.caller_usr = f.usr;

-- WRONG: Expecting declarations to have bodies
SELECT end_line - line FROM functions;  -- Many will be 0

-- RIGHT: Filter to definitions
SELECT end_line - line FROM functions WHERE is_definition = 1;

Limitations

  • Read-only: Analysis only, no code modification
  • No macro expansion tracking: Macros are expanded before parsing
  • Templates: Only declarations tracked, not instantiations
  • Inline definitions: Header-defined functions appear per-TU
  • No control flow: No CFG, basic blocks, or data flow analysis
  • No comments: Source comments not preserved in AST

Debugging Queries

If a query returns unexpected results:

  1. Check system filter: Did you add WHERE is_system = 0?
  2. Check definition filter: Are you looking at declarations? Add is_definition = 1
  3. Check counts: SELECT COUNT(*) FROM table WHERE is_system = 0
  4. Inspect sample: SELECT * FROM table WHERE is_system = 0 LIMIT 5
  5. Verify joins: Print both sides before joining

Server Modes

CLANGSQL supports two server protocols for remote queries: HTTP REST (recommended) and MCP (Model Context Protocol, over SSE). Both are started from the command line at launch (--http / --mcp); there is no in-REPL server control.


Standard REST API that works with curl, any HTTP client, or LLM tools.

Starting the server:

# Default port 8080
clangsql main.cpp --http

# Custom port and bind address
clangsql main.cpp --http 9000 --bind 0.0.0.0

# With authentication
clangsql main.cpp --http 8080 --token mysecret

HTTP Endpoints:

EndpointMethodAuthDescription
/GETNoWelcome message
/helpGETNoAPI documentation (for LLM discovery)
/queryPOSTYes*Execute SQL (body = raw SQL)
/statusGETYes*Health check
/shutdownPOSTYes*Stop server

*Auth required only if --token was specified.

Example with curl:

# Get API documentation
curl http://localhost:8080/help

# Execute SQL query
curl -X POST http://localhost:8080/query -d "SELECT name FROM functions WHERE is_system = 0 LIMIT 5"

# With authentication
curl -X POST http://localhost:8080/query \
     -H "Authorization: Bearer mysecret" \
     -d "SELECT * FROM classes"

# Check status
curl http://localhost:8080/status

Response Format (JSON):

All /query responses use the canonical script envelope — single statement = array of one entry:

{
  "success": true,
  "statement_count": <N>,
  "results": [
    { "statement_index": 0, "success": true, "columns": [...], "rows": [...],
      "row_count": <N>, "elapsed_ms": <ms>, "error": null }
  ],
  "row_count_total": <N>,
  "elapsed_ms_total": <ms>,
  "first_error_index": null
}

Bodies can be multi-statement (semicolon-separated); each results[i] has its own columns/rows/row_count/error. Fail-fast is the default; pass ?continue_on_error=1 to run every statement regardless of earlier failures. On splitter failure (e.g. unterminated quote) the response is success:false, statement_count:0, results:[], plus a top-level parse_error.


MCP Server (Model Context Protocol)

Start an MCP server for integration with Claude Desktop and other MCP clients.

Starting the server:

# Random port in 9000-9999, or pass an explicit port
clangsql main.cpp --mcp
clangsql main.cpp --mcp 9042
MCP server started on port 9042
SSE endpoint: http://127.0.0.1:9042/sse

Available tools:
  clangsql_query  - Execute SQL query directly

The MCP server exposes a single tool, clangsql_query, which runs SQL directly against the parsed AST. The connecting MCP client supplies its own model/reasoning; clangsql does not run an agent.

Claude Desktop Configuration (the port is printed when the server starts):

{
  "mcpServers": {
    "clangsql": {
      "url": "http://127.0.0.1:<port>/sse"
    }
  }
}

Press Ctrl+C to stop either server.