voight is a small SQL compiler and policy engine for a restricted SELECT subset of MySQL.
voight runs SQL through these stages:
lexparses raw text into tokensparsebuilds an ASTrewriteapplies query rewritesbindresolves tables and columns against a catalogenforceapplies compiler policiesemitproduces canonical SQL
The result is a normalized query plus structured diagnostics when something fails.
- Accepts a controlled subset of
SELECT - Resolves tables and columns from a catalog
- Normalizes emitted SQL
- Supports CTEs, joins, subqueries, grouping, ordering, and limits
- Supports virtual table aliases through the catalog
- Provides stage-by-stage compiler results for debugging
import { compile, InMemoryCatalog, createTableSchema } from "voight";
const catalog = new InMemoryCatalog([
createTableSchema({
id: "users",
path: ["users"],
columns: ["id", "name", "tenant_id", "created_at"],
}),
]);
const result = compile(
"SELECT id, name FROM users WHERE tenant_id = ? ORDER BY created_at DESC LIMIT 10",
{
catalog,
},
);
if (result.ok) {
console.log(result.emitted.sql);
// SELECT `users`.`id`, `users`.`name` FROM `users`
// WHERE `users`.`tenant_id` = ? ORDER BY `users`.`created_at` DESC LIMIT 10
}Logical table names can be mapped to physical tables by wrapping the catalog.
import {
AliasCatalog,
InMemoryCatalog,
compile,
createCatalogAlias,
createTableSchema,
} from "voight";
const baseCatalog = new InMemoryCatalog([
createTableSchema({
id: "internal_projects",
path: ["internal_projects"],
columns: ["id", "name", "tenant_id"],
}),
]);
const catalog = new AliasCatalog(baseCatalog, [
createCatalogAlias({
from: ["projects"],
to: ["internal_projects"],
}),
]);
const result = compile("SELECT id, name FROM projects WHERE tenant_id = ?", {
catalog,
});This emits SQL against internal_projects, while the user can keep writing projects.
Policies are higher-level compiler features that can both rewrite queries and enforce invariants.
Built-in policies hide low-level AST and binding details from library users. For example, tenantScopingPolicy(...) can inject tenant filters automatically:
import { InMemoryCatalog, compile, createTableSchema, tenantScopingPolicy } from "voight";
const catalog = new InMemoryCatalog([
createTableSchema({
id: "timeseries",
path: ["timeseries"],
columns: ["metric", "tenant_id"],
}),
]);
const result = compile("SELECT metric FROM timeseries", {
catalog,
policies: [
tenantScopingPolicy({
tables: ["timeseries"],
scopeColumn: "tenant_id",
contextKey: "tenantId",
}),
],
policyContext: {
tenantId: "tenant-123",
},
});This rewrites the query so every timeseries scan is scoped to the provided tenant. Policies also work through nested query blocks such as CTEs and derived tables.
Failures are reported at the stage where they happen.
const result = compile("SELECT id FROM missing", {
catalog,
});
console.log(result.ok); // false
console.log(result.terminalStage); // "binder"
console.log(result.diagnostics[0]?.message); // Unknown table "missing".bun install
bun run test