Skip to content

lukaskratzel/voight

Repository files navigation

voight

voight is a small SQL compiler and policy engine for a restricted SELECT subset of MySQL.

Stages

voight runs SQL through these stages:

  1. lex parses raw text into tokens
  2. parse builds an AST
  3. rewrite applies query rewrites
  4. bind resolves tables and columns against a catalog
  5. enforce applies compiler policies
  6. emit produces canonical SQL

The result is a normalized query plus structured diagnostics when something fails.

What It Does

  • 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

Example

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
}

Virtual Table Aliases

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

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.

Diagnostics

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".

Development

bun install
bun run test

About

Typescript SQL compiler and policy enforcement engine.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors