Open-source project
fsprojects/Rezoom.SQL avatar
fsprojects/Rezoom.SQL

Rezoom.SQL turns SQL into an F# type-checked query

Statically typechecks a common SQL dialect and translates it to various RDBMS backends

681 stars23 forksF#MIT

At a glance

What is it?
Rezoom.SQL is an F# ORM that typechecks a common SQL dialect at compile time and translates it to SQLite, SQL Server or PostgreSQL. The unusual part is where the schema comes from: it is inferred by reading your migration scripts, so the type provider knows the Id column of Users is an int without ever talking to a database, and it can order independent migrations around each other so a team never has to agree on one true migration order.
Who is it for?
Adopt Rezoom.SQL if you write F# and want your queries checked by the compiler rather than at runtime, since the type provider infers the schema from your migration scripts and reports over 45 error types before you ever reach a database. Skip it if you are not on F#, since the generated types and the type provider are the whole product, and if you need a database the project does not target, because only SQLite, SQL Server and PostgreSQL are supported.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 121 days ago.
What is it written in?
Mainly F#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The dialect is the product, and there is a playground for it

Rezoom.SQL is an F# ORM for SQL databases, and the two sentences that follow define the whole approach. It integrates with the F# compiler via a generative type provider to statically typecheck its own dialect of SQL, and it knows how to translate that dialect to various backends, currently SQLite, SQL Server and PostgreSQL.

The dialect being a dialect rather than a set of extensions on standard SQL is the first thing to understand. You are writing SQL that Rezoom.SQL parses and typechecks, and then it emits something the target backend understands. That is a different proposition from writing portable SQL and hoping.

The good news is that you can try it before committing, and the documentation leads with that. There is a query playground, described as a place to try out the SQL dialect live, and the link is at the top of the README. The advice to check it out to see what kinds of SQL you can write is a sensible first step, because the dialect's expressiveness is the thing you need to know before you design a schema against it.

The developer workflow is the ordinary one for an F# library. You add a NuGet package, and you do not need to install any editor extensions or custom tooling, which is the practical appeal of the generative type provider over a code-generation step. The animated example in the README shows the loop: write a query, and the type provider gives you typed results to consume from F# code.

The project is F#, MIT licensed, and the title of the README says it is new and improved for 2026. The last push was on 2026-06-24 and there are no GitHub releases, so the repository is the distribution route.

The schema is inferred from your migrations

This is the part of the design that makes the type checking possible, and it is unusual enough to be worth understanding in detail.

In order to typecheck your queries, Rezoom.SQL has to know your database schema, so it can know, for example, that the Id column in the Users table is an int. It learns the schema by reading your migration scripts and observing what tables and views are created, columns added, and so on.

So the schema is not declared separately and not introspected from a live database. The migration scripts are the single source of truth, and the type provider derives the types from them.

That has two consequences that matter in practice. The first is that you can sketch out a model and change it as you code without touching a real database until you are ready to run, which is what the documentation recommends for the first iteration of an application or for a new feature with its own migration script.

The second is that a schema change is a compile-time event. When you make schema changes, for example replacing FirstName and LastName fields with a single FullName field, it is comforting to know the compiler will point out the queries you need to update.

The animation in the README, the model change one, shows exactly that loop: the model changes, the affected queries stop compiling, and you fix them before the database has ever been migrated.

This design also explains why the tool needs to read migrations in a form it can parse rather than run arbitrary SQL. It is a static analysis of a script, not an execution of one.

More than forty-five error types, caught before you run

The typechecker tightens up the feedback loop, so you do not waste your time tracking down typos and trivial SQL mistakes you would normally only encounter at runtime. There are currently over 45 different error types that can be detected at compile time.

The documentation shows three examples with screenshots, and each one is a class of mistake that a dynamic SQL layer lets through.

A mistyped table name is the first, and the screenshot is labelled as an example error on a mistyped table name. This is the case that a string-based query library cannot catch at all, since the table name is just a string until the database rejects it.

