1use core::pin::Pin;
30use std::future::Future;
31
32use mongreldb_types::errors::CategoryError;
33use mongreldb_types::ids::{DatabaseId, QueryId, SchemaVersion, TransactionId};
34
35use crate::prepared::PreparedStatementBinding;
36use crate::request::{AuthenticatedIdentity, ExecuteRequest, IsolationLevel, SessionId};
37use crate::session::Session;
38
39pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, CategoryError>> + Send + 'a>>;
43
44#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
49pub enum Credentials {
50 Password {
53 username: String,
55 password: String,
58 },
59}
60
61#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
63pub struct ExecuteResponse {
64 pub query_id: QueryId,
66 pub rows_affected: u64,
68 pub frames: Vec<Vec<u8>>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
80pub enum QueryPhase {
81 Queued,
83 Planning,
85 Executing,
87 Serializing,
89 Completed,
91 Failed,
94 Cancelled,
96}
97
98#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
101pub struct QueryStatus {
102 pub query_id: QueryId,
104 pub phase: QueryPhase,
106 pub error: Option<CategoryError>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
113pub struct ColumnSchema {
114 pub name: String,
116 pub data_type: String,
118 pub nullable: bool,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
125pub struct TableSchema {
126 pub table: String,
128 pub schema_version: SchemaVersion,
131 pub columns: Vec<ColumnSchema>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
137pub struct HealthStatus {
138 pub serving: bool,
140 pub detail: Option<String>,
142}
143
144pub trait ArrowFrameStream: Send {
154 fn next_frame(&mut self) -> BoxFuture<'_, Option<Vec<u8>>>;
156}
157
158impl ArrowFrameStream for std::vec::IntoIter<Vec<u8>> {
159 fn next_frame(&mut self) -> BoxFuture<'_, Option<Vec<u8>>> {
160 let frame = self.next();
161 Box::pin(async move { Ok(frame) })
162 }
163}
164
165pub trait AuthService: Send + Sync {
169 fn authenticate<'a>(
171 &'a self,
172 credentials: &'a Credentials,
173 ) -> BoxFuture<'a, AuthenticatedIdentity>;
174}
175
176pub trait SessionService: Send + Sync {
178 fn open_session(
180 &self,
181 principal: AuthenticatedIdentity,
182 database_id: DatabaseId,
183 ) -> BoxFuture<'_, Session>;
184
185 fn close_session(&self, session_id: SessionId) -> BoxFuture<'_, ()>;
188}
189
190pub trait QueryService: Send + Sync {
192 fn prepare(
195 &self,
196 session_id: SessionId,
197 sql: String,
198 ) -> BoxFuture<'_, PreparedStatementBinding>;
199
200 fn execute(&self, request: ExecuteRequest) -> BoxFuture<'_, ExecuteResponse>;
202
203 fn execute_stream(&self, request: ExecuteRequest) -> BoxFuture<'_, Box<dyn ArrowFrameStream>>;
206
207 fn cancel_query(&self, query_id: QueryId) -> BoxFuture<'_, ()>;
211
212 fn get_query_status(&self, query_id: QueryId) -> BoxFuture<'_, QueryStatus>;
215}
216
217pub trait TransactionService: Send + Sync {
219 fn begin(
221 &self,
222 session_id: SessionId,
223 isolation: IsolationLevel,
224 ) -> BoxFuture<'_, TransactionId>;
225
226 fn commit(&self, session_id: SessionId) -> BoxFuture<'_, ()>;
233
234 fn rollback(&self, session_id: SessionId) -> BoxFuture<'_, ()>;
236}
237
238pub trait CatalogService: Send + Sync {
240 fn get_schema(&self, database_id: DatabaseId, table: String) -> BoxFuture<'_, TableSchema>;
242}
243
244pub trait AdminService: Send + Sync {
248 fn execute_admin(&self, request: ExecuteRequest) -> BoxFuture<'_, ()>;
250}
251
252pub trait HealthService: Send + Sync {
254 fn status(&self) -> BoxFuture<'_, HealthStatus>;
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::request::{ExecuteCommand, ResultLimits};
262 use crate::test_support::{assert_serde_round_trip, block_on};
263 use mongreldb_types::errors::ErrorCategory;
264 use std::sync::Arc;
265
266 #[test]
267 fn dto_serde_round_trips() {
268 assert_serde_round_trip(&Credentials::Password {
269 username: "alice".to_owned(),
270 password: "s3cret".to_owned(),
271 });
272 assert_serde_round_trip(&ExecuteResponse {
273 query_id: QueryId::new_random(),
274 rows_affected: 17,
275 frames: vec![b"arrow-ipc-bytes".to_vec(), vec![]],
276 });
277 for phase in [
278 QueryPhase::Queued,
279 QueryPhase::Planning,
280 QueryPhase::Executing,
281 QueryPhase::Serializing,
282 QueryPhase::Completed,
283 QueryPhase::Failed,
284 QueryPhase::Cancelled,
285 ] {
286 assert_serde_round_trip(&phase);
287 }
288 assert_serde_round_trip(&QueryStatus {
289 query_id: QueryId::new_random(),
290 phase: QueryPhase::Failed,
291 error: Some(CategoryError::new(
292 ErrorCategory::DeadlineExceeded,
293 "deadline expired during execution",
294 )),
295 });
296 assert_serde_round_trip(&QueryStatus {
297 query_id: QueryId::new_random(),
298 phase: QueryPhase::Completed,
299 error: None,
300 });
301 assert_serde_round_trip(&ColumnSchema {
302 name: "tenant".to_owned(),
303 data_type: "INT64".to_owned(),
304 nullable: false,
305 });
306 assert_serde_round_trip(&TableSchema {
307 table: "events".to_owned(),
308 schema_version: SchemaVersion::new(10),
309 columns: vec![
310 ColumnSchema {
311 name: "tenant".to_owned(),
312 data_type: "INT64".to_owned(),
313 nullable: false,
314 },
315 ColumnSchema {
316 name: "payload".to_owned(),
317 data_type: "TEXT".to_owned(),
318 nullable: true,
319 },
320 ],
321 });
322 assert_serde_round_trip(&HealthStatus {
323 serving: true,
324 detail: None,
325 });
326 assert_serde_round_trip(&HealthStatus {
327 serving: false,
328 detail: Some("draining".to_owned()),
329 });
330 }
331
332 struct StubAuth;
334
335 impl AuthService for StubAuth {
336 fn authenticate<'a>(
337 &'a self,
338 credentials: &'a Credentials,
339 ) -> BoxFuture<'a, AuthenticatedIdentity> {
340 Box::pin(async move {
341 let Credentials::Password { username, .. } = credentials;
342 Err(CategoryError::new(
343 ErrorCategory::Unauthenticated,
344 format!("invalid credentials for {username:?}"),
345 ))
346 })
347 }
348 }
349
350 #[test]
351 fn category_error_propagates_through_dyn_dispatch() {
352 let service: Arc<dyn AuthService> = Arc::new(StubAuth);
353 let credentials = Credentials::Password {
354 username: "alice".to_owned(),
355 password: "wrong".to_owned(),
356 };
357 let error = block_on(service.authenticate(&credentials)).unwrap_err();
358 assert_eq!(error.category, ErrorCategory::Unauthenticated);
361 assert_eq!(error.code(), 19);
362 assert_eq!(error.message, "invalid credentials for \"alice\"");
363 assert!(!error.category.is_retryable());
364 assert_eq!(
365 error.to_string(),
366 "unauthenticated: invalid credentials for \"alice\""
367 );
368 }
369
370 struct StubQuery;
371
372 impl QueryService for StubQuery {
373 fn prepare(
374 &self,
375 _session_id: SessionId,
376 sql: String,
377 ) -> BoxFuture<'_, PreparedStatementBinding> {
378 Box::pin(async move {
379 Err(CategoryError::new(
380 ErrorCategory::SchemaVersionMismatch,
381 format!("cannot prepare {sql:?}: stale catalog"),
382 ))
383 })
384 }
385
386 fn execute(&self, request: ExecuteRequest) -> BoxFuture<'_, ExecuteResponse> {
387 Box::pin(async move {
388 Ok(ExecuteResponse {
389 query_id: request.query_id,
390 rows_affected: 0,
391 frames: vec![b"frame-1".to_vec(), b"frame-2".to_vec()],
392 })
393 })
394 }
395
396 fn execute_stream(
397 &self,
398 _request: ExecuteRequest,
399 ) -> BoxFuture<'_, Box<dyn ArrowFrameStream>> {
400 Box::pin(async move {
401 let stream: Box<dyn ArrowFrameStream> =
402 Box::new(vec![b"frame-1".to_vec(), b"frame-2".to_vec()].into_iter());
403 Ok(stream)
404 })
405 }
406
407 fn cancel_query(&self, query_id: QueryId) -> BoxFuture<'_, ()> {
408 Box::pin(async move {
409 Err(CategoryError::new(
410 ErrorCategory::Cancelled,
411 format!("query {query_id} is not running"),
412 ))
413 })
414 }
415
416 fn get_query_status(&self, query_id: QueryId) -> BoxFuture<'_, QueryStatus> {
417 Box::pin(async move {
418 Ok(QueryStatus {
419 query_id,
420 phase: QueryPhase::Completed,
421 error: None,
422 })
423 })
424 }
425 }
426
427 fn sample_request() -> ExecuteRequest {
428 ExecuteRequest {
429 request_id: [0x99; 16],
430 query_id: QueryId::new_random(),
431 session_id: Some(SessionId::from_bytes([0x88; 16])),
432 database_id: DatabaseId::new_random(),
433 principal: AuthenticatedIdentity::Credentialless,
434 command: ExecuteCommand::Sql {
435 text: "SELECT 1".to_owned(),
436 params: vec![],
437 },
438 deadline_unix_micros: None,
439 result_limits: ResultLimits::default(),
440 resource_group: None,
441 idempotency_key: None,
442 }
443 }
444
445 #[test]
446 fn query_service_methods_work_through_dyn_dispatch() {
447 let service: Arc<dyn QueryService> = Arc::new(StubQuery);
448 let request = sample_request();
449
450 let response = block_on(service.execute(request.clone())).unwrap();
451 assert_eq!(response.query_id, request.query_id);
452 assert_eq!(response.frames.len(), 2);
453
454 let mut stream = block_on(service.execute_stream(request)).unwrap();
455 assert_eq!(
456 block_on(stream.next_frame()).unwrap(),
457 Some(b"frame-1".to_vec())
458 );
459 assert_eq!(
460 block_on(stream.next_frame()).unwrap(),
461 Some(b"frame-2".to_vec())
462 );
463 assert_eq!(block_on(stream.next_frame()).unwrap(), None);
464
465 let status = block_on(service.get_query_status(response.query_id)).unwrap();
466 assert_eq!(status.phase, QueryPhase::Completed);
467
468 let error = block_on(service.cancel_query(response.query_id)).unwrap_err();
469 assert_eq!(error.category, ErrorCategory::Cancelled);
470
471 let error = block_on(service.prepare(SessionId::ZERO, "SELECT 1".to_owned())).unwrap_err();
472 assert_eq!(error.category, ErrorCategory::SchemaVersionMismatch);
473 }
474
475 #[test]
476 fn vec_into_iter_stream_yields_all_frames_then_ends() {
477 let mut stream: Box<dyn ArrowFrameStream> = Box::new(Vec::<Vec<u8>>::new().into_iter());
478 assert_eq!(block_on(stream.next_frame()).unwrap(), None);
479 }
480}