1use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::Arc;
19
20use axum::extract::{Path, State};
21use axum::http::header;
22use axum::http::StatusCode;
23use axum::response::{IntoResponse, Response};
24use axum::routing::{get, post};
25use axum::Json;
26use mongreldb_core::schema::{Schema, TypeId};
27use mongreldb_core::{CancellationReason, Database, Value};
28use mongreldb_query::{
29 CancelOutcome, ExternalTableModule, ManagedQueryBatches, MongrelSession, QueryId,
30 RegisteredQueryGuard, RegisteredSqlQuery, SqlQueryOptions, SqlQueryPhase, SqlQueryRegistry,
31 SqlStreamCompletion,
32};
33use serde::{Deserialize, Serialize};
34use serde_json::json;
35
36mod audit;
37mod kit;
38mod metrics;
39mod procedure;
40mod sessions;
41mod trigger;
42
43pub use sessions::{spawn_session_reaper, SessionStore};
44
45fn status_for_error(e: &mongreldb_core::MongrelError) -> StatusCode {
50 use mongreldb_core::MongrelError;
51 match e {
52 MongrelError::AuthRequired | MongrelError::InvalidCredentials { .. } => {
53 StatusCode::UNAUTHORIZED
54 }
55 MongrelError::AuthNotRequired => StatusCode::BAD_REQUEST,
56 MongrelError::PermissionDenied { .. } => StatusCode::FORBIDDEN,
57 MongrelError::InvalidArgument(_) => StatusCode::CONFLICT,
58 MongrelError::Conflict(_) => StatusCode::CONFLICT,
59 MongrelError::ReadOnlyReplica => StatusCode::CONFLICT,
60 MongrelError::NotFound(_) => StatusCode::NOT_FOUND,
61 MongrelError::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
62 MongrelError::WorkBudgetExceeded => StatusCode::TOO_MANY_REQUESTS,
63 MongrelError::Cancelled => StatusCode::from_u16(499).expect("499 is valid"),
64 MongrelError::CursorStale(_) => StatusCode::CONFLICT,
65 MongrelError::CursorExpired => StatusCode::GONE,
66 _ => StatusCode::INTERNAL_SERVER_ERROR,
67 }
68}
69
70fn status_for_query_error(e: &mongreldb_query::MongrelQueryError) -> StatusCode {
73 use mongreldb_query::MongrelQueryError;
74 match e {
75 MongrelQueryError::Core(core) => status_for_error(core),
76 MongrelQueryError::DeadlineExceeded { .. } => StatusCode::GATEWAY_TIMEOUT,
77 MongrelQueryError::QueryCancelled { .. } => {
78 StatusCode::from_u16(499).expect("499 is valid")
79 }
80 MongrelQueryError::QueryIdConflict { .. } => StatusCode::CONFLICT,
81 MongrelQueryError::QueryRegistryFull => StatusCode::SERVICE_UNAVAILABLE,
82 MongrelQueryError::TransactionAborted => StatusCode::CONFLICT,
83 MongrelQueryError::CommitOutcome { .. } => StatusCode::CONFLICT,
84 _ => StatusCode::INTERNAL_SERVER_ERROR,
85 }
86}
87
88struct OptionalPrincipal(Option<mongreldb_core::Principal>);
92
93impl<S> axum::extract::FromRequestParts<S> for OptionalPrincipal
94where
95 S: Send + Sync,
96{
97 type Rejection = std::convert::Infallible;
98
99 async fn from_request_parts(
100 parts: &mut axum::http::request::Parts,
101 _state: &S,
102 ) -> Result<Self, Self::Rejection> {
103 Ok(OptionalPrincipal(
104 parts.extensions.get::<mongreldb_core::Principal>().cloned(),
105 ))
106 }
107}
108
109struct AppState {
110 db: Arc<Database>,
111 idem: kit::IdempotencyStore,
112 external_modules: Vec<Arc<dyn ExternalTableModule>>,
113 auth_token: Option<String>,
114 user_auth: bool,
116 metrics: Arc<metrics::Metrics>,
118 slow_query_threshold: std::time::Duration,
120 audit: Arc<audit::AuditLog>,
122 sessions: Arc<sessions::SessionStore>,
125 ai_semaphore: Arc<tokio::sync::Semaphore>,
127 query_registry: Arc<SqlQueryRegistry>,
129 sql_semaphore: Arc<tokio::sync::Semaphore>,
131 sql_default_timeout: std::time::Duration,
132 sql_max_timeout: std::time::Duration,
133 sql_cancel_grace: std::time::Duration,
134 sql_max_output_bytes: usize,
135 sql_max_output_rows: usize,
136 accepting_sql: Arc<AtomicBool>,
137 cursor_mac_key: [u8; 32],
139}
140
141#[derive(Clone)]
144pub struct ServerControl {
145 query_registry: Arc<SqlQueryRegistry>,
146 sessions: Arc<sessions::SessionStore>,
147 accepting_sql: Arc<AtomicBool>,
148 cancel_grace: std::time::Duration,
149 metrics: Arc<metrics::Metrics>,
150}
151
152impl ServerControl {
153 pub async fn shutdown(&self) -> usize {
154 self.accepting_sql.store(false, Ordering::Release);
155 self.query_registry
156 .cancel_all(CancellationReason::ServerShutdown);
157 let deadline = tokio::time::Instant::now() + self.cancel_grace;
158 while self.query_registry.active_count() > 0 && tokio::time::Instant::now() < deadline {
159 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
160 }
161 self.sessions.close_all();
162 let stuck_queries = self.query_registry.active_statuses();
163 for status in &stuck_queries {
164 eprintln!(
165 "[sql-cancel-stuck] query_id={} phase={}",
166 status.query_id,
167 query_phase_name(status.phase)
168 );
169 }
170 let stuck = stuck_queries.len();
171 self.metrics.add_sql_stuck_after_cancel(stuck);
172 stuck
173 }
174}
175
176pub fn build_app(db: Arc<Database>) -> axum::Router {
177 build_app_with_config(
178 db,
179 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
180 None,
181 None,
182 )
183}
184
185pub fn build_app_with_external_modules(
186 db: Arc<Database>,
187 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
188) -> axum::Router {
189 build_app_with_config(db, external_modules, None, None)
190}
191
192pub fn build_app_with_config(
194 db: Arc<Database>,
195 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
196 auth_token: Option<String>,
197 max_connections: Option<usize>,
198) -> axum::Router {
199 build_app_full(db, external_modules, auth_token, max_connections, false)
200}
201
202pub fn build_app_full(
205 db: Arc<Database>,
206 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
207 auth_token: Option<String>,
208 max_connections: Option<usize>,
209 user_auth: bool,
210) -> axum::Router {
211 let sessions = Arc::new(sessions::SessionStore::new(
212 default_max_sessions(),
213 default_session_idle_timeout(),
214 ));
215 build_app_with_sessions(
216 db,
217 external_modules,
218 auth_token,
219 max_connections,
220 user_auth,
221 sessions,
222 )
223}
224
225pub fn build_app_with_sessions(
229 db: Arc<Database>,
230 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
231 auth_token: Option<String>,
232 max_connections: Option<usize>,
233 user_auth: bool,
234 sessions: Arc<sessions::SessionStore>,
235) -> axum::Router {
236 build_app_with_sessions_and_control(
237 db,
238 external_modules,
239 auth_token,
240 max_connections,
241 user_auth,
242 sessions,
243 )
244 .0
245}
246
247pub fn build_app_with_sessions_and_control(
249 db: Arc<Database>,
250 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
251 auth_token: Option<String>,
252 max_connections: Option<usize>,
253 user_auth: bool,
254 sessions: Arc<sessions::SessionStore>,
255) -> (axum::Router, ServerControl) {
256 db.set_replication_wal_retention_segments(default_replication_wal_segments());
257 if let Err(error) = db.set_history_retention_epochs(default_history_retention_epochs()) {
258 eprintln!("[history] failed to configure retention: {error}");
259 }
260 let mut cursor_mac_key = [0u8; 32];
261 mongreldb_core::encryption::fill_random(&mut cursor_mac_key);
262 let max_active_queries = default_sql_max_active_queries();
263 let query_registry = Arc::new(SqlQueryRegistry::new(
264 max_active_queries,
265 max_active_queries.saturating_mul(2),
266 2 * 1024 * 1024,
267 default_sql_finished_query_ttl(),
268 ));
269 let accepting_sql = Arc::new(AtomicBool::new(true));
270 let sql_cancel_grace = default_sql_cancel_grace();
271 let sql_max_timeout = default_sql_max_timeout();
272 let sql_default_timeout = default_sql_default_timeout().min(sql_max_timeout);
273 let metrics = Arc::new(metrics::Metrics::default());
274 let server_control = ServerControl {
275 query_registry: Arc::clone(&query_registry),
276 sessions: Arc::clone(&sessions),
277 accepting_sql: Arc::clone(&accepting_sql),
278 cancel_grace: sql_cancel_grace,
279 metrics: Arc::clone(&metrics),
280 };
281 let state = Arc::new(AppState {
282 idem: kit::IdempotencyStore::new(db.root()),
283 db,
284 external_modules: external_modules.into_iter().collect(),
285 auth_token,
286 user_auth,
287 metrics,
288 slow_query_threshold: metrics::slow_query_threshold(),
289 audit: Arc::new(audit::AuditLog::new(8192)),
290 sessions,
291 ai_semaphore: Arc::new(tokio::sync::Semaphore::new(default_ai_max_concurrent())),
292 query_registry,
293 sql_semaphore: Arc::new(tokio::sync::Semaphore::new(default_sql_max_concurrent())),
294 sql_default_timeout,
295 sql_max_timeout,
296 sql_cancel_grace,
297 sql_max_output_bytes: default_sql_max_output_bytes(),
298 sql_max_output_rows: default_sql_max_output_rows(),
299 accepting_sql,
300 cursor_mac_key,
301 });
302 let router = axum::Router::new()
303 .route("/health", get(health))
304 .route("/capabilities", get(capabilities))
305 .route(
306 "/history/retention",
307 get(history_retention).put(set_history_retention),
308 )
309 .route("/metrics", get(metrics_handler))
310 .route("/audit", get(audit_handler))
311 .route("/tables", get(list_tables).post(create_table))
312 .route("/tables/{name}", axum::routing::delete(drop_table))
313 .route("/tables/{name}/put", post(put_row))
314 .route("/tables/{name}/count", get(count))
315 .route("/tables/{name}/commit", post(commit))
316 .route("/sql", post(sql))
317 .route("/queries/{query_id}", get(query_status))
318 .route("/queries/{query_id}/cancel", post(cancel_query))
319 .route("/txn", post(txn))
320 .route("/sessions", post(create_session))
321 .route("/sessions/{id}", axum::routing::delete(close_session))
322 .route("/sessions/{id}/prepare", post(prepare_statement))
323 .route("/sessions/{id}/execute", post(execute_statement))
324 .route(
325 "/sessions/{id}/statements/{name}",
326 axum::routing::delete(deallocate_statement),
327 )
328 .route("/procedures", get(procedure::list).post(procedure::create))
329 .route(
330 "/procedures/{name}",
331 get(procedure::describe)
332 .put(procedure::replace)
333 .delete(procedure::drop_procedure),
334 )
335 .route("/procedures/{name}/call", post(procedure::call))
336 .route("/triggers", get(trigger::list).post(trigger::create))
337 .route(
338 "/triggers/{name}",
339 get(trigger::describe)
340 .put(trigger::replace)
341 .delete(trigger::drop_trigger),
342 )
343 .route("/kit/schema", get(kit::schema_all))
345 .route("/kit/schema/{table}", get(kit::schema_one))
346 .route("/kit/txn", post(kit::kit_txn))
347 .route("/kit/query", post(kit::kit_query))
348 .route("/kit/retrieve", post(kit::kit_retrieve))
349 .route("/kit/ann_rerank", post(kit::kit_ann_rerank))
350 .route("/kit/ai/metrics", get(kit::kit_ai_metrics))
351 .route("/kit/set_similarity", post(kit::kit_set_similarity))
352 .route("/kit/search", post(kit::kit_search))
353 .route("/kit/create_table", post(kit::kit_create_table))
354 .route("/kit/procedures/{name}/call", post(procedure::kit_call))
355 .route("/compact", post(compact_all))
356 .route("/tables/{name}/compact", post(compact_table))
357 .route("/wal/stream", get(wal_stream))
358 .route("/replication/snapshot", get(replication_snapshot))
359 .route("/events", get(events_stream))
360 .with_state(state.clone());
361
362 let router = if state.auth_token.is_some() || state.user_auth {
364 router.layer(axum::middleware::from_fn_with_state(
365 state.clone(),
366 auth_middleware,
367 ))
368 } else {
369 router
370 };
371
372 let router = if let Some(max) = max_connections {
374 router.layer(tower::limit::ConcurrencyLimitLayer::new(max))
375 } else {
376 router
377 };
378 (router, server_control)
379}
380
381async fn auth_middleware(
388 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
389 mut req: axum::extract::Request,
390 next: axum::middleware::Next,
391) -> Result<axum::response::Response, axum::http::StatusCode> {
392 let header = req
393 .headers()
394 .get("authorization")
395 .and_then(|v| v.to_str().ok())
396 .unwrap_or("");
397
398 let mut attempted = String::new();
402 let mut fail_reason = "no credentials provided".to_string();
403
404 if let Some(token) = &state.auth_token {
406 if let Some(provided) = header.strip_prefix("Bearer ") {
407 attempted = "token".to_string();
408 if provided == token {
409 state
410 .audit
411 .record("token", "login.ok", "bearer token accepted");
412 return Ok(next.run(req).await);
413 }
414 fail_reason = "invalid bearer token".to_string();
415 }
416 }
417
418 if state.user_auth {
420 if let Some(encoded) = header.strip_prefix("Basic ") {
421 if let Ok(decoded) = base64_decode(encoded) {
422 if let Ok(creds) = std::str::from_utf8(&decoded) {
423 if let Some((username, password)) = creds.split_once(':') {
424 attempted = username.to_string();
425 if let Ok(Some(_user)) = state.db.verify_user(username, password) {
426 state
427 .audit
428 .record(username, "login.ok", "basic credentials accepted");
429 if let Some(principal) = state.db.resolve_principal(username) {
431 req.extensions_mut().insert(principal);
432 }
433 return Ok(next.run(req).await);
434 }
435 fail_reason = "invalid basic credentials".to_string();
436 } else {
437 fail_reason = "malformed basic credentials (no ':')".to_string();
438 }
439 } else {
440 fail_reason = "malformed basic credentials (non-utf8)".to_string();
441 }
442 } else {
443 fail_reason = "malformed basic credentials (bad base64)".to_string();
444 }
445 }
446 }
447
448 let who = if attempted.is_empty() {
449 "anonymous"
450 } else {
451 attempted.as_str()
452 };
453 state.audit.record(who, "login.fail", fail_reason);
454 Err(axum::http::StatusCode::UNAUTHORIZED)
455}
456
457fn base64_decode(input: &str) -> Result<Vec<u8>, ()> {
459 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
460 let input: Vec<u8> = input
461 .bytes()
462 .filter(|&b| b != b'\n' && b != b'\r' && b != b' ')
463 .collect();
464 let mut out = Vec::with_capacity(input.len() * 3 / 4);
465 let mut buf = 0u32;
466 let mut bits = 0u32;
467 for &b in &input {
468 if b == b'=' {
469 break;
470 }
471 let val = TABLE.iter().position(|&t| t == b).ok_or(())? as u32;
472 buf = (buf << 6) | val;
473 bits += 6;
474 if bits >= 8 {
475 bits -= 8;
476 out.push((buf >> bits) as u8);
477 }
478 }
479 Ok(out)
480}
481
482async fn wal_stream(
489 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
490 OptionalPrincipal(principal): OptionalPrincipal,
491 axum::extract::Query(params): axum::extract::Query<WalStreamParams>,
492) -> Result<Response, StatusCode> {
493 state
494 .db
495 .require_for(
496 request_principal(&state, &principal).as_ref(),
497 &mongreldb_core::Permission::Admin,
498 )
499 .map_err(|error| status_for_error(&error))?;
500 let since = params.since.unwrap_or(0);
501 let db = Arc::clone(&state.db);
502 let batch = tokio::task::spawn_blocking(move || db.replication_batch_since(since))
503 .await
504 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
505 let batch = batch.map_err(|e| {
506 eprintln!("wal_stream error: {e}");
507 StatusCode::INTERNAL_SERVER_ERROR
508 })?;
509 if batch.requires_snapshot {
510 let mut response = (
511 StatusCode::CONFLICT,
512 "replication snapshot required: WAL retention gap or spilled run",
513 )
514 .into_response();
515 set_replication_headers(&mut response, batch.current_epoch, batch.earliest_epoch);
516 return Ok(response);
517 }
518 let mut body = String::new();
519 for record in &batch.records {
520 let json = serde_json::to_string(record).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
521 body.push_str(&json);
522 body.push('\n');
523 }
524 let mut response = (
525 [
526 (header::CONTENT_TYPE, "application/x-ndjson".to_string()),
527 (header::CACHE_CONTROL, "no-cache".to_string()),
528 ],
529 body,
530 )
531 .into_response();
532 set_replication_headers(&mut response, batch.current_epoch, batch.earliest_epoch);
533 Ok(response)
534}
535
536async fn replication_snapshot(
537 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
538 OptionalPrincipal(principal): OptionalPrincipal,
539) -> Response {
540 if let Err(error) = state.db.require_for(
541 request_principal(&state, &principal).as_ref(),
542 &mongreldb_core::Permission::Admin,
543 ) {
544 return (status_for_error(&error), error.to_string()).into_response();
545 }
546 let db = Arc::clone(&state.db);
547 let snapshot = match tokio::task::spawn_blocking(move || db.replication_snapshot()).await {
548 Ok(Ok(snapshot)) => snapshot,
549 Ok(Err(error)) => {
550 return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
551 }
552 Err(error) => {
553 return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
554 }
555 };
556 let epoch = snapshot.epoch();
557 match snapshot.encode() {
560 Ok(bytes) => {
561 let mut response =
562 ([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response();
563 set_replication_headers(&mut response, epoch, None);
564 response
565 }
566 Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
567 }
568}
569
570fn set_replication_headers(response: &mut Response, current: u64, earliest: Option<u64>) {
571 response.headers_mut().insert(
572 "x-mongreldb-current-epoch",
573 current.to_string().parse().unwrap(),
574 );
575 if let Some(earliest) = earliest {
576 response.headers_mut().insert(
577 "x-mongreldb-earliest-epoch",
578 earliest.to_string().parse().unwrap(),
579 );
580 }
581}
582
583#[derive(serde::Deserialize)]
584struct WalStreamParams {
585 since: Option<u64>,
586}
587
588async fn events_stream(
593 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
594 OptionalPrincipal(principal): OptionalPrincipal,
595 headers: axum::http::HeaderMap,
596) -> Result<Response, StatusCode> {
597 use axum::response::sse::{Event, KeepAlive, Sse};
598 use futures::Stream;
599 use std::collections::VecDeque;
600 use std::convert::Infallible;
601
602 state
603 .db
604 .require_for(
605 request_principal(&state, &principal).as_ref(),
606 &mongreldb_core::Permission::Admin,
607 )
608 .map_err(|error| status_for_error(&error))?;
609
610 struct State {
611 db: Arc<Database>,
612 receiver: tokio::sync::broadcast::Receiver<mongreldb_core::ChangeEvent>,
613 change_wake: tokio::sync::broadcast::Receiver<()>,
614 interval: tokio::time::Interval,
615 pending: VecDeque<mongreldb_core::ChangeEvent>,
616 last_id: Option<String>,
617 poll_now: bool,
618 done: bool,
619 }
620
621 fn event(change: mongreldb_core::ChangeEvent) -> Event {
622 let id = change.id.clone();
623 let kind = if change.op == "notify" {
624 "notify"
625 } else {
626 "change"
627 };
628 let mut event = Event::default().event(kind).data(
629 serde_json::to_string(&change)
630 .unwrap_or_else(|error| format!(r#"{{"error":"{error}"}}"#)),
631 );
632 if let Some(id) = id {
633 event = event.id(id);
634 }
635 event
636 }
637
638 let last_id = headers
639 .get("last-event-id")
640 .and_then(|value| value.to_str().ok())
641 .map(str::to_string);
642 let receiver = state.db.subscribe_changes();
643 let change_wake = state.db.subscribe_change_commits();
644 let db = Arc::clone(&state.db);
645 let resume = last_id.clone();
646 let initial = tokio::task::spawn_blocking(move || db.change_events_since(resume.as_deref()))
647 .await
648 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
649 .map_err(|error| match error {
650 mongreldb_core::MongrelError::InvalidArgument(_) => StatusCode::BAD_REQUEST,
651 _ => StatusCode::INTERNAL_SERVER_ERROR,
652 })?;
653 if initial.gap {
654 return Ok((
655 StatusCode::CONFLICT,
656 Json(json!({
657 "error": "cdc retention gap",
658 "earliest_epoch": initial.earliest_epoch,
659 "current_epoch": initial.current_epoch,
660 })),
661 )
662 .into_response());
663 }
664
665 let stream: std::pin::Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>> = Box::pin(
666 futures::stream::unfold(
667 State {
668 db: Arc::clone(&state.db),
669 receiver,
670 change_wake,
671 interval: tokio::time::interval(std::time::Duration::from_millis(250)),
672 pending: initial.events.into(),
673 last_id,
674 poll_now: false,
675 done: false,
676 },
677 |mut stream| async move {
678 if stream.done {
679 return None;
680 }
681 loop {
682 if stream.poll_now {
683 stream.poll_now = false;
684 let db = Arc::clone(&stream.db);
685 let last_id = stream.last_id.clone();
686 match tokio::task::spawn_blocking(move || {
687 db.change_events_since(last_id.as_deref())
688 })
689 .await
690 {
691 Ok(Ok(batch)) if batch.gap => {
692 stream.done = true;
693 let gap = Event::default().event("gap").data(
694 json!({
695 "error": "cdc retention gap",
696 "earliest_epoch": batch.earliest_epoch,
697 "current_epoch": batch.current_epoch,
698 })
699 .to_string(),
700 );
701 return Some((Ok(gap), stream));
702 }
703 Ok(Ok(batch)) => stream.pending.extend(batch.events),
704 Ok(Err(error)) => {
705 stream.done = true;
706 return Some((
707 Ok(Event::default().event("error").data(error.to_string())),
708 stream,
709 ));
710 }
711 Err(error) => {
712 stream.done = true;
713 return Some((
714 Ok(Event::default().event("error").data(error.to_string())),
715 stream,
716 ));
717 }
718 }
719 }
720 if let Some(change) = stream.pending.pop_front() {
721 if let Some(id) = &change.id {
722 stream.last_id = Some(id.clone());
723 }
724 return Some((Ok(event(change)), stream));
725 }
726 tokio::select! {
727 received = stream.receiver.recv() => {
728 match received {
729 Ok(change) => return Some((Ok(event(change)), stream)),
730 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {},
731 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
732 return None;
733 }
734 }
735 }
736 received = stream.change_wake.recv() => {
737 match received {
738 Ok(()) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
739 stream.poll_now = true;
740 }
741 Err(tokio::sync::broadcast::error::RecvError::Closed) => {}
742 }
743 }
744 _ = stream.interval.tick() => {
745 stream.poll_now = true;
746 }
747 }
748 }
749 },
750 ),
751 );
752
753 Ok(Sse::new(stream)
754 .keep_alive(
755 KeepAlive::new()
756 .interval(std::time::Duration::from_secs(15))
757 .text("keep-alive"),
758 )
759 .into_response())
760}
761
762pub fn spawn_auto_compactor(db: Arc<Database>) {
767 std::thread::Builder::new()
768 .name("mongreldb-auto-compact".into())
769 .spawn(move || loop {
770 std::thread::sleep(std::time::Duration::from_secs(30));
771 for name in db.table_names() {
772 let Ok(handle) = db.table(&name) else {
773 continue;
774 };
775 let mut t = handle.lock();
776 let before = t.run_count();
777 match t.maybe_compact() {
778 Ok(true) => {
779 eprintln!(
780 "[auto-compact] {name}: {} runs -> {}",
781 before,
782 t.run_count()
783 );
784 }
785 Ok(false) => {}
786 Err(e) => {
787 eprintln!("[auto-compact] {name}: compaction failed: {e}");
788 }
789 }
790 }
791 })
792 .expect("spawn auto-compact thread");
793}
794
795async fn health() -> StatusCode {
796 StatusCode::OK
797}
798
799#[derive(Debug, Serialize)]
800struct SqlCancellationCapabilities {
801 version: u8,
802 client_query_ids: bool,
803 cancel_endpoint: bool,
804 query_status: bool,
805 stream_disconnect_cancels: bool,
806}
807
808#[derive(Debug, Serialize)]
809struct CapabilitiesResponse {
810 sql_cancellation: SqlCancellationCapabilities,
811}
812
813async fn capabilities() -> Json<CapabilitiesResponse> {
814 Json(CapabilitiesResponse {
815 sql_cancellation: SqlCancellationCapabilities {
816 version: 1,
817 client_query_ids: true,
818 cancel_endpoint: true,
819 query_status: true,
820 stream_disconnect_cancels: true,
821 },
822 })
823}
824
825#[derive(Debug, Deserialize)]
826struct HistoryRetentionRequest {
827 #[serde(default)]
828 history_retention_epochs: serde_json::Value,
829}
830
831#[derive(Debug, Serialize)]
832struct HistoryRetentionResponse {
833 history_retention_epochs: u64,
834 earliest_retained_epoch: u64,
835}
836
837fn history_retention_response(db: &Database) -> HistoryRetentionResponse {
838 HistoryRetentionResponse {
839 history_retention_epochs: db.history_retention_epochs(),
840 earliest_retained_epoch: db.earliest_retained_epoch().0,
841 }
842}
843
844async fn history_retention(
846 State(state): State<Arc<AppState>>,
847 OptionalPrincipal(principal): OptionalPrincipal,
848) -> Response {
849 if let Err(error) = state.db.require_for(
850 request_principal(&state, &principal).as_ref(),
851 &mongreldb_core::Permission::Admin,
852 ) {
853 return (status_for_error(&error), error.to_string()).into_response();
854 }
855 Json(history_retention_response(&state.db)).into_response()
856}
857
858async fn set_history_retention(
860 State(state): State<Arc<AppState>>,
861 OptionalPrincipal(principal): OptionalPrincipal,
862 Json(request): Json<HistoryRetentionRequest>,
863) -> Response {
864 if let Err(error) = state.db.require_for(
865 request_principal(&state, &principal).as_ref(),
866 &mongreldb_core::Permission::Admin,
867 ) {
868 return (status_for_error(&error), error.to_string()).into_response();
869 }
870 let Some(epochs) = request.history_retention_epochs.as_u64() else {
871 return (
872 StatusCode::BAD_REQUEST,
873 Json(json!({"error": "history_retention_epochs must be a u64"})),
874 )
875 .into_response();
876 };
877 match state.db.set_history_retention_epochs(epochs) {
878 Ok(()) => Json(history_retention_response(&state.db)).into_response(),
879 Err(error) => (status_for_error(&error), error.to_string()).into_response(),
880 }
881}
882
883async fn audit_handler(
888 State(state): State<Arc<AppState>>,
889 OptionalPrincipal(principal): OptionalPrincipal,
890) -> Response {
891 if let Err(error) = state.db.require_for(
892 request_principal(&state, &principal).as_ref(),
893 &mongreldb_core::Permission::Admin,
894 ) {
895 return (status_for_error(&error), error.to_string()).into_response();
896 }
897 let recent = state.audit.recent();
898 Json(recent).into_response()
899}
900
901fn default_max_sessions() -> usize {
903 std::env::var("MONGRELBL_MAX_SESSIONS")
904 .ok()
905 .and_then(|v| v.parse().ok())
906 .unwrap_or(256)
907}
908
909fn default_session_idle_timeout() -> std::time::Duration {
911 std::time::Duration::from_secs(
912 std::env::var("MONGRELBL_SESSION_IDLE_TIMEOUT_SECS")
913 .ok()
914 .and_then(|v| v.parse().ok())
915 .unwrap_or(300),
916 )
917}
918
919fn default_replication_wal_segments() -> usize {
920 let replication = std::env::var("MONGRELDB_REPLICATION_WAL_SEGMENTS")
921 .ok()
922 .and_then(|value| value.parse().ok())
923 .unwrap_or(16);
924 let cdc = std::env::var("MONGRELDB_CDC_WAL_SEGMENTS")
925 .ok()
926 .and_then(|value| value.parse().ok())
927 .unwrap_or(16);
928 replication.max(cdc)
929}
930
931fn default_history_retention_epochs() -> u64 {
932 std::env::var("MONGRELDB_HISTORY_RETENTION_EPOCHS")
933 .ok()
934 .and_then(|value| value.parse().ok())
935 .unwrap_or(1024)
936}
937
938fn default_ai_max_concurrent() -> usize {
939 std::env::var("MONGRELDB_AI_MAX_CONCURRENT")
940 .ok()
941 .and_then(|value| value.parse().ok())
942 .filter(|value| *value > 0)
943 .unwrap_or(4)
944}
945
946fn positive_env_u64(name: &str, default: u64) -> u64 {
947 std::env::var(name)
948 .ok()
949 .and_then(|value| value.parse().ok())
950 .filter(|value| *value > 0)
951 .unwrap_or(default)
952}
953
954fn positive_env_usize(name: &str, default: usize) -> usize {
955 std::env::var(name)
956 .ok()
957 .and_then(|value| value.parse().ok())
958 .filter(|value| *value > 0)
959 .unwrap_or(default)
960}
961
962fn default_sql_default_timeout() -> std::time::Duration {
963 std::time::Duration::from_millis(positive_env_u64("MONGRELDB_SQL_DEFAULT_TIMEOUT_MS", 30_000))
964}
965
966fn default_sql_max_timeout() -> std::time::Duration {
967 std::time::Duration::from_millis(positive_env_u64("MONGRELDB_SQL_MAX_TIMEOUT_MS", 300_000))
968}
969
970fn default_sql_max_concurrent() -> usize {
971 positive_env_usize(
972 "MONGRELDB_SQL_MAX_CONCURRENT",
973 std::thread::available_parallelism()
974 .map(usize::from)
975 .unwrap_or(4),
976 )
977}
978
979fn default_sql_max_active_queries() -> usize {
980 positive_env_usize("MONGRELDB_SQL_MAX_ACTIVE_QUERIES", 1_024)
981}
982
983fn default_sql_finished_query_ttl() -> std::time::Duration {
984 std::time::Duration::from_secs(positive_env_u64(
985 "MONGRELDB_SQL_FINISHED_QUERY_TTL_SECS",
986 60,
987 ))
988}
989
990fn default_sql_cancel_grace() -> std::time::Duration {
991 std::time::Duration::from_millis(positive_env_u64("MONGRELDB_SQL_CANCEL_GRACE_MS", 1_000))
992}
993
994fn default_sql_max_output_bytes() -> usize {
995 positive_env_usize("MONGRELDB_SQL_MAX_OUTPUT_BYTES", 64 * 1024 * 1024)
996}
997
998fn default_sql_max_output_rows() -> usize {
999 positive_env_usize("MONGRELDB_SQL_MAX_OUTPUT_ROWS", 1_000_000)
1000}
1001
1002fn request_owner(state: &AppState, principal: &Option<mongreldb_core::Principal>) -> String {
1006 if let Some(p) = principal {
1007 return p.username.clone();
1008 }
1009 if state.auth_token.is_some() {
1010 return "token".into();
1011 }
1012 "anonymous".into()
1013}
1014
1015fn request_principal(
1016 state: &AppState,
1017 principal: &Option<mongreldb_core::Principal>,
1018) -> Option<mongreldb_core::Principal> {
1019 principal.clone().or_else(|| {
1020 state
1021 .auth_token
1022 .as_ref()
1023 .map(|_| mongreldb_core::Principal {
1024 username: "token".into(),
1025 is_admin: true,
1026 roles: Vec::new(),
1027 permissions: Vec::new(),
1028 })
1029 })
1030}
1031
1032async fn create_session(
1037 State(state): State<Arc<AppState>>,
1038 OptionalPrincipal(principal): OptionalPrincipal,
1039) -> Response {
1040 if !state.accepting_sql.load(Ordering::Acquire) {
1041 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
1042 }
1043 let owner = request_owner(&state, &principal);
1044 let session = match MongrelSession::open_with_external_modules_as(
1045 Arc::clone(&state.db),
1046 state.external_modules.iter().cloned(),
1047 request_principal(&state, &principal),
1048 ) {
1049 Ok(session) => session.with_query_registry(Arc::clone(&state.query_registry)),
1050 Err(e) => return (status_for_query_error(&e), e.to_string()).into_response(),
1051 };
1052 match state.sessions.create(session, owner.clone()) {
1053 Some(token) => {
1054 state.audit.record(owner, "session.open", "session created");
1055 Json(json!({ "session_id": token })).into_response()
1056 }
1057 None => (
1058 StatusCode::SERVICE_UNAVAILABLE,
1059 "session limit reached; close an idle session or raise --max-sessions",
1060 )
1061 .into_response(),
1062 }
1063}
1064
1065async fn close_session(
1068 State(state): State<Arc<AppState>>,
1069 OptionalPrincipal(principal): OptionalPrincipal,
1070 Path(id): Path<String>,
1071) -> Response {
1072 let owner = request_owner(&state, &principal);
1073 if let Some(entry) = state.sessions.take_for_close(&id, &owner) {
1074 entry
1075 .session
1076 .query_registry()
1077 .cancel_session(&id, CancellationReason::SessionClosed);
1078 let deadline = tokio::time::Instant::now() + state.sql_cancel_grace;
1079 while entry.session.query_registry().active_for_session(&id) > 0
1080 && tokio::time::Instant::now() < deadline
1081 {
1082 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1083 }
1084 state.audit.record(owner, "session.close", "session closed");
1085 StatusCode::OK.into_response()
1086 } else {
1087 (
1088 StatusCode::NOT_FOUND,
1089 "session not found or not owned by caller",
1090 )
1091 .into_response()
1092 }
1093}
1094
1095async fn dispatch_buffered_sql_format(
1098 state: &AppState,
1099 format: Option<&str>,
1100 output: ManagedQueryBatches,
1101 query_id: QueryId,
1102 test_hook: Option<mongreldb_query::SqlTestHook>,
1103) -> Response {
1104 let format = format.unwrap_or("json").to_owned();
1105 let max_rows = state.sql_max_output_rows;
1106 let max_bytes = state.sql_max_output_bytes;
1107 let serialized = tokio::task::spawn_blocking(move || {
1108 let result =
1109 serialize_buffered_output(&format, &output, max_rows, max_bytes, test_hook.as_ref());
1110 (output, result)
1111 })
1112 .await;
1113 let (output, result) = match serialized {
1114 Ok(result) => result,
1115 Err(error) => {
1116 return with_query_id(
1117 (
1118 StatusCode::INTERNAL_SERVER_ERROR,
1119 Json(json!({
1120 "status": "aborted",
1121 "error": {
1122 "code": "SERIALIZATION_WORKER_FAILED",
1123 "message": error.to_string(),
1124 "query_id": query_id.to_string(),
1125 }
1126 })),
1127 )
1128 .into_response(),
1129 query_id,
1130 )
1131 }
1132 };
1133 match result {
1134 Ok(serialized) => {
1135 state.metrics.add_sql_output_bytes(serialized.bytes.len());
1136 output.complete();
1137 let content_type = if serialized.arrow {
1138 "application/vnd.apache.arrow.file"
1139 } else {
1140 "application/json"
1141 };
1142 with_query_id(
1143 ([(header::CONTENT_TYPE, content_type)], serialized.bytes).into_response(),
1144 query_id,
1145 )
1146 }
1147 Err(BufferedSerializationError::Query(error)) => {
1148 output.fail();
1149 state.metrics.inc_sql_errors();
1150 tracked_query_error_response(state, &error, Some(query_id))
1151 }
1152 Err(BufferedSerializationError::Limit(message)) => {
1153 output.fail();
1154 state.metrics.inc_sql_errors();
1155 with_query_id(
1156 (
1157 StatusCode::PAYLOAD_TOO_LARGE,
1158 Json(json!({
1159 "status": "aborted",
1160 "error": {
1161 "code": "RESULT_LIMIT_EXCEEDED",
1162 "message": message,
1163 "query_id": query_id.to_string(),
1164 }
1165 })),
1166 )
1167 .into_response(),
1168 query_id,
1169 )
1170 }
1171 Err(BufferedSerializationError::Encoding(message)) => {
1172 output.fail();
1173 state.metrics.inc_sql_errors();
1174 with_query_id(
1175 (
1176 StatusCode::INTERNAL_SERVER_ERROR,
1177 Json(json!({
1178 "status": "aborted",
1179 "error": {
1180 "code": "SERIALIZATION_FAILED",
1181 "message": message,
1182 "query_id": query_id.to_string(),
1183 }
1184 })),
1185 )
1186 .into_response(),
1187 query_id,
1188 )
1189 }
1190 }
1191}
1192
1193fn validate_stmt_name(name: &str) -> Result<(), String> {
1197 let mut chars = name.chars();
1198 match chars.next() {
1199 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
1200 _ => return Err("statement name must start with a letter or underscore".into()),
1201 }
1202 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
1203 return Err("statement name may contain only letters, digits, or underscore".into());
1204 }
1205 Ok(())
1206}
1207
1208fn render_sql_literal(v: &serde_json::Value) -> Result<String, String> {
1213 match v {
1214 serde_json::Value::Null => Ok("NULL".into()),
1215 serde_json::Value::Bool(b) => {
1216 if *b {
1217 Ok("TRUE".into())
1218 } else {
1219 Ok("FALSE".into())
1220 }
1221 }
1222 serde_json::Value::Number(n) => Ok(n.to_string()),
1223 serde_json::Value::String(s) => {
1225 let mut out = String::with_capacity(s.len() + 2);
1226 out.push('\'');
1227 for c in s.chars() {
1228 if c == '\'' {
1229 out.push_str("''");
1230 } else {
1231 out.push(c);
1232 }
1233 }
1234 out.push('\'');
1235 Ok(out)
1236 }
1237 _ => Err("prepared-statement parameters must be scalar (null/bool/number/string)".into()),
1239 }
1240}
1241
1242#[derive(Deserialize)]
1243struct PrepareRequest {
1244 name: String,
1245 sql: String,
1246 #[serde(default)]
1247 query_id: Option<QueryId>,
1248 #[serde(default)]
1249 timeout_ms: Option<u64>,
1250}
1251
1252async fn prepare_statement(
1256 State(state): State<Arc<AppState>>,
1257 OptionalPrincipal(principal): OptionalPrincipal,
1258 Path(id): Path<String>,
1259 headers: axum::http::HeaderMap,
1260 Json(req): Json<PrepareRequest>,
1261) -> Response {
1262 if !state.accepting_sql.load(Ordering::Acquire) {
1263 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
1264 }
1265 if let Err(msg) = validate_stmt_name(&req.name) {
1266 return (StatusCode::BAD_REQUEST, msg).into_response();
1267 }
1268 if mongreldb_query::contains_boolean_ai_predicate(&req.sql) {
1269 return remote_boolean_ai_error();
1270 }
1271 let owner = request_owner(&state, &principal);
1272 let Some(entry) = state.sessions.get(&id, &owner) else {
1273 return (
1274 StatusCode::NOT_FOUND,
1275 "session not found or not owned by caller",
1276 )
1277 .into_response();
1278 };
1279 let (options, query_id) = match resolve_query_options(
1280 &state,
1281 &headers,
1282 req.query_id,
1283 req.timeout_ms,
1284 owner,
1285 Some(id),
1286 ) {
1287 Ok(options) => options,
1288 Err(response) => return *response,
1289 };
1290 let query = match entry.session.register_query(options) {
1291 Ok(query) => query,
1292 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
1293 };
1294 let registration = RegisteredQueryGuard::new(query);
1295 let _sql_permit = match acquire_sql_permit(&state, &entry.session, registration.query()).await {
1296 Ok(permit) => permit,
1297 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
1298 };
1299 let _guard = tokio::select! {
1300 guard = entry.lock.lock() => guard,
1301 _ = registration.query().control().cancelled() => {
1302 return query_error_response(
1303 ®istration.query().checkpoint().unwrap_err(),
1304 Some(query_id),
1305 );
1306 }
1307 };
1308 if entry.is_closed() {
1309 return with_query_id(
1310 (StatusCode::NOT_FOUND, "session no longer available").into_response(),
1311 query_id,
1312 );
1313 }
1314 entry.touch();
1315 let sql = format!("PREPARE {} AS {}", req.name, req.sql);
1316 let query = registration.into_query();
1317 match entry.session.run_with_query(&sql, query).await {
1318 Ok(_) => with_query_id(
1319 Json(json!({ "prepared": req.name })).into_response(),
1320 query_id,
1321 ),
1322 Err(error) => tracked_query_error_response(&state, &error, Some(query_id)),
1323 }
1324}
1325
1326#[derive(Deserialize)]
1327struct ExecuteRequest {
1328 name: String,
1329 params: Vec<serde_json::Value>,
1330 #[serde(default)]
1331 format: Option<String>,
1332 #[serde(default)]
1333 query_id: Option<QueryId>,
1334 #[serde(default)]
1335 timeout_ms: Option<u64>,
1336}
1337
1338async fn execute_statement(
1342 State(state): State<Arc<AppState>>,
1343 OptionalPrincipal(principal): OptionalPrincipal,
1344 Path(id): Path<String>,
1345 headers: axum::http::HeaderMap,
1346 Json(req): Json<ExecuteRequest>,
1347) -> Response {
1348 if !state.accepting_sql.load(Ordering::Acquire) {
1349 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
1350 }
1351 if let Err(msg) = validate_stmt_name(&req.name) {
1352 return (StatusCode::BAD_REQUEST, msg).into_response();
1353 }
1354 let owner = request_owner(&state, &principal);
1355 let Some(entry) = state.sessions.get(&id, &owner) else {
1356 return (
1357 StatusCode::NOT_FOUND,
1358 "session not found or not owned by caller",
1359 )
1360 .into_response();
1361 };
1362 let (options, query_id) = match resolve_query_options(
1363 &state,
1364 &headers,
1365 req.query_id,
1366 req.timeout_ms,
1367 owner,
1368 Some(id),
1369 ) {
1370 Ok(options) => options,
1371 Err(response) => return *response,
1372 };
1373 let query = match entry.session.register_query(options) {
1374 Ok(query) => query,
1375 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
1376 };
1377 let registration = RegisteredQueryGuard::new(query);
1378 let sql_permit = match acquire_sql_permit(&state, &entry.session, registration.query()).await {
1379 Ok(permit) => permit,
1380 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
1381 };
1382 let _guard = tokio::select! {
1383 guard = entry.lock.lock() => guard,
1384 _ = registration.query().control().cancelled() => {
1385 return query_error_response(
1386 ®istration.query().checkpoint().unwrap_err(),
1387 Some(query_id),
1388 );
1389 }
1390 };
1391 if entry.is_closed() {
1392 return with_query_id(
1393 (StatusCode::NOT_FOUND, "session no longer available").into_response(),
1394 query_id,
1395 );
1396 }
1397 entry.touch();
1398 state.metrics.inc_sql_queries();
1399 let literals: Vec<String> = match req
1400 .params
1401 .iter()
1402 .map(render_sql_literal)
1403 .collect::<Result<_, _>>()
1404 {
1405 Ok(v) => v,
1406 Err(msg) => {
1407 state.metrics.inc_sql_errors();
1408 return (StatusCode::BAD_REQUEST, msg).into_response();
1409 }
1410 };
1411 let sql = format!("EXECUTE {}({})", req.name, literals.join(", "));
1412 let start = std::time::Instant::now();
1413 let result = if req.format.as_deref() == Some("arrow-stream") {
1414 let query = registration.into_query();
1415 match entry
1416 .session
1417 .run_stream_with_query_for_serialization(&sql, query)
1418 .await
1419 {
1420 Ok((stream, completion)) => Ok(sql_arrow_stream_response_controlled(
1421 stream,
1422 completion,
1423 sql_permit,
1424 state.sql_max_output_rows,
1425 state.sql_max_output_bytes,
1426 Arc::clone(&state.metrics),
1427 )),
1428 Err(error) => Err(error),
1429 }
1430 } else {
1431 let query = registration.into_query();
1432 match entry
1433 .session
1434 .run_with_query_for_serialization(&sql, query)
1435 .await
1436 {
1437 Ok(output) => Ok(dispatch_buffered_sql_format(
1438 &state,
1439 req.format.as_deref(),
1440 output,
1441 query_id,
1442 entry.session.sql_test_hook(),
1443 )
1444 .await),
1445 Err(error) => Err(error),
1446 }
1447 };
1448 let elapsed = start.elapsed();
1449 if elapsed >= state.slow_query_threshold {
1450 state.metrics.inc_slow_queries();
1451 eprintln!(
1452 "[slow-query] {}\u{00b5}s \u{2014} EXECUTE {}",
1453 elapsed.as_micros(),
1454 req.name
1455 );
1456 }
1457 match result {
1458 Ok(response) => with_query_id(response, query_id),
1459 Err(e) => {
1460 state.metrics.inc_sql_errors();
1461 let msg = format!("{e}");
1463 let status = if msg.contains("does not exist") {
1464 StatusCode::NOT_FOUND
1465 } else {
1466 status_for_query_error(&e)
1467 };
1468 if status == status_for_query_error(&e) {
1469 tracked_query_error_response(&state, &e, Some(query_id))
1470 } else {
1471 with_query_id(
1472 (status, format!("{msg} ({}µs)", elapsed.as_micros())).into_response(),
1473 query_id,
1474 )
1475 }
1476 }
1477 }
1478}
1479
1480async fn deallocate_statement(
1483 State(state): State<Arc<AppState>>,
1484 OptionalPrincipal(principal): OptionalPrincipal,
1485 Path((id, name)): Path<(String, String)>,
1486) -> Response {
1487 if let Err(msg) = validate_stmt_name(&name) {
1488 return (StatusCode::BAD_REQUEST, msg).into_response();
1489 }
1490 let owner = request_owner(&state, &principal);
1491 let Some(entry) = state.sessions.get(&id, &owner) else {
1492 return (
1493 StatusCode::NOT_FOUND,
1494 "session not found or not owned by caller",
1495 )
1496 .into_response();
1497 };
1498 let _guard = entry.lock.lock().await;
1499 if entry.is_closed() {
1500 return (StatusCode::NOT_FOUND, "session no longer available").into_response();
1501 }
1502 entry.touch();
1503 let sql = format!("DEALLOCATE {name}");
1504 match entry.session.run(&sql).await {
1505 Ok(_) => Json(json!({ "deallocated": name })).into_response(),
1506 Err(e) => (status_for_query_error(&e), e.to_string()).into_response(),
1507 }
1508}
1509
1510async fn metrics_handler(
1513 State(state): State<Arc<AppState>>,
1514 OptionalPrincipal(principal): OptionalPrincipal,
1515) -> Response {
1516 if let Err(error) = state.db.require_for(
1517 request_principal(&state, &principal).as_ref(),
1518 &mongreldb_core::Permission::Admin,
1519 ) {
1520 return (status_for_error(&error), error.to_string()).into_response();
1521 }
1522 let body = state.metrics.prometheus_text(
1523 state.db.table_names().len(),
1524 state.query_registry.active_count(),
1525 state.query_registry.queued_count(),
1526 state.query_registry.entry_count(),
1527 state.query_registry.approximate_bytes(),
1528 );
1529 (
1530 [(
1531 header::CONTENT_TYPE,
1532 "text/plain; version=0.0.4; charset=utf-8".to_string(),
1533 )],
1534 body,
1535 )
1536 .into_response()
1537}
1538
1539async fn compact_all(
1541 State(state): State<Arc<AppState>>,
1542 OptionalPrincipal(principal): OptionalPrincipal,
1543) -> (StatusCode, Json<serde_json::Value>) {
1544 if let Err(error) = state.db.require_for(
1545 request_principal(&state, &principal).as_ref(),
1546 &mongreldb_core::Permission::Ddl,
1547 ) {
1548 return (
1549 status_for_error(&error),
1550 Json(json!({ "status": "error", "message": error.to_string() })),
1551 );
1552 }
1553 match state.db.compact() {
1554 Ok((compacted, skipped)) => (
1555 StatusCode::OK,
1556 Json(json!({
1557 "status": "ok",
1558 "compacted": compacted,
1559 "skipped": skipped,
1560 })),
1561 ),
1562 Err(e) => (
1563 StatusCode::INTERNAL_SERVER_ERROR,
1564 Json(json!({ "status": "error", "message": format!("{e}") })),
1565 ),
1566 }
1567}
1568
1569async fn compact_table(
1571 State(state): State<Arc<AppState>>,
1572 OptionalPrincipal(principal): OptionalPrincipal,
1573 Path(name): Path<String>,
1574) -> (StatusCode, Json<serde_json::Value>) {
1575 if let Err(error) = state.db.require_for(
1576 request_principal(&state, &principal).as_ref(),
1577 &mongreldb_core::Permission::Ddl,
1578 ) {
1579 return (
1580 status_for_error(&error),
1581 Json(json!({ "status": "error", "table": name, "message": error.to_string() })),
1582 );
1583 }
1584 match state.db.compact_table(&name) {
1585 Ok(true) => (
1586 StatusCode::OK,
1587 Json(json!({ "status": "compacted", "table": name })),
1588 ),
1589 Ok(false) => (
1590 StatusCode::OK,
1591 Json(json!({ "status": "skipped", "table": name, "reason": "fewer than 2 runs" })),
1592 ),
1593 Err(e) => (
1594 StatusCode::INTERNAL_SERVER_ERROR,
1595 Json(json!({ "status": "error", "table": name, "message": format!("{e}") })),
1596 ),
1597 }
1598}
1599
1600#[derive(Deserialize)]
1601struct CreateTableRequest {
1602 name: String,
1603 columns: Vec<ColumnDefJson>,
1604}
1605
1606#[derive(Deserialize)]
1607struct ColumnDefJson {
1608 id: u16,
1609 name: String,
1610 ty: String,
1611 primary_key: bool,
1612 #[serde(default)]
1613 nullable: bool,
1614}
1615
1616async fn create_table(
1617 State(state): State<Arc<AppState>>,
1618 OptionalPrincipal(principal): OptionalPrincipal,
1619 Json(req): Json<CreateTableRequest>,
1620) -> Response {
1621 if let Err(error) = state.db.require_for(
1622 request_principal(&state, &principal).as_ref(),
1623 &mongreldb_core::Permission::Ddl,
1624 ) {
1625 return (status_for_error(&error), error.to_string()).into_response();
1626 }
1627 let mut columns = Vec::new();
1628 for c in &req.columns {
1629 let ty = match c.ty.as_str() {
1630 "int64" | "bigint" => TypeId::Int64,
1631 "float64" | "double" => TypeId::Float64,
1632 "bytes" | "varchar" | "text" => TypeId::Bytes,
1633 "bool" => TypeId::Bool,
1634 other => {
1635 return (StatusCode::BAD_REQUEST, format!("unknown type: {other}")).into_response()
1636 }
1637 };
1638 let mut flags = mongreldb_core::schema::ColumnFlags::empty();
1639 if c.primary_key {
1640 flags = flags.with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY);
1641 }
1642 if c.nullable {
1643 flags = flags.with(mongreldb_core::schema::ColumnFlags::NULLABLE);
1644 }
1645 columns.push(mongreldb_core::schema::ColumnDef {
1646 id: c.id,
1647 name: c.name.clone(),
1648 ty,
1649 flags,
1650 default_value: None,
1651 });
1652 }
1653 let schema = Schema {
1654 schema_id: 0,
1655 columns,
1656 indexes: vec![],
1657 colocation: vec![],
1658 constraints: Default::default(),
1659 clustered: false,
1660 };
1661 if let Err(msg) = validate_table_name(&req.name) {
1662 return (StatusCode::BAD_REQUEST, msg).into_response();
1663 }
1664 match state.db.create_table(&req.name, schema) {
1665 Ok(id) => Json(json!({ "table_id": id })).into_response(),
1666 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1667 }
1668}
1669
1670async fn list_tables(
1671 State(state): State<Arc<AppState>>,
1672 OptionalPrincipal(principal): OptionalPrincipal,
1673) -> Json<Vec<String>> {
1674 let principal = request_principal(&state, &principal);
1675 Json(
1676 state
1677 .db
1678 .table_names()
1679 .into_iter()
1680 .filter(|table| {
1681 state
1682 .db
1683 .select_column_ids_for(table, principal.as_ref())
1684 .is_ok()
1685 })
1686 .collect(),
1687 )
1688}
1689
1690async fn drop_table(
1691 State(state): State<Arc<AppState>>,
1692 OptionalPrincipal(principal): OptionalPrincipal,
1693 Path(name): Path<String>,
1694) -> Response {
1695 if let Err(error) = state.db.require_for(
1696 request_principal(&state, &principal).as_ref(),
1697 &mongreldb_core::Permission::Ddl,
1698 ) {
1699 return (status_for_error(&error), error.to_string()).into_response();
1700 }
1701 match state.db.drop_table(&name) {
1702 Ok(_) => {
1703 state.idem.clear();
1707 StatusCode::OK.into_response()
1708 }
1709 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1710 }
1711}
1712
1713#[derive(Deserialize)]
1714struct PutRequest {
1715 row: Vec<serde_json::Value>,
1716}
1717
1718pub(crate) fn json_to_value(v: &serde_json::Value, expected: &TypeId) -> Value {
1719 match (v, expected) {
1720 (serde_json::Value::Number(n), TypeId::Float64) => {
1721 n.as_f64().map(Value::Float64).unwrap_or(Value::Null)
1722 }
1723 (serde_json::Value::Number(n), TypeId::Int64) => {
1724 n.as_i64().map(Value::Int64).unwrap_or(Value::Null)
1725 }
1726 (serde_json::Value::String(s), TypeId::Bytes) => Value::Bytes(s.as_bytes().to_vec()),
1727 (serde_json::Value::String(s), TypeId::Enum { variants }) => {
1728 if variants.iter().any(|v| v == s) {
1729 Value::Bytes(s.as_bytes().to_vec())
1730 } else {
1731 Value::Null
1732 }
1733 }
1734 (serde_json::Value::Bool(b), TypeId::Bool) => Value::Bool(*b),
1735 (serde_json::Value::Array(arr), TypeId::Embedding { dim }) => {
1738 if arr.len() as u32 != *dim {
1739 return Value::Null;
1740 }
1741 let vec: Option<Vec<f32>> =
1742 arr.iter().map(|el| el.as_f64().map(|f| f as f32)).collect();
1743 vec.map(Value::Embedding).unwrap_or(Value::Null)
1744 }
1745 (serde_json::Value::Null, _) => Value::Null,
1746 (serde_json::Value::Number(n), _) => {
1748 if let Some(i) = n.as_i64() {
1749 Value::Int64(i)
1750 } else if let Some(f) = n.as_f64() {
1751 Value::Float64(f)
1752 } else {
1753 Value::Null
1754 }
1755 }
1756 (serde_json::Value::String(s), _) => Value::Bytes(s.as_bytes().to_vec()),
1757 (serde_json::Value::Bool(b), _) => Value::Bool(*b),
1758 _ => Value::Null,
1759 }
1760}
1761
1762fn parse_cells(
1765 row: &[serde_json::Value],
1766 schema: &mongreldb_core::schema::Schema,
1767) -> Result<Vec<(u16, Value)>, String> {
1768 if row.len() & 1 != 0 {
1769 return Err("row must be an even-length array of [col_id, value] pairs".into());
1770 }
1771 let mut out = Vec::with_capacity(row.len() / 2);
1772 for chunk in row.chunks(2) {
1773 let col_id = chunk[0]
1774 .as_u64()
1775 .ok_or("column id must be a non-negative integer")? as u16;
1776 let expected = schema
1777 .columns
1778 .iter()
1779 .find(|c| c.id == col_id)
1780 .map(|c| c.ty.clone())
1781 .ok_or_else(|| format!("unknown column id {col_id}"))?;
1782 let val = json_to_value(&chunk[1], &expected);
1783 out.push((col_id, val));
1784 }
1785 Ok(out)
1786}
1787
1788pub(crate) fn validate_table_name(name: &str) -> Result<(), String> {
1790 if name.is_empty() {
1791 return Err("table name must not be empty".into());
1792 }
1793 if name.contains('/') || name.contains('\\') || name.contains('\0') {
1794 return Err("table name contains invalid characters".into());
1795 }
1796 Ok(())
1797}
1798
1799async fn put_row(
1800 State(state): State<Arc<AppState>>,
1801 OptionalPrincipal(principal): OptionalPrincipal,
1802 Path(name): Path<String>,
1803 Json(req): Json<PutRequest>,
1804) -> Response {
1805 let handle = match state.db.table(&name) {
1806 Ok(h) => h,
1807 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
1808 };
1809 let schema = handle.lock().schema().clone();
1810 let row = match parse_cells(&req.row, &schema) {
1811 Ok(r) => r,
1812 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
1813 };
1814 state.metrics.inc_puts();
1815 let principal = request_principal(&state, &principal);
1816 match state.db.put_for(&name, row, principal.as_ref()) {
1817 Ok(rid) => Json(json!({ "row_id": rid.0.to_string() })).into_response(),
1818 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1819 }
1820}
1821
1822async fn count(
1823 State(state): State<Arc<AppState>>,
1824 OptionalPrincipal(principal): OptionalPrincipal,
1825 Path(name): Path<String>,
1826) -> Response {
1827 let principal = request_principal(&state, &principal);
1828 match state.db.count_for(&name, principal.as_ref()) {
1829 Ok(count) => Json(json!({ "count": count })).into_response(),
1830 Err(error) => (status_for_error(&error), error.to_string()).into_response(),
1831 }
1832}
1833
1834async fn commit(
1835 State(state): State<Arc<AppState>>,
1836 OptionalPrincipal(principal): OptionalPrincipal,
1837 Path(name): Path<String>,
1838) -> Response {
1839 if let Err(error) = state.db.require_for(
1840 request_principal(&state, &principal).as_ref(),
1841 &mongreldb_core::Permission::Update {
1842 table: name.clone(),
1843 },
1844 ) {
1845 return (status_for_error(&error), error.to_string()).into_response();
1846 }
1847 let handle = match state.db.table(&name) {
1848 Ok(h) => h,
1849 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
1850 };
1851 let mut g = handle.lock();
1852 state.metrics.inc_commits();
1853 match g.commit() {
1854 Ok(epoch) => Json(json!({ "epoch": epoch.0 })).into_response(),
1855 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1856 }
1857}
1858
1859#[derive(Deserialize)]
1860struct SqlRequest {
1861 sql: String,
1862 #[serde(default)]
1865 format: Option<String>,
1866 #[serde(default)]
1868 query_id: Option<QueryId>,
1869 #[serde(default)]
1870 timeout_ms: Option<u64>,
1871}
1872
1873fn query_error_response(
1874 error: &mongreldb_query::MongrelQueryError,
1875 query_id: Option<QueryId>,
1876) -> Response {
1877 use mongreldb_query::MongrelQueryError;
1878 let (code, id) = match error {
1879 MongrelQueryError::QueryCancelled { query_id, .. } => ("QUERY_CANCELLED", Some(*query_id)),
1880 MongrelQueryError::DeadlineExceeded { query_id, .. } => {
1881 ("DEADLINE_EXCEEDED", Some(*query_id))
1882 }
1883 MongrelQueryError::QueryIdConflict { query_id } => ("QUERY_ID_CONFLICT", Some(*query_id)),
1884 MongrelQueryError::QueryRegistryFull => ("QUERY_REGISTRY_FULL", query_id),
1885 MongrelQueryError::TransactionAborted => ("TRANSACTION_ABORTED", query_id),
1886 MongrelQueryError::CommitOutcome { query_id, .. } => ("COMMIT_OUTCOME", Some(*query_id)),
1887 _ => ("QUERY_FAILED", query_id),
1888 };
1889 let mut response = (
1890 status_for_query_error(error),
1891 Json(json!({
1892 "status": "aborted",
1893 "error": {
1894 "code": code,
1895 "message": error.to_string(),
1896 "query_id": id.map(|value| value.to_string()),
1897 }
1898 })),
1899 )
1900 .into_response();
1901 if let Some(id) = id {
1902 add_query_id_header(&mut response, id);
1903 }
1904 response
1905}
1906
1907fn record_query_error(metrics: &metrics::Metrics, error: &mongreldb_query::MongrelQueryError) {
1908 match error {
1909 mongreldb_query::MongrelQueryError::QueryCancelled { reason, .. } => {
1910 metrics.inc_sql_cancelled(*reason)
1911 }
1912 mongreldb_query::MongrelQueryError::DeadlineExceeded { .. } => {
1913 metrics.inc_sql_deadline_exceeded();
1914 metrics.inc_sql_cancelled(CancellationReason::Deadline);
1915 }
1916 _ => {}
1917 }
1918}
1919
1920fn tracked_query_error_response(
1921 state: &AppState,
1922 error: &mongreldb_query::MongrelQueryError,
1923 query_id: Option<QueryId>,
1924) -> Response {
1925 record_query_error(&state.metrics, error);
1926 if let mongreldb_query::MongrelQueryError::QueryCancelled { query_id, .. } = error {
1927 if let Some(requested_at) = state
1928 .query_registry
1929 .status(*query_id)
1930 .and_then(|status| status.cancel_requested_at)
1931 {
1932 state
1933 .metrics
1934 .observe_sql_cancel_latency(requested_at.elapsed());
1935 }
1936 }
1937 query_error_response(error, query_id)
1938}
1939
1940fn add_query_id_header(response: &mut Response, query_id: QueryId) {
1941 if let Ok(value) = axum::http::HeaderValue::from_str(&query_id.to_string()) {
1942 response.headers_mut().insert("x-mongreldb-query-id", value);
1943 }
1944}
1945
1946fn with_query_id(mut response: Response, query_id: QueryId) -> Response {
1947 add_query_id_header(&mut response, query_id);
1948 response
1949}
1950
1951fn bad_query_control_request(message: impl Into<String>, query_id: Option<QueryId>) -> Response {
1952 let mut response = (
1953 StatusCode::BAD_REQUEST,
1954 Json(json!({
1955 "status": "aborted",
1956 "error": {
1957 "code": "INVALID_QUERY_OPTIONS",
1958 "message": message.into(),
1959 "query_id": query_id.map(|value| value.to_string()),
1960 }
1961 })),
1962 )
1963 .into_response();
1964 if let Some(query_id) = query_id {
1965 add_query_id_header(&mut response, query_id);
1966 }
1967 response
1968}
1969
1970fn resolve_query_options(
1971 state: &AppState,
1972 headers: &axum::http::HeaderMap,
1973 body_query_id: Option<QueryId>,
1974 body_timeout_ms: Option<u64>,
1975 owner: String,
1976 session_id: Option<String>,
1977) -> std::result::Result<(SqlQueryOptions, QueryId), Box<Response>> {
1978 let query_id = match body_query_id {
1979 Some(query_id) => query_id,
1980 None => match headers.get("x-mongreldb-query-id") {
1981 Some(value) => {
1982 let value = value.to_str().map_err(|_| {
1983 Box::new(bad_query_control_request(
1984 "X-MongrelDB-Query-ID is not valid text",
1985 None,
1986 ))
1987 })?;
1988 value
1989 .parse()
1990 .map_err(|error: mongreldb_query::MongrelQueryError| {
1991 Box::new(bad_query_control_request(error.to_string(), None))
1992 })?
1993 }
1994 None => {
1995 QueryId::random().map_err(|error| Box::new(query_error_response(&error, None)))?
1996 }
1997 },
1998 };
1999 let timeout_ms = match body_timeout_ms {
2000 Some(timeout_ms) => timeout_ms,
2001 None => match headers.get("x-mongreldb-timeout-ms") {
2002 Some(value) => value
2003 .to_str()
2004 .ok()
2005 .and_then(|value| value.parse::<u64>().ok())
2006 .ok_or_else(|| {
2007 Box::new(bad_query_control_request(
2008 "X-MongrelDB-Timeout-Ms must be a positive integer",
2009 Some(query_id),
2010 ))
2011 })?,
2012 None => state
2013 .sql_default_timeout
2014 .as_millis()
2015 .min(u128::from(u64::MAX)) as u64,
2016 },
2017 };
2018 if timeout_ms == 0 {
2019 return Err(Box::new(bad_query_control_request(
2020 "timeout_ms must be positive",
2021 Some(query_id),
2022 )));
2023 }
2024 let timeout = std::time::Duration::from_millis(timeout_ms);
2025 if timeout > state.sql_max_timeout {
2026 return Err(Box::new(bad_query_control_request(
2027 format!(
2028 "timeout_ms exceeds server maximum of {}",
2029 state.sql_max_timeout.as_millis()
2030 ),
2031 Some(query_id),
2032 )));
2033 }
2034 Ok((
2035 SqlQueryOptions {
2036 query_id: Some(query_id),
2037 timeout: Some(timeout),
2038 owner: Some(owner),
2039 session_id,
2040 parent_control: None,
2041 },
2042 query_id,
2043 ))
2044}
2045
2046async fn acquire_sql_permit(
2047 state: &AppState,
2048 session: &MongrelSession,
2049 query: &RegisteredSqlQuery,
2050) -> std::result::Result<tokio::sync::OwnedSemaphorePermit, mongreldb_query::MongrelQueryError> {
2051 session.fire_test_hook(mongreldb_query::SqlTestHookPoint::WaitingForSqlPermit);
2052 tokio::select! {
2053 permit = Arc::clone(&state.sql_semaphore).acquire_owned() => permit.map_err(|_| {
2054 mongreldb_query::MongrelQueryError::InvalidQueryState(
2055 "SQL admission semaphore closed".into(),
2056 )
2057 }),
2058 _ = query.control().cancelled() => Err(query.checkpoint().unwrap_err()),
2059 }
2060}
2061
2062fn caller_may_manage_query(
2063 state: &AppState,
2064 principal: &Option<mongreldb_core::Principal>,
2065 owner: Option<&str>,
2066) -> bool {
2067 request_principal(state, principal).is_some_and(|principal| principal.is_admin)
2068 || owner == Some(request_owner(state, principal).as_str())
2069}
2070
2071fn query_phase_name(phase: SqlQueryPhase) -> &'static str {
2072 match phase {
2073 SqlQueryPhase::Queued => "queued",
2074 SqlQueryPhase::Planning => "planning",
2075 SqlQueryPhase::Executing => "executing",
2076 SqlQueryPhase::Streaming => "streaming",
2077 SqlQueryPhase::Serializing => "serializing",
2078 SqlQueryPhase::CommitCritical => "commit_critical",
2079 SqlQueryPhase::Cancelling => "cancelling",
2080 SqlQueryPhase::Completed => "completed",
2081 SqlQueryPhase::Failed => "failed",
2082 SqlQueryPhase::Cancelled => "cancelled",
2083 }
2084}
2085
2086fn commit_fence_outcome_name(outcome: mongreldb_query::CommitFenceOutcome) -> &'static str {
2087 match outcome {
2088 mongreldb_query::CommitFenceOutcome::NotReached => "not_reached",
2089 mongreldb_query::CommitFenceOutcome::CancelWon => "cancel_won",
2090 mongreldb_query::CommitFenceOutcome::CommitWon => "commit_won",
2091 }
2092}
2093
2094async fn query_status(
2095 State(state): State<Arc<AppState>>,
2096 OptionalPrincipal(principal): OptionalPrincipal,
2097 Path(query_id): Path<String>,
2098) -> Response {
2099 let Ok(query_id) = query_id.parse::<QueryId>() else {
2100 return StatusCode::NOT_FOUND.into_response();
2101 };
2102 let Some(status) = state.query_registry.status(query_id) else {
2103 return StatusCode::NOT_FOUND.into_response();
2104 };
2105 if !caller_may_manage_query(&state, &principal, status.owner.as_deref()) {
2106 return StatusCode::NOT_FOUND.into_response();
2107 }
2108 let response = Json(json!({
2109 "query_id": query_id.to_string(),
2110 "state": query_phase_name(status.phase),
2111 "started_ms_ago": status.started_at.elapsed().as_millis(),
2112 "deadline_ms_remaining": status.deadline.map(|deadline| {
2113 deadline.saturating_duration_since(std::time::Instant::now()).as_millis()
2114 }),
2115 "session_id": status.session_id,
2116 "operation": status.operation,
2117 "committed": status.committed,
2118 "cancellation_reason": format!("{:?}", status.cancellation_reason).to_ascii_lowercase(),
2119 "completed_statements": status.completed_statements,
2120 "statement_index": status.statement_index,
2121 "trace": {
2122 "queue_duration_us": status.queue_duration.as_micros(),
2123 "planning_duration_us": status.planning_duration.as_micros(),
2124 "execution_duration_us": status.execution_duration.as_micros(),
2125 "serialization_duration_us": status.serialization_duration.as_micros(),
2126 "cancel_requested_phase": status.cancel_requested_phase.map(query_phase_name),
2127 "cancel_observed_phase": status.cancel_observed_phase.map(query_phase_name),
2128 "commit_fence_outcome": commit_fence_outcome_name(status.commit_fence_outcome),
2129 },
2130 }))
2131 .into_response();
2132 with_query_id(response, query_id)
2133}
2134
2135async fn cancel_query(
2136 State(state): State<Arc<AppState>>,
2137 OptionalPrincipal(principal): OptionalPrincipal,
2138 Path(query_id): Path<String>,
2139) -> Response {
2140 let Ok(query_id) = query_id.parse::<QueryId>() else {
2141 return StatusCode::NOT_FOUND.into_response();
2142 };
2143 let Some(status) = state.query_registry.status(query_id) else {
2144 return StatusCode::NOT_FOUND.into_response();
2145 };
2146 if !caller_may_manage_query(&state, &principal, status.owner.as_deref()) {
2147 return StatusCode::NOT_FOUND.into_response();
2148 }
2149 state.metrics.inc_sql_cancel_requests();
2150 let (http_status, body) = match state.query_registry.cancel(query_id) {
2151 CancelOutcome::Accepted => (
2152 {
2153 state.metrics.inc_sql_commit_cancel_winner_cancel();
2154 StatusCode::ACCEPTED
2155 },
2156 json!({ "query_id": query_id.to_string(), "state": "cancellation_requested" }),
2157 ),
2158 CancelOutcome::AlreadyCancelling => (
2159 StatusCode::OK,
2160 json!({ "query_id": query_id.to_string(), "state": "cancelling" }),
2161 ),
2162 CancelOutcome::TooLate => (
2163 {
2164 state.metrics.inc_sql_commit_cancel_winner_commit();
2165 StatusCode::CONFLICT
2166 },
2167 json!({
2168 "query_id": query_id.to_string(),
2169 "state": "commit_critical",
2170 "error": {
2171 "code": "CANCEL_TOO_LATE",
2172 "message": "the query has entered its durable commit phase"
2173 }
2174 }),
2175 ),
2176 CancelOutcome::AlreadyFinished => (
2177 StatusCode::OK,
2178 json!({ "query_id": query_id.to_string(), "state": "finished" }),
2179 ),
2180 CancelOutcome::NotFound => return StatusCode::NOT_FOUND.into_response(),
2181 };
2182 with_query_id((http_status, Json(body)).into_response(), query_id)
2183}
2184
2185async fn sql(
2186 State(state): State<Arc<AppState>>,
2187 OptionalPrincipal(principal): OptionalPrincipal,
2188 headers: axum::http::HeaderMap,
2189 Json(req): Json<SqlRequest>,
2190) -> Response {
2191 if !state.accepting_sql.load(Ordering::Acquire) {
2192 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
2193 }
2194 if mongreldb_query::contains_boolean_ai_predicate(&req.sql) {
2195 return remote_boolean_ai_error();
2196 }
2197 let session_id = headers
2202 .get("x-session-id")
2203 .and_then(|v| v.to_str().ok())
2204 .map(str::to_owned);
2205
2206 let owner = request_owner(&state, &principal);
2207 if let Some(sid) = session_id {
2208 let Some(entry) = state.sessions.get(&sid, &owner) else {
2209 return (
2210 StatusCode::NOT_FOUND,
2211 "session not found or not owned by caller",
2212 )
2213 .into_response();
2214 };
2215 let (options, query_id) = match resolve_query_options(
2216 &state,
2217 &headers,
2218 req.query_id,
2219 req.timeout_ms,
2220 owner,
2221 Some(sid),
2222 ) {
2223 Ok(options) => options,
2224 Err(response) => return *response,
2225 };
2226 let query = match entry.session.register_query(options) {
2227 Ok(query) => query,
2228 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2229 };
2230 let registration = RegisteredQueryGuard::new(query);
2231 let sql_permit =
2232 match acquire_sql_permit(&state, &entry.session, registration.query()).await {
2233 Ok(permit) => permit,
2234 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2235 };
2236 let _guard = tokio::select! {
2238 guard = entry.lock.lock() => guard,
2239 _ = registration.query().control().cancelled() => {
2240 return query_error_response(
2241 ®istration.query().checkpoint().unwrap_err(),
2242 Some(query_id),
2243 );
2244 }
2245 };
2246 if entry.is_closed() {
2249 return (StatusCode::NOT_FOUND, "session no longer available").into_response();
2250 }
2251 entry.touch();
2252 let query = registration.into_query();
2253 execute_sql(
2254 &state,
2255 &principal,
2256 &entry.session,
2257 req,
2258 query,
2259 query_id,
2260 sql_permit,
2261 )
2262 .await
2263 } else {
2264 let session = match MongrelSession::open_with_external_modules_as(
2265 Arc::clone(&state.db),
2266 state.external_modules.iter().cloned(),
2267 request_principal(&state, &principal),
2268 ) {
2269 Ok(session) => session.with_query_registry(Arc::clone(&state.query_registry)),
2270 Err(e) => return (status_for_query_error(&e), e.to_string()).into_response(),
2271 };
2272 let (options, query_id) = match resolve_query_options(
2273 &state,
2274 &headers,
2275 req.query_id,
2276 req.timeout_ms,
2277 owner,
2278 None,
2279 ) {
2280 Ok(options) => options,
2281 Err(response) => return *response,
2282 };
2283 let query = match session.register_query(options) {
2284 Ok(query) => query,
2285 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2286 };
2287 let registration = RegisteredQueryGuard::new(query);
2288 let sql_permit = match acquire_sql_permit(&state, &session, registration.query()).await {
2289 Ok(permit) => permit,
2290 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2291 };
2292 let query = registration.into_query();
2293 execute_sql(
2294 &state, &principal, &session, req, query, query_id, sql_permit,
2295 )
2296 .await
2297 }
2298}
2299
2300async fn execute_sql(
2305 state: &AppState,
2306 principal: &Option<mongreldb_core::Principal>,
2307 session: &MongrelSession,
2308 req: SqlRequest,
2309 query: RegisteredSqlQuery,
2310 query_id: QueryId,
2311 sql_permit: tokio::sync::OwnedSemaphorePermit,
2312) -> Response {
2313 state.metrics.inc_sql_queries();
2314 let audited = audit::is_audited_sql(&req.sql);
2315 let actor = request_owner(state, principal);
2316 let start = std::time::Instant::now();
2317 let result = if req.format.as_deref() == Some("arrow-stream") {
2323 match session
2324 .run_stream_with_query_for_serialization(&req.sql, query)
2325 .await
2326 {
2327 Ok((stream, completion)) => Ok(sql_arrow_stream_response_controlled(
2328 stream,
2329 completion,
2330 sql_permit,
2331 state.sql_max_output_rows,
2332 state.sql_max_output_bytes,
2333 Arc::clone(&state.metrics),
2334 )),
2335 Err(error) => Err(error),
2336 }
2337 } else {
2338 match session
2339 .run_with_query_for_serialization(&req.sql, query)
2340 .await
2341 {
2342 Ok(output) => Ok(dispatch_buffered_sql_format(
2343 state,
2344 req.format.as_deref(),
2345 output,
2346 query_id,
2347 session.sql_test_hook(),
2348 )
2349 .await),
2350 Err(error) => Err(error),
2351 }
2352 };
2353 let elapsed = start.elapsed();
2354 if elapsed >= state.slow_query_threshold {
2357 state.metrics.inc_slow_queries();
2358 eprintln!(
2359 "[slow-query] {}\u{00b5}s query_id={} operation={}",
2360 elapsed.as_micros(),
2361 query_id,
2362 safe_sql_operation(&req.sql)
2363 );
2364 }
2365 if audited {
2368 let (action, detail) = audit::redacted_ddl_detail(&req.sql, result.is_ok());
2369 state.audit.record(actor, action, detail);
2370 }
2371 match result {
2372 Ok(response) => with_query_id(response, query_id),
2373 Err(e) => {
2374 state.metrics.inc_sql_errors();
2375 tracked_query_error_response(state, &e, Some(query_id))
2376 }
2377 }
2378}
2379
2380fn safe_sql_operation(sql: &str) -> String {
2381 sql.split_whitespace()
2382 .next()
2383 .unwrap_or("UNKNOWN")
2384 .chars()
2385 .filter(|character| character.is_ascii_alphabetic())
2386 .take(16)
2387 .collect::<String>()
2388 .to_ascii_uppercase()
2389}
2390
2391fn remote_boolean_ai_error() -> Response {
2392 (
2393 StatusCode::BAD_REQUEST,
2394 "Boolean ANN/Sparse SQL is disabled remotely; use scored SQL functions",
2395 )
2396 .into_response()
2397}
2398
2399#[derive(Debug)]
2400struct SerializedOutput {
2401 bytes: Vec<u8>,
2402 arrow: bool,
2403}
2404
2405#[derive(Debug)]
2406enum BufferedSerializationError {
2407 Query(mongreldb_query::MongrelQueryError),
2408 Limit(String),
2409 Encoding(String),
2410}
2411
2412struct LimitedOutput {
2413 bytes: Vec<u8>,
2414 max_bytes: usize,
2415 exceeded: bool,
2416}
2417
2418impl LimitedOutput {
2419 fn new(max_bytes: usize) -> Self {
2420 Self {
2421 bytes: Vec::new(),
2422 max_bytes,
2423 exceeded: false,
2424 }
2425 }
2426}
2427
2428impl std::io::Write for LimitedOutput {
2429 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
2430 if self.bytes.len().saturating_add(bytes.len()) > self.max_bytes {
2431 self.exceeded = true;
2432 return Err(std::io::Error::other("SQL output byte limit exceeded"));
2433 }
2434 self.bytes.extend_from_slice(bytes);
2435 Ok(bytes.len())
2436 }
2437
2438 fn flush(&mut self) -> std::io::Result<()> {
2439 Ok(())
2440 }
2441}
2442
2443fn serialize_buffered_output(
2444 format: &str,
2445 output: &ManagedQueryBatches,
2446 max_rows: usize,
2447 max_bytes: usize,
2448 test_hook: Option<&mongreldb_query::SqlTestHook>,
2449) -> std::result::Result<SerializedOutput, BufferedSerializationError> {
2450 const ROW_CHECKPOINT_INTERVAL: usize = 256;
2451 let batches = output.batches();
2452 let mut rows = 0usize;
2453 let mut writer_output = LimitedOutput::new(max_bytes);
2454
2455 if format == "arrow" {
2456 if batches.is_empty() {
2457 return Ok(SerializedOutput {
2458 bytes: Vec::new(),
2459 arrow: true,
2460 });
2461 }
2462 let schema = batches[0].schema();
2463 let encoding_result = (|| {
2464 let mut writer =
2465 arrow::ipc::writer::FileWriter::try_new(&mut writer_output, schema.as_ref())
2466 .map_err(|error| error.to_string())?;
2467 for batch in batches {
2468 for offset in (0..batch.num_rows()).step_by(ROW_CHECKPOINT_INTERVAL) {
2469 if let Some(hook) = test_hook {
2470 hook(mongreldb_query::SqlTestHookPoint::BeforeSerializationBatch);
2471 }
2472 output
2473 .query()
2474 .checkpoint()
2475 .map_err(|error| error.to_string())?;
2476 let length = ROW_CHECKPOINT_INTERVAL.min(batch.num_rows() - offset);
2477 rows = rows.saturating_add(length);
2478 if rows > max_rows {
2479 return Err("SQL output row limit exceeded".into());
2480 }
2481 writer
2482 .write(&batch.slice(offset, length))
2483 .map_err(|error| error.to_string())?;
2484 }
2485 }
2486 writer.finish().map_err(|error| error.to_string())
2487 })();
2488 if let Err(error) = encoding_result {
2489 if let Err(query_error) = output.query().checkpoint() {
2490 return Err(BufferedSerializationError::Query(query_error));
2491 }
2492 if writer_output.exceeded || rows > max_rows {
2493 return Err(BufferedSerializationError::Limit(error));
2494 }
2495 return Err(BufferedSerializationError::Encoding(error));
2496 }
2497 return Ok(SerializedOutput {
2498 bytes: writer_output.bytes,
2499 arrow: true,
2500 });
2501 }
2502
2503 let encoding_result = (|| {
2504 let mut writer = arrow::json::writer::ArrayWriter::new(&mut writer_output);
2505 for batch in batches {
2506 for offset in (0..batch.num_rows()).step_by(ROW_CHECKPOINT_INTERVAL) {
2507 if let Some(hook) = test_hook {
2508 hook(mongreldb_query::SqlTestHookPoint::BeforeSerializationBatch);
2509 }
2510 output
2511 .query()
2512 .checkpoint()
2513 .map_err(|error| error.to_string())?;
2514 let length = ROW_CHECKPOINT_INTERVAL.min(batch.num_rows() - offset);
2515 rows = rows.saturating_add(length);
2516 if rows > max_rows {
2517 return Err("SQL output row limit exceeded".into());
2518 }
2519 let slice = batch.slice(offset, length);
2520 writer
2521 .write_batches(&[&slice])
2522 .map_err(|error| error.to_string())?;
2523 }
2524 }
2525 writer.finish().map_err(|error| error.to_string())
2526 })();
2527 if let Err(error) = encoding_result {
2528 if let Err(query_error) = output.query().checkpoint() {
2529 return Err(BufferedSerializationError::Query(query_error));
2530 }
2531 if writer_output.exceeded || rows > max_rows {
2532 return Err(BufferedSerializationError::Limit(error));
2533 }
2534 return Err(BufferedSerializationError::Encoding(error));
2535 }
2536 Ok(SerializedOutput {
2537 bytes: writer_output.bytes,
2538 arrow: false,
2539 })
2540}
2541
2542#[cfg(test)]
2545fn sql_arrow_stream_response(batches: mongreldb_query::MongrelRecordBatchStream) -> Response {
2546 use futures::{stream, StreamExt};
2547
2548 const STREAM_CT: &str = "application/vnd.apache.arrow.stream";
2549
2550 let schema = batches.schema();
2551 let mut writer = match arrow::ipc::writer::StreamWriter::try_new(Vec::new(), schema.as_ref()) {
2552 Ok(w) => w,
2553 Err(e) => {
2554 return (
2555 StatusCode::INTERNAL_SERVER_ERROR,
2556 format!("arrow stream init error: {e}"),
2557 )
2558 .into_response()
2559 }
2560 };
2561 let schema_chunk: Vec<u8> = std::mem::take(writer.get_mut());
2564 let batch_stream = stream::unfold(
2565 (batches, Some(writer)),
2566 |(mut batches, writer)| async move {
2567 let mut writer = writer?;
2568 match batches.next().await {
2569 Some(Ok(batch)) => match writer.write(&batch) {
2570 Ok(()) => {
2571 let chunk = std::mem::take(writer.get_mut());
2572 Some((Ok(chunk), (batches, Some(writer))))
2573 }
2574 Err(error) => Some((Err(std::io::Error::other(error)), (batches, None))),
2575 },
2576 Some(Err(error)) => Some((Err(std::io::Error::other(error)), (batches, None))),
2577 None => match writer.finish() {
2578 Ok(()) => {
2579 let chunk = std::mem::take(writer.get_mut());
2580 Some((Ok(chunk), (batches, None)))
2581 }
2582 Err(error) => Some((Err(std::io::Error::other(error)), (batches, None))),
2583 },
2584 }
2585 },
2586 );
2587
2588 let schema_item: Result<Vec<u8>, std::io::Error> = Ok(schema_chunk);
2591 let full = stream::iter([schema_item]).chain(batch_stream);
2592 let body = axum::body::Body::from_stream(full);
2593 ([(header::CONTENT_TYPE, STREAM_CT)], body).into_response()
2594}
2595
2596fn sql_arrow_stream_response_controlled(
2597 batches: mongreldb_query::MongrelRecordBatchStream,
2598 completion: SqlStreamCompletion,
2599 sql_permit: tokio::sync::OwnedSemaphorePermit,
2600 max_rows: usize,
2601 max_bytes: usize,
2602 metrics: Arc<metrics::Metrics>,
2603) -> Response {
2604 use futures::{stream, StreamExt};
2605
2606 const STREAM_CT: &str = "application/vnd.apache.arrow.stream";
2607 let schema = batches.schema();
2608 let mut writer = match arrow::ipc::writer::StreamWriter::try_new(Vec::new(), schema.as_ref()) {
2609 Ok(writer) => writer,
2610 Err(error) => {
2611 return (
2612 StatusCode::INTERNAL_SERVER_ERROR,
2613 format!("arrow stream init error: {error}"),
2614 )
2615 .into_response()
2616 }
2617 };
2618 let schema_chunk = std::mem::take(writer.get_mut());
2619 if schema_chunk.len() > max_bytes {
2620 return (
2621 StatusCode::PAYLOAD_TOO_LARGE,
2622 "SQL output byte limit exceeded",
2623 )
2624 .into_response();
2625 }
2626 metrics.add_sql_output_bytes(schema_chunk.len());
2627 let batch_stream = stream::unfold(
2628 (
2629 batches,
2630 Some(writer),
2631 completion,
2632 Some(sql_permit),
2633 0usize,
2634 schema_chunk.len(),
2635 metrics,
2636 ),
2637 move |(mut batches, writer, completion, permit, rows, bytes, metrics)| async move {
2638 let mut writer = writer?;
2639 match batches.next().await {
2640 Some(Ok(batch)) => {
2641 let next_rows = rows.saturating_add(batch.num_rows());
2642 if next_rows > max_rows {
2643 return Some((
2644 Err(std::io::Error::other("SQL output row limit exceeded")),
2645 (batches, None, completion, permit, next_rows, bytes, metrics),
2646 ));
2647 }
2648 match writer.write(&batch) {
2649 Ok(()) => {
2650 let chunk = std::mem::take(writer.get_mut());
2651 let next_bytes = bytes.saturating_add(chunk.len());
2652 if next_bytes > max_bytes {
2653 return Some((
2654 Err(std::io::Error::other("SQL output byte limit exceeded")),
2655 (
2656 batches, None, completion, permit, next_rows, next_bytes,
2657 metrics,
2658 ),
2659 ));
2660 }
2661 metrics.add_sql_output_bytes(chunk.len());
2662 Some((
2663 Ok(chunk),
2664 (
2665 batches,
2666 Some(writer),
2667 completion,
2668 permit,
2669 next_rows,
2670 next_bytes,
2671 metrics,
2672 ),
2673 ))
2674 }
2675 Err(error) => Some((
2676 Err(std::io::Error::other(error)),
2677 (batches, None, completion, permit, rows, bytes, metrics),
2678 )),
2679 }
2680 }
2681 Some(Err(error)) => Some((
2682 Err(std::io::Error::other(error)),
2683 (batches, None, completion, permit, rows, bytes, metrics),
2684 )),
2685 None => match writer.finish() {
2686 Ok(()) => {
2687 let chunk = std::mem::take(writer.get_mut());
2688 let next_bytes = bytes.saturating_add(chunk.len());
2689 if next_bytes > max_bytes {
2690 return Some((
2691 Err(std::io::Error::other("SQL output byte limit exceeded")),
2692 (batches, None, completion, permit, rows, next_bytes, metrics),
2693 ));
2694 }
2695 metrics.add_sql_output_bytes(chunk.len());
2696 completion.complete();
2697 Some((
2698 Ok(chunk),
2699 (batches, None, completion, permit, rows, next_bytes, metrics),
2700 ))
2701 }
2702 Err(error) => Some((
2703 Err(std::io::Error::other(error)),
2704 (batches, None, completion, permit, rows, bytes, metrics),
2705 )),
2706 },
2707 }
2708 },
2709 );
2710 let schema_item: Result<Vec<u8>, std::io::Error> = Ok(schema_chunk);
2711 let body = axum::body::Body::from_stream(stream::iter([schema_item]).chain(batch_stream));
2712 ([(header::CONTENT_TYPE, STREAM_CT)], body).into_response()
2713}
2714
2715#[derive(Deserialize)]
2716struct TxnOp {
2717 table: String,
2718 op: String,
2719 cells: Option<Vec<serde_json::Value>>,
2720 row_id: Option<u64>,
2721}
2722
2723#[derive(Deserialize)]
2724struct TxnRequest {
2725 ops: Vec<TxnOp>,
2726}
2727
2728async fn txn(
2729 State(state): State<Arc<AppState>>,
2730 OptionalPrincipal(principal): OptionalPrincipal,
2731 Json(req): Json<TxnRequest>,
2732) -> Response {
2733 let mut parsed: Vec<(String, TxnAction)> = Vec::with_capacity(req.ops.len());
2737 for op in &req.ops {
2738 match op.op.as_str() {
2739 "put" => {
2740 let cells_json = match op.cells.as_ref() {
2741 Some(c) if !c.is_empty() => c,
2742 _ => {
2743 return (StatusCode::BAD_REQUEST, "put op requires non-empty cells")
2744 .into_response()
2745 }
2746 };
2747 let handle = match state.db.table(&op.table) {
2748 Ok(h) => h,
2749 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
2750 };
2751 let schema = handle.lock().schema().clone();
2752 let cells = match parse_cells(cells_json, &schema) {
2753 Ok(c) => c,
2754 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
2755 };
2756 parsed.push((op.table.clone(), TxnAction::Put(cells)));
2757 }
2758 "delete" => {
2759 let rid = match op.row_id {
2760 Some(r) => r,
2761 None => {
2762 return (StatusCode::BAD_REQUEST, "delete op requires row_id")
2763 .into_response()
2764 }
2765 };
2766 parsed.push((op.table.clone(), TxnAction::Delete(rid)));
2767 }
2768 other => {
2769 return (StatusCode::BAD_REQUEST, format!("unknown op: {other}")).into_response()
2770 }
2771 }
2772 }
2773
2774 state.metrics.inc_txns();
2775 let mut transaction = state.db.begin_as(request_principal(&state, &principal));
2776 let result = (|| {
2777 for (table, action) in &parsed {
2778 match action {
2779 TxnAction::Put(cells) => {
2780 transaction.put(table, cells.clone())?;
2781 }
2782 TxnAction::Delete(rid) => {
2783 transaction.delete(table, mongreldb_core::RowId(*rid))?;
2784 }
2785 }
2786 }
2787 transaction.commit()
2788 })();
2789 match result {
2790 Ok(_) => Json(json!({ "status": "committed" })).into_response(),
2791 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
2792 }
2793}
2794
2795enum TxnAction {
2796 Put(Vec<(u16, Value)>),
2797 Delete(u64),
2798}
2799
2800#[cfg(test)]
2801mod auth_tests {
2802 use super::*;
2803 use mongreldb_core::Database;
2804 use tempfile::tempdir;
2805
2806 #[test]
2807 fn slow_query_operation_does_not_include_literals() {
2808 let sql = "CREATE USER alice PASSWORD 'never-log-this'";
2809 let operation = safe_sql_operation(sql);
2810 assert_eq!(operation, "CREATE");
2811 assert!(!operation.contains("never-log-this"));
2812 }
2813
2814 #[tokio::test]
2815 async fn auth_rejects_missing_token() {
2816 let dir = tempdir().unwrap();
2817 let db = Arc::new(Database::create(dir.path()).unwrap());
2818 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
2819 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2820 let addr = listener.local_addr().unwrap();
2821 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2822 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2824
2825 let client = reqwest::Client::new();
2826 let resp = client
2827 .get(format!("http://{addr}/health"))
2828 .send()
2829 .await
2830 .unwrap();
2831 assert_eq!(resp.status(), 401);
2832 }
2833
2834 #[tokio::test]
2835 async fn auth_accepts_valid_token() {
2836 let dir = tempdir().unwrap();
2837 let db = Arc::new(Database::create(dir.path()).unwrap());
2838 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
2839 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2840 let addr = listener.local_addr().unwrap();
2841 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2842 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2843
2844 let client = reqwest::Client::new();
2845 let resp = client
2846 .get(format!("http://{addr}/health"))
2847 .header("Authorization", "Bearer secret")
2848 .send()
2849 .await
2850 .unwrap();
2851 assert_eq!(resp.status(), 200);
2852 }
2853
2854 #[tokio::test]
2855 async fn no_auth_when_token_unset() {
2856 let dir = tempdir().unwrap();
2857 let db = Arc::new(Database::create(dir.path()).unwrap());
2858 let app = build_app_with_config(db, std::iter::empty(), None, None);
2859 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2860 let addr = listener.local_addr().unwrap();
2861 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2862 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2863
2864 let client = reqwest::Client::new();
2865 let resp = client
2866 .get(format!("http://{addr}/health"))
2867 .send()
2868 .await
2869 .unwrap();
2870 assert_eq!(resp.status(), 200);
2871 }
2872
2873 #[tokio::test]
2874 async fn capabilities_advertise_sql_cancellation_v1() {
2875 let dir = tempdir().unwrap();
2876 let db = Arc::new(Database::create(dir.path()).unwrap());
2877 let app = build_app_with_config(db, std::iter::empty(), None, None);
2878 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2879 let addr = listener.local_addr().unwrap();
2880 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2881
2882 let body: serde_json::Value = reqwest::Client::new()
2883 .get(format!("http://{addr}/capabilities"))
2884 .send()
2885 .await
2886 .unwrap()
2887 .json()
2888 .await
2889 .unwrap();
2890 assert_eq!(body["sql_cancellation"]["version"], 1);
2891 assert_eq!(body["sql_cancellation"]["client_query_ids"], true);
2892 assert_eq!(body["sql_cancellation"]["cancel_endpoint"], true);
2893 assert_eq!(body["sql_cancellation"]["query_status"], true);
2894 assert_eq!(body["sql_cancellation"]["stream_disconnect_cancels"], true);
2895 }
2896}
2897
2898#[cfg(test)]
2899mod wal_stream_tests {
2900 use super::*;
2901 use mongreldb_client::ReplicationFollower;
2902 use mongreldb_core::Database;
2903 use tempfile::tempdir;
2904
2905 #[tokio::test]
2906 async fn wal_stream_returns_records_after_commit() {
2907 let dir = tempdir().unwrap();
2908 let db = Arc::new(Database::create(dir.path()).unwrap());
2909 let table_schema = mongreldb_core::schema::Schema {
2910 schema_id: 1,
2911 columns: vec![mongreldb_core::schema::ColumnDef {
2912 id: 1,
2913 name: "id".into(),
2914 ty: TypeId::Int64,
2915 flags: mongreldb_core::schema::ColumnFlags::empty()
2916 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
2917 default_value: None,
2918 }],
2919 indexes: vec![],
2920 colocation: vec![],
2921 constraints: Default::default(),
2922 clustered: false,
2923 };
2924 db.create_table("items", table_schema).unwrap();
2925 let handle = db.table("items").unwrap();
2927 handle.lock().put(vec![(1, Value::Int64(1))]).unwrap();
2928 handle.lock().flush().unwrap();
2929
2930 let app = build_app(db);
2931 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2932 let addr = listener.local_addr().unwrap();
2933 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2934 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2935
2936 let resp = reqwest::get(format!("http://{addr}/wal/stream"))
2937 .await
2938 .unwrap();
2939 assert_eq!(resp.status(), 200);
2940 let body = resp.text().await.unwrap();
2941 assert!(!body.is_empty(), "wal_stream should return records");
2943 assert!(body.contains("seq"), "response should contain seq field");
2944 }
2945
2946 #[tokio::test]
2947 async fn follower_bootstraps_and_applies_incremental_commit() {
2948 let leader_dir = tempdir().unwrap();
2949 let follower_dir = tempdir().unwrap();
2950 let follower_path = follower_dir.path().join("copy");
2951 let db = Arc::new(Database::create(leader_dir.path()).unwrap());
2952 db.create_table(
2953 "items",
2954 mongreldb_core::schema::Schema {
2955 schema_id: 1,
2956 columns: vec![mongreldb_core::schema::ColumnDef {
2957 id: 1,
2958 name: "id".into(),
2959 ty: TypeId::Int64,
2960 flags: mongreldb_core::schema::ColumnFlags::empty()
2961 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
2962 default_value: None,
2963 }],
2964 indexes: vec![],
2965 colocation: vec![],
2966 constraints: Default::default(),
2967 clustered: false,
2968 },
2969 )
2970 .unwrap();
2971 let handle = db.table("items").unwrap();
2972 handle.lock().put(vec![(1, Value::Int64(1))]).unwrap();
2973 handle.lock().commit().unwrap();
2974
2975 let app = build_app_with_config(
2976 Arc::clone(&db),
2977 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
2978 Some("replication-secret".into()),
2979 None,
2980 );
2981 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2982 let addr = listener.local_addr().unwrap();
2983 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2984 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2985
2986 let leader_url = format!("http://{addr}");
2987 let first_path = follower_path.clone();
2988 let (mut follower, initial) = tokio::task::spawn_blocking(move || {
2989 let mut follower = ReplicationFollower::new(&leader_url, first_path)
2990 .with_bearer_token("replication-secret");
2991 let applied = follower.sync().unwrap();
2992 (follower, applied)
2993 })
2994 .await
2995 .unwrap();
2996 assert_eq!(initial, 0);
2997
2998 handle.lock().put(vec![(1, Value::Int64(2))]).unwrap();
2999 handle.lock().commit().unwrap();
3000 let applied = tokio::task::spawn_blocking(move || {
3001 let count = follower.sync().unwrap();
3002 (follower, count)
3003 })
3004 .await
3005 .unwrap();
3006 follower = applied.0;
3007 assert!(applied.1 > 0);
3008 assert!(follower.last_epoch() > 0);
3009
3010 let replica = Database::open(&follower_path).unwrap();
3011 assert_eq!(replica.table("items").unwrap().lock().count(), 2);
3012 drop(replica);
3013
3014 db.set_spill_threshold(1);
3015 db.transaction(|txn| {
3016 txn.put("items", vec![(1, Value::Int64(3))])?;
3017 Ok(())
3018 })
3019 .unwrap();
3020 let (follower_after_bootstrap, applied) = tokio::task::spawn_blocking(move || {
3021 let count = follower.sync().unwrap();
3022 (follower, count)
3023 })
3024 .await
3025 .unwrap();
3026 assert_eq!(applied, 0, "spilled run should trigger safe rebootstrap");
3027 assert!(follower_after_bootstrap.last_epoch() > 0);
3028 let replica = Database::open(&follower_path).unwrap();
3029 assert_eq!(replica.table("items").unwrap().lock().count(), 3);
3030 }
3031}
3032
3033#[cfg(test)]
3034mod metrics_tests {
3035 use super::*;
3036 use mongreldb_core::Database;
3037 use tempfile::tempdir;
3038
3039 async fn setup() -> std::net::SocketAddr {
3042 let dir = tempdir().unwrap();
3043 let db = Arc::new(Database::create(dir.path()).unwrap());
3044 let table_schema = mongreldb_core::schema::Schema {
3045 schema_id: 1,
3046 columns: vec![mongreldb_core::schema::ColumnDef {
3047 id: 1,
3048 name: "id".into(),
3049 ty: TypeId::Int64,
3050 flags: mongreldb_core::schema::ColumnFlags::empty()
3051 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
3052 default_value: None,
3053 }],
3054 indexes: vec![],
3055 colocation: vec![],
3056 constraints: Default::default(),
3057 clustered: false,
3058 };
3059 db.create_table("items", table_schema).unwrap();
3060 let app = build_app(db);
3061 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3062 let addr = listener.local_addr().unwrap();
3063 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
3064 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3065 addr
3066 }
3067
3068 #[tokio::test]
3069 async fn metrics_endpoint_returns_prometheus_text() {
3070 let addr = setup().await;
3071 let client = reqwest::Client::new();
3072
3073 let _ = client
3075 .post(format!("http://{addr}/tables/items/put"))
3076 .json(&json!({ "row": [1, 1] }))
3077 .send()
3078 .await
3079 .unwrap();
3080 let _ = client
3081 .post(format!("http://{addr}/sql"))
3082 .json(&json!({ "sql": "SELECT count(*) FROM items" }))
3083 .send()
3084 .await
3085 .unwrap();
3086
3087 let resp = client
3088 .get(format!("http://{addr}/metrics"))
3089 .send()
3090 .await
3091 .unwrap();
3092 assert_eq!(resp.status(), 200);
3093 let ct = resp
3094 .headers()
3095 .get("content-type")
3096 .and_then(|v| v.to_str().ok())
3097 .unwrap_or_default()
3098 .to_string();
3099 assert!(
3100 ct.contains("text/plain"),
3101 "content-type is prometheus text: {ct}"
3102 );
3103 let body = resp.text().await.unwrap();
3104 assert!(body.contains("# TYPE mongreldb_sql_queries_total counter"));
3106 assert!(body.contains("# TYPE mongreldb_puts_total counter"));
3107 assert!(body.contains("# TYPE mongreldb_tables gauge"));
3108 assert!(
3110 body.contains("mongreldb_sql_queries_total 1"),
3111 "sql_queries counter should reflect the /sql call: {body}"
3112 );
3113 assert!(
3114 body.contains("mongreldb_puts_total 1"),
3115 "puts counter should reflect the put call: {body}"
3116 );
3117 assert!(body.contains("mongreldb_tables 1"));
3119 }
3120
3121 #[tokio::test]
3122 async fn metrics_error_counter_increments_on_bad_sql() {
3123 let addr = setup().await;
3124 let client = reqwest::Client::new();
3125 let _ = client
3127 .post(format!("http://{addr}/sql"))
3128 .json(&json!({ "sql": "SELECT * FROM does_not_exist" }))
3129 .send()
3130 .await
3131 .unwrap();
3132 let body = client
3133 .get(format!("http://{addr}/metrics"))
3134 .send()
3135 .await
3136 .unwrap()
3137 .text()
3138 .await
3139 .unwrap();
3140 assert!(
3141 body.contains("mongreldb_sql_errors_total 1"),
3142 "sql_errors should increment on a failed query: {body}"
3143 );
3144 }
3145
3146 #[tokio::test]
3147 async fn arrow_stream_returns_ipc_stream_bytes() {
3148 let addr = setup().await;
3149 let client = reqwest::Client::new();
3150 for i in 1..=3 {
3153 let resp = client
3154 .post(format!("http://{addr}/tables/items/put"))
3155 .json(&json!({ "row": [1, i] }))
3156 .send()
3157 .await
3158 .unwrap();
3159 assert_eq!(resp.status(), 200, "put should succeed");
3160 }
3161 let _ = client
3162 .post(format!("http://{addr}/tables/items/commit"))
3163 .send()
3164 .await
3165 .unwrap();
3166 let count_body = client
3168 .get(format!("http://{addr}/tables/items/count"))
3169 .send()
3170 .await
3171 .unwrap()
3172 .text()
3173 .await
3174 .unwrap();
3175 assert!(
3176 count_body.contains("\"count\":3"),
3177 "expected 3 visible rows, got: {count_body}"
3178 );
3179 let resp = client
3180 .post(format!("http://{addr}/sql"))
3181 .json(&json!({ "sql": "SELECT count(*) FROM items", "format": "arrow-stream" }))
3182 .send()
3183 .await
3184 .unwrap();
3185 assert_eq!(resp.status(), 200, "streaming query should succeed");
3186 let ct = resp
3187 .headers()
3188 .get("content-type")
3189 .and_then(|v| v.to_str().ok())
3190 .unwrap_or_default()
3191 .to_string();
3192 assert!(
3193 ct.contains("application/vnd.apache.arrow.stream"),
3194 "content-type should be the arrow stream format: {ct}"
3195 );
3196 let bytes = resp.bytes().await.unwrap();
3197 assert!(
3201 !bytes.is_empty(),
3202 "arrow stream body should contain schema + batch + EOS"
3203 );
3204 assert!(
3205 bytes.starts_with(&0xFFFFFFFFu32.to_le_bytes()),
3206 "arrow stream must begin with the IPC continuation marker"
3207 );
3208 assert!(
3209 bytes.ends_with(&[0u8, 0, 0, 0]),
3210 "arrow stream should end with the EOS marker (trailing zero length)"
3211 );
3212 }
3213}
3214
3215#[cfg(test)]
3216mod streaming_tests {
3217 use super::*;
3218 use arrow::array::Int64Array;
3219 use arrow::datatypes::{DataType, Field, Schema};
3220 use arrow::record_batch::RecordBatch;
3221 use datafusion::common::DataFusionError;
3222 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
3223 use futures::StreamExt;
3224 use std::sync::Arc as StdArc;
3225
3226 fn batch_stream(batches: Vec<RecordBatch>) -> mongreldb_query::MongrelRecordBatchStream {
3227 let schema = batches
3228 .first()
3229 .map(RecordBatch::schema)
3230 .unwrap_or_else(|| StdArc::new(Schema::empty()));
3231 let batches =
3232 futures::stream::iter(batches.into_iter().map(Ok::<RecordBatch, DataFusionError>));
3233 Box::pin(RecordBatchStreamAdapter::new(schema, batches))
3234 }
3235
3236 #[tokio::test]
3241 async fn arrow_stream_serializes_multiple_batches_roundtrip() {
3242 let schema = StdArc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)]));
3243 let b1 = RecordBatch::try_new(
3244 schema.clone(),
3245 vec![StdArc::new(Int64Array::from(vec![1, 2]))],
3246 )
3247 .unwrap();
3248 let b2 = RecordBatch::try_new(
3249 schema.clone(),
3250 vec![StdArc::new(Int64Array::from(vec![3, 4, 5]))],
3251 )
3252 .unwrap();
3253
3254 let resp = sql_arrow_stream_response(batch_stream(vec![b1, b2]));
3255 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
3256 .await
3257 .unwrap();
3258
3259 assert!(bytes.starts_with(&0xFFFFFFFFu32.to_le_bytes()));
3261
3262 let slice: &[u8] = bytes.as_ref();
3265 let mut reader = arrow::ipc::reader::StreamReader::try_new(slice, None).unwrap();
3266 let mut total_rows = 0;
3267 for batch in reader.by_ref() {
3268 let batch = batch.expect("each IPC message should decode");
3269 total_rows += batch.num_rows();
3270 }
3271 assert_eq!(
3272 total_rows, 5,
3273 "all rows should round-trip through the stream"
3274 );
3275 }
3276
3277 #[tokio::test]
3278 async fn arrow_stream_emits_schema_before_first_batch() {
3279 let schema = StdArc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)]));
3280 let pending = futures::stream::pending::<Result<RecordBatch, DataFusionError>>();
3281 let batches = Box::pin(RecordBatchStreamAdapter::new(schema, pending));
3282 let mut body = sql_arrow_stream_response(batches)
3283 .into_body()
3284 .into_data_stream();
3285
3286 let chunk = tokio::time::timeout(std::time::Duration::from_millis(100), body.next())
3287 .await
3288 .expect("schema chunk should not wait for a query batch")
3289 .unwrap()
3290 .unwrap();
3291 assert!(chunk.starts_with(&0xFFFFFFFFu32.to_le_bytes()));
3292 }
3293
3294 #[tokio::test]
3295 async fn arrow_stream_empty_query_is_valid_ipc() {
3296 let resp = sql_arrow_stream_response(batch_stream(Vec::new()));
3297 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
3298 .await
3299 .unwrap();
3300 let slice: &[u8] = bytes.as_ref();
3301 let reader = arrow::ipc::reader::StreamReader::try_new(slice, None).unwrap();
3302 assert_eq!(reader.count(), 0);
3303 }
3304
3305 #[tokio::test]
3306 async fn buffered_output_limits_are_typed() {
3307 let dir = tempfile::tempdir().unwrap();
3308 let db = StdArc::new(mongreldb_core::Database::create(dir.path()).unwrap());
3309 let session = MongrelSession::open(db).unwrap();
3310 let query = session.register_query(SqlQueryOptions::default()).unwrap();
3311 let output = session
3312 .run_with_query_for_serialization("SELECT 1", query)
3313 .await
3314 .unwrap();
3315
3316 let row_error = serialize_buffered_output("json", &output, 0, 1024, None).unwrap_err();
3317 assert!(matches!(row_error, BufferedSerializationError::Limit(_)));
3318 let byte_error = serialize_buffered_output("json", &output, 10, 1, None).unwrap_err();
3319 assert!(matches!(byte_error, BufferedSerializationError::Limit(_)));
3320 output.fail();
3321 }
3322}
3323
3324#[cfg(test)]
3325mod audit_tests {
3326 use super::*;
3327 use mongreldb_core::Database;
3328 use tempfile::tempdir;
3329
3330 async fn auth_setup(password: &str) -> std::net::SocketAddr {
3332 let dir = tempdir().unwrap();
3333 let db = Arc::new(Database::create(dir.path()).unwrap());
3334 db.create_user("alice", password).unwrap();
3335 db.set_user_admin("alice", true).unwrap();
3336 let app = build_app_full(
3337 db,
3338 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
3339 None,
3340 None,
3341 true,
3342 );
3343 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3344 let addr = listener.local_addr().unwrap();
3345 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
3346 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3347 addr
3348 }
3349
3350 #[tokio::test]
3351 async fn audit_records_login_success_and_failure() {
3352 let addr = auth_setup("s3cret").await;
3353 let client = reqwest::Client::new();
3354
3355 let resp = client
3357 .get(format!("http://{addr}/health"))
3358 .header("Authorization", basic("alice", "s3cret"))
3359 .send()
3360 .await
3361 .unwrap();
3362 assert_eq!(resp.status(), 200);
3363
3364 let resp = client
3366 .get(format!("http://{addr}/health"))
3367 .header("Authorization", basic("alice", "wrong"))
3368 .send()
3369 .await
3370 .unwrap();
3371 assert_eq!(resp.status(), 401);
3372
3373 let body = client
3374 .get(format!("http://{addr}/audit"))
3375 .header("Authorization", basic("alice", "s3cret"))
3376 .send()
3377 .await
3378 .unwrap()
3379 .text()
3380 .await
3381 .unwrap();
3382 assert!(
3383 body.contains("\"action\":\"login.ok\""),
3384 "audit should record the successful login: {body}"
3385 );
3386 assert!(
3387 body.contains("\"action\":\"login.fail\""),
3388 "audit should record the failed login: {body}"
3389 );
3390 assert!(
3391 body.contains("\"principal\":\"alice\""),
3392 "audit should attribute events to alice: {body}"
3393 );
3394 }
3395
3396 #[tokio::test]
3397 async fn audit_records_ddl_sql() {
3398 let dir = tempdir().unwrap();
3399 let db = Arc::new(Database::create(dir.path()).unwrap());
3400 let app = build_app(db);
3401 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3402 let addr = listener.local_addr().unwrap();
3403 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
3404 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3405
3406 let client = reqwest::Client::new();
3407 let _ = client
3408 .post(format!("http://{addr}/sql"))
3409 .json(&json!({ "sql": "CREATE TABLE t (id BIGINT PRIMARY KEY)" }))
3410 .send()
3411 .await
3412 .unwrap();
3413 let _ = client
3415 .post(format!("http://{addr}/sql"))
3416 .json(&json!({ "sql": "SELECT 1" }))
3417 .send()
3418 .await
3419 .unwrap();
3420
3421 let body = client
3422 .get(format!("http://{addr}/audit"))
3423 .send()
3424 .await
3425 .unwrap()
3426 .text()
3427 .await
3428 .unwrap();
3429 assert!(
3430 body.contains("\"action\":\"ddl.ok\""),
3431 "audit should record the successful DDL statement: {body}"
3432 );
3433 assert!(
3434 body.contains("CREATE TABLE"),
3435 "audit detail should carry the DDL snippet: {body}"
3436 );
3437 assert!(
3439 !body.contains("SELECT 1"),
3440 "non-DDL reads should not be audited: {body}"
3441 );
3442 }
3443
3444 #[tokio::test]
3445 async fn audit_redacts_credential_passwords() {
3446 let dir = tempdir().unwrap();
3447 let db = Arc::new(Database::create(dir.path()).unwrap());
3448 let app = build_app(db);
3449 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3450 let addr = listener.local_addr().unwrap();
3451 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
3452 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3453
3454 let client = reqwest::Client::new();
3455 let _ = client
3458 .post(format!("http://{addr}/sql"))
3459 .json(&json!({ "sql": "CREATE USER alice WITH PASSWORD 'topsecret'" }))
3460 .send()
3461 .await
3462 .unwrap();
3463
3464 let body = client
3465 .get(format!("http://{addr}/audit"))
3466 .send()
3467 .await
3468 .unwrap()
3469 .text()
3470 .await
3471 .unwrap();
3472 assert!(
3473 !body.contains("topsecret"),
3474 "password must never appear in the audit log: {body}"
3475 );
3476 assert!(
3477 body.contains("redacted credential statement"),
3478 "credential DDL should be recorded as redacted: {body}"
3479 );
3480 }
3481
3482 fn basic(user: &str, pass: &str) -> String {
3483 let raw = format!("{user}:{pass}");
3484 format!("Basic {}", base64_encode(raw.as_bytes()))
3485 }
3486
3487 fn base64_encode(input: &[u8]) -> String {
3488 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3489 let mut out = String::new();
3490 let mut buf = 0u32;
3491 let mut bits = 0u32;
3492 for &b in input {
3493 buf = (buf << 8) | b as u32;
3494 bits += 8;
3495 while bits >= 6 {
3496 bits -= 6;
3497 out.push(TABLE[((buf >> bits) & 0x3F) as usize] as char);
3498 }
3499 }
3500 if bits > 0 {
3501 out.push(TABLE[((buf << (6 - bits)) & 0x3F) as usize] as char);
3502 }
3503 while !out.len().is_multiple_of(4) {
3504 out.push('=');
3505 }
3506 out
3507 }
3508}
3509
3510#[cfg(test)]
3511mod session_tests {
3512 use super::*;
3513 use mongreldb_core::Database;
3514 use tempfile::tempdir;
3515
3516 async fn setup() -> (tempfile::TempDir, std::net::SocketAddr) {
3520 let dir = tempdir().unwrap();
3521 let db = Arc::new(Database::create(dir.path()).unwrap());
3522 let table_schema = mongreldb_core::schema::Schema {
3523 schema_id: 1,
3524 columns: vec![mongreldb_core::schema::ColumnDef {
3525 id: 1,
3526 name: "id".into(),
3527 ty: TypeId::Int64,
3528 flags: mongreldb_core::schema::ColumnFlags::empty()
3529 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
3530 default_value: None,
3531 }],
3532 indexes: vec![],
3533 colocation: vec![],
3534 constraints: Default::default(),
3535 clustered: false,
3536 };
3537 db.create_table("items", table_schema).unwrap();
3538 let app = build_app(db);
3539 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3540 let addr = listener.local_addr().unwrap();
3541 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
3542 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3543 (dir, addr)
3544 }
3545
3546 async fn open_session(client: &reqwest::Client, addr: &std::net::SocketAddr) -> String {
3548 let resp = client
3549 .post(format!("http://{addr}/sessions"))
3550 .send()
3551 .await
3552 .unwrap();
3553 assert_eq!(resp.status(), 200);
3554 resp.json::<serde_json::Value>()
3555 .await
3556 .unwrap()
3557 .get("session_id")
3558 .unwrap()
3559 .as_str()
3560 .unwrap()
3561 .to_string()
3562 }
3563
3564 async fn sql_on(
3565 client: &reqwest::Client,
3566 addr: &std::net::SocketAddr,
3567 session: &str,
3568 sql: &str,
3569 ) -> reqwest::Response {
3570 client
3571 .post(format!("http://{addr}/sql"))
3572 .header("X-Session-ID", session)
3573 .json(&json!({ "sql": sql }))
3574 .send()
3575 .await
3576 .unwrap()
3577 }
3578
3579 async fn count_items(client: &reqwest::Client, addr: &std::net::SocketAddr) -> u64 {
3580 client
3581 .get(format!("http://{addr}/tables/items/count"))
3582 .send()
3583 .await
3584 .unwrap()
3585 .json::<serde_json::Value>()
3586 .await
3587 .unwrap()
3588 .get("count")
3589 .unwrap()
3590 .as_u64()
3591 .unwrap()
3592 }
3593
3594 #[tokio::test]
3595 async fn cross_request_transaction_commits() {
3596 let (_dir, addr) = setup().await;
3597 let client = reqwest::Client::new();
3598 let session = open_session(&client, &addr).await;
3599
3600 let r = sql_on(&client, &addr, &session, "BEGIN").await;
3602 assert_eq!(r.status(), 200);
3603 let r = sql_on(
3604 &client,
3605 &addr,
3606 &session,
3607 "INSERT INTO items (id) VALUES (1)",
3608 )
3609 .await;
3610 assert_eq!(r.status(), 200, "INSERT should stage successfully");
3611 assert_eq!(count_items(&client, &addr).await, 0);
3613 let r = sql_on(&client, &addr, &session, "COMMIT").await;
3614 assert_eq!(r.status(), 200);
3615
3616 assert_eq!(count_items(&client, &addr).await, 1);
3618 }
3619
3620 #[tokio::test]
3621 async fn cross_request_transaction_rolls_back() {
3622 let (_dir, addr) = setup().await;
3623 let client = reqwest::Client::new();
3624 let session = open_session(&client, &addr).await;
3625
3626 sql_on(&client, &addr, &session, "BEGIN").await;
3627 sql_on(
3628 &client,
3629 &addr,
3630 &session,
3631 "INSERT INTO items (id) VALUES (5)",
3632 )
3633 .await;
3634 let r = sql_on(&client, &addr, &session, "ROLLBACK").await;
3636 assert_eq!(r.status(), 200);
3637 assert_eq!(
3638 count_items(&client, &addr).await,
3639 0,
3640 "rollback discards staged writes"
3641 );
3642 }
3643
3644 #[tokio::test]
3645 async fn unknown_session_id_is_404() {
3646 let (_dir, addr) = setup().await;
3647 let client = reqwest::Client::new();
3648 let resp = client
3649 .post(format!("http://{addr}/sql"))
3650 .header("X-Session-ID", "does-not-exist")
3651 .json(&json!({ "sql": "SELECT 1" }))
3652 .send()
3653 .await
3654 .unwrap();
3655 assert_eq!(resp.status(), 404);
3656 }
3657
3658 #[tokio::test]
3659 async fn close_session_ends_cross_request_state() {
3660 let (_dir, addr) = setup().await;
3661 let client = reqwest::Client::new();
3662 let session = open_session(&client, &addr).await;
3663
3664 sql_on(&client, &addr, &session, "BEGIN").await;
3666 let r = client
3667 .delete(format!("http://{addr}/sessions/{session}"))
3668 .send()
3669 .await
3670 .unwrap();
3671 assert_eq!(r.status(), 200);
3672
3673 let resp = sql_on(&client, &addr, &session, "COMMIT").await;
3675 assert_eq!(resp.status(), 404, "closed session is no longer usable");
3676 }
3677
3678 #[tokio::test]
3679 async fn no_session_header_uses_fresh_ephemeral_session() {
3680 let (_dir, addr) = setup().await;
3683 let client = reqwest::Client::new();
3684 let resp = client
3685 .post(format!("http://{addr}/sql"))
3686 .json(&json!({ "sql": "INSERT INTO items (id) VALUES (42)" }))
3687 .send()
3688 .await
3689 .unwrap();
3690 assert_eq!(resp.status(), 200);
3691 assert_eq!(count_items(&client, &addr).await, 1);
3692 }
3693
3694 #[tokio::test]
3695 async fn prepared_statement_prepare_execute_and_reuse() {
3696 let (_dir, addr) = setup().await;
3697 let client = reqwest::Client::new();
3698 let session = open_session(&client, &addr).await;
3699 sql_on(
3700 &client,
3701 &addr,
3702 &session,
3703 "INSERT INTO items (id) VALUES (1), (2), (3), (4)",
3704 )
3705 .await;
3706
3707 let resp = client
3709 .post(format!("http://{addr}/sessions/{session}/prepare"))
3710 .json(&json!({"name":"gt","sql":"SELECT id FROM items WHERE id > $1"}))
3711 .send()
3712 .await
3713 .unwrap();
3714 assert_eq!(resp.status(), 200);
3715
3716 let resp = client
3718 .post(format!("http://{addr}/sessions/{session}/execute"))
3719 .json(&json!({"name":"gt","params":[2]}))
3720 .send()
3721 .await
3722 .unwrap();
3723 assert_eq!(resp.status(), 200);
3724 let body = resp.json::<serde_json::Value>().await.unwrap();
3725 let arr = body
3726 .as_array()
3727 .expect("execute returns a JSON array of rows");
3728 assert_eq!(arr.len(), 2, "ids > 2 are {{3,4}}: {body}");
3729
3730 let resp = client
3732 .post(format!("http://{addr}/sessions/{session}/execute"))
3733 .json(&json!({"name":"gt","params":[3]}))
3734 .send()
3735 .await
3736 .unwrap();
3737 let body = resp.json::<serde_json::Value>().await.unwrap();
3738 assert_eq!(body.as_array().unwrap().len(), 1, "ids > 3 is {{4}}");
3739 }
3740
3741 #[tokio::test]
3742 async fn prepared_statement_deallocate_then_execute_fails() {
3743 let (_dir, addr) = setup().await;
3744 let client = reqwest::Client::new();
3745 let session = open_session(&client, &addr).await;
3746 let _ = client
3747 .post(format!("http://{addr}/sessions/{session}/prepare"))
3748 .json(&json!({"name":"p","sql":"SELECT $1"}))
3749 .send()
3750 .await
3751 .unwrap();
3752 let resp = client
3753 .delete(format!("http://{addr}/sessions/{session}/statements/p"))
3754 .send()
3755 .await
3756 .unwrap();
3757 assert_eq!(resp.status(), 200);
3758 let resp = client
3760 .post(format!("http://{addr}/sessions/{session}/execute"))
3761 .json(&json!({"name":"p","params":[1]}))
3762 .send()
3763 .await
3764 .unwrap();
3765 assert_ne!(resp.status(), 200, "execute after DEALLOCATE must fail");
3766 }
3767
3768 #[tokio::test]
3769 async fn prepared_statement_rejects_bad_name() {
3770 let (_dir, addr) = setup().await;
3771 let client = reqwest::Client::new();
3772 let session = open_session(&client, &addr).await;
3773 let resp = client
3774 .post(format!("http://{addr}/sessions/{session}/prepare"))
3775 .json(&json!({"name":"1bad","sql":"SELECT 1"}))
3776 .send()
3777 .await
3778 .unwrap();
3779 assert_eq!(
3780 resp.status(),
3781 400,
3782 "statement name starting with a digit must be rejected"
3783 );
3784 }
3785}