Arrow Flight / ADBC columnar interface for LadybugDB — Rust port of
ladybug-adbc-python. Serves LadybugDB Cypher
results as native Arrow columnar batches over gRPC, consumed with the standard
columnar.tech call pattern — no row-by-row JSON.
cargo build --release# 1. server (:memory: demo graph, same data as the Python package)
./target/release/ladybug-flight-server
# grpc://127.0.0.1:50051, try: --db /tmp/graph.lbdb --port 50051 --no-demo
# 2. client — ADBC-equivalent prepared-statement flow (default)
./target/release/ladybug-client
./target/release/ladybug-client --demo # all 6 demo queries
./target/release/ladybug-client --demo --no-adbc # plain FlightClient pathuse ladybug_adbc::LadybugClient;
let mut client = LadybugClient::connect("grpc://localhost:50051").await?;
let (schema, batches) = client
.query_flightsql("MATCH (u:User) RETURN u.name, u.age ORDER BY u.id")
.await?;query_flightsql is wire-for-wire what adbc_driver_flightsql.dbapi does:
CreatePreparedStatement action → GetFlightInfo with
CommandPreparedStatementQuery → DoGet with TicketStatementQuery →
ClosePreparedStatement. The query field is just a string on the wire, so
Cypher passes straight through. query_flight is the plain-Flight equivalent
(GetFlightInfo(new_cmd(cypher)) → DoGet).
The two servers are wire-compatible: the Python ADBC client queries this Rust server and the Rust client queries the Python server (verified for all demo queries, both flows).
Two ways to measure — both report queries/s, rows/s, MiB/s:
# Standalone throughput table (spawns an ephemeral in-process server by default,
# or hammers an existing one with --uri; use separate processes for real numbers)
./target/release/ladybug-bench
./target/release/ladybug-bench --uri grpc://localhost:50051 --iterations 500 --concurrency 8
./target/release/ladybug-bench --query all_users --no-adbc
# Criterion micro-benchmarks: direct (no gRPC) vs plain Flight vs prepared flow
cargo benchExample (release, server + bench as separate processes, concurrency 8):
| query | qps | rows/s |
|---|---|---|
| all_users | ~1700 | ~8500 |
| follows / lives_in | ~190 | ~1000 |
| two_hop / same_city | ~80–130 | ~150–260 |
# Same 1M mixed-type rows (UNWIND range(...)) over both paths:
./target/release/ladybug-bench-million --rows 1000000 --iterations 3
./target/release/ladybug-bench-million --uri grpc://localhost:50051 --rows 1000000Measured (release, separate server process, 1M rows × 3 cols):
| leg | secs | rows/s | MiB | MiB/s |
|---|---|---|---|---|
| ADBC round trip | 2.18 | ~460k | 25.2 | 11.6 |
| JSON codec (convert+ser+parse) | 0.75 | ~1.34M | 40.7 | 54.5 |
| Arrow IPC codec (enc+dec) | 0.010 | ~101M | 25.2 | ~2560 |
Takeaways: JSON wire payload is 1.6x the Arrow IPC payload, and row-JSON
ser/parse is ~75x the Arrow IPC encode/decode on identical data.
GetFlightInfo resolves schema via prepare (no execution), so a round trip
executes the query exactly once, in DoGet — the ADBC number above is ~1s
faster than the old double-execution design (was 3.23s).
adbc_driver_flightsql is typically tied to SQL (cursor.execute(sql),
CommandStatementQuery{query}), but query is just a string on the wire.
LadybugFlightServer treats it as Cypher (see extract_query()), so the
columnar.tech call pattern works unchanged for a graph DB. CALL show_tables()
keeps working for schema introspection.
| REST (Flask) | Arrow (this crate / Python twin) | |
|---|---|---|
| encoding | to_pydict → json.dumps → parse |
Arrow IPC over gRPC |
| types | lost (everything is JSON) | preserved (schema, native batches) |
| client | POST /query {"data": [[0, cypher]]} |
prepared statement / new_cmd(cypher) |
| batching | manual row-index envelope | FlightDataEncoder stream |
src/lib.rs—demo_queries(),build_demo_graph(),execute_cypher(), dependency-free Flight SQL protobuf codec (extract_query, …).src/server.rs—LadybugFlightServer(FlightService+ Flight SQL prepared-statement handshake),serve_ephemeral()test helper.src/client.rs—LadybugClient(persistent connection) plusquery_cypher_flight()/query_cypher_flightsql()one-shot functions.src/main.rs—ladybug-flight-serverbinary (--host/--port/--db,FLIGHT_HOST/FLIGHT_PORT/LADYBUG_DBenv).src/bin/client.rs—ladybug-clientdemo.src/bin/bench.rs—ladybug-benchthroughput table.src/bin/healthcheck.rs— containerHEALTHCHECKprobe.benches/throughput.rs— criterion benches (direct / plain / prepared).tests/test_flight.rs— ephemeral server, FlightSQL ≡ Flight Arrow tables.
docker build -t ladybug-adbc-rs:dev .
# :memory: demo graph on localhost:50051
docker run --rm -p 50051:50051 ladybug-adbc-rs:dev
# persistent graph via a volume
docker run --rm -p 50051:50051 -v ladybug-data:/data \
-e LADYBUG_DB=/data/graph.lbdb ladybug-adbc-rs:devEnv knobs: FLIGHT_HOST (default 0.0.0.0 in the image), FLIGHT_PORT
(default 50051), LADYBUG_DB (default :memory:).