---
title: "Code-Graph Tutorial"
---

# Code-Graph Tutorial

**Build:** `cargo build --release --features code-graph`
**APIs:** `EmbeddedDatabase::code_index`, `lsp_definition`, `lsp_references`, `lsp_call_hierarchy`, `lsp_hover` (plus the matching `FROM lsp_*(...)` table-function shapes in SQL)
**Tables created on first index:** `_hdb_code_files`, `_hdb_code_symbols`, `_hdb_code_symbol_refs`

---

## UVP

Most "AI code search" stacks bolt a vector DB onto a separate AST service and a third LSP daemon, then keep three consistency stories in their head. HeliosDB Nano collapses the three into one embedded process. Tree-sitter parses your corpus into the same engine that already serves PostgreSQL queries; LSP-shaped lookups (`definition`, `references`, `call hierarchy`, `hover`) push their `WHERE`s through the bloom-filter / zone-map / SIMD storage path you already paid for. **One binary, plain SQL tables, branch-aware, time-travel-aware, no microservices.**

---

## Prerequisites

- HeliosDB Nano source tree
- Rust 1.85+ (`rustc --version`)
- About 15 minutes
- (Optional) Node, Python, or Go to compare extracted symbols against your own reading

The tutorial works against an in-memory database; swap to `EmbeddedDatabase::open(path)` if you want persistence.

---

## 1. Build with `--features code-graph`

```bash
git clone https://github.com/HeliosDatabase/HeliosDB-Nano
cd HDB-HeliosDB-Nano
cargo build --release --features code-graph
```

Default builds skip every tree-sitter dependency. When the flag is on, `src/code_graph/` compiles, the seven static grammars (Rust, Python, TypeScript, TSX, JavaScript, Go, Markdown, SQL) ship inside the binary, and the methods below appear on `EmbeddedDatabase`. Without the flag, `code_index` does not exist and the SQL `lsp_*` table-functions fall through to the base parser as plain references (i.e. they error).

---

## 2. The Source Table Contract

The indexer reads from a user table you control. The contract is exactly three columns:

```sql
CREATE TABLE src (
  path    TEXT PRIMARY KEY,
  lang    TEXT,
  content TEXT
);
```

Other columns (e.g. `commit_sha`, `last_modified`) are fine — they're ignored by the indexer. The PK is the file path; re-inserting the same path replaces the previous row.

Accepted `lang` values (case-insensitive, alias-aware): `rust`/`rs`, `python`/`py`, `typescript`/`ts`, `tsx`, `javascript`/`js`/`mjs`/`cjs`, `go`, `markdown`/`md`, `sql`. Anything else routes through the runtime grammar registry (see §10) or, if no match, gets counted in `files_skipped`.

---

## 3. First Index — Rust

```rust
use heliosdb_nano::{
    code_graph::{CodeIndexOptions, DefinitionHint},
    EmbeddedDatabase, Result,
};

fn main() -> Result<()> {
    let db = EmbeddedDatabase::new_in_memory()?;

    db.execute(
        "CREATE TABLE src (path TEXT PRIMARY KEY, lang TEXT, content TEXT)",
    )?;
    db.execute(
        "INSERT INTO src VALUES \
           ('lib.rs', 'rust', 'pub fn answer() -> i32 { 42 } \
                               pub fn caller() -> i32 { answer() }')",
    )?;

    // First call bootstraps _hdb_code_files / _hdb_code_symbols /
    // _hdb_code_symbol_refs and parses every row of `src`.
    let stats = db.code_index(CodeIndexOptions::for_table("src"))?;
    println!("{:?}", stats);
    // → CodeIndexStats { files_seen: 1, files_parsed: 1,
    //   files_skipped: 0, files_unchanged: 0,
    //   symbols_written: 2, refs_written: 1, embed_calls: 0,
    //   languages_seen: ["rust"] }

    let defs = db.lsp_definition("answer", &DefinitionHint::default())?;
    for d in &defs {
        println!("{} :: {} @ {}:{}", d.qualified, d.signature, d.path, d.line);
    }
    Ok(())
}
```

`code_index` is **idempotent**. Running it twice without changes gives `files_unchanged: 1, symbols_written: 0` — the indexer SHA-256s each row's `content` against the stored hash.

---

## 4. The Four LSP-Shaped APIs

| Method | Question it answers |
|---|---|
| `lsp_definition(name, hint)` | Where is `name` defined? |
| `lsp_references(symbol_id)` | Who calls / references this symbol? |
| `lsp_call_hierarchy(symbol_id, dir, depth)` | What does the call tree rooted at this symbol look like? |
| `lsp_hover(symbol_id)` | Show me the signature. |

Every method goes through the regular SQL planner: each `WHERE` is an Eq-pushdown the existing `FilteredScan` path serves cheaply.

```rust
use heliosdb_nano::code_graph::lsp::CallDirection;

let defs = db.lsp_definition("answer", &DefinitionHint::default())?;
let target = defs.first().unwrap();
let refs   = db.lsp_references(target.symbol_id)?;
let tree   = db.lsp_call_hierarchy(target.symbol_id, CallDirection::Incoming, 3)?;
let hover  = db.lsp_hover(target.symbol_id)?;
```

