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 flight feature, with FERROSA_FLIGHT_SIGNING_KEY set (so bearer tokens survive restarts and work across nodes).

  • CQL reachable on localhost:9042, Flight on localhost:8815.

  • Run schema.cql first 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:

  1. The client opens Handshake and sends one HandshakeRequest whose payload is the bytes username\0password (username, a NUL byte, password).

  2. Ferrosa validates the credentials against its CQL roles and replies with a HandshakeResponse whose payload is the signed token.

  3. 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).

  • GetSchema returns just the Arrow schema for a command (no data).

  • On a cluster, GetFlightInfo returns one endpoint per token range — each ticket a token-bounded SELECT, 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

  • DoPut ingests Arrow record batches as CQL INSERT`s into the table named by the `ferrosa-table: keyspace.table gRPC metadata header. Identifiers are validated and literals escaped (injection-safe). The PutResult.app_metadata carries the number of rows applied.

  • DoExchange is a bidirectional streaming upsert: the client streams record batches in and receives one acknowledgement frame per batch (rows-applied count in app_metadata), reusing the same Arrow→CQL write path as DoPut.

Discovery and actions

  • ListFlights enumerates the queryable (non-system) tables, one FlightInfo each, with a SELECT * FROM ks.t ticket; an optional Criteria expression prefix-filters by keyspace.table.

  • PollFlightInfo returns a completed PollInfo for a command.

  • ListActions / DoAction expose server.info (service + version) and token.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.