← All projects

OwlSQL

Write raw SQL and get fully typed results, with no ORM, no codegen and no runtime parsing.

OwlSQL exists because of a familiar backend problem: you pick raw SQL over an ORM to keep control of your queries, and every result comes back as unknown[]. The usual fix is writing an interface by hand for each query. That interface restates the column list in a second syntax, and it quietly drifts out of sync the moment someone edits the SQL and forgets the type.

The library's answer is to make the compiler read the query. The entire SQL parser is written as recursive template literal types evaluated by tsc: it normalizes the string, splits the column list, resolves aliases and qualified columns against your schema type, and assembles the exact row shape while you type, in the editor, with no build step.

What ships to production is a passthrough of about 175 lines. It forwards your SQL string, unchanged, to whatever driver you already use (pg, mysql2, better-sqlite3 or node:sqlite) and wraps the rows in a Result. There is no generated file to keep in sync and no SQL parser in the bundle: all the intelligence lives in the .d.ts files.

The trade-off is compile time, and the project measures it instead of hiding it. A fixture of 100 tables and 32 queries (joins, GROUP BY, CTEs, UNION, strict mode) type-checks in about 0.4 s, and CI enforces a ceiling on type instantiations so the parser can't quietly get slower.

Highlights

Zero runtimeThe parser costs 0 bytes; the JavaScript that ships is a thin wrapper around your driver.
No build stepNo codegen, no watcher, no database connection at build time, no generated files in version control.
Any driverYour query string reaches the driver exactly as you wrote it.
Real SQL subsetAliases, *, joins, aggregates, CTEs, UNION, INSERT/UPDATE/DELETE with RETURNING, typed parameters, and a strict mode that turns typos into type errors.

In practice

type DB = {
  users: { id: number; name: string; email: string; active: boolean };
};

const db = createTypedDb<DB>(createPgExecutor(pool));

const a = await db.query('select id from users');
//        a.value: { id: number }[]

const b = await db.query('select name as handle, active from users');
//        b.value: { handle: string; active: boolean }[]

const c = await db.query('select id from users where id = $1', 7);
//                                                          ^ typed as number
← All projects