`DefinitionHint` carries optional disambiguators:

```rust
let hint = DefinitionHint {
    hint_file: Some("src/lib.rs".into()),   // restrict to a specific file
    hint_kind: Some("function".into()),     // function / struct / class / method / trait …
};
```

---

## 5. Same Queries from SQL

If you'd rather drive everything from psql, an MCP transport, or a Drizzle client, the four functions also expose as table functions:

```sql
SELECT * FROM lsp_definition('answer');
SELECT * FROM lsp_definition('answer', 'src/lib.rs');
SELECT * FROM lsp_references(42);
SELECT * FROM lsp_call_hierarchy(42, 'incoming', 3);
SELECT * FROM lsp_hover(42);
```

The pre-parser rewrites these to `SELECT … FROM _hdb_code_*` subqueries before the planner sees the SQL, so the result columns and filtering behaviour are identical to the Rust API.

You can also bypass the table functions and write the joins by hand — the `_hdb_code_*` tables are plain user tables:

```sql
SELECT s.qualified, f.path, s.line_start, s.signature
FROM _hdb_code_symbols s
JOIN _hdb_code_files   f ON f.node_id = s.file_id
WHERE s.kind = 'function'
  AND s.name LIKE 'parse_%'
ORDER BY f.path, s.line_start;
```

---

## 6. `CREATE EXTENSION hdb_code`

For users who prefer DDL ergonomics, Nano accepts a no-op extension marker:

```sql
CREATE EXTENSION IF NOT EXISTS hdb_code;
```