Incompatible data types is the second, the example error on comparing a string to an int. This is the one with the most value, because a type mismatch in a comparison is a logic bug rather than a typo, and the compiler catches it.

Selecting columns not included in a GROUP BY clause is the third, and it is the one that shows the typechecker is doing real SQL analysis rather than just name resolution. That rule is a SQL standard requirement about which columns may appear in the select list of a grouped query, and knowing it at compile time is exactly the kind of check that is expensive to debug from a runtime error.

So the type provider is not only a column-to-type map. It models enough of the language to apply semantic rules, which is what makes the difference between a statically typed ORM and a typed wrapper around strings.

Migration trees remove the need for one true migration order

There is a feature here that solves a real team problem, and it is the one that justifies the existence of a migration analyser in the first place.

Since Rezoom.SQL understands the language, it knows that some migrations like alter table X add column Y and alter table X add column Z can be run in any order and produce the same effects. When you are working with a team, you can take advantage of this to add the tables and columns you need for the feature you are coding, while your other team members do the same for their features, without having to decide the One True Migration Order when you merge.

The documentation links a MigrationTrees document for the details, so the mechanism is a tree rather than a list: the dependency structure between migrations is derived, and migrations that do not depend on each other can be applied in any order.

The value is larger than it first appears, because the One True Migration Order problem is usually solved by serialising people rather than by solving it. Every branch that adds a feature needs to rebase onto a moving target, and every merge needs a hand-checked sequence of SQL statements. Deriving the order removes a class of merge conflict that has nothing to do with code.

It also composes with the schema inference, since the schema is computed by applying migrations, and applying them in any valid order has to produce the same schema. That is a consistency requirement the implementation has to satisfy for the whole design to hold, and the fact that it is a documented feature rather than an accident suggests it was designed in from the start.

Interoperability with C# and VB.NET, including the nullable question

A generative type provider in F# usually produces F# types, which is a problem when the rest of your solution is not F#. The project handles this explicitly.

Because it is a generative type provider, it makes plain old .NET types you can use from other languages. You can write an F# project that uses Rezoom.SQL and defines your migrations and queries, then reference that from C# or VB.NET projects and use the generated query types with no problem in those languages.

There is a specific accommodation for the nullable case, which is the detail that makes the interop story work in practice. There is an option to represent nullable types with C#-style `System.Nullable<T>` instead of `FSharpOption<T>`, to make this scenario work extra smoothly.

That option exists because the two languages model absence differently. F# uses a discriminated union, and C# has no idea what that is, so without the option the generated types would be unusable from C#. The cost of the option is that an F# consumer gets the less precise representation, but since the choice is a setting rather than a hard-coded decision, you can pick the representation that fits the consumer of the types.

That is the shape of a library that is meant to live in a mixed-language solution rather than in a pure F# codebase.

Rezoom adds batching and caching on top

Rezoom.SQL can be used on its own, but as the name implies it is designed to work with Rezoom, and the two features that come with that integration are the reason to use the combination.

The first is automatic batching. With Rezoom, you build up a Plan to represent a transaction, which may involve multiple SQL commands or web API calls or other high-latency data manipulation. The example is two plans, one that makes three queries and one that makes two, and the difference is whether they are combined.

Combined sequentially, the plan expression is a sequence of let bindings, and it costs five round trips to the database:

fsharp
let sequential =
    plan {
        let! x = threeTrip
        let! y = twoTrip
        return (x, y)
    }

Combined concurrently, the same plan is a single tuple binding, and it costs three round trips, because the first two query batches sent to the database include pending queries from both plans:

fsharp
let concurrent =
    plan {
        let! x, y = threeTrip, twoTrip
        return (x, y)
    }

The change is one line of syntax and it cuts the round trips, because the two plans are in flight together rather than one after the other.

