Koragraph

SQLite as the store

SQLite is a full relational database that lives in a single file with no server, which makes it the natural home for a code graph that has to persist on your machine across sessions and follow renames.

SQLite is a complete relational database that lives inside a single ordinary file, with no server to run and nothing to connect to. That combination is exactly what a code graph needs, because the graph has to sit on your own machine, survive being closed and reopened, and keep track of each piece of code even after the file it lived in has been moved or renamed.

A file and a database are not the same thing

Start with the most familiar way to keep information: a plain file. A file is a run of bytes with a name. When a program wants to save something, it can write those bytes to a file, and later read them back. A text document, a photo, a spreadsheet saved to disk, all of these are files. Files are simple and they are everywhere, and for a great deal of work they are all you need.

The trouble starts when the information inside the file has structure and you want to ask questions of it. Imagine you kept a list of every person in a company in one long text file, one person per line. Finding one person by name means reading the whole file from the top until you hit the right line. Finding everyone in a particular department means reading the whole file and checking each line. Changing one person’s job title means rewriting the file. Two programs trying to change it at the same time can corrupt each other’s writes. None of this is impossible, but you are doing by hand the work that a database exists to do for you.

A database is a program, or a piece of a program, whose entire job is to store structured information and answer questions about it quickly and safely. It keeps the data in an organized form on disk, builds indexes so it can find a record without scanning everything, and it guarantees that a change either happens completely or not at all, even if the power fails halfway through. The word people use for that last promise is a transaction: a set of changes that the database treats as a single all-or-nothing unit. A database is still, underneath, writing bytes to files. The difference is everything it does on top of that so you do not have to.

Relational means tables, rows, and columns

The most common kind of database is the relational database, and the idea behind it is one you already know from spreadsheets. Information is kept in tables. A table has columns, which name the fields, and rows, which are the individual records. A table of people might have columns for an identifier, a name, and a department, and one row for each person. Every row in a table has the same columns, so the shape is regular and predictable, and that regularity is what lets the database index and search it fast.

The important habit in a relational database is giving each row a stable identifier of its own, a small unique value that names that row and nothing else. This is usually called a primary key. It matters because rows in one table can then refer to rows in another by that key. If a table of orders needs to say which customer placed each order, it does not copy the whole customer record into the order. It stores the customer’s key. That reference is what the word relational is pointing at: tables relate to each other by keys, and the database can follow those references to answer a question that spans several tables at once. You ask, in a query language called SQL, for the orders whose customer key matches a given customer, and the database does the matching.

A graph fits into two tables

Here is where the pieces meet. A graph is a set of nodes and the edges between them, and both of those map onto relational tables so cleanly that it barely feels like a translation.

You keep one table for the nodes. Each row is one node, with a primary key that names it and columns for whatever the node is: in a code graph a node might be a function or a class or a file, so the row records what kind of thing it is, what it is called, and where it lives. You keep a second table for the edges. Each row is one edge, and the two most important columns on it are the key of the node the edge starts at and the key of the node the edge ends at. A third column records what kind of edge it is, a call or an import or an inheritance link. That is the whole design. Nodes in one table, edges in another, and the edges point into the nodes table by key.

nodes:  id | kind     | name          | file
        7  | function | load_config   | config.py
        8  | function | read_file     | io.py

edges:  src | dst | kind
        7   | 8   | calls

Read that small example back as a sentence and it says: the function load_config calls the function read_file. The edge row does not repeat the names. It holds the two keys, 7 and 8, and the kind of link. To find everything load_config calls, the database looks in the edges table for rows whose source key is 7. To find everything that calls read_file, it looks for rows whose destination key is 8. Both of those are the exact operation a relational database is built to do quickly, especially once an index is placed on those key columns. Walking the graph, in this form, is a sequence of ordinary lookups.

A graph is a nodes table and an edges table. Every question you ask by walking the graph becomes a lookup by key, which is the one thing a relational database does best.

What makes SQLite different from other databases

Most relational databases you may have heard of run as a separate program that sits and waits. You start a database server, it listens on a network port, and your application opens a connection to it and sends queries across that connection. This is a sound design for a system with many users and many programs sharing one central store, but it carries a cost: something has to install, configure, run, and keep that server alive, and your program cannot do anything until it can reach it.

SQLite takes the opposite path, and its defining traits all follow from one decision: there is no server. It is a library that your program includes directly, and the entire database is a single file on disk. When your program wants to read or write, it calls straight into that library, which reads and writes the file. Nothing listens on a port. Nothing has to be started. From this one choice come the four things worth remembering about it.

  • It is serverless. There is no separate process to run, so there is nothing to launch before your program works and nothing that can be down.
  • It is a single file. The whole database, every table and index and row, is one file you can copy, move, back up, or delete like any other file.
  • It is embedded. The database engine runs inside your program rather than beside it, so a query is a plain function call with no network in the middle.
  • It is ubiquitous. The same SQLite engine sits inside phones, browsers, and countless applications, which has made it one of the most widely deployed pieces of software in the world and, in turn, extremely well tested.

