1use std::{
2 collections::HashMap,
3 pin::Pin,
4 sync::{Arc, LazyLock, Mutex},
5};
6
7use arrow::ipc::writer::IpcWriteOptions;
8use arrow_array::RecordBatch;
9use arrow_flight::{
10 FlightData, FlightDescriptor, FlightEndpoint, FlightInfo, HandshakeRequest, HandshakeResponse,
11 IpcMessage, SchemaAsIpc, Ticket,
12 decode::FlightRecordBatchStream,
13 encode::FlightDataEncoderBuilder,
14 error::FlightError,
15 sql::{
16 ActionClosePreparedStatementRequest, ActionCreatePreparedStatementRequest,
17 ActionCreatePreparedStatementResult, CommandGetCatalogs, CommandGetDbSchemas,
18 CommandGetSqlInfo, CommandGetTableTypes, CommandGetTables, CommandPreparedStatementQuery,
19 CommandStatementQuery, CommandStatementUpdate, DoPutPreparedStatementResult,
20 ProstMessageExt, SqlInfo, TicketStatementQuery,
21 metadata::{SqlInfoData, SqlInfoDataBuilder},
22 server::{FlightSqlService, PeekableFlightDataStream},
23 },
24};
25use futures::{Stream, TryStreamExt};
26use graphar_falkordb::FalkorDbLoader;
27use prost::Message;
28use tonic::{Request, Response, Status, Streaming};
29
30use crate::{
31 auth::{AuthConfig, Identity},
32 error::FlightSqlError,
33 falkor::FalkorExecutor,
34 registry::{SchemaRegistry, parse_select_from},
35};
36
37static SQL_INFO: LazyLock<SqlInfoData> = LazyLock::new(|| {
41 let mut b = SqlInfoDataBuilder::new();
42 b.append(
43 SqlInfo::FlightSqlServerName,
44 "knut graphar-flight (Cypher over FalkorDB)",
45 );
46 b.append(SqlInfo::FlightSqlServerVersion, env!("CARGO_PKG_VERSION"));
47 b.append(SqlInfo::FlightSqlServerArrowVersion, "1.3");
49 b.append(SqlInfo::FlightSqlServerReadOnly, true);
50 b.append(SqlInfo::SqlDdlCatalog, false);
52 b.append(SqlInfo::SqlIdentifierQuoteChar, "\"");
53 b.build().expect("static SqlInfoData builds")
54});
55
56type BoxStream = Pin<Box<dyn Stream<Item = Result<FlightData, Status>> + Send + 'static>>;
57
58const SCHEMA_CACHE_CAP: usize = 16;
64
65#[derive(Clone, Default)]
74struct SchemaCache {
75 inner: Arc<Mutex<HashMap<String, RecordBatch>>>,
76}
77
78impl SchemaCache {
79 fn insert(&self, cypher: String, batch: RecordBatch) {
83 let mut map = self.inner.lock().expect("schema cache mutex");
84 if map.len() >= SCHEMA_CACHE_CAP && !map.contains_key(&cypher) {
85 if let Some(victim) = map.keys().next().cloned() {
88 map.remove(&victim);
89 }
90 }
91 map.insert(cypher, batch);
92 }
93
94 fn take(&self, cypher: &str) -> Option<RecordBatch> {
96 self.inner
97 .lock()
98 .expect("schema cache mutex")
99 .remove(cypher)
100 }
101}
102
103#[derive(Clone)]
114pub struct CypherFlightService {
115 executor: FalkorExecutor,
116 registry: Arc<SchemaRegistry>,
117 redis_url: Arc<String>,
118 graph: Arc<String>,
119 auth: Arc<AuthConfig>,
120 schema_cache: SchemaCache,
122}
123
124impl CypherFlightService {
125 pub async fn connect(
126 redis_url: impl Into<String>,
127 graph: impl Into<String>,
128 registry: Arc<SchemaRegistry>,
129 ) -> crate::error::Result<Self> {
130 let redis_url = Arc::new(redis_url.into());
131 let graph = Arc::new(graph.into());
132 let executor = FalkorExecutor::connect(redis_url.as_str(), graph.as_str()).await?;
133 Ok(Self {
134 executor,
135 registry,
136 redis_url,
137 graph,
138 auth: Arc::new(AuthConfig::default()),
139 schema_cache: SchemaCache::default(),
140 })
141 }
142
143 pub fn with_auth(mut self, auth: Arc<AuthConfig>) -> Self {
146 self.auth = auth;
147 self
148 }
149
150 async fn new_loader(&self) -> crate::error::Result<FalkorDbLoader> {
151 FalkorDbLoader::connect(&self.redis_url, self.graph.as_str())
152 .await
153 .map_err(FlightSqlError::from)
154 }
155
156 fn target_cypher(&self, incoming: &str) -> String {
163 if let Some(query) = self.registry.resolve_table(incoming) {
164 return query;
165 }
166 if let Some(ident) = parse_select_from(incoming)
167 && let Some(query) = self.registry.resolve_table(&ident)
168 {
169 return query;
170 }
171 incoming.to_string()
172 }
173
174 async fn schema_for(&self, cypher: &str) -> Result<arrow_schema::SchemaRef, Status> {
182 if let Some(s) = self.registry.get(cypher) {
183 Ok(s)
184 } else {
185 let batch = self
186 .executor
187 .clone()
188 .query_auto(cypher)
189 .await
190 .map_err(|e| Status::internal(e.to_string()))?;
191 let schema = batch.schema();
192 self.schema_cache.insert(cypher.to_string(), batch);
193 Ok(schema)
194 }
195 }
196
197 fn authz_name(&self, cypher: &str) -> String {
201 self.registry
202 .name_of(cypher)
203 .unwrap_or_else(|| cypher.to_string())
204 }
205
206 fn authorize<T>(&self, request: &Request<T>, cypher: &str) -> Result<(), Status> {
212 let identity = request
213 .extensions()
214 .get::<Identity>()
215 .cloned()
216 .unwrap_or(Identity::Anonymous);
217 self.auth.authorize(&identity, &self.authz_name(cypher))
218 }
219
220 async fn execute_cypher_stream(&self, cypher: &str) -> Result<Response<BoxStream>, Status> {
230 let mut exec = self.executor.clone();
231 let batch = if let Some(schema) = self.registry.get(cypher) {
232 exec.query(cypher, &schema)
233 .await
234 .map_err(|e| Status::internal(e.to_string()))?
235 } else if let Some(cached) = self.schema_cache.take(cypher) {
236 cached
237 } else {
238 exec.query_auto(cypher)
239 .await
240 .map_err(|e| Status::internal(e.to_string()))?
241 };
242 let schema = batch.schema();
243 let stream: BoxStream = Box::pin(
244 FlightDataEncoderBuilder::new()
245 .with_schema(schema)
246 .build(futures::stream::iter([Ok(batch)]))
247 .map_err(|e: FlightError| Status::internal(e.to_string())),
248 );
249 Ok(Response::new(stream))
250 }
251}
252
253#[tonic::async_trait]
254impl FlightSqlService for CypherFlightService {
255 type FlightService = CypherFlightService;
256
257 async fn register_sql_info(&self, _id: i32, _result: &SqlInfo) {}
260
261 async fn do_handshake(
270 &self,
271 request: Request<Streaming<HandshakeRequest>>,
272 ) -> Result<
273 Response<Pin<Box<dyn Stream<Item = Result<HandshakeResponse, Status>> + Send>>>,
274 Status,
275 > {
276 let header = request
277 .metadata()
278 .get("authorization")
279 .and_then(|v| v.to_str().ok());
280 self.auth.check_header(header)?;
282
283 let token = self.auth.issued_token().unwrap_or("").to_string();
284 let response_msg = HandshakeResponse {
285 protocol_version: 0,
286 payload: token.clone().into(),
287 };
288 let output = futures::stream::once(async move { Ok(response_msg) });
289 let mut response: Response<Pin<Box<dyn Stream<Item = _> + Send>>> =
290 Response::new(Box::pin(output));
291 if !token.is_empty() {
292 let bearer = format!("Bearer {token}")
293 .parse()
294 .map_err(|_| Status::internal("token not header-safe"))?;
295 response.metadata_mut().insert("authorization", bearer);
296 }
297 Ok(response)
298 }
299
300 async fn get_flight_info_statement(
308 &self,
309 query: CommandStatementQuery,
310 request: Request<arrow_flight::FlightDescriptor>,
311 ) -> Result<Response<FlightInfo>, Status> {
312 let cypher = self.target_cypher(&query.query);
315 self.authorize(&request, &cypher)?;
316 let schema = match self.schema_for(&cypher).await {
317 Ok(s) => {
318 crate::functional_status(
319 "graphar-flight/get_flight_info_statement",
320 "schema_resolved",
321 true,
322 &cypher,
323 );
324 s
325 }
326 Err(e) => {
327 crate::functional_status(
328 "graphar-flight/get_flight_info_statement",
329 "schema_resolved",
330 false,
331 &cypher,
332 );
333 return Err(e);
334 }
335 };
336
337 let ticket_bytes = TicketStatementQuery {
338 statement_handle: cypher.into_bytes().into(),
339 }
340 .encode_to_vec();
341
342 let info = FlightInfo::new()
343 .try_with_schema(schema.as_ref())
344 .map_err(|e| Status::internal(e.to_string()))?
345 .with_endpoint(FlightEndpoint::new().with_ticket(Ticket::new(ticket_bytes)))
346 .with_total_records(-1)
347 .with_total_bytes(-1)
348 .with_ordered(false);
349
350 Ok(Response::new(info))
351 }
352
353 async fn do_get_statement(
356 &self,
357 ticket: TicketStatementQuery,
358 request: Request<Ticket>,
359 ) -> Result<Response<BoxStream>, Status> {
360 let query = String::from_utf8(ticket.statement_handle.to_vec())
361 .map_err(|e| Status::invalid_argument(format!("invalid ticket: {e}")))?;
362 self.authorize(&request, &query)?;
363 let result = self.execute_cypher_stream(&query).await;
364 crate::functional_status(
365 "graphar-flight/do_get_statement",
366 "cypher_stream",
367 result.is_ok(),
368 &query,
369 );
370 result
371 }
372
373 async fn do_put_statement_update(
376 &self,
377 cmd: CommandStatementUpdate,
378 request: Request<PeekableFlightDataStream>,
379 ) -> Result<i64, Status> {
380 let directive = cmd.query.trim().to_string();
381 let action =
382 parse_put_directive(&directive).map_err(|e| Status::invalid_argument(e.to_string()))?;
383
384 let raw = request.into_inner().into_inner(); let batches = FlightRecordBatchStream::new_from_flight_data(
387 raw.map_err(|s| FlightError::Tonic(Box::new(s))),
388 )
389 .try_collect::<Vec<_>>()
390 .await
391 .map_err(|e| Status::internal(e.to_string()))?;
392
393 if batches.is_empty() {
394 return Ok(0);
395 }
396
397 let mut loader = self
398 .new_loader()
399 .await
400 .map_err(|e| Status::internal(e.to_string()))?;
401
402 let n = match action {
403 PutAction::Vertices { label } => loader
404 .insert_vertices(&label, &batches)
405 .await
406 .map_err(|e| Status::internal(e.to_string()))?,
407 PutAction::Edges {
408 src,
409 edge_type,
410 dst,
411 } => loader
412 .insert_edges(&src, &edge_type, &dst, &batches)
413 .await
414 .map_err(|e| Status::internal(e.to_string()))?,
415 };
416
417 Ok(n as i64)
418 }
419
420 async fn get_flight_info_catalogs(
426 &self,
427 query: CommandGetCatalogs,
428 request: Request<FlightDescriptor>,
429 ) -> Result<Response<FlightInfo>, Status> {
430 let descriptor = request.into_inner();
431 let ticket = Ticket::new(query.as_any().encode_to_vec());
432 metadata_info(descriptor, ticket, query.into_builder().schema())
433 }
434
435 async fn get_flight_info_schemas(
436 &self,
437 query: CommandGetDbSchemas,
438 request: Request<FlightDescriptor>,
439 ) -> Result<Response<FlightInfo>, Status> {
440 let descriptor = request.into_inner();
441 let ticket = Ticket::new(query.as_any().encode_to_vec());
442 metadata_info(descriptor, ticket, query.into_builder().schema())
443 }
444
445 async fn get_flight_info_tables(
446 &self,
447 query: CommandGetTables,
448 request: Request<FlightDescriptor>,
449 ) -> Result<Response<FlightInfo>, Status> {
450 let descriptor = request.into_inner();
451 let ticket = Ticket::new(query.as_any().encode_to_vec());
452 metadata_info(descriptor, ticket, query.into_builder().schema())
453 }
454
455 async fn get_flight_info_table_types(
456 &self,
457 query: CommandGetTableTypes,
458 request: Request<FlightDescriptor>,
459 ) -> Result<Response<FlightInfo>, Status> {
460 let descriptor = request.into_inner();
461 let ticket = Ticket::new(query.as_any().encode_to_vec());
462 metadata_info(descriptor, ticket, query.into_builder().schema())
463 }
464
465 async fn get_flight_info_sql_info(
466 &self,
467 query: CommandGetSqlInfo,
468 request: Request<FlightDescriptor>,
469 ) -> Result<Response<FlightInfo>, Status> {
470 let descriptor = request.into_inner();
471 let ticket = Ticket::new(query.as_any().encode_to_vec());
472 metadata_info(descriptor, ticket, query.into_builder(&SQL_INFO).schema())
473 }
474
475 async fn do_get_catalogs(
476 &self,
477 query: CommandGetCatalogs,
478 _request: Request<Ticket>,
479 ) -> Result<Response<BoxStream>, Status> {
480 let mut builder = query.into_builder();
481 builder.append(self.graph.as_str());
482 let schema = builder.schema();
483 Ok(Response::new(metadata_stream(schema, builder.build())))
484 }
485
486 async fn do_get_schemas(
487 &self,
488 query: CommandGetDbSchemas,
489 _request: Request<Ticket>,
490 ) -> Result<Response<BoxStream>, Status> {
491 let mut builder = query.into_builder();
492 builder.append(self.graph.as_str(), DB_SCHEMA);
493 let schema = builder.schema();
494 Ok(Response::new(metadata_stream(schema, builder.build())))
495 }
496
497 async fn do_get_tables(
498 &self,
499 query: CommandGetTables,
500 _request: Request<Ticket>,
501 ) -> Result<Response<BoxStream>, Status> {
502 let mut builder = query.into_builder();
503 for (name, table_schema) in self.registry.tables() {
504 builder
505 .append(
506 self.graph.as_str(),
507 DB_SCHEMA,
508 &name,
509 "TABLE",
510 table_schema.as_ref(),
511 )
512 .map_err(|e| Status::internal(e.to_string()))?;
513 }
514 let schema = builder.schema();
515 Ok(Response::new(metadata_stream(schema, builder.build())))
516 }
517
518 async fn do_get_table_types(
519 &self,
520 query: CommandGetTableTypes,
521 _request: Request<Ticket>,
522 ) -> Result<Response<BoxStream>, Status> {
523 let mut builder = query.into_builder();
524 builder.append("TABLE");
525 let schema = builder.schema();
526 Ok(Response::new(metadata_stream(schema, builder.build())))
527 }
528
529 async fn do_get_sql_info(
530 &self,
531 query: CommandGetSqlInfo,
532 _request: Request<Ticket>,
533 ) -> Result<Response<BoxStream>, Status> {
534 let builder = query.into_builder(&SQL_INFO);
535 let schema = builder.schema();
536 Ok(Response::new(metadata_stream(schema, builder.build())))
537 }
538
539 async fn do_action_create_prepared_statement(
544 &self,
545 query: ActionCreatePreparedStatementRequest,
546 request: Request<arrow_flight::Action>,
547 ) -> Result<ActionCreatePreparedStatementResult, Status> {
548 let cypher = self.target_cypher(&query.query);
549 self.authorize(&request, &cypher)?;
550 let schema = self.schema_for(&cypher).await?;
551 let IpcMessage(schema_bytes) =
552 SchemaAsIpc::new(schema.as_ref(), &IpcWriteOptions::default())
553 .try_into()
554 .map_err(|e: arrow::error::ArrowError| Status::internal(e.to_string()))?;
555 Ok(ActionCreatePreparedStatementResult {
556 prepared_statement_handle: cypher.into_bytes().into(),
557 dataset_schema: schema_bytes,
558 parameter_schema: Default::default(), })
560 }
561
562 async fn do_action_close_prepared_statement(
563 &self,
564 _query: ActionClosePreparedStatementRequest,
565 _request: Request<arrow_flight::Action>,
566 ) -> Result<(), Status> {
567 Ok(()) }
569
570 async fn get_flight_info_prepared_statement(
571 &self,
572 cmd: CommandPreparedStatementQuery,
573 request: Request<FlightDescriptor>,
574 ) -> Result<Response<FlightInfo>, Status> {
575 let cypher = String::from_utf8(cmd.prepared_statement_handle.to_vec())
576 .map_err(|e| Status::invalid_argument(format!("invalid handle: {e}")))?;
577 self.authorize(&request, &cypher)?;
578 let schema = self.schema_for(&cypher).await?;
579
580 let ticket_bytes = TicketStatementQuery {
581 statement_handle: cypher.into_bytes().into(),
582 }
583 .encode_to_vec();
584
585 let info = FlightInfo::new()
586 .try_with_schema(schema.as_ref())
587 .map_err(|e| Status::internal(e.to_string()))?
588 .with_endpoint(FlightEndpoint::new().with_ticket(Ticket::new(ticket_bytes)))
589 .with_total_records(-1)
590 .with_total_bytes(-1)
591 .with_ordered(false);
592 Ok(Response::new(info))
593 }
594
595 async fn do_get_prepared_statement(
596 &self,
597 cmd: CommandPreparedStatementQuery,
598 request: Request<Ticket>,
599 ) -> Result<Response<BoxStream>, Status> {
600 let cypher = String::from_utf8(cmd.prepared_statement_handle.to_vec())
601 .map_err(|e| Status::invalid_argument(format!("invalid handle: {e}")))?;
602 self.authorize(&request, &cypher)?;
603 self.execute_cypher_stream(&cypher).await
604 }
605
606 async fn do_put_prepared_statement_query(
607 &self,
608 query: CommandPreparedStatementQuery,
609 _request: Request<PeekableFlightDataStream>,
610 ) -> Result<DoPutPreparedStatementResult, Status> {
611 Ok(DoPutPreparedStatementResult {
613 prepared_statement_handle: Some(query.prepared_statement_handle),
614 })
615 }
616}
617
618const DB_SCHEMA: &str = "public";
619
620#[cfg(test)]
623pub(crate) fn sql_info_batch() -> arrow_array::RecordBatch {
624 CommandGetSqlInfo::default()
625 .into_builder(&SQL_INFO)
626 .build()
627 .expect("sql info batch builds")
628}
629
630fn metadata_info(
633 descriptor: FlightDescriptor,
634 ticket: Ticket,
635 schema: arrow_schema::SchemaRef,
636) -> Result<Response<FlightInfo>, Status> {
637 let info = FlightInfo::new()
638 .try_with_schema(schema.as_ref())
639 .map_err(|e| Status::internal(e.to_string()))?
640 .with_endpoint(FlightEndpoint::new().with_ticket(ticket))
641 .with_descriptor(descriptor);
642 Ok(Response::new(info))
643}
644
645fn metadata_stream(
648 schema: arrow_schema::SchemaRef,
649 batch: std::result::Result<arrow_array::RecordBatch, FlightError>,
650) -> BoxStream {
651 Box::pin(
652 FlightDataEncoderBuilder::new()
653 .with_schema(schema)
654 .build(futures::stream::once(async move { batch }))
655 .map_err(|e: FlightError| Status::internal(e.to_string())),
656 )
657}
658
659enum PutAction {
662 Vertices {
663 label: String,
664 },
665 Edges {
666 src: String,
667 edge_type: String,
668 dst: String,
669 },
670}
671
672fn parse_put_directive(s: &str) -> Result<PutAction, FlightSqlError> {
673 if let Some(rest) = s.strip_prefix("VERTICES:") {
674 return Ok(PutAction::Vertices {
675 label: rest.to_string(),
676 });
677 }
678 if let Some(rest) = s.strip_prefix("EDGES:") {
679 let parts: Vec<&str> = rest.splitn(3, ':').collect();
680 if parts.len() == 3 {
681 return Ok(PutAction::Edges {
682 src: parts[0].to_string(),
683 edge_type: parts[1].to_string(),
684 dst: parts[2].to_string(),
685 });
686 }
687 }
688 Err(FlightSqlError::UnknownPutCommand(s.to_string()))
689}