This runs the same bootstrap that `code_index` runs, marks the extension installed in process-local state, and is permissive about `IF NOT EXISTS` for unknown extensions (silent no-op, matching PostgreSQL's migration behaviour). It does **not** turn the feature flag on at runtime — the flag is build-time. The DDL only affects already-`code-graph`-enabled binaries.

---

## 7. Indexing a Multi-Language Corpus

```rust
db.execute("INSERT INTO src VALUES \
  ('app.ts',  'typescript', 'export function hello() { return 1; }'), \
  ('main.go', 'go',         'package main\nfunc main() { println(hello()) }'), \
  ('mod.py',  'python',     'def hello():\n    return 1')")?;

let stats = db.code_index(CodeIndexOptions::for_table("src"))?;
// languages_seen contains [\"go\", \"python\", \"rust\", \"typescript\"]
```

The TypeScript extractor handles `function_declaration`, `method_definition`, `class_declaration`, `abstract_class_declaration`, `interface_declaration`, `type_alias_declaration`, `enum_declaration`. The Go and Python extractors handle their respective canonical declaration shapes. Cross-file symbol resolution runs at the end of every `code_index` call and rebinds each `resolution = 'unresolved'` edge against a corpus-wide name index — single match → `exact`, multiple → first-seen with `heuristic` resolution.

### Scope-chain resolver via IMPORTS

When a CALLS / REFERENCES edge stays unresolved after the cross-file pass, the resolver looks for an unambiguous IMPORTS edge in the same file ending in the bare name and upgrades to `Exact`. Handles Rust `use foo::bar`, Python `from foo import bar`, TypeScript `import { bar } from './foo'`, and Go imports.

---

## 8. TypeScript Client (over MCP)

:::caution[No engine subcommand]
HeliosDB Nano has no `mcp-server` subcommand. For stdio MCP clients, use the
separate HeliosDB CodeKB MCP server (`cargo install heliosdb-codekb-mcp`); for HTTP,
WebSocket or SSE, run `heliosdb-nano start` from a build with `mcp-endpoint` and use
`POST /mcp`, `/mcp/ws` or `/mcp/sse`.
:::

The same APIs surface as MCP tools — see [MCP_ENDPOINT](https://www.heliosdb.com/docs/nano/guides/tutorials/mcp_endpoint_tutorial/). A trimmed Node example:

```typescript
import { spawn } from "node:child_process";
import { createInterface } from "node:readline";

const child = spawn("heliosdb-nano", ["mcp-server", "--data-dir", "./data"]);
const rl = createInterface({ input: child.stdout });

function send(req: any) { child.stdin.write(JSON.stringify(req) + "\n"); }
send({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
send({
  jsonrpc: "2.0", id: 2,
  method: "tools/call",
  params: {
    name: "helios_lsp_definition",
    arguments: { name: "answer" },
  },
});

rl.on("line", line => console.log(line));
```

---

## 9. Python Client (direct over `psycopg`)

With the SQL surface, Python doesn't even need a code-graph SDK:

```python
import psycopg

with psycopg.connect("postgresql://postgres:s3cret@127.0.0.1:5432/postgres") as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT * FROM lsp_definition(%s)", ("answer",))
        for sym_id, path, line, signature, qualified, kind in cur:
            print(f"{qualified} @ {path}:{line} -- {signature}")

        # Walk callers two hops up.
        cur.execute("SELECT * FROM lsp_call_hierarchy(%s, 'incoming', 2)", (sym_id,))
        for depth, node_id, qualified, path, line in cur:
            print(f"{'  ' * depth}{qualified} @ {path}:{line}")
```

---

## 10. Pluggable Embedders — `body_vec`

The indexer adds a `body_vec VECTOR(n)` column to `_hdb_code_symbols`, materialised lazily on first non-null embedding. The indexer is agnostic about who computes the vector — Nano includes **no inference runtime**.

### External HTTP embedder (default)

Any service that accepts `POST { "input": "<text>" } → 200 { "embedding": [f32, …] }` works:

```rust
let opts = CodeIndexOptions {
    source_table: "src".into(),
    embed_bodies: true,
    embed_endpoint: Some("http://localhost:8080/embed".into()),
    embed_bearer:   Some(std::env::var("EMBED_TOKEN").unwrap()),
    force_reparse:  false,
};
db.code_index(opts)?;
```

### In-process via `code-embed` (fastembed-rs)

If you'd rather not run a sidecar:

```bash
cargo build --release --features code-graph,code-embed
```

```rust
use heliosdb_nano::code_graph::{
    code_index_with_embedder, CodeIndexOptions, FastEmbedder,
};

let embedder = Box::new(FastEmbedder::try_default()?);   // BGESmallENV15, 384-dim
code_index_with_embedder(&db, opts, embedder)?;
```

The model cache (~30 MB on first run) lives under `$XDG_CACHE_HOME/.fastembed_cache`. The binary itself isn't changed in size by the feature — fastembed pulls its ORT runtime as a dynamic dep at runtime.

### Runtime grammar + extractor registration

For languages outside the static set:

```rust
use heliosdb_nano::code_graph::{
    parse::register_grammar,
    register_extractor,
    SymbolExtractor,
};
use std::sync::Arc;

// Side-load a tree_sitter::Language. The consumer chooses the
// loader — dynamically-linked native grammar via libloading, a
// WASM grammar via tree_sitter::WasmStore, etc. See
// src/code_graph/parse.rs for the canonical loader patterns.
let cobol_lang: tree_sitter::Language = load_cobol_grammar()?;
register_grammar("cobol", cobol_lang);

// Pair it with your SymbolExtractor so the indexer emits real
// symbols instead of an empty parse tree.
struct CobolExtractor;
impl SymbolExtractor for CobolExtractor { /* ... */ }
register_extractor("cobol", Arc::new(CobolExtractor));
```

Without a paired extractor the indexer skips the file outright (an empty parse tree is worse than a clean skip). Inspect `code_graph::registered_grammars()` and `code_graph::registered_extractors()` to see what's live, or query the system view:

```sql
SELECT * FROM hdb_code_languages;
-- name        | source
-- rust        | static
-- python      | static
-- ...
-- cobol       | runtime
```

---

## 11. Branch-Aware Indexing

Every `_hdb_code_*` table is just a Nano table, so branches and time travel work for free:

```sql
CREATE BRANCH refactor FROM main;
\branch refactor

-- Mutate src on this branch
UPDATE src SET content = '<new lib.rs>' WHERE path = 'lib.rs';

-- Reindex on the branch only — main is untouched.
SELECT * FROM lsp_definition('answer');
```

You can also pin a single `lsp_*` call to a branch without switching session-wide:

```sql
SELECT * FROM lsp_definition('answer') ON BRANCH 'refactor';
SELECT * FROM lsp_references(42) AS OF COMMIT 'abc123' ON BRANCH 'main';
```

The pre-parser strips `ON BRANCH '<name>'`, scopes a RAII guard around execution, and restores the prior branch on every early-return path.

---

## 12. Incremental Re-Index — `merkle_refresh`

Big corpora amortise re-indexing through the semantic-Merkle roll-up:

```sql
CREATE SEMANTIC HASH INDEX IF NOT EXISTS code_merkle;
```

After this DDL fires (or after the first call to `db.code_graph_merkle_refresh()`), every file gets a `_hdb_code_merkle.rollup_hash` covering its symbols. Files whose member symbols haven't changed get `files_unchanged += 1` instead of being re-embedded. See [SEMANTIC_HASH_INDEX_QUICKREF](https://www.heliosdb.com/docs/nano/guides/quickstart/semantic_hash_index_quickref/).

---

## 13. Where Next

- [SEMANTIC_HASH_INDEX_QUICKREF](https://www.heliosdb.com/docs/nano/guides/quickstart/semantic_hash_index_quickref/) — incremental reparse via the Merkle subtree-hash DDL.
- [GRAPH_RAG_TUTORIAL](https://www.heliosdb.com/docs/nano/guides/tutorials/graph_rag_tutorial/) — project the code symbols into the universal graph and run cross-modal seed → expand → rerank queries.
- [MCP_ENDPOINT_TUTORIAL](https://www.heliosdb.com/docs/nano/guides/tutorials/mcp_endpoint_tutorial/) — drive every LSP query from Claude Code, Cursor, Continue, or Aider over JSON-RPC.
- [VECTOR_SEARCH_TUTORIAL](https://www.heliosdb.com/docs/nano/guides/tutorials/vector_search_tutorial/) — the HNSW index that backs `body_vec` retrieval once an embedder is wired.
