← Back to list

I Have No Idea How a SQL Engine Works — So I’m Building One | Stage -1

Hello everyone, back to Medium after a looong break… and I’m not coming back empty handed. I’m coming back with a new project. A big one.

zoolpher · 2026-05-21 12:16 · 0 claps · 3.5 min read
#google #facebook #amazon #microsoft #zerodha
Open on Medium ↗

I Have No Idea How a SQL Engine Works — So I’m Building One | Stage -1

Hello everyone, back to Medium after a looong break… and I’m not coming back empty handed. I’m coming back with a new project. A big one.

It’s called sql-engine — and no, I don’t fully know how it works yet. That’s kind of the point.

I’m learning and building it simultaneously. No course. No tutorial. Just me, C++, and a lot of confused staring at the screen. I’m documenting everything as I go — the wins, the bugs, the moments where I had absolutely no idea what I was doing.

You can follow along everywhere:

So what even is a SQL engine?

When you write something like:

SELECT name FROM users WHERE age > 20;

Something has to read that. Something has to understand it, validate it, figure out the cheapest way to execute it, and then actually go fetch the data.

That something is a SQL engine. And it’s not magic — it’s a pipeline. A series of components, each doing one job, handing off to the next.

Here’s what the full pipeline looks like:

Raw SQL string
     ↓
Lexer          → breaks string into tokens
     ↓
Parser         → builds an Abstract Syntax Tree (AST)
     ↓
Semantic Analyzer → validates tables, columns, types
     ↓
Query Planner  → converts AST into a logical execution plan
     ↓
Optimizer      → picks the cheapest way to execute
     ↓
Executor       → fetches data, returns results

I’m building every single one of these from scratch. In C++. No shortcuts.

Stage 1 — The Lexer

Stage 1 is complete and it’s the Lexer — also called a Tokenizer.

The lexer’s job is simple to explain and surprisingly fun to build: take a raw SQL string and break it into a list of tokens. A token is just a pair — a type and a value.

So this:

SELECT name FROM users WHERE age >= 18;

Becomes this:

[SELECT]      => "SELECT"
[IDENTIFIER]  => "name"
[FROM]        => "FROM"
[IDENTIFIER]  => "users"
[WHERE]       => "WHERE"
[IDENTIFIER]  => "age"
[GTE]         => ">="
[INTEGER]     => "18"
[SEMICOLON]   => ";"

Every keyword, every operator, every identifier, every literal — all classified and labeled before the parser ever sees them.

The interesting parts

A few things in the lexer that I found genuinely interesting to implement:

The >= lookahead — when the lexer hits a > character, it can't just immediately emit a GT token. It has to peek at the next character first. If it's =, the token is GTE. This peek-ahead pattern is a classic lexer technique and shows up everywhere.

The i-- trick — the lexer uses a for loop with i++ at the end. But when scanning a multi-character token like a keyword or number, the inner while loop already advances i. So before returning control to the for loop, you have to i-- to avoid skipping a character. Miss this once and you'll spend an hour debugging disappearing characters.

Integer vs Float detection — when scanning a number, the lexer checks for a . character mid-scan. If it finds one, the token type flips from INTEGER to FLOAT. Simple, but satisfying.

A Bug I Found — and Why It Matters

While writing tests for the lexer, I found a silent bug that I think is worth talking about.

Take this broken query:

SELECT name FROM users WHERE city = 'Delhi

No closing quote. The lexer hit the opening ', started collecting characters, and just... kept going until it ran out of input. Then it quietly emitted a STRING token with value Delhi and moved on like nothing happened.

No error. No warning. Nothing.

The problem? Everything downstream — the parser, the semantic analyzer, the planner — receives what looks like a perfectly valid token stream. They have no idea the original query was broken. The error surfaces deep inside the pipeline, far from where it actually happened, which makes debugging a nightmare.

The fix is one line — after the string scanning loop, check why the loop exited:

if (i >= input.length()) {
    throw std::runtime_error("Lexer error: unterminated string literal");
}

If the loop ran out of input before finding a closing quote, throw immediately. Fail loudly and early.

A lexer that silently swallows malformed input makes debugging hell for every stage that comes after it.

This is a real principle — Postgres, SQLite, and MySQL all catch this at the lexer level for exactly this reason.

What’s Next

Stage 2 is the Parser.

The lexer hands off a flat list of tokens. The parser’s job is to take that list and build an Abstract Syntax Tree — a nested structure that actually represents the meaning of the query, not just its characters.

That’s where things get genuinely hard. And genuinely interesting.

I’ll be documenting it the same way — building in public, sharing everything, bugs included.

If you want to follow along, the GitHub repo has the full source with stage-by-stage documentation:

See you in Stage 2.

Google Machines Geekologi™ Microsoft + Open Source FAANG Master

Built by [zoolpher](https://github.com/zoolpher/sql-engine.git) — B.Tech CS, systems engineering track.


메타데이터
post_id
e849cdf6574b
slug
i-have-no-idea-how-a-sql-engine-works-so-im-building-one-stage-1-e849cdf6574b
url
https://medium.com/@zoolpher/i-have-no-idea-how-a-sql-engine-works-so-im-building-one-stage-1-e849cdf6574b
canonical_url
https://medium.com/@zoolpher/i-have-no-idea-how-a-sql-engine-works-so-im-building-one-stage-1-e849cdf6574b
author_url
https://medium.com/@zoolpher
status
ok
fetched_at
2026-07-11 20:50:18