# Moebius: SQL-shaped queries as data in Elixir

> Moebius builds PostgreSQL queries by piping small functions together and hands them to the database, with no schemas or migrations, and version 5.0 swapped the driver from Postgrex to epgsql.

**robconery/moebius** — A functional query tool for Elixir

- Repository: https://github.com/robconery/moebius
- Stars: 606 · Forks: 42
- Language: Elixir
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/robconery-moebius

## Queries are data, and functions transform them

Moebius is a functional query library for Elixir and PostgreSQL, and the whole model fits in one snippet:

```elixir
import Moebius.Query

{:ok, users} =
  db(:users)
  |> filter(:order_count, gt: 5)
  |> sort(:last, :asc)
  |> limit(20)
  |> Moebius.Db.run()
```

You pipe small functions together to build a query and then hand it to the database to run, and you get plain maps back. The origin story is stated plainly in the project documentation. It started in 2015 as a port of the ideas in MassiveJS, talk to Postgres directly, treat SQL as a friend rather than something to hide, and keep the API small enough to hold in your head. Queries are data, functions transform them, the database runs them. The project describes who it is for as people who like SQL and want to write it, or something close to it, instead of mapping it, people who want to store documents in Postgres and query them without setting up a separate database, and people who would rather have maps than structs and a pipe than a schema.

## It is deliberately not an ORM

The documentation puts this in the first paragraph, and it is the decision that defines the library. Moebius is not an ORM. There are no schemas, no mappings and no migrations, only queries and data. What it offers instead is close access to what PostgreSQL itself can do, with JSONB documents, full-text search, COPY, cursors, savepoints and EXPLAIN all a function call away. That framing also explains the tone of the project towards the incumbent. The documentation says Elixir is lucky, because the people who build the language also built Ecto and Postgrex, and both are excellent, and most languages do not get that. It says if Ecto fits the way you think, use it, since it is a great piece of work and is not going anywhere. The argument is not that Ecto is wrong but that the language maintainers should not have to build every tool, which is why an alternative take is allowed to exist.

## 5.0 swapped Postgrex for epgsql for stability and CVEs

The headline change in version 5.0 is the driver. Moebius used to run on Postgrex and now runs on epgsql, the Erlang PostgreSQL driver, with a pooler connection pool from the same group. The query builders and the run, first, find, save and transaction API did not change, so the upgrade is a dependency swap rather than a rewrite. The reasons given are practical. On stability, Postgrex has been a 0.x library for its whole life, so any minor release is allowed to break things, and a library depending on it has to pin something like ~> 0.19 and ask its users to live with that pin, while epgsql has been past 1.0 since 2015 and on 4.x since 2018, and pooler is at 1.7, with neither pulling in anything else. On security, Postgrex held decimal at 2.x, which kept a known vulnerability in every application that installed Moebius, and with 5.0 the mix hex.audit output is clean. The comparison table gives the numbers, with three open CVEs on the 4.2 side against none on 5.0, and the test count rising from 104 to 209.

## Behaviour changes in 5.0 that will reach your code

Since a major version was happening anyway, 5.0 also fixed old behaviour that could not be fixed without breaking something, and the documentation lists the ones you are most likely to hit. The transaction handle is now a %Moebius.Connection{} struct, though passing tx back to run/2 and save/3 behaves the same. Table, column and function names are checked, and a bad one raises ArgumentError. filter(col: nil) now means col IS NULL, where it used to build col = $1, which never matched anything. run/1 on a statement with no rows returns {:ok, []} rather than a bare list. An exception raised inside transaction/1 is re-raised after the rollback instead of being converted into an {:error, message} tuple, which changes error handling code that relied on the old shape. And the :types config for Postgrex extensions is gone, since types are handled by the library's own codecs. The full list with a reason for each is in the CHANGELOG file.

## PostgreSQL features arrive as function calls

The new in 5.0 list is where the library leans hardest on the database. copy/3 bulk loads any Enumerable with the binary COPY protocol, which is the fast path for importing data that would otherwise be inserted row by row. stream/2 reads a query through a server-side cursor a chunk at a time, so a large result set does not have to be held in memory at once. explain/2 returns the query plan as text, with an analyze mode that leaves nothing behind, which is a better way to answer the question of how Postgres will run a query than a hand written EXPLAIN. Nested transactions become savepoints, and rollback/1 aborts with a reason. Exact types are handled explicitly, with numeric returned as a Decimal in both directions, timestamps exact to the microsecond, and infinity supported. There is also a Moebius.Error struct carrying the SQLSTATE code, detail, hint, constraint, table and column, so a database error arrives with enough context to act on.

