NEW  New: cqlint (lint + format), cql-migrate and cqlprof → see the benchmarks

cqlx

A typesafe, zero-overhead, code-first ORM for ScyllaDB and Cassandra, built on procedural macros. The code-first counterpart to cqlc.

A struct is the schema

use cqlx::scylla::DeserializeRow;
use cqlx::{CqlEntity, CqlModel, Session};

#[derive(CqlEntity, DeserializeRow, Debug)]
#[cqlx(table_name = "events", keyspace = "app")]
struct Event {
    #[cqlx(partition_key)] tenant: String,
    #[cqlx(partition_key)] day: i32,
    #[cqlx(clustering_key(order = "desc"))] ts: i64,
    #[cqlx(rename = "payload")] body: String,
}

let session = Session::connect("127.0.0.1:9042").await?;
session.create_table::<Event>().await?;
session.insert(&event).await?;

let rows = Event::query()
    .tenant("acme".into())   // EventQuery<Missing, Missing>  → <Provided, Missing>
    .day(7)                  //                              → <Provided, Provided>
    .ts_gt(0)
    .limit(10)
    .fetch(&session)         // only exists once every partition key is Provided
    .await?;

The compiler enforces the partition key

The macro generates a builder whose type parameters track which partition key columns are set. fetch, build and delete exist only when all of them are Provided.

let _ = Event::query().tenant("acme".into()).build();
error[E0599]: no method named `build` found for struct `EventQuery<Provided, Missing>`
  = note: the method was found for `EventQuery<Provided, Provided>`

This is a real compiler message from the project's trybuild test suite. Clustering restrictions (_eq, _gt, _ge, _lt, _le) are validated when the statement is built: a prefix of the clustering columns with =, then at most one range.

1.0

Production feature set

AreaAPI
Readstype-state query(): partition keys (=/_in), clustering _eq/_gt/_ge/_lt/_le/_in, limit, reverse, fetch, first, count, stream, paging, per-query consistency
Partial updatesupdate() needs the full primary key and one assignment: set_*, add_*/remove_* for collections, increment_* for counters, ttl, timestamp
Lightweight transactionsinsert_with(.. if_not_exists), update().if_exists(), if_<col>_eq(..) return whether the write was applied
Batches and streamingbatch(Logged|Unlogged|Counter), stream() over millions of rows
Schemavalidate_schema reports drift, migrate creates tables and adds columns, never drops or retypes
Safetyno fetch without the partition key, no update without key and assignment, no insert into counter tables, non-key filters only after allow_filtering(): all compile errors

The integration suite (inserts, partial updates, collections, counters, TTL, LWTs, batches, 1,000-row streaming, schema migration) runs against ScyllaDB, Apache Cassandra 5.0 and 4.1. Statements are rendered at compile time as &'static str constants; at runtime a query only assembles restrictions and bind values. YCSB numbers against the raw driver and charybdis.

Schema tooling

cargo install cqlx-cli
cqlx schema src/ --keyspaces            # CREATE KEYSPACE/TABLE for every #[derive(CqlEntity)]
cqlx diff src/ --schema schema.cql      # ALTER TABLE … ADD, plus warnings for what CQL cannot alter

The existing schema is read with cqlc-core's CQL parser, so the same .cql files work with cqlc.