>_STRYKE-SCYLLA
ScyllaDB / Apache Cassandra client for stryke. CQL query, DDL, and schema introspection — over scylla, ScyllaDB's official pure-Rust driver, which also speaks Apache Cassandra. It speaks the native CQL binary protocol; the cdylib owns one embedded tokio runtime and blocks on each call, so the stryke-facing API is synchronous. A Session is cached per (nodes, auth, keyspace). An opt-in package, kept out of the stryke core binary so the daily-driver install stays slim.
Install
s add github.com/MenkeTechnologies/stryke-scylla
On first use Scylla, stryke dlopens the cdylib in-process and registers every scylla__* export.
Quick start
use Scylla
var %conn
$conn{node} = "127.0.0.1:9042"
$conn{keyspace} = "app"
Scylla::create_keyspace("app", %conn)
Scylla::create_table(
"app.users",
columns => "id uuid, name text, age int",
primary_key => "id",
%conn,
)
Scylla::execute("INSERT INTO app.users (id, name, age) VALUES (uuid(), 'ada', 36)", %conn)
p Scylla::count("app.users", %conn)
val @rows = Scylla::query("SELECT name, age FROM app.users", %conn)
Connection params travel on the same %conn opts hash that every call already takes — there is no explicit connect/disconnect step. A Session is opened lazily on the first call that needs the cluster, then cached for the rest of the process.
Connecting
Contact points, auth, and the active keyspace come from the %opts hash on every call. With no node given, the wrapper falls back to $SCYLLA_NODES (comma-separated), and the cdylib defaults to 127.0.0.1:9042. A bare host without a port has :9042 appended.
| node | One contact point, host or host:port. Default 127.0.0.1:9042. |
| host | Alias for node; combine with port. |
| port | Default CQL port 9042; applied to node/host when they carry no :port. |
| nodes | Array of contact points; overrides node/host when present. |
| username | CQL auth user (PasswordAuthenticator). Omit for no-auth clusters. |
| password | CQL auth password. |
| keyspace | USEd on the session right after connect. |
The Session is cached per (nodes, username, password, keyspace) tuple, so calls that share those four values reuse one connection pool; differing on any of them opens (and caches) a separate session.
# multi-node cluster, authenticated, pinned to a keyspace
var %conn
$conn{nodes} = ["10.0.0.1", "10.0.0.2:9100"]
$conn{username} = "cassandra"
$conn{password} = "cassandra"
$conn{keyspace} = "app"
p Scylla::ping(%conn) # 1 when the cluster answers, 0 otherwise
p Scylla::server_version(%conn) # release_version from system.local
p Scylla::cluster_name(%conn) # configured cluster name
# resolve what these opts normalize to, without connecting
val @cp = Scylla::contact_points(node => "db") # ("db:9042")
Rows as JSON
Result rows are read generically: each CqlValue maps to its natural JSON form — integers (Int, BigInt, SmallInt, TinyInt), floats (Float, Double), Text/Ascii strings, Boolean, Uuid (as its string form), List/Set as JSON arrays, and Map as an array of [key, value] pairs — with a debug-string fallback for exotic types so the converter never fails. Empty becomes null. Rows come back as objects keyed by column name, taken from the result's column specs.
A statement that returns no result set (DDL, INSERT, UPDATE, DELETE) yields an ok marker rather than rows; the row-returning wrappers expose an empty array for those.
CQL injection safety
Queries are sent as unparameterized CQL strings, so any untrusted value you interpolate must be escaped first. The pure helpers do this without touching the cluster, and are unit-tested in-crate so they validate in CI with no cluster running.
# double the single quote (CQL's literal-escape rule)
val $name = Scylla::escape($user_input)
val @hits = Scylla::query("SELECT * FROM app.users WHERE name = '$name' ALLOW FILTERING", %conn)
# or build the whole literal at once
val $lit = Scylla::quote_literal("O'Brien") # 'O''Brien'
val $id = Scylla::quote_ident("MixedCase") # "MixedCase" (case-sensitive)
# render typed values / IN-lists for a WHERE clause
val $v = Scylla::format_value(42) # 42
val $in = Scylla::format_in_list([1, 2, 3]) # (1, 2, 3)
# gate an identifier you intend to use unquoted
if (Scylla::valid_identifier($col)) { ... } # [A-Za-z][A-Za-z0-9_]*
escape doubles the single quote; quote_literal wraps and escapes in one step; quote_ident double-quotes an identifier for case sensitivity; format_value renders a value as a CQL literal (string→'...', number as-is, bool→true/false, null→NULL, array→CQL list); format_in_list joins values into an IN (...) list, emitting (NULL) for an empty input so it matches nothing; valid_identifier is true only for an unquoted-safe CQL identifier.
Batches
batch applies an arrayref of CQL statements as one CQL batch and returns the statement count. By default it is a logged batch; pass unlogged => 1 for an UNLOGGED batch. A batch is atomic within a single partition; cross-partition batches are allowed but not atomic.
val $n = Scylla::batch([
"INSERT INTO app.users (id, name) VALUES (uuid(), 'a')",
"INSERT INTO app.users (id, name) VALUES (uuid(), 'b')",
], %conn)
p $n # 2
Schema introspection
The introspection calls read system_schema.* / system.* and return plain stryke arrays or row objects — no DESCRIBE parsing required.
val @ks = Scylla::keyspaces(%conn) # keyspace names
val @tbl = Scylla::tables("app", %conn) # table names in a keyspace
val @cols = Scylla::columns("app", "users", %conn) # {column_name, type, kind} rows
val @pk = Scylla::partition_keys("app", "users", %conn) # in key order
val @ck = Scylla::clustering_keys("app", "users", %conn) # in clustering order
val @idx = Scylla::indexes("app", "users", %conn) # {index_name, kind, options}
val @mv = Scylla::views("app", %conn) # materialized-view names
val @udt = Scylla::types("app", %conn) # user-defined type names
val @peer = Scylla::peers(%conn) # peer node addresses
partition_keys and clustering_keys read system_schema.columns and sort by each column's position, so they return in declared key order rather than the unordered order the system table emits.
API reference
34 public Scylla::* functions, each a thin wrapper over a scylla__* cdylib export. Every networked call takes a trailing %opts hash for connection params.
Liveness
| version() | Package version string. |
| ping(%opts) | Connectivity probe — 1 when the cluster answers, 0 otherwise. |
| server_version(%opts) | Cluster release version (release_version from system.local). |
| cluster_name(%opts) | Cluster name configured on the coordinator (system.local). |
| peers(%opts) | Peer node addresses known to the coordinator (system.peers). |
Query
| query($cql, %opts) | Run a SELECT; returns the array of row objects. |
| query_row($cql, %opts) | Run a SELECT; returns the first row object (or undef). |
| query_value($cql, %opts) | Run a SELECT; returns the first column of the first row (a scalar). |
| execute($cql, %opts) | Run a statement that returns no rows (INSERT/UPDATE/DELETE/DDL). |
| batch($statements, %opts) | Apply an arrayref of statements as one CQL batch; returns the count. unlogged => 1 selects an UNLOGGED batch. |
| raw($cql, %opts) | Escape hatch: run arbitrary CQL. rows => 0 for a no-row statement; default returns the row array. |
Introspection
| keyspaces(%opts) | Array of keyspace names. |
| tables($keyspace, %opts) | Array of table names in a keyspace. |
| columns($keyspace, $table, %opts) | Column descriptor rows (column_name, type, kind). |
| count($table, %opts) | Row count of a table (full-table scan — use sparingly on large tables). |
| indexes($keyspace, $table, %opts) | Secondary index descriptors (index_name, kind, options). |
| views($keyspace, %opts) | Array of materialized-view names. |
| types($keyspace, %opts) | Array of user-defined type names. |
| partition_keys($keyspace, $table, %opts) | Partition-key column names, in key order. |
| clustering_keys($keyspace, $table, %opts) | Clustering column names, in clustering order. |
DDL
| create_keyspace($name, %opts) | CREATE KEYSPACE IF NOT EXISTS. replication => sets the CQL replication map (default SimpleStrategy RF 1). |
| drop_keyspace($name, %opts) | DROP KEYSPACE IF EXISTS. |
| create_table($name, %opts) | Either cql => (full CREATE) or columns => + primary_key =>. Uses IF NOT EXISTS. |
| drop_table($table, %opts) | DROP TABLE IF EXISTS. |
| create_index($table, $column, %opts) | CREATE INDEX IF NOT EXISTS on a column. name => names the index; omit to let the cluster assign one. |
| drop_index($name, %opts) | DROP INDEX IF EXISTS (name may be keyspace-qualified). |
| truncate($table, %opts) | TRUNCATE a table. |
Pure helpers (no connection)
| escape($value) | Escape a string for a single-quoted CQL literal (doubles '). |
| quote_literal($value) | Wrap a string as a single-quoted CQL literal (escaping embedded quotes). |
| quote_ident($value) | Double-quote a string as a case-sensitive CQL identifier. |
| valid_identifier($value) | True when a string is a valid unquoted CQL identifier. |
| format_value($value) | Format a value as a CQL literal (string/number/bool/null/list). |
| format_in_list($values) | Render an arrayref of values into a CQL IN (...) list. |
| contact_points(%opts) | Normalize the contact points these opts resolve to (each host:port). |
The same surface is summarized in the README and detailed in the engineering report.
Build & test
make debug # cargo build make test # cargo test, then `s test t/` make install # s pkg install -g .
cargo test runs the in-crate unit tests (CQL escaping, identifier quoting, contact-point normalization, batch construction, CqlValue→JSON, column-position sorting) with no cluster required. t/test_stryke_scylla_surface.stk pins that every public wrapper resolves and exercises the connection-free helpers; t/test_scylla.stk runs an end-to-end keyspace/table/insert/query/index lifecycle against a live cluster ($SCYLLA_NODES or 127.0.0.1:9042) and short-circuits when none answers, so CI without a cluster still passes the connection-free asserts.
Why a package, not a builtin
The CQL + TLS stack (the scylla driver, tokio, rustls) ships once, on demand, as an opt-in package — stryke core is never linked against it. On first use Scylla, stryke dlopens the cdylib in-process and registers every scylla__* export. The cdylib owns one embedded tokio runtime and presents a blocking facade, so from stryke's perspective every Scylla::* function is an ordinary blocking call.
Sibling packages
Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging