The embedded database that returns
shaped results, not flat rows

From-scratch pure-Rust database with PowQL -- a pipeline query language that reads left to right -- plus a SQL frontend on the same engine. Ask for a parent and its children and get exactly that: one row per parent, children as a native nested value, no join fan-out to undo and no JSON text to re-parse. Built for single-writer embedded state and read-only snapshot serving, not for many clients sharing one read-write database.

$ cargo install powdb-cli

Latest release: v0.28.0

What PowQL does differently from SQL

Speed is not the interesting part of PowDB, and the benchmarks below are honest about where it loses. One of these four changes the answer you get. The other three change how much you have to write to get it.

Symmetric aggregates: a join cannot inflate an average

SQL
SELECT a.tier, AVG(a.balance)
FROM Account a JOIN Ord o ON a.id = o.account_id
GROUP BY a.tier;
-- 15.0. The join repeats each account once per
-- order, so the account with 4 orders is counted
-- four times. The true average is 20.0.
PowQL
Account as a join Ord as o on a.id = o.account_id
  group a.tier { a.tier, avg_bal: avg(a.balance) }
# 20.0. Each account contributes once.

# Opt back in to the joined-row number explicitly:
avg(raw a.balance)   # 15.0

Balances 10, 10 and 40 with 4, 1 and 1 orders. Both numbers are legitimate answers to different questions, and SQL gives you the inflated one unless you know to write a DISTINCT subquery. PowQL makes the safe one the default and the other one explicit. PowDB's own SQL frontend keeps SQL's semantics on purpose, so SQL results stay SQL-compatible.

Nested projections: one row per parent, children nested inside it

SQL
SELECT u.name, o.total, o.product_id
FROM User u
LEFT JOIN "Order" o ON o.user_id = u.id;
-- Alice repeats once per order.
-- Cara (no orders) comes back as a row of NULLs.
-- The client regroups by hand.
--
-- A correlated subquery with json_group_array
-- gets the same shape as PowQL, as text.
PowQL
User as u {
  u.name,
  orders: Order as o filter o.user_id = u.id
    { o.total, o.product_id }
}
# Alice, [{"product_id":101,"total":9.5},
#         {"product_id":102,"total":20.25}]
# Bob,   [{"product_id":null,"total":5.5}]
# Cara,  []

Every parent appears exactly once, so there is no fan-out to undo. A parent with no children gets an empty array, never NULL and never a dropped row. Children take their own order, limit, and offset, and nesting goes more than one level deep.

Entity links: declare the relationship once, then traverse it by name

SQL
-- Rewrite the join condition every time
SELECT o.total, u.name
FROM "Order" o
JOIN User u ON o.user_id = u.id;
PowQL
link Order.user -> User on user_id = id

Order as o { o.total, o.user.name }
User  as u { u.name, u.orders { .total } }

A scalar hop through a non-unique key is refused as a hard error rather than silently multiplying your rows. That is the failure mode a join gives you quietly and a link gives you loudly.

Native JSON: a real binary type, and paths you can index

PowQL
-- Filter, group, aggregate and order on a path
Post group .data->category {
  .data->category,
  total: sum(.data->amount)
}

# Index a scalar JSON path like any other column
alter Post add index (.data->published_at)

JSON is stored as PJ1, a binary document type, not as text that gets re-parsed on every read. Paths can be filtered, grouped, aggregated, ordered, and indexed.

What is spelling and what is not. The nested shape itself is not exclusive: stock SQLite produces the same output, empty array for a childless parent included, with a correlated subquery and json_group_array(json_object(...)), and DuckDB, SurrealDB and several ORMs have their own spellings. What differs here is that PowDB owns the storage engine, the executor and the wire protocol, so the nested value crosses the wire as PJ1 binary and the client decodes that rather than parsing JSON text. It is a binary decode instead of a text parse, not the absence of a decode, and we have not published a benchmark of the difference. The aggregate above is the one item on this page that changes the answer rather than the typing.

Full detail: Symmetric aggregates, nested projections, entity links, and the PowQL reference.

