Schema and query languages for PostgreSQL, compiled to SQL.
Axel is a schema and query language tool for PostgreSQL. You define your data model in ASL (Axel Schema Language) and write queries in AQL (Axel Query Language). Axel compiles both to SQL — migrations from ASL, parameterized query strings from AQL.
Axel never wraps a driver or executes queries on your behalf. It generates SQL; you run it however you like.
Note
Disclaimer: The code in this repository is majorly AI-generated. Review it carefully before relying on it in production.
Modern applications rely on PostgreSQL for its power, performance, and advanced capabilities. Yet the tools we use to interact with PostgreSQL often fight against it:
- Framework-coupled ORMs (Django, Rails Active Record): Deeply embedded inside specific runtimes. They abstract away the relational model so heavily that complex queries stop feeling like SQL and become impossible to optimize without dropping to raw text.
- Fragile relationship models (TypeORM, Sequelize): Convoluted entity decorators, confusing ownership models, and endless mental gymnastics just to model simple one-to-many or many-to-many relationships.
- TypeScript-first query builders (Drizzle, Kysely): Great for type inference, but relationship definitions and relational queries often feel estranged and bolted-on.
- Black-box runtime ORMs (Prisma 6 vs. Prisma 7): Prisma 6 relied on a heavy Rust binary sidecar (causing high latency, large bundles, and serverless cold starts). Prisma 7 migrated to TypeScript/WASM to fix binary issues, but locked Prisma strictly to the JS/TS ecosystem. In both versions, it remains a heavy runtime layer that struggles with native upsert conflict handling (
ON CONFLICT DO UPDATE), cannot declaratively manage PostgreSQL extensions/triggers, and incurs query execution overhead. - Neglected PostgreSQL features: Good luck defining native PostgreSQL extensions (
pgvector,pg_trgm,postgis,citext), row-level security (RLS) policies, triggers, custom functions, partial indexes, and conditional aggregates cleanly across any of them. - Enterprise grade: Traditional ORMs struggle to deliver clean, predictable SQL that database administrators and high-throughput systems demand.
Axel rethinks database tooling by separating data modeling and query compilation from runtime execution:
- Compiler, Not a Runtime ORM: Axel does not wrap a driver, maintain a connection pool, or execute queries behind your back. It compiles
.aslschemas into deterministic migration files and.aqlqueries into pure, optimal PostgreSQL queries. You run the SQL using your language's native driver (pgx,node-postgres,sqlx, etc.). - First-Class Relational Modeling: Define single links (
link author: User) and many-to-many relationships (multi link members: User) directly in ASL. Axel automatically creates and manages junction tables and foreign keys. - Zero N+1 Query Aggregation: Fetch deeply nested relational shapes in a single query. Axel compiles nested shapes directly to PostgreSQL's native
json_aggin one scan — no N+1 query problem, no multiple roundtrips. - Native PostgreSQL First: Built-in syntax for PostgreSQL extensions (
extension pgcrypto;), triggers (trigger ... do (...)), row-level security (policy ... using (...)), conditional aggregates (FILTER (WHERE ...)), top-level CTEs (with (...)), and upserts (unless conflict on ... else (...)). - Universal Language Support: Because Axel compiles to pure SQL, it works with any programming language. Axel provides first-party code generators for Go and TypeScript, and you can write a generator for any other language in Go (or as an external plugin). If your language generator is missing, file an issue to request it!
- Integrated Tooling: Language server (LSP) for VSCode and Zed, and an interactive database studio (
axel studio).
schema.asl ──► axel diff ──► migration.sql ──► axel up ──► PostgreSQL
query.aql ──► axel compile ──► parameterized SQL (you execute this)
Benchmarked on September 11, 2026, using pnpm, Node 24.21.0 LTS, and PostgreSQL 18.6: Axel's generated TypeScript AQL runtime and query builder versus Prisma 6/7, TypeORM, and Drizzle. AQL parsing and SQL generation are included in the timings.
- At concurrency 10, Axel's builder had the highest measured median throughput in the saved comparisons: 6,520 point reads/sec, 4,094 100-row reads/sec, 5,345 nested reads/sec with join loading, and 2,134 inserts/sec.
- For nested reads, builder join loading measured 31% higher throughput than its default aggregate strategy. Prisma 7's query-loading variant measured 5,239/sec, close to Axel builder join. At concurrency 1, aggregate loading performed better for both Axel modes; Prisma 6 led point reads.
- These are provisional local results, with wide variation between trials. Workloads and the additional Prisma 7 variant were collected in separate runs; the report preserves their dates and provenance. All measured variants passed result-equivalence checks. The nested strategy run includes the TypeScript runtime strategy support added in this repository.
See the complete benchmark report for every workload, sorted throughput tables, latency percentiles, and trial ranges. The benchmark harness includes reproduction commands, raw measurements, SQL captures, and known limitations.
Define types, links, constraints, and indexes in .asl files:
abstract type Base {
required id: uuid { default := gen_uuid(); constraint pk; };
required created_at: datetime { default := datetime_current(); };
}
type User extending Base {
required email: str { constraint exclusive; };
name: str;
required age: int32;
active: bool { default := true };
}
type Post extending Base {
required title: str;
required link author: User; # adds an FK column named "author"
multi link likes: User; # creates post_likes junction table
multi tags: str; # a TEXT[] array column (multi scalar, not a link)
}Run axel diff -n "initial schema" to diff against the last migration and produce a .sql file. Run axel up to apply it.
Full reference: docs/asl.md
Write queries in .aql files and compile them to parameterized SQL:
multi select User {
id,
email,
posts: {
id,
title
}
}
filter .active = true and .age >= $min_age
order by .created_at desc
limit $limit;
-- $1: min_age
-- $2: limit
SELECT
u.id AS id,
u.email AS email,
(SELECT COALESCE(json_agg(row_to_json(p_posts_sub)), '[]')
FROM (...) p_posts_sub) AS posts
FROM "user" u
WHERE u.active = true AND u.age >= $1
ORDER BY u.created_at DESC
LIMIT $2;Nested shapes compile to a single query using PostgreSQL's json_agg — no N+1.
AQL supports SELECT, INSERT, UPDATE, and DELETE:
insert User { email := $email, name := $name, age := $age };
update User filter .id = $id set { name := $name };
delete User filter .id = $id;
select count(User filter .active = true);
Full reference: docs/aql.md
Point axel at your project directory and it discovers the config automatically:
axel -d ./myproject validate
axel -d ./myproject compile --aql 'select User { id, email };'
axel -d ./myproject upDiscovery order inside --dir:
axel.yaml— loaded as the full config if foundschema.asl— used as the schema if noaxel.yamldefault.asl— fallback schema name
Or use an explicit config file:
# yaml-language-server: $schema=https://raw.githubusercontent.com/struckchure/axel/main/schema.json
database-url: postgres://user:pass@localhost:5432/mydb
schema-path: ./schema.asl
migrations-dir: ./migrations
rel-load-strategy: query # query | join
codegen:
generator: go # go | ts
out-dir: ./db/generated
queries:
- ./queries/*.aqlaxel -c axel.yaml <command>| Command | Description |
|---|---|
axel init |
Scaffold a new Axel project (config, schema, migrations) |
axel validate |
Parse and validate a .asl schema file |
axel compile |
Compile an AQL query to parameterized SQL |
axel run |
Execute an AQL query against the database |
axel repl |
Interactive query REPL session |
axel codegen |
Generate type-safe Go or TypeScript code |
axel diff |
Diff schema and write a new migration file |
axel up |
Apply all pending migrations |
axel down <n> |
Roll back the last N migrations |
axel status |
Show migration state |
axel studio |
Browser database viewer and query editor |
# Initialize project
axel init
# Validate schema
axel -d . validate
# Interactive REPL
axel repl
# Run query against database
axel run -c 'select User { id, email }'
# Compile a query (use single quotes to prevent shell $expansion)
axel -d . compile --aql 'select User { id, email } filter .id = $id;'
axel -d . compile --file queries/get_user.aql --out queries/get_user.sql
# Migrations
axel -d . diff -n "add posts table"
axel -d . up
axel -d . down 1
axel -d . statusFull reference: docs/cli.md
# 1. Write your schema
vim schema.asl
# 2. Validate it
axel -d . validate
# 3. Generate a migration
axel -d . diff -n "initial schema"
# 4. Apply it
axel -d . up
# 5. Write queries
vim queries/get_users.aql
# 6. Compile to SQL
axel -d . compile --file queries/get_users.aql
# 7. Execute the SQL in your application however you likeA Zed extension provides syntax highlighting for .asl and .aql files.
To install it, open the command palette in Zed, run zed: install dev extension, and select the tools/zed directory.
A VS Code extension is in tools/vscode. Both run the axel CLI as their language
server (axel lsp), so diagnostics, hover, go-to-definition and completion need axel on your
PATH.
See tools/zed/README.md for details.
tools/agent is a drop-in guide that teaches any coding agent ASL, AQL, and the
axel workflow:
npx skills add struckchure/axelThat covers Claude Code, Codex, Cursor, OpenCode, Copilot and 70-odd others. It is plain Markdown, so you can also copy it by hand — see tools/agent/README.md.
Full documentation: https://struckchure.github.io/axel
- Schema language (ASL) — types, links, constraints, triggers, policies
- Query language (AQL) — selects, shapes, inserts, upserts
- CLI reference — commands, flags, and
axel.yaml - AI tooling —
llms.txtand the agent guide
The docs are also published for machines as llms.txt (page index) and llms-full.txt (everything, one file).
See LICENSE for details.