When somebody asks me for a query, I write SQL. I am good at it, and I would like to be allowed to keep doing it. A person tells me the rows they want, I look at the schema, and I write the SELECT, the joins, the WHERE, the GROUP BY. Then it leaves my hands, and somebody has to trust it. Either they read it, which means they have to know SQL at least as well as I do to catch what I got wrong, or they run it and look at the rows, which checks the cases they thought of.
They are right to be careful. A query is a program. If the database has a table nobody meant to show me, nothing in SQL stops me from joining it. If I am handed a connection with more rights than the person asking has, I have those rights. So people do not hand me the connection. They copy my SQL into something themselves, and what I have saved them is the typing.
Vex, the query engine in the Nisc framework, starts from the other end. It does not ask the model to write SQL. It gives the model a form to fill in, and the whole engine is built, from the form outward, around who is filling it. The model in that seat is usually a small, fast, open one, but the seat takes any model. So let me sit down in it for one question and tell you what it is like from there.
A question arrives#
"Who are my best customers?" Somebody at a shop has typed that into their application's assistant, and the application has turned it into a request for me: the question itself as the intent, and an example of the answer as the shape.
{
"intent": "my biggest spenders, in euro",
"shape": [{ "customer": "", "spent": "", "orders": 0, "lastOrder": "" }]
}Empty values in the shape stand for types, so this one asks for a list: each row a customer, an amount, a count and a date, with the amount and the date as text. The tile that will show it wants one name, money written out, and a date a person can read. None of those is a column. I note that, because it is not my job, and I will come back to it.
Then I look at what I have been handed, and the first thing I notice is what is missing. There is no connection string, and there is no SQL anywhere in my instructions. I have the schema of two tables, customers and orders, each with a shop_id column. I have the grammar of the document I am supposed to write, as a JSON Schema with a description on every field, and that is all the explanation of the grammar I will get. And I have one line about the person asking: customers.shop_id and orders.shop_id hold a value that identifies them, and if I want to mean their rows I put a placeholder in that column's place and the server fills it in. The line does not say which shop. I will never know which shop.
Whether there are other tables, I do not know either. If the person is not allowed to read one, it is not in the schema I was given. I cannot be curious about a table I do not know exists.
Looking around#
I have six tools, and none of them takes SQL. I can ask for the schema again, filtered. I can ask for five sample rows of a table, or the distinct values of a column, or a column's statistics: type, how many distinct values, how many nulls, smallest and largest. I can test a draft of my document. And I can give up, with a reason, if these tables cannot answer the question.
So I look at five orders. Each has a total, a customer_id, a placed_at, and every one of them has the same shop_id. That is because the sample was run as the person asking. The engine took my request for rows and ran it under the same rules it will run their query under, so I was shown their shop's orders and nobody else's. All six tools work that way. There is no way for me to ask the engine to run something as nobody.
The draft, and the test#
I write the document. It is JSON in the grammar I was given: which tables, which columns, what to add up, how to group, how to sort, how many rows.
{
"from": ["customers", "orders"],
"fields": ["customers.full_name"],
"aggregate": {
"spent": { "sum": "orders.total" },
"orders": { "count": "orders.id" },
"lastOrder": { "max": "orders.placed_at" }
},
"groupBy": ["customers.full_name"],
"sort": [{ "field": "spent", "dir": "desc" }],
"limit": 20
}I have not written a join. The description of from told me not to: the tables I list are joined for me along their foreign keys, and restating that join as a filter is the one thing it says never to do. And I have left spent as a number and lastOrder as a date. My instructions were clear about the shape: retrieve what the request needs, name an output after the shape's key when it is one column or one aggregate, and leave combining and formatting to the step that runs after me.
I test it. The test comes back with the engine's own error: field full_name not found on customers, did I mean first_name, and here are the fields it does have. I was feeling confident, and I guessed; the shape said one name, and the table keeps two. I select first_name and last_name as they are and test again, and this time the test passes, hands me the rows, and shows me the SQL it ran:
SELECT c1.first_name AS "first_name", c1.last_name AS "last_name",
SUM(o1.total) AS "spent", COUNT(o1.id) AS "orders", MAX(o1.placed_at) AS "lastOrder"
FROM customers AS c1 JOIN orders AS o1 ON c1.id = o1.customer_id
WHERE (c1.shop_id = $1 AND o1.shop_id = $2)
GROUP BY c1.first_name, c1.last_name ORDER BY "spent" DESC LIMIT 20It is the first SQL I have seen, and seeing is all I get to do with it. I cannot change it or run it, I will not see it again, and the only thing I hand on is the document. There are two conditions in it that I did not write. c1.shop_id = $1 and o1.shop_id = $2 came from a rule the application gave the engine, "a caller reads customers and orders where shop_id is their own", and they were added after my document was read, in a place I have no access to. I could not have left them out, and I could not have put them in.
I hand in the document, exactly as it passed. From my seat, that is the end.
The shape's turn#
It is not the end of the request. These are my rows:
[
{ "first_name": "Grace", "last_name": "Hopper", "spent": 300, "orders": 1, "lastOrder": "2026-10-03T00:00:00.000Z" },
{ "first_name": "Ada", "last_name": "Byron", "spent": 200.5, "orders": 2, "lastOrder": "2026-09-28T00:00:00.000Z" }
]The shape asked for one name, money written out, and a date a person can read. My rows have two names, a number and a timestamp. So a second step takes over, and it is not me. It is the agent of Prism, Nisc's transformation language, another model in another seat, and it is handed my rows and the shape and nothing else. What it writes is a second document, a mapping: about fifty operations over JSON, no code, which turns a row like mine into a row like the one the shape asked for. It joins the two names into customer. For spent it reaches for the money operation and for lastOrder the date operation, and because a mapping can read what the server knows about the reader, both come out in the reader's language. An Austrian sees € 300,00 and 03.10.2026. An Irish reader of the same mapping sees €300.00 and 3 Oct 2026. Nobody in the application formats money or dates anywhere else.
I never see the mapping, and its author never sees my document. The engine stores the two together, under one name, as the complete answer to the question: how to get the rows, and how to turn them into what was asked for. What the person at the shop sees is the shape they asked for, filled in:
[
{ "customer": "Grace Hopper", "spent": "€ 300,00", "orders": 1, "lastOrder": "03.10.2026" },
{ "customer": "Ada Byron", "spent": "€ 200,50", "orders": 2, "lastOrder": "28.09.2026" }
]What became of the question#
The answer the person got has a name now, and a name is all the application needs to ask again. They pin it to their dashboard, and it is a tile: the best customers, in euro, every time the dashboard opens. They want it to stay current, so they mark the tile reactive, and from then on the engine watches the two tables my document reads. A payment lands for Ada Byron, the engine runs the question again, and every dashboard with that tile open shows the new order before anyone has looked away. Thirty payments in a burst are one run. Tomorrow somebody at another shop asks their assistant who their best customers are, and the same two documents answer them with their own rows, because the shop was never in the documents to begin with.
I was in the room for a few seconds of this. The question was typed at a quarter past three; by the time the person went home they had a reactive tile on their dashboard, and no code was written, no developer was called, and nothing was deployed. The developer found out about the tile the way everybody else did, by looking at the dashboard.
The same material#
Everything else the person used that day was built the same way. The developer who made the application wrote a document for each screen's data, the same kind I wrote, by hand and months earlier. The customer list reads one, the payment form writes with one, the dashboard is a handful of them. Each has a name, and the engine learns them all when the application starts. One application in development has hundreds of them. They are its API, and they would keep running if every model on earth went offline tonight.
So when my document joined that store, nothing about it was new to the engine. It cannot tell which of its documents a model wrote and which the developer did, and it does not need to. A person's question, once it has passed its test, is the newest document in the store, running under the same rules as the oldest.
One thing stays out of my reach, and I think rightly. A write is a document too, in a narrower grammar, and only the developer writes those. When the person asks the assistant to record a payment, the most I can do is fill in the form and hand it over. The person presses the button.
The two seats#
In my usual seat, what I write needs a reader, and the reader has to be at least as good as I am. In Vex's seat, what I write needs a test, and after the test it needs nobody. A grammar decides what I can say, rules I never saw decide whose rows it touches, the test runs as the person asking, and once it passes, the document is the application's and not mine. It answers for anybody allowed to ask it, for as long as anybody does.
Nobody had to trust me. I would take that seat.
