A Tabularis driver plugin for Apache Cassandra
and ScyllaDB, written in Rust against ScyllaDB's own
scylla-rust-driver. Built
against the Cassandra/ScyllaDB plugin bounty:
keyspaces, tables, paged CQL queries, and row editing.
Implements:
- Connection:
initialize(applieslocal_datacenter/consistency_level/request_timeout_ms/scylla_shard_awarefrom the manifest'ssettings),test_connection,ping - Schema browsing:
get_databases(keyspaces),get_tables,get_columns,get_indexes.get_views,get_routines,get_triggers, andget_foreign_keysare implemented as no-ops returning an empty list, since CQL has none of these concepts - see "Known limitations" for why they're implemented at all rather than just declared unsupported - Querying:
execute_querywith CQL-native forward paging (see below) - Row editing:
insert_record,update_record,delete_record(single- column primary keys only - see "Known limitations") - CI and release automation: every push runs a full functional smoke test against a real Cassandra service container; tagged releases build and publish per-platform binaries (Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64) via GitHub Actions
- Verified against real ScyllaDB, not just Cassandra: the full protocol surface has been exercised end-to-end against an actual ScyllaDB container, in addition to Cassandra
Not yet implemented: TLS. See "Known limitations" and "Roadmap" below.
ScyllaDB is wire-compatible with Cassandra's CQL native protocol, so a
scylla-rust-driver-based client talks to either without any protocol-level
branching - this mirrors the bounty's own recommendation to build the
Cassandra path first and validate ScyllaDB on top of it rather than
duplicating a second plugin.
Cassandra vs ScyllaDB, concretely:
- Connection settings, schema discovery, querying, and row editing all work identically against both - confirmed directly against real Cassandra and real ScyllaDB containers, not just assumed from protocol compatibility.
- ScyllaDB additionally supports shard-aware and tablet-aware routing for
lower tail latency under load. This plugin does not implement shard-aware
routing yet - the
scylla_shard_awareconnection setting is currently informational only (recorded, not acted on). Both databases work correctly without it; it's purely a performance optimization for high-throughput ScyllaDB workloads. Tracked as follow-up work.
Requires the Rust toolchain (stable). On Windows, you
also need a linker - install Visual Studio Build Tools' "Desktop development
with C++" workload (or winget install Microsoft.VisualStudio.2022.BuildTools --silent --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"), since Rust's
default Windows target expects MSVC's link.exe.
cargo build # debug build -> target/debug/tabularis-cassandra-plugin
cargo build --release # release build -> target/release/tabularis-cassandra-plugin
cargo test # unit tests (none yet - this currently just confirms the harness runs)
cargo fmt --all -- --check # formatting check, matches CI
cargo clippy --all-targets --all-features -- -D warnings # lint, matches CICI (.github/workflows/ci.yml) runs all of the above plus a full functional
smoke test against a real Cassandra service container on every push.
Tagged pushes (vX.Y.Z) additionally trigger .github/workflows/release.yml,
which cross-compiles a binary for each supported platform and publishes them,
plus a per-platform .tabularium, as GitHub Release assets.
Download the zip for your platform from the
latest release,
which contains the built binary and a matching .tabularium. Extract both
into Tabularis's plugin directory and restart Tabularis (or reload plugins
from Settings).
Copy the built binary and manifest into Tabularis's plugin directory, then restart Tabularis or reload plugins from Settings:
# Linux/macOS
mkdir -p ~/.local/share/tabularis/plugins/cassandra
cp target/release/tabularis-cassandra-plugin ~/.local/share/tabularis/plugins/cassandra/
cp .tabularium ~/.local/share/tabularis/plugins/cassandra/# Windows
mkdir "$env:APPDATA\tabularis\plugins\cassandra"
copy target\release\tabularis-cassandra-plugin.exe "$env:APPDATA\tabularis\plugins\cassandra\"
copy .tabularium "$env:APPDATA\tabularis\plugins\cassandra\"scripts/exercise-plugin.sh pipes a full sequence of JSON-RPC requests
(connect, browse schema, page a query, insert/update/delete a row) into the
built binary against a real Cassandra/Scylla, exactly as Tabularis itself
would, and fails if any response comes back with a JSON-RPC error:
docker run --rm -d --name smoke-cassandra -p 9042:9042 cassandra:5.0
# wait for it to come up (cqlsh 127.0.0.1 9042 -e "DESCRIBE KEYSPACES"), then:
cargo build
./scripts/exercise-plugin.sh # against the debug build
./scripts/exercise-plugin.sh --release # against a release build (cargo build --release first)The same script has also been run unmodified against a real ScyllaDB
container (docker run --name scylla -p 9042:9042 -d scylladb/scylla --smp 1 --memory 750M --overprovisioned 1 --broadcast-rpc-address 127.0.0.1).
You can also drive a single request by hand:
echo '{"jsonrpc":"2.0","method":"test_connection","params":{"params":{"host":"127.0.0.1","port":9042,"database":"my_keyspace"}},"id":1}' \
| ./target/debug/tabularis-cassandra-pluginCQL has no OFFSET/row-number-based paging - a page is only reachable via
the opaque PagingState token the server returns alongside the previous
page. The plugin caches that token per query (keyed by the raw query text) so
paging forward one page at a time - the common UI pattern - resumes exactly
where it left off, without replaying earlier pages. A page requested "out of
order" (the cache doesn't already have the token leading to it) falls back to
replaying forward from the start, which is correct but costs more requests;
in practice this only happens after a process restart or a UI jump straight
to an unvisited page.
Similarly, CQL has no cheap COUNT(*) for an arbitrary statement - it's a
full coordinator-side scan, which this plugin deliberately never issues just
to populate a total. total_count is therefore the number of rows actually
counted so far, across every page fetched for that query in this process's
lifetime - accurate as "how many rows have we seen," not a claim about the
true total until the query is actually exhausted.
- Views, routines, triggers, and foreign keys don't exist in CQL, so
get_views/get_routines/get_triggers/get_foreign_keysalways return an empty list. They're implemented (not just declaredfalsein.tabularium) because Tabularis's schema-tree UI calls some of these unconditionally - in the same batched request as calls this plugin does need to succeed (get_tables, orget_columns/get_indexeswhen expanding a table). A driver that returns "Method not found" for any one call in that batch fails the whole batch, silently discarding the good results too (with nothing shown in the UI - Tabularis only logs it to the browser console). Concretely: leavingget_viewsunimplemented meant no table ever appeared under any keyspace, and leavingget_foreign_keysunimplemented meant expanding a table never showed its columns. Returning an empty list for each keeps those batches alive. - Composite primary keys and row editing. Tabularis's
update_record/delete_recordprotocol identifies a row with a singlepk_col/pk_valpair. CQL primary keys are frequently composite (partition key plus clustering columns), which that shape can't express. Tables with a composite primary key are therefore browsable and queryable, but not editable through the grid - edit them with CQL via the query editor instead.insert_recordis unaffected, since it supplies every column explicitly. - Row editing only covers a subset of CQL scalar types.
insert_record/update_recordcurrently accept text/ascii, boolean, int, bigint, smallint, tinyint, float, double, blob (as hex), and uuid. Decimal, duration, date/time/timestamp, varint, counter, timeuuid, and any collection/tuple/user-defined-type column can't be written through the grid yet - write those via CQL through the query editor. All of these types, plus collections/tuples/UDTs, display correctly when reading (execute_query); only the write side is narrower. - No TLS. The
ssl_modeconnection field is accepted but not yet acted on -.tabulariumaccordingly declaressupports_ssl: false. - No shard-aware ScyllaDB routing - see "Why Cassandra and ScyllaDB share one plugin" above.
request_timeout_msonly governs per-statement timeouts, not the initial connection handshake - the driver's own default applies there.- Secondary index columns are reported using CQL's raw index target
expression (e.g.
values(tags)) rather than a parsed column list, since CQL index targets are expressions, not always plain columns.
| Setting | Default | Notes |
|---|---|---|
local_datacenter |
datacenter1 |
Required by the driver's default load-balancing policy; must match a real datacenter name in your cluster. |
consistency_level |
LOCAL_QUORUM |
Any standard CQL consistency level; an unrecognized value falls back to LOCAL_QUORUM rather than failing the connection. |
request_timeout_ms |
10000 |
Applies to per-request timeouts only - see "Known limitations". |
scylla_shard_aware |
false |
Informational only for now - see "Known limitations". |
- TLS support (
rustls, most likely). - Wider read/write type coverage (see "Known limitations").
- Shard-aware ScyllaDB routing.
Bug reports and pull requests are welcome - please open an issue first for anything beyond a small fix. See CODE_OF_CONDUCT.md for community expectations and CHANGELOG.md for release history.
Apache License 2.0 - see LICENSE.