use std::{
collections::HashMap,
pin::Pin,
sync::{Arc, LazyLock, Mutex},
};
use arrow::ipc::writer::IpcWriteOptions;
use arrow_array::RecordBatch;
use arrow_flight::{
FlightData, FlightDescriptor, FlightEndpoint, FlightInfo, HandshakeRequest, HandshakeResponse,
IpcMessage, SchemaAsIpc, Ticket,
decode::FlightRecordBatchStream,
encode::FlightDataEncoderBuilder,
error::FlightError,
sql::{
ActionClosePreparedStatementRequest, ActionCreatePreparedStatementRequest,
ActionCreatePreparedStatementResult, CommandGetCatalogs, CommandGetDbSchemas,
CommandGetSqlInfo, CommandGetTableTypes, CommandGetTables, CommandPreparedStatementQuery,
CommandStatementQuery, CommandStatementUpdate, DoPutPreparedStatementResult,
ProstMessageExt, SqlInfo, TicketStatementQuery,
metadata::{SqlInfoData, SqlInfoDataBuilder},
server::{FlightSqlService, PeekableFlightDataStream},
},
};
use futures::{Stream, TryStreamExt};
use graphar_falkordb::FalkorDbLoader;
use prost::Message;
use tonic::{Request, Response, Status, Streaming};
use crate::{
auth::{AuthConfig, Identity},
error::FlightSqlError,
falkor::FalkorExecutor,
registry::{SchemaRegistry, parse_select_from},
};
static SQL_INFO: LazyLock<SqlInfoData> = LazyLock::new(|| {
let mut b = SqlInfoDataBuilder::new();
b.append(
SqlInfo::FlightSqlServerName,
"knut graphar-flight (Cypher over FalkorDB)",
);
b.append(SqlInfo::FlightSqlServerVersion, env!("CARGO_PKG_VERSION"));
b.append(SqlInfo::FlightSqlServerArrowVersion, "1.3");
b.append(SqlInfo::FlightSqlServerReadOnly, true);
b.append(SqlInfo::SqlDdlCatalog, false);
b.append(SqlInfo::SqlIdentifierQuoteChar, "\"");
b.build().expect("static SqlInfoData builds")
});
type BoxStream = Pin<Box<dyn Stream<Item = Result<FlightData, Status>> + Send + 'static>>;
const SCHEMA_CACHE_CAP: usize = 16;
#[derive(Clone, Default)]
struct SchemaCache {
inner: Arc<Mutex<HashMap<String, RecordBatch>>>,
}
impl SchemaCache {
fn insert(&self, cypher: String, batch: RecordBatch) {
let mut map = self.inner.lock().expect("schema cache mutex");
if map.len() >= SCHEMA_CACHE_CAP && !map.contains_key(&cypher) {
if let Some(victim) = map.keys().next().cloned() {
map.remove(&victim);
}
}
map.insert(cypher, batch);
}
fn take(&self, cypher: &str) -> Option<RecordBatch> {
self.inner
.lock()
.expect("schema cache mutex")
.remove(cypher)
}
}
#[derive(Clone)]
pub struct CypherFlightService {
executor: FalkorExecutor,
registry: Arc<SchemaRegistry>,
redis_url: Arc<String>,
graph: Arc<String>,
auth: Arc<AuthConfig>,
schema_cache: SchemaCache,
}
impl CypherFlightService {
pub async fn connect(
redis_url: impl Into<String>,
graph: impl Into<String>,
registry: Arc<SchemaRegistry>,
) -> crate::error::Result<Self> {
let redis_url = Arc::new(redis_url.into());
let graph = Arc::new(graph.into());
let executor = FalkorExecutor::connect(redis_url.as_str(), graph.as_str()).await?;
Ok(Self {
executor,
registry,
redis_url,
graph,
auth: Arc::new(AuthConfig::default()),
schema_cache: SchemaCache::default(),
})
}
pub fn with_auth(mut self, auth: Arc<AuthConfig>) -> Self {
self.auth = auth;
self
}
async fn new_loader(&self) -> crate::error::Result<FalkorDbLoader> {
FalkorDbLoader::connect(&self.redis_url, self.graph.as_str())
.await
.map_err(FlightSqlError::from)
}
fn target_cypher(&self, incoming: &str) -> String {
if let Some(query) = self.registry.resolve_table(incoming) {
return query;
}
if let Some(ident) = parse_select_from(incoming)
&& let Some(query) = self.registry.resolve_table(&ident)
{
return query;
}
incoming.to_string()
}
async fn schema_for(&self, cypher: &str) -> Result<arrow_schema::SchemaRef, Status> {
if let Some(s) = self.registry.get(cypher) {
Ok(s)
} else {
let batch = self
.executor
.clone()
.query_auto(cypher)
.await
.map_err(|e| Status::internal(e.to_string()))?;
let schema = batch.schema();
self.schema_cache.insert(cypher.to_string(), batch);
Ok(schema)
}
}
fn authz_name(&self, cypher: &str) -> String {
self.registry
.name_of(cypher)
.unwrap_or_else(|| cypher.to_string())
}
fn authorize<T>(&self, request: &Request<T>, cypher: &str) -> Result<(), Status> {
let identity = request
.extensions()
.get::<Identity>()
.cloned()
.unwrap_or(Identity::Anonymous);
self.auth.authorize(&identity, &self.authz_name(cypher))
}
async fn execute_cypher_stream(&self, cypher: &str) -> Result<Response<BoxStream>, Status> {
let mut exec = self.executor.clone();
let batch = if let Some(schema) = self.registry.get(cypher) {
exec.query(cypher, &schema)
.await
.map_err(|e| Status::internal(e.to_string()))?
} else if let Some(cached) = self.schema_cache.take(cypher) {
cached
} else {
exec.query_auto(cypher)
.await
.map_err(|e| Status::internal(e.to_string()))?
};
let schema = batch.schema();
let stream: BoxStream = Box::pin(
FlightDataEncoderBuilder::new()
.with_schema(schema)
.build(futures::stream::iter([Ok(batch)]))
.map_err(|e: FlightError| Status::internal(e.to_string())),
);
Ok(Response::new(stream))
}
}
#[tonic::async_trait]
impl FlightSqlService for CypherFlightService {
type FlightService = CypherFlightService;
async fn register_sql_info(&self, _id: i32, _result: &SqlInfo) {}
async fn do_handshake(
&self,
request: Request<Streaming<HandshakeRequest>>,
) -> Result<
Response<Pin<Box<dyn Stream<Item = Result<HandshakeResponse, Status>> + Send>>>,
Status,
> {
let header = request
.metadata()
.get("authorization")
.and_then(|v| v.to_str().ok());
self.auth.check_header(header)?;
let token = self.auth.issued_token().unwrap_or("").to_string();
let response_msg = HandshakeResponse {
protocol_version: 0,
payload: token.clone().into(),
};
let output = futures::stream::once(async move { Ok(response_msg) });
let mut response: Response<Pin<Box<dyn Stream<Item = _> + Send>>> =
Response::new(Box::pin(output));
if !token.is_empty() {
let bearer = format!("Bearer {token}")
.parse()
.map_err(|_| Status::internal("token not header-safe"))?;
response.metadata_mut().insert("authorization", bearer);
}
Ok(response)
}
async fn get_flight_info_statement(
&self,
query: CommandStatementQuery,
request: Request<arrow_flight::FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let cypher = self.target_cypher(&query.query);
self.authorize(&request, &cypher)?;
let schema = match self.schema_for(&cypher).await {
Ok(s) => {
crate::functional_status(
"graphar-flight/get_flight_info_statement",
"schema_resolved",
true,
&cypher,
);
s
}
Err(e) => {
crate::functional_status(
"graphar-flight/get_flight_info_statement",
"schema_resolved",
false,
&cypher,
);
return Err(e);
}
};
let ticket_bytes = TicketStatementQuery {
statement_handle: cypher.into_bytes().into(),
}
.encode_to_vec();
let info = FlightInfo::new()
.try_with_schema(schema.as_ref())
.map_err(|e| Status::internal(e.to_string()))?
.with_endpoint(FlightEndpoint::new().with_ticket(Ticket::new(ticket_bytes)))
.with_total_records(-1)
.with_total_bytes(-1)
.with_ordered(false);
Ok(Response::new(info))
}
async fn do_get_statement(
&self,
ticket: TicketStatementQuery,
request: Request<Ticket>,
) -> Result<Response<BoxStream>, Status> {
let query = String::from_utf8(ticket.statement_handle.to_vec())
.map_err(|e| Status::invalid_argument(format!("invalid ticket: {e}")))?;
self.authorize(&request, &query)?;
let result = self.execute_cypher_stream(&query).await;
crate::functional_status(
"graphar-flight/do_get_statement",
"cypher_stream",
result.is_ok(),
&query,
);
result
}
async fn do_put_statement_update(
&self,
cmd: CommandStatementUpdate,
request: Request<PeekableFlightDataStream>,
) -> Result<i64, Status> {
let directive = cmd.query.trim().to_string();
let action =
parse_put_directive(&directive).map_err(|e| Status::invalid_argument(e.to_string()))?;
let raw = request.into_inner().into_inner(); let batches = FlightRecordBatchStream::new_from_flight_data(
raw.map_err(|s| FlightError::Tonic(Box::new(s))),
)
.try_collect::<Vec<_>>()
.await
.map_err(|e| Status::internal(e.to_string()))?;
if batches.is_empty() {
return Ok(0);
}
let mut loader = self
.new_loader()
.await
.map_err(|e| Status::internal(e.to_string()))?;
let n = match action {
PutAction::Vertices { label } => loader
.insert_vertices(&label, &batches)
.await
.map_err(|e| Status::internal(e.to_string()))?,
PutAction::Edges {
src,
edge_type,
dst,
} => loader
.insert_edges(&src, &edge_type, &dst, &batches)
.await
.map_err(|e| Status::internal(e.to_string()))?,
};
Ok(n as i64)
}
async fn get_flight_info_catalogs(
&self,
query: CommandGetCatalogs,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let descriptor = request.into_inner();
let ticket = Ticket::new(query.as_any().encode_to_vec());
metadata_info(descriptor, ticket, query.into_builder().schema())
}
async fn get_flight_info_schemas(
&self,
query: CommandGetDbSchemas,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let descriptor = request.into_inner();
let ticket = Ticket::new(query.as_any().encode_to_vec());
metadata_info(descriptor, ticket, query.into_builder().schema())
}
async fn get_flight_info_tables(
&self,
query: CommandGetTables,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let descriptor = request.into_inner();
let ticket = Ticket::new(query.as_any().encode_to_vec());
metadata_info(descriptor, ticket, query.into_builder().schema())
}
async fn get_flight_info_table_types(
&self,
query: CommandGetTableTypes,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let descriptor = request.into_inner();
let ticket = Ticket::new(query.as_any().encode_to_vec());
metadata_info(descriptor, ticket, query.into_builder().schema())
}
async fn get_flight_info_sql_info(
&self,
query: CommandGetSqlInfo,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let descriptor = request.into_inner();
let ticket = Ticket::new(query.as_any().encode_to_vec());
metadata_info(descriptor, ticket, query.into_builder(&SQL_INFO).schema())
}
async fn do_get_catalogs(
&self,
query: CommandGetCatalogs,
_request: Request<Ticket>,
) -> Result<Response<BoxStream>, Status> {
let mut builder = query.into_builder();
builder.append(self.graph.as_str());
let schema = builder.schema();
Ok(Response::new(metadata_stream(schema, builder.build())))
}
async fn do_get_schemas(
&self,
query: CommandGetDbSchemas,
_request: Request<Ticket>,
) -> Result<Response<BoxStream>, Status> {
let mut builder = query.into_builder();
builder.append(self.graph.as_str(), DB_SCHEMA);
let schema = builder.schema();
Ok(Response::new(metadata_stream(schema, builder.build())))
}
async fn do_get_tables(
&self,
query: CommandGetTables,
_request: Request<Ticket>,
) -> Result<Response<BoxStream>, Status> {
let mut builder = query.into_builder();
for (name, table_schema) in self.registry.tables() {
builder
.append(
self.graph.as_str(),
DB_SCHEMA,
&name,
"TABLE",
table_schema.as_ref(),
)
.map_err(|e| Status::internal(e.to_string()))?;
}
let schema = builder.schema();
Ok(Response::new(metadata_stream(schema, builder.build())))
}
async fn do_get_table_types(
&self,
query: CommandGetTableTypes,
_request: Request<Ticket>,
) -> Result<Response<BoxStream>, Status> {
let mut builder = query.into_builder();
builder.append("TABLE");
let schema = builder.schema();
Ok(Response::new(metadata_stream(schema, builder.build())))
}
async fn do_get_sql_info(
&self,
query: CommandGetSqlInfo,
_request: Request<Ticket>,
) -> Result<Response<BoxStream>, Status> {
let builder = query.into_builder(&SQL_INFO);
let schema = builder.schema();
Ok(Response::new(metadata_stream(schema, builder.build())))
}
async fn do_action_create_prepared_statement(
&self,
query: ActionCreatePreparedStatementRequest,
request: Request<arrow_flight::Action>,
) -> Result<ActionCreatePreparedStatementResult, Status> {
let cypher = self.target_cypher(&query.query);
self.authorize(&request, &cypher)?;
let schema = self.schema_for(&cypher).await?;
let IpcMessage(schema_bytes) =
SchemaAsIpc::new(schema.as_ref(), &IpcWriteOptions::default())
.try_into()
.map_err(|e: arrow::error::ArrowError| Status::internal(e.to_string()))?;
Ok(ActionCreatePreparedStatementResult {
prepared_statement_handle: cypher.into_bytes().into(),
dataset_schema: schema_bytes,
parameter_schema: Default::default(), })
}
async fn do_action_close_prepared_statement(
&self,
_query: ActionClosePreparedStatementRequest,
_request: Request<arrow_flight::Action>,
) -> Result<(), Status> {
Ok(()) }
async fn get_flight_info_prepared_statement(
&self,
cmd: CommandPreparedStatementQuery,
request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
let cypher = String::from_utf8(cmd.prepared_statement_handle.to_vec())
.map_err(|e| Status::invalid_argument(format!("invalid handle: {e}")))?;
self.authorize(&request, &cypher)?;
let schema = self.schema_for(&cypher).await?;
let ticket_bytes = TicketStatementQuery {
statement_handle: cypher.into_bytes().into(),
}
.encode_to_vec();
let info = FlightInfo::new()
.try_with_schema(schema.as_ref())
.map_err(|e| Status::internal(e.to_string()))?
.with_endpoint(FlightEndpoint::new().with_ticket(Ticket::new(ticket_bytes)))
.with_total_records(-1)
.with_total_bytes(-1)
.with_ordered(false);
Ok(Response::new(info))
}
async fn do_get_prepared_statement(
&self,
cmd: CommandPreparedStatementQuery,
request: Request<Ticket>,
) -> Result<Response<BoxStream>, Status> {
let cypher = String::from_utf8(cmd.prepared_statement_handle.to_vec())
.map_err(|e| Status::invalid_argument(format!("invalid handle: {e}")))?;
self.authorize(&request, &cypher)?;
self.execute_cypher_stream(&cypher).await
}
async fn do_put_prepared_statement_query(
&self,
query: CommandPreparedStatementQuery,
_request: Request<PeekableFlightDataStream>,
) -> Result<DoPutPreparedStatementResult, Status> {
Ok(DoPutPreparedStatementResult {
prepared_statement_handle: Some(query.prepared_statement_handle),
})
}
}
const DB_SCHEMA: &str = "public";
#[cfg(test)]
pub(crate) fn sql_info_batch() -> arrow_array::RecordBatch {
CommandGetSqlInfo::default()
.into_builder(&SQL_INFO)
.build()
.expect("sql info batch builds")
}
fn metadata_info(
descriptor: FlightDescriptor,
ticket: Ticket,
schema: arrow_schema::SchemaRef,
) -> Result<Response<FlightInfo>, Status> {
let info = FlightInfo::new()
.try_with_schema(schema.as_ref())
.map_err(|e| Status::internal(e.to_string()))?
.with_endpoint(FlightEndpoint::new().with_ticket(ticket))
.with_descriptor(descriptor);
Ok(Response::new(info))
}
fn metadata_stream(
schema: arrow_schema::SchemaRef,
batch: std::result::Result<arrow_array::RecordBatch, FlightError>,
) -> BoxStream {
Box::pin(
FlightDataEncoderBuilder::new()
.with_schema(schema)
.build(futures::stream::once(async move { batch }))
.map_err(|e: FlightError| Status::internal(e.to_string())),
)
}
enum PutAction {
Vertices {
label: String,
},
Edges {
src: String,
edge_type: String,
dst: String,
},
}
fn parse_put_directive(s: &str) -> Result<PutAction, FlightSqlError> {
if let Some(rest) = s.strip_prefix("VERTICES:") {
return Ok(PutAction::Vertices {
label: rest.to_string(),
});
}
if let Some(rest) = s.strip_prefix("EDGES:") {
let parts: Vec<&str> = rest.splitn(3, ':').collect();
if parts.len() == 3 {
return Ok(PutAction::Edges {
src: parts[0].to_string(),
edge_type: parts[1].to_string(),
dst: parts[2].to_string(),
});
}
}
Err(FlightSqlError::UnknownPutCommand(s.to_string()))
}