use core::pin::Pin;
use std::future::Future;
use mongreldb_types::errors::CategoryError;
use mongreldb_types::ids::{DatabaseId, QueryId, SchemaVersion, TransactionId};
use crate::prepared::PreparedStatementBinding;
use crate::request::{AuthenticatedIdentity, ExecuteRequest, IsolationLevel, SessionId};
use crate::session::Session;
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, CategoryError>> + Send + 'a>>;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Credentials {
Password {
username: String,
password: String,
},
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ExecuteResponse {
pub query_id: QueryId,
pub rows_affected: u64,
pub frames: Vec<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum QueryPhase {
Queued,
Planning,
Executing,
Serializing,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct QueryStatus {
pub query_id: QueryId,
pub phase: QueryPhase,
pub error: Option<CategoryError>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ColumnSchema {
pub name: String,
pub data_type: String,
pub nullable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TableSchema {
pub table: String,
pub schema_version: SchemaVersion,
pub columns: Vec<ColumnSchema>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HealthStatus {
pub serving: bool,
pub detail: Option<String>,
}
pub trait ArrowFrameStream: Send {
fn next_frame(&mut self) -> BoxFuture<'_, Option<Vec<u8>>>;
}
impl ArrowFrameStream for std::vec::IntoIter<Vec<u8>> {
fn next_frame(&mut self) -> BoxFuture<'_, Option<Vec<u8>>> {
let frame = self.next();
Box::pin(async move { Ok(frame) })
}
}
pub trait AuthService: Send + Sync {
fn authenticate<'a>(
&'a self,
credentials: &'a Credentials,
) -> BoxFuture<'a, AuthenticatedIdentity>;
}
pub trait SessionService: Send + Sync {
fn open_session(
&self,
principal: AuthenticatedIdentity,
database_id: DatabaseId,
) -> BoxFuture<'_, Session>;
fn close_session(&self, session_id: SessionId) -> BoxFuture<'_, ()>;
}
pub trait QueryService: Send + Sync {
fn prepare(
&self,
session_id: SessionId,
sql: String,
) -> BoxFuture<'_, PreparedStatementBinding>;
fn execute(&self, request: ExecuteRequest) -> BoxFuture<'_, ExecuteResponse>;
fn execute_stream(&self, request: ExecuteRequest) -> BoxFuture<'_, Box<dyn ArrowFrameStream>>;
fn cancel_query(&self, query_id: QueryId) -> BoxFuture<'_, ()>;
fn get_query_status(&self, query_id: QueryId) -> BoxFuture<'_, QueryStatus>;
}
pub trait TransactionService: Send + Sync {
fn begin(
&self,
session_id: SessionId,
isolation: IsolationLevel,
) -> BoxFuture<'_, TransactionId>;
fn commit(&self, session_id: SessionId) -> BoxFuture<'_, ()>;
fn rollback(&self, session_id: SessionId) -> BoxFuture<'_, ()>;
}
pub trait CatalogService: Send + Sync {
fn get_schema(&self, database_id: DatabaseId, table: String) -> BoxFuture<'_, TableSchema>;
}
pub trait AdminService: Send + Sync {
fn execute_admin(&self, request: ExecuteRequest) -> BoxFuture<'_, ()>;
}
pub trait HealthService: Send + Sync {
fn status(&self) -> BoxFuture<'_, HealthStatus>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::request::{ExecuteCommand, ResultLimits};
use crate::test_support::{assert_serde_round_trip, block_on};
use mongreldb_types::errors::ErrorCategory;
use std::sync::Arc;
#[test]
fn dto_serde_round_trips() {
assert_serde_round_trip(&Credentials::Password {
username: "alice".to_owned(),
password: "s3cret".to_owned(),
});
assert_serde_round_trip(&ExecuteResponse {
query_id: QueryId::new_random(),
rows_affected: 17,
frames: vec![b"arrow-ipc-bytes".to_vec(), vec![]],
});
for phase in [
QueryPhase::Queued,
QueryPhase::Planning,
QueryPhase::Executing,
QueryPhase::Serializing,
QueryPhase::Completed,
QueryPhase::Failed,
QueryPhase::Cancelled,
] {
assert_serde_round_trip(&phase);
}
assert_serde_round_trip(&QueryStatus {
query_id: QueryId::new_random(),
phase: QueryPhase::Failed,
error: Some(CategoryError::new(
ErrorCategory::DeadlineExceeded,
"deadline expired during execution",
)),
});
assert_serde_round_trip(&QueryStatus {
query_id: QueryId::new_random(),
phase: QueryPhase::Completed,
error: None,
});
assert_serde_round_trip(&ColumnSchema {
name: "tenant".to_owned(),
data_type: "INT64".to_owned(),
nullable: false,
});
assert_serde_round_trip(&TableSchema {
table: "events".to_owned(),
schema_version: SchemaVersion::new(10),
columns: vec![
ColumnSchema {
name: "tenant".to_owned(),
data_type: "INT64".to_owned(),
nullable: false,
},
ColumnSchema {
name: "payload".to_owned(),
data_type: "TEXT".to_owned(),
nullable: true,
},
],
});
assert_serde_round_trip(&HealthStatus {
serving: true,
detail: None,
});
assert_serde_round_trip(&HealthStatus {
serving: false,
detail: Some("draining".to_owned()),
});
}
struct StubAuth;
impl AuthService for StubAuth {
fn authenticate<'a>(
&'a self,
credentials: &'a Credentials,
) -> BoxFuture<'a, AuthenticatedIdentity> {
Box::pin(async move {
let Credentials::Password { username, .. } = credentials;
Err(CategoryError::new(
ErrorCategory::Unauthenticated,
format!("invalid credentials for {username:?}"),
))
})
}
}
#[test]
fn category_error_propagates_through_dyn_dispatch() {
let service: Arc<dyn AuthService> = Arc::new(StubAuth);
let credentials = Credentials::Password {
username: "alice".to_owned(),
password: "wrong".to_owned(),
};
let error = block_on(service.authenticate(&credentials)).unwrap_err();
assert_eq!(error.category, ErrorCategory::Unauthenticated);
assert_eq!(error.code(), 19);
assert_eq!(error.message, "invalid credentials for \"alice\"");
assert!(!error.category.is_retryable());
assert_eq!(
error.to_string(),
"unauthenticated: invalid credentials for \"alice\""
);
}
struct StubQuery;
impl QueryService for StubQuery {
fn prepare(
&self,
_session_id: SessionId,
sql: String,
) -> BoxFuture<'_, PreparedStatementBinding> {
Box::pin(async move {
Err(CategoryError::new(
ErrorCategory::SchemaVersionMismatch,
format!("cannot prepare {sql:?}: stale catalog"),
))
})
}
fn execute(&self, request: ExecuteRequest) -> BoxFuture<'_, ExecuteResponse> {
Box::pin(async move {
Ok(ExecuteResponse {
query_id: request.query_id,
rows_affected: 0,
frames: vec![b"frame-1".to_vec(), b"frame-2".to_vec()],
})
})
}
fn execute_stream(
&self,
_request: ExecuteRequest,
) -> BoxFuture<'_, Box<dyn ArrowFrameStream>> {
Box::pin(async move {
let stream: Box<dyn ArrowFrameStream> =
Box::new(vec![b"frame-1".to_vec(), b"frame-2".to_vec()].into_iter());
Ok(stream)
})
}
fn cancel_query(&self, query_id: QueryId) -> BoxFuture<'_, ()> {
Box::pin(async move {
Err(CategoryError::new(
ErrorCategory::Cancelled,
format!("query {query_id} is not running"),
))
})
}
fn get_query_status(&self, query_id: QueryId) -> BoxFuture<'_, QueryStatus> {
Box::pin(async move {
Ok(QueryStatus {
query_id,
phase: QueryPhase::Completed,
error: None,
})
})
}
}
fn sample_request() -> ExecuteRequest {
ExecuteRequest {
request_id: [0x99; 16],
query_id: QueryId::new_random(),
session_id: Some(SessionId::from_bytes([0x88; 16])),
database_id: DatabaseId::new_random(),
principal: AuthenticatedIdentity::Credentialless,
command: ExecuteCommand::Sql {
text: "SELECT 1".to_owned(),
params: vec![],
},
deadline_unix_micros: None,
result_limits: ResultLimits::default(),
resource_group: None,
idempotency_key: None,
}
}
#[test]
fn query_service_methods_work_through_dyn_dispatch() {
let service: Arc<dyn QueryService> = Arc::new(StubQuery);
let request = sample_request();
let response = block_on(service.execute(request.clone())).unwrap();
assert_eq!(response.query_id, request.query_id);
assert_eq!(response.frames.len(), 2);
let mut stream = block_on(service.execute_stream(request)).unwrap();
assert_eq!(
block_on(stream.next_frame()).unwrap(),
Some(b"frame-1".to_vec())
);
assert_eq!(
block_on(stream.next_frame()).unwrap(),
Some(b"frame-2".to_vec())
);
assert_eq!(block_on(stream.next_frame()).unwrap(), None);
let status = block_on(service.get_query_status(response.query_id)).unwrap();
assert_eq!(status.phase, QueryPhase::Completed);
let error = block_on(service.cancel_query(response.query_id)).unwrap_err();
assert_eq!(error.category, ErrorCategory::Cancelled);
let error = block_on(service.prepare(SessionId::ZERO, "SELECT 1".to_owned())).unwrap_err();
assert_eq!(error.category, ErrorCategory::SchemaVersionMismatch);
}
#[test]
fn vec_into_iter_stream_yields_all_frames_then_ends() {
let mut stream: Box<dyn ArrowFrameStream> = Box::new(Vec::<Vec<u8>>::new().into_iter());
assert_eq!(block_on(stream.next_frame()).unwrap(), None);
}
}