// STRYKE-SCYLLA — SCYLLADB / CASSANDRA CLIENT FOR STRYKE // CQL QUERY + DDL + SCHEMA INTROSPECTION

stryke package · namespace Scylla · cdylib libstryke_scylla · opt-in (kept out of stryke core)

Report GitHub Issues

>_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.

nodeOne contact point, host or host:port. Default 127.0.0.1:9042.
hostAlias for node; combine with port.
portDefault CQL port 9042; applied to node/host when they carry no :port.
nodesArray of contact points; overrides node/host when present.
usernameCQL auth user (PasswordAuthenticator). Omit for no-auth clusters.
passwordCQL auth password.
keyspaceUSEd 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: