All notes

Why I Built SQLTerrain

Why I started building a browser-based SQL workbench, what it does today, and why the part I care about most is the uncertainty it reports rather than the diagram it draws.

Mario Caesar · Aug 1, 2026 · 24 min read

A query can run without errors and still be wrong.

That sentence has followed me through most of my working life in data. I've used SQL to build pipelines, dig through broken reports, prepare data marts, answer business questions, and chase metrics that moved when nobody expected them to. Sometimes the problem announces itself. A missing comma, a misspelled column, the kind of error that stops everything before it can reach anyone. Those are the good days. The uncomfortable cases are quiet ones: the query runs, the numbers look about right, a dashboard refreshes, and somebody puts the result on a slide.

Then a week later someone asks one small question about it, and your stomach drops before you've even opened the file, because you already know you can't answer them without reading all four hundred lines again.

SQL is generous like that. It lets you be wrong at scale.

I don't think that feeling ever fully left. Not the embarrassment, that fades. What stayed was narrower and more annoying. I could read every line of a query, follow every clause, and still not be able to prove that the number it produced meant what everyone in the room had assumed it meant. Reading was never the hard part. Proving was.

That's where SQLTerrain started.

I kept drawing the same query map

When I read a long query, I rarely understand it in one pass. I jump between CTEs, search for aliases, write down the grain of intermediate tables, check where filters happen, and run parts of the query separately so I can compare the counts. At some point I usually make a small map in my notes.

raw sources
    ↓
filtered records
    ↓
enriched records
    ↓
aggregation
    ↓
window calculation
    ↓
final output

The map is rarely pretty. It does the job. What bothered me was how often I repeated the same work, especially when reviewing a query written by someone else, or returning to my own SQL several months later. Apparently past me also believed that aliases such as a, b, and x2 were enough documentation.

So I started thinking about a tool that could expose part of this structure automatically. I did not want it to decide whether the business logic was correct, because syntax alone cannot tell us that. I wanted it to answer smaller, more defensible questions. Where did this column come from? Which scopes aggregate rows? What is the final grain? Which filters affect row retention? What changed after a query was translated into another dialect? And which facts come directly from the SQL, rather than needing schema or data evidence we do not have?

Every one of those questions has an answer the SQL itself can support, or an honest admission that it cannot. That felt like a reasonable place to draw the line. I wanted something I could use during an investigation rather than a report I read afterwards, and that became SQLTerrain.

Not only the people who are learning

When I started I assumed a tool like this would mostly serve people early in their SQL career. That assumption didn't last long. The people I watched reading long queries most carefully were usually the ones with the most experience, including engineers a good deal more senior than me. They weren't stuck on syntax. They were doing exactly what I was doing: scrolling back to check what a CTE actually returned, running a subquery on its own to see the row count, asking out loud whether a join could duplicate anything.

Honestly, the first time I really noticed that, it came as a relief. I had assumed the slowness was my own gap to close, and that enough years would eventually make long queries legible at a glance. Then I watched people I look up to being just as careful, and it told me something more useful than reassurance would have. The slowness belongs to the language, not to the reader.

What experience buys you is a better sense of where to look first. It doesn't buy the ability to skip looking. Someone with fifteen years behind them reads an unfamiliar three-hundred-line query slowly for the same reason someone with two years does, which is that the query doesn't explain itself and the cost of guessing lands on somebody else's report.

Experience changes how we investigate the problem. It does not make the problem disappear.

So those turned out to be the people I was building for, which is not where I expected to end up. SQLTerrain isn't shaped like a tutorial and it doesn't try to teach SQL. It assumes you already know what a window function is, and that you're currently trying to work out what this particular one does to this particular row set, in a query somebody else wrote and never documented. Learners are welcome, and a fair amount of the interface explains itself as you go. But the person I picture is someone already good at this, mid-investigation, with less time than they'd like. That's a different design target than a teaching tool, and it produces different decisions all the way down.

SQLTerrain homepage with Atlas, Passage, and Draft
SQLTerrain brings query analysis, dialect conversion, and visual SQL construction into one browser-based workspace.

