use std::pin::Pin;
use std::sync::Arc;
use arrow_flight::encode::FlightDataEncoderBuilder;
use arrow_flight::flight_service_server::{FlightService, FlightServiceServer};
use arrow_flight::sql::server::{FlightSqlService, PeekableFlightDataStream};
use arrow_flight::sql::{
ActionClosePreparedStatementRequest, ActionCreatePreparedStatementRequest,
ActionCreatePreparedStatementResult, CommandGetCatalogs, CommandGetDbSchemas,
CommandGetTableTypes, CommandGetTables, CommandPreparedStatementQuery,
CommandPreparedStatementUpdate, CommandStatementQuery, CommandStatementUpdate, ProstMessageExt,
SqlInfo, TicketStatementQuery,
};
use arrow_flight::{
Action, FlightDescriptor, FlightEndpoint, FlightInfo, HandshakeRequest, HandshakeResponse,
IpcMessage, SchemaAsIpc, Ticket,
};
use futures::{StreamExt, TryStreamExt, stream};
use prost::Message;
use tonic::{Request, Response, Status, Streaming};
use tracing::{debug, info};
use crate::arrow::array::RecordBatch;
use crate::arrow::datatypes::SchemaRef;
#[allow(unused_imports)]
use crate::session::MeterStore;
use crate::session::SqlSurface;
#[derive(Clone)]
pub struct FlightSqlServer {
surface: Arc<dyn SqlSurface>,
}
impl std::fmt::Debug for FlightSqlServer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlightSqlServer")
.field("serving", &self.surface.label())
.finish_non_exhaustive()
}
}
impl FlightSqlServer {
pub fn new(surface: impl SqlSurface) -> Self {
Self {
surface: Arc::new(surface),
}
}
pub fn shared(surface: Arc<dyn SqlSurface>) -> Self {
Self { surface }
}
pub fn into_service(self) -> FlightServiceServer<Self> {
FlightServiceServer::new(self)
}
fn with_provenance(described: &crate::session::QueryDescription) -> SchemaRef {
let tiers = described
.tiers_scanned()
.iter()
.map(|t| format!("{t:?}").to_lowercase())
.collect::<Vec<_>>()
.join(",");
let watermarks = described
.watermarks()
.iter()
.map(|(table, at)| format!("{table}={at}"))
.collect::<Vec<_>>()
.join(",");
let metadata = std::collections::HashMap::from([
(
crate::watermark::WATERMARK_PROPERTY.to_string(),
described.watermark().to_string(),
),
("meterstore.watermarks".to_string(), watermarks),
("meterstore.tiers_scanned".to_string(), tiers),
(
"meterstore.read_mode".to_string(),
format!("{:?}", described.read_mode()),
),
]);
Arc::new(described.schema().as_ref().clone().with_metadata(metadata))
}
async fn describe(&self, sql: &str) -> Result<SchemaRef, Status> {
debug!(%sql, "flight sql describe");
let described = self
.surface
.describe_sql(sql)
.await
.map_err(|e| Status::invalid_argument(format!("query failed: {e}")))?;
Ok(Self::with_provenance(&described))
}
async fn respond(
&self,
sql: &str,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
debug!(%sql, "flight sql query");
let (described, rows) = self
.surface
.stream_sql(sql, Vec::new())
.await
.map_err(|e| Status::invalid_argument(format!("query failed: {e}")))?;
let schema = Self::with_provenance(&described);
let stamped = schema.clone();
let batches = rows.map(move |batch| {
let batch =
batch.map_err(|e| arrow_flight::error::FlightError::ExternalError(Box::new(e)))?;
RecordBatch::try_new(stamped.clone(), batch.columns().to_vec())
.map_err(arrow_flight::error::FlightError::Arrow)
});
let flight = FlightDataEncoderBuilder::new()
.with_schema(schema)
.build(batches)
.map_err(|e| Status::internal(format!("encoding flight data: {e}")));
Ok(Response::new(Box::pin(flight)))
}
async fn info_for(
&self,
sql: String,
descriptor: FlightDescriptor,
) -> Result<Response<FlightInfo>, Status> {
let schema = self.describe(&sql).await?;
let ticket = Ticket::new(
TicketStatementQuery {
statement_handle: sql.into_bytes().into(),
}
.as_any()
.encode_to_vec(),
);
let info = FlightInfo::new()
.try_with_schema(&schema)
.map_err(|e| Status::internal(format!("schema: {e}")))?
.with_endpoint(FlightEndpoint::new().with_ticket(ticket))
.with_descriptor(descriptor);
Ok(Response::new(info))
}
fn read_only(operation: &str) -> Status {
Status::permission_denied(format!(
"{operation} is not available over Flight SQL: this endpoint is read-only. \
A write here would bypass MeterStore's tier routing — a correction for an \
already-archived interval would land in PostgreSQL below the watermark, where \
no query reads it — and the subject-reference check that stops a replay \
re-linking an erased subject. Write through MeterStore::append."
))
}
}
#[tonic::async_trait]
impl FlightSqlService for FlightSqlServer {
type FlightService = Self;
async fn do_handshake(
&self,
_request: Request<Streaming<HandshakeRequest>>,
) -> Result<
Response<Pin<Box<dyn futures::Stream<Item = Result<HandshakeResponse, Status>> + Send>>>,
Status,
> {
let response = HandshakeResponse {
protocol_version: 0,
payload: Default::default(),
};
Ok(Response::new(Box::pin(stream::once(async move {
Ok(response)
}))))
}
async fn get_flight_info_statement(
&self,
query: CommandStatementQuery,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
self.info_for(query.query, request.into_inner()).await
}
async fn do_get_statement(
&self,
ticket: TicketStatementQuery,
_request: Request<Ticket>,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
let sql = String::from_utf8(ticket.statement_handle.to_vec())
.map_err(|e| Status::invalid_argument(format!("statement handle: {e}")))?;
self.respond(&sql).await
}
async fn do_action_create_prepared_statement(
&self,
query: ActionCreatePreparedStatementRequest,
_request: Request<Action>,
) -> Result<ActionCreatePreparedStatementResult, Status> {
let schema = self.describe(&query.query).await?;
let message: IpcMessage = SchemaAsIpc::new(&schema, &Default::default())
.try_into()
.map_err(|e| Status::internal(format!("schema: {e}")))?;
Ok(ActionCreatePreparedStatementResult {
prepared_statement_handle: query.query.into_bytes().into(),
dataset_schema: message.0,
parameter_schema: Default::default(),
})
}
async fn get_flight_info_prepared_statement(
&self,
query: CommandPreparedStatementQuery,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let sql = String::from_utf8(query.prepared_statement_handle.to_vec())
.map_err(|e| Status::invalid_argument(format!("statement handle: {e}")))?;
self.info_for(sql, request.into_inner()).await
}
async fn do_get_prepared_statement(
&self,
query: CommandPreparedStatementQuery,
_request: Request<Ticket>,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
let sql = String::from_utf8(query.prepared_statement_handle.to_vec())
.map_err(|e| Status::invalid_argument(format!("statement handle: {e}")))?;
self.respond(&sql).await
}
async fn do_action_close_prepared_statement(
&self,
_query: ActionClosePreparedStatementRequest,
_request: Request<Action>,
) -> Result<(), Status> {
Ok(())
}
async fn get_flight_info_catalogs(
&self,
_query: CommandGetCatalogs,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
self.info_for(
"SELECT DISTINCT table_catalog AS catalog_name FROM information_schema.tables \
ORDER BY 1"
.to_string(),
request.into_inner(),
)
.await
}
async fn do_get_catalogs(
&self,
_query: CommandGetCatalogs,
_request: Request<Ticket>,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
self.respond(
"SELECT DISTINCT table_catalog AS catalog_name FROM information_schema.tables \
ORDER BY 1",
)
.await
}
async fn get_flight_info_schemas(
&self,
_query: CommandGetDbSchemas,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
self.info_for(SCHEMAS_SQL.to_string(), request.into_inner())
.await
}
async fn do_get_schemas(
&self,
_query: CommandGetDbSchemas,
_request: Request<Ticket>,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
self.respond(SCHEMAS_SQL).await
}
async fn get_flight_info_tables(
&self,
_query: CommandGetTables,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
self.info_for(TABLES_SQL.to_string(), request.into_inner())
.await
}
async fn do_get_tables(
&self,
_query: CommandGetTables,
_request: Request<Ticket>,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
self.respond(TABLES_SQL).await
}
async fn get_flight_info_table_types(
&self,
_query: CommandGetTableTypes,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
self.info_for(TABLE_TYPES_SQL.to_string(), request.into_inner())
.await
}
async fn do_get_table_types(
&self,
_query: CommandGetTableTypes,
_request: Request<Ticket>,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
self.respond(TABLE_TYPES_SQL).await
}
async fn do_put_statement_update(
&self,
_ticket: CommandStatementUpdate,
_request: Request<PeekableFlightDataStream>,
) -> Result<i64, Status> {
Err(Self::read_only("a SQL update"))
}
async fn do_put_prepared_statement_update(
&self,
_query: CommandPreparedStatementUpdate,
_request: Request<PeekableFlightDataStream>,
) -> Result<i64, Status> {
Err(Self::read_only("a prepared update"))
}
async fn do_action_begin_transaction(
&self,
_query: arrow_flight::sql::ActionBeginTransactionRequest,
_request: Request<Action>,
) -> Result<arrow_flight::sql::ActionBeginTransactionResult, Status> {
Err(Self::read_only("a transaction"))
}
async fn register_sql_info(&self, _id: i32, _result: &SqlInfo) {}
}
const SCHEMAS_SQL: &str = "SELECT DISTINCT table_catalog AS catalog_name, \
table_schema AS db_schema_name \
FROM information_schema.tables ORDER BY 1, 2";
const TABLES_SQL: &str = "SELECT table_catalog AS catalog_name, \
table_schema AS db_schema_name, \
table_name, table_type \
FROM information_schema.tables ORDER BY 1, 2, 3";
const TABLE_TYPES_SQL: &str =
"SELECT DISTINCT table_type FROM information_schema.tables ORDER BY 1";
pub async fn serve(
store: MeterStore,
address: std::net::SocketAddr,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!(%address, "flight sql listening");
tonic::transport::Server::builder()
.add_service(FlightSqlServer::new(store).into_service())
.serve(address)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_refusal_names_what_it_would_break() {
let status = FlightSqlServer::read_only("a SQL update");
assert_eq!(status.code(), tonic::Code::PermissionDenied);
let message = status.message();
assert!(message.contains("watermark"), "{message}");
assert!(message.contains("erased subject"), "{message}");
assert!(message.contains("MeterStore::append"), "{message}");
}
#[test]
fn the_browse_queries_name_the_resolved_table_first() {
assert!(TABLES_SQL.contains("ORDER BY"));
assert!("readings" < "readings_versions");
}
}