The last C library left in OxiSQL’s dependency closure wasn’t a database driver at all — it was a compression codec, buried several dependency hops downstream inside DataFusion’s Arrow IPC layer.
Today we released OxiSQL 0.3.2 — a release that closes the final C-FFI gap in the --all-features dependency closure with a new Pure-Rust zstd-shim, and fixes a GROUP BY … HAVING COUNT(*) accumulator-reset bug in the oxisqlite-core query engine.
No C. No Fortran. No libpq, no libmysqlclient, no libsqlite3, and — as of 0.3.2 — no zstd-sys anywhere in the workspace’s dependency graph, --all-features included. OxiSQL’s SQLite path already ran on oxisqlite, an in-tree, from-scratch C-free fork of limbo; 0.3.2 plugs the one remaining transitive leak, a C-FFI compression codec that arrow-ipc pulled in for DataFusion. It compiles to a single static binary and runs everywhere Rust runs.
Why OxiSQL 0.3.2 is a game changer
Building OxiSQL with the datafusion feature — or just --all-features — meant living with a quiet compromise:
- Enabling
datafusionpulled inarrow-ipc, which useszstd’s bulk compression API for IPC-stream buffers — meaning the real, upstreamzstdcrate, which meanszstd-sys: a C-FFI crate sitting downstream of a facade whose entire pitch is “no C.” GROUP BY … HAVING COUNT(*) …— orCOUNT(*)/COUNT()reached viaORDER BYor any nested expression instead of a plain result column — was planned with zero arguments by theoxisqlite-corequery planner, undercounting its sorter-column span.- That undercount left an aggregate’s accumulator register unreset across
GROUP BYgroup boundaries on multi-group queries — which could read the sorter out of bounds, or panic on a stale accumulator. - Internal invariant violations inside the VDBE’s aggregate-step handler surfaced as a bare
panic!()instead of a typed, catchable error.
OxiSQL 0.3.2 ends all of that:
zstd-shim— a local, unpublished Pure-Rust patch crate (crates/zstd-shim/), wired in via[patch.crates-io](zstd = { path = "crates/zstd-shim" }). It implements only the surfacearrow-ipcactually calls —bulk::{Compressor, Decompressor}andDEFAULT_COMPRESSION_LEVEL— backed byoxiarc-zstd.zstd-sysno longer appears anywhere in the--all-featuresdependency graph.COUNT(*)now carries the same synthetic literal-1argument the result-column path already used, so its sorter-column span is counted correctly no matter where it appears —HAVING,ORDER BY, or nested inside another expression.- The accumulator-clear range in
group_by_emit_row_phasenow covers the full aggregate block, not just the (potentially undercounted) sorter-column span — every group starts every accumulator clean. op_agg_stepinitialization is self-healing: it now re-initializes whenever a register doesn’t already hold anAggContext, rather than only onValue::Null;Max/Minno longer type-match against the first column value up front.- Internal invariant violations now return
LimboError::InternalErrorinstead of callingpanic!(). - Routine dependency bumps:
oxiarc-zstdto0.3.5(backing the new shim),timeto0.3.53,uuidto1.23.4(also bumped inoxisqlite-uuid), and, inoxisqlite-core,env_loggerto0.11.11and the Linux-onlyio-uringto0.7.13.
This is not the first C dependency OxiSQL has cut, and the mechanism is the same each time: a small, local, unpublished patch crate wired in via [patch.crates-io]. 0.2.0 replaced the entire SQLite backend this way — the oxisqlite fork of limbo, dropping libsqlite3, the mimalloc C allocator, and the lemon C parser generator in one move. 0.3.0 dropped objc2-system-configuration from the macOS build via whoami-patched. 0.3.2 is the third pass of the same knife, aimed at zstd-sys.
Technical Deep Dive: how a compression codec and a query planner both got fixed in one release
crates/zstd-shim/masquerades as the real thing, on purpose. ItsCargo.tomlnames the packagezstd, versions it0.13.3to satisfyarrow-ipc’s semver requirement, and depends onoxiarc-zstd = "0.3.5". Two files —src/lib.rsandsrc/bulk.rs— implement exactly thebulk::{Compressor, Decompressor}API andDEFAULT_COMPRESSION_LEVELconstant thatarrow-ipccalls, nothing more. The workspace rootCargo.tomlredirects every consumer to it with one line under[patch.crates-io], alongside the existingrustls-rustcrypto-patchedandwhoami-patchedentries.- The query planner (
oxisqlite-core/translate/group_by.rs) computes the aggregate-block span for everyGROUP BY. Before 0.3.2, aCOUNT(*)reached throughHAVING,ORDER BY, or a nested expression carried zero arguments into that span calculation instead of the synthetic literal-1argument the plain result-column path already attached — undercounting how many sorter columns the aggregate block actually needed. - The VDBE executor (
oxisqlite-core/vdbe/execute/aggregate.rs) runs the per-row accumulator step,op_agg_step. Itsgroup_by_emit_row_phaseclear range now spans the full aggregate block computed in step 2, rather than trusting the previously short sorter-column count — so a stale accumulator from the prior group can’t leak into the next one. - Regression coverage lives in
oxisqlite-core/tests/group_by_having.rs, built around exactly the multi-groupGROUP BY … HAVING COUNT(*) …shapes that used to trigger out-of-bounds sorter reads and stale-accumulator panics.
Getting Started
cargo add oxisql --features sqlite,pool-sqlite-compat
The C-free SQLite path — oxisqlite, the same engine this release’s GROUP BY fix lives in — supports real transactional ROLLBACK:
use oxisql::SqliteConnection;
use oxisql::prelude::*;
#[tokio::main]
async fn main() -> Result<(), oxisql::OxiSqlError> {
let conn = SqliteConnection::open_memory().await?;
conn.execute("CREATE TABLE t (id INTEGER, name TEXT)", &[]).await?;
// BEGIN ... ROLLBACK discards the INSERT.
conn.execute("BEGIN", &[]).await?;
conn.execute("INSERT INTO t VALUES (1, 'Alice')", &[]).await?;
conn.execute("ROLLBACK", &[]).await?;
let rows = conn.query("SELECT COUNT(*) FROM t", &[]).await?;
println!("rows after ROLLBACK: {:?}", rows); // → one row holding COUNT(*) = 0
Ok(())
}
Reach the same engine through the facade with oxisql::connect("sqlite::memory:") or oxisql::connect("sqlite://path/to/file.db"). For the DataFusion OLAP path that made zstd-shim necessary, add the datafusion feature and call oxisql::connect_datafusion("datafusion://") to obtain an OxiSqlContext.
What’s New in 0.3.2
- A new Pure-Rust
zstd-shimcrate removes the C-FFIzstd-syscrate from the--all-featuresdependency closure, patched in for the upstreamzstdcrate whereverarrow-ipc/DataFusion needs bulk compression. - Fixed a
GROUP BY … HAVING COUNT(*)accumulator-reset bug in theoxisqlite-corequery planner and VDBE — multi-group queries withCOUNT(*)reached viaHAVING,ORDER BY, or a nested expression could read the sorter out of bounds or panic on a stale accumulator; both are fixed, with new regression tests intests/group_by_having.rs. - Hardened aggregate-step initialization (
op_agg_step) to self-heal against stale register state, and replaced an internalpanic!()with a typedLimboError::InternalError. - Dependency bumps:
oxiarc-zstd0.3.5,time0.3.53,uuid1.23.4,env_logger0.11.11,io-uring0.7.13.
Tips
- Confirm the C-FFI crate is actually gone from your build.
cargo tree -p oxisql --all-features -i zstd-sysnow reportsdid not match any packages— it isn’t in the graph at all.cargo tree -p oxisql --all-featuresshowszstd v0.13.3resolving straight tocrates/zstd-shim, backed byoxiarc-zstd v0.3.5. zstd-shimis not a general-purposezstdreplacement. It implements onlybulk::{Compressor, Decompressor}andDEFAULT_COMPRESSION_LEVEL— the exact surfacearrow-ipccalls. If you need Pure-Rust compression in your own code, reach foroxiarc-zstddirectly instead of depending on this shim.- Re-run any
GROUP BY … HAVING COUNT(*) …query against the C-free SQLite path if you ever saw an incorrect count or a panic on a multi-group aggregate before 0.3.2 —tests/group_by_having.rshas the exact shapes that now pass. COUNT(*)insideORDER BYor a nested expression is now planned identically toCOUNT(*)as a result column — no query rewrite needed on your end; the fix is entirely internal to the planner and VDBE.- If you were catching panics out of
oxisqlite-core’s aggregate path, switch to matchingLimboError::InternalErrorinstead — internal invariant violations no longer abort the process. - Bump
time,uuid,env_logger, andio-uringalongside OxiSQL if your own crate pins them transitively — all four moved this release.
This is the foundation
OxiSQL’s facade/driver layer is 10 crates (oxisql, oxisql-core, oxisql-parse, oxisql-embedded, oxisql-postgres, oxisql-mysql, oxisql-datafusion, oxisql-pool, oxisql-migrate, oxisql-sqlite-compat) sitting on the 7-crate, C-free oxisqlite-* engine, alongside a perf benchmark crate and three vendored [patch.crates-io] crates: rustls-rustcrypto-patched, whoami-patched, and — new this release — zstd-shim.
OxiSQL itself sits on OxiTLS (transport/TLS), oxicode (row serde), OxiCrypto (encryption-at-rest), OxiStore (lower storage layer), and, as of 0.3.2, OxiARC’s oxiarc-zstd 0.3.5 (backing zstd-shim, replacing zstd-sys on the DataFusion IPC path). It is depended on by oxirouter, oxirs, oxify, oxigdal-db-connectors, oximedia, oxigaf, and oxirag — the database tier each of those reaches for when it needs SQL.
Repository: https://github.com/cool-japan/oxisql
Star the repo if you want a database layer where turning on an OLAP feature doesn’t quietly hand you a C compression library, and a GROUP BY that counts the same way on group 1 and group 100.
The era of “Pure Rust, except for whatever your transitive dependencies pulled in” is over. Pure Rust SQL — sovereign, safe, and FFI-free — is here.
— KitaSan at COOLJAPAN OÜ July 11, 2026