Why this needs to exist at all

There are good SQL visualizers already, and some of them draw a nicer graph than mine. Formatters, linters, lineage platforms, catalog tools with column-level tracking: this is not an empty space, and I would rather not pretend otherwise. So the question I had to answer for myself was not whether I could produce another diagram. It was whether anything was missing that I actually needed.

The thing I kept wanting was a tool that reports what it cannot prove.

Most tools in this area tell you what your query says. They parse it, resolve what they can, and render the result. The difficulty is that a rendered edge looks identical whether it came from an explicit SELECT a.customer_id or was guessed at through a SELECT * against a table whose schema the tool never saw. Both arrive as a confident line on a diagram. That is not dishonesty, it is a missing vocabulary: with no way to express the difference, the reader inherits an inference and cannot tell it apart from a fact.

That distinction is the one I care about most, because it is where the expensive mistakes live. An unproven assumption presented as a certainty is worse than an admitted gap, since a gap sends you off to check.

So Atlas carries its uncertainty as an actual output rather than a caveat in a footer. The Review workspace has a mode called Verify whose whole purpose is listing what the query cannot establish about itself: whether a join key is unique, what the real cardinality is, whether a column can be null, whether the implementation matches the requirement that was actually asked for. Each entry names the assumption and why it matters. Lineage behaves the same way, so a source that cannot be resolved stays unresolved and individually identifiable rather than being folded into one anonymous bucket or quietly dropped to make the graph look complete. Row influence is also kept structurally separate from value lineage, for reasons the Lineage section below gets into properly.

None of that is technically hard. It is mostly a decision about what the tool is allowed to claim, made early enough that the data model can hold both kinds of answer. I think it was worth making, and it is the honest reason for this project to exist beside tools that already work well.

Does a deterministic SQL tool still make sense beside AI?

I use AI regularly, and SQLTerrain itself was built with help from coding agents. They helped me explore ideas, inspect code, generate test cases, write implementation plans, and move through work that would have taken much longer alone. So this project did not begin with the belief that AI is useless for SQL. That would be difficult to defend, and also slightly awkward considering how much Claude and Codex have seen of this repository.

AI can explain a query, generate one from a plain-language requirement, suggest a rewrite, or translate the whole thing between dialects, and all of that genuinely works. How well it works depends on the context you hand over. A 400-line query rarely counts as enough context by itself. The model may also need table schemas, uniqueness constraints, metric definitions, the expected grain, the database dialect, and an explanation of what the query is supposed to produce. Supplying all of that takes time and tokens, and missing details leave room for assumptions. The answer can still sound convincing.

That is the part I am careful about. Fluent output is useful, but fluency does not tell me whether a lineage path was inferred from the actual syntax or invented because it sounded likely. The same problem appears when a model describes join behavior without knowing uniqueness, cardinality, or the data distribution.

SQLTerrain handles a narrower job. It parses the query and builds a structural model from what is present in the SQL, and Atlas, Passage, and Draft all read that model for deterministic analysis, conversion, or synchronization. When evidence is missing, the result is allowed to stay ambiguous or unknown.

I think that makes SQLTerrain useful beside AI rather than instead of it. A deterministic analysis gives the conversation a firmer starting point. I would rather hand an AI assistant a query map, explicit lineage, known warnings, and a list of unresolved questions than ask it to infer everything from raw SQL and hope the explanation lands in the right postcode. AI can help interpret the findings. SQLTerrain tries to establish the findings first.

Why I wanted to give something back

Almost everything I know how to do, I learned from people who had no obligation to teach me. Some wrote documentation, or long technical posts that must have cost them a weekend they would rather have spent on something else. Others published libraries, repositories, conference talks, evaluation methods, unflinching postmortems of their own outages. More recently I've been learning from people sharing their AI models, agent systems, prompts, half-finished product experiments. A few were honest enough to write about where their business model got expensive, or unreliable, or just too tiring to keep alive, which is the part almost nobody publishes.

I never paid any of them back. You can't, really. That isn't how it works.

I've carried one principle for a long time because of that.

Bring something back to the community from what I have received.

