Borrow the Language, Not the Database

I wanted to ask my models a question.

Not a hard question. Something like: which teams depend on this system of record, and what would break if this library turned out to have a vulnerability. The information was all there. I have the architecture written down, the org structure written down, the dependencies written down, and they are all typed models in one repository, which is the entire point of having spent years writing them down.

And to ask it, I was going to have to write a Java method with a nested loop in it - a JUnit test.

That method would work. It would live in one class, answer one question, and be invisible to anyone who later wanted to ask something adjacent. I could also wrap it into a CLI command. The next question would get its own method or command. This is how most model-driven projects quietly become a pile of bespoke traversal code, and it is worth noticing that the pile is a symptom of a missing interface rather than of insufficient effort.

Two reflexes, both wrong

When an engineer notices a missing query interface, two reflexes fire.

The first is to invent a small language. I am unusually prone to this one. I have written about why DSL proliferation is good, I believe it, and I have the design notes to prove I meant it. So the temptation was to extend the path language I already have until it could express joins and aggregation, and call the result a query language.

The second reflex is to install the database that already has one. Put the models into a graph database, get Cypher for free, and query away. This is the reflex the industry rewards, and it is also how a repository stops being the source of truth: once the interesting queries only run against the database, the database is the system and the files are an export.

The thing both reflexes miss

A query language is not a feature of a database. It is an interface, and interfaces are separable from their first implementation.

Cypher was born inside Neo4j, but Cypher is not Neo4j. There is a specification, openCypher, written down and freely licensed. There are other implementations: Memgraph, Apache AGE, Amazon Neptune. The international standard that grew out of it, GQL, was published in 2024 and is now the direction of travel. None of that requires a server, a bolt connection, or a copy of anything.

And the thing you actually need in order to run a query language is a graph. I have one. Every typed model is a graph: objects are nodes, references are edges, and I already had a layer that says so, because I needed it for other reasons. MATCH (t:Team)-[:dependsOn*]->(s:SystemOfRecord) is a well-formed question to ask of an object tree in memory. Nothing about it requires the object tree to be in a database first.

So the move is: take the language, leave the store. Parse Cypher into a model, evaluate it against the graph I already have, and let the database remain what it should have been all along, which is one possible backend among several for the same query text.

Why not my own language, given that I like writing languages

This is the part I had to argue myself out of, so it is the part worth writing down.

The specification came with its tests. openCypher publishes a conformance kit: executable scenarios that decide whether an implementation is correct, which other implementers use to make claims about themselves. If I adopt the language, I adopt the test suite, and I did not write either one. That matters more than it sounds. A language I invent cannot surprise me, because I also write its tests, and my tests encode the same misunderstandings as my implementation. A conformance suite written by people who argued about null semantics for years is a machine for finding out that I am wrong. You cannot buy that, and you certainly cannot write it for yourself.

It also converts a vague roadmap into a number. “The engine passes this fraction of the suite, and here is the list of what it does not do yet” is a sentence a stranger can evaluate. It is the most credible thing a small project can say about an engine, and an invented language has no way to say it at all.

Every assistant already writes it. This is the argument that did not exist three years ago and now decides the question. If I invent a query language, then every time I want an agent to use it, I have to teach it: paste in a grammar, get plausible nonsense back, and have no way to tell good from bad except by running it. Cypher is in the training data at a scale I will never approach. An assistant writes it correctly most of the time, and the failures are the ordinary kind that a type check catches.

A widely known language is not a convenience any more. It is a distribution channel, and it is one of the few kinds of leverage a one-person project can pick up off the floor.

It can be read before it is run. A Cypher statement is declarative, so I can tell from the syntax tree which types it touches, which properties it reads, which it writes, and whether it writes anything at all. Two statements can be compared for overlap without executing either. A read-only statement can be verified as read-only rather than promised as read-only.

That last property is the one I did not expect to care about and now care about most. I intend to ship catalogs of curated content, and other people’s data packs are exactly where a scripting language becomes a supply chain. A language with no I/O, no reflection and no host access is a fundamentally different security proposition from an embedded script engine, and I did not have to design that in. It came with the language, because the language was designed by people who had to run other people’s queries.

Thirty tools, or one

Here is where it stopped being about my nested loop.

The current way to let an assistant work with a system is to hand it tools. One to list children, one to find the parent, one to look up a type, one to render a summary, one per report, one per export format.

Thirty tools is worse than it sounds. Every one of them costs prompt tokens whether or not it gets used. Selection accuracy drops as the list grows, because picking the right tool from thirty is itself a task the model can fail. And every new capability means editing the prompt, which means the prompt is a second copy of the system’s abilities that drifts out of date the moment somebody ships a plugin.

Now consider what happens once the model, its types, its operations and its views are all one graph and there is one query language over all of it. The assistant gets a compact schema summary, a couple of bound variables, and a single tool that takes a query. Children are an edge. The type is an edge. A computed descendant traversal is an edge that takes a parameter. A rendered summary is a “view” node. Finding out what a thing can even do is a query, because the available views and operations are nodes in the same graph as everything else.

The tool count is the least of it. The thing that matters is that capabilities became data. When somebody adds a view, nobody edits a prompt. The assistant finds it by looking, the way it would find anything else. The system can grow new abilities without the description of the system needing to be updated, because there is no separate description anymore.