## The test rewrite found eleven bugs, two of them SQL injection

The most instructive part of the 5.0 work is how the bugs were found. The test suite was rewritten before any driver code changed, so that every test creates the rows it checks and asserts an exact value, and then the driver swap added tests for every type, failure and race. That process turned up eleven bugs, all fixed in 5.0, and two of them were SQL injection. One was find("1 or 1=1") returning a row, because the id was pasted into the SQL, and the other was a single quote inside a DocumentQuery.contains/2 value breaking out of the SQL string. The rest are the kind of bugs that only appear when the assertions are exact, including concurrent saves to a new document table losing writes, delete(id) piped into first() deleting nothing, filter(col: nil) never matching, a url silently overriding an explicit port, in: [] building invalid SQL, document tables in a schema getting bad index names, a failed document query retrying forever, and two doctests that were wrong and never ran. A library that pastes a value into SQL text is the failure mode this project was most exposed to, and it is the first thing to check in any code that grew around version 4.

## Safety checks and a pool that survives an outage

Two of the 5.0 behaviours are about what happens when things go wrong. Parameters are checked before they are sent, so a wrong type produces a clear error and never takes a connection down, which matters for a library that hands your strings to a database engine. And the pool survives an outage, so if Postgres goes away, calls return an {:error, message} until it is back, and nothing else in your supervision tree restarts. The second point is the more interesting one for an Elixir shop, because the usual behaviour of a supervision tree is to restart the child, and a restart loop against a database that is down is a noisy way to spend a database's connection budget. The project is published on Hex as moebius with API documentation on hexdocs, the licence is MIT, and the release history shows v5.0.0 on 2026-09-30 and v5.0.1 on 2026-10-01, after v4.0.2 in February 2024, with the last push on 2026-10-01 and the repository not archived. The repository also carries a .formatter.exs, a .sobelow-conf, a SECURITY.md, a mix.lock and a test directory, which is what you would expect from a library that now treats its own dependency tree as a security surface.

## Conclusion

Adopt Moebius if you want to write SQL-shaped queries in Elixir and get maps back without an ORM, and read the 5.0 changelog before upgrading because transaction handles, nil filters and error shapes changed. Do not adopt it if Ecto already fits how you think, since the project itself points you there first, and note the trade-off is that you give up schemas and migrations.

## FAQ

### What is Moebius and how does it differ from Ecto?

Moebius is a functional query library for Elixir and PostgreSQL that builds queries by piping functions and returns plain maps. It is not an ORM, with no schemas, no mappings and no migrations, and the project documentation says that if Ecto fits how you think, you should use Ecto.

### Why did Moebius 5.0 change its PostgreSQL driver?

Two reasons given in the documentation. Postgrex has been a 0.x library, so a dependency on it has to pin a version and pass that pin on to users, while epgsql has been past 1.0 since 2015. And Postgrex held decimal at 2.x, which carried a known vulnerability, so with 5.0 the mix hex.audit output is clean.

### What broke when upgrading from Moebius 4 to 5?

The transaction handle is now a %Moebius.Connection{} struct, names are checked and raise ArgumentError when bad, filter(col: nil) means IS NULL, run/1 on a statement with no rows returns {:ok, []}, an exception inside transaction/1 is re-raised after the rollback rather than returned as an error tuple, and the :types config is gone.

### How do I bulk load or stream large results with Moebius?

copy/3 bulk loads any Enumerable with the binary COPY protocol, and stream/2 reads a query through a server-side cursor a chunk at a time. explain/2 also returns the query plan as text, with an analyze mode that leaves nothing behind.

### What security bugs were fixed in Moebius 5.0?

The rewritten test suite and the driver swap turned up eleven bugs, two of them SQL injection, where find("1 or 1=1") returned a row because the id was pasted into the SQL, and a quote inside a DocumentQuery.contains/2 value broke out of the SQL string. All eleven were fixed in 5.0.

## Sources

- [Issues](https://github.com/robconery/moebius/issues)
- [License: MIT](https://github.com/robconery/moebius/blob/master/LICENSE)
- [README](https://github.com/robconery/moebius/blob/master/README.md)
- [Releases](https://github.com/robconery/moebius/releases)
- [robconery/moebius on GitHub](https://github.com/robconery/moebius)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/robconery-moebius