It doesn't have to be a course or a paid product, and it certainly doesn't need a motivational launch post. Sometimes it's a small tool that solves one irritating problem properly. SQLTerrain is my attempt at that.

There's a second reason and it's more selfish. I wanted a project that made me responsible for more than the demo. Generating a good-looking interface is easy now. Building something that keeps your input when the engine dies, ties an export to the exact query revision it analysed, admits what it doesn't know, and is still maintainable eight months later is not easy at all. That's the work I actually wanted to practise.

The less photogenic work.

Why the name SQLTerrain?

Complex SQL has a shape. Tables provide the starting points, CTEs and subqueries create intermediate routes, and joins connect them. Filters decide which rows continue, aggregations change the level of detail, and window functions add calculations over whatever row set survives. The final SELECT is the destination everyone sees, and a lot happened before it arrived there.

The name came from that idea. A query has routes, branches, boundaries, and places where the meaning quietly changes, and terrain was the word that kept fitting. Not a picture of the query. The ground it covers.

The three tools are named on the same theme, and each name is doing a small amount of work explaining what its tool is for. The plain functional labels stay visible in the interface as well, because I like the theme but nobody should have to solve a riddle before they can find the converter.

Atlas

An atlas is a bound collection of maps you consult, not a single poster you hang on the wall. You open it at the page you need, and every page is drawn to the same survey. That is exactly the behaviour I wanted here, so the name came easily.

Atlas is the query visualizer and review workspace, and I didn't want it to produce a decorative node graph and stop there. A graph can look impressive while saying very little. Its Map, Lineage, Joins and Review panels, plus every export, all read the same accepted analysis result, so they're four pages of one book about one query revision rather than four views that can quietly disagree with each other.

Map

The Map view follows the query from source tables through CTEs, subqueries, and the final result. Users can pan, zoom, fit the graph, reset the layout, or open it fullscreen, and a trace picker can follow a source, an intermediate scope, or the final output either upstream, downstream, or in both directions.

Selecting a node also opens its related details and underlines the exact clause in the editor. That connection matters more than it sounds: a visual diagram becomes much less useful when the user still has to hunt through the editor to find the clause it represents. Semantic zoom reduces secondary text at lower zoom levels while keeping the nodes in place, so nothing quietly disappears from the analysis just because the canvas became crowded.

Atlas dependency map with a selected query scope
The Map view connects sources, intermediate scopes, and outputs while keeping the related SQL within reach.

Lineage

The Lineage view traces how output columns are produced, and it separates value lineage from row influence. Value lineage answers a familiar question: which source columns contributed values to this output? Row influence is a different question. A column may decide whether a record survives a filter without contributing a value to the selected metric at all.

Suppose deleted_users.user_id removes rows before an aggregation. That column affects the population used by COUNT(DISTINCT email_encrypted), but it does not produce the value of email_encrypted. Treating those two relationships as the same thing creates a lineage graph that is confidently misleading, so Atlas shows them differently.

The lineage canvas pans and zooms. Real lineage needs that almost immediately, once several CTEs and calculated outputs are in play. A fixed strip lasted about as long as you would expect.

Not long.

Joins

The Joins view focuses on row-retention behavior. It reads the join type, the predicate, any post-join filters, and whatever syntax-backed evidence exists about possible fanout, and it does not assume a join column is unique unless the query itself proves something relevant.

This mattered to me because join explanations are easy to oversimplify. A filter on the nullable side of a LEFT JOIN can remove unmatched rows, but the resulting behavior is not always well described by saying "the join became an INNER JOIN." Sometimes the condition applies only to part of the population, sometimes the query creates a conditional anti-match, and sometimes the syntax simply is not enough to make a stronger claim. Atlas explains the observed row effect and keeps the missing evidence visible instead of rounding it off.

Review

The Review view turns the structural model into something closer to a SQL code-review companion, and it currently has four modes.

