1use std::sync::Arc;
18
19use axum::extract::{Path, State};
20use axum::http::header;
21use axum::http::StatusCode;
22use axum::response::{IntoResponse, Response};
23use axum::routing::{get, post};
24use axum::Json;
25use mongreldb_core::schema::{Schema, TypeId};
26use mongreldb_core::{Database, Value};
27use mongreldb_query::{ExternalTableModule, MongrelSession};
28use serde::{Deserialize, Serialize};
29use serde_json::json;
30
31mod audit;
32mod kit;
33mod metrics;
34mod procedure;
35mod sessions;
36mod trigger;
37
38pub use sessions::{spawn_session_reaper, SessionStore};
39
40fn status_for_error(e: &mongreldb_core::MongrelError) -> StatusCode {
45 use mongreldb_core::MongrelError;
46 match e {
47 MongrelError::AuthRequired | MongrelError::InvalidCredentials { .. } => {
48 StatusCode::UNAUTHORIZED
49 }
50 MongrelError::AuthNotRequired => StatusCode::BAD_REQUEST,
51 MongrelError::PermissionDenied { .. } => StatusCode::FORBIDDEN,
52 MongrelError::InvalidArgument(_) => StatusCode::CONFLICT,
53 MongrelError::Conflict(_) => StatusCode::CONFLICT,
54 MongrelError::ReadOnlyReplica => StatusCode::CONFLICT,
55 MongrelError::NotFound(_) => StatusCode::NOT_FOUND,
56 MongrelError::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
57 MongrelError::WorkBudgetExceeded => StatusCode::TOO_MANY_REQUESTS,
58 MongrelError::Cancelled => StatusCode::from_u16(499).expect("499 is valid"),
59 _ => StatusCode::INTERNAL_SERVER_ERROR,
60 }
61}
62
63fn status_for_query_error(e: &mongreldb_query::MongrelQueryError) -> StatusCode {
66 use mongreldb_query::MongrelQueryError;
67 match e {
68 MongrelQueryError::Core(core) => status_for_error(core),
69 _ if e.to_string().contains("deadline exceeded") => StatusCode::GATEWAY_TIMEOUT,
70 _ if e.to_string().contains("work budget exceeded") => StatusCode::TOO_MANY_REQUESTS,
71 _ if e.to_string().contains("cancelled") => {
72 StatusCode::from_u16(499).expect("499 is valid")
73 }
74 _ => StatusCode::INTERNAL_SERVER_ERROR,
75 }
76}
77
78struct OptionalPrincipal(Option<mongreldb_core::Principal>);
82
83impl<S> axum::extract::FromRequestParts<S> for OptionalPrincipal
84where
85 S: Send + Sync,
86{
87 type Rejection = std::convert::Infallible;
88
89 async fn from_request_parts(
90 parts: &mut axum::http::request::Parts,
91 _state: &S,
92 ) -> Result<Self, Self::Rejection> {
93 Ok(OptionalPrincipal(
94 parts.extensions.get::<mongreldb_core::Principal>().cloned(),
95 ))
96 }
97}
98
99struct AppState {
100 db: Arc<Database>,
101 idem: kit::IdempotencyStore,
102 external_modules: Vec<Arc<dyn ExternalTableModule>>,
103 auth_token: Option<String>,
104 user_auth: bool,
106 metrics: Arc<metrics::Metrics>,
108 slow_query_threshold: std::time::Duration,
110 audit: Arc<audit::AuditLog>,
112 sessions: Arc<sessions::SessionStore>,
115 ai_semaphore: Arc<tokio::sync::Semaphore>,
117}
118
119pub fn build_app(db: Arc<Database>) -> axum::Router {
120 build_app_with_config(
121 db,
122 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
123 None,
124 None,
125 )
126}
127
128pub fn build_app_with_external_modules(
129 db: Arc<Database>,
130 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
131) -> axum::Router {
132 build_app_with_config(db, external_modules, None, None)
133}
134
135pub fn build_app_with_config(
137 db: Arc<Database>,
138 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
139 auth_token: Option<String>,
140 max_connections: Option<usize>,
141) -> axum::Router {
142 build_app_full(db, external_modules, auth_token, max_connections, false)
143}
144
145pub fn build_app_full(
148 db: Arc<Database>,
149 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
150 auth_token: Option<String>,
151 max_connections: Option<usize>,
152 user_auth: bool,
153) -> axum::Router {
154 let sessions = Arc::new(sessions::SessionStore::new(
155 default_max_sessions(),
156 default_session_idle_timeout(),
157 ));
158 build_app_with_sessions(
159 db,
160 external_modules,
161 auth_token,
162 max_connections,
163 user_auth,
164 sessions,
165 )
166}
167
168pub fn build_app_with_sessions(
172 db: Arc<Database>,
173 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
174 auth_token: Option<String>,
175 max_connections: Option<usize>,
176 user_auth: bool,
177 sessions: Arc<sessions::SessionStore>,
178) -> axum::Router {
179 db.set_replication_wal_retention_segments(default_replication_wal_segments());
180 if let Err(error) = db.set_history_retention_epochs(default_history_retention_epochs()) {
181 eprintln!("[history] failed to configure retention: {error}");
182 }
183 let state = Arc::new(AppState {
184 idem: kit::IdempotencyStore::new(db.root()),
185 db,
186 external_modules: external_modules.into_iter().collect(),
187 auth_token,
188 user_auth,
189 metrics: Arc::new(metrics::Metrics::default()),
190 slow_query_threshold: metrics::slow_query_threshold(),
191 audit: Arc::new(audit::AuditLog::new(8192)),
192 sessions,
193 ai_semaphore: Arc::new(tokio::sync::Semaphore::new(default_ai_max_concurrent())),
194 });
195 let router = axum::Router::new()
196 .route("/health", get(health))
197 .route(
198 "/history/retention",
199 get(history_retention).put(set_history_retention),
200 )
201 .route("/metrics", get(metrics_handler))
202 .route("/audit", get(audit_handler))
203 .route("/tables", get(list_tables).post(create_table))
204 .route("/tables/{name}", axum::routing::delete(drop_table))
205 .route("/tables/{name}/put", post(put_row))
206 .route("/tables/{name}/count", get(count))
207 .route("/tables/{name}/commit", post(commit))
208 .route("/sql", post(sql))
209 .route("/txn", post(txn))
210 .route("/sessions", post(create_session))
211 .route("/sessions/{id}", axum::routing::delete(close_session))
212 .route("/sessions/{id}/prepare", post(prepare_statement))
213 .route("/sessions/{id}/execute", post(execute_statement))
214 .route(
215 "/sessions/{id}/statements/{name}",
216 axum::routing::delete(deallocate_statement),
217 )
218 .route("/procedures", get(procedure::list).post(procedure::create))
219 .route(
220 "/procedures/{name}",
221 get(procedure::describe)
222 .put(procedure::replace)
223 .delete(procedure::drop_procedure),
224 )
225 .route("/procedures/{name}/call", post(procedure::call))
226 .route("/triggers", get(trigger::list).post(trigger::create))
227 .route(
228 "/triggers/{name}",
229 get(trigger::describe)
230 .put(trigger::replace)
231 .delete(trigger::drop_trigger),
232 )
233 .route("/kit/schema", get(kit::schema_all))
235 .route("/kit/schema/{table}", get(kit::schema_one))
236 .route("/kit/txn", post(kit::kit_txn))
237 .route("/kit/query", post(kit::kit_query))
238 .route("/kit/retrieve", post(kit::kit_retrieve))
239 .route("/kit/ann_rerank", post(kit::kit_ann_rerank))
240 .route("/kit/ai/metrics", get(kit::kit_ai_metrics))
241 .route("/kit/set_similarity", post(kit::kit_set_similarity))
242 .route("/kit/search", post(kit::kit_search))
243 .route("/kit/create_table", post(kit::kit_create_table))
244 .route("/kit/procedures/{name}/call", post(procedure::kit_call))
245 .route("/compact", post(compact_all))
246 .route("/tables/{name}/compact", post(compact_table))
247 .route("/wal/stream", get(wal_stream))
248 .route("/replication/snapshot", get(replication_snapshot))
249 .route("/events", get(events_stream))
250 .with_state(state.clone());
251
252 let router = if state.auth_token.is_some() || state.user_auth {
254 router.layer(axum::middleware::from_fn_with_state(
255 state.clone(),
256 auth_middleware,
257 ))
258 } else {
259 router
260 };
261
262 if let Some(max) = max_connections {
264 router.layer(tower::limit::ConcurrencyLimitLayer::new(max))
265 } else {
266 router
267 }
268}
269
270async fn auth_middleware(
277 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
278 mut req: axum::extract::Request,
279 next: axum::middleware::Next,
280) -> Result<axum::response::Response, axum::http::StatusCode> {
281 let header = req
282 .headers()
283 .get("authorization")
284 .and_then(|v| v.to_str().ok())
285 .unwrap_or("");
286
287 let mut attempted = String::new();
291 let mut fail_reason = "no credentials provided".to_string();
292
293 if let Some(token) = &state.auth_token {
295 if let Some(provided) = header.strip_prefix("Bearer ") {
296 attempted = "token".to_string();
297 if provided == token {
298 state
299 .audit
300 .record("token", "login.ok", "bearer token accepted");
301 return Ok(next.run(req).await);
302 }
303 fail_reason = "invalid bearer token".to_string();
304 }
305 }
306
307 if state.user_auth {
309 if let Some(encoded) = header.strip_prefix("Basic ") {
310 if let Ok(decoded) = base64_decode(encoded) {
311 if let Ok(creds) = std::str::from_utf8(&decoded) {
312 if let Some((username, password)) = creds.split_once(':') {
313 attempted = username.to_string();
314 if let Ok(Some(_user)) = state.db.verify_user(username, password) {
315 state
316 .audit
317 .record(username, "login.ok", "basic credentials accepted");
318 if let Some(principal) = state.db.resolve_principal(username) {
320 req.extensions_mut().insert(principal);
321 }
322 return Ok(next.run(req).await);
323 }
324 fail_reason = "invalid basic credentials".to_string();
325 } else {
326 fail_reason = "malformed basic credentials (no ':')".to_string();
327 }
328 } else {
329 fail_reason = "malformed basic credentials (non-utf8)".to_string();
330 }
331 } else {
332 fail_reason = "malformed basic credentials (bad base64)".to_string();
333 }
334 }
335 }
336
337 let who = if attempted.is_empty() {
338 "anonymous"
339 } else {
340 attempted.as_str()
341 };
342 state.audit.record(who, "login.fail", fail_reason);
343 Err(axum::http::StatusCode::UNAUTHORIZED)
344}
345
346fn base64_decode(input: &str) -> Result<Vec<u8>, ()> {
348 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
349 let input: Vec<u8> = input
350 .bytes()
351 .filter(|&b| b != b'\n' && b != b'\r' && b != b' ')
352 .collect();
353 let mut out = Vec::with_capacity(input.len() * 3 / 4);
354 let mut buf = 0u32;
355 let mut bits = 0u32;
356 for &b in &input {
357 if b == b'=' {
358 break;
359 }
360 let val = TABLE.iter().position(|&t| t == b).ok_or(())? as u32;
361 buf = (buf << 6) | val;
362 bits += 6;
363 if bits >= 8 {
364 bits -= 8;
365 out.push((buf >> bits) as u8);
366 }
367 }
368 Ok(out)
369}
370
371async fn wal_stream(
378 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
379 OptionalPrincipal(principal): OptionalPrincipal,
380 axum::extract::Query(params): axum::extract::Query<WalStreamParams>,
381) -> Result<Response, StatusCode> {
382 state
383 .db
384 .require_for(
385 request_principal(&state, &principal).as_ref(),
386 &mongreldb_core::Permission::Admin,
387 )
388 .map_err(|error| status_for_error(&error))?;
389 let since = params.since.unwrap_or(0);
390 let db = Arc::clone(&state.db);
391 let batch = tokio::task::spawn_blocking(move || db.replication_batch_since(since))
392 .await
393 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
394 let batch = batch.map_err(|e| {
395 eprintln!("wal_stream error: {e}");
396 StatusCode::INTERNAL_SERVER_ERROR
397 })?;
398 if batch.requires_snapshot {
399 let mut response = (
400 StatusCode::CONFLICT,
401 "replication snapshot required: WAL retention gap or spilled run",
402 )
403 .into_response();
404 set_replication_headers(&mut response, batch.current_epoch, batch.earliest_epoch);
405 return Ok(response);
406 }
407 let mut body = String::new();
408 for record in &batch.records {
409 let json = serde_json::to_string(record).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
410 body.push_str(&json);
411 body.push('\n');
412 }
413 let mut response = (
414 [
415 (header::CONTENT_TYPE, "application/x-ndjson".to_string()),
416 (header::CACHE_CONTROL, "no-cache".to_string()),
417 ],
418 body,
419 )
420 .into_response();
421 set_replication_headers(&mut response, batch.current_epoch, batch.earliest_epoch);
422 Ok(response)
423}
424
425async fn replication_snapshot(
426 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
427 OptionalPrincipal(principal): OptionalPrincipal,
428) -> Response {
429 if let Err(error) = state.db.require_for(
430 request_principal(&state, &principal).as_ref(),
431 &mongreldb_core::Permission::Admin,
432 ) {
433 return (status_for_error(&error), error.to_string()).into_response();
434 }
435 let db = Arc::clone(&state.db);
436 let snapshot = match tokio::task::spawn_blocking(move || db.replication_snapshot()).await {
437 Ok(Ok(snapshot)) => snapshot,
438 Ok(Err(error)) => {
439 return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
440 }
441 Err(error) => {
442 return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
443 }
444 };
445 let epoch = snapshot.epoch();
446 match snapshot.encode() {
449 Ok(bytes) => {
450 let mut response =
451 ([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response();
452 set_replication_headers(&mut response, epoch, None);
453 response
454 }
455 Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
456 }
457}
458
459fn set_replication_headers(response: &mut Response, current: u64, earliest: Option<u64>) {
460 response.headers_mut().insert(
461 "x-mongreldb-current-epoch",
462 current.to_string().parse().unwrap(),
463 );
464 if let Some(earliest) = earliest {
465 response.headers_mut().insert(
466 "x-mongreldb-earliest-epoch",
467 earliest.to_string().parse().unwrap(),
468 );
469 }
470}
471
472#[derive(serde::Deserialize)]
473struct WalStreamParams {
474 since: Option<u64>,
475}
476
477async fn events_stream(
482 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
483 OptionalPrincipal(principal): OptionalPrincipal,
484 headers: axum::http::HeaderMap,
485) -> Result<Response, StatusCode> {
486 use axum::response::sse::{Event, KeepAlive, Sse};
487 use futures::Stream;
488 use std::collections::VecDeque;
489 use std::convert::Infallible;
490
491 state
492 .db
493 .require_for(
494 request_principal(&state, &principal).as_ref(),
495 &mongreldb_core::Permission::Admin,
496 )
497 .map_err(|error| status_for_error(&error))?;
498
499 struct State {
500 db: Arc<Database>,
501 receiver: tokio::sync::broadcast::Receiver<mongreldb_core::ChangeEvent>,
502 change_wake: tokio::sync::broadcast::Receiver<()>,
503 interval: tokio::time::Interval,
504 pending: VecDeque<mongreldb_core::ChangeEvent>,
505 last_id: Option<String>,
506 poll_now: bool,
507 done: bool,
508 }
509
510 fn event(change: mongreldb_core::ChangeEvent) -> Event {
511 let id = change.id.clone();
512 let kind = if change.op == "notify" {
513 "notify"
514 } else {
515 "change"
516 };
517 let mut event = Event::default().event(kind).data(
518 serde_json::to_string(&change)
519 .unwrap_or_else(|error| format!(r#"{{"error":"{error}"}}"#)),
520 );
521 if let Some(id) = id {
522 event = event.id(id);
523 }
524 event
525 }
526
527 let last_id = headers
528 .get("last-event-id")
529 .and_then(|value| value.to_str().ok())
530 .map(str::to_string);
531 let receiver = state.db.subscribe_changes();
532 let change_wake = state.db.subscribe_change_commits();
533 let db = Arc::clone(&state.db);
534 let resume = last_id.clone();
535 let initial = tokio::task::spawn_blocking(move || db.change_events_since(resume.as_deref()))
536 .await
537 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
538 .map_err(|error| match error {
539 mongreldb_core::MongrelError::InvalidArgument(_) => StatusCode::BAD_REQUEST,
540 _ => StatusCode::INTERNAL_SERVER_ERROR,
541 })?;
542 if initial.gap {
543 return Ok((
544 StatusCode::CONFLICT,
545 Json(json!({
546 "error": "cdc retention gap",
547 "earliest_epoch": initial.earliest_epoch,
548 "current_epoch": initial.current_epoch,
549 })),
550 )
551 .into_response());
552 }
553
554 let stream: std::pin::Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>> = Box::pin(
555 futures::stream::unfold(
556 State {
557 db: Arc::clone(&state.db),
558 receiver,
559 change_wake,
560 interval: tokio::time::interval(std::time::Duration::from_millis(250)),
561 pending: initial.events.into(),
562 last_id,
563 poll_now: false,
564 done: false,
565 },
566 |mut stream| async move {
567 if stream.done {
568 return None;
569 }
570 loop {
571 if stream.poll_now {
572 stream.poll_now = false;
573 let db = Arc::clone(&stream.db);
574 let last_id = stream.last_id.clone();
575 match tokio::task::spawn_blocking(move || {
576 db.change_events_since(last_id.as_deref())
577 })
578 .await
579 {
580 Ok(Ok(batch)) if batch.gap => {
581 stream.done = true;
582 let gap = Event::default().event("gap").data(
583 json!({
584 "error": "cdc retention gap",
585 "earliest_epoch": batch.earliest_epoch,
586 "current_epoch": batch.current_epoch,
587 })
588 .to_string(),
589 );
590 return Some((Ok(gap), stream));
591 }
592 Ok(Ok(batch)) => stream.pending.extend(batch.events),
593 Ok(Err(error)) => {
594 stream.done = true;
595 return Some((
596 Ok(Event::default().event("error").data(error.to_string())),
597 stream,
598 ));
599 }
600 Err(error) => {
601 stream.done = true;
602 return Some((
603 Ok(Event::default().event("error").data(error.to_string())),
604 stream,
605 ));
606 }
607 }
608 }
609 if let Some(change) = stream.pending.pop_front() {
610 if let Some(id) = &change.id {
611 stream.last_id = Some(id.clone());
612 }
613 return Some((Ok(event(change)), stream));
614 }
615 tokio::select! {
616 received = stream.receiver.recv() => {
617 match received {
618 Ok(change) => return Some((Ok(event(change)), stream)),
619 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {},
620 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
621 return None;
622 }
623 }
624 }
625 received = stream.change_wake.recv() => {
626 match received {
627 Ok(()) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
628 stream.poll_now = true;
629 }
630 Err(tokio::sync::broadcast::error::RecvError::Closed) => {}
631 }
632 }
633 _ = stream.interval.tick() => {
634 stream.poll_now = true;
635 }
636 }
637 }
638 },
639 ),
640 );
641
642 Ok(Sse::new(stream)
643 .keep_alive(
644 KeepAlive::new()
645 .interval(std::time::Duration::from_secs(15))
646 .text("keep-alive"),
647 )
648 .into_response())
649}
650
651pub fn spawn_auto_compactor(db: Arc<Database>) {
656 std::thread::Builder::new()
657 .name("mongreldb-auto-compact".into())
658 .spawn(move || loop {
659 std::thread::sleep(std::time::Duration::from_secs(30));
660 for name in db.table_names() {
661 let Ok(handle) = db.table(&name) else {
662 continue;
663 };
664 let mut t = handle.lock();
665 let before = t.run_count();
666 match t.maybe_compact() {
667 Ok(true) => {
668 eprintln!(
669 "[auto-compact] {name}: {} runs -> {}",
670 before,
671 t.run_count()
672 );
673 }
674 Ok(false) => {}
675 Err(e) => {
676 eprintln!("[auto-compact] {name}: compaction failed: {e}");
677 }
678 }
679 }
680 })
681 .expect("spawn auto-compact thread");
682}
683
684async fn health() -> StatusCode {
685 StatusCode::OK
686}
687
688#[derive(Debug, Deserialize)]
689struct HistoryRetentionRequest {
690 #[serde(default)]
691 history_retention_epochs: serde_json::Value,
692}
693
694#[derive(Debug, Serialize)]
695struct HistoryRetentionResponse {
696 history_retention_epochs: u64,
697 earliest_retained_epoch: u64,
698}
699
700fn history_retention_response(db: &Database) -> HistoryRetentionResponse {
701 HistoryRetentionResponse {
702 history_retention_epochs: db.history_retention_epochs(),
703 earliest_retained_epoch: db.earliest_retained_epoch().0,
704 }
705}
706
707async fn history_retention(
709 State(state): State<Arc<AppState>>,
710 OptionalPrincipal(principal): OptionalPrincipal,
711) -> Response {
712 if let Err(error) = state.db.require_for(
713 request_principal(&state, &principal).as_ref(),
714 &mongreldb_core::Permission::Admin,
715 ) {
716 return (status_for_error(&error), error.to_string()).into_response();
717 }
718 Json(history_retention_response(&state.db)).into_response()
719}
720
721async fn set_history_retention(
723 State(state): State<Arc<AppState>>,
724 OptionalPrincipal(principal): OptionalPrincipal,
725 Json(request): Json<HistoryRetentionRequest>,
726) -> Response {
727 if let Err(error) = state.db.require_for(
728 request_principal(&state, &principal).as_ref(),
729 &mongreldb_core::Permission::Admin,
730 ) {
731 return (status_for_error(&error), error.to_string()).into_response();
732 }
733 let Some(epochs) = request.history_retention_epochs.as_u64() else {
734 return (
735 StatusCode::BAD_REQUEST,
736 Json(json!({"error": "history_retention_epochs must be a u64"})),
737 )
738 .into_response();
739 };
740 match state.db.set_history_retention_epochs(epochs) {
741 Ok(()) => Json(history_retention_response(&state.db)).into_response(),
742 Err(error) => (status_for_error(&error), error.to_string()).into_response(),
743 }
744}
745
746async fn audit_handler(
751 State(state): State<Arc<AppState>>,
752 OptionalPrincipal(principal): OptionalPrincipal,
753) -> Response {
754 if let Err(error) = state.db.require_for(
755 request_principal(&state, &principal).as_ref(),
756 &mongreldb_core::Permission::Admin,
757 ) {
758 return (status_for_error(&error), error.to_string()).into_response();
759 }
760 let recent = state.audit.recent();
761 Json(recent).into_response()
762}
763
764fn default_max_sessions() -> usize {
766 std::env::var("MONGRELBL_MAX_SESSIONS")
767 .ok()
768 .and_then(|v| v.parse().ok())
769 .unwrap_or(256)
770}
771
772fn default_session_idle_timeout() -> std::time::Duration {
774 std::time::Duration::from_secs(
775 std::env::var("MONGRELBL_SESSION_IDLE_TIMEOUT_SECS")
776 .ok()
777 .and_then(|v| v.parse().ok())
778 .unwrap_or(300),
779 )
780}
781
782fn default_replication_wal_segments() -> usize {
783 let replication = std::env::var("MONGRELDB_REPLICATION_WAL_SEGMENTS")
784 .ok()
785 .and_then(|value| value.parse().ok())
786 .unwrap_or(16);
787 let cdc = std::env::var("MONGRELDB_CDC_WAL_SEGMENTS")
788 .ok()
789 .and_then(|value| value.parse().ok())
790 .unwrap_or(16);
791 replication.max(cdc)
792}
793
794fn default_history_retention_epochs() -> u64 {
795 std::env::var("MONGRELDB_HISTORY_RETENTION_EPOCHS")
796 .ok()
797 .and_then(|value| value.parse().ok())
798 .unwrap_or(1024)
799}
800
801fn default_ai_max_concurrent() -> usize {
802 std::env::var("MONGRELDB_AI_MAX_CONCURRENT")
803 .ok()
804 .and_then(|value| value.parse().ok())
805 .filter(|value| *value > 0)
806 .unwrap_or(4)
807}
808
809fn request_owner(state: &AppState, principal: &Option<mongreldb_core::Principal>) -> String {
813 if let Some(p) = principal {
814 return p.username.clone();
815 }
816 if state.auth_token.is_some() {
817 return "token".into();
818 }
819 "anonymous".into()
820}
821
822fn request_principal(
823 state: &AppState,
824 principal: &Option<mongreldb_core::Principal>,
825) -> Option<mongreldb_core::Principal> {
826 principal.clone().or_else(|| {
827 state
828 .auth_token
829 .as_ref()
830 .map(|_| mongreldb_core::Principal {
831 username: "token".into(),
832 is_admin: true,
833 roles: Vec::new(),
834 permissions: Vec::new(),
835 })
836 })
837}
838
839async fn create_session(
844 State(state): State<Arc<AppState>>,
845 OptionalPrincipal(principal): OptionalPrincipal,
846) -> Response {
847 let owner = request_owner(&state, &principal);
848 let session = match MongrelSession::open_with_external_modules_as(
849 Arc::clone(&state.db),
850 state.external_modules.iter().cloned(),
851 request_principal(&state, &principal),
852 ) {
853 Ok(s) => s,
854 Err(e) => return (status_for_query_error(&e), e.to_string()).into_response(),
855 };
856 match state.sessions.create(session, owner.clone()) {
857 Some(token) => {
858 state.audit.record(owner, "session.open", "session created");
859 Json(json!({ "session_id": token })).into_response()
860 }
861 None => (
862 StatusCode::SERVICE_UNAVAILABLE,
863 "session limit reached; close an idle session or raise --max-sessions",
864 )
865 .into_response(),
866 }
867}
868
869async fn close_session(
872 State(state): State<Arc<AppState>>,
873 OptionalPrincipal(principal): OptionalPrincipal,
874 Path(id): Path<String>,
875) -> Response {
876 let owner = request_owner(&state, &principal);
877 if state.sessions.close(&id, &owner) {
878 state.audit.record(owner, "session.close", "session closed");
879 StatusCode::OK.into_response()
880 } else {
881 (
882 StatusCode::NOT_FOUND,
883 "session not found or not owned by caller",
884 )
885 .into_response()
886 }
887}
888
889fn dispatch_buffered_sql_format(
892 format: Option<&str>,
893 batches: &[arrow::record_batch::RecordBatch],
894) -> Response {
895 match format {
896 Some("arrow") => sql_arrow_response(batches),
897 _ => sql_json_response(batches),
898 }
899}
900
901fn validate_stmt_name(name: &str) -> Result<(), String> {
905 let mut chars = name.chars();
906 match chars.next() {
907 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
908 _ => return Err("statement name must start with a letter or underscore".into()),
909 }
910 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
911 return Err("statement name may contain only letters, digits, or underscore".into());
912 }
913 Ok(())
914}
915
916fn render_sql_literal(v: &serde_json::Value) -> Result<String, String> {
921 match v {
922 serde_json::Value::Null => Ok("NULL".into()),
923 serde_json::Value::Bool(b) => {
924 if *b {
925 Ok("TRUE".into())
926 } else {
927 Ok("FALSE".into())
928 }
929 }
930 serde_json::Value::Number(n) => Ok(n.to_string()),
931 serde_json::Value::String(s) => {
933 let mut out = String::with_capacity(s.len() + 2);
934 out.push('\'');
935 for c in s.chars() {
936 if c == '\'' {
937 out.push_str("''");
938 } else {
939 out.push(c);
940 }
941 }
942 out.push('\'');
943 Ok(out)
944 }
945 _ => Err("prepared-statement parameters must be scalar (null/bool/number/string)".into()),
947 }
948}
949
950#[derive(Deserialize)]
951struct PrepareRequest {
952 name: String,
953 sql: String,
954}
955
956async fn prepare_statement(
960 State(state): State<Arc<AppState>>,
961 OptionalPrincipal(principal): OptionalPrincipal,
962 Path(id): Path<String>,
963 Json(req): Json<PrepareRequest>,
964) -> Response {
965 if let Err(msg) = validate_stmt_name(&req.name) {
966 return (StatusCode::BAD_REQUEST, msg).into_response();
967 }
968 if mongreldb_query::contains_boolean_ai_predicate(&req.sql) {
969 return remote_boolean_ai_error();
970 }
971 let owner = request_owner(&state, &principal);
972 let Some(entry) = state.sessions.get(&id, &owner) else {
973 return (
974 StatusCode::NOT_FOUND,
975 "session not found or not owned by caller",
976 )
977 .into_response();
978 };
979 let _guard = entry.lock.lock().await;
980 if entry.is_closed() {
981 return (StatusCode::NOT_FOUND, "session no longer available").into_response();
982 }
983 entry.touch();
984 let sql = format!("PREPARE {} AS {}", req.name, req.sql);
985 match entry.session.run(&sql).await {
986 Ok(_) => Json(json!({ "prepared": req.name })).into_response(),
987 Err(e) => (status_for_query_error(&e), e.to_string()).into_response(),
988 }
989}
990
991#[derive(Deserialize)]
992struct ExecuteRequest {
993 name: String,
994 params: Vec<serde_json::Value>,
995 #[serde(default)]
996 format: Option<String>,
997}
998
999async fn execute_statement(
1003 State(state): State<Arc<AppState>>,
1004 OptionalPrincipal(principal): OptionalPrincipal,
1005 Path(id): Path<String>,
1006 Json(req): Json<ExecuteRequest>,
1007) -> Response {
1008 if let Err(msg) = validate_stmt_name(&req.name) {
1009 return (StatusCode::BAD_REQUEST, msg).into_response();
1010 }
1011 let owner = request_owner(&state, &principal);
1012 let Some(entry) = state.sessions.get(&id, &owner) else {
1013 return (
1014 StatusCode::NOT_FOUND,
1015 "session not found or not owned by caller",
1016 )
1017 .into_response();
1018 };
1019 let _guard = entry.lock.lock().await;
1020 if entry.is_closed() {
1021 return (StatusCode::NOT_FOUND, "session no longer available").into_response();
1022 }
1023 entry.touch();
1024 state.metrics.inc_sql_queries();
1025 let literals: Vec<String> = match req
1026 .params
1027 .iter()
1028 .map(render_sql_literal)
1029 .collect::<Result<_, _>>()
1030 {
1031 Ok(v) => v,
1032 Err(msg) => {
1033 state.metrics.inc_sql_errors();
1034 return (StatusCode::BAD_REQUEST, msg).into_response();
1035 }
1036 };
1037 let sql = format!("EXECUTE {}({})", req.name, literals.join(", "));
1038 let start = std::time::Instant::now();
1039 let result = if req.format.as_deref() == Some("arrow-stream") {
1040 entry
1041 .session
1042 .run_stream(&sql)
1043 .await
1044 .map(sql_arrow_stream_response)
1045 } else {
1046 entry
1047 .session
1048 .run(&sql)
1049 .await
1050 .map(|batches| dispatch_buffered_sql_format(req.format.as_deref(), &batches))
1051 };
1052 let elapsed = start.elapsed();
1053 if elapsed >= state.slow_query_threshold {
1054 state.metrics.inc_slow_queries();
1055 eprintln!(
1056 "[slow-query] {}\u{00b5}s \u{2014} EXECUTE {}",
1057 elapsed.as_micros(),
1058 req.name
1059 );
1060 }
1061 match result {
1062 Ok(response) => response,
1063 Err(e) => {
1064 state.metrics.inc_sql_errors();
1065 let msg = format!("{e}");
1067 let status = if msg.contains("does not exist") {
1068 StatusCode::NOT_FOUND
1069 } else {
1070 status_for_query_error(&e)
1071 };
1072 (status, format!("{msg} ({}µs)", elapsed.as_micros())).into_response()
1073 }
1074 }
1075}
1076
1077async fn deallocate_statement(
1080 State(state): State<Arc<AppState>>,
1081 OptionalPrincipal(principal): OptionalPrincipal,
1082 Path((id, name)): Path<(String, String)>,
1083) -> Response {
1084 if let Err(msg) = validate_stmt_name(&name) {
1085 return (StatusCode::BAD_REQUEST, msg).into_response();
1086 }
1087 let owner = request_owner(&state, &principal);
1088 let Some(entry) = state.sessions.get(&id, &owner) else {
1089 return (
1090 StatusCode::NOT_FOUND,
1091 "session not found or not owned by caller",
1092 )
1093 .into_response();
1094 };
1095 let _guard = entry.lock.lock().await;
1096 if entry.is_closed() {
1097 return (StatusCode::NOT_FOUND, "session no longer available").into_response();
1098 }
1099 entry.touch();
1100 let sql = format!("DEALLOCATE {name}");
1101 match entry.session.run(&sql).await {
1102 Ok(_) => Json(json!({ "deallocated": name })).into_response(),
1103 Err(e) => (status_for_query_error(&e), e.to_string()).into_response(),
1104 }
1105}
1106
1107async fn metrics_handler(
1110 State(state): State<Arc<AppState>>,
1111 OptionalPrincipal(principal): OptionalPrincipal,
1112) -> Response {
1113 if let Err(error) = state.db.require_for(
1114 request_principal(&state, &principal).as_ref(),
1115 &mongreldb_core::Permission::Admin,
1116 ) {
1117 return (status_for_error(&error), error.to_string()).into_response();
1118 }
1119 let body = state.metrics.prometheus_text(state.db.table_names().len());
1120 (
1121 [(
1122 header::CONTENT_TYPE,
1123 "text/plain; version=0.0.4; charset=utf-8".to_string(),
1124 )],
1125 body,
1126 )
1127 .into_response()
1128}
1129
1130async fn compact_all(
1132 State(state): State<Arc<AppState>>,
1133 OptionalPrincipal(principal): OptionalPrincipal,
1134) -> (StatusCode, Json<serde_json::Value>) {
1135 if let Err(error) = state.db.require_for(
1136 request_principal(&state, &principal).as_ref(),
1137 &mongreldb_core::Permission::Ddl,
1138 ) {
1139 return (
1140 status_for_error(&error),
1141 Json(json!({ "status": "error", "message": error.to_string() })),
1142 );
1143 }
1144 match state.db.compact() {
1145 Ok((compacted, skipped)) => (
1146 StatusCode::OK,
1147 Json(json!({
1148 "status": "ok",
1149 "compacted": compacted,
1150 "skipped": skipped,
1151 })),
1152 ),
1153 Err(e) => (
1154 StatusCode::INTERNAL_SERVER_ERROR,
1155 Json(json!({ "status": "error", "message": format!("{e}") })),
1156 ),
1157 }
1158}
1159
1160async fn compact_table(
1162 State(state): State<Arc<AppState>>,
1163 OptionalPrincipal(principal): OptionalPrincipal,
1164 Path(name): Path<String>,
1165) -> (StatusCode, Json<serde_json::Value>) {
1166 if let Err(error) = state.db.require_for(
1167 request_principal(&state, &principal).as_ref(),
1168 &mongreldb_core::Permission::Ddl,
1169 ) {
1170 return (
1171 status_for_error(&error),
1172 Json(json!({ "status": "error", "table": name, "message": error.to_string() })),
1173 );
1174 }
1175 match state.db.compact_table(&name) {
1176 Ok(true) => (
1177 StatusCode::OK,
1178 Json(json!({ "status": "compacted", "table": name })),
1179 ),
1180 Ok(false) => (
1181 StatusCode::OK,
1182 Json(json!({ "status": "skipped", "table": name, "reason": "fewer than 2 runs" })),
1183 ),
1184 Err(e) => (
1185 StatusCode::INTERNAL_SERVER_ERROR,
1186 Json(json!({ "status": "error", "table": name, "message": format!("{e}") })),
1187 ),
1188 }
1189}
1190
1191#[derive(Deserialize)]
1192struct CreateTableRequest {
1193 name: String,
1194 columns: Vec<ColumnDefJson>,
1195}
1196
1197#[derive(Deserialize)]
1198struct ColumnDefJson {
1199 id: u16,
1200 name: String,
1201 ty: String,
1202 primary_key: bool,
1203 #[serde(default)]
1204 nullable: bool,
1205}
1206
1207async fn create_table(
1208 State(state): State<Arc<AppState>>,
1209 OptionalPrincipal(principal): OptionalPrincipal,
1210 Json(req): Json<CreateTableRequest>,
1211) -> Response {
1212 if let Err(error) = state.db.require_for(
1213 request_principal(&state, &principal).as_ref(),
1214 &mongreldb_core::Permission::Ddl,
1215 ) {
1216 return (status_for_error(&error), error.to_string()).into_response();
1217 }
1218 let mut columns = Vec::new();
1219 for c in &req.columns {
1220 let ty = match c.ty.as_str() {
1221 "int64" | "bigint" => TypeId::Int64,
1222 "float64" | "double" => TypeId::Float64,
1223 "bytes" | "varchar" | "text" => TypeId::Bytes,
1224 "bool" => TypeId::Bool,
1225 other => {
1226 return (StatusCode::BAD_REQUEST, format!("unknown type: {other}")).into_response()
1227 }
1228 };
1229 let mut flags = mongreldb_core::schema::ColumnFlags::empty();
1230 if c.primary_key {
1231 flags = flags.with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY);
1232 }
1233 if c.nullable {
1234 flags = flags.with(mongreldb_core::schema::ColumnFlags::NULLABLE);
1235 }
1236 columns.push(mongreldb_core::schema::ColumnDef {
1237 id: c.id,
1238 name: c.name.clone(),
1239 ty,
1240 flags,
1241 default_value: None,
1242 });
1243 }
1244 let schema = Schema {
1245 schema_id: 0,
1246 columns,
1247 indexes: vec![],
1248 colocation: vec![],
1249 constraints: Default::default(),
1250 clustered: false,
1251 };
1252 if let Err(msg) = validate_table_name(&req.name) {
1253 return (StatusCode::BAD_REQUEST, msg).into_response();
1254 }
1255 match state.db.create_table(&req.name, schema) {
1256 Ok(id) => Json(json!({ "table_id": id })).into_response(),
1257 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1258 }
1259}
1260
1261async fn list_tables(
1262 State(state): State<Arc<AppState>>,
1263 OptionalPrincipal(principal): OptionalPrincipal,
1264) -> Json<Vec<String>> {
1265 let principal = request_principal(&state, &principal);
1266 Json(
1267 state
1268 .db
1269 .table_names()
1270 .into_iter()
1271 .filter(|table| {
1272 state
1273 .db
1274 .select_column_ids_for(table, principal.as_ref())
1275 .is_ok()
1276 })
1277 .collect(),
1278 )
1279}
1280
1281async fn drop_table(
1282 State(state): State<Arc<AppState>>,
1283 OptionalPrincipal(principal): OptionalPrincipal,
1284 Path(name): Path<String>,
1285) -> Response {
1286 if let Err(error) = state.db.require_for(
1287 request_principal(&state, &principal).as_ref(),
1288 &mongreldb_core::Permission::Ddl,
1289 ) {
1290 return (status_for_error(&error), error.to_string()).into_response();
1291 }
1292 match state.db.drop_table(&name) {
1293 Ok(_) => {
1294 state.idem.clear();
1298 StatusCode::OK.into_response()
1299 }
1300 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1301 }
1302}
1303
1304#[derive(Deserialize)]
1305struct PutRequest {
1306 row: Vec<serde_json::Value>,
1307}
1308
1309pub(crate) fn json_to_value(v: &serde_json::Value, expected: &TypeId) -> Value {
1310 match (v, expected) {
1311 (serde_json::Value::Number(n), TypeId::Float64) => {
1312 n.as_f64().map(Value::Float64).unwrap_or(Value::Null)
1313 }
1314 (serde_json::Value::Number(n), TypeId::Int64) => {
1315 n.as_i64().map(Value::Int64).unwrap_or(Value::Null)
1316 }
1317 (serde_json::Value::String(s), TypeId::Bytes) => Value::Bytes(s.as_bytes().to_vec()),
1318 (serde_json::Value::String(s), TypeId::Enum { variants }) => {
1319 if variants.iter().any(|v| v == s) {
1320 Value::Bytes(s.as_bytes().to_vec())
1321 } else {
1322 Value::Null
1323 }
1324 }
1325 (serde_json::Value::Bool(b), TypeId::Bool) => Value::Bool(*b),
1326 (serde_json::Value::Array(arr), TypeId::Embedding { dim }) => {
1329 if arr.len() as u32 != *dim {
1330 return Value::Null;
1331 }
1332 let vec: Option<Vec<f32>> =
1333 arr.iter().map(|el| el.as_f64().map(|f| f as f32)).collect();
1334 vec.map(Value::Embedding).unwrap_or(Value::Null)
1335 }
1336 (serde_json::Value::Null, _) => Value::Null,
1337 (serde_json::Value::Number(n), _) => {
1339 if let Some(i) = n.as_i64() {
1340 Value::Int64(i)
1341 } else if let Some(f) = n.as_f64() {
1342 Value::Float64(f)
1343 } else {
1344 Value::Null
1345 }
1346 }
1347 (serde_json::Value::String(s), _) => Value::Bytes(s.as_bytes().to_vec()),
1348 (serde_json::Value::Bool(b), _) => Value::Bool(*b),
1349 _ => Value::Null,
1350 }
1351}
1352
1353fn parse_cells(
1356 row: &[serde_json::Value],
1357 schema: &mongreldb_core::schema::Schema,
1358) -> Result<Vec<(u16, Value)>, String> {
1359 if row.len() & 1 != 0 {
1360 return Err("row must be an even-length array of [col_id, value] pairs".into());
1361 }
1362 let mut out = Vec::with_capacity(row.len() / 2);
1363 for chunk in row.chunks(2) {
1364 let col_id = chunk[0]
1365 .as_u64()
1366 .ok_or("column id must be a non-negative integer")? as u16;
1367 let expected = schema
1368 .columns
1369 .iter()
1370 .find(|c| c.id == col_id)
1371 .map(|c| c.ty.clone())
1372 .ok_or_else(|| format!("unknown column id {col_id}"))?;
1373 let val = json_to_value(&chunk[1], &expected);
1374 out.push((col_id, val));
1375 }
1376 Ok(out)
1377}
1378
1379pub(crate) fn validate_table_name(name: &str) -> Result<(), String> {
1381 if name.is_empty() {
1382 return Err("table name must not be empty".into());
1383 }
1384 if name.contains('/') || name.contains('\\') || name.contains('\0') {
1385 return Err("table name contains invalid characters".into());
1386 }
1387 Ok(())
1388}
1389
1390async fn put_row(
1391 State(state): State<Arc<AppState>>,
1392 OptionalPrincipal(principal): OptionalPrincipal,
1393 Path(name): Path<String>,
1394 Json(req): Json<PutRequest>,
1395) -> Response {
1396 let handle = match state.db.table(&name) {
1397 Ok(h) => h,
1398 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
1399 };
1400 let schema = handle.lock().schema().clone();
1401 let row = match parse_cells(&req.row, &schema) {
1402 Ok(r) => r,
1403 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
1404 };
1405 state.metrics.inc_puts();
1406 let principal = request_principal(&state, &principal);
1407 match state.db.put_for(&name, row, principal.as_ref()) {
1408 Ok(rid) => Json(json!({ "row_id": rid.0.to_string() })).into_response(),
1409 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1410 }
1411}
1412
1413async fn count(
1414 State(state): State<Arc<AppState>>,
1415 OptionalPrincipal(principal): OptionalPrincipal,
1416 Path(name): Path<String>,
1417) -> Response {
1418 let principal = request_principal(&state, &principal);
1419 match state.db.count_for(&name, principal.as_ref()) {
1420 Ok(count) => Json(json!({ "count": count })).into_response(),
1421 Err(error) => (status_for_error(&error), error.to_string()).into_response(),
1422 }
1423}
1424
1425async fn commit(
1426 State(state): State<Arc<AppState>>,
1427 OptionalPrincipal(principal): OptionalPrincipal,
1428 Path(name): Path<String>,
1429) -> Response {
1430 if let Err(error) = state.db.require_for(
1431 request_principal(&state, &principal).as_ref(),
1432 &mongreldb_core::Permission::Update {
1433 table: name.clone(),
1434 },
1435 ) {
1436 return (status_for_error(&error), error.to_string()).into_response();
1437 }
1438 let handle = match state.db.table(&name) {
1439 Ok(h) => h,
1440 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
1441 };
1442 let mut g = handle.lock();
1443 state.metrics.inc_commits();
1444 match g.commit() {
1445 Ok(epoch) => Json(json!({ "epoch": epoch.0 })).into_response(),
1446 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1447 }
1448}
1449
1450#[derive(Deserialize)]
1451struct SqlRequest {
1452 sql: String,
1453 #[serde(default)]
1456 format: Option<String>,
1457}
1458
1459async fn sql(
1460 State(state): State<Arc<AppState>>,
1461 OptionalPrincipal(principal): OptionalPrincipal,
1462 headers: axum::http::HeaderMap,
1463 Json(req): Json<SqlRequest>,
1464) -> Response {
1465 let session_id = headers
1470 .get("x-session-id")
1471 .and_then(|v| v.to_str().ok())
1472 .map(str::to_owned);
1473
1474 if let Some(sid) = session_id {
1475 let owner = request_owner(&state, &principal);
1476 let Some(entry) = state.sessions.get(&sid, &owner) else {
1477 return (
1478 StatusCode::NOT_FOUND,
1479 "session not found or not owned by caller",
1480 )
1481 .into_response();
1482 };
1483 let _guard = entry.lock.lock().await;
1486 if entry.is_closed() {
1489 return (StatusCode::NOT_FOUND, "session no longer available").into_response();
1490 }
1491 entry.touch();
1492 execute_sql(&state, &principal, &entry.session, req).await
1493 } else {
1494 let session = match MongrelSession::open_with_external_modules_as(
1495 Arc::clone(&state.db),
1496 state.external_modules.iter().cloned(),
1497 request_principal(&state, &principal),
1498 ) {
1499 Ok(s) => s,
1500 Err(e) => return (status_for_query_error(&e), e.to_string()).into_response(),
1501 };
1502 execute_sql(&state, &principal, &session, req).await
1503 }
1504}
1505
1506async fn execute_sql(
1511 state: &AppState,
1512 principal: &Option<mongreldb_core::Principal>,
1513 session: &MongrelSession,
1514 req: SqlRequest,
1515) -> Response {
1516 if mongreldb_query::contains_boolean_ai_predicate(&req.sql) {
1517 return remote_boolean_ai_error();
1518 }
1519 state.metrics.inc_sql_queries();
1520 let audited = audit::is_audited_sql(&req.sql);
1521 let actor = request_owner(state, principal);
1522 let start = std::time::Instant::now();
1523 let result = if req.format.as_deref() == Some("arrow-stream") {
1529 session
1530 .run_stream(&req.sql)
1531 .await
1532 .map(sql_arrow_stream_response)
1533 } else {
1534 session
1535 .run(&req.sql)
1536 .await
1537 .map(|batches| dispatch_buffered_sql_format(req.format.as_deref(), &batches))
1538 };
1539 let elapsed = start.elapsed();
1540 if elapsed >= state.slow_query_threshold {
1543 state.metrics.inc_slow_queries();
1544 let preview: String = req.sql.chars().take(80).collect();
1545 eprintln!(
1546 "[slow-query] {}\u{00b5}s \u{2014} {preview}",
1547 elapsed.as_micros()
1548 );
1549 }
1550 if audited {
1553 let (action, detail) = audit::redacted_ddl_detail(&req.sql, result.is_ok());
1554 state.audit.record(actor, action, detail);
1555 }
1556 match result {
1557 Ok(response) => response,
1558 Err(e) => {
1559 state.metrics.inc_sql_errors();
1560 (
1561 status_for_query_error(&e),
1562 format!("{e} ({}µs)", elapsed.as_micros()),
1563 )
1564 .into_response()
1565 }
1566 }
1567}
1568
1569fn remote_boolean_ai_error() -> Response {
1570 (
1571 StatusCode::BAD_REQUEST,
1572 "Boolean ANN/Sparse SQL is disabled remotely; use scored SQL functions",
1573 )
1574 .into_response()
1575}
1576
1577fn sql_arrow_response(batches: &[arrow::record_batch::RecordBatch]) -> Response {
1580 if batches.is_empty() {
1581 return StatusCode::OK.into_response();
1582 }
1583 let schema = batches[0].schema();
1584 let mut buf = Vec::new();
1585 let mut writer = arrow::ipc::writer::FileWriter::try_new(&mut buf, schema.as_ref()).unwrap();
1586 for b in batches {
1587 let _ = writer.write(b);
1588 }
1589 let _ = writer.finish();
1590 drop(writer);
1591 (
1592 [(header::CONTENT_TYPE, "application/vnd.apache.arrow.file")],
1593 buf,
1594 )
1595 .into_response()
1596}
1597
1598fn sql_arrow_stream_response(batches: mongreldb_query::MongrelRecordBatchStream) -> Response {
1601 use futures::{stream, StreamExt};
1602
1603 const STREAM_CT: &str = "application/vnd.apache.arrow.stream";
1604
1605 let schema = batches.schema();
1606 let mut writer = match arrow::ipc::writer::StreamWriter::try_new(Vec::new(), schema.as_ref()) {
1607 Ok(w) => w,
1608 Err(e) => {
1609 return (
1610 StatusCode::INTERNAL_SERVER_ERROR,
1611 format!("arrow stream init error: {e}"),
1612 )
1613 .into_response()
1614 }
1615 };
1616 let schema_chunk: Vec<u8> = std::mem::take(writer.get_mut());
1619 let batch_stream = stream::unfold(
1620 (batches, Some(writer)),
1621 |(mut batches, writer)| async move {
1622 let mut writer = writer?;
1623 match batches.next().await {
1624 Some(Ok(batch)) => match writer.write(&batch) {
1625 Ok(()) => {
1626 let chunk = std::mem::take(writer.get_mut());
1627 Some((Ok(chunk), (batches, Some(writer))))
1628 }
1629 Err(error) => Some((Err(std::io::Error::other(error)), (batches, None))),
1630 },
1631 Some(Err(error)) => Some((Err(std::io::Error::other(error)), (batches, None))),
1632 None => match writer.finish() {
1633 Ok(()) => {
1634 let chunk = std::mem::take(writer.get_mut());
1635 Some((Ok(chunk), (batches, None)))
1636 }
1637 Err(error) => Some((Err(std::io::Error::other(error)), (batches, None))),
1638 },
1639 }
1640 },
1641 );
1642
1643 let schema_item: Result<Vec<u8>, std::io::Error> = Ok(schema_chunk);
1646 let full = stream::iter([schema_item]).chain(batch_stream);
1647 let body = axum::body::Body::from_stream(full);
1648 ([(header::CONTENT_TYPE, STREAM_CT)], body).into_response()
1649}
1650
1651fn sql_json_response(batches: &[arrow::record_batch::RecordBatch]) -> Response {
1657 if batches.is_empty() {
1658 return ([(header::CONTENT_TYPE, "application/json")], b"[]" as &[u8]).into_response();
1659 }
1660
1661 let mut buf = Vec::new();
1662 {
1663 let mut writer = arrow::json::writer::ArrayWriter::new(&mut buf);
1664 let refs: Vec<&_> = batches.iter().collect();
1665 if let Err(e) = writer.write_batches(&refs) {
1666 return (
1667 StatusCode::INTERNAL_SERVER_ERROR,
1668 format!("JSON serialization error: {e}"),
1669 )
1670 .into_response();
1671 }
1672 let _ = writer.finish();
1673 }
1674
1675 ([(header::CONTENT_TYPE, "application/json")], buf).into_response()
1676}
1677
1678#[derive(Deserialize)]
1679struct TxnOp {
1680 table: String,
1681 op: String,
1682 cells: Option<Vec<serde_json::Value>>,
1683 row_id: Option<u64>,
1684}
1685
1686#[derive(Deserialize)]
1687struct TxnRequest {
1688 ops: Vec<TxnOp>,
1689}
1690
1691async fn txn(
1692 State(state): State<Arc<AppState>>,
1693 OptionalPrincipal(principal): OptionalPrincipal,
1694 Json(req): Json<TxnRequest>,
1695) -> Response {
1696 let mut parsed: Vec<(String, TxnAction)> = Vec::with_capacity(req.ops.len());
1700 for op in &req.ops {
1701 match op.op.as_str() {
1702 "put" => {
1703 let cells_json = match op.cells.as_ref() {
1704 Some(c) if !c.is_empty() => c,
1705 _ => {
1706 return (StatusCode::BAD_REQUEST, "put op requires non-empty cells")
1707 .into_response()
1708 }
1709 };
1710 let handle = match state.db.table(&op.table) {
1711 Ok(h) => h,
1712 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
1713 };
1714 let schema = handle.lock().schema().clone();
1715 let cells = match parse_cells(cells_json, &schema) {
1716 Ok(c) => c,
1717 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
1718 };
1719 parsed.push((op.table.clone(), TxnAction::Put(cells)));
1720 }
1721 "delete" => {
1722 let rid = match op.row_id {
1723 Some(r) => r,
1724 None => {
1725 return (StatusCode::BAD_REQUEST, "delete op requires row_id")
1726 .into_response()
1727 }
1728 };
1729 parsed.push((op.table.clone(), TxnAction::Delete(rid)));
1730 }
1731 other => {
1732 return (StatusCode::BAD_REQUEST, format!("unknown op: {other}")).into_response()
1733 }
1734 }
1735 }
1736
1737 state.metrics.inc_txns();
1738 let mut transaction = state.db.begin_as(request_principal(&state, &principal));
1739 let result = (|| {
1740 for (table, action) in &parsed {
1741 match action {
1742 TxnAction::Put(cells) => {
1743 transaction.put(table, cells.clone())?;
1744 }
1745 TxnAction::Delete(rid) => {
1746 transaction.delete(table, mongreldb_core::RowId(*rid))?;
1747 }
1748 }
1749 }
1750 transaction.commit()
1751 })();
1752 match result {
1753 Ok(_) => Json(json!({ "status": "committed" })).into_response(),
1754 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
1755 }
1756}
1757
1758enum TxnAction {
1759 Put(Vec<(u16, Value)>),
1760 Delete(u64),
1761}
1762
1763#[cfg(test)]
1764mod auth_tests {
1765 use super::*;
1766 use mongreldb_core::Database;
1767 use tempfile::tempdir;
1768
1769 #[tokio::test]
1770 async fn auth_rejects_missing_token() {
1771 let dir = tempdir().unwrap();
1772 let db = Arc::new(Database::create(dir.path()).unwrap());
1773 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
1774 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1775 let addr = listener.local_addr().unwrap();
1776 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
1777 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1779
1780 let client = reqwest::Client::new();
1781 let resp = client
1782 .get(format!("http://{addr}/health"))
1783 .send()
1784 .await
1785 .unwrap();
1786 assert_eq!(resp.status(), 401);
1787 }
1788
1789 #[tokio::test]
1790 async fn auth_accepts_valid_token() {
1791 let dir = tempdir().unwrap();
1792 let db = Arc::new(Database::create(dir.path()).unwrap());
1793 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
1794 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1795 let addr = listener.local_addr().unwrap();
1796 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
1797 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1798
1799 let client = reqwest::Client::new();
1800 let resp = client
1801 .get(format!("http://{addr}/health"))
1802 .header("Authorization", "Bearer secret")
1803 .send()
1804 .await
1805 .unwrap();
1806 assert_eq!(resp.status(), 200);
1807 }
1808
1809 #[tokio::test]
1810 async fn no_auth_when_token_unset() {
1811 let dir = tempdir().unwrap();
1812 let db = Arc::new(Database::create(dir.path()).unwrap());
1813 let app = build_app_with_config(db, std::iter::empty(), None, None);
1814 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1815 let addr = listener.local_addr().unwrap();
1816 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
1817 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1818
1819 let client = reqwest::Client::new();
1820 let resp = client
1821 .get(format!("http://{addr}/health"))
1822 .send()
1823 .await
1824 .unwrap();
1825 assert_eq!(resp.status(), 200);
1826 }
1827}
1828
1829#[cfg(test)]
1830mod wal_stream_tests {
1831 use super::*;
1832 use mongreldb_client::ReplicationFollower;
1833 use mongreldb_core::Database;
1834 use tempfile::tempdir;
1835
1836 #[tokio::test]
1837 async fn wal_stream_returns_records_after_commit() {
1838 let dir = tempdir().unwrap();
1839 let db = Arc::new(Database::create(dir.path()).unwrap());
1840 let table_schema = mongreldb_core::schema::Schema {
1841 schema_id: 1,
1842 columns: vec![mongreldb_core::schema::ColumnDef {
1843 id: 1,
1844 name: "id".into(),
1845 ty: TypeId::Int64,
1846 flags: mongreldb_core::schema::ColumnFlags::empty()
1847 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
1848 default_value: None,
1849 }],
1850 indexes: vec![],
1851 colocation: vec![],
1852 constraints: Default::default(),
1853 clustered: false,
1854 };
1855 db.create_table("items", table_schema).unwrap();
1856 let handle = db.table("items").unwrap();
1858 handle.lock().put(vec![(1, Value::Int64(1))]).unwrap();
1859 handle.lock().flush().unwrap();
1860
1861 let app = build_app(db);
1862 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1863 let addr = listener.local_addr().unwrap();
1864 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
1865 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1866
1867 let resp = reqwest::get(format!("http://{addr}/wal/stream"))
1868 .await
1869 .unwrap();
1870 assert_eq!(resp.status(), 200);
1871 let body = resp.text().await.unwrap();
1872 assert!(!body.is_empty(), "wal_stream should return records");
1874 assert!(body.contains("seq"), "response should contain seq field");
1875 }
1876
1877 #[tokio::test]
1878 async fn follower_bootstraps_and_applies_incremental_commit() {
1879 let leader_dir = tempdir().unwrap();
1880 let follower_dir = tempdir().unwrap();
1881 let follower_path = follower_dir.path().join("copy");
1882 let db = Arc::new(Database::create(leader_dir.path()).unwrap());
1883 db.create_table(
1884 "items",
1885 mongreldb_core::schema::Schema {
1886 schema_id: 1,
1887 columns: vec![mongreldb_core::schema::ColumnDef {
1888 id: 1,
1889 name: "id".into(),
1890 ty: TypeId::Int64,
1891 flags: mongreldb_core::schema::ColumnFlags::empty()
1892 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
1893 default_value: None,
1894 }],
1895 indexes: vec![],
1896 colocation: vec![],
1897 constraints: Default::default(),
1898 clustered: false,
1899 },
1900 )
1901 .unwrap();
1902 let handle = db.table("items").unwrap();
1903 handle.lock().put(vec![(1, Value::Int64(1))]).unwrap();
1904 handle.lock().commit().unwrap();
1905
1906 let app = build_app_with_config(
1907 Arc::clone(&db),
1908 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
1909 Some("replication-secret".into()),
1910 None,
1911 );
1912 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1913 let addr = listener.local_addr().unwrap();
1914 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
1915 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1916
1917 let leader_url = format!("http://{addr}");
1918 let first_path = follower_path.clone();
1919 let (mut follower, initial) = tokio::task::spawn_blocking(move || {
1920 let mut follower = ReplicationFollower::new(&leader_url, first_path)
1921 .with_bearer_token("replication-secret");
1922 let applied = follower.sync().unwrap();
1923 (follower, applied)
1924 })
1925 .await
1926 .unwrap();
1927 assert_eq!(initial, 0);
1928
1929 handle.lock().put(vec![(1, Value::Int64(2))]).unwrap();
1930 handle.lock().commit().unwrap();
1931 let applied = tokio::task::spawn_blocking(move || {
1932 let count = follower.sync().unwrap();
1933 (follower, count)
1934 })
1935 .await
1936 .unwrap();
1937 follower = applied.0;
1938 assert!(applied.1 > 0);
1939 assert!(follower.last_epoch() > 0);
1940
1941 let replica = Database::open(&follower_path).unwrap();
1942 assert_eq!(replica.table("items").unwrap().lock().count(), 2);
1943 drop(replica);
1944
1945 db.set_spill_threshold(1);
1946 db.transaction(|txn| {
1947 txn.put("items", vec![(1, Value::Int64(3))])?;
1948 Ok(())
1949 })
1950 .unwrap();
1951 let (follower_after_bootstrap, applied) = tokio::task::spawn_blocking(move || {
1952 let count = follower.sync().unwrap();
1953 (follower, count)
1954 })
1955 .await
1956 .unwrap();
1957 assert_eq!(applied, 0, "spilled run should trigger safe rebootstrap");
1958 assert!(follower_after_bootstrap.last_epoch() > 0);
1959 let replica = Database::open(&follower_path).unwrap();
1960 assert_eq!(replica.table("items").unwrap().lock().count(), 3);
1961 }
1962}
1963
1964#[cfg(test)]
1965mod metrics_tests {
1966 use super::*;
1967 use mongreldb_core::Database;
1968 use tempfile::tempdir;
1969
1970 async fn setup() -> std::net::SocketAddr {
1973 let dir = tempdir().unwrap();
1974 let db = Arc::new(Database::create(dir.path()).unwrap());
1975 let table_schema = mongreldb_core::schema::Schema {
1976 schema_id: 1,
1977 columns: vec![mongreldb_core::schema::ColumnDef {
1978 id: 1,
1979 name: "id".into(),
1980 ty: TypeId::Int64,
1981 flags: mongreldb_core::schema::ColumnFlags::empty()
1982 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
1983 default_value: None,
1984 }],
1985 indexes: vec![],
1986 colocation: vec![],
1987 constraints: Default::default(),
1988 clustered: false,
1989 };
1990 db.create_table("items", table_schema).unwrap();
1991 let app = build_app(db);
1992 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1993 let addr = listener.local_addr().unwrap();
1994 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
1995 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1996 addr
1997 }
1998
1999 #[tokio::test]
2000 async fn metrics_endpoint_returns_prometheus_text() {
2001 let addr = setup().await;
2002 let client = reqwest::Client::new();
2003
2004 let _ = client
2006 .post(format!("http://{addr}/tables/items/put"))
2007 .json(&json!({ "row": [1, 1] }))
2008 .send()
2009 .await
2010 .unwrap();
2011 let _ = client
2012 .post(format!("http://{addr}/sql"))
2013 .json(&json!({ "sql": "SELECT count(*) FROM items" }))
2014 .send()
2015 .await
2016 .unwrap();
2017
2018 let resp = client
2019 .get(format!("http://{addr}/metrics"))
2020 .send()
2021 .await
2022 .unwrap();
2023 assert_eq!(resp.status(), 200);
2024 let ct = resp
2025 .headers()
2026 .get("content-type")
2027 .and_then(|v| v.to_str().ok())
2028 .unwrap_or_default()
2029 .to_string();
2030 assert!(
2031 ct.contains("text/plain"),
2032 "content-type is prometheus text: {ct}"
2033 );
2034 let body = resp.text().await.unwrap();
2035 assert!(body.contains("# TYPE mongreldb_sql_queries_total counter"));
2037 assert!(body.contains("# TYPE mongreldb_puts_total counter"));
2038 assert!(body.contains("# TYPE mongreldb_tables gauge"));
2039 assert!(
2041 body.contains("mongreldb_sql_queries_total 1"),
2042 "sql_queries counter should reflect the /sql call: {body}"
2043 );
2044 assert!(
2045 body.contains("mongreldb_puts_total 1"),
2046 "puts counter should reflect the put call: {body}"
2047 );
2048 assert!(body.contains("mongreldb_tables 1"));
2050 }
2051
2052 #[tokio::test]
2053 async fn metrics_error_counter_increments_on_bad_sql() {
2054 let addr = setup().await;
2055 let client = reqwest::Client::new();
2056 let _ = client
2058 .post(format!("http://{addr}/sql"))
2059 .json(&json!({ "sql": "SELECT * FROM does_not_exist" }))
2060 .send()
2061 .await
2062 .unwrap();
2063 let body = client
2064 .get(format!("http://{addr}/metrics"))
2065 .send()
2066 .await
2067 .unwrap()
2068 .text()
2069 .await
2070 .unwrap();
2071 assert!(
2072 body.contains("mongreldb_sql_errors_total 1"),
2073 "sql_errors should increment on a failed query: {body}"
2074 );
2075 }
2076
2077 #[tokio::test]
2078 async fn arrow_stream_returns_ipc_stream_bytes() {
2079 let addr = setup().await;
2080 let client = reqwest::Client::new();
2081 for i in 1..=3 {
2084 let resp = client
2085 .post(format!("http://{addr}/tables/items/put"))
2086 .json(&json!({ "row": [1, i] }))
2087 .send()
2088 .await
2089 .unwrap();
2090 assert_eq!(resp.status(), 200, "put should succeed");
2091 }
2092 let _ = client
2093 .post(format!("http://{addr}/tables/items/commit"))
2094 .send()
2095 .await
2096 .unwrap();
2097 let count_body = client
2099 .get(format!("http://{addr}/tables/items/count"))
2100 .send()
2101 .await
2102 .unwrap()
2103 .text()
2104 .await
2105 .unwrap();
2106 assert!(
2107 count_body.contains("\"count\":3"),
2108 "expected 3 visible rows, got: {count_body}"
2109 );
2110 let resp = client
2111 .post(format!("http://{addr}/sql"))
2112 .json(&json!({ "sql": "SELECT count(*) FROM items", "format": "arrow-stream" }))
2113 .send()
2114 .await
2115 .unwrap();
2116 assert_eq!(resp.status(), 200, "streaming query should succeed");
2117 let ct = resp
2118 .headers()
2119 .get("content-type")
2120 .and_then(|v| v.to_str().ok())
2121 .unwrap_or_default()
2122 .to_string();
2123 assert!(
2124 ct.contains("application/vnd.apache.arrow.stream"),
2125 "content-type should be the arrow stream format: {ct}"
2126 );
2127 let bytes = resp.bytes().await.unwrap();
2128 assert!(
2132 !bytes.is_empty(),
2133 "arrow stream body should contain schema + batch + EOS"
2134 );
2135 assert!(
2136 bytes.starts_with(&0xFFFFFFFFu32.to_le_bytes()),
2137 "arrow stream must begin with the IPC continuation marker"
2138 );
2139 assert!(
2140 bytes.ends_with(&[0u8, 0, 0, 0]),
2141 "arrow stream should end with the EOS marker (trailing zero length)"
2142 );
2143 }
2144}
2145
2146#[cfg(test)]
2147mod streaming_tests {
2148 use super::*;
2149 use arrow::array::Int64Array;
2150 use arrow::datatypes::{DataType, Field, Schema};
2151 use arrow::record_batch::RecordBatch;
2152 use datafusion::common::DataFusionError;
2153 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
2154 use futures::StreamExt;
2155 use std::sync::Arc as StdArc;
2156
2157 fn batch_stream(batches: Vec<RecordBatch>) -> mongreldb_query::MongrelRecordBatchStream {
2158 let schema = batches
2159 .first()
2160 .map(RecordBatch::schema)
2161 .unwrap_or_else(|| StdArc::new(Schema::empty()));
2162 let batches =
2163 futures::stream::iter(batches.into_iter().map(Ok::<RecordBatch, DataFusionError>));
2164 Box::pin(RecordBatchStreamAdapter::new(schema, batches))
2165 }
2166
2167 #[tokio::test]
2172 async fn arrow_stream_serializes_multiple_batches_roundtrip() {
2173 let schema = StdArc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)]));
2174 let b1 = RecordBatch::try_new(
2175 schema.clone(),
2176 vec![StdArc::new(Int64Array::from(vec![1, 2]))],
2177 )
2178 .unwrap();
2179 let b2 = RecordBatch::try_new(
2180 schema.clone(),
2181 vec![StdArc::new(Int64Array::from(vec![3, 4, 5]))],
2182 )
2183 .unwrap();
2184
2185 let resp = sql_arrow_stream_response(batch_stream(vec![b1, b2]));
2186 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
2187 .await
2188 .unwrap();
2189
2190 assert!(bytes.starts_with(&0xFFFFFFFFu32.to_le_bytes()));
2192
2193 let slice: &[u8] = bytes.as_ref();
2196 let mut reader = arrow::ipc::reader::StreamReader::try_new(slice, None).unwrap();
2197 let mut total_rows = 0;
2198 for batch in reader.by_ref() {
2199 let batch = batch.expect("each IPC message should decode");
2200 total_rows += batch.num_rows();
2201 }
2202 assert_eq!(
2203 total_rows, 5,
2204 "all rows should round-trip through the stream"
2205 );
2206 }
2207
2208 #[tokio::test]
2209 async fn arrow_stream_emits_schema_before_first_batch() {
2210 let schema = StdArc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)]));
2211 let pending = futures::stream::pending::<Result<RecordBatch, DataFusionError>>();
2212 let batches = Box::pin(RecordBatchStreamAdapter::new(schema, pending));
2213 let mut body = sql_arrow_stream_response(batches)
2214 .into_body()
2215 .into_data_stream();
2216
2217 let chunk = tokio::time::timeout(std::time::Duration::from_millis(100), body.next())
2218 .await
2219 .expect("schema chunk should not wait for a query batch")
2220 .unwrap()
2221 .unwrap();
2222 assert!(chunk.starts_with(&0xFFFFFFFFu32.to_le_bytes()));
2223 }
2224
2225 #[tokio::test]
2226 async fn arrow_stream_empty_query_is_valid_ipc() {
2227 let resp = sql_arrow_stream_response(batch_stream(Vec::new()));
2228 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
2229 .await
2230 .unwrap();
2231 let slice: &[u8] = bytes.as_ref();
2232 let reader = arrow::ipc::reader::StreamReader::try_new(slice, None).unwrap();
2233 assert_eq!(reader.count(), 0);
2234 }
2235}
2236
2237#[cfg(test)]
2238mod audit_tests {
2239 use super::*;
2240 use mongreldb_core::Database;
2241 use tempfile::tempdir;
2242
2243 async fn auth_setup(password: &str) -> std::net::SocketAddr {
2245 let dir = tempdir().unwrap();
2246 let db = Arc::new(Database::create(dir.path()).unwrap());
2247 db.create_user("alice", password).unwrap();
2248 db.set_user_admin("alice", true).unwrap();
2249 let app = build_app_full(
2250 db,
2251 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
2252 None,
2253 None,
2254 true,
2255 );
2256 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2257 let addr = listener.local_addr().unwrap();
2258 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2259 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2260 addr
2261 }
2262
2263 #[tokio::test]
2264 async fn audit_records_login_success_and_failure() {
2265 let addr = auth_setup("s3cret").await;
2266 let client = reqwest::Client::new();
2267
2268 let resp = client
2270 .get(format!("http://{addr}/health"))
2271 .header("Authorization", basic("alice", "s3cret"))
2272 .send()
2273 .await
2274 .unwrap();
2275 assert_eq!(resp.status(), 200);
2276
2277 let resp = client
2279 .get(format!("http://{addr}/health"))
2280 .header("Authorization", basic("alice", "wrong"))
2281 .send()
2282 .await
2283 .unwrap();
2284 assert_eq!(resp.status(), 401);
2285
2286 let body = client
2287 .get(format!("http://{addr}/audit"))
2288 .header("Authorization", basic("alice", "s3cret"))
2289 .send()
2290 .await
2291 .unwrap()
2292 .text()
2293 .await
2294 .unwrap();
2295 assert!(
2296 body.contains("\"action\":\"login.ok\""),
2297 "audit should record the successful login: {body}"
2298 );
2299 assert!(
2300 body.contains("\"action\":\"login.fail\""),
2301 "audit should record the failed login: {body}"
2302 );
2303 assert!(
2304 body.contains("\"principal\":\"alice\""),
2305 "audit should attribute events to alice: {body}"
2306 );
2307 }
2308
2309 #[tokio::test]
2310 async fn audit_records_ddl_sql() {
2311 let dir = tempdir().unwrap();
2312 let db = Arc::new(Database::create(dir.path()).unwrap());
2313 let app = build_app(db);
2314 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2315 let addr = listener.local_addr().unwrap();
2316 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2317 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2318
2319 let client = reqwest::Client::new();
2320 let _ = client
2321 .post(format!("http://{addr}/sql"))
2322 .json(&json!({ "sql": "CREATE TABLE t (id BIGINT PRIMARY KEY)" }))
2323 .send()
2324 .await
2325 .unwrap();
2326 let _ = client
2328 .post(format!("http://{addr}/sql"))
2329 .json(&json!({ "sql": "SELECT 1" }))
2330 .send()
2331 .await
2332 .unwrap();
2333
2334 let body = client
2335 .get(format!("http://{addr}/audit"))
2336 .send()
2337 .await
2338 .unwrap()
2339 .text()
2340 .await
2341 .unwrap();
2342 assert!(
2343 body.contains("\"action\":\"ddl.ok\""),
2344 "audit should record the successful DDL statement: {body}"
2345 );
2346 assert!(
2347 body.contains("CREATE TABLE"),
2348 "audit detail should carry the DDL snippet: {body}"
2349 );
2350 assert!(
2352 !body.contains("SELECT 1"),
2353 "non-DDL reads should not be audited: {body}"
2354 );
2355 }
2356
2357 #[tokio::test]
2358 async fn audit_redacts_credential_passwords() {
2359 let dir = tempdir().unwrap();
2360 let db = Arc::new(Database::create(dir.path()).unwrap());
2361 let app = build_app(db);
2362 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2363 let addr = listener.local_addr().unwrap();
2364 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2365 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2366
2367 let client = reqwest::Client::new();
2368 let _ = client
2371 .post(format!("http://{addr}/sql"))
2372 .json(&json!({ "sql": "CREATE USER alice WITH PASSWORD 'topsecret'" }))
2373 .send()
2374 .await
2375 .unwrap();
2376
2377 let body = client
2378 .get(format!("http://{addr}/audit"))
2379 .send()
2380 .await
2381 .unwrap()
2382 .text()
2383 .await
2384 .unwrap();
2385 assert!(
2386 !body.contains("topsecret"),
2387 "password must never appear in the audit log: {body}"
2388 );
2389 assert!(
2390 body.contains("redacted credential statement"),
2391 "credential DDL should be recorded as redacted: {body}"
2392 );
2393 }
2394
2395 fn basic(user: &str, pass: &str) -> String {
2396 let raw = format!("{user}:{pass}");
2397 format!("Basic {}", base64_encode(raw.as_bytes()))
2398 }
2399
2400 fn base64_encode(input: &[u8]) -> String {
2401 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2402 let mut out = String::new();
2403 let mut buf = 0u32;
2404 let mut bits = 0u32;
2405 for &b in input {
2406 buf = (buf << 8) | b as u32;
2407 bits += 8;
2408 while bits >= 6 {
2409 bits -= 6;
2410 out.push(TABLE[((buf >> bits) & 0x3F) as usize] as char);
2411 }
2412 }
2413 if bits > 0 {
2414 out.push(TABLE[((buf << (6 - bits)) & 0x3F) as usize] as char);
2415 }
2416 while !out.len().is_multiple_of(4) {
2417 out.push('=');
2418 }
2419 out
2420 }
2421}
2422
2423#[cfg(test)]
2424mod session_tests {
2425 use super::*;
2426 use mongreldb_core::Database;
2427 use tempfile::tempdir;
2428
2429 async fn setup() -> (tempfile::TempDir, std::net::SocketAddr) {
2433 let dir = tempdir().unwrap();
2434 let db = Arc::new(Database::create(dir.path()).unwrap());
2435 let table_schema = mongreldb_core::schema::Schema {
2436 schema_id: 1,
2437 columns: vec![mongreldb_core::schema::ColumnDef {
2438 id: 1,
2439 name: "id".into(),
2440 ty: TypeId::Int64,
2441 flags: mongreldb_core::schema::ColumnFlags::empty()
2442 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
2443 default_value: None,
2444 }],
2445 indexes: vec![],
2446 colocation: vec![],
2447 constraints: Default::default(),
2448 clustered: false,
2449 };
2450 db.create_table("items", table_schema).unwrap();
2451 let app = build_app(db);
2452 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2453 let addr = listener.local_addr().unwrap();
2454 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
2455 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2456 (dir, addr)
2457 }
2458
2459 async fn open_session(client: &reqwest::Client, addr: &std::net::SocketAddr) -> String {
2461 let resp = client
2462 .post(format!("http://{addr}/sessions"))
2463 .send()
2464 .await
2465 .unwrap();
2466 assert_eq!(resp.status(), 200);
2467 resp.json::<serde_json::Value>()
2468 .await
2469 .unwrap()
2470 .get("session_id")
2471 .unwrap()
2472 .as_str()
2473 .unwrap()
2474 .to_string()
2475 }
2476
2477 async fn sql_on(
2478 client: &reqwest::Client,
2479 addr: &std::net::SocketAddr,
2480 session: &str,
2481 sql: &str,
2482 ) -> reqwest::Response {
2483 client
2484 .post(format!("http://{addr}/sql"))
2485 .header("X-Session-ID", session)
2486 .json(&json!({ "sql": sql }))
2487 .send()
2488 .await
2489 .unwrap()
2490 }
2491
2492 async fn count_items(client: &reqwest::Client, addr: &std::net::SocketAddr) -> u64 {
2493 client
2494 .get(format!("http://{addr}/tables/items/count"))
2495 .send()
2496 .await
2497 .unwrap()
2498 .json::<serde_json::Value>()
2499 .await
2500 .unwrap()
2501 .get("count")
2502 .unwrap()
2503 .as_u64()
2504 .unwrap()
2505 }
2506
2507 #[tokio::test]
2508 async fn cross_request_transaction_commits() {
2509 let (_dir, addr) = setup().await;
2510 let client = reqwest::Client::new();
2511 let session = open_session(&client, &addr).await;
2512
2513 let r = sql_on(&client, &addr, &session, "BEGIN").await;
2515 assert_eq!(r.status(), 200);
2516 let r = sql_on(
2517 &client,
2518 &addr,
2519 &session,
2520 "INSERT INTO items (id) VALUES (1)",
2521 )
2522 .await;
2523 assert_eq!(r.status(), 200, "INSERT should stage successfully");
2524 assert_eq!(count_items(&client, &addr).await, 0);
2526 let r = sql_on(&client, &addr, &session, "COMMIT").await;
2527 assert_eq!(r.status(), 200);
2528
2529 assert_eq!(count_items(&client, &addr).await, 1);
2531 }
2532
2533 #[tokio::test]
2534 async fn cross_request_transaction_rolls_back() {
2535 let (_dir, addr) = setup().await;
2536 let client = reqwest::Client::new();
2537 let session = open_session(&client, &addr).await;
2538
2539 sql_on(&client, &addr, &session, "BEGIN").await;
2540 sql_on(
2541 &client,
2542 &addr,
2543 &session,
2544 "INSERT INTO items (id) VALUES (5)",
2545 )
2546 .await;
2547 let r = sql_on(&client, &addr, &session, "ROLLBACK").await;
2549 assert_eq!(r.status(), 200);
2550 assert_eq!(
2551 count_items(&client, &addr).await,
2552 0,
2553 "rollback discards staged writes"
2554 );
2555 }
2556
2557 #[tokio::test]
2558 async fn unknown_session_id_is_404() {
2559 let (_dir, addr) = setup().await;
2560 let client = reqwest::Client::new();
2561 let resp = client
2562 .post(format!("http://{addr}/sql"))
2563 .header("X-Session-ID", "does-not-exist")
2564 .json(&json!({ "sql": "SELECT 1" }))
2565 .send()
2566 .await
2567 .unwrap();
2568 assert_eq!(resp.status(), 404);
2569 }
2570
2571 #[tokio::test]
2572 async fn close_session_ends_cross_request_state() {
2573 let (_dir, addr) = setup().await;
2574 let client = reqwest::Client::new();
2575 let session = open_session(&client, &addr).await;
2576
2577 sql_on(&client, &addr, &session, "BEGIN").await;
2579 let r = client
2580 .delete(format!("http://{addr}/sessions/{session}"))
2581 .send()
2582 .await
2583 .unwrap();
2584 assert_eq!(r.status(), 200);
2585
2586 let resp = sql_on(&client, &addr, &session, "COMMIT").await;
2588 assert_eq!(resp.status(), 404, "closed session is no longer usable");
2589 }
2590
2591 #[tokio::test]
2592 async fn no_session_header_uses_fresh_ephemeral_session() {
2593 let (_dir, addr) = setup().await;
2596 let client = reqwest::Client::new();
2597 let resp = client
2598 .post(format!("http://{addr}/sql"))
2599 .json(&json!({ "sql": "INSERT INTO items (id) VALUES (42)" }))
2600 .send()
2601 .await
2602 .unwrap();
2603 assert_eq!(resp.status(), 200);
2604 assert_eq!(count_items(&client, &addr).await, 1);
2605 }
2606
2607 #[tokio::test]
2608 async fn prepared_statement_prepare_execute_and_reuse() {
2609 let (_dir, addr) = setup().await;
2610 let client = reqwest::Client::new();
2611 let session = open_session(&client, &addr).await;
2612 sql_on(
2613 &client,
2614 &addr,
2615 &session,
2616 "INSERT INTO items (id) VALUES (1), (2), (3), (4)",
2617 )
2618 .await;
2619
2620 let resp = client
2622 .post(format!("http://{addr}/sessions/{session}/prepare"))
2623 .json(&json!({"name":"gt","sql":"SELECT id FROM items WHERE id > $1"}))
2624 .send()
2625 .await
2626 .unwrap();
2627 assert_eq!(resp.status(), 200);
2628
2629 let resp = client
2631 .post(format!("http://{addr}/sessions/{session}/execute"))
2632 .json(&json!({"name":"gt","params":[2]}))
2633 .send()
2634 .await
2635 .unwrap();
2636 assert_eq!(resp.status(), 200);
2637 let body = resp.json::<serde_json::Value>().await.unwrap();
2638 let arr = body
2639 .as_array()
2640 .expect("execute returns a JSON array of rows");
2641 assert_eq!(arr.len(), 2, "ids > 2 are {{3,4}}: {body}");
2642
2643 let resp = client
2645 .post(format!("http://{addr}/sessions/{session}/execute"))
2646 .json(&json!({"name":"gt","params":[3]}))
2647 .send()
2648 .await
2649 .unwrap();
2650 let body = resp.json::<serde_json::Value>().await.unwrap();
2651 assert_eq!(body.as_array().unwrap().len(), 1, "ids > 3 is {{4}}");
2652 }
2653
2654 #[tokio::test]
2655 async fn prepared_statement_deallocate_then_execute_fails() {
2656 let (_dir, addr) = setup().await;
2657 let client = reqwest::Client::new();
2658 let session = open_session(&client, &addr).await;
2659 let _ = client
2660 .post(format!("http://{addr}/sessions/{session}/prepare"))
2661 .json(&json!({"name":"p","sql":"SELECT $1"}))
2662 .send()
2663 .await
2664 .unwrap();
2665 let resp = client
2666 .delete(format!("http://{addr}/sessions/{session}/statements/p"))
2667 .send()
2668 .await
2669 .unwrap();
2670 assert_eq!(resp.status(), 200);
2671 let resp = client
2673 .post(format!("http://{addr}/sessions/{session}/execute"))
2674 .json(&json!({"name":"p","params":[1]}))
2675 .send()
2676 .await
2677 .unwrap();
2678 assert_ne!(resp.status(), 200, "execute after DEALLOCATE must fail");
2679 }
2680
2681 #[tokio::test]
2682 async fn prepared_statement_rejects_bad_name() {
2683 let (_dir, addr) = setup().await;
2684 let client = reqwest::Client::new();
2685 let session = open_session(&client, &addr).await;
2686 let resp = client
2687 .post(format!("http://{addr}/sessions/{session}/prepare"))
2688 .json(&json!({"name":"1bad","sql":"SELECT 1"}))
2689 .send()
2690 .await
2691 .unwrap();
2692 assert_eq!(
2693 resp.status(),
2694 400,
2695 "statement name starting with a digit must be rejected"
2696 );
2697 }
2698}