None of this makes SQLite better than a server database in general. A server database is the right answer when many machines must share one live store and write to it at the same time. The point is narrower and more useful: for a store that belongs to a single machine and a single user, SQLite removes every moving part that a server would have added.

Why this is the natural home for a local-first tool

A tool is local-first when it runs on your own machine and keeps your data there, rather than sending it away to a service and asking for it back. For a tool that reads your source code, that property is not a preference, it is often a requirement, because source code is frequently private and cannot be allowed to leave the building.

Koragraph is built this way. It runs entirely on the developer’s machine, with no cloud dependency and no large language model in the loop, and no source code leaves the machine. A store that fit that shape had to be one that also asks nothing of the network and nothing of a server, and SQLite is exactly that. Koragraph keeps its whole code graph in a local SQLite database, on disk, at ~/.koragraph/practice.db. That single file is the graph. Because it is just a file in a known place, it is yours in the plainest sense: you can see it, copy it, and delete it, and nothing about it reaches outward.

Persistence, or why the graph is still there tomorrow

The word persistence means simply that the data outlives the program that made it. When you close a program, everything it was holding in memory is gone, because memory is temporary working space that the machine reclaims. Anything that must survive has to be written to disk before the program ends. Persistence is that survival across the boundary of a program starting and stopping.

This matters enormously for a code graph, because building the graph is not free. It takes real work to read every file, understand its structure, and resolve the connections between pieces that may live in different files or even different repositories. If that work vanished every time the tool closed, you would pay the full cost again at the start of every session, and the tool would be answering questions about the code from a standing start each morning. Because the graph lives in a file on disk, it is simply still there. You close your editor, you shut down for the day, you come back, and the accumulated understanding of your code is waiting, exactly as it was. Koragraph’s SQLite store persists across sessions for precisely this reason: the facts it has learned about your code stay learned.

There is a second kind of memory built on the same foundation. Beyond the structure of the code, an agent can be told things worth keeping, notes and decisions that should carry from one session to the next. Koragraph exposes tools named remember and recall for this, where remember is the one that writes a fact down and recall reads it back later. Those facts, like the graph itself, rest in the same durable local file, which is what lets them survive the gap between one working session and the next.

Identity that survives a rename

The subtlest reason a real database matters here is about identity. Recall that every node has a primary key, a stable identifier that is its own and belongs to it regardless of anything else recorded about it. A function’s key is not its file path and not its position in the file. It is a separate, durable handle for that function as a thing.

This is what makes it possible to follow a piece of code through a rename. Files get moved and renamed constantly as a project grows: a folder is reorganized, a module is split, a badly named file finally gets a good name. If the graph identified each function only by its file path, then moving the file would look, to the graph, like the old function vanishing and a brand new one appearing. Every edge that pointed at it would break, and all the history attached to it would be orphaned. That would be wrong, because the code did not disappear. It just changed address.

Because the node has an identity separate from its location, the tool can recognize that the function in the new file is the same function that used to be in the old one, update the row’s recorded location, and leave every edge and every fact still attached to it. Koragraph’s store follows file renames for exactly this reason. The graph tracks the thing, not the path, so the web of relationships you have built up does not shatter every time someone tidies the folder structure.

A node’s identity is not its address. Because the store keeps a stable handle for each piece of code, moving or renaming a file changes where the code lives without erasing what the graph knows about it.

Where this leads

A store is only ever half of the story, because a store has to be filled. All of the nodes and edges in that SQLite file arrive there through a single pass over your repositories that reads the code, works out its structure, resolves the connections, and writes the results down. That pass is the ingest pipeline, and it is the natural next idea to walk to from here: the store is where the graph rests, and the pipeline is how it gets built and kept current.

Connected concepts

Local-first, offline by designA local-first tool does its work on your own machine with no cloud round trip, which for source code is not a feature but a requirement, and it also happens to be faster and to work on a plane.Memory for agentsMemory is what lets an agent keep a fact past the end of a conversation, so a lesson learned once, a fix that worked, a hazard, does not have to be rediscovered on every session.The ingest pipelineIngest is the pass that turns a pile of repositories into one graph: parse with tree-sitter, extract declarations, resolve every edge, mine the git history, and write it all to the local store.The code knowledge graphA code knowledge graph is a structured map of a codebase where every declaration is a node and every real relationship, calls, imports, inheritance, cross-service links, is an edge.

Where this sits

Back to the full graphThe short glossary