Execution explains how rows move through the query, with a guided mode that focuses on what enters, changes, and leaves each stage, and a technical mode that follows scopes and SQL clauses more closely. Risks lists findings with the relevant evidence, why they matter, and a next action, and a finding can link back to the related SQL or Map node. Verify collects the questions the query cannot answer by itself, such as uniqueness, actual row counts, data distribution, or whether the business requirement matches the implementation. Refactor suggests possible cleanup or restructuring, and it avoids calling a rewrite faster when no execution plan or runtime evidence exists.

Atlas also reports the final grain and a complexity breakdown, though the score is less interesting than the explanation behind it. A label such as "high complexity" is cheap. Naming the scopes, joins, windows, and expressions that produced the score is the part that helps.

Exports

Atlas can export the diagram as a PNG and the full analysis as a Markdown report. The report covers query identity and an executive summary, the query contract and output grain, the dependency flow, an execution walkthrough, column lineage, join behavior, risks and verification items, refactor notes, known unknowns, and the exact SQL used for the analysis.

The export reads the same immutable result the interface reads. If the editor changes after an analysis, the report does not quietly mix the old findings with the new SQL. That sounds like a small implementation detail until it fails during an investigation.

Then it becomes the whole story.

Atlas lineage and exported analysis report
The interface and exported report describe the same analyzed query revision.

Passage

Passage has two meanings and I wanted both of them. A passage is a route between two places, which is what a dialect conversion actually is. It's also a passage of text, which is the thing you're handing over. Either way you end up somewhere slightly different from where you started, and that is the honest description of moving SQL between engines.

Passage converts SQL between dialects, and cross-dialect conversion gets awkward quickly. Date functions change. String concatenation may behave differently around NULL. Identifier quoting changes. Ordering rules can require added expressions just to preserve the source behavior. Sometimes the converted SQL is correct and still looks as though a compiler had a difficult morning.

The workspace keeps three result views available: Output holds the converted SQL, Changes shows a side-by-side diff between the normalized source and target, and Warnings holds portability findings and conversion evidence. Around them sit source and target dialect selection, route swapping with a one-level undo, live conversion, formatting, file opening, copy, downloads, and a Markdown conversion report. The downloaded file carries both queries under labelled headings, because a file containing only the output cannot answer the obvious question of what it was converted from.

Above the result, Passage states one verdict rather than a row of metrics. Whether the query is ready to run in the target dialect, or was rejected by it, is a conclusion a reader can act on; three separate technical measurements ask them to derive that conclusion themselves. When a conversion fails, the engine's own diagnostic appears with its line and column, rather than a message pointing at a panel that has nothing in it.

While the user is typing an incomplete query, Passage keeps the last complete conversion visible, because a half-written clause should not erase a valid result. Converted SQL can also be opened directly in Atlas. That handoff matters because conversion should not end with "the target parser accepted it." The converted structure may still deserve inspection, since a different engine can express the same intent using a much stranger route.

Passage uses cautious language around all of this on purpose. A successful conversion means the tool produced target SQL. It does not mean two database engines are guaranteed to return identical results for every dataset and configuration.

Passage conversion with changes and portability findings
Passage keeps the generated SQL, textual changes, and review findings available as separate views.

Draft

Draft carries two useful meanings too. A draft is an early version you fully expect to revise, which is the right posture for a query you're still assembling. Drafting is also the older sense of the word, drawing something to scale before anyone builds it. Both of those felt more truthful than calling the thing a builder, since builder rather implies what comes out the other end is finished.

Draft is the visual SQL builder, and I have mixed feelings about query builders in general. They can make SQL easier to approach, but they can also hide so much that users no longer know what will actually run. Draft keeps the SQL visible at all times.

The workspace is a coloured block palette, a canvas, and an editable SQL pane. Every value is edited inside the block that owns it, so a table name, an alias, a join type, or a sort direction is typed where it appears rather than in a separate form. Blocks are shaped like puzzle pieces, and the shape carries meaning: a piece that stacks in a sequence has a notch on top and a tab underneath, while a piece that produces a value has rounded ends and a plug on its left. You can tell what a block does before reading its label. Between them the blocks cover sources, projections, joins, filters, aggregations, windows, ordering, limits, CTEs, subqueries, and raw SQL.

