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