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::{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;
use crate::session::MeterStore;
#[derive(Clone)]
pub struct FlightSqlServer {
store: MeterStore,
}
impl std::fmt::Debug for FlightSqlServer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlightSqlServer")
.field("table", &self.store.resolved_table())
.finish_non_exhaustive()
}
}
impl FlightSqlServer {
pub fn new(store: MeterStore) -> Self {
Self { store }
}
pub fn into_service(self) -> FlightServiceServer<Self> {
FlightServiceServer::new(self)
}
fn with_provenance(
schema: SchemaRef,
watermark: crate::watermark::TieringWatermark,
tiers: &[crate::watermark::Tier],
mode: crate::planner::ReadMode,
) -> SchemaRef {
let tiers = tiers
.iter()
.map(|t| format!("{t:?}").to_lowercase())
.collect::<Vec<_>>()
.join(",");
let metadata = std::collections::HashMap::from([
(
crate::watermark::WATERMARK_PROPERTY.to_string(),
watermark.to_string(),
),
("meterstore.tiers_scanned".to_string(), tiers),
("meterstore.read_mode".to_string(), format!("{mode:?}")),
]);
Arc::new(schema.as_ref().clone().with_metadata(metadata))
}
async fn describe(&self, sql: &str) -> Result<SchemaRef, Status> {
debug!(%sql, "flight sql describe");
let described = self
.store
.describe(sql)
.await
.map_err(|e| Status::invalid_argument(format!("query failed: {e}")))?;
Ok(Self::with_provenance(
described.schema(),
described.watermark(),
described.tiers_scanned(),
described.read_mode(),
))
}
async fn run(&self, sql: &str) -> Result<(SchemaRef, Vec<RecordBatch>), Status> {
debug!(%sql, "flight sql query");
let result = self
.store
.query(sql)
.await
.map_err(|e| Status::invalid_argument(format!("query failed: {e}")))?;
let schema = Self::with_provenance(
result.schema(),
result.watermark(),
result.tiers_scanned(),
result.read_mode(),
);
Ok((schema, result.into_batches()))
}
fn stream(
schema: SchemaRef,
batches: Vec<RecordBatch>,
) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
let batches: Vec<RecordBatch> = batches
.into_iter()
.map(|b| RecordBatch::try_new(schema.clone(), b.columns().to_vec()))
.collect::<std::result::Result<_, _>>()
.map_err(|e| Status::internal(format!("attaching provenance to a batch: {e}")))?;
let flight = FlightDataEncoderBuilder::new()
.with_schema(schema)
.build(stream::iter(batches.into_iter().map(Ok)))
.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}")))?;
let (schema, batches) = self.run(&sql).await?;
Self::stream(schema, batches)
}
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}")))?;
let (schema, batches) = self.run(&sql).await?;
Self::stream(schema, batches)
}
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> {
let (schema, batches) = self
.run(
"SELECT DISTINCT table_catalog AS catalog_name FROM information_schema.tables \
ORDER BY 1",
)
.await?;
Self::stream(schema, batches)
}
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> {
let (schema, batches) = self.run(SCHEMAS_SQL).await?;
Self::stream(schema, batches)
}
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> {
let (schema, batches) = self.run(TABLES_SQL).await?;
Self::stream(schema, batches)
}
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> {
let (schema, batches) = self.run(TABLE_TYPES_SQL).await?;
Self::stream(schema, batches)
}
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");
}
}