Where PowDB fits

PowDB is a single-writer embedded engine with truly parallel reads. Every writer takes the whole write-admission gate, and there is no MVCC. That shape decides where it shines.

Reach for PowDB when
  • Single-writer embedded app state in Rust or Node (in-process, typed results, injection-inert params)
  • Local agent or tool memory: local-first, single-process, refreshed by swapping in a new snapshot in seconds
  • Read-only edge snapshot serving from N processes, with no write gate at all
  • Per-tenant, process-isolated databases (one writer per tenant)
  • Fast, disposable CI and test databases
  • Bulk ingest feeding read-heavy internal tools and dashboards
Use something else when
  • Many concurrent clients share one read-write database -- that is Postgres's home turf; PowDB serializes writers through one gate, so shared read-write concurrency amplifies read latency
  • You need live replication or sync across nodes -- reach for Turso
  • Your workload is analytical column-crunching over one big dataset -- reach for DuckDB

Benchmarks

PowDB vs SQLite on 100K rows, neither engine fsyncs. All 15 workloads are listed, including the four where PowDB ties or loses. These are single-request latencies (one query at a time), not throughput under many simultaneous clients. Single-row durable inserts are fsync-bound; that write story is covered in Embedded in Node below.

$ cargo run --release -p powdb-compare

Median of 5 runs on an Apple M5 Max laptop (macOS 26.5.1, rustc 1.97.0), commit e3dfa71, measured 2026-08-15. Laptop numbers, not CI numbers. PowDB writes to a real temp directory while SQLite is :memory:, so the three write rows (insert_single, insert_batch_1k, delete_by_filter) are sensitive to competing disk I/O and change sign depending on machine load. Treat them as ties and re-measure them yourself. Full methodology, per-run spread, and what changed in the harness: the 2026-07-24 snapshot.

Workload PowDB SQLite Speedup
agg_min 221µs 1.70ms 7.7x
agg_max 217µs 1.47ms 6.8x
agg_sum 234µs 1.45ms 6.2x
update_by_pk 60ns 272ns 4.5x
agg_avg 455µs 1.70ms 3.7x
scan_filter_count 380µs 1.40ms 3.7x
point_lookup_nonindexed 101µs 319µs 3.2x
scan_filter_sort_limit 2.46ms 6.41ms 2.6x
multi_col_and_filter 1.58ms 3.21ms 2.0x
update_by_filter 2.36ms 4.54ms 1.9x
insert_single 380ns 638ns tied
scan_filter_project_top100 8.1µs 8.9µs tied
delete_by_filter 1.57ms 1.75ms tied
insert_batch_1k 242ns 214ns tied
point_lookup_indexed 3.17µs 202ns 15.7x slower

PowQL vs SQL

PowQL reads left to right like a pipeline. No SELECT ... FROM ... WHERE juggling.

Filter and project

SQL
SELECT name, price
FROM Product
WHERE price > 10
ORDER BY name;
PowQL
Product filter .price > 10
order .name
{ .name, .price }

Aggregate with filter

SQL
SELECT AVG(age)
FROM User
WHERE city = 'NYC';
PowQL
avg(User filter .city = "NYC"
{ .age })

Group by with having

SQL
SELECT status, COUNT(*)
FROM User
GROUP BY status
HAVING COUNT(*) > 5;
PowQL
User group .status
having count(*) > 5
{ .status, count(*) }

Insert a row

SQL
INSERT INTO User (name, email, age)
VALUES ('Alice', 'alice@example.com', 30);
PowQL
insert User {
  name := "Alice",
  email := "alice@example.com",
  age := 30
}

Embedded in Node — no server

The same engine, in-process. @zvndev/powdb-embedded is a native Node addon: open a database, run PowQL or SQL, no TCP, no daemon. Durability is a mode you pick, so batched writes can be made fast without giving up crash safety.

$ npm install @zvndev/powdb-embedded
index.mjs
import { Database } from "@zvndev/powdb-embedded";

const db = Database.open("./data");

