Repository navigation
5.0.0
Moebius now runs on epgsql 4.8 with a
pooler 1.7 connection pool, instead of Postgrex. Both are
past 1.0 and neither has dependencies of its own. The runtime dependency tree went from
postgrex, db_connection, telemetry and decimal 2 to epgsql, pooler, jason and decimal 3.
That also closes CVE-2026-32687, CVE-2026-58225 (both in Postgrex) and CVE-2026-32686 (in
decimal 2).
The query builders and the run/first/find/save/transaction API are unchanged. The
breaking changes are listed below, each with its reason.
Breaking
- The transaction handle is a
%Moebius.Connection{}, not a%DBConnection{}. Code that
only passestxback torun/2andsave/3doesn't change. - The
:typesconfig (Postgrex extensions) is gone. Types are handled by Moebius's own
epgsql codecs; see the README for what each Postgres type becomes. - Names are checked. A table, column, function or SQL file name that isn't a plain name
(users,membership.users,"Order Items") raisesArgumentError. So do a sort
direction other than:asc/:desc, an unknown join type or document operator, and a
non-integerlimit/offset. SQL fragments you write yourself are still used as written. filter(col: nil)meanscol IS NULL(#35). Before, it builtcol = $1, which never
matches anything.run/1on a statement without rows returns{:ok, []}, not a bare[].run_batch/1andtransact_batch/1return{:ok, %Moebius.Result{}}per command,
instead of Postgrex result structs.- An exception raised inside
transaction/1is re-raised after the rollback. Before,
exceptions with a:messagefield were turned into{:error, message}, which hid bugs.
Database errors,rollback/1andthrow({:error, reason})still return{:error, _}. - Explicit connection options win over the parts of
:url. Before,urlsilently
overrodeport:and friends. - New document tables use
id bigint generated by default as identityand
updated_at timestamptz not null default now(). Existing tables are untouched. The
search column is built frombody ->> 'field'(plain text, without JSON quotes). - Full-text search takes what people type.
search/2(both builders) uses
websearch_to_tsquery, so"red shoes","O'Brien"and"apple -pie"work. Before,
to_tsqueryraised a syntax error on anything but a single word or hand-written tsquery. count/1ignoressort,limitandoffset. Before,sort |> countbuilt invalid
SQL (select count(1) ... order by).- Empty builder input raises.
insert([]),update([])andbulk_insert([])raise
ArgumentErrorinstead of buildingvalues($1, $0);filter([])leaves the command alone
instead of emittingwhere ;. - Numeric strings longer than 34 digits are refused as parameters. This is decimal 3's
protection against CVE-2026-32686. Pass aDecimalbuilt withmax_digits: :infinityif
you really mean it. Values read from the database keep full precision. - Deprecated:
Moebius.run_with_psql/2(useMoebius.run_script/2, which doesn't needpsql
installed) andMoebius.pool_opts/0(pool options go in the connection options).
Fixed
bulk_insert/2read values by position. A row whose keys were in a different order from
the first row was inserted with its values swapped, silently. Rows are now read by key, maps
are accepted, and a row missing a column raises.- A malformed uuid parameter crashed the connection process. It is now
{:error, "parameter $1 must be uuid, ..."}like every other type mismatch. - A password containing
:was cut at the first colon byparse_connection/1. Bad URLs
raiseArgumentError(wasRuntimeError). - Document field names may not contain a backslash. With
standard_conforming_strings
off (a legacy server setting) a backslash could end the quoted key early. stream/2checks:chunkis a positive integer before it goes intoFETCH.- A pool that is busy no longer reports "isn't started"; only a missing pool process does.
Added
copy/3: bulk load any Enumerable (a lazyStreamincluded) with Postgres's binary
COPYprotocol. 100,000 rows load in about 130ms, 4.6x faster than 4.x'sbulk_inserton
the same machine. All or nothing; bad values are reported with their row and column.stream/2: read any query or document query through a server-side cursor, a chunk at a
time. Lazy, and halting early gives the connection back.explain/2: the query plan as text.analyze: trueruns the query inside a rolled-back
transaction, so explaining an insert leaves nothing behind.- Nested transactions become savepoints, so an inner one can fail on its own.
rollback/1aborts the current transaction with a reason.- Queries in the same process join an open transaction even without the
txargument. pool_status/0: pool size and usage.- The pool opens all
pool_sizeconnections at start (as Postgrex did); setpool_minlower
to let an idle pool shrink. - Connection options:
pool_min,checkout_timeout,queue_max,max_lifetime,
statement_timeout,lock_timeout,idle_in_transaction_session_timeout,settings,
application_name,socket_dir,ssl/ssl_opts. Moebius.Error, with the SQLSTATE code and name, detail, hint, constraint, table and column.- Parameters are checked against the types Postgres expects before they're sent, so a wrong
type is a clear error (parameter $1 must be int4, got: "five") and never takes a
connection down. numericdecodes to an exactDecimaland encodes fromDecimal, integers, floats or
numeric strings. Timestamps are exact to the microsecond (they're read as integers, not
float seconds).infinitydates and timestamps are supported.tsvectorcolumns come back as text instead of raising.mix moebius.create/drop/migrate/seedno longer needpsqlinstalled.
Performance
Measured with the same script against 4.2 (Postgrex 0.19) on the same machine and database,
both pools holding 10 open connections:
- Single queries: median latency 5-11% lower on every operation measured (find by id 73µs
vs 80µs), and p99 latency 10-42% lower (insert returning 118µs vs 205µs). - 50 processes sharing the pool: about 12% slower (187ms vs 167ms for 10,000 finds). epgsql
encodes and decodes in the connection process, where DBConnection does it in each caller. bulk_insert+transact_batchof 100k rows: about 24% slower (739ms vs 595ms). Use
copy/3instead, at 130ms.
Fixed
find/2put the id into the SQL (find("1 or 1=1")returned a row). It's a parameter now.DocumentQuery.contains/2put the JSON into the SQL as a literal, so a'in a value broke
the query and could be used for injection. It's a parameter now.DocumentQueryfield names infilter/4,exists/3,sort/3andsearch/2are quoted,
so any key is safe.db(:docs) |> delete(id) |> first()ran a select and deleted nothing.- Saving to a new document table from several processes at once lost writes to a race in
create table if not exists. Creation now takes an advisory lock. - Document tables with a schema (
public.docs) got invalid index names. - A document query that failed for any reason other than a missing table retried forever.
filter(:col, in: [])builtIN(), a syntax error. It now matches nothing (and
not_in: []matches everything).- The
DBConnection.TransactionErrorlog noise after a failed transaction is gone. - The
Moebius.QueryFilterdoctests were wrong and weren't run. They run now.