A SQL engine that cannot fire a trigger, attach a second database, or cache a query result is not incomplete by design — it is incomplete by TODO list. OxiSQL 0.4.1 closes three of the biggest ones at once, and ships a brand-new crate along the way.
Today — August 7, 2026 — we released OxiSQL 0.4.1, the sovereign Pure-Rust SQL layer for the COOLJAPAN ecosystem. This is the largest feature release since 0.3.3: CREATE TRIGGER and full row-trigger execution, TEMP objects and ATTACH/DETACH DATABASE multi-database support, PRAGMA index_list/index_info index introspection, a new oxisql-cache crate, fuzz targets, runnable quickstart examples for every facade/driver crate, and six process-abort/undefined-behavior fixes in the C-free oxisqlite storage engine.
No C. No Fortran. No libpq, no libmysqlclient, no libsqlite3 — the workspace still compiles cleanly with the C compiler disabled (CC=/usr/bin/false cargo build --workspace → exit 0), across 18 crates now instead of 17, backed by 2,261 tests passing with default features (2,755 with --all-features), 0 failing, 0 clippy warnings.
Why 0.4.1 is a game changer
Before this release, three gaps forced real applications to reach outside OxiSQL entirely:
- No triggers.
CREATE TRIGGERhitbail_parse_error!unconditionally. Audit logging, denormalized counters, cascading business rules — anything that wanted the database itself to react to a write had to be reimplemented in application code, with all the race conditions that implies. - No multi-database support.
ATTACH DATABASEandCREATE TEMP TABLEwere also hard rejections. A connection could only ever see one database file — no scratch tables, no cross-database joins, no read-only reference data attached alongside a writable primary. - No caching layer that belonged to OxiSQL. SQL-result caching lived in
oxistore-cache’ssqlfeature, which depended back onoxisql-core— a cross-repo cycle that made the caching layer awkward to evolve independently. - Six live process-abort sites.
PRAGMA page_size = Non certain databases,PRAGMA auto_vacuum = 2, three release-active B-treeassert!()s, and a JSONBstr::from_utf8_uncheckedon attacker-controlled bytes could all take down the host process — not return an error, abort it.
OxiSQL 0.4.1 ends all of that:
- All six row-trigger kinds fire —
BEFORE/AFTER×INSERT/UPDATE/DELETE— withWHENguards,UPDATE OF (cols)column filtering,OLD.*/NEW.*(includingrowid), andRAISE(ABORT|FAIL|ROLLBACK|IGNORE)control flow, persisted as realsqlite_schemaobjects that survive a close/reopen. - A full per-connection database registry — modelled on upstream SQLite’s
sqlite3.aDb[]— backsTEMPtables/views/triggers and realATTACH/DETACH DATABASE, closing all six formertodo!("temp databases not implemented yet")sites that would have aborted the process the moment they were reached. oxisql-cacheis a first publish, and it inverts the dependency: it depends onoxistore-cache, not the other way around, matching the direction OxiSQL already uses foroxistore-columnar.- Every one of the six abort sites is now a typed error.
PRAGMA page_size/auto_vacuum = 2validate and defer instead of hittingtodo!()/unimplemented!(); three release-active B-treeassert!()s becameLimboError::Corrupt; the JSONB path-navigation helper uses checkedstr::from_utf8. PRAGMA index_list/index_infonow surface real index metadata from the schema, sooxisql-sqlite-compat’sConnection::indexes()stopped string-parsingCREATE INDEXSQL text (which mishandledDESC,COLLATE, and quoted identifiers).
Technical Deep Dive: how triggers and ATTACH actually work
- Triggers are inlined, not framed. This VDBE has no
OP_Program/frame stack, so trigger bodies are inlined into the firing program via the existingincr_nesting()mechanism.OLD.*/NEW.*are rewritten into a new code-generator-onlyast::Expr::Register(usize)node before translation, so every existing planner/optimizer/emitter path handles a trigger body with zero trigger-specific plumbing. A trigger cannot re-enter itself (recursive_triggers = offby default), while non-recursive nesting — A fires B fires C — works. - Every database slot owns its own pager, catalog, and transaction state. Index 0 is
main, index 1 istemp(created lazily, backed by a private in-memory pager), and indices 2+ areATTACHed databases — each a nestedDatabasethat reuses the tested top-level open path for WAL setup, header bootstrap, and schema parsing. - Name resolution follows upstream exactly. A schema qualifier (
main.t,temp.t,alias.t) addresses that database directly; an unqualified name searchestemp→main→ attached, exceptsqlite_schema/sqlite_master, which stay pinned tomainthe way upstream keeps them. oxisql-cachewrapsoxistore-cache’s Pure-Rust LRU primitive.SqlQueryCachecachesRowSetresults keyed by normalized SQL text;SqlPlanCache<P>caches an opaque, caller-supplied prepared-statement representation;CachedQueryRunnerturns anyFnMut(&str) -> Result<RowSet, E>executor into a caching one with hit/miss counters — all#![forbid(unsafe_code)].
Getting Started
cargo add oxisql --features sqlite,pool-sqlite-compat
Or pin the exact release in Cargo.toml:
[dependencies]
oxisql = { version = "0.4.1", features = ["embedded", "postgres", "pool-embedded", "migrate"] }
The C-free SQLite path, unchanged in shape, still runs on the same oxisqlite engine:
use oxisql_core::Connection;
use oxisql_sqlite_compat::SqliteConnection;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let conn = SqliteConnection::open_memory().await?;
conn.execute(
"CREATE TABLE pilots (id INTEGER PRIMARY KEY, name TEXT NOT NULL, callsign TEXT)",
&[],
)
.await?;
conn.execute(
"INSERT INTO pilots (name, callsign) VALUES ($1, $2)",
&[&"Maverick", &"Pete Mitchell"],
)
.await?;
let rows = conn
.query("SELECT id, name, callsign FROM pilots ORDER BY id", &[])
.await?;
println!("pilots: {}", rows.len());
Ok(())
}
New in 0.4.1 — a trigger that keeps an audit log in sync with every insert, entirely inside the database:
CREATE TABLE t (a INTEGER);
CREATE TABLE log (val INTEGER);
CREATE TRIGGER t_ai AFTER INSERT ON t
BEGIN
INSERT INTO log VALUES (NEW.a);
END;
INSERT INTO t VALUES (42); -- log now contains 42, no application code involved
And oxisql-cache’s read-through adapter, wrapping any query executor with an LRU cache:
use oxisql_core::{Row, RowSet, Value};
use oxisql_cache::CachedQueryRunner;
let mut runner = CachedQueryRunner::new(32, |sql: &str| -> Result<RowSet, String> {
Ok(RowSet::from_rows(vec![Row::new(vec!["n".into()], vec![Value::I64(1)])]))
});
let r1 = runner.run("SELECT 1").unwrap();
let r2 = runner.run("SELECT 1").unwrap(); // served from cache
assert_eq!(runner.hits(), 1);
assert_eq!(runner.misses(), 1);
What’s New in 0.4.1
CREATE TRIGGER/DROP TRIGGERand full row-trigger execution — all six kinds,WHENguards,UPDATE OF (cols),OLD.*/NEW.*,RAISE()— persisted as real schema objects (26 new tests inoxisqlite-core/tests/triggers.rs).TEMPobjects andATTACH/DETACH DATABASEvia a new per-connection database registry, closing all sixtodo!("temp databases not implemented yet")process-abort sites (26 new tests inoxisqlite-core/tests/multi_database.rs).- New crate
oxisql-cache—SqlQueryCache,SqlPlanCache<P>,CachedQueryRunner,QueryCacheStats, available through theoxisqlfacade’s newcachefeature. First publish: 23 unit tests + 3 doc tests,#![forbid(unsafe_code)]. PRAGMA index_list/PRAGMA index_info— real index metadata from the in-memory schema, replacingoxisql-sqlite-compat’s previous best-effortCREATE INDEXSQL text parsing.- Fuzz targets (
fuzz/, a detached workspace) for the on-disk parser and the SQL lexer/parser, and runnable quickstart examples for every facade/driver crate:oxisql-sqlite-compat,oxisql-embedded,oxisql-postgres,oxisql-mysql, and the unifiedoxisqlfacade. - Six process-abort and undefined-behavior fixes:
PRAGMA page_size/auto_vacuum = 2no longertodo!()/unimplemented!(); three release-active B-tree balancingassert!()s converted to typedCorrupterrors; a JSONB malformed-blobstr::from_utf8_uncheckedand unchecked size-field reads fixed; four unmessagedTableroot-page/drop panics now typed errors (a breaking change tooxisqlite-core’sTable::get_root_page()signature, not reachable through theoxisqlfacade). - Routine dependency bumps:
oxiarc-zstd→0.4.1,oxitls→0.3.0,oxistore-cache→0.3.0.
Tips
- Reach for
oxisql::cachebefore rolling your own LRU. Enable thecachefeature and wrap your query path inCachedQueryRunner— it tracks hit/miss counts for you viaQueryCacheStats, and bothSqlQueryCache/SqlPlanCache<P>normalize SQL text so whitespace/case differences still hit the same entry. RAISE(IGNORE)vsRAISE(ABORT|FAIL|ROLLBACK, 'msg')inside a trigger body have different blast radii.IGNOREabandons only the offending row and lets the statement continue; the other three abort the whole statement withSQLITE_CONSTRAINT_TRIGGERand your message — pickIGNOREfor soft validation, the others for hard invariants.ATTACH/DETACHmust happen outside an open transaction. OxiSQL returns a typed error rather than silently queuing the attach — commit or roll back first, then attach.- Schema-qualify when you mean it. An unqualified name resolves
temp→main→ attached, so aTEMPtable can shadow amaintable of the same name — usemain.texplicitly if that’s not what you want. Table::get_root_page()changed signature if you calloxisqlite-coredirectly. It went fromfn(&self) -> usizetofn(&self) -> Result<usize>. This is not reachable through theoxisqlfacade’s public API — only directoxisqlite-coreconsumers need to update.- Fuzzing lives in its own workspace now.
fuzz/is detached from the main workspace specifically solibfuzzer-sys’s nightly-only requirement never touches your normal stable-toolchain build —cargo build --workspaceat the root is unaffected.
This is the foundation
OxiSQL’s facade/driver layer is now 11 crates — up from 10, with the addition of oxisql-cache — sitting on the 7-crate, C-free oxisqlite-* engine (18 workspace crates total, plus two non-published patch-shim crates). OxiSQL sits on OxiTLS (transport/TLS), oxicode (row serde), OxiCrypto (encryption-at-rest), and OxiStore (lower storage layer, now consumed the right direction via oxisql-cache → oxistore-cache). It is depended on by oxirs, oxify, oxigeo, oximedia, celers, legalis, oxicar, oxi3d, oxiaero, scirs, oxiproj, tensorlogic, and trustformers — a growing list of COOLJAPAN services that need SQL without a C dependency in sight.
Repository: https://github.com/cool-japan/oxisql
Star the repo if you want triggers, multi-database ATTACH, and query caching that all ship Pure Rust, with zero libsqlite3 anywhere in the graph.
The era of reimplementing trigger logic in application code because the database wouldn’t do it for you is over. Pure Rust SQL — sovereign, safe, and now considerably more capable — is here.
— KitaSan at COOLJAPAN OÜ August 7, 2026