A query language cannot open a file, cannot call out to the network, and cannot loop forever, and I can tell by reading a statement whether it writes anything before I run it. For a human author that is a mild convenience. For an autonomous one it is the entire safety argument, and I got it for free by not inventing anything.

Coraline’s other world, and a flashlight

The objection I expected, and made to myself first, is that a general query language is too much freedom. Thirty narrow tools at least constrain what an assistant can attempt. One tool that takes any query is a blank page, and a blank page is where small models go wrong.

That objection turns out to be aimed at the wrong thing. How much freedom an assistant has is not decided by the language. It is decided by how much graph it can see.

So the query does not run against everything. It runs against a neighborhood: walk out from where the assistant is standing, stop after so many steps or so many elements, and include only the parts of the type system that are actually in use there. A schema with six hundred classes, of which twelve appear in this neighborhood, produces a summary with twelve classes in it. The assistant is not choosing among everything I have ever modeled. It is choosing among what is in front of it.

One projection, and three problems go quiet at once: the prompt gets small, the decision gets small, and the damage a bad query can do gets small.

The detail that took me longest to get right is what happens at the edge.

There is a sequence in Coraline, Henry Selick’s 2009 film of the Neil Gaiman novella, that is the best illustration of the wrong answer I know. Coraline has found a door into an other world that is a copy of her own, better in every way, made for her by something pretending to be her mother. At some point she walks away from the house to see what else is out there. The world gets vaguer as she goes, then blank, then white. And then she arrives back at the house from the opposite side.

The other mother had built exactly as much world as was needed and no more. The trick is not that it was small. The trick is that walking to the edge returns you to the middle, so from the inside it seems complete. Nothing announces the boundary. You are meant to conclude there is nothing beyond it, which is precisely the conclusion that keeps you there.

That is the failure mode, exactly, and it is easy to ship. Ask an assistant what depends on a component, with the truth four hops out and the horizon at three, and it says nothing depends on it. Not “I cannot see far enough.” Nothing. A view that has been cut down but presents itself as whole produces confident wrong answers, which is worse than producing none, and worse still because the confidence is what you will act on.

A flashlight has the same limits and none of the dishonesty. It shows you a circle, and it shows you the circle’s edge, and the dark past it is plainly still the world. Nobody has ever been fooled by a flashlight.

So: things just past the boundary appear as outlines. You can see that something is there and that you cannot see into it. Step toward one and the light moves with you and a bit more resolves. Both worlds are bounded. Only one of them tells you.

There is a second half to the warning, and it is the part that applies even when nobody is being deceived. The other mother’s world was built to fit what Coraline wanted. A view assembled to be exactly big enough for the question will always look like it answers the question. That is not malice, it is just what fitted things do, and it is why the edge has to be visible even when you were the one who drew it.

This is not only about machines. Nobody works with a large system by holding all of it in mind. You stand somewhere, you see what is around you, you walk, and what you see changes. The mistake we keep making is building views that end without saying so, and then being surprised when people, or models, take the edge of the map for the edge of the world.

Except once

The ancient Greeks had a word for the known world, the part that was inhabited and mapped: oikoumene, the ecumene. It is a better word than “scope” because it admits whose knowledge it is. A world is always somebody’s.

Having written down that a bounded view must always confess its boundary, I have to take one piece of it back, because there is a case where concealing the edge is the entire point.

In the Soviet Union, having relatives abroad was dangerous. So was descent from the wrong sort of ancestor: not proletarian roots. So families kept two versions of themselves. There was the official one, in which the ties simply did not exist, and there was the one you were told at home, quietly, when you were old enough.

The official family tree was not a summary. It was a cover story, and its usefulness depended entirely on looking complete. A footnote saying “fifteen relatives withheld” would have been worse than useless, because the count alone was the incriminating fact.

That is the same structure as the thing I called a bug, working correctly. A bounded world indistinguishable from an unbounded one is a catastrophe when the boundary is a budget, and it is the whole mechanism when the boundary is a secret. Same construction, opposite purposes, and no system can tell which one it is looking at, because the difference is not in the data. It is in why the line was drawn.

Which is a small, annoying, entirely practical conclusion. You cannot make this a global setting. Every boundary has to say what kind of boundary it is, and the ones that are permissions have to hide even the fact that they are there, while the ones that are budgets have to announce themselves loudly. Get those two backwards and you have either lied to someone who was counting on you, or handed someone the one number they needed.

When to invent, and when to borrow

I still believe DSL proliferation is good. These two positions are not in tension once the line is drawn in the right place, and drawing it is the point of this piece.

Invent when the design space is yours. Nobody has standardized a language for describing the concerns of a persona, or the occupancy of a region of a design space, or the mapping between two of your own models. Those languages are small, they are specific to a structure you defined, and building one is an afternoon that pays for itself immediately. Proliferate freely.

Borrow when someone has already standardized. Querying a graph is not your design space. It is a solved problem with a published specification, a conformance suite, four implementations and a million fluent speakers, and every hour you spend on your own version is an hour spent rebuilding infrastructure you could have had for the price of a dependency and some discipline.

The test I now use: would a competent stranger already know this language? If yes, my version of it is a tax I am charging myself and everyone who comes near my project. If no, and the shape is genuinely mine, then writing it down as a language is the cheapest thing I can do.

The cheapest DSL, it turns out, is usually someone else’s.