Familiarity
Familiar keywords + pipeline syntax. Minimum surprises in generated SQL.
using analyticsusers .where(active == TRUE) .addColumn(active -> active_flag) .orderBy(created_at.desc) .limit(10)
SELECT "users"."id", "users"."name", "users"."active", "users"."created_at", "users"."active" AS "active_flag"FROM "analytics"."users" AS "users"WHERE "users"."active" = TRUEORDER BY "created_at" DESCLIMIT 10;
SELECT users.id, users.name, users.active, users.created_at, users.active AS active_flagFROM analytics.users AS usersWHERE users.active = TRUEORDER BY created_at DESCLIMIT 10;
Modern programming language
Type safety
-
Static Structural Typing
-
Higher confidence in large codebases
-
Coding agents get feedback early
LSP server
-
Syntax highlighting
-
Autocomplete
-
BtrQL Outline
Debugger
-
Views/TVF/extension methods
-
Stored procedures not supported yet
-
Available via CLI/MCP for agents
Reusability
Less code - less bugs. Extension methods and columns stick to compatible relation types.
using analytics// Any type that contains column dept_id of type INT// will now have this method available.extension [dept_id: INT] { method withDepartmentName = self .innerJoin(department on dept_id == department_id) .select(department_name)}
using analyticsimport department_opsemployees .withDepartmentName() .select(name, department_name)
Alternatives
BtrQL aims to look transparent to those who know SQL but discoverable for newbies (via code completion and static type checking).
- Strong First class support
- Partial Possible, with caveats
- Missing
| Capability | BtrQL | ORMs (Entity Framework, Hybernate) | SQL builders(JOOQ,...) | PRQL |
|---|---|---|---|---|
| Independent from particular PL/VM ecosystem |
Portable source
Same btrql could be used to generate sql stored procedures that could be called from any programming language. |
Runtime-bound
Queries are often generated in runtime using reflection or some platform dependent mechanism. |
Library-bound
Closer to generated SQL, easier to understand by folks from different tech stack, but not portable. |
Portable source
A standalone language compiled to SQL. |
| Static Structural typing |
Structural rows
Row shape carries column names and types through transformations. Extension methods and columns define minimal contract for an argument type. |
Nominal type system
Frameworks don't help here much. Some languages offer crutches. |
Nominal type system
Frameworks don't help here much. Some languages offer crutches. |
Missing
Quote from official roadmap "Currently PRQL compiles into SQL with no understanding of the underlying tables. " |
| Reusable query abstractions |
Main focus of the project
Modules, extension methods and columns were created to make reusing query logic easier. |
Runtime code generation
Which breaks transparency and makes reviewing actually executed SQL harder. Nominal type system limits what could be reused. |
Runtime code generation
Which breaks transparency and makes reviewing actually executed SQL harder. Nominal type system limits what could be reused. |
Doesn't seem like a focus of the project
No module system or extension-method/column equivalent. |
| Reviewable generated SQL before deployment |
Direct output
Generated SQL is the reviewable before deployment and it's also quite similar to btrql. |
Runtime code generation prevents it.
Generated SQL depends on mapping, lazy loading, and runtime code generation. But review-ability depends on complexity of your queries. |
Close to SQL, more transparent.
Builder calls often mirror SQL clauses directly, unless you need flexibility, which requires runtime code generation - then only DBMS logs. |
SQL is reviewable
|
| Tooling |
Tools for devs and agents
VSIX, LSP, CLI, MCP toolkit |
Host tooling
IDE support comes from the host language and framework. Debugging queries are out of scope for them. |
Host tooling
IDE support comes from the host language and framework. Debugging queries are out of scope for them. |
Language tooling
LSP/MCP/Debugger are on the roadmap. |
Type safety
Catching problems early, helps both developers and agents.
using analyticsusers .select(id, missing_name)
using analyticsextension [id: INT, active: BOOLEAN] { method onlyActive = self .where(active == TRUE)}view broken = users .select(id, name) .onlyActive()