// "normal" = off-lock background fsync: much faster writes,
// a bounded crash-loss window. This closes the write gap vs SQLite.
db.setSyncMode("normal");

db.query('type User { required name: str, required email: str, age: int }');
db.query('insert User { name := "Alice", email := "alice@example.com", age := 30 }');

// PowQL
db.query('User filter .age > 25 { .name, .age }');

// ...or SQL, on the same handle — lowered to PowQL by the SQL frontend
db.querySql("SELECT name, age FROM User WHERE age > 25 ORDER BY age DESC");

Durability is a knob, not a fork: "full" fsyncs every commit (safest, the default), "normal" moves the fsync off the write lock for a bounded crash-loss window, and batching writes in a begin/commit transaction drops the cost from one fsync per row to roughly one per 64. That is how single-row insert throughput stops being fsync-bound.

Built for Performance

Every layer of PowDB is written in Rust, from the storage engine to the query executor.

Compiled Predicates

Filter expressions compile into byte-level operations that skip full row decoding. This is why the aggregates measure 3.7-7.7x SQLite and filtered scans 1-3.7x, while point lookups measure about 15x slower.

Nested Projections

PowQL, not PowDB's SQL subset. One row per parent with correlated children assembled into a native JSON array, with per-parent order and limit and multi-level nesting. No join fan-out to undo.

Entity Links

PowQL, not PowDB's SQL subset. Declare a relationship once with link Order.user -> User on user_id = id, then traverse it by name. A scalar hop through a non-unique key is a hard error, never a silent fan-out.

Native JSON (PJ1)

JSON is a first-class binary document type, not text. Filter, group, aggregate, and order on -> paths, and build B+tree indexes over scalar JSON paths.

EXPLAIN

Inspect the plan PowDB chose, including link paths and index selection, so a slow query is diagnosable instead of mysterious.

Prometheus Metrics

Opt-in /metrics endpoint (POWDB_METRICS_ADDR) exposing connection, query, and auth-failure counts. Unauthenticated by design, so keep it on a private network.

B+ Tree Indexes

Disk-persisted B+ tree indexes in a custom BIDX binary format, including scalar JSON-path indexes. Indexes survive restarts and are chosen automatically using collected statistics.

WAL + Crash Recovery

Write-ahead log with statement-boundary group commit. Full crash recovery via WAL replay, page-zero recovery, and automatic index rebuild.

Plan Cache

FNV-1a hashed plan cache with literal substitution. Parse and plan once, execute thousands of times. Prepared queries skip the entire front-end pipeline.

SQL Frontend

A SQL parser lowers a supported subset — SELECT/JOIN/GROUP BY, INSERT ... RETURNING, AUTOINCREMENT, DDL, transactions — to the same PowQL AST and plan cache. One engine, two languages, no second execution path.

Embedded in Node

Native in-process addon (@zvndev/powdb-embedded): Database.open, then PowQL or SQL with no server. setSyncMode("normal") trades per-commit fsync for OS-level durability, which is what makes writes fast.

TypeScript Client

First-class TypeScript client (@zvndev/powdb-client) with a clean async API. Connect to PowDB server over TCP with full type safety.

TLS + Authentication

TLS encryption plus two auth modes: a shared password (POWDB_PASSWORD) or named users with roles (admin / readwrite / readonly, argon2id-hashed).

Pure-Rust Engine

The engine libraries are pure Rust: no C FFI, no libsqlite3-sys, no bindgen, and nothing to install beside a built binary. Building powdb-server or powdb-cli from source does need a C toolchain and cmake, because their TLS stack compiles aws-lc-sys. Windows is not supported: the mmap scan path is Unix-only.

Pipeline Query Language

PowQL reads left to right: table, filter, order, limit, project. No inside-out clause structure. Queries read like sentences.

When PowDB is not the right fit

PowDB is pre-1.0 and deliberately scoped. Be honest with yourself before adopting it:

If any of those are dealbreakers, use SQLite -- and we say so plainly in PowDB vs SQLite: when to use which.