You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Disclaimer: this issue was drafted with an LLM (Claude Code) at my request. The code, git, and issue references were verified against main at 5e54503, but the design proposal is a starting point for discussion, not a settled plan.
Is your feature request related to a problem or challenge? Please describe what you are trying to do.
Ballista has no way for a generic SQL client to connect to it. Every client must be a Rust program that builds a DataFusion LogicalPlan, serializes it, and speaks SchedulerGrpc. That rules out JDBC/ODBC tooling, BI tools, DBeaver, Python (adbc/pyarrow), and anything else that expects to send SQL text and get Arrow back.
Flight SQL used to fill that gap, and was removed in 46.0.0 (#1227, #1228). It's worth being precise about why, because it wasn't "Flight SQL is a bad fit for Ballista":
It was substantially incomplete. Of ~40 FlightSqlService methods in ballista/scheduler/src/flight_sql.rs, well over half were Status::unimplemented, including CommandGetSqlInfo — which carried the comment // TODO: implement for FlightSQL JDBC to work. Catalog metadata (GetCatalogs, GetDbSchemas, GetTables as a real RPC, GetXdbcTypeInfo, keys) was stubbed, so driver-level introspection didn't work.
It was structurally coupled to scheduler internals: it took a concrete SchedulerServer<LogicalPlanNode, PhysicalPlanNode>, reached into server.state.config, faked catalog responses by smuggling well-known strings ("get_flight_info_tables") through the job_id field of a FetchPartition ticket, and carried its own inline Flight proxy.
Authentication was if user != "admin" || pass != "password", Basic-only.
It had zero tests (grep -c '#\[test\]' <old file> → 0).
So the removal was right, and reviving that file as-is would be wrong. But the capability is still valuable, and @avantgardnerio made the point directly in #2249: "using this to restore FlightSQL could add a significant audience as well." This issue is to design a new implementation properly.
Why now
Several things have landed since 46.0.0 that make this materially easier than it was in 2023–2025:
The scheduler has a real embedded Flight proxy (feat: Add arrow flight proxy to scheduler #1351, ballista/scheduler/src/flight_proxy_service.rs), plus FlightProxy::Local/External advertisement in GetJobStatusResult. This fixes the old implementation's worst structural problem: it built FlightEndpoint locations out of per-executor host:port (and make_local_fieps hardcoded 127.0.0.1:50050 behind a // TODO: use advertise host), which is exactly why it broke behind NAT, Docker, Kubernetes, and load balancers (wrong location in FlightEndpoint causing not support Flight SQL JDBC Driver #1012, Clarify usage of advertise_flight_sql_endpoint #1349). A new implementation can hand clients one stable address and let the scheduler fan out to executors.
The scheduler already owns a per-session SessionContext built by a pluggable SessionBuilder (ballista/scheduler/src/state/session_manager.rs:48-85). Server-side SQL planning needs a catalog, and this is an existing, documented extension point for supplying one — rather than the old UX of "re-register your tables with CREATE EXTERNAL TABLE on every new connection."
A richer scheduler RPC surface: CreateUpdateSession, RemoveSession, ExecuteQueryPush (streaming status, so no polling loop), GetJobMetrics, CancelJob, CleanJobData.
arrow-flight 58.4 is already a workspace dependency with flight-sql-experimental enabled (Cargo.toml:41), so this adds no new dependency — that feature flag is currently enabled for nothing.
Flight SQL is arguably the best first consumer of that abstraction, and a useful forcing function for it:
It is much smaller in scope than Spark Connect (no artifacts, no UDF upload, no Spark relation tree to translate) — SQL text in, Arrow out.
It exercises every part of the contract anyway: session open/close, auth, plan decode, submit, status, cancel, result streaming, and metadata.
It has an existing, external, standards-defined client population, so "does the abstraction actually work" gets tested by third-party drivers rather than by our own client.
If #2249's QueryBackend/GrpcQueryFrontend traits land first, this issue implements a FlightSqlFrontend against them. If Flight SQL work starts first, it should define its needs in those terms so the traits are shaped by two real consumers instead of one. Concretely, this frontend must not depend on SchedulerServer internals the way the old one did — that coupling is what made it unmaintainable.
Describe the solution you'd like
Placement
A new optional crate, ballista/flight-sql, exposed through a non-default flight-sql feature on the scheduler (and mountable by embedders into their own Tonic server per #2249). Rationale: it keeps the scheduler's default dependency surface unchanged, and it means the thing can be maintained — or deprecated — independently. That directly answers the maintainability objection in #1227.
Core flow
GetFlightInfo(CommandStatementQuery):
Resolve the Flight SQL session (from the handshake token) to a Ballista session_id; CreateUpdateSession if new.
Plan the SQL text server-side with that session's SessionContext → LogicalPlan.
Submit via the query backend (today: ExecuteQuery), obtaining a job_id.
Await completion via ExecuteQueryPush's status stream rather than a poll loop.
Build FlightEndpoints whose tickets are the existing Action::FetchPartition protobuf, and whose location is resolved through the existingFlightProxy logic — scheduler/LB by default, direct-to-executor only when explicitly configured.
DoGet(ticket) is then satisfied by the existing proxy path in flight_proxy_service.rs, which already decodes Action::FetchPartition and forwards to the owning executor. Ideally the frontend reuses that service rather than re-implementing a proxy inline, as the old code did.
Open design decisions
These are the things I'd like input on; I don't think any of them should be settled unilaterally.
Catalog / planning model. SQL text has to be planned against something. Options: (a) session-scoped DDL only, as before — simple, but poor UX and no cross-connection persistence; (b) embedder-supplied catalog via SessionBuilder — my preference, reuses an existing extension point; (c) scheduler-side configured tables/object stores. Also: should CommandStatementSubstraitPlan be supported for plan-shaped clients, given substrait is already an optional scheduler feature?
Long-running queries. Blocking GetFlightInfo until the job completes (old behaviour) will hit client and proxy timeouts for TPC-H-scale queries. PollFlightInfo exists in arrow-flight 58 (the old impl stubbed it) and is designed for exactly this. Do we require it, and do the drivers we care about actually use it?
Metadata surface. What is the minimum viable set for real drivers? At minimum CommandGetSqlInfo and GetXdbcTypeInfo, which the old implementation never had — and without which the Arrow Flight JDBC driver doesn't work. Proposal: define the set by "the driver connects and introspects," not by "the trait method exists," and drive catalog answers from the session's real DataFusion catalog rather than hand-built RecordBatches.
Prepared statements. Plan cache keyed by handle needs a TTL/eviction policy — the old DashMap<Uuid, LogicalPlan> only shed entries on explicit close, so any client that disconnected leaked. Parameter binding (DoPutPreparedStatementQuery, parameter_schema) was a // TODO: parameters; is bound-parameter support in scope for v1 or explicitly deferred?
Auth. Proposal: a pluggable authenticator trait with no built-in credentials, so embedders wire in their own, plus a clearly-labelled insecure dev default (or none at all). Reuse the existing use_tls and endpoint-customization hooks (feat: Cluster RPC customisations to support TLS and custom headers #1400). Anything resembling admin/password should not come back.
Lifecycle and cleanup. Map CancelFlightInfo/CancelQuery → CancelJob, connection close → RemoveSession, and result cleanup → CleanJobData. The old implementation cleaned up none of this.
Write path.CommandStatementUpdate (DDL/DML/INSERT INTO) — in scope for v1 or deferred?
Each phase should be independently reviewable and independently useful:
GetFlightInfo/DoGet for CommandStatementQuery against a scheduler-configured catalog, endpoints resolved via the Flight proxy, with a Rust FlightSqlServiceClient integration test.
Session + handshake + pluggable auth; cancellation and cleanup wired to CancelJob/RemoveSession/CleanJobData.
Metadata RPCs sufficient for the Arrow Flight JDBC driver to connect and introspect, sourced from the real catalog.
Prepared statements with parameter binding; PollFlightInfo for long queries.
Docs (a replacement for the removed docs/source/user-guide/flightsql.md) and a docker-compose example that works from outside the container network — the scenario Clarify usage of advertise_flight_sql_endpoint #1349 reported as broken.
Definition of done (proposed)
A third-party client works end-to-end: Arrow Flight SQL JDBC driver and adbc/pyarrow both connect, introspect, and run TPC-H queries against a multi-executor cluster.
Results are fetchable from outside the cluster network without leaking executor addresses to the client.
Integration tests in CI covering the query, metadata, prepared-statement, and cancellation paths — non-negotiable this time, given the old implementation shipped with none.
Resurrect flight_sql.rs from 45.0.0. Cheapest start, and it's a genuinely useful reference for statement handling and endpoint construction (git show 559bcf29^:ballista/scheduler/src/flight_sql.rs). But it's coupled to concrete scheduler generics, predates the Flight proxy, targets an arrow-flight API that has drifted several major versions, and carries the incompleteness that got it removed. Reference, don't restore.
Reuse an existing DataFusion Flight SQL server implementation (e.g. from datafusion-contrib, which Remove flight-sql from ballista in 46.0.0 #1227 floated as the home for the old code) and adapt it to submit through Ballista instead of executing locally. Worth evaluating seriously before writing a new server from scratch — I haven't assessed how well its abstractions fit distributed submission and multi-endpoint results. If it fits, most of this issue reduces to a QueryBackend implementation.
Do nothing; wait for Spark Connect ([DISCUSSION] Add support for Spark Connect protocol #2249). Spark Connect is a much larger surface and reaches a different audience. It doesn't serve JDBC/ODBC/BI tools, so it isn't a substitute.
Client-side Flight SQL gateway (a separate process holding a DataFusion SessionContext that forwards to Ballista). Keeps the scheduler clean, but duplicates session and catalog state and adds a hop; probably strictly worse than a mountable frontend once [DISCUSSION] Add support for Spark Connect protocol #2249 exists.
I'm happy to help shepherd this, but I'd rather agree the layering and the open questions above first, particularly (1) the catalog model and (3) the driver-driven definition of the metadata surface.
Is your feature request related to a problem or challenge? Please describe what you are trying to do.
Ballista has no way for a generic SQL client to connect to it. Every client must be a Rust program that builds a DataFusion
LogicalPlan, serializes it, and speaksSchedulerGrpc. That rules out JDBC/ODBC tooling, BI tools, DBeaver, Python (adbc/pyarrow), and anything else that expects to send SQL text and get Arrow back.Flight SQL used to fill that gap, and was removed in 46.0.0 (#1227, #1228). It's worth being precise about why, because it wasn't "Flight SQL is a bad fit for Ballista":
FlightSqlServicemethods inballista/scheduler/src/flight_sql.rs, well over half wereStatus::unimplemented, includingCommandGetSqlInfo— which carried the comment// TODO: implement for FlightSQL JDBC to work. Catalog metadata (GetCatalogs,GetDbSchemas,GetTablesas a real RPC,GetXdbcTypeInfo, keys) was stubbed, so driver-level introspection didn't work.SchedulerServer<LogicalPlanNode, PhysicalPlanNode>, reached intoserver.state.config, faked catalog responses by smuggling well-known strings ("get_flight_info_tables") through thejob_idfield of aFetchPartitionticket, and carried its own inline Flight proxy.if user != "admin" || pass != "password", Basic-only.grep -c '#\[test\]' <old file>→ 0).So the removal was right, and reviving that file as-is would be wrong. But the capability is still valuable, and @avantgardnerio made the point directly in #2249: "using this to restore FlightSQL could add a significant audience as well." This issue is to design a new implementation properly.
Why now
Several things have landed since 46.0.0 that make this materially easier than it was in 2023–2025:
ballista/scheduler/src/flight_proxy_service.rs), plusFlightProxy::Local/Externaladvertisement inGetJobStatusResult. This fixes the old implementation's worst structural problem: it builtFlightEndpointlocations out of per-executorhost:port(andmake_local_fiepshardcoded127.0.0.1:50050behind a// TODO: use advertise host), which is exactly why it broke behind NAT, Docker, Kubernetes, and load balancers (wrong location in FlightEndpoint causing not support Flight SQL JDBC Driver #1012, Clarify usage of advertise_flight_sql_endpoint #1349). A new implementation can hand clients one stable address and let the scheduler fan out to executors.SessionContextbuilt by a pluggableSessionBuilder(ballista/scheduler/src/state/session_manager.rs:48-85). Server-side SQL planning needs a catalog, and this is an existing, documented extension point for supplying one — rather than the old UX of "re-register your tables withCREATE EXTERNAL TABLEon every new connection."CreateUpdateSession,RemoveSession,ExecuteQueryPush(streaming status, so no polling loop),GetJobMetrics,CancelJob,CleanJobData.arrow-flight58.4 is already a workspace dependency withflight-sql-experimentalenabled (Cargo.toml:41), so this adds no new dependency — that feature flag is currently enabled for nothing.Relationship to #2249
This should be built on top of the pluggable-frontend layering @phillipleblanc proposed in #2249, not alongside it:
Flight SQL is arguably the best first consumer of that abstraction, and a useful forcing function for it:
If #2249's
QueryBackend/GrpcQueryFrontendtraits land first, this issue implements aFlightSqlFrontendagainst them. If Flight SQL work starts first, it should define its needs in those terms so the traits are shaped by two real consumers instead of one. Concretely, this frontend must not depend onSchedulerServerinternals the way the old one did — that coupling is what made it unmaintainable.Describe the solution you'd like
Placement
A new optional crate,
ballista/flight-sql, exposed through a non-defaultflight-sqlfeature on the scheduler (and mountable by embedders into their own Tonic server per #2249). Rationale: it keeps the scheduler's default dependency surface unchanged, and it means the thing can be maintained — or deprecated — independently. That directly answers the maintainability objection in #1227.Core flow
GetFlightInfo(CommandStatementQuery):session_id;CreateUpdateSessionif new.SessionContext→LogicalPlan.ExecuteQuery), obtaining ajob_id.ExecuteQueryPush's status stream rather than a poll loop.FlightEndpoints whose tickets are the existingAction::FetchPartitionprotobuf, and whoselocationis resolved through the existingFlightProxylogic — scheduler/LB by default, direct-to-executor only when explicitly configured.DoGet(ticket)is then satisfied by the existing proxy path inflight_proxy_service.rs, which already decodesAction::FetchPartitionand forwards to the owning executor. Ideally the frontend reuses that service rather than re-implementing a proxy inline, as the old code did.Open design decisions
These are the things I'd like input on; I don't think any of them should be settled unilaterally.
SessionBuilder— my preference, reuses an existing extension point; (c) scheduler-side configured tables/object stores. Also: shouldCommandStatementSubstraitPlanbe supported for plan-shaped clients, givensubstraitis already an optional scheduler feature?GetFlightInfountil the job completes (old behaviour) will hit client and proxy timeouts for TPC-H-scale queries.PollFlightInfoexists inarrow-flight58 (the old impl stubbed it) and is designed for exactly this. Do we require it, and do the drivers we care about actually use it?CommandGetSqlInfoandGetXdbcTypeInfo, which the old implementation never had — and without which the Arrow Flight JDBC driver doesn't work. Proposal: define the set by "the driver connects and introspects," not by "the trait method exists," and drive catalog answers from the session's real DataFusion catalog rather than hand-builtRecordBatches.DashMap<Uuid, LogicalPlan>only shed entries on explicit close, so any client that disconnected leaked. Parameter binding (DoPutPreparedStatementQuery,parameter_schema) was a// TODO: parameters; is bound-parameter support in scope for v1 or explicitly deferred?use_tlsand endpoint-customization hooks (feat: Cluster RPC customisations to support TLS and custom headers #1400). Anything resemblingadmin/passwordshould not come back.CancelFlightInfo/CancelQuery→CancelJob, connection close →RemoveSession, and result cleanup →CleanJobData. The old implementation cleaned up none of this.CommandStatementUpdate(DDL/DML/INSERT INTO) — in scope for v1 or deferred?advertise_flight_sql_endpointcurrently has nothing to do with Flight SQL — it's the plain-Arrow-Flight result-proxy address, misnamed because it predates the Flight SQL removal (see Replace the advertise_flight_sql_endpoint empty-string sentinel with an explicit flag #2281 and my comment on feat: embedded flight proxy explicit config entry #2288). Either this frontend gives that name a legitimate meaning again, or the existing knob should be renamed and the Flight SQL frontend gets its own. Worth settling here rather than accumulating a third overlapping option.Suggested phasing
Each phase should be independently reviewable and independently useful:
GetFlightInfo/DoGetforCommandStatementQueryagainst a scheduler-configured catalog, endpoints resolved via the Flight proxy, with a RustFlightSqlServiceClientintegration test.CancelJob/RemoveSession/CleanJobData.PollFlightInfofor long queries.docs/source/user-guide/flightsql.md) and adocker-composeexample that works from outside the container network — the scenario Clarify usage of advertise_flight_sql_endpoint #1349 reported as broken.Definition of done (proposed)
adbc/pyarrowboth connect, introspect, and run TPC-H queries against a multi-executor cluster.SchedulerServerinternals.Describe alternatives you've considered
flight_sql.rsfrom 45.0.0. Cheapest start, and it's a genuinely useful reference for statement handling and endpoint construction (git show 559bcf29^:ballista/scheduler/src/flight_sql.rs). But it's coupled to concrete scheduler generics, predates the Flight proxy, targets anarrow-flightAPI that has drifted several major versions, and carries the incompleteness that got it removed. Reference, don't restore.datafusion-contrib, which Removeflight-sqlfrom ballista in 46.0.0 #1227 floated as the home for the old code) and adapt it to submit through Ballista instead of executing locally. Worth evaluating seriously before writing a new server from scratch — I haven't assessed how well its abstractions fit distributed submission and multi-endpoint results. If it fits, most of this issue reduces to aQueryBackendimplementation.SessionContextthat forwards to Ballista). Keeps the scheduler clean, but duplicates session and catalog state and adds a hop; probably strictly worse than a mountable frontend once [DISCUSSION] Add support for Spark Connect protocol #2249 exists.Additional context
flight-sqlfrom ballista in 46.0.0 #1227 → feat: remove flight-sql from scheduler #1228 (559bcf2, released 46.0.0). Also removeddocs/source/user-guide/flightsql.md.I'm happy to help shepherd this, but I'd rather agree the layering and the open questions above first, particularly (1) the catalog model and (3) the driver-driven definition of the metadata surface.