Ferrosa exposes an Apache Arrow Flight (gRPC) endpoint so analytics clients can read and write query results as Arrow record batches — no row-by-row CQL decoding, columnar on the wire.
|
Note
|
The Flight endpoint is built behind the flight cargo feature and listens on gRPC port 8815 by default (FERROSA_FLIGHT_BIND). It runs alongside the CQL endpoint (9042); the schema you create over CQL is what Flight serves.
|
Prerequisites
-
A Ferrosa build with the
flightfeature, withFERROSA_FLIGHT_SIGNING_KEYset (so bearer tokens survive restarts and work across nodes). -
CQL reachable on
localhost:9042, Flight onlocalhost:8815. -
Run
schema.cqlfirst to create the table the examples read/write.
-- Arrow Flight example — schema + seed data.
--
-- Run this against a running Ferrosa (CQL on localhost:9042) before exercising
-- the Flight endpoint (gRPC on localhost:8815). The Flight DoGet/DoPut RPCs read
-- and write this same table over Arrow; the CQL here is the setup the
-- "Example CQL Scripts" CI job executes.
CREATE KEYSPACE IF NOT EXISTS flight_demo
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
CREATE TABLE IF NOT EXISTS flight_demo.sensor_readings (
sensor_id text,
ts timestamp,
temperature double,
humidity double,
PRIMARY KEY (sensor_id, ts)
) WITH CLUSTERING ORDER BY (ts DESC);
INSERT INTO flight_demo.sensor_readings (sensor_id, ts, temperature, humidity)
VALUES ('sensor-1', '2026-06-19T10:00:00Z', 21.5, 44.0);
INSERT INTO flight_demo.sensor_readings (sensor_id, ts, temperature, humidity)
VALUES ('sensor-1', '2026-06-19T10:01:00Z', 21.7, 43.8);
INSERT INTO flight_demo.sensor_readings (sensor_id, ts, temperature, humidity)
VALUES ('sensor-2', '2026-06-19T10:00:00Z', 19.2, 51.3);
-- Read it back over CQL (the same rows DoGet returns as an Arrow stream):
SELECT sensor_id, ts, temperature, humidity FROM flight_demo.sensor_readings;
Authentication
Every Flight RPC except Handshake requires a bearer token. The handshake contract is deliberately simple:
-
The client opens
Handshakeand sends oneHandshakeRequestwhosepayloadis the bytesusername\0password(username, a NUL byte, password). -
Ferrosa validates the credentials against its CQL roles and replies with a
HandshakeResponsewhosepayloadis the signed token. -
On every subsequent RPC the client sets the
authorization: Bearer <token>gRPC metadata header.
Tokens carry the role + superuser flag and expire after FERROSA_FLIGHT_TOKEN_TTL_SECS (default 3600s). Signing keys rotate via FERROSA_FLIGHT_SIGNING_KEY_PREVIOUS (all listed keys verify; the primary signs) so tokens issued by a just-retired key still validate during the overlap window.
Reading: GetFlightInfo + DoGet
A read places a CQL SELECT string in the descriptor command. GetFlightInfo returns the Arrow schema plus the endpoint(s) to fetch; DoGet streams the result one page at a time (peak memory is bounded to a single page, not the whole result set).
-
GetSchemareturns just the Arrow schema for a command (no data). -
On a cluster,
GetFlightInforeturns one endpoint per token range — each ticket a token-boundedSELECT, located on the replica(s) that own the range — so a client can fetch ranges in parallel. On a single node it returns one endpoint redeeming the command on this connection.
Writing: DoPut + DoExchange
-
DoPutingests Arrow record batches as CQLINSERT`s into the table named by the `ferrosa-table: keyspace.tablegRPC metadata header. Identifiers are validated and literals escaped (injection-safe). ThePutResult.app_metadatacarries the number of rows applied. -
DoExchangeis a bidirectional streaming upsert: the client streams record batches in and receives one acknowledgement frame per batch (rows-applied count inapp_metadata), reusing the same Arrow→CQL write path asDoPut.
Discovery and actions
-
ListFlightsenumerates the queryable (non-system) tables, oneFlightInfoeach, with aSELECT * FROM ks.tticket; an optionalCriteriaexpression prefix-filters bykeyspace.table. -
PollFlightInforeturns a completedPollInfofor a command. -
ListActions/DoActionexposeserver.info(service + version) andtoken.validate(echoes the verified bearer identity).
CQL ↔ Arrow type mapping
The converter covers the full CQL type set: integers/floats/booleans/text/blob/uuid/timestamp map to their Arrow equivalents; decimal is rendered exactly; list/set/map/tuple/UDT map to Arrow List/Struct/Map; vector columns map to fixed-size lists.
Live change streams (SUBSCRIBE)
For streaming changes rather than point-in-time reads, use CQL SUBSCRIBE via the Python drop-in driver in drivers/ferrosa_driver (SUBSCRIBE SELECT * FROM ks.t ON LOCAL|COMMITTED). It delivers each committed change in real time over the CQL wire. See drivers/README.md.
Reference client
flight_demo.py is a runnable pyarrow.flight client that performs the handshake above (custom ClientAuthHandler sending username\0password, capturing the token, then attaching authorization: Bearer), calls GetFlightInfo, and DoGet`s each endpoint as Arrow. It is verified live against a `flight-enabled server (pip install pyarrow):
ap.add_argument("--flight", default="grpc://127.0.0.1:8815")
ap.add_argument("--user", default="cassandra")
ap.add_argument("--password", default="cassandra")
ap.add_argument("--keyspace", default="flight_demo")
ap.add_argument("--table", default="sensor_readings")
args = ap.parse_args()
client = flight.FlightClient(args.flight)
# 1. Handshake -> bearer token, then attach it to every call.
hs = _Handshake(args.user, args.password)
client.authenticate(hs)
opts = flight.FlightCallOptions(headers=[(b"authorization", b"Bearer " + hs.token)])
print(f"[flight] authenticated, token {len(hs.token)} bytes")
# 2. GetFlightInfo for a SELECT, then DoGet each endpoint as Arrow.
cmd = f"SELECT sensor_id, ts, temperature, humidity FROM {args.keyspace}.{args.table}".encode()
info = client.get_flight_info(flight.FlightDescriptor.for_command(cmd), opts)
print(f"[flight] GetFlightInfo: {len(info.endpoints)} endpoint(s), columns {info.schema.names}")
total = 0
for endpoint in info.endpoints:
table = client.do_get(endpoint.ticket, opts).read_all()
total += table.num_rows
for row in table.to_pylist():
print(f"[flight] row: {row}")
print(f"[flight] read {total} rows over Arrow Flight")
return 0
if __name__ == "__main__":
sys.exit(main())
Running it against the seeded schema.cql prints the three sensor rows read back over Arrow:
[flight] authenticated, token 109 bytes [flight] GetFlightInfo: 1 endpoint(s), columns ['sensor_id', 'ts', 'temperature', 'humidity'] [flight] read 3 rows over Arrow Flight
|
Note
|
Wiring flight_demo.py into CI (a job that boots a flight-enabled server and runs it, alongside the existing "Example CQL Scripts" job which already runs schema.cql) is tracked in forge t_0c1f92ee.
|