← All writing

ADL vs SQL

The Spec That Runs — One Layer Down

TL;DR: SQL is the most successful executable specification in software history: you don’t describe a table and then build it; the DDL is the table. That’s exactly the property ADL wants, which is why this comparison is an inheritance, not a fight. But SQL executes only the storage layer. It knows nothing about your application’s users (database roles are not your auth system), your API, your state machines, or your business policy. The industry’s one attempt to push application logic down into the database (stored procedures) failed for reasons worth remembering. ADL takes the opposite direction: it lifts the declarative, executable property up to the application layer, and writes the SQL for you. Same YAML, same behavior, on SQLite and PostgreSQL.


SQL already won the argument

The first article in this series claimed that specifications drift from implementations because they’re separate artifacts. There’s one glaring, sixty-year-old counterexample, and it’s sitting under every application you’ve ever built.

Nobody maintains a Word document describing their database schema and a separate implementation of it. The CREATE TABLE statement is not documentation of the table. It is the table. Constraints declared in DDL are enforced on every insert, forever, without anyone remembering to. When you want to know what the schema is, you ask the database, and the answer is true by construction. Schema and implementation cannot drift because there is only one artifact.

This is why SQL is the strongest proof the interpreted-specification idea has: a declarative language, interpreted by an engine, running the real thing in production for half a century. ADL isn’t arguing with SQL. It’s arguing that SQL stopped one layer too low.

What the table doesn’t know

Stand at the table boundary and look up. Everything above it is invisible to SQL.

SQL has no concept of your application’s users. Database roles exist, but nobody maps end users to Postgres roles. Your auth system lives in the application, which means every access decision (“owner can edit their own drafts, editors can edit anything, only admins delete”) lives in application code the database never sees. SQL can enforce that status is one of five strings via a CHECK constraint; it cannot enforce that only an editor may move it to published, or that published requires a publish date, or that the transition should stamp publishedAt and notify the author. It can enforce a UNIQUE constraint; it cannot enforce “total effort commitments for this person may not exceed 100%,” because that’s a policy across rows evaluated in a business context, with an error message a human reads and an audit trail a regulator reads.

And SQL has no API. The gap between “a correct schema” and “a running backend” is the entire application tier: HTTP routing, serialization, auth middleware, permission checks, validation, pagination, the works. In a conventional stack, that tier is tens of thousands of lines of hand-written or framework-shaped code, the exact artifact that drifts from every document describing it.

The road already tried: pushing logic down

The obvious response, historically, was to push application logic into the database: stored procedures, triggers, the “thick database” architecture. It genuinely delivered the single-artifact property: the logic lived where the data lived, enforced by the same engine.

It also failed, culturally and practically, and honesty requires saying why. Procedural SQL dialects are miserable languages for expressing business logic: imperative code wearing declarative clothing, with weak abstraction, weak testing stories, and no ecosystem. The logic became invisible to application developers, welded to one vendor’s dialect, and versioned outside every tool the team used. “The behavior is in the database” became a warning, not a boast.

The lesson is precise, and it’s not “logic near data is bad.” It’s that the language was wrong. PL/SQL asked developers to express application semantics in a storage engine’s procedural dialect. The semantics deserved their own declarative language, at their own layer.

Lifting the property up

That’s the ADL move. Keep SQL’s defining property (the spec is the running thing) and apply it at the application layer, in a language built for application concepts.

An ADL resource declares fields and types, and the Kalos runtime derives the DDL: tables, columns, foreign keys, indexes, junction tables for many-to-many relationships. Declare onDelete: restrict on a relationship and the runtime both generates the referential constraint and returns the correct 409 at the API boundary. Declare an index and it exists. You write types and relationships; the engine writes SQL — and writes different SQL per backend, which is where this gets concrete.

Change a field from optional to required. On PostgreSQL, that’s one statement: ALTER TABLE posts ALTER COLUMN title SET NOT NULL. On SQLite, which can’t do that, it’s a four-step column rebuild (add a temp column, copy with COALESCE for the nulls, drop the old column, rename), all inside a single transaction so failure leaves the schema untouched. The ADL author changed one line of YAML. The migration was diffed against the last known model, generated for the specific backend, and applied atomically, with destructive changes refused unless explicitly opted into. This is the class of work (correct, tedious, dialect-specific, dangerous to get wrong) that the declarative layer exists to absorb.

And above the schema, the same document declares what SQL never could: the roles, the state machine, the validation, the cross-row policy with its human-readable message and its regulation citation. One artifact, storage layer to API surface, enforced by one runtime.

Where SQL is simply better

Three concessions, because they’re real.

First, analytics. Generated transactional queries will not beat a hand-tuned analytical query, and were never meant to. ADL is a specification of the application layer: the transactional core. Your warehouse, your reporting replicas, your window-function artistry remain SQL’s kingdom, and since ADL data lives in plain SQLite or PostgreSQL tables with predictable names, that kingdom has full access to it. Nothing is trapped.

Second, escape reads. Even inside a Kalos application, some logic needs a query the declarative layer doesn’t express, and the Lua scripting layer exposes read-only SQL for exactly that: read-only by default, because writes must pass through the pipeline. Raw SQL exists only as an explicit, loudly-logged opt-in for trusted deployments. The boundary is deliberate: SQL as a query surface is welcome; SQL as a bypass is not.

Third, permanence. SQL is an ISO standard with a fifty-year installed base. ADL is one vendor’s language. That asymmetry is real, and it’s precisely why ADL compiles to standard SQL on standard databases rather than inventing storage: the most durable artifact in the system — your data — lives in the most durable format in the industry.

The one-line version

SQL proved that a specification can be the running system. ADL extends that proof from the table to the application — and hands the SQL back down, correctly, for two databases, from one YAML file.

Next in the series: ADL vs GraphQL — a beautiful contract for reads, silence on the only hard part: writes.