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