The second is automatic caching, and it works because each statically typed query comes with some useful info for caching. A compiler-generated ID. A boolean indicating whether it could make sense to cache, meaning it has no side effects and does not use rand(), newid() or similar. A bitmask of the tables it reads from. And a bitmask of the tables it writes to.

Rezoom uses that to avoid re-querying the same data during the execution of a Plan, which is within a transaction. The example is thirty functions calling LoadUserPermissions(currentUserId), where only one query for permissions actually runs when you use those functions together in a transaction. Unless you edit the permissions table during the transaction, in which case the cached result is automatically invalidated and the permissions are re-queried.

The write bitmask is the part that makes this safe rather than merely fast. A cache that does not know what a query writes cannot be invalidated correctly, and a transaction that reads and writes the same table would give you a wrong answer. Here the invalidation is derived from the query's static properties, so it is right by construction rather than by convention.

Getting started and what the repository holds

The getting started section is short and points at a tutorial. To get started using RZSQL, read the tutorial, and the documentation promises it will get you up and running in five minutes or your money back.

The navigation bar at the top of the README lists six documentation sections, and the spread of them tells you what the library does and how much surface area it has. There is a Tutorial, a Using Rezoom page for the integration, a Configuration page, a Language page for the SQL dialect itself, a UserTypes page and an API page.

A Language page is a strong signal that the dialect is meant to be learned properly rather than picked up incidentally, and a UserTypes page means you can extend the type system with your own types, which is how you would support a domain type that maps onto a column.

The top-level entries match that shape. There is src/ for the implementation, doc/ for the documentation sources including the Tutorial and Configuration directories, demos/ for the example applications, build/ for the build scripts, a Directory.Build.props and a version.props for the shared build configuration, a Gemfile for the Jekyll site build, _config.yml and _sass/ for the documentation site, and a SUMMARY.md which is the table of contents for a GitBook-style document.

There is also a CHANGELOG.md, a DEVELOPER_README.md and an AGENTS.md, and a LICENSE.txt. The AGENTS.md is worth a note in a project of this size, since it is a file that describes conventions for automated coding assistants, and its presence in a compiler project suggests the maintainers expect contributors to work with tooling rather than against it.

The library covers SQL Server through the T-SQL topic in the repository tags, along with SQLite and PostgreSQL, and the typechecker is the topic listed, which is the accurate summary of what this project is.

Editorial conclusion

Adopt Rezoom.SQL if you write F# and want your queries checked by the compiler rather than at runtime, since the type provider infers the schema from your migration scripts and reports over 45 error types before you ever reach a database. Skip it if you are not on F#, since the generated types and the type provider are the whole product, and if you need a database the project does not target, because only SQLite, SQL Server and PostgreSQL are supported. Verify first that your migrations are in a form the schema inference can read, that you can accept the dialect's restrictions, and that the query playground gives you enough to judge whether the dialect fits your queries before you commit.

Frequently asked questions

How does Rezoom.SQL know my database schema?

It reads your migration scripts and observes what tables and views are created and which columns are added, rather than introspecting a live database or requiring a separate schema declaration. That is why you can sketch a model and change it while coding without touching a real database until you are ready to run.

Which SQL errors does Rezoom.SQL catch at compile time?

There are currently over 45 different error types detected at compile time. The documentation shows three examples: a mistyped table name, an incompatible data type comparison such as a string compared to an int, and selecting a column not included in a GROUP BY clause.

Which databases does Rezoom.SQL support and how do I use it with Rezoom?

It currently supports SQLite, SQL Server and PostgreSQL. Used on its own it is an F# ORM with a statically typechecked dialect of SQL. Used with Rezoom it adds automatic batching, where two plans can be combined concurrently for fewer round trips, and automatic caching driven by compiler-generated query IDs and bitmasks of the tables each query reads and writes.

Official sources

  1. fsprojects/Rezoom.SQL on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fsprojects-rezoom-sql.svg)](https://hysenlabs.com/projects/fsprojects-rezoom-sql)

Was this useful?