The canvas pans and zooms, and a block can be parked off to the side while you think. A parked block contributes nothing to the generated SQL, and the canvas says so plainly rather than leaving you to notice that your WHERE clause has quietly stopped appearing. Blocks can be moved with a pointer, touch input, or keyboard controls, and placement follows the document rules, so a block cannot be dropped somewhere merely because the cursor happened to be nearby. Templates, dialect selection, undo and redo, live diagnostics, block-level validation, SQL and text downloads, and a direct handoff to Atlas fill out the rest.

The difficult part is keeping blocks and SQL synchronized. A user may change the visual structure and then edit the generated SQL. If both sides change, Draft does not silently take whichever update arrived last; it reports the conflict and asks which version should win. Unsupported syntax can be preserved as raw SQL, because losing part of a query because the visual model did not understand it would be a very efficient way to destroy trust.

I would prefer not to test that theory in production.

Draft blocks beside editable SQL and diagnostics
Draft keeps the visual model and generated SQL beside each other, with explicit conflict handling.

Why the analysis stays in the browser

Your SQL isn't neutral text. It carries internal table names, metric definitions, business rules, schema structure, and sometimes an uncomfortable amount of information about how a company actually runs. I've written queries I would never have pasted into a stranger's website, and I assume you have too.

Browser-based gets used loosely, so here is exactly what happens. The parser and the analysis engine download into your browser and run on your machine. Your query isn't posted to a server to be analysed, isn't queued somewhere for processing later, and doesn't arrive anywhere that I can read it. There's no account and no login, so there's nothing for me to attach a query to even if I wanted one. And I don't. Holding other people's production SQL is a responsibility I have no appetite for whatsoever.

Two things I should be precise about, because a vague privacy promise is worth roughly as much as no promise. Draft does save your work in your own browser's local storage, so that a refresh doesn't destroy twenty minutes of block assembly. That copy lives on your machine, under your control, and clearing your site data removes it. Separately, the app is able to count anonymous page views. That counting stays off unless a specific environment variable is configured, it honours Do Not Track, and it's built so it cannot carry your SQL even by accident, because the sanitizer throws away every long string and every field whose name looks like a query, a token, an address or a person.

The browser-only choice costs something, and I'd rather name the costs than pretend they aren't there. An engine has to download before anything works at all. Very large queries need limits and recovery paths. When a worker dies it has to hand your text back intact, and a good result needs to stay on screen while a newer revision is still half-typed. Every one of those is more work than posting your query to a server would have been.

Worth it, though. I already have enough tables in my life.

Why five database dialects?

SQLTerrain currently works with five primary database dialects:

  • PostgreSQL
  • MySQL
  • SQLite
  • BigQuery
  • Snowflake

Their support levels aren't identical. PostgreSQL, MySQL and SQLite carry the deeper test corpus today, while BigQuery and Snowflake are still experimental, best-effort routes that I'm widening. That distinction is deliberate, because a dialect is a good deal more than a name in a selector. It means parsing, formatting, date expressions, intervals, casting, identifier quoting, CTEs, windows, limits, the constructs an engine flatly refuses, and the conversion rules connecting all of it. Granted, five is already enough to produce a startling amount of disagreement about how to write the first day of a month.

More will follow. The constraint is not the length of the list, it is that adding a dialect honestly means teaching the converter how that engine quotes identifiers, formats dates, handles intervals and casts, orders nulls, and which constructs it refuses outright, and then building enough test coverage to know when a conversion is wrong rather than merely unfamiliar. Right now I would rather have five dialects I trust than twelve that look supported in a dropdown. Once the accuracy work on these has settled, the sixth becomes a much smaller decision than the first five were.

There is also AQL, the SawitDB dual-syntax experiment hidden inside SQLTerrain. I do not count it as a sixth database, because that would require explaining SawitDB with a straight face for much longer than planned.

Yes, AI helped build this

Coding agents have contributed a lot to SQLTerrain. They helped with implementation, code review, tests, documentation, design experiments, and the repetitive work that would have slowed the project down. AI made the project move faster. It also created more things for me to distrust.

