Build a real query engine from scratch in Clojure — the kind that powers Datomic and DataScript. Start with a bag of facts, end with recursive rules, aggregates and negation. Queries are data; you write the engine that runs them.
Every database you've ever touched hides a query engine behind a language — SQL, Cypher, whatever — and treats it as a sealed box you're allowed to use but never see. This project pries the box open. You won't build SQL; you'll build Datalog: the small, startlingly elegant, logic-programming query language at the heart of Datomic and DataScript, descended from Prolog and the deductive-database research of the 1980s, and a long-standing favourite in the Clojure world for reasons you're about to feel firsthand. By the end you'll have a working engine — written line by line in your own hands — that answers questions like “who are all of Alice's ancestors?” over an in-memory database you also built.
It all rests on one idea so simple it feels like a trick: a database is just a set of facts. Each fact is an [entity attribute value] triple called a datom — [1 :name "Alice"], [1 :age 38] — and that's the entire storage model. No tables, no schema, no ALTER TABLE; you add a new kind of information by simply storing a new attribute. The twist that makes Datalog beautiful is that a query is data too — a plain vector like [:find ?name :where [?e :name ?name] [?e :age ?a] [(>= ?a 18)]] that you can read, build, and pass around like any other value. Your engine is the thing that turns that second piece of data into answers over the first.
You build it as a ladder of twelve small, testable rungs. First you model facts and pull entities back out of the soup. Then comes the engine's beating heart: logic variables and unification — the act of making a pattern like [?e :name ?n] agree with a datom and binding what it learns — which turns out to be the same primitive behind Prolog, type checkers, and every logic engine ever written. Get unify right and joins fall out for free: a variable shared across two clauses links facts together with no JOIN ... ON and no query planner, just consistency. From there the engine gains real teeth — find specifications (scalars, collections), predicates like [(> ?age 18)] resolved through a safe whitelist so a query can never run arbitrary code, and function bindings that compute fresh values mid-query.
Then the crown jewel: recursive rules. You define ancestor in terms of itself and watch your engine compute the transitive closure of a family tree — every ancestor, however many generations up — with a seen-set guard that keeps recursion finite even when the graph has cycles. This is the thing SQL bolted on late and awkwardly as WITH RECURSIVE; in Datalog it falls out naturally, because rules are data and a clause is allowed to name itself. You finish the engine with aggregates (count / sum / max, grouped implicitly by the plain :find variables) and negation (not as a correlated antijoin — "keep the rows for which no match exists").
Everything is plain Clojure with zero dependencies — just clojure.test. Each challenge is graded by running real test suites against your engine, and because the tasks are cumulative, your finished datalog.clj answers all twelve at once: a complete query engine that matches, joins, derives, recurses, aggregates, and negates. You will never look at a query language as magic again — you'll know exactly how a declarative question becomes an answer.