Generated code can look finished while carrying duplicated logic, stale state, weak recovery paths, controls a keyboard can't reach, or tests that prove almost nothing. Then a feature works beautifully on the one sample query it was written against and falls over on the first real one with two nested CTEs and an odd CASE. So I keep going back through it. I don't want this to be a polished demo held together by optimism, which means work gets split into checkpoints, complex queries become named fixtures, outputs are compared between the interface and the exported reports, failure states get tests of their own, and a feature comes back out when it adds more surface area than it earns.

Some of that checking pays off in ways I did not plan. A generated accuracy suite recently found that converting to MySQL silently drops explicit NULL ordering, which changes the row order without changing a single value. Nothing in the interface would have shown me that. A test that actually ran the converted query and compared the rows did.

There are still bugs.

There will be more. The real question is whether a product treats them as engineering work or papers over them with better copy. I want SQLTerrain to stay free while I'm still working out whether the idea holds up, and I want it to feel considered long before it's mature. Free and careless is easy to make. Free and careful takes longer, and it's the only version worth anybody's time.

A premium feeling doesn't come from gradients, or from a long word ending in "intelligence." It comes from much smaller things, like your text surviving a crash, a clause the parser couldn't handle being kept as raw SQL instead of quietly vanishing, and being told which of two answers was verified rather than guessed at. None of that photographs well, which is more or less the point. I didn't build this so it would look clever in a screenshot.

A checkpoint plan beside the limitations it recorded
Each revamp is scoped as numbered checkpoints, and the walkthrough records what was left unfinished rather than only what passed.

What SQLTerrain cannot know

SQLTerrain does not run queries against production data. It does not inspect a real EXPLAIN plan, and it cannot see actual row counts, indexes, partitions, constraints, distribution, or warehouse cost unless that evidence is supplied. Syntax cannot prove that the business requirement is correct, and it cannot guarantee that a converted query behaves identically on another database engine.

Those are not temporary disclaimers waiting to be removed once the product looks more confident. They define the boundary of the tool. SQLTerrain should show what it can observe, explain where a result came from, and leave missing information unresolved. A trustworthy tool also needs to know when to stop.

What I am trying to build

I am not trying to collect the largest number of SQL utilities. I want SQL review to feel less opaque. Atlas should leave the user with a clearer picture of the query, Passage should make a conversion easier to inspect, and Draft should make structure and syntax easier to connect.

Different users will read the same evidence differently. A learner may focus on why a filter removes rows, an analyst may care about grain, and a data engineer may look at lineage, dependencies, risks, and the effect of a change on historical data. The same query can support each of those conversations, and that is the part of SQLTerrain I want to keep developing.

What comes next

I have a long list of ideas. Some involve comparing query revisions; others involve schema-aware analysis, stronger diagnostics, and better ways to inspect what changed after a conversion. I am keeping most of that roadmap private for now, because some ideas will fail during testing and others may solve a problem nobody actually has. Announcing ten features is much easier than finishing three properly.

The direction is enough for now. Build things that help SQL practitioners inspect and review their work, and remove things that only make the homepage longer.

I need real feedback

SQLTerrain is free to use while I continue testing the concept, and specific feedback helps most. A dependency map may be wrong. Lineage may point to the wrong source, or row influence may be presented as value lineage. A grain explanation may overstate the evidence. A dialect conversion may look suspicious. Draft may lose part of a query. The interface itself may also make a simple task harder than it needs to be.

Please tell me when that happens. A feature that looks impressive but is not useful is also worth reporting, possibly more so.

You can reach me at hello@caesarmar.io, or submit a structured report through [SQLTerrain feedback form - link coming soon]. Please remove confidential table names, customer information, values, and business rules before sharing a query.

One small thing I can give back

SQLTerrain began with a feeling I kept having while working with SQL. I could read the syntax. I still wanted a better way to see the structure. This project is my attempt to make part of that reasoning easier to inspect, teach, discuss, and challenge.

Some features will improve, some will be replaced, and a few will disappear quietly and receive no farewell post.

That is fine.

The principle stays:

Build something useful, be honest about its limits, and return part of what I have learned to the community.