1use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::{Arc, Mutex};
19use std::time::Duration;
20
21use axum::extract::{Path, State};
22use axum::http::header;
23use axum::http::StatusCode;
24use axum::response::{IntoResponse, Response};
25use axum::routing::{get, post};
26use axum::Json;
27use mongreldb_core::schema::{Schema, TypeId};
28use mongreldb_core::{CancellationReason, Database, Value};
29use mongreldb_query::{
30 CancelOutcome, CompactFinishedQuery, ExternalTableModule, ManagedQueryBatches, MongrelSession,
31 QueryId, RegisteredQueryGuard, RegisteredSqlQuery, SqlQueryOptions, SqlQueryPhase,
32 SqlQueryRegistry, SqlStreamCompletion,
33};
34use serde::{Deserialize, Serialize};
35use serde_json::json;
36use sha2::Digest;
37use zeroize::Zeroizing;
38
39mod audit;
40mod kit;
41mod metrics;
42mod pre_cancel;
43mod procedure;
44mod sessions;
45mod sql_idempotency;
46mod sql_pages;
47mod trigger;
48
49pub use sessions::{spawn_session_reaper, SessionStore};
50
51fn client_closed_request_status() -> StatusCode {
52 StatusCode::from_u16(499).unwrap_or(StatusCode::BAD_REQUEST)
53}
54
55fn cancellation_checkpoint_error(query: &RegisteredSqlQuery) -> mongreldb_query::MongrelQueryError {
56 query.checkpoint().err().unwrap_or_else(|| {
57 mongreldb_query::MongrelQueryError::InvalidQueryState(
58 "cancellation notification observed without a terminal checkpoint".into(),
59 )
60 })
61}
62
63fn status_for_error(e: &mongreldb_core::MongrelError) -> StatusCode {
68 use mongreldb_core::MongrelError;
69 match e {
70 MongrelError::AuthRequired | MongrelError::InvalidCredentials { .. } => {
71 StatusCode::UNAUTHORIZED
72 }
73 MongrelError::AuthNotRequired => StatusCode::BAD_REQUEST,
74 MongrelError::PermissionDenied { .. } => StatusCode::FORBIDDEN,
75 MongrelError::InvalidArgument(_) => StatusCode::CONFLICT,
76 MongrelError::Conflict(_) => StatusCode::CONFLICT,
77 MongrelError::ReadOnlyReplica => StatusCode::CONFLICT,
78 MongrelError::NotFound(_) => StatusCode::NOT_FOUND,
79 MongrelError::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
80 MongrelError::WorkBudgetExceeded => StatusCode::TOO_MANY_REQUESTS,
81 MongrelError::Cancelled => client_closed_request_status(),
82 MongrelError::CursorStale(_) => StatusCode::CONFLICT,
83 MongrelError::CursorExpired => StatusCode::GONE,
84 _ => StatusCode::INTERNAL_SERVER_ERROR,
85 }
86}
87
88fn status_for_query_error(e: &mongreldb_query::MongrelQueryError) -> StatusCode {
91 use mongreldb_query::MongrelQueryError;
92 match e {
93 MongrelQueryError::Core(core) => status_for_error(core),
94 MongrelQueryError::DeadlineExceeded { .. } => StatusCode::GATEWAY_TIMEOUT,
95 MongrelQueryError::QueryCancelled { .. } => client_closed_request_status(),
96 MongrelQueryError::QueryIdConflict { .. } => StatusCode::CONFLICT,
97 MongrelQueryError::QueryRegistryFull => StatusCode::SERVICE_UNAVAILABLE,
98 MongrelQueryError::ResultLimitExceeded { .. } => StatusCode::PAYLOAD_TOO_LARGE,
99 MongrelQueryError::TransactionAborted => StatusCode::CONFLICT,
100 MongrelQueryError::NoSqlTransaction | MongrelQueryError::SavepointNotFound { .. } => {
101 StatusCode::CONFLICT
102 }
103 MongrelQueryError::CommitOutcome { .. } => StatusCode::CONFLICT,
104 MongrelQueryError::OutcomeUnknown { .. } => StatusCode::CONFLICT,
105 _ => StatusCode::INTERNAL_SERVER_ERROR,
106 }
107}
108
109struct OptionalPrincipal(Option<mongreldb_core::Principal>);
113
114impl<S> axum::extract::FromRequestParts<S> for OptionalPrincipal
115where
116 S: Send + Sync,
117{
118 type Rejection = std::convert::Infallible;
119
120 async fn from_request_parts(
121 parts: &mut axum::http::request::Parts,
122 _state: &S,
123 ) -> Result<Self, Self::Rejection> {
124 Ok(OptionalPrincipal(
125 parts.extensions.get::<mongreldb_core::Principal>().cloned(),
126 ))
127 }
128}
129
130struct AppState {
131 db: Arc<Database>,
132 idem: kit::IdempotencyStore,
133 external_modules: Vec<Arc<dyn ExternalTableModule>>,
134 auth_token: Option<String>,
135 user_auth: bool,
137 metrics: Arc<metrics::Metrics>,
139 slow_query_threshold: std::time::Duration,
141 audit: Arc<audit::AuditLog>,
143 sessions: Arc<sessions::SessionStore>,
146 ai_semaphore: Arc<tokio::sync::Semaphore>,
148 query_registry: Arc<SqlQueryRegistry>,
150 query_lifecycle: Mutex<()>,
152 pre_cancellations: pre_cancel::PreCancelStore,
154 sql_idempotency: Arc<sql_idempotency::SqlIdempotencyStore>,
156 sql_pages: sql_pages::SqlPageStore,
158 sql_semaphore: Arc<tokio::sync::Semaphore>,
160 sql_page_semaphore: Arc<tokio::sync::Semaphore>,
162 sql_page_default_timeout: std::time::Duration,
163 sql_page_max_timeout: std::time::Duration,
164 sql_default_timeout: std::time::Duration,
165 sql_max_timeout: std::time::Duration,
166 sql_cancel_grace: std::time::Duration,
167 sql_max_output_bytes: usize,
168 sql_max_output_rows: usize,
169 accepting_sql: Arc<AtomicBool>,
170 cursor_mac_key: CursorMacKey,
172}
173
174#[derive(Default)]
175struct CursorMacKey {
176 key: Mutex<Option<[u8; 32]>>,
177}
178
179impl CursorMacKey {
180 fn get(&self) -> mongreldb_core::Result<[u8; 32]> {
181 let mut key = match self.key.lock() {
182 Ok(key) => key,
183 Err(poisoned) => poisoned.into_inner(),
184 };
185 if let Some(key) = *key {
186 return Ok(key);
187 }
188 let mut generated = [0u8; 32];
189 mongreldb_core::encryption::fill_random(&mut generated)?;
190 *key = Some(generated);
191 Ok(generated)
192 }
193}
194
195#[derive(Clone)]
198pub struct ServerControl {
199 query_registry: Arc<SqlQueryRegistry>,
200 sessions: Arc<sessions::SessionStore>,
201 accepting_sql: Arc<AtomicBool>,
202 cancel_grace: std::time::Duration,
203 metrics: Arc<metrics::Metrics>,
204}
205
206impl ServerControl {
207 pub async fn shutdown(&self) -> usize {
208 self.accepting_sql.store(false, Ordering::Release);
209 self.query_registry
210 .cancel_all(CancellationReason::ServerShutdown);
211 let deadline = tokio::time::Instant::now() + self.cancel_grace;
212 while self.query_registry.active_count() > 0 && tokio::time::Instant::now() < deadline {
213 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
214 }
215 self.sessions.close_all();
216 let stuck_queries = self.query_registry.active_statuses();
217 for status in &stuck_queries {
218 eprintln!(
219 "[sql-cancel-stuck] query_id={} phase={}",
220 status.query_id,
221 query_phase_name(status.phase)
222 );
223 }
224 let stuck = stuck_queries.len();
225 self.metrics.add_sql_stuck_after_cancel(stuck);
226 stuck
227 }
228}
229
230pub fn build_app(db: Arc<Database>) -> axum::Router {
231 build_app_with_config(
232 db,
233 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
234 None,
235 None,
236 )
237}
238
239pub fn build_app_with_external_modules(
240 db: Arc<Database>,
241 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
242) -> axum::Router {
243 build_app_with_config(db, external_modules, None, None)
244}
245
246pub fn build_app_with_config(
248 db: Arc<Database>,
249 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
250 auth_token: Option<String>,
251 max_connections: Option<usize>,
252) -> axum::Router {
253 build_app_full(db, external_modules, auth_token, max_connections, false)
254}
255
256pub fn build_app_full(
259 db: Arc<Database>,
260 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
261 auth_token: Option<String>,
262 max_connections: Option<usize>,
263 user_auth: bool,
264) -> axum::Router {
265 let sessions = Arc::new(sessions::SessionStore::new(
266 default_max_sessions(),
267 default_session_idle_timeout(),
268 ));
269 build_app_with_sessions(
270 db,
271 external_modules,
272 auth_token,
273 max_connections,
274 user_auth,
275 sessions,
276 )
277}
278
279pub fn build_app_with_sessions(
283 db: Arc<Database>,
284 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
285 auth_token: Option<String>,
286 max_connections: Option<usize>,
287 user_auth: bool,
288 sessions: Arc<sessions::SessionStore>,
289) -> axum::Router {
290 build_app_with_sessions_and_control(
291 db,
292 external_modules,
293 auth_token,
294 max_connections,
295 user_auth,
296 sessions,
297 )
298 .0
299}
300
301pub fn build_app_with_sessions_and_control(
303 db: Arc<Database>,
304 external_modules: impl IntoIterator<Item = Arc<dyn ExternalTableModule>>,
305 auth_token: Option<String>,
306 max_connections: Option<usize>,
307 user_auth: bool,
308 sessions: Arc<sessions::SessionStore>,
309) -> (axum::Router, ServerControl) {
310 db.set_replication_wal_retention_segments(default_replication_wal_segments());
311 if let Err(error) = db.set_history_retention_epochs(default_history_retention_epochs()) {
312 eprintln!("[history] failed to configure retention: {error}");
313 }
314 let max_active_queries = default_sql_max_active_queries();
315 let query_registry = Arc::new(SqlQueryRegistry::new_with_limits(
316 max_active_queries,
317 default_sql_finished_detail_max_entries(),
318 default_sql_finished_detail_max_bytes(),
319 default_sql_finished_compact_max_entries(),
320 default_sql_finished_compact_max_bytes(),
321 default_sql_finished_query_ttl(),
322 ));
323 let accepting_sql = Arc::new(AtomicBool::new(true));
324 let sql_cancel_grace = default_sql_cancel_grace();
325 let sql_max_timeout = default_sql_max_timeout();
326 let sql_default_timeout = default_sql_default_timeout().min(sql_max_timeout);
327 let metrics = Arc::new(metrics::Metrics::default());
328 let (idempotency_root, idempotency_integrity) =
329 match sql_idempotency::IdempotencyIntegrity::for_database(&db) {
330 Ok((root, integrity)) => (root, Some(integrity)),
331 Err(error) => {
332 eprintln!("[idempotency] durable integrity key unavailable: {error}");
333 (db.durable_root(), None)
334 }
335 };
336 let sql_idempotency = Arc::new(sql_idempotency::SqlIdempotencyStore::new_with_integrity(
337 Arc::clone(&idempotency_root),
338 idempotency_integrity.clone(),
339 default_sql_idempotency_ttl(),
340 default_sql_idempotency_max_entries(),
341 ));
342 let server_control = ServerControl {
343 query_registry: Arc::clone(&query_registry),
344 sessions: Arc::clone(&sessions),
345 accepting_sql: Arc::clone(&accepting_sql),
346 cancel_grace: sql_cancel_grace,
347 metrics: Arc::clone(&metrics),
348 };
349 let state = Arc::new(AppState {
350 idem: kit::IdempotencyStore::new_with_integrity(
351 idempotency_root,
352 idempotency_integrity,
353 default_sql_idempotency_ttl(),
354 default_sql_idempotency_max_entries(),
355 ),
356 db,
357 external_modules: external_modules.into_iter().collect(),
358 auth_token,
359 user_auth,
360 metrics,
361 slow_query_threshold: metrics::slow_query_threshold(),
362 audit: Arc::new(audit::AuditLog::new(8192)),
363 sessions,
364 ai_semaphore: Arc::new(tokio::sync::Semaphore::new(default_ai_max_concurrent())),
365 query_registry,
366 query_lifecycle: Mutex::new(()),
367 pre_cancellations: pre_cancel::PreCancelStore::new(
368 default_sql_pre_cancel_ttl(),
369 default_sql_pre_cancel_max_entries(),
370 default_sql_pre_cancel_max_bytes(),
371 default_sql_pre_cancel_max_entries_per_owner(),
372 default_sql_pre_cancel_rate_window(),
373 default_sql_pre_cancel_rate_per_owner(),
374 ),
375 sql_idempotency,
376 sql_pages: sql_pages::SqlPageStore::new(
377 default_sql_page_ttl(),
378 default_sql_page_max_entries(),
379 default_sql_page_max_bytes(),
380 default_sql_page_max_entries_per_owner(),
381 ),
382 sql_semaphore: Arc::new(tokio::sync::Semaphore::new(default_sql_max_concurrent())),
383 sql_page_semaphore: Arc::new(tokio::sync::Semaphore::new(
384 default_sql_page_max_concurrent(),
385 )),
386 sql_page_default_timeout: default_sql_page_default_timeout()
387 .min(default_sql_page_max_timeout()),
388 sql_page_max_timeout: default_sql_page_max_timeout(),
389 sql_default_timeout,
390 sql_max_timeout,
391 sql_cancel_grace,
392 sql_max_output_bytes: default_sql_max_output_bytes(),
393 sql_max_output_rows: default_sql_max_output_rows(),
394 accepting_sql,
395 cursor_mac_key: CursorMacKey::default(),
396 });
397 let router = axum::Router::new()
398 .route("/health", get(health))
399 .route("/build-info", get(build_info))
400 .route("/capabilities", get(capabilities))
401 .route(
402 "/history/retention",
403 get(history_retention).put(set_history_retention),
404 )
405 .route("/metrics", get(metrics_handler))
406 .route("/audit", get(audit_handler))
407 .route("/tables", get(list_tables).post(create_table))
408 .route("/tables/{name}", axum::routing::delete(drop_table))
409 .route("/tables/{name}/put", post(put_row))
410 .route("/tables/{name}/count", get(count))
411 .route("/tables/{name}/commit", post(commit))
412 .route("/sql", post(sql))
413 .route("/sql/continue", post(continue_sql_page))
414 .route("/queries/{query_id}", get(query_status))
415 .route("/queries/{query_id}/cancel", post(cancel_query))
416 .route("/txn", post(txn))
417 .route("/sessions", post(create_session))
418 .route("/sessions/{id}", axum::routing::delete(close_session))
419 .route("/sessions/{id}/prepare", post(prepare_statement))
420 .route("/sessions/{id}/execute", post(execute_statement))
421 .route(
422 "/sessions/{id}/statements/{name}",
423 axum::routing::delete(deallocate_statement),
424 )
425 .route("/procedures", get(procedure::list).post(procedure::create))
426 .route(
427 "/procedures/{name}",
428 get(procedure::describe)
429 .put(procedure::replace)
430 .delete(procedure::drop_procedure),
431 )
432 .route("/procedures/{name}/call", post(procedure::call))
433 .route("/triggers", get(trigger::list).post(trigger::create))
434 .route(
435 "/triggers/{name}",
436 get(trigger::describe)
437 .put(trigger::replace)
438 .delete(trigger::drop_trigger),
439 )
440 .route("/kit/schema", get(kit::schema_all))
442 .route("/kit/schema/{table}", get(kit::schema_one))
443 .route("/kit/txn", post(kit::kit_txn))
444 .route("/kit/query", post(kit::kit_query))
445 .route("/kit/retrieve", post(kit::kit_retrieve))
446 .route("/kit/ann_rerank", post(kit::kit_ann_rerank))
447 .route("/kit/ai/metrics", get(kit::kit_ai_metrics))
448 .route("/kit/set_similarity", post(kit::kit_set_similarity))
449 .route("/kit/search", post(kit::kit_search))
450 .route("/kit/create_table", post(kit::kit_create_table))
451 .route("/kit/procedures/{name}/call", post(procedure::kit_call))
452 .route("/compact", post(compact_all))
453 .route("/tables/{name}/compact", post(compact_table))
454 .route("/wal/stream", get(wal_stream))
455 .route("/replication/snapshot", get(replication_snapshot))
456 .route("/events", get(events_stream))
457 .with_state(state.clone());
458
459 let router = if state.auth_token.is_some() || state.user_auth || state.db.require_auth_enabled()
463 {
464 router.layer(axum::middleware::from_fn_with_state(
465 state.clone(),
466 auth_middleware,
467 ))
468 } else {
469 router
470 };
471
472 let router = if let Some(max) = max_connections {
474 router.layer(tower::limit::ConcurrencyLimitLayer::new(max))
475 } else {
476 router
477 };
478 (router, server_control)
479}
480
481async fn auth_middleware(
488 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
489 mut req: axum::extract::Request,
490 next: axum::middleware::Next,
491) -> Result<axum::response::Response, axum::http::StatusCode> {
492 let header = req
493 .headers()
494 .get("authorization")
495 .and_then(|v| v.to_str().ok())
496 .unwrap_or("");
497
498 let mut attempted = String::new();
502 let mut fail_reason = "no credentials provided".to_string();
503
504 if let Some(token) = &state.auth_token {
506 if let Some(provided) = header.strip_prefix("Bearer ") {
507 attempted = "token".to_string();
508 if provided == token {
509 state
510 .audit
511 .record("token", "login.ok", "bearer token accepted");
512 return Ok(next.run(req).await);
513 }
514 fail_reason = "invalid bearer token".to_string();
515 }
516 }
517
518 if state.user_auth {
520 if let Some(encoded) = header.strip_prefix("Basic ") {
521 if let Ok(decoded) = base64_decode(encoded) {
522 let decoded = Zeroizing::new(decoded);
523 if let Ok(creds) = std::str::from_utf8(&decoded) {
524 if let Some((username, password)) = creds.split_once(':') {
525 attempted = username.to_string();
526 if let Ok(Some(principal)) =
527 state.db.authenticate_principal(username, password)
528 {
529 let username = principal.username.clone();
530 drop(decoded);
531 state
532 .audit
533 .record(&username, "login.ok", "basic credentials accepted");
534 req.extensions_mut().insert(principal);
535 return Ok(next.run(req).await);
536 }
537 fail_reason = "invalid basic credentials".to_string();
538 } else {
539 fail_reason = "malformed basic credentials (no ':')".to_string();
540 }
541 } else {
542 fail_reason = "malformed basic credentials (non-utf8)".to_string();
543 }
544 } else {
545 fail_reason = "malformed basic credentials (bad base64)".to_string();
546 }
547 }
548 }
549
550 let who = if attempted.is_empty() {
551 "anonymous"
552 } else {
553 attempted.as_str()
554 };
555 state.audit.record(who, "login.fail", fail_reason);
556 Err(axum::http::StatusCode::UNAUTHORIZED)
557}
558
559fn base64_decode(input: &str) -> Result<Vec<u8>, ()> {
561 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
562 let input: Vec<u8> = input
563 .bytes()
564 .filter(|&b| b != b'\n' && b != b'\r' && b != b' ')
565 .collect();
566 let mut out = Vec::with_capacity(input.len() * 3 / 4);
567 let mut buf = 0u32;
568 let mut bits = 0u32;
569 for &b in &input {
570 if b == b'=' {
571 break;
572 }
573 let val = TABLE.iter().position(|&t| t == b).ok_or(())? as u32;
574 buf = (buf << 6) | val;
575 bits += 6;
576 if bits >= 8 {
577 bits -= 8;
578 out.push((buf >> bits) as u8);
579 }
580 }
581 Ok(out)
582}
583
584async fn wal_stream(
591 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
592 OptionalPrincipal(principal): OptionalPrincipal,
593 axum::extract::Query(params): axum::extract::Query<WalStreamParams>,
594) -> Result<Response, StatusCode> {
595 state
596 .db
597 .require_for(
598 request_principal(&state, &principal).as_ref(),
599 &mongreldb_core::Permission::Admin,
600 )
601 .map_err(|error| status_for_error(&error))?;
602 let since = params.since.unwrap_or(0);
603 let db = Arc::clone(&state.db);
604 let batch = tokio::task::spawn_blocking(move || db.replication_batch_since(since))
605 .await
606 .map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
607 let batch = batch.map_err(|e| {
608 eprintln!("wal_stream error: {e}");
609 StatusCode::INTERNAL_SERVER_ERROR
610 })?;
611 if batch.requires_snapshot {
612 let mut response = (
613 StatusCode::CONFLICT,
614 "replication snapshot required: WAL retention gap or spilled run",
615 )
616 .into_response();
617 response.headers_mut().insert(
618 "x-mongreldb-replication-status",
619 "snapshot-required"
620 .parse()
621 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
622 );
623 set_replication_headers(
624 &mut response,
625 &batch.source_id,
626 batch.from_epoch,
627 batch.current_epoch,
628 batch.earliest_epoch,
629 batch.commit_count,
630 &batch.records_sha256,
631 )?;
632 return Ok(response);
633 }
634 let mut body = String::new();
635 for record in &batch.records {
636 let json = serde_json::to_string(record).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
637 body.push_str(&json);
638 body.push('\n');
639 }
640 let mut response = (
641 [
642 (header::CONTENT_TYPE, "application/x-ndjson".to_string()),
643 (header::CACHE_CONTROL, "no-cache".to_string()),
644 ],
645 body,
646 )
647 .into_response();
648 set_replication_headers(
649 &mut response,
650 &batch.source_id,
651 batch.from_epoch,
652 batch.current_epoch,
653 batch.earliest_epoch,
654 batch.commit_count,
655 &batch.records_sha256,
656 )?;
657 Ok(response)
658}
659
660async fn replication_snapshot(
661 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
662 OptionalPrincipal(principal): OptionalPrincipal,
663) -> Response {
664 if let Err(error) = state.db.require_for(
665 request_principal(&state, &principal).as_ref(),
666 &mongreldb_core::Permission::Admin,
667 ) {
668 return (status_for_error(&error), error.to_string()).into_response();
669 }
670 let db = Arc::clone(&state.db);
671 let snapshot = match tokio::task::spawn_blocking(move || db.replication_snapshot()).await {
672 Ok(Ok(snapshot)) => snapshot,
673 Ok(Err(error)) => {
674 return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
675 }
676 Err(error) => {
677 return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response()
678 }
679 };
680 let epoch = snapshot.epoch();
681 match snapshot.encode() {
684 Ok(bytes) => {
685 let mut response =
686 ([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response();
687 let Ok(value) = epoch.to_string().parse() else {
688 return (
689 StatusCode::INTERNAL_SERVER_ERROR,
690 "invalid replication epoch response header",
691 )
692 .into_response();
693 };
694 response
695 .headers_mut()
696 .insert("x-mongreldb-current-epoch", value);
697 let source_id = snapshot
698 .source_id()
699 .iter()
700 .map(|byte| format!("{byte:02x}"))
701 .collect::<String>();
702 let Ok(source_id) = source_id.parse() else {
703 return (
704 StatusCode::INTERNAL_SERVER_ERROR,
705 "invalid replication source response header",
706 )
707 .into_response();
708 };
709 response
710 .headers_mut()
711 .insert("x-mongreldb-source-id", source_id);
712 response
713 }
714 Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
715 }
716}
717
718fn set_replication_headers(
719 response: &mut Response,
720 source_id: &[u8; 32],
721 from: u64,
722 current: u64,
723 earliest: Option<u64>,
724 commit_count: u64,
725 records_sha256: &[u8; 32],
726) -> Result<(), StatusCode> {
727 let source_id = source_id
728 .iter()
729 .map(|byte| format!("{byte:02x}"))
730 .collect::<String>();
731 response.headers_mut().insert(
732 "x-mongreldb-source-id",
733 source_id
734 .parse()
735 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
736 );
737 response.headers_mut().insert(
738 "x-mongreldb-from-epoch",
739 from.to_string()
740 .parse()
741 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
742 );
743 response.headers_mut().insert(
744 "x-mongreldb-current-epoch",
745 current
746 .to_string()
747 .parse()
748 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
749 );
750 response.headers_mut().insert(
751 "x-mongreldb-commit-count",
752 commit_count
753 .to_string()
754 .parse()
755 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
756 );
757 let digest = records_sha256
758 .iter()
759 .map(|byte| format!("{byte:02x}"))
760 .collect::<String>();
761 response.headers_mut().insert(
762 "x-mongreldb-records-sha256",
763 digest
764 .parse()
765 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
766 );
767 if let Some(earliest) = earliest {
768 response.headers_mut().insert(
769 "x-mongreldb-earliest-epoch",
770 earliest
771 .to_string()
772 .parse()
773 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
774 );
775 }
776 Ok(())
777}
778
779#[derive(serde::Deserialize)]
780struct WalStreamParams {
781 since: Option<u64>,
782}
783
784async fn events_stream(
789 axum::extract::State(state): axum::extract::State<Arc<AppState>>,
790 OptionalPrincipal(principal): OptionalPrincipal,
791 headers: axum::http::HeaderMap,
792) -> Result<Response, StatusCode> {
793 use axum::response::sse::{Event, KeepAlive, Sse};
794 use futures::Stream;
795 use std::collections::VecDeque;
796 use std::convert::Infallible;
797
798 state
799 .db
800 .require_for(
801 request_principal(&state, &principal).as_ref(),
802 &mongreldb_core::Permission::Admin,
803 )
804 .map_err(|error| status_for_error(&error))?;
805
806 struct State {
807 db: Arc<Database>,
808 receiver: tokio::sync::broadcast::Receiver<mongreldb_core::ChangeEvent>,
809 change_wake: tokio::sync::broadcast::Receiver<()>,
810 interval: tokio::time::Interval,
811 pending: VecDeque<mongreldb_core::ChangeEvent>,
812 last_id: Option<String>,
813 poll_now: bool,
814 done: bool,
815 }
816
817 fn event(change: mongreldb_core::ChangeEvent) -> Event {
818 let id = change.id.clone();
819 let kind = if change.op == "notify" {
820 "notify"
821 } else {
822 "change"
823 };
824 let mut event = Event::default().event(kind).data(
825 serde_json::to_string(&change)
826 .unwrap_or_else(|error| format!(r#"{{"error":"{error}"}}"#)),
827 );
828 if let Some(id) = id {
829 event = event.id(id);
830 }
831 event
832 }
833
834 let last_id = match headers.get("last-event-id") {
835 Some(value) => Some(
836 value
837 .to_str()
838 .map_err(|_| StatusCode::BAD_REQUEST)?
839 .to_owned(),
840 ),
841 None => None,
842 };
843 let receiver = state.db.subscribe_changes();
844 let change_wake = state.db.subscribe_change_commits();
845 let db = Arc::clone(&state.db);
846 let resume = last_id.clone();
847 let initial = tokio::task::spawn_blocking(move || db.change_events_since(resume.as_deref()))
848 .await
849 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
850 .map_err(|error| match error {
851 mongreldb_core::MongrelError::InvalidArgument(_) => StatusCode::BAD_REQUEST,
852 _ => StatusCode::INTERNAL_SERVER_ERROR,
853 })?;
854 if initial.gap {
855 return Ok((
856 StatusCode::CONFLICT,
857 Json(json!({
858 "error": "cdc retention gap",
859 "earliest_epoch": initial.earliest_epoch,
860 "current_epoch": initial.current_epoch,
861 })),
862 )
863 .into_response());
864 }
865
866 let stream: std::pin::Pin<Box<dyn Stream<Item = Result<Event, Infallible>> + Send>> = Box::pin(
867 futures::stream::unfold(
868 State {
869 db: Arc::clone(&state.db),
870 receiver,
871 change_wake,
872 interval: tokio::time::interval(std::time::Duration::from_millis(250)),
873 pending: initial.events.into(),
874 last_id,
875 poll_now: false,
876 done: false,
877 },
878 |mut stream| async move {
879 if stream.done {
880 return None;
881 }
882 loop {
883 if stream.poll_now {
884 stream.poll_now = false;
885 let db = Arc::clone(&stream.db);
886 let last_id = stream.last_id.clone();
887 match tokio::task::spawn_blocking(move || {
888 db.change_events_since(last_id.as_deref())
889 })
890 .await
891 {
892 Ok(Ok(batch)) if batch.gap => {
893 stream.done = true;
894 let gap = Event::default().event("gap").data(
895 json!({
896 "error": "cdc retention gap",
897 "earliest_epoch": batch.earliest_epoch,
898 "current_epoch": batch.current_epoch,
899 })
900 .to_string(),
901 );
902 return Some((Ok(gap), stream));
903 }
904 Ok(Ok(batch)) => stream.pending.extend(batch.events),
905 Ok(Err(error)) => {
906 stream.done = true;
907 return Some((
908 Ok(Event::default().event("error").data(error.to_string())),
909 stream,
910 ));
911 }
912 Err(error) => {
913 stream.done = true;
914 return Some((
915 Ok(Event::default().event("error").data(error.to_string())),
916 stream,
917 ));
918 }
919 }
920 }
921 if let Some(change) = stream.pending.pop_front() {
922 if let Some(id) = &change.id {
923 stream.last_id = Some(id.clone());
924 }
925 return Some((Ok(event(change)), stream));
926 }
927 tokio::select! {
928 received = stream.receiver.recv() => {
929 match received {
930 Ok(change) => return Some((Ok(event(change)), stream)),
931 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {},
932 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
933 return None;
934 }
935 }
936 }
937 received = stream.change_wake.recv() => {
938 match received {
939 Ok(()) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
940 stream.poll_now = true;
941 }
942 Err(tokio::sync::broadcast::error::RecvError::Closed) => {}
943 }
944 }
945 _ = stream.interval.tick() => {
946 stream.poll_now = true;
947 }
948 }
949 }
950 },
951 ),
952 );
953
954 Ok(Sse::new(stream)
955 .keep_alive(
956 KeepAlive::new()
957 .interval(std::time::Duration::from_secs(15))
958 .text("keep-alive"),
959 )
960 .into_response())
961}
962
963pub fn spawn_auto_compactor(db: Arc<Database>) {
968 if let Err(error) = std::thread::Builder::new()
969 .name("mongreldb-auto-compact".into())
970 .spawn(move || loop {
971 std::thread::sleep(std::time::Duration::from_secs(30));
972 for name in db.table_names() {
973 let Ok(handle) = db.table(&name) else {
974 continue;
975 };
976 let mut t = handle.lock();
977 let before = t.run_count();
978 match t.maybe_compact() {
979 Ok(true) => {
980 eprintln!(
981 "[auto-compact] {name}: {} runs -> {}",
982 before,
983 t.run_count()
984 );
985 }
986 Ok(false) => {}
987 Err(e) => {
988 eprintln!("[auto-compact] {name}: compaction failed: {e}");
989 }
990 }
991 }
992 })
993 {
994 eprintln!("[auto-compact] failed to start background thread: {error}");
995 }
996}
997
998async fn health() -> StatusCode {
999 StatusCode::OK
1000}
1001
1002#[derive(Debug, Serialize)]
1003struct SqlCancellationCapabilities {
1004 version: u8,
1005 client_query_ids: bool,
1006 cancel_endpoint: bool,
1007 query_status: bool,
1008 pre_registration_cancel: bool,
1009 stream_disconnect_cancels: bool,
1010}
1011
1012#[derive(Debug, Serialize)]
1013struct SqlIdempotencyCapabilities {
1014 version: u8,
1015 durable_pre_execution_intent: bool,
1016 replay_committed_receipt: bool,
1017 indeterminate_never_reexecutes: bool,
1018}
1019
1020#[derive(Debug, Serialize)]
1021struct SqlPaginationCapabilities {
1022 version: u8,
1023 continuation_endpoint: &'static str,
1024 retained_snapshot: bool,
1025 projection_required: bool,
1026 byte_and_token_hints: bool,
1027}
1028
1029#[derive(Debug, Serialize)]
1030struct CapabilitiesResponse {
1031 sql_cancellation: SqlCancellationCapabilities,
1032 sql_idempotency: SqlIdempotencyCapabilities,
1033 sql_pagination: SqlPaginationCapabilities,
1034}
1035
1036async fn capabilities() -> Json<CapabilitiesResponse> {
1037 Json(CapabilitiesResponse {
1038 sql_cancellation: SqlCancellationCapabilities {
1039 version: 2,
1040 client_query_ids: true,
1041 cancel_endpoint: true,
1042 query_status: true,
1043 pre_registration_cancel: true,
1044 stream_disconnect_cancels: true,
1045 },
1046 sql_idempotency: SqlIdempotencyCapabilities {
1047 version: 1,
1048 durable_pre_execution_intent: true,
1049 replay_committed_receipt: true,
1050 indeterminate_never_reexecutes: true,
1051 },
1052 sql_pagination: SqlPaginationCapabilities {
1053 version: 1,
1054 continuation_endpoint: "/sql/continue",
1055 retained_snapshot: true,
1056 projection_required: true,
1057 byte_and_token_hints: true,
1058 },
1059 })
1060}
1061
1062async fn build_info() -> Json<mongreldb_query::BuildInfo> {
1063 Json(mongreldb_query::build_info())
1064}
1065
1066#[derive(Debug, Deserialize)]
1067struct HistoryRetentionRequest {
1068 #[serde(default)]
1069 history_retention_epochs: serde_json::Value,
1070}
1071
1072#[derive(Debug, Serialize)]
1073struct HistoryRetentionResponse {
1074 history_retention_epochs: u64,
1075 earliest_retained_epoch: u64,
1076}
1077
1078fn history_retention_response(db: &Database) -> HistoryRetentionResponse {
1079 HistoryRetentionResponse {
1080 history_retention_epochs: db.history_retention_epochs(),
1081 earliest_retained_epoch: db.earliest_retained_epoch().0,
1082 }
1083}
1084
1085async fn history_retention(
1087 State(state): State<Arc<AppState>>,
1088 OptionalPrincipal(principal): OptionalPrincipal,
1089) -> Response {
1090 if let Err(error) = state.db.require_for(
1091 request_principal(&state, &principal).as_ref(),
1092 &mongreldb_core::Permission::Admin,
1093 ) {
1094 return (status_for_error(&error), error.to_string()).into_response();
1095 }
1096 Json(history_retention_response(&state.db)).into_response()
1097}
1098
1099async fn set_history_retention(
1101 State(state): State<Arc<AppState>>,
1102 OptionalPrincipal(principal): OptionalPrincipal,
1103 Json(request): Json<HistoryRetentionRequest>,
1104) -> Response {
1105 if let Err(error) = state.db.require_for(
1106 request_principal(&state, &principal).as_ref(),
1107 &mongreldb_core::Permission::Admin,
1108 ) {
1109 return (status_for_error(&error), error.to_string()).into_response();
1110 }
1111 let Some(epochs) = request.history_retention_epochs.as_u64() else {
1112 return (
1113 StatusCode::BAD_REQUEST,
1114 Json(json!({"error": "history_retention_epochs must be a u64"})),
1115 )
1116 .into_response();
1117 };
1118 match state.db.set_history_retention_epochs(epochs) {
1119 Ok(()) => Json(history_retention_response(&state.db)).into_response(),
1120 Err(error) => (status_for_error(&error), error.to_string()).into_response(),
1121 }
1122}
1123
1124async fn audit_handler(
1129 State(state): State<Arc<AppState>>,
1130 OptionalPrincipal(principal): OptionalPrincipal,
1131) -> Response {
1132 if let Err(error) = state.db.require_for(
1133 request_principal(&state, &principal).as_ref(),
1134 &mongreldb_core::Permission::Admin,
1135 ) {
1136 return (status_for_error(&error), error.to_string()).into_response();
1137 }
1138 let recent = state.audit.recent();
1139 Json(recent).into_response()
1140}
1141
1142fn default_max_sessions() -> usize {
1144 std::env::var("MONGRELBL_MAX_SESSIONS")
1145 .ok()
1146 .and_then(|v| v.parse().ok())
1147 .unwrap_or(256)
1148}
1149
1150fn default_session_idle_timeout() -> std::time::Duration {
1152 std::time::Duration::from_secs(
1153 std::env::var("MONGRELBL_SESSION_IDLE_TIMEOUT_SECS")
1154 .ok()
1155 .and_then(|v| v.parse().ok())
1156 .unwrap_or(300),
1157 )
1158}
1159
1160fn default_replication_wal_segments() -> usize {
1161 let replication = std::env::var("MONGRELDB_REPLICATION_WAL_SEGMENTS")
1162 .ok()
1163 .and_then(|value| value.parse().ok())
1164 .unwrap_or(16);
1165 let cdc = std::env::var("MONGRELDB_CDC_WAL_SEGMENTS")
1166 .ok()
1167 .and_then(|value| value.parse().ok())
1168 .unwrap_or(16);
1169 replication.max(cdc)
1170}
1171
1172fn default_history_retention_epochs() -> u64 {
1173 std::env::var("MONGRELDB_HISTORY_RETENTION_EPOCHS")
1174 .ok()
1175 .and_then(|value| value.parse().ok())
1176 .unwrap_or(1024)
1177}
1178
1179fn default_ai_max_concurrent() -> usize {
1180 std::env::var("MONGRELDB_AI_MAX_CONCURRENT")
1181 .ok()
1182 .and_then(|value| value.parse().ok())
1183 .filter(|value| *value > 0)
1184 .unwrap_or(4)
1185}
1186
1187fn positive_env_u64(name: &str, default: u64) -> u64 {
1188 std::env::var(name)
1189 .ok()
1190 .and_then(|value| value.parse().ok())
1191 .filter(|value| *value > 0)
1192 .unwrap_or(default)
1193}
1194
1195fn positive_env_usize(name: &str, default: usize) -> usize {
1196 std::env::var(name)
1197 .ok()
1198 .and_then(|value| value.parse().ok())
1199 .filter(|value| *value > 0)
1200 .unwrap_or(default)
1201}
1202
1203fn default_sql_default_timeout() -> std::time::Duration {
1204 std::time::Duration::from_millis(positive_env_u64("MONGRELDB_SQL_DEFAULT_TIMEOUT_MS", 30_000))
1205}
1206
1207fn default_sql_max_timeout() -> std::time::Duration {
1208 std::time::Duration::from_millis(positive_env_u64("MONGRELDB_SQL_MAX_TIMEOUT_MS", 300_000))
1209}
1210
1211fn default_sql_max_concurrent() -> usize {
1212 positive_env_usize(
1213 "MONGRELDB_SQL_MAX_CONCURRENT",
1214 std::thread::available_parallelism()
1215 .map(usize::from)
1216 .unwrap_or(4),
1217 )
1218}
1219
1220fn default_sql_max_active_queries() -> usize {
1221 positive_env_usize("MONGRELDB_SQL_MAX_ACTIVE_QUERIES", 1_024)
1222}
1223
1224fn default_sql_page_max_concurrent() -> usize {
1225 positive_env_usize("MONGRELDB_SQL_PAGE_MAX_CONCURRENT", 16)
1226}
1227
1228fn default_sql_page_default_timeout() -> std::time::Duration {
1229 std::time::Duration::from_millis(positive_env_u64(
1230 "MONGRELDB_SQL_PAGE_DEFAULT_TIMEOUT_MS",
1231 5_000,
1232 ))
1233}
1234
1235fn default_sql_page_max_timeout() -> std::time::Duration {
1236 std::time::Duration::from_millis(positive_env_u64(
1237 "MONGRELDB_SQL_PAGE_MAX_TIMEOUT_MS",
1238 30_000,
1239 ))
1240}
1241
1242fn default_sql_finished_query_ttl() -> std::time::Duration {
1243 std::time::Duration::from_secs(positive_env_u64(
1244 "MONGRELDB_SQL_FINISHED_QUERY_TTL_SECS",
1245 60,
1246 ))
1247}
1248
1249fn default_sql_finished_detail_max_entries() -> usize {
1250 positive_env_usize("MONGRELDB_SQL_FINISHED_DETAIL_MAX_ENTRIES", 2_048)
1251}
1252
1253fn default_sql_finished_detail_max_bytes() -> usize {
1254 positive_env_usize("MONGRELDB_SQL_FINISHED_DETAIL_MAX_BYTES", 8 * 1024 * 1024)
1255}
1256
1257fn default_sql_finished_compact_max_entries() -> usize {
1258 positive_env_usize("MONGRELDB_SQL_FINISHED_COMPACT_MAX_ENTRIES", 100_000)
1259}
1260
1261fn default_sql_finished_compact_max_bytes() -> usize {
1262 positive_env_usize("MONGRELDB_SQL_FINISHED_COMPACT_MAX_BYTES", 32 * 1024 * 1024)
1263}
1264
1265fn default_sql_pre_cancel_ttl() -> std::time::Duration {
1266 std::time::Duration::from_millis(positive_env_u64("MONGRELDB_SQL_PRE_CANCEL_TTL_MS", 15_000))
1267}
1268
1269fn default_sql_pre_cancel_max_entries() -> usize {
1270 positive_env_usize("MONGRELDB_SQL_PRE_CANCEL_MAX_ENTRIES", 2_048)
1271}
1272
1273fn default_sql_pre_cancel_max_bytes() -> usize {
1274 positive_env_usize("MONGRELDB_SQL_PRE_CANCEL_MAX_BYTES", 1024 * 1024)
1275}
1276
1277fn default_sql_pre_cancel_max_entries_per_owner() -> usize {
1278 positive_env_usize("MONGRELDB_SQL_PRE_CANCEL_MAX_PER_OWNER", 256)
1279}
1280
1281fn default_sql_pre_cancel_rate_window() -> std::time::Duration {
1282 std::time::Duration::from_millis(positive_env_u64(
1283 "MONGRELDB_SQL_PRE_CANCEL_RATE_WINDOW_MS",
1284 1_000,
1285 ))
1286}
1287
1288fn default_sql_pre_cancel_rate_per_owner() -> usize {
1289 positive_env_usize("MONGRELDB_SQL_PRE_CANCEL_RATE_PER_OWNER", 256)
1290}
1291
1292pub(crate) fn default_sql_idempotency_ttl() -> std::time::Duration {
1293 std::time::Duration::from_secs(positive_env_u64(
1294 "MONGRELDB_SQL_IDEMPOTENCY_TTL_SECS",
1295 86_400,
1296 ))
1297}
1298
1299pub(crate) fn default_sql_idempotency_max_entries() -> usize {
1300 positive_env_usize("MONGRELDB_SQL_IDEMPOTENCY_MAX_ENTRIES", 4_096)
1301}
1302
1303fn default_sql_page_ttl() -> std::time::Duration {
1304 std::time::Duration::from_secs(positive_env_u64("MONGRELDB_SQL_PAGE_TTL_SECS", 60))
1305}
1306
1307fn default_sql_page_max_entries() -> usize {
1308 positive_env_usize("MONGRELDB_SQL_PAGE_MAX_ENTRIES", 128)
1309}
1310
1311fn default_sql_page_max_bytes() -> usize {
1312 positive_env_usize("MONGRELDB_SQL_PAGE_MAX_RETAINED_BYTES", 128 * 1024 * 1024)
1313}
1314
1315fn default_sql_page_max_entries_per_owner() -> usize {
1316 positive_env_usize("MONGRELDB_SQL_PAGE_MAX_PER_OWNER", 16)
1317}
1318
1319fn default_sql_cancel_grace() -> std::time::Duration {
1320 std::time::Duration::from_millis(positive_env_u64("MONGRELDB_SQL_CANCEL_GRACE_MS", 1_000))
1321}
1322
1323fn default_sql_max_output_bytes() -> usize {
1324 positive_env_usize("MONGRELDB_SQL_MAX_OUTPUT_BYTES", 64 * 1024 * 1024)
1325}
1326
1327fn default_sql_max_output_rows() -> usize {
1328 positive_env_usize("MONGRELDB_SQL_MAX_OUTPUT_ROWS", 1_000_000)
1329}
1330
1331fn request_owner(state: &AppState, principal: &Option<mongreldb_core::Principal>) -> String {
1334 if let Some(p) = principal {
1335 return format!("user:{}:{}", p.user_id, p.created_epoch);
1336 }
1337 if let Some(token) = state.auth_token.as_deref() {
1338 let mut digest = sha2::Sha256::new();
1339 digest.update(b"mongreldb-server-bearer-owner-v1\0");
1340 digest.update((token.len() as u64).to_le_bytes());
1341 digest.update(token.as_bytes());
1342 return format!("bearer:{}", sql_idempotency::hex(&digest.finalize()));
1343 }
1344 if state.user_auth || state.db.require_auth_enabled() {
1345 return "unauthenticated".into();
1346 }
1347 "anonymous".into()
1348}
1349
1350fn request_principal(
1351 state: &AppState,
1352 principal: &Option<mongreldb_core::Principal>,
1353) -> Option<mongreldb_core::Principal> {
1354 if let Some(principal) = principal {
1355 return state
1356 .db
1357 .resolve_current_principal(principal)
1358 .or_else(|| Some(principal.clone()));
1359 }
1360 state.auth_token.as_ref().and_then(|_| {
1361 if state.db.require_auth_enabled() {
1362 return state
1363 .db
1364 .principal_snapshot()
1365 .and_then(|principal| state.db.resolve_current_principal(&principal))
1366 .filter(|principal| principal.is_admin);
1367 }
1368 Some(mongreldb_core::Principal {
1369 user_id: 0,
1370 created_epoch: 0,
1371 username: "token".into(),
1372 is_admin: true,
1373 roles: Vec::new(),
1374 permissions: Vec::new(),
1375 })
1376 })
1377}
1378
1379fn current_request_principal(
1380 state: &AppState,
1381 principal: &Option<mongreldb_core::Principal>,
1382) -> Option<mongreldb_core::Principal> {
1383 if let Some(principal) = principal {
1384 return state.db.resolve_current_principal(principal);
1385 }
1386 request_principal(state, principal)
1387}
1388
1389fn request_identity_is_current(
1390 state: &AppState,
1391 principal: &Option<mongreldb_core::Principal>,
1392) -> bool {
1393 if principal.is_some() || state.auth_token.is_some() {
1394 return current_request_principal(state, principal).is_some();
1395 }
1396 !state.user_auth && !state.db.require_auth_enabled()
1397}
1398
1399async fn create_session(
1404 State(state): State<Arc<AppState>>,
1405 OptionalPrincipal(principal): OptionalPrincipal,
1406) -> Response {
1407 if !state.accepting_sql.load(Ordering::Acquire) {
1408 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
1409 }
1410 if !request_identity_is_current(&state, &principal) {
1411 return StatusCode::UNAUTHORIZED.into_response();
1412 }
1413 let owner = request_owner(&state, &principal);
1414 let session = match MongrelSession::open_with_external_modules_as(
1415 Arc::clone(&state.db),
1416 state.external_modules.iter().cloned(),
1417 request_principal(&state, &principal),
1418 ) {
1419 Ok(session) => session.with_query_registry(Arc::clone(&state.query_registry)),
1420 Err(e) => return (status_for_query_error(&e), e.to_string()).into_response(),
1421 };
1422 match state.sessions.create(session, owner.clone()) {
1423 Some(token) => {
1424 state.audit.record(owner, "session.open", "session created");
1425 Json(json!({ "session_id": token })).into_response()
1426 }
1427 None => (
1428 StatusCode::SERVICE_UNAVAILABLE,
1429 "session limit reached; close an idle session or raise --max-sessions",
1430 )
1431 .into_response(),
1432 }
1433}
1434
1435async fn close_session(
1438 State(state): State<Arc<AppState>>,
1439 OptionalPrincipal(principal): OptionalPrincipal,
1440 Path(id): Path<String>,
1441) -> Response {
1442 if !request_identity_is_current(&state, &principal) {
1443 return StatusCode::NOT_FOUND.into_response();
1444 }
1445 let owner = request_owner(&state, &principal);
1446 if let Some(entry) = state.sessions.take_for_close(&id, &owner) {
1447 entry
1448 .session
1449 .query_registry()
1450 .cancel_session(&id, CancellationReason::SessionClosed);
1451 let deadline = tokio::time::Instant::now() + state.sql_cancel_grace;
1452 while entry.session.query_registry().active_for_session(&id) > 0
1453 && tokio::time::Instant::now() < deadline
1454 {
1455 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1456 }
1457 state.audit.record(owner, "session.close", "session closed");
1458 StatusCode::OK.into_response()
1459 } else {
1460 (
1461 StatusCode::NOT_FOUND,
1462 "session not found or not owned by caller",
1463 )
1464 .into_response()
1465 }
1466}
1467
1468async fn dispatch_buffered_sql_format(
1471 state: &AppState,
1472 format: Option<&str>,
1473 output: ManagedQueryBatches,
1474 query_id: QueryId,
1475 test_hook: Option<mongreldb_query::SqlTestHook>,
1476 output_limits: (usize, usize),
1477) -> Response {
1478 let format = format.unwrap_or("json").to_owned();
1479 let (max_rows, max_bytes) = output_limits;
1480 let serialization_batches = output.batches().to_vec();
1484 let serialization_query = output.query().clone();
1485 let serialized = tokio::task::spawn_blocking(move || {
1486 serialize_buffered_output(
1487 &format,
1488 &serialization_batches,
1489 &serialization_query,
1490 max_rows,
1491 max_bytes,
1492 test_hook.as_ref(),
1493 )
1494 })
1495 .await;
1496 let result = match serialized {
1497 Ok(result) => result,
1498 Err(error) => {
1499 output
1500 .query()
1501 .record_serialization_failure("SERIALIZATION_WORKER_FAILED");
1502 output.fail();
1503 state.metrics.inc_sql_errors();
1504 return terminal_server_error_response(
1505 state,
1506 query_id,
1507 StatusCode::INTERNAL_SERVER_ERROR,
1508 "SERIALIZATION_WORKER_FAILED",
1509 error.to_string(),
1510 );
1511 }
1512 };
1513 match result {
1514 Ok(serialized) => {
1515 if let Err(error) = output.try_complete() {
1516 state.metrics.inc_sql_errors();
1517 return tracked_query_error_response(state, &error, Some(query_id));
1518 }
1519 state.metrics.add_sql_output_bytes(serialized.bytes.len());
1520 let content_type = if serialized.arrow {
1521 "application/vnd.apache.arrow.file"
1522 } else {
1523 "application/json"
1524 };
1525 with_query_id(
1526 ([(header::CONTENT_TYPE, content_type)], serialized.bytes).into_response(),
1527 query_id,
1528 )
1529 }
1530 Err(BufferedSerializationError::Query(error)) => {
1531 output.fail();
1532 state.metrics.inc_sql_errors();
1533 tracked_query_error_response(state, &error, Some(query_id))
1534 }
1535 Err(BufferedSerializationError::Limit(message)) => {
1536 output.fail_result_limit();
1537 state.metrics.inc_sql_errors();
1538 terminal_server_error_response(
1539 state,
1540 query_id,
1541 StatusCode::PAYLOAD_TOO_LARGE,
1542 "RESULT_LIMIT_EXCEEDED",
1543 message,
1544 )
1545 }
1546 Err(BufferedSerializationError::Encoding(message)) => {
1547 output.fail_serialization();
1548 state.metrics.inc_sql_errors();
1549 terminal_server_error_response(
1550 state,
1551 query_id,
1552 StatusCode::INTERNAL_SERVER_ERROR,
1553 "SERIALIZATION_FAILED",
1554 message,
1555 )
1556 }
1557 }
1558}
1559
1560#[derive(Debug)]
1561enum PaginatedSerializationError {
1562 Query(mongreldb_query::MongrelQueryError),
1563 Limit(String),
1564 Projection(String),
1565 Encoding(String),
1566}
1567
1568struct SerializedPageRows {
1569 rows: Vec<serde_json::Value>,
1570 retained_bytes: usize,
1571}
1572
1573struct PaginatedJsonReader<'a> {
1574 cursor: std::io::Cursor<&'a [u8]>,
1575 query: &'a RegisteredSqlQuery,
1576 test_hook: Option<&'a mongreldb_query::SqlTestHook>,
1577}
1578
1579impl std::io::Read for PaginatedJsonReader<'_> {
1580 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1581 if let Some(hook) = self.test_hook {
1582 hook(mongreldb_query::SqlTestHookPoint::DuringPaginationDeserialization);
1583 }
1584 self.query
1585 .checkpoint()
1586 .map_err(|error| std::io::Error::other(error.to_string()))?;
1587 let length = buffer.len().min(64 * 1024);
1589 std::io::Read::read(&mut self.cursor, &mut buffer[..length])
1590 }
1591}
1592
1593const PAGINATED_VALUE_NODE_BYTES: usize = 64;
1594const PAGINATED_OBJECT_ENTRY_BYTES: usize = 128;
1595const PAGINATED_MEMORY_LIMIT_ERROR: &str = "SQL retained output memory limit exceeded";
1596
1597struct PaginatedDecodeBudget<'a> {
1598 used: usize,
1599 limit: usize,
1600 nodes: usize,
1601 exceeded: bool,
1602 query: &'a RegisteredSqlQuery,
1603 test_hook: Option<&'a mongreldb_query::SqlTestHook>,
1604}
1605
1606impl PaginatedDecodeBudget<'_> {
1607 fn begin_value<E: serde::de::Error>(&mut self) -> Result<(), E> {
1608 self.nodes = self.nodes.saturating_add(1);
1609 if self.nodes & 255 == 0 {
1610 if let Some(hook) = self.test_hook {
1611 hook(mongreldb_query::SqlTestHookPoint::DuringPaginationDeserialization);
1612 }
1613 self.query.checkpoint().map_err(E::custom)?;
1614 }
1615 self.charge::<E>(PAGINATED_VALUE_NODE_BYTES)
1616 }
1617
1618 fn charge<E: serde::de::Error>(&mut self, bytes: usize) -> Result<(), E> {
1619 let next = self.used.saturating_add(bytes);
1620 if next > self.limit {
1621 self.exceeded = true;
1622 return Err(E::custom(PAGINATED_MEMORY_LIMIT_ERROR));
1623 }
1624 self.used = next;
1625 Ok(())
1626 }
1627}
1628
1629struct BudgetedJsonValueSeed<'a, 'query> {
1630 budget: &'a mut PaginatedDecodeBudget<'query>,
1631}
1632
1633impl<'de> serde::de::DeserializeSeed<'de> for BudgetedJsonValueSeed<'_, '_> {
1634 type Value = serde_json::Value;
1635
1636 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1637 where
1638 D: serde::Deserializer<'de>,
1639 {
1640 self.budget.begin_value::<D::Error>()?;
1641 deserializer.deserialize_any(BudgetedJsonValueVisitor {
1642 budget: self.budget,
1643 })
1644 }
1645}
1646
1647struct BudgetedJsonValueVisitor<'a, 'query> {
1648 budget: &'a mut PaginatedDecodeBudget<'query>,
1649}
1650
1651impl<'de> serde::de::Visitor<'de> for BudgetedJsonValueVisitor<'_, '_> {
1652 type Value = serde_json::Value;
1653
1654 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1655 formatter.write_str("a JSON value")
1656 }
1657
1658 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
1659 Ok(serde_json::Value::Bool(value))
1660 }
1661
1662 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
1663 Ok(serde_json::Value::Number(value.into()))
1664 }
1665
1666 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
1667 Ok(serde_json::Value::Number(value.into()))
1668 }
1669
1670 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1671 where
1672 E: serde::de::Error,
1673 {
1674 serde_json::Number::from_f64(value)
1675 .map(serde_json::Value::Number)
1676 .ok_or_else(|| E::custom("JSON number is not finite"))
1677 }
1678
1679 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1680 where
1681 E: serde::de::Error,
1682 {
1683 self.budget.charge::<E>(value.len())?;
1684 Ok(serde_json::Value::String(value.to_owned()))
1685 }
1686
1687 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
1688 where
1689 E: serde::de::Error,
1690 {
1691 self.visit_str(value)
1692 }
1693
1694 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1695 where
1696 E: serde::de::Error,
1697 {
1698 self.budget.charge::<E>(value.capacity())?;
1699 Ok(serde_json::Value::String(value))
1700 }
1701
1702 fn visit_none<E>(self) -> Result<Self::Value, E> {
1703 Ok(serde_json::Value::Null)
1704 }
1705
1706 fn visit_unit<E>(self) -> Result<Self::Value, E> {
1707 Ok(serde_json::Value::Null)
1708 }
1709
1710 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1711 where
1712 D: serde::Deserializer<'de>,
1713 {
1714 deserializer.deserialize_any(self)
1715 }
1716
1717 fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1718 where
1719 D: serde::Deserializer<'de>,
1720 {
1721 deserializer.deserialize_any(self)
1722 }
1723
1724 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1725 where
1726 A: serde::de::SeqAccess<'de>,
1727 {
1728 let mut values = Vec::new();
1729 while let Some(value) = sequence.next_element_seed(BudgetedJsonValueSeed {
1730 budget: self.budget,
1731 })? {
1732 values.push(value);
1733 }
1734 Ok(serde_json::Value::Array(values))
1735 }
1736
1737 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1738 where
1739 A: serde::de::MapAccess<'de>,
1740 {
1741 let mut values = serde_json::Map::new();
1742 while let Some(key) = map.next_key::<String>()? {
1743 self.budget
1744 .charge::<A::Error>(PAGINATED_OBJECT_ENTRY_BYTES.saturating_add(key.capacity()))?;
1745 let value = map.next_value_seed(BudgetedJsonValueSeed {
1746 budget: self.budget,
1747 })?;
1748 values.insert(key, value);
1749 }
1750 Ok(serde_json::Value::Object(values))
1751 }
1752}
1753
1754struct BudgetedJsonRowsSeed<'a, 'query> {
1755 budget: &'a mut PaginatedDecodeBudget<'query>,
1756}
1757
1758impl<'de> serde::de::DeserializeSeed<'de> for BudgetedJsonRowsSeed<'_, '_> {
1759 type Value = Vec<serde_json::Value>;
1760
1761 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1762 where
1763 D: serde::Deserializer<'de>,
1764 {
1765 deserializer.deserialize_seq(BudgetedJsonRowsVisitor {
1766 budget: self.budget,
1767 })
1768 }
1769}
1770
1771struct BudgetedJsonRowsVisitor<'a, 'query> {
1772 budget: &'a mut PaginatedDecodeBudget<'query>,
1773}
1774
1775impl<'de> serde::de::Visitor<'de> for BudgetedJsonRowsVisitor<'_, '_> {
1776 type Value = Vec<serde_json::Value>;
1777
1778 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1779 formatter.write_str("a JSON array of SQL rows")
1780 }
1781
1782 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1783 where
1784 A: serde::de::SeqAccess<'de>,
1785 {
1786 let mut rows = Vec::new();
1787 while let Some(row) = sequence.next_element_seed(BudgetedJsonValueSeed {
1788 budget: self.budget,
1789 })? {
1790 rows.push(row);
1791 }
1792 Ok(rows)
1793 }
1794}
1795
1796#[allow(clippy::too_many_arguments)]
1797async fn dispatch_paginated_sql(
1798 state: &AppState,
1799 output: ManagedQueryBatches,
1800 query_id: QueryId,
1801 owner: &str,
1802 pagination: ResolvedSqlPagination,
1803 output_limits: (usize, usize),
1804 test_hook: Option<mongreldb_query::SqlTestHook>,
1805 binding: sql_pages::SqlPageBinding,
1806) -> Response {
1807 let projection = pagination.projection;
1808 let serialization_projection = projection.clone();
1809 let serialization_batches = output.batches().to_vec();
1810 let serialization_query = output.query().clone();
1811 let response_test_hook = test_hook.clone();
1812 let retained_memory_limit = output_limits.1.min(state.sql_pages.max_retained_bytes());
1813 let serialized = tokio::task::spawn_blocking(move || {
1814 serialize_paginated_rows(
1815 &serialization_batches,
1816 &serialization_query,
1817 &serialization_projection,
1818 output_limits.0,
1819 output_limits.1,
1820 retained_memory_limit,
1821 test_hook.as_ref(),
1822 )
1823 })
1824 .await;
1825 let serialized = match serialized {
1826 Ok(result) => result,
1827 Err(error) => {
1828 output
1829 .query()
1830 .record_serialization_failure("SERIALIZATION_WORKER_FAILED");
1831 output.fail();
1832 state.metrics.inc_sql_errors();
1833 return terminal_server_error_response(
1834 state,
1835 query_id,
1836 StatusCode::INTERNAL_SERVER_ERROR,
1837 "SERIALIZATION_WORKER_FAILED",
1838 error.to_string(),
1839 );
1840 }
1841 };
1842 let serialized = match serialized {
1843 Ok(serialized) => serialized,
1844 Err(PaginatedSerializationError::Query(error)) => {
1845 output.fail();
1846 state.metrics.inc_sql_errors();
1847 return tracked_query_error_response(state, &error, Some(query_id));
1848 }
1849 Err(PaginatedSerializationError::Limit(message)) => {
1850 output.fail_result_limit();
1851 state.metrics.inc_sql_errors();
1852 return terminal_server_error_response(
1853 state,
1854 query_id,
1855 StatusCode::PAYLOAD_TOO_LARGE,
1856 "RESULT_LIMIT_EXCEEDED",
1857 message,
1858 );
1859 }
1860 Err(PaginatedSerializationError::Projection(message)) => {
1861 output.fail_with_error(
1862 "INVALID_SQL_PROJECTION",
1863 mongreldb_query::QueryTerminalErrorCategory::Execution,
1864 );
1865 state.metrics.inc_sql_errors();
1866 return terminal_server_error_response(
1867 state,
1868 query_id,
1869 StatusCode::BAD_REQUEST,
1870 "INVALID_SQL_PROJECTION",
1871 message,
1872 );
1873 }
1874 Err(PaginatedSerializationError::Encoding(message)) => {
1875 output.fail_serialization();
1876 state.metrics.inc_sql_errors();
1877 return terminal_server_error_response(
1878 state,
1879 query_id,
1880 StatusCode::INTERNAL_SERVER_ERROR,
1881 "SERIALIZATION_FAILED",
1882 message,
1883 );
1884 }
1885 };
1886 let retained_bytes = serialized.retained_bytes;
1887 let current_binding = sql_pages::SqlPageBinding {
1888 security_version: state.db.security_version(),
1889 catalog_epoch: state.db.catalog_snapshot().db_epoch,
1890 };
1891 if current_binding != binding {
1892 output.fail_with_error(
1893 "SQL_CURSOR_EXPIRED",
1894 mongreldb_query::QueryTerminalErrorCategory::Execution,
1895 );
1896 return with_query_id(
1897 sql_cursor_error_response(
1898 StatusCode::CONFLICT,
1899 "SQL_CURSOR_EXPIRED",
1900 "authorization or schema changed while retaining the SQL result",
1901 query_id,
1902 ),
1903 query_id,
1904 );
1905 }
1906 let retained = match state.sql_pages.insert(
1907 owner,
1908 serialized.rows,
1909 projection,
1910 pagination.limits,
1911 retained_bytes,
1912 binding,
1913 ) {
1914 Ok(retained) => retained,
1915 Err(sql_pages::InsertError::Full | sql_pages::InsertError::OwnerLimit) => {
1916 output.fail_with_error(
1917 "SQL_PAGE_STORE_FULL",
1918 mongreldb_query::QueryTerminalErrorCategory::Execution,
1919 );
1920 state.metrics.inc_sql_errors();
1921 return terminal_server_error_response(
1922 state,
1923 query_id,
1924 StatusCode::SERVICE_UNAVAILABLE,
1925 "SQL_PAGE_STORE_FULL",
1926 "retained SQL result capacity reached",
1927 );
1928 }
1929 Err(sql_pages::InsertError::EntropyUnavailable) => {
1930 output.fail_with_error(
1931 "ENTROPY_UNAVAILABLE",
1932 mongreldb_query::QueryTerminalErrorCategory::Execution,
1933 );
1934 state.metrics.inc_sql_errors();
1935 return terminal_server_error_response(
1936 state,
1937 query_id,
1938 StatusCode::INTERNAL_SERVER_ERROR,
1939 "ENTROPY_UNAVAILABLE",
1940 "OS CSPRNG unavailable",
1941 );
1942 }
1943 };
1944 let cursor_mac_key = match state.cursor_mac_key.get() {
1945 Ok(key) => key,
1946 Err(_) => {
1947 state.sql_pages.discard(&retained);
1948 output.fail_with_error(
1949 "ENTROPY_UNAVAILABLE",
1950 mongreldb_query::QueryTerminalErrorCategory::Execution,
1951 );
1952 state.metrics.inc_sql_errors();
1953 return terminal_server_error_response(
1954 state,
1955 query_id,
1956 StatusCode::INTERNAL_SERVER_ERROR,
1957 "ENTROPY_UNAVAILABLE",
1958 "OS CSPRNG unavailable",
1959 );
1960 }
1961 };
1962 let page = match sql_pages::SqlPageStore::first_page(&retained, &cursor_mac_key) {
1963 Ok(page) => page,
1964 Err(sql_pages::PageError::RowExceedsLimits) => {
1965 state.sql_pages.discard(&retained);
1966 output.fail_result_limit();
1967 state.metrics.inc_sql_errors();
1968 return terminal_server_error_response(
1969 state,
1970 query_id,
1971 StatusCode::PAYLOAD_TOO_LARGE,
1972 "RESULT_LIMIT_EXCEEDED",
1973 "one projected row exceeds the page byte or token limit",
1974 );
1975 }
1976 Err(sql_pages::PageError::OffsetInvalid) => {
1977 state.sql_pages.discard(&retained);
1978 output.fail_with_error(
1979 "INVALID_PAGE_OFFSET",
1980 mongreldb_query::QueryTerminalErrorCategory::Serialization,
1981 );
1982 state.metrics.inc_sql_errors();
1983 return terminal_server_error_response(
1984 state,
1985 query_id,
1986 StatusCode::INTERNAL_SERVER_ERROR,
1987 "INVALID_PAGE_OFFSET",
1988 "failed to create the first SQL result page",
1989 );
1990 }
1991 Err(sql_pages::PageError::Cancelled) => unreachable!("first-page rendering has no control"),
1992 };
1993 let discard_after_response = page.next_cursor.is_none();
1994 let page_byte_count = page.byte_count;
1995 let encoded_page = tokio::task::spawn_blocking(move || serialize_sql_page(page)).await;
1996 let encoded_page = match encoded_page {
1997 Ok(Ok(encoded_page)) => encoded_page,
1998 Ok(Err(error)) => {
1999 state.sql_pages.discard(&retained);
2000 output.fail_serialization();
2001 state.metrics.inc_sql_errors();
2002 return terminal_server_error_response(
2003 state,
2004 query_id,
2005 StatusCode::INTERNAL_SERVER_ERROR,
2006 "SERIALIZATION_FAILED",
2007 error.to_string(),
2008 );
2009 }
2010 Err(_) => {
2011 state.sql_pages.discard(&retained);
2012 output.fail_serialization();
2013 state.metrics.inc_sql_errors();
2014 return terminal_server_error_response(
2015 state,
2016 query_id,
2017 StatusCode::INTERNAL_SERVER_ERROR,
2018 "SERIALIZATION_WORKER_FAILED",
2019 "SQL page response serialization worker failed",
2020 );
2021 }
2022 };
2023 if let Some(hook) = response_test_hook {
2024 hook(mongreldb_query::SqlTestHookPoint::AfterPageResponseSerialization);
2025 }
2026 if let Err(error) = output.query().checkpoint() {
2027 state.sql_pages.discard(&retained);
2028 state.metrics.inc_sql_errors();
2029 return tracked_query_error_response(state, &error, Some(query_id));
2030 }
2031 if let Err(error) = output.try_complete() {
2032 state.sql_pages.discard(&retained);
2033 state.metrics.inc_sql_errors();
2034 return tracked_query_error_response(state, &error, Some(query_id));
2035 }
2036 if discard_after_response {
2037 state.sql_pages.discard(&retained);
2038 }
2039 state.metrics.add_sql_output_bytes(page_byte_count);
2040 with_query_id(sql_page_response(encoded_page), query_id)
2041}
2042
2043fn serialize_paginated_rows(
2044 batches: &[arrow::record_batch::RecordBatch],
2045 query: &RegisteredSqlQuery,
2046 projection: &[String],
2047 max_rows: usize,
2048 max_bytes: usize,
2049 retained_memory_limit: usize,
2050 test_hook: Option<&mongreldb_query::SqlTestHook>,
2051) -> Result<SerializedPageRows, PaginatedSerializationError> {
2052 const ROW_CHECKPOINT_INTERVAL: usize = 256;
2053 if batches.is_empty() {
2054 let retained_bytes = sql_pages::accounted_bytes(2, &[], projection, || query.checkpoint())
2055 .map_err(PaginatedSerializationError::Query)?;
2056 if retained_bytes > retained_memory_limit {
2057 return Err(PaginatedSerializationError::Limit(
2058 PAGINATED_MEMORY_LIMIT_ERROR.into(),
2059 ));
2060 }
2061 return Ok(SerializedPageRows {
2062 rows: Vec::new(),
2063 retained_bytes,
2064 });
2065 }
2066 let fields = batches[0].schema();
2067 let mut indices = Vec::with_capacity(projection.len());
2068 for name in projection {
2069 let matches: Vec<_> = fields
2070 .fields()
2071 .iter()
2072 .enumerate()
2073 .filter_map(|(index, field)| (field.name() == name).then_some(index))
2074 .collect();
2075 if matches.len() != 1 {
2076 return Err(PaginatedSerializationError::Projection(format!(
2077 "projected output column {name:?} is missing or ambiguous"
2078 )));
2079 }
2080 indices.push(matches[0]);
2081 }
2082
2083 let mut writer_output = LimitedOutput::new(max_bytes);
2084 let mut rows = 0usize;
2085 let encoding = (|| {
2086 let mut writer = arrow::json::writer::ArrayWriter::new(&mut writer_output);
2087 for batch in batches {
2088 let batch = batch.project(&indices).map_err(|error| error.to_string())?;
2089 for offset in (0..batch.num_rows()).step_by(ROW_CHECKPOINT_INTERVAL) {
2090 if let Some(hook) = test_hook {
2091 hook(mongreldb_query::SqlTestHookPoint::BeforeSerializationBatch);
2092 }
2093 query.checkpoint().map_err(|error| error.to_string())?;
2094 let length = ROW_CHECKPOINT_INTERVAL.min(batch.num_rows() - offset);
2095 rows = rows.saturating_add(length);
2096 if rows > max_rows {
2097 return Err("SQL retained output row limit exceeded".into());
2098 }
2099 let slice = batch.slice(offset, length);
2100 writer
2101 .write_batches(&[&slice])
2102 .map_err(|error| error.to_string())?;
2103 }
2104 }
2105 writer.finish().map_err(|error| error.to_string())
2106 })();
2107 if let Err(error) = encoding {
2108 if let Err(query_error) = query.checkpoint() {
2109 return Err(PaginatedSerializationError::Query(query_error));
2110 }
2111 if writer_output.exceeded || rows > max_rows {
2112 return Err(PaginatedSerializationError::Limit(error));
2113 }
2114 return Err(PaginatedSerializationError::Encoding(error));
2115 }
2116 let bytes = writer_output.bytes.len();
2117 let retained_base = sql_pages::accounted_bytes(bytes, &[], projection, || query.checkpoint())
2118 .map_err(PaginatedSerializationError::Query)?;
2119 if retained_base > retained_memory_limit {
2120 return Err(PaginatedSerializationError::Limit(
2121 PAGINATED_MEMORY_LIMIT_ERROR.into(),
2122 ));
2123 }
2124 let reader = PaginatedJsonReader {
2125 cursor: std::io::Cursor::new(writer_output.bytes.as_slice()),
2126 query,
2127 test_hook,
2128 };
2129 let mut deserializer =
2130 serde_json::Deserializer::from_reader(std::io::BufReader::with_capacity(64 * 1024, reader));
2131 let mut budget = PaginatedDecodeBudget {
2132 used: retained_base,
2133 limit: retained_memory_limit,
2134 nodes: 0,
2135 exceeded: false,
2136 query,
2137 test_hook,
2138 };
2139 let rows = match serde::de::DeserializeSeed::deserialize(
2140 BudgetedJsonRowsSeed {
2141 budget: &mut budget,
2142 },
2143 &mut deserializer,
2144 ) {
2145 Ok(rows) => {
2146 if let Err(error) = deserializer.end() {
2147 return Err(PaginatedSerializationError::Encoding(error.to_string()));
2148 }
2149 rows
2150 }
2151 Err(error) => {
2152 if let Err(query_error) = query.checkpoint() {
2153 return Err(PaginatedSerializationError::Query(query_error));
2154 }
2155 if budget.exceeded {
2156 return Err(PaginatedSerializationError::Limit(
2157 PAGINATED_MEMORY_LIMIT_ERROR.into(),
2158 ));
2159 }
2160 return Err(PaginatedSerializationError::Encoding(error.to_string()));
2161 }
2162 };
2163 if let Some(hook) = test_hook {
2164 hook(mongreldb_query::SqlTestHookPoint::AfterSerialization);
2165 }
2166 let retained_bytes =
2167 sql_pages::accounted_bytes(bytes, &rows, projection, || query.checkpoint())
2168 .map_err(PaginatedSerializationError::Query)?;
2169 if retained_bytes > retained_memory_limit {
2170 return Err(PaginatedSerializationError::Limit(
2171 PAGINATED_MEMORY_LIMIT_ERROR.into(),
2172 ));
2173 }
2174 Ok(SerializedPageRows {
2175 rows,
2176 retained_bytes,
2177 })
2178}
2179
2180fn serialize_sql_page(page: sql_pages::SqlPage) -> Result<Vec<u8>, serde_json::Error> {
2181 serde_json::to_vec(&json!({
2182 "status": "completed",
2183 "rows": page.rows,
2184 "next_cursor": page.next_cursor,
2185 "page": {
2186 "offset": page.offset,
2187 "row_count": page.row_count,
2188 "total_rows": page.total_rows,
2189 "byte_count": page.byte_count,
2190 "estimated_tokens": page.estimated_tokens,
2191 "limits": page.limits,
2192 "projection": page.projection,
2193 "expires_at_ms": page.expires_at_ms,
2194 "snapshot": "retained_result",
2195 "token_estimate": "ceil(projected_json_bytes/4)",
2196 }
2197 }))
2198}
2199
2200enum ControlledPageSerializationError {
2201 Query(mongreldb_query::MongrelQueryError),
2202 Encoding,
2203}
2204
2205struct ControlledPageWriter<'a> {
2206 bytes: Vec<u8>,
2207 query: &'a RegisteredSqlQuery,
2208}
2209
2210impl std::io::Write for ControlledPageWriter<'_> {
2211 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
2212 self.query
2213 .checkpoint()
2214 .map_err(|error| std::io::Error::other(error.to_string()))?;
2215 self.bytes.extend_from_slice(buffer);
2216 Ok(buffer.len())
2217 }
2218
2219 fn flush(&mut self) -> std::io::Result<()> {
2220 Ok(())
2221 }
2222}
2223
2224fn serialize_sql_page_controlled(
2225 page: sql_pages::SqlPage,
2226 query: &RegisteredSqlQuery,
2227) -> Result<Vec<u8>, ControlledPageSerializationError> {
2228 query
2229 .checkpoint()
2230 .map_err(ControlledPageSerializationError::Query)?;
2231 let value = json!({
2232 "status": "completed",
2233 "rows": page.rows,
2234 "next_cursor": page.next_cursor,
2235 "page": {
2236 "offset": page.offset,
2237 "row_count": page.row_count,
2238 "total_rows": page.total_rows,
2239 "byte_count": page.byte_count,
2240 "estimated_tokens": page.estimated_tokens,
2241 "limits": page.limits,
2242 "projection": page.projection,
2243 "expires_at_ms": page.expires_at_ms,
2244 "snapshot": "retained_result",
2245 "token_estimate": "ceil(projected_json_bytes/4)",
2246 }
2247 });
2248 let mut writer = ControlledPageWriter {
2249 bytes: Vec::new(),
2250 query,
2251 };
2252 if let Err(error) = serde_json::to_writer(&mut writer, &value) {
2253 return match query.checkpoint() {
2254 Err(error) => Err(ControlledPageSerializationError::Query(error)),
2255 Ok(()) => {
2256 let _ = error;
2257 Err(ControlledPageSerializationError::Encoding)
2258 }
2259 };
2260 }
2261 query
2262 .checkpoint()
2263 .map_err(ControlledPageSerializationError::Query)?;
2264 Ok(writer.bytes)
2265}
2266
2267fn sql_page_response(body: Vec<u8>) -> Response {
2268 ([(header::CONTENT_TYPE, "application/json")], body).into_response()
2269}
2270
2271fn validate_stmt_name(name: &str) -> Result<(), String> {
2275 let mut chars = name.chars();
2276 match chars.next() {
2277 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
2278 _ => return Err("statement name must start with a letter or underscore".into()),
2279 }
2280 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
2281 return Err("statement name may contain only letters, digits, or underscore".into());
2282 }
2283 Ok(())
2284}
2285
2286fn render_sql_literal(v: &serde_json::Value) -> Result<String, String> {
2291 match v {
2292 serde_json::Value::Null => Ok("NULL".into()),
2293 serde_json::Value::Bool(b) => {
2294 if *b {
2295 Ok("TRUE".into())
2296 } else {
2297 Ok("FALSE".into())
2298 }
2299 }
2300 serde_json::Value::Number(n) => Ok(n.to_string()),
2301 serde_json::Value::String(s) => {
2303 let mut out = String::with_capacity(s.len() + 2);
2304 out.push('\'');
2305 for c in s.chars() {
2306 if c == '\'' {
2307 out.push_str("''");
2308 } else {
2309 out.push(c);
2310 }
2311 }
2312 out.push('\'');
2313 Ok(out)
2314 }
2315 _ => Err("prepared-statement parameters must be scalar (null/bool/number/string)".into()),
2317 }
2318}
2319
2320#[derive(Deserialize)]
2321struct PrepareRequest {
2322 name: String,
2323 sql: String,
2324 #[serde(default)]
2325 query_id: Option<QueryId>,
2326 #[serde(default)]
2327 timeout_ms: Option<u64>,
2328}
2329
2330async fn prepare_statement(
2334 State(state): State<Arc<AppState>>,
2335 OptionalPrincipal(principal): OptionalPrincipal,
2336 Path(id): Path<String>,
2337 headers: axum::http::HeaderMap,
2338 Json(req): Json<PrepareRequest>,
2339) -> Response {
2340 if !state.accepting_sql.load(Ordering::Acquire) {
2341 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
2342 }
2343 if !request_identity_is_current(&state, &principal) {
2344 return StatusCode::NOT_FOUND.into_response();
2345 }
2346 if let Err(msg) = validate_stmt_name(&req.name) {
2347 return (StatusCode::BAD_REQUEST, msg).into_response();
2348 }
2349 let owner = request_owner(&state, &principal);
2350 let Some(entry) = state.sessions.get(&id, &owner) else {
2351 return (
2352 StatusCode::NOT_FOUND,
2353 "session not found or not owned by caller",
2354 )
2355 .into_response();
2356 };
2357 let (options, query_id) = match resolve_query_options(
2358 &state,
2359 &headers,
2360 req.query_id,
2361 req.timeout_ms,
2362 owner,
2363 Some(id),
2364 ) {
2365 Ok(options) => options,
2366 Err(response) => return *response,
2367 };
2368 let query = match register_controlled_query(&state, &entry.session, options) {
2369 Ok(query) => query,
2370 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2371 };
2372 let registration = RegisteredQueryGuard::new(query);
2373 if mongreldb_query::contains_boolean_ai_predicate(&req.sql) {
2374 registration.fail();
2375 return with_query_id(remote_boolean_ai_error(), query_id);
2376 }
2377 let _sql_permit = match acquire_sql_permit(&state, &entry.session, registration.query()).await {
2378 Ok(permit) => permit,
2379 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2380 };
2381 let _guard = tokio::select! {
2382 guard = entry.lock.lock() => guard,
2383 _ = registration.query().control().cancelled() => {
2384 return tracked_query_error_response(
2385 &state,
2386 &cancellation_checkpoint_error(registration.query()),
2387 Some(query_id),
2388 );
2389 }
2390 };
2391 if entry.is_closed() {
2392 return with_query_id(
2393 (StatusCode::NOT_FOUND, "session no longer available").into_response(),
2394 query_id,
2395 );
2396 }
2397 entry.touch();
2398 let sql = format!("PREPARE {} AS {}", req.name, req.sql);
2399 let query = registration.into_query();
2400 match entry.session.run_with_query(&sql, query).await {
2401 Ok(_) => with_query_id(
2402 Json(json!({ "prepared": req.name })).into_response(),
2403 query_id,
2404 ),
2405 Err(error) => tracked_query_error_response(&state, &error, Some(query_id)),
2406 }
2407}
2408
2409#[derive(Deserialize)]
2410struct ExecuteRequest {
2411 name: String,
2412 params: Vec<serde_json::Value>,
2413 #[serde(default)]
2414 format: Option<String>,
2415 #[serde(default)]
2416 query_id: Option<QueryId>,
2417 #[serde(default)]
2418 timeout_ms: Option<u64>,
2419}
2420
2421async fn execute_statement(
2425 State(state): State<Arc<AppState>>,
2426 OptionalPrincipal(principal): OptionalPrincipal,
2427 Path(id): Path<String>,
2428 headers: axum::http::HeaderMap,
2429 Json(req): Json<ExecuteRequest>,
2430) -> Response {
2431 if !state.accepting_sql.load(Ordering::Acquire) {
2432 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
2433 }
2434 if !request_identity_is_current(&state, &principal) {
2435 return StatusCode::NOT_FOUND.into_response();
2436 }
2437 if let Err(msg) = validate_stmt_name(&req.name) {
2438 return (StatusCode::BAD_REQUEST, msg).into_response();
2439 }
2440 let owner = request_owner(&state, &principal);
2441 let Some(entry) = state.sessions.get(&id, &owner) else {
2442 return (
2443 StatusCode::NOT_FOUND,
2444 "session not found or not owned by caller",
2445 )
2446 .into_response();
2447 };
2448 let (options, query_id) = match resolve_query_options(
2449 &state,
2450 &headers,
2451 req.query_id,
2452 req.timeout_ms,
2453 owner,
2454 Some(id),
2455 ) {
2456 Ok(options) => options,
2457 Err(response) => return *response,
2458 };
2459 let query = match register_controlled_query(&state, &entry.session, options) {
2460 Ok(query) => query,
2461 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2462 };
2463 let registration = RegisteredQueryGuard::new(query);
2464 let sql_permit = match acquire_sql_permit(&state, &entry.session, registration.query()).await {
2465 Ok(permit) => permit,
2466 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2467 };
2468 let _guard = tokio::select! {
2469 guard = entry.lock.lock() => guard,
2470 _ = registration.query().control().cancelled() => {
2471 return tracked_query_error_response(
2472 &state,
2473 &cancellation_checkpoint_error(registration.query()),
2474 Some(query_id),
2475 );
2476 }
2477 };
2478 if entry.is_closed() {
2479 return with_query_id(
2480 (StatusCode::NOT_FOUND, "session no longer available").into_response(),
2481 query_id,
2482 );
2483 }
2484 entry.touch();
2485 state.metrics.inc_sql_queries();
2486 let literals: Vec<String> = match req
2487 .params
2488 .iter()
2489 .map(render_sql_literal)
2490 .collect::<Result<_, _>>()
2491 {
2492 Ok(v) => v,
2493 Err(msg) => {
2494 state.metrics.inc_sql_errors();
2495 return (StatusCode::BAD_REQUEST, msg).into_response();
2496 }
2497 };
2498 let sql = format!("EXECUTE {}({})", req.name, literals.join(", "));
2499 let start = std::time::Instant::now();
2500 let result = if req.format.as_deref() == Some("arrow-stream") {
2501 let query = registration.into_query();
2502 match entry
2503 .session
2504 .run_stream_with_query_for_serialization(&sql, query)
2505 .await
2506 {
2507 Ok((stream, completion)) => Ok(sql_arrow_stream_response_controlled(
2508 stream,
2509 completion,
2510 sql_permit,
2511 (state.sql_max_output_rows, state.sql_max_output_bytes),
2512 &state,
2513 query_id,
2514 entry.session.sql_test_hook(),
2515 )),
2516 Err(error) => Err(error),
2517 }
2518 } else {
2519 let query = registration.into_query();
2520 match entry
2521 .session
2522 .run_with_query_for_serialization_with_limits(
2523 &sql,
2524 query,
2525 mongreldb_query::SqlCollectionLimits::new(
2526 state.sql_max_output_rows,
2527 state.sql_max_output_bytes,
2528 ),
2529 )
2530 .await
2531 {
2532 Ok(output) => Ok(dispatch_buffered_sql_format(
2533 &state,
2534 req.format.as_deref(),
2535 output,
2536 query_id,
2537 entry.session.sql_test_hook(),
2538 (state.sql_max_output_rows, state.sql_max_output_bytes),
2539 )
2540 .await),
2541 Err(error) => Err(error),
2542 }
2543 };
2544 let elapsed = start.elapsed();
2545 if elapsed >= state.slow_query_threshold {
2546 state.metrics.inc_slow_queries();
2547 eprintln!(
2548 "[slow-query] {}\u{00b5}s \u{2014} EXECUTE {}",
2549 elapsed.as_micros(),
2550 req.name
2551 );
2552 }
2553 match result {
2554 Ok(response) => with_query_id(response, query_id),
2555 Err(e) => {
2556 state.metrics.inc_sql_errors();
2557 let msg = format!("{e}");
2559 let status = if msg.contains("does not exist") {
2560 StatusCode::NOT_FOUND
2561 } else {
2562 status_for_query_error(&e)
2563 };
2564 if status == status_for_query_error(&e) {
2565 tracked_query_error_response(&state, &e, Some(query_id))
2566 } else {
2567 with_query_id(
2568 (status, format!("{msg} ({}µs)", elapsed.as_micros())).into_response(),
2569 query_id,
2570 )
2571 }
2572 }
2573 }
2574}
2575
2576async fn deallocate_statement(
2579 State(state): State<Arc<AppState>>,
2580 OptionalPrincipal(principal): OptionalPrincipal,
2581 Path((id, name)): Path<(String, String)>,
2582 headers: axum::http::HeaderMap,
2583) -> Response {
2584 if !state.accepting_sql.load(Ordering::Acquire) {
2585 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
2586 }
2587 if !request_identity_is_current(&state, &principal) {
2588 return StatusCode::NOT_FOUND.into_response();
2589 }
2590 if let Err(msg) = validate_stmt_name(&name) {
2591 return (StatusCode::BAD_REQUEST, msg).into_response();
2592 }
2593 let owner = request_owner(&state, &principal);
2594 let Some(entry) = state.sessions.get(&id, &owner) else {
2595 return (
2596 StatusCode::NOT_FOUND,
2597 "session not found or not owned by caller",
2598 )
2599 .into_response();
2600 };
2601 let (options, query_id) =
2602 match resolve_query_options(&state, &headers, None, None, owner, Some(id)) {
2603 Ok(options) => options,
2604 Err(response) => return *response,
2605 };
2606 let query = match register_controlled_query(&state, &entry.session, options) {
2607 Ok(query) => query,
2608 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2609 };
2610 let registration = RegisteredQueryGuard::new(query);
2611 let _sql_permit = match acquire_sql_permit(&state, &entry.session, registration.query()).await {
2612 Ok(permit) => permit,
2613 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
2614 };
2615 let _guard = tokio::select! {
2616 guard = entry.lock.lock() => guard,
2617 _ = registration.query().control().cancelled() => {
2618 return tracked_query_error_response(
2619 &state,
2620 &cancellation_checkpoint_error(registration.query()),
2621 Some(query_id),
2622 );
2623 }
2624 };
2625 if entry.is_closed() {
2626 return with_query_id(
2627 (StatusCode::NOT_FOUND, "session no longer available").into_response(),
2628 query_id,
2629 );
2630 }
2631 entry.touch();
2632 state.metrics.inc_sql_queries();
2633 let sql = format!("DEALLOCATE {name}");
2634 let start = std::time::Instant::now();
2635 let result = entry
2636 .session
2637 .run_with_query(&sql, registration.into_query())
2638 .await;
2639 let elapsed = start.elapsed();
2640 if elapsed >= state.slow_query_threshold {
2641 state.metrics.inc_slow_queries();
2642 eprintln!(
2643 "[slow-query] {}\u{00b5}s query_id={} operation=DEALLOCATE",
2644 elapsed.as_micros(),
2645 query_id,
2646 );
2647 }
2648 match result {
2649 Ok(_) => with_query_id(
2650 Json(json!({ "deallocated": name })).into_response(),
2651 query_id,
2652 ),
2653 Err(error) => {
2654 state.metrics.inc_sql_errors();
2655 tracked_query_error_response(&state, &error, Some(query_id))
2656 }
2657 }
2658}
2659
2660async fn metrics_handler(
2663 State(state): State<Arc<AppState>>,
2664 OptionalPrincipal(principal): OptionalPrincipal,
2665) -> Response {
2666 if let Err(error) = state.db.require_for(
2667 request_principal(&state, &principal).as_ref(),
2668 &mongreldb_core::Permission::Admin,
2669 ) {
2670 return (status_for_error(&error), error.to_string()).into_response();
2671 }
2672 let body = state.metrics.prometheus_text(
2673 state.db.table_names().len(),
2674 state.query_registry.stats(),
2675 (
2676 state.pre_cancellations.len(),
2677 state.pre_cancellations.approximate_bytes(),
2678 ),
2679 );
2680 (
2681 [(
2682 header::CONTENT_TYPE,
2683 "text/plain; version=0.0.4; charset=utf-8".to_string(),
2684 )],
2685 body,
2686 )
2687 .into_response()
2688}
2689
2690async fn compact_all(
2692 State(state): State<Arc<AppState>>,
2693 OptionalPrincipal(principal): OptionalPrincipal,
2694) -> (StatusCode, Json<serde_json::Value>) {
2695 if let Err(error) = state.db.require_for(
2696 request_principal(&state, &principal).as_ref(),
2697 &mongreldb_core::Permission::Ddl,
2698 ) {
2699 return (
2700 status_for_error(&error),
2701 Json(json!({ "status": "error", "message": error.to_string() })),
2702 );
2703 }
2704 match state.db.compact() {
2705 Ok((compacted, skipped)) => (
2706 StatusCode::OK,
2707 Json(json!({
2708 "status": "ok",
2709 "compacted": compacted,
2710 "skipped": skipped,
2711 })),
2712 ),
2713 Err(e) => (
2714 StatusCode::INTERNAL_SERVER_ERROR,
2715 Json(json!({ "status": "error", "message": format!("{e}") })),
2716 ),
2717 }
2718}
2719
2720async fn compact_table(
2722 State(state): State<Arc<AppState>>,
2723 OptionalPrincipal(principal): OptionalPrincipal,
2724 Path(name): Path<String>,
2725) -> (StatusCode, Json<serde_json::Value>) {
2726 if let Err(error) = state.db.require_for(
2727 request_principal(&state, &principal).as_ref(),
2728 &mongreldb_core::Permission::Ddl,
2729 ) {
2730 return (
2731 status_for_error(&error),
2732 Json(json!({ "status": "error", "table": name, "message": error.to_string() })),
2733 );
2734 }
2735 match state.db.compact_table(&name) {
2736 Ok(true) => (
2737 StatusCode::OK,
2738 Json(json!({ "status": "compacted", "table": name })),
2739 ),
2740 Ok(false) => (
2741 StatusCode::OK,
2742 Json(json!({ "status": "skipped", "table": name, "reason": "fewer than 2 runs" })),
2743 ),
2744 Err(e) => (
2745 StatusCode::INTERNAL_SERVER_ERROR,
2746 Json(json!({ "status": "error", "table": name, "message": format!("{e}") })),
2747 ),
2748 }
2749}
2750
2751#[derive(Deserialize)]
2752struct CreateTableRequest {
2753 name: String,
2754 columns: Vec<ColumnDefJson>,
2755}
2756
2757#[derive(Deserialize)]
2758struct ColumnDefJson {
2759 id: u16,
2760 name: String,
2761 ty: String,
2762 primary_key: bool,
2763 #[serde(default)]
2764 nullable: bool,
2765}
2766
2767async fn create_table(
2768 State(state): State<Arc<AppState>>,
2769 OptionalPrincipal(principal): OptionalPrincipal,
2770 Json(req): Json<CreateTableRequest>,
2771) -> Response {
2772 if let Err(error) = state.db.require_for(
2773 request_principal(&state, &principal).as_ref(),
2774 &mongreldb_core::Permission::Ddl,
2775 ) {
2776 return (status_for_error(&error), error.to_string()).into_response();
2777 }
2778 let mut columns = Vec::new();
2779 for c in &req.columns {
2780 let ty = match c.ty.as_str() {
2781 "int64" | "bigint" => TypeId::Int64,
2782 "float64" | "double" => TypeId::Float64,
2783 "bytes" | "varchar" | "text" => TypeId::Bytes,
2784 "bool" => TypeId::Bool,
2785 other => {
2786 return (StatusCode::BAD_REQUEST, format!("unknown type: {other}")).into_response()
2787 }
2788 };
2789 let mut flags = mongreldb_core::schema::ColumnFlags::empty();
2790 if c.primary_key {
2791 flags = flags.with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY);
2792 }
2793 if c.nullable {
2794 flags = flags.with(mongreldb_core::schema::ColumnFlags::NULLABLE);
2795 }
2796 columns.push(mongreldb_core::schema::ColumnDef {
2797 id: c.id,
2798 name: c.name.clone(),
2799 ty,
2800 flags,
2801 default_value: None,
2802 });
2803 }
2804 let schema = Schema {
2805 schema_id: 0,
2806 columns,
2807 indexes: vec![],
2808 colocation: vec![],
2809 constraints: Default::default(),
2810 clustered: false,
2811 };
2812 if let Err(msg) = validate_table_name(&req.name) {
2813 return (StatusCode::BAD_REQUEST, msg).into_response();
2814 }
2815 match state.db.create_table(&req.name, schema) {
2816 Ok(id) => Json(json!({
2817 "table_id": id,
2818 "table_id_text": id.to_string()
2819 }))
2820 .into_response(),
2821 Err(error) => crate::kit::durable_core_error_response(&error)
2822 .unwrap_or_else(|| (status_for_error(&error), error.to_string()).into_response()),
2823 }
2824}
2825
2826async fn list_tables(
2827 State(state): State<Arc<AppState>>,
2828 OptionalPrincipal(principal): OptionalPrincipal,
2829) -> Json<Vec<String>> {
2830 let principal = request_principal(&state, &principal);
2831 Json(
2832 state
2833 .db
2834 .table_names()
2835 .into_iter()
2836 .filter(|table| {
2837 state
2838 .db
2839 .select_column_ids_for(table, principal.as_ref())
2840 .is_ok()
2841 })
2842 .collect(),
2843 )
2844}
2845
2846async fn drop_table(
2847 State(state): State<Arc<AppState>>,
2848 OptionalPrincipal(principal): OptionalPrincipal,
2849 Path(name): Path<String>,
2850) -> Response {
2851 if let Err(error) = state.db.require_for(
2852 request_principal(&state, &principal).as_ref(),
2853 &mongreldb_core::Permission::Ddl,
2854 ) {
2855 return (status_for_error(&error), error.to_string()).into_response();
2856 }
2857 match state.db.drop_table_with_epoch(&name) {
2858 Ok(epoch) => Json(json!({
2859 "status": "committed",
2860 "epoch": epoch.0,
2861 "epoch_text": epoch.0.to_string()
2862 }))
2863 .into_response(),
2864 Err(error) => crate::kit::durable_core_error_response(&error)
2865 .unwrap_or_else(|| (status_for_error(&error), error.to_string()).into_response()),
2866 }
2867}
2868
2869#[derive(Deserialize)]
2870struct PutRequest {
2871 row: Vec<serde_json::Value>,
2872}
2873
2874pub(crate) fn json_to_value(v: &serde_json::Value, expected: &TypeId) -> Value {
2875 match (v, expected) {
2876 (serde_json::Value::Number(n), TypeId::Float64) => {
2877 n.as_f64().map(Value::Float64).unwrap_or(Value::Null)
2878 }
2879 (serde_json::Value::Number(n), TypeId::Int64) => {
2880 n.as_i64().map(Value::Int64).unwrap_or(Value::Null)
2881 }
2882 (serde_json::Value::String(s), TypeId::Bytes) => Value::Bytes(s.as_bytes().to_vec()),
2883 (serde_json::Value::String(s), TypeId::Enum { variants }) => {
2884 if variants.iter().any(|v| v == s) {
2885 Value::Bytes(s.as_bytes().to_vec())
2886 } else {
2887 Value::Null
2888 }
2889 }
2890 (serde_json::Value::Bool(b), TypeId::Bool) => Value::Bool(*b),
2891 (serde_json::Value::Array(arr), TypeId::Embedding { dim }) => {
2894 if arr.len() as u32 != *dim {
2895 return Value::Null;
2896 }
2897 let vec: Option<Vec<f32>> =
2898 arr.iter().map(|el| el.as_f64().map(|f| f as f32)).collect();
2899 vec.map(Value::Embedding).unwrap_or(Value::Null)
2900 }
2901 (serde_json::Value::Null, _) => Value::Null,
2902 (serde_json::Value::Number(n), _) => {
2904 if let Some(i) = n.as_i64() {
2905 Value::Int64(i)
2906 } else if let Some(f) = n.as_f64() {
2907 Value::Float64(f)
2908 } else {
2909 Value::Null
2910 }
2911 }
2912 (serde_json::Value::String(s), _) => Value::Bytes(s.as_bytes().to_vec()),
2913 (serde_json::Value::Bool(b), _) => Value::Bool(*b),
2914 _ => Value::Null,
2915 }
2916}
2917
2918fn legacy_json_to_value(value: &serde_json::Value, expected: &TypeId) -> Result<Value, String> {
2919 if value.is_null() {
2920 return Ok(Value::Null);
2921 }
2922 match expected {
2923 TypeId::Bool => value
2924 .as_bool()
2925 .map(Value::Bool)
2926 .ok_or_else(|| "expected a boolean".into()),
2927 TypeId::Int8
2928 | TypeId::Int16
2929 | TypeId::Int32
2930 | TypeId::Int64
2931 | TypeId::UInt8
2932 | TypeId::UInt16
2933 | TypeId::UInt32
2934 | TypeId::UInt64
2935 | TypeId::TimestampNanos
2936 | TypeId::Date32
2937 | TypeId::Date64
2938 | TypeId::Time64 => value
2939 .as_i64()
2940 .map(Value::Int64)
2941 .ok_or_else(|| "expected a signed 64-bit integer".into()),
2942 TypeId::Float32 | TypeId::Float64 => value
2943 .as_f64()
2944 .filter(|value| value.is_finite())
2945 .map(Value::Float64)
2946 .ok_or_else(|| "expected a finite number".into()),
2947 TypeId::Bytes => match value.as_str() {
2948 Some(value) => Ok(Value::Bytes(value.as_bytes().to_vec())),
2949 None => decode_tagged_hex(value, "bytes").map(Value::Bytes),
2950 },
2951 TypeId::Enum { variants } => {
2952 let bytes = match value.as_str() {
2953 Some(value) => value.as_bytes().to_vec(),
2954 None => decode_tagged_hex(value, "bytes")?,
2955 };
2956 let value =
2957 std::str::from_utf8(&bytes).map_err(|_| "enum variant is not UTF-8".to_string())?;
2958 if !variants.iter().any(|variant| variant == value) {
2959 return Err("expected a declared enum variant".into());
2960 }
2961 Ok(Value::Bytes(bytes))
2962 }
2963 TypeId::Embedding { dim } => {
2964 let values = value
2965 .as_array()
2966 .ok_or_else(|| "expected an embedding array".to_string())?;
2967 if values.len() != *dim as usize {
2968 return Err(format!("expected an embedding with {dim} values"));
2969 }
2970 values
2971 .iter()
2972 .map(|value| {
2973 value
2974 .as_f64()
2975 .map(|value| value as f32)
2976 .filter(|value| value.is_finite())
2977 .ok_or_else(|| "embedding values must be finite numbers".to_string())
2978 })
2979 .collect::<Result<Vec<_>, _>>()
2980 .map(Value::Embedding)
2981 }
2982 TypeId::Decimal128 { .. } => {
2983 let object = exact_tagged_object(value, "decimal", &["unscaled"])?;
2984 let text = object["unscaled"]
2985 .as_str()
2986 .ok_or_else(|| "decimal unscaled value must be a string".to_string())?;
2987 let value = text
2988 .parse::<i128>()
2989 .map_err(|_| "decimal unscaled value is invalid".to_string())?;
2990 if value.to_string() != text {
2991 return Err("decimal unscaled value is not canonical".into());
2992 }
2993 Ok(Value::Decimal(value))
2994 }
2995 TypeId::Interval => {
2996 let object = exact_tagged_object(value, "interval", &["months", "days", "nanos"])?;
2997 let months = canonical_i64(&object["months"], "interval months")?;
2998 let days = canonical_i64(&object["days"], "interval days")?
2999 .try_into()
3000 .map_err(|_| "interval days is outside i32 range".to_string())?;
3001 let nanos = canonical_i64(&object["nanos"], "interval nanos")?;
3002 Ok(Value::Interval {
3003 months,
3004 days,
3005 nanos,
3006 })
3007 }
3008 TypeId::Uuid => {
3009 let bytes = decode_tagged_hex(value, "uuid")?;
3010 let bytes: [u8; 16] = bytes
3011 .try_into()
3012 .map_err(|_| "UUID must contain exactly 16 bytes".to_string())?;
3013 Ok(Value::Uuid(bytes))
3014 }
3015 TypeId::Json => {
3016 let bytes = decode_tagged_hex(value, "json")?;
3017 std::str::from_utf8(&bytes).map_err(|_| "JSON value is not UTF-8".to_string())?;
3018 serde_json::from_slice::<serde_json::Value>(&bytes)
3019 .map_err(|error| format!("JSON value is invalid: {error}"))?;
3020 Ok(Value::Json(bytes))
3021 }
3022 TypeId::Array { .. } => Err("legacy put does not support array columns".into()),
3023 }
3024}
3025
3026fn exact_tagged_object<'a>(
3027 value: &'a serde_json::Value,
3028 expected_kind: &str,
3029 fields: &[&str],
3030) -> Result<&'a serde_json::Map<String, serde_json::Value>, String> {
3031 let object = value
3032 .as_object()
3033 .ok_or_else(|| format!("expected tagged {expected_kind} value"))?;
3034 if object.len() != fields.len() + 1
3035 || object
3036 .get("$mongreldb_type")
3037 .and_then(|value| value.as_str())
3038 != Some(expected_kind)
3039 || fields.iter().any(|field| !object.contains_key(*field))
3040 {
3041 return Err(format!("invalid tagged {expected_kind} value"));
3042 }
3043 Ok(object)
3044}
3045
3046fn decode_tagged_hex(value: &serde_json::Value, expected_kind: &str) -> Result<Vec<u8>, String> {
3047 let object = exact_tagged_object(value, expected_kind, &["hex"])?;
3048 let encoded = object["hex"]
3049 .as_str()
3050 .ok_or_else(|| format!("tagged {expected_kind} hex must be a string"))?;
3051 if encoded.len() % 2 != 0 {
3052 return Err(format!("tagged {expected_kind} hex has odd length"));
3053 }
3054 encoded
3055 .as_bytes()
3056 .chunks_exact(2)
3057 .map(|pair| {
3058 let high = hex_nibble(pair[0])?;
3059 let low = hex_nibble(pair[1])?;
3060 Ok((high << 4) | low)
3061 })
3062 .collect()
3063}
3064
3065fn hex_nibble(value: u8) -> Result<u8, String> {
3066 match value {
3067 b'0'..=b'9' => Ok(value - b'0'),
3068 b'a'..=b'f' => Ok(value - b'a' + 10),
3069 _ => Err("hex value must use lowercase ASCII digits".into()),
3070 }
3071}
3072
3073fn canonical_i64(value: &serde_json::Value, field: &str) -> Result<i64, String> {
3074 let text = value
3075 .as_str()
3076 .ok_or_else(|| format!("{field} must be a string"))?;
3077 let value = text
3078 .parse::<i64>()
3079 .map_err(|_| format!("{field} is invalid"))?;
3080 if value.to_string() != text {
3081 return Err(format!("{field} is not canonical"));
3082 }
3083 Ok(value)
3084}
3085
3086#[cfg(test)]
3087mod legacy_wire_tests {
3088 use super::*;
3089
3090 #[test]
3091 fn typed_values_are_exact_and_malformed_values_fail_closed() {
3092 assert_eq!(
3093 legacy_json_to_value(
3094 &json!({"$mongreldb_type": "bytes", "hex": "00ff61"}),
3095 &TypeId::Bytes,
3096 )
3097 .unwrap(),
3098 Value::Bytes(vec![0, 0xff, b'a'])
3099 );
3100 for (value, ty) in [
3101 (
3102 json!({"$mongreldb_type": "bytes", "hex": "00FF"}),
3103 TypeId::Bytes,
3104 ),
3105 (
3106 json!({"$mongreldb_type": "decimal", "unscaled": "01"}),
3107 TypeId::Decimal128 {
3108 precision: 38,
3109 scale: 0,
3110 },
3111 ),
3112 (
3113 json!({"$mongreldb_type": "uuid", "hex": "00"}),
3114 TypeId::Uuid,
3115 ),
3116 (
3117 json!({"$mongreldb_type": "json", "hex": "7b"}),
3118 TypeId::Json,
3119 ),
3120 (json!([1.0]), TypeId::Embedding { dim: 2 }),
3121 (json!([1e100, 1.0]), TypeId::Embedding { dim: 2 }),
3122 ] {
3123 assert!(legacy_json_to_value(&value, &ty).is_err(), "{value}");
3124 }
3125 }
3126}
3127
3128fn parse_cells(
3131 row: &[serde_json::Value],
3132 schema: &mongreldb_core::schema::Schema,
3133) -> Result<Vec<(u16, Value)>, String> {
3134 if row.len() & 1 != 0 {
3135 return Err("row must be an even-length array of [col_id, value] pairs".into());
3136 }
3137 let mut out = Vec::with_capacity(row.len() / 2);
3138 let mut seen = std::collections::HashSet::new();
3139 for chunk in row.chunks(2) {
3140 let col_id = chunk[0]
3141 .as_u64()
3142 .and_then(|value| u16::try_from(value).ok())
3143 .ok_or("column id must be an unsigned 16-bit integer")?;
3144 if !seen.insert(col_id) {
3145 return Err(format!("duplicate column id {col_id}"));
3146 }
3147 let expected = schema
3148 .columns
3149 .iter()
3150 .find(|c| c.id == col_id)
3151 .map(|c| c.ty.clone())
3152 .ok_or_else(|| format!("unknown column id {col_id}"))?;
3153 let val = legacy_json_to_value(&chunk[1], &expected)?;
3154 out.push((col_id, val));
3155 }
3156 Ok(out)
3157}
3158
3159pub(crate) fn validate_table_name(name: &str) -> Result<(), String> {
3161 if name.is_empty() {
3162 return Err("table name must not be empty".into());
3163 }
3164 if name.contains('/') || name.contains('\\') || name.contains('\0') {
3165 return Err("table name contains invalid characters".into());
3166 }
3167 Ok(())
3168}
3169
3170async fn put_row(
3171 State(state): State<Arc<AppState>>,
3172 OptionalPrincipal(principal): OptionalPrincipal,
3173 Path(name): Path<String>,
3174 Json(req): Json<PutRequest>,
3175) -> Response {
3176 let handle = match state.db.table(&name) {
3177 Ok(h) => h,
3178 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
3179 };
3180 let schema = handle.lock().schema().clone();
3181 let row = match parse_cells(&req.row, &schema) {
3182 Ok(r) => r,
3183 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
3184 };
3185 state.metrics.inc_puts();
3186 let principal = request_principal(&state, &principal);
3187 match state.db.put_for(&name, row, principal.as_ref()) {
3188 Ok(rid) => Json(json!({ "row_id": rid.0.to_string() })).into_response(),
3189 Err(e) => (status_for_error(&e), e.to_string()).into_response(),
3190 }
3191}
3192
3193async fn count(
3194 State(state): State<Arc<AppState>>,
3195 OptionalPrincipal(principal): OptionalPrincipal,
3196 Path(name): Path<String>,
3197) -> Response {
3198 let principal = request_principal(&state, &principal);
3199 match state.db.count_for(&name, principal.as_ref()) {
3200 Ok(count) => Json(json!({ "count": count })).into_response(),
3201 Err(error) => (status_for_error(&error), error.to_string()).into_response(),
3202 }
3203}
3204
3205async fn commit(
3206 State(state): State<Arc<AppState>>,
3207 OptionalPrincipal(principal): OptionalPrincipal,
3208 Path(name): Path<String>,
3209) -> Response {
3210 if let Err(error) = state.db.require_for(
3211 request_principal(&state, &principal).as_ref(),
3212 &mongreldb_core::Permission::Update {
3213 table: name.clone(),
3214 },
3215 ) {
3216 return (status_for_error(&error), error.to_string()).into_response();
3217 }
3218 let handle = match state.db.table(&name) {
3219 Ok(h) => h,
3220 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
3221 };
3222 let mut g = handle.lock();
3223 state.metrics.inc_commits();
3224 match g.commit() {
3225 Ok(epoch) => Json(json!({
3226 "epoch": epoch.0,
3227 "epoch_text": epoch.0.to_string()
3228 }))
3229 .into_response(),
3230 Err(error) => crate::kit::durable_core_error_response(&error)
3231 .unwrap_or_else(|| (status_for_error(&error), error.to_string()).into_response()),
3232 }
3233}
3234
3235#[derive(Deserialize)]
3236struct SqlRequest {
3237 sql: String,
3238 #[serde(default)]
3241 format: Option<String>,
3242 #[serde(default)]
3244 query_id: Option<QueryId>,
3245 #[serde(default)]
3246 timeout_ms: Option<u64>,
3247 #[serde(default)]
3248 max_output_rows: Option<u64>,
3249 #[serde(default)]
3250 max_output_bytes: Option<u64>,
3251 #[serde(default)]
3252 idempotency_key: Option<String>,
3253 #[serde(default)]
3254 pagination: Option<SqlPaginationRequest>,
3255}
3256
3257#[derive(Clone, Debug, Deserialize, Serialize)]
3258struct SqlPaginationRequest {
3259 page_size_rows: u64,
3260 projection: Vec<String>,
3261 #[serde(default)]
3262 max_page_bytes: Option<u64>,
3263 #[serde(default)]
3264 max_page_tokens: Option<u64>,
3265}
3266
3267#[derive(Clone)]
3268struct ResolvedSqlPagination {
3269 projection: Vec<String>,
3270 limits: sql_pages::SqlPageLimits,
3271}
3272
3273struct ResolvedSqlRequest {
3274 request: SqlRequest,
3275 output_limits: (usize, usize),
3276 idempotency: Option<sql_idempotency::SqlIdempotencyExecution>,
3277 pagination: Option<ResolvedSqlPagination>,
3278}
3279
3280fn query_error_response(
3281 error: &mongreldb_query::MongrelQueryError,
3282 query_id: Option<QueryId>,
3283) -> Response {
3284 query_error_response_with_status(error, query_id, None)
3285}
3286
3287fn query_error_response_with_status(
3288 error: &mongreldb_query::MongrelQueryError,
3289 query_id: Option<QueryId>,
3290 status: Option<&mongreldb_query::QueryStatus>,
3291) -> Response {
3292 use mongreldb_query::MongrelQueryError;
3293 let (base_code, id) = match error {
3294 MongrelQueryError::QueryCancelled { query_id, .. } => ("QUERY_CANCELLED", Some(*query_id)),
3295 MongrelQueryError::DeadlineExceeded { query_id, .. } => {
3296 ("DEADLINE_EXCEEDED", Some(*query_id))
3297 }
3298 MongrelQueryError::QueryIdConflict { query_id } => ("QUERY_ID_CONFLICT", Some(*query_id)),
3299 MongrelQueryError::QueryRegistryFull => ("QUERY_REGISTRY_FULL", query_id),
3300 MongrelQueryError::ResultLimitExceeded { query_id, .. } => {
3301 ("RESULT_LIMIT_EXCEEDED", Some(*query_id))
3302 }
3303 MongrelQueryError::TransactionAborted => ("TRANSACTION_ABORTED", query_id),
3304 MongrelQueryError::NoSqlTransaction => ("NO_SQL_TRANSACTION", query_id),
3305 MongrelQueryError::SavepointNotFound { .. } => ("SAVEPOINT_NOT_FOUND", query_id),
3306 MongrelQueryError::CommitOutcome { query_id, .. } => ("COMMIT_OUTCOME", Some(*query_id)),
3307 MongrelQueryError::OutcomeUnknown { query_id, .. } => {
3308 ("QUERY_OUTCOME_UNKNOWN", Some(*query_id))
3309 }
3310 _ => ("QUERY_FAILED", query_id),
3311 };
3312 let (
3313 error_committed,
3314 error_committed_statements,
3315 error_last_commit_epoch,
3316 error_first_commit_statement_index,
3317 error_last_commit_statement_index,
3318 ) = match error {
3319 MongrelQueryError::QueryCancelled {
3320 committed,
3321 committed_statements,
3322 last_commit_epoch,
3323 first_commit_statement_index,
3324 last_commit_statement_index,
3325 ..
3326 }
3327 | MongrelQueryError::DeadlineExceeded {
3328 committed,
3329 committed_statements,
3330 last_commit_epoch,
3331 first_commit_statement_index,
3332 last_commit_statement_index,
3333 ..
3334 }
3335 | MongrelQueryError::ResultLimitExceeded {
3336 committed,
3337 committed_statements,
3338 last_commit_epoch,
3339 first_commit_statement_index,
3340 last_commit_statement_index,
3341 ..
3342 } => (
3343 *committed,
3344 *committed_statements,
3345 *last_commit_epoch,
3346 *first_commit_statement_index,
3347 *last_commit_statement_index,
3348 ),
3349 MongrelQueryError::CommitOutcome {
3350 committed,
3351 committed_statements,
3352 last_commit_epoch,
3353 first_commit_statement_index,
3354 last_commit_statement_index,
3355 ..
3356 } => (
3357 *committed,
3358 *committed_statements,
3359 *last_commit_epoch,
3360 *first_commit_statement_index,
3361 *last_commit_statement_index,
3362 ),
3363 _ => (false, 0, None, None, None),
3364 };
3365 let committed = status.map_or_else(
3366 || error_committed,
3367 |status| status.durable_outcome.committed,
3368 );
3369 let outcome_unknown = matches!(error, MongrelQueryError::OutcomeUnknown { .. })
3370 || status.is_some_and(|status| status.outcome_unknown);
3371 let code = status
3372 .and_then(|status| {
3373 status
3374 .terminal_error
3375 .as_ref()
3376 .map(|error| error.code.as_str())
3377 })
3378 .unwrap_or(match (base_code, committed) {
3379 ("QUERY_CANCELLED", true) => "QUERY_CANCELLED_AFTER_COMMIT",
3380 ("DEADLINE_EXCEEDED", true) => "DEADLINE_AFTER_COMMIT",
3381 _ => base_code,
3382 });
3383 let response_status = if outcome_unknown {
3384 "outcome_unknown"
3385 } else {
3386 status
3387 .and_then(mongreldb_query::QueryStatus::terminal_state)
3388 .map(terminal_state_name)
3389 .unwrap_or_else(|| match (error, committed) {
3390 (MongrelQueryError::QueryCancelled { .. }, true) => "cancelled_after_commit",
3391 (MongrelQueryError::QueryCancelled { .. }, false) => "cancelled_before_commit",
3392 (MongrelQueryError::DeadlineExceeded { .. }, true) => "deadline_after_commit",
3393 (MongrelQueryError::DeadlineExceeded { .. }, false) => "deadline_before_commit",
3394 (_, true) => "committed_with_error",
3395 _ => "failed_before_commit",
3396 })
3397 };
3398 let (completed_statements, statement_index) = status.map_or_else(
3399 || match error {
3400 MongrelQueryError::QueryCancelled {
3401 completed_statements,
3402 cancelled_statement_index,
3403 ..
3404 }
3405 | MongrelQueryError::DeadlineExceeded {
3406 completed_statements,
3407 cancelled_statement_index,
3408 ..
3409 } => (*completed_statements, *cancelled_statement_index),
3410 MongrelQueryError::ResultLimitExceeded {
3411 completed_statements,
3412 statement_index,
3413 ..
3414 } => (*completed_statements, *statement_index),
3415 MongrelQueryError::CommitOutcome {
3416 completed_statements,
3417 statement_index,
3418 ..
3419 } => (*completed_statements, *statement_index),
3420 _ => (0, 0),
3421 },
3422 |status| (status.completed_statements, status.statement_index),
3423 );
3424 let committed_statements = status.map_or(error_committed_statements, |status| {
3425 status.durable_outcome.committed_statements
3426 });
3427 let last_commit_epoch = status.map_or(error_last_commit_epoch, |status| {
3428 status.durable_outcome.last_commit_epoch
3429 });
3430 let first_commit_statement_index = status
3431 .map_or(error_first_commit_statement_index, |status| {
3432 status.durable_outcome.first_commit_statement_index
3433 });
3434 let last_commit_statement_index = status.map_or(error_last_commit_statement_index, |status| {
3435 status.durable_outcome.last_commit_statement_index
3436 });
3437 let cancellation_reason = status
3438 .map(|status| status.cancellation_reason)
3439 .or(match error {
3440 MongrelQueryError::QueryCancelled { reason, .. } => Some(*reason),
3441 MongrelQueryError::DeadlineExceeded { .. } => Some(CancellationReason::Deadline),
3442 _ => None,
3443 })
3444 .map(cancellation_reason_name);
3445 let cancel_outcome = match error {
3446 MongrelQueryError::QueryCancelled { .. } | MongrelQueryError::DeadlineExceeded { .. } => {
3447 Some("accepted")
3448 }
3449 _ => status.and_then(query_cancel_outcome),
3450 };
3451 let outcome = if outcome_unknown {
3452 json!({
3453 "committed": null,
3454 "committed_statements": null,
3455 "last_commit_epoch": null,
3456 "last_commit_epoch_text": null,
3457 "first_commit_statement_index": null,
3458 "last_commit_statement_index": null,
3459 "completed_statements": null,
3460 "statement_index": null,
3461 "serialization": "unknown",
3462 })
3463 } else {
3464 status.map_or_else(
3465 || {
3466 json!({
3467 "committed": committed,
3468 "committed_statements": committed_statements,
3469 "last_commit_epoch": last_commit_epoch,
3470 "last_commit_epoch_text": epoch_text(last_commit_epoch),
3471 "first_commit_statement_index": first_commit_statement_index,
3472 "last_commit_statement_index": last_commit_statement_index,
3473 "completed_statements": completed_statements,
3474 "statement_index": statement_index,
3475 "serialization": "unknown",
3476 })
3477 },
3478 |status| query_outcome_json(Some(status)),
3479 )
3480 };
3481 let http_status = match code {
3482 "QUERY_CANCELLED_AFTER_COMMIT" | "DEADLINE_AFTER_COMMIT" => StatusCode::CONFLICT,
3483 "QUERY_CANCELLED" => client_closed_request_status(),
3484 "DEADLINE_EXCEEDED" => StatusCode::GATEWAY_TIMEOUT,
3485 _ => status_for_query_error(error),
3486 };
3487 let mut response = (
3488 http_status,
3489 Json(json!({
3490 "query_id": id.map(|value| value.to_string()),
3491 "status": response_status,
3492 "terminal_state": response_status,
3493 "committed": (!outcome_unknown).then_some(committed),
3494 "committed_statements": (!outcome_unknown).then_some(committed_statements),
3495 "last_commit_epoch": (!outcome_unknown).then_some(last_commit_epoch).flatten(),
3496 "last_commit_epoch_text": (!outcome_unknown).then_some(epoch_text(last_commit_epoch)).flatten(),
3497 "first_commit_statement_index": (!outcome_unknown).then_some(first_commit_statement_index).flatten(),
3498 "last_commit_statement_index": (!outcome_unknown).then_some(last_commit_statement_index).flatten(),
3499 "completed_statements": (!outcome_unknown).then_some(completed_statements),
3500 "statement_index": (!outcome_unknown).then_some(statement_index),
3501 "cancel_outcome": cancel_outcome,
3502 "cancellation_reason": cancellation_reason,
3503 "retryable": matches!(error, MongrelQueryError::QueryRegistryFull),
3504 "server_state": status.map(|status| query_phase_name(status.phase)),
3505 "outcome": outcome,
3506 "error": {
3507 "code": code,
3508 "message": error.to_string(),
3509 "query_id": id.map(|value| value.to_string()),
3510 "committed": (!outcome_unknown).then_some(committed),
3511 "retryable": matches!(error, MongrelQueryError::QueryRegistryFull),
3512 }
3513 })),
3514 )
3515 .into_response();
3516 if let Some(id) = id {
3517 add_query_id_header(&mut response, id);
3518 }
3519 response
3520}
3521
3522fn record_query_error(metrics: &metrics::Metrics, error: &mongreldb_query::MongrelQueryError) {
3523 match error {
3524 mongreldb_query::MongrelQueryError::QueryCancelled { reason, .. } => {
3525 metrics.inc_sql_cancelled(*reason)
3526 }
3527 mongreldb_query::MongrelQueryError::DeadlineExceeded { .. } => {
3528 metrics.inc_sql_deadline_exceeded();
3529 metrics.inc_sql_cancelled(CancellationReason::Deadline);
3530 }
3531 _ => {}
3532 }
3533}
3534
3535fn tracked_query_error_response(
3536 state: &AppState,
3537 error: &mongreldb_query::MongrelQueryError,
3538 query_id: Option<QueryId>,
3539) -> Response {
3540 record_query_error(&state.metrics, error);
3541 if let mongreldb_query::MongrelQueryError::QueryCancelled { query_id, .. } = error {
3542 if let Some(requested_at) = state
3543 .query_registry
3544 .status(*query_id)
3545 .and_then(|status| status.cancel_requested_at)
3546 {
3547 state
3548 .metrics
3549 .observe_sql_cancel_latency(requested_at.elapsed());
3550 }
3551 }
3552 let status = if matches!(
3553 error,
3554 mongreldb_query::MongrelQueryError::QueryIdConflict { .. }
3555 | mongreldb_query::MongrelQueryError::QueryRegistryFull
3556 ) {
3557 None
3558 } else {
3559 query_id
3560 .or(match error {
3561 mongreldb_query::MongrelQueryError::QueryCancelled { query_id, .. }
3562 | mongreldb_query::MongrelQueryError::DeadlineExceeded { query_id, .. }
3563 | mongreldb_query::MongrelQueryError::CommitOutcome { query_id, .. }
3564 | mongreldb_query::MongrelQueryError::OutcomeUnknown { query_id, .. } => {
3565 Some(*query_id)
3566 }
3567 _ => None,
3568 })
3569 .and_then(|query_id| state.query_registry.status(query_id))
3570 };
3571 query_error_response_with_status(error, query_id, status.as_ref())
3572}
3573
3574fn add_query_id_header(response: &mut Response, query_id: QueryId) {
3575 if let Ok(value) = axum::http::HeaderValue::from_str(&query_id.to_string()) {
3576 response.headers_mut().insert("x-mongreldb-query-id", value);
3577 }
3578}
3579
3580fn with_query_id(mut response: Response, query_id: QueryId) -> Response {
3581 add_query_id_header(&mut response, query_id);
3582 response
3583}
3584
3585fn bad_query_control_request(message: impl Into<String>, query_id: Option<QueryId>) -> Response {
3586 let mut response = (
3587 StatusCode::BAD_REQUEST,
3588 Json(json!({
3589 "query_id": query_id.map(|value| value.to_string()),
3590 "status": "failed_before_commit",
3591 "terminal_state": "failed_before_commit",
3592 "committed": false,
3593 "committed_statements": 0,
3594 "last_commit_epoch": null,
3595 "last_commit_epoch_text": null,
3596 "first_commit_statement_index": null,
3597 "last_commit_statement_index": null,
3598 "completed_statements": 0,
3599 "statement_index": 0,
3600 "cancel_outcome": null,
3601 "cancellation_reason": null,
3602 "retryable": false,
3603 "server_state": "failed",
3604 "outcome": {
3605 "committed": false,
3606 "committed_statements": 0,
3607 "last_commit_epoch": null,
3608 "last_commit_epoch_text": null,
3609 "first_commit_statement_index": null,
3610 "last_commit_statement_index": null,
3611 "completed_statements": 0,
3612 "statement_index": 0,
3613 "serialization": "not_started",
3614 },
3615 "error": {
3616 "code": "INVALID_QUERY_OPTIONS",
3617 "message": message.into(),
3618 "query_id": query_id.map(|value| value.to_string()),
3619 "committed": false,
3620 "retryable": false,
3621 }
3622 })),
3623 )
3624 .into_response();
3625 if let Some(query_id) = query_id {
3626 add_query_id_header(&mut response, query_id);
3627 }
3628 response
3629}
3630
3631fn resolve_query_options(
3632 state: &AppState,
3633 headers: &axum::http::HeaderMap,
3634 body_query_id: Option<QueryId>,
3635 body_timeout_ms: Option<u64>,
3636 owner: String,
3637 session_id: Option<String>,
3638) -> std::result::Result<(SqlQueryOptions, QueryId), Box<Response>> {
3639 let query_id = match body_query_id {
3640 Some(query_id) => query_id,
3641 None => match headers.get("x-mongreldb-query-id") {
3642 Some(value) => {
3643 let value = value.to_str().map_err(|_| {
3644 Box::new(bad_query_control_request(
3645 "X-MongrelDB-Query-ID is not valid text",
3646 None,
3647 ))
3648 })?;
3649 value
3650 .parse()
3651 .map_err(|error: mongreldb_query::MongrelQueryError| {
3652 Box::new(bad_query_control_request(error.to_string(), None))
3653 })?
3654 }
3655 None => {
3656 QueryId::random().map_err(|error| Box::new(query_error_response(&error, None)))?
3657 }
3658 },
3659 };
3660 let timeout_ms = match body_timeout_ms {
3661 Some(timeout_ms) => timeout_ms,
3662 None => match headers.get("x-mongreldb-timeout-ms") {
3663 Some(value) => value
3664 .to_str()
3665 .ok()
3666 .and_then(|value| value.parse::<u64>().ok())
3667 .ok_or_else(|| {
3668 Box::new(bad_query_control_request(
3669 "X-MongrelDB-Timeout-Ms must be a positive integer",
3670 Some(query_id),
3671 ))
3672 })?,
3673 None => state
3674 .sql_default_timeout
3675 .as_millis()
3676 .min(u128::from(u64::MAX)) as u64,
3677 },
3678 };
3679 if timeout_ms == 0 {
3680 return Err(Box::new(bad_query_control_request(
3681 "timeout_ms must be positive",
3682 Some(query_id),
3683 )));
3684 }
3685 let timeout = std::time::Duration::from_millis(timeout_ms);
3686 if timeout > state.sql_max_timeout {
3687 return Err(Box::new(bad_query_control_request(
3688 format!(
3689 "timeout_ms exceeds server maximum of {}",
3690 state.sql_max_timeout.as_millis()
3691 ),
3692 Some(query_id),
3693 )));
3694 }
3695 Ok((
3696 SqlQueryOptions {
3697 query_id: Some(query_id),
3698 timeout: Some(timeout),
3699 owner: Some(owner),
3700 session_id,
3701 parent_control: None,
3702 },
3703 query_id,
3704 ))
3705}
3706
3707fn resolve_sql_output_limits(
3708 state: &AppState,
3709 request: &SqlRequest,
3710 query_id: QueryId,
3711) -> std::result::Result<(usize, usize), Box<Response>> {
3712 fn resolve(
3713 requested: Option<u64>,
3714 configured: usize,
3715 name: &str,
3716 query_id: QueryId,
3717 ) -> std::result::Result<usize, Box<Response>> {
3718 if requested == Some(0) {
3719 return Err(Box::new(bad_query_control_request(
3720 format!("{name} must be positive"),
3721 Some(query_id),
3722 )));
3723 }
3724 let requested = requested
3725 .and_then(|value| usize::try_from(value).ok())
3726 .unwrap_or(usize::MAX);
3727 Ok(requested.min(configured))
3728 }
3729
3730 Ok((
3731 resolve(
3732 request.max_output_rows,
3733 state.sql_max_output_rows,
3734 "max_output_rows",
3735 query_id,
3736 )?,
3737 resolve(
3738 request.max_output_bytes,
3739 state.sql_max_output_bytes,
3740 "max_output_bytes",
3741 query_id,
3742 )?,
3743 ))
3744}
3745
3746fn resolve_sql_pagination(
3747 headers: &axum::http::HeaderMap,
3748 request: &SqlRequest,
3749 output_limits: (usize, usize),
3750 registration: RegisteredQueryGuard,
3751 query_id: QueryId,
3752) -> Result<(RegisteredQueryGuard, Option<ResolvedSqlPagination>), Box<Response>> {
3753 let Some(pagination) = request.pagination.as_ref() else {
3754 return Ok((registration, None));
3755 };
3756 if requested_sql_idempotency_key(headers, request)
3757 .ok()
3758 .flatten()
3759 .is_some()
3760 {
3761 return Err(Box::new(registered_sql_error_response(
3762 registration,
3763 query_id,
3764 StatusCode::BAD_REQUEST,
3765 "INCOMPATIBLE_SQL_CONTROLS",
3766 "idempotency_key cannot be combined with SQL pagination",
3767 false,
3768 )));
3769 }
3770 if request
3771 .format
3772 .as_deref()
3773 .is_some_and(|format| format != "json")
3774 {
3775 return Err(Box::new(registered_sql_error_response(
3776 registration,
3777 query_id,
3778 StatusCode::BAD_REQUEST,
3779 "PAGINATION_REQUIRES_JSON",
3780 "SQL pagination supports JSON responses only",
3781 false,
3782 )));
3783 }
3784 registration.query().set_sql_metadata(&request.sql);
3785 if !mongreldb_query::is_single_read_only_query(&request.sql) {
3786 return Err(Box::new(registered_sql_error_response(
3787 registration,
3788 query_id,
3789 StatusCode::BAD_REQUEST,
3790 "PAGINATION_REQUIRES_SINGLE_READ_QUERY",
3791 "SQL pagination accepts exactly one read-only query statement",
3792 false,
3793 )));
3794 }
3795 let page_size = match usize::try_from(pagination.page_size_rows) {
3796 Ok(0) | Err(_) => {
3797 return Err(Box::new(registered_sql_error_response(
3798 registration,
3799 query_id,
3800 StatusCode::BAD_REQUEST,
3801 "INVALID_PAGINATION_OPTIONS",
3802 "pagination.page_size_rows must be positive",
3803 false,
3804 )))
3805 }
3806 Ok(value) => value.min(output_limits.0),
3807 };
3808 if pagination.projection.is_empty() || pagination.projection.len() > 128 {
3809 return Err(Box::new(registered_sql_error_response(
3810 registration,
3811 query_id,
3812 StatusCode::BAD_REQUEST,
3813 "INVALID_SQL_PROJECTION",
3814 "pagination.projection must contain between 1 and 128 output column names",
3815 false,
3816 )));
3817 }
3818 let mut seen = std::collections::HashSet::new();
3819 let metadata_bytes = pagination
3820 .projection
3821 .iter()
3822 .map(String::len)
3823 .fold(0usize, usize::saturating_add);
3824 if metadata_bytes > 16 * 1024
3825 || pagination.projection.iter().any(|column| {
3826 column.is_empty()
3827 || column == "*"
3828 || column.len() > 256
3829 || !seen.insert(column.as_str())
3830 })
3831 {
3832 return Err(Box::new(registered_sql_error_response(
3833 registration,
3834 query_id,
3835 StatusCode::BAD_REQUEST,
3836 "INVALID_SQL_PROJECTION",
3837 "pagination.projection requires unique explicit output names of at most 256 bytes",
3838 false,
3839 )));
3840 }
3841 let max_page_bytes = match pagination.max_page_bytes {
3842 Some(0) => {
3843 return Err(Box::new(registered_sql_error_response(
3844 registration,
3845 query_id,
3846 StatusCode::BAD_REQUEST,
3847 "INVALID_PAGINATION_OPTIONS",
3848 "pagination.max_page_bytes must be positive",
3849 false,
3850 )))
3851 }
3852 Some(value) => usize::try_from(value)
3853 .unwrap_or(usize::MAX)
3854 .min(output_limits.1),
3855 None => output_limits.1.min(1024 * 1024),
3856 };
3857 let token_cap = (output_limits.1.saturating_add(3) / 4).max(1);
3858 let max_page_tokens = match pagination.max_page_tokens {
3859 Some(0) => {
3860 return Err(Box::new(registered_sql_error_response(
3861 registration,
3862 query_id,
3863 StatusCode::BAD_REQUEST,
3864 "INVALID_PAGINATION_OPTIONS",
3865 "pagination.max_page_tokens must be positive",
3866 false,
3867 )))
3868 }
3869 Some(value) => usize::try_from(value).unwrap_or(usize::MAX).min(token_cap),
3870 None => (max_page_bytes.saturating_add(3) / 4).max(1),
3871 };
3872 Ok((
3873 registration,
3874 Some(ResolvedSqlPagination {
3875 projection: pagination.projection.clone(),
3876 limits: sql_pages::SqlPageLimits {
3877 rows: page_size,
3878 bytes: max_page_bytes,
3879 tokens: max_page_tokens,
3880 },
3881 }),
3882 ))
3883}
3884
3885fn requested_sql_idempotency_key(
3886 headers: &axum::http::HeaderMap,
3887 request: &SqlRequest,
3888) -> Result<Option<String>, &'static str> {
3889 let header = match headers.get("idempotency-key") {
3890 Some(value) => Some(
3891 value
3892 .to_str()
3893 .map_err(|_| "Idempotency-Key must be valid UTF-8")?,
3894 ),
3895 None => None,
3896 };
3897 match (request.idempotency_key.as_deref(), header) {
3898 (Some(body), Some(header)) if body != header => {
3899 Err("body idempotency_key and Idempotency-Key header must match")
3900 }
3901 (Some(body), _) => Ok(Some(body.to_owned())),
3902 (None, Some(header)) => Ok(Some(header.to_owned())),
3903 (None, None) => Ok(None),
3904 }
3905}
3906
3907fn sql_idempotency_binding(
3908 request: &SqlRequest,
3909 output_limits: (usize, usize),
3910 session_id: Option<&str>,
3911 expires_after_ms: u64,
3912) -> Result<sql_idempotency::SqlIdempotencyBinding, serde_json::Error> {
3913 let request_semantics = serde_json::to_vec(&json!({
3914 "format": request.format.as_deref().unwrap_or("json"),
3915 "max_output_rows": output_limits.0,
3916 "max_output_bytes": output_limits.1,
3917 "pagination": request.pagination.as_ref(),
3918 }))?;
3919 let session_semantics = session_id.map_or_else(
3920 || b"ephemeral".to_vec(),
3921 |session_id| {
3922 let mut semantics = b"session\0".to_vec();
3923 semantics.extend_from_slice(session_id.as_bytes());
3924 semantics
3925 },
3926 );
3927 Ok(sql_idempotency::SqlIdempotencyBinding {
3928 sql_fingerprint: mongreldb_query::normalized_sql_fingerprint(&request.sql),
3929 parameter_hash: sql_idempotency::hash(b"[]"),
3932 request_semantics_hash: sql_idempotency::hash(&request_semantics),
3933 session_semantics_hash: sql_idempotency::hash(&session_semantics),
3934 expires_after_ms,
3935 })
3936}
3937
3938struct SqlIdempotencyContext<'a> {
3939 headers: &'a axum::http::HeaderMap,
3940 request: &'a SqlRequest,
3941 output_limits: (usize, usize),
3942 owner: &'a str,
3943 session_id: Option<&'a str>,
3944 session_in_transaction: bool,
3945 query_id: QueryId,
3946}
3947
3948async fn begin_sql_idempotency(
3949 state: &AppState,
3950 context: SqlIdempotencyContext<'_>,
3951 registration: RegisteredQueryGuard,
3952) -> Result<
3953 (
3954 RegisteredQueryGuard,
3955 Option<sql_idempotency::SqlIdempotencyExecution>,
3956 ),
3957 Response,
3958> {
3959 let SqlIdempotencyContext {
3960 headers,
3961 request,
3962 output_limits,
3963 owner,
3964 session_id,
3965 session_in_transaction,
3966 query_id,
3967 } = context;
3968 let key = match requested_sql_idempotency_key(headers, request) {
3969 Ok(key) => key,
3970 Err(message) => {
3971 return Err(registered_sql_error_response(
3972 registration,
3973 query_id,
3974 StatusCode::BAD_REQUEST,
3975 "INVALID_IDEMPOTENCY_KEY",
3976 message,
3977 false,
3978 ))
3979 }
3980 };
3981 let Some(key) = key else {
3982 return Ok((registration, None));
3983 };
3984 match mongreldb_query::classify_sql_idempotency(&request.sql) {
3985 mongreldb_query::SqlIdempotencyClass::ReadOnly
3986 | mongreldb_query::SqlIdempotencyClass::Unsupported => {
3987 return Err(registered_sql_error_response(
3988 registration,
3989 query_id,
3990 StatusCode::BAD_REQUEST,
3991 "IDEMPOTENCY_REQUIRES_SINGLE_WRITE",
3992 "idempotency_key accepts one non-transaction SQL write statement",
3993 false,
3994 ));
3995 }
3996 mongreldb_query::SqlIdempotencyClass::SingleWrite => {}
3997 }
3998 if let Err(message) = sql_idempotency::SqlIdempotencyStore::validate_key(&key) {
3999 return Err(registered_sql_error_response(
4000 registration,
4001 query_id,
4002 StatusCode::BAD_REQUEST,
4003 "INVALID_IDEMPOTENCY_KEY",
4004 message,
4005 false,
4006 ));
4007 }
4008 if request
4009 .format
4010 .as_deref()
4011 .is_some_and(|format| format != "json")
4012 {
4013 return Err(registered_sql_error_response(
4014 registration,
4015 query_id,
4016 StatusCode::BAD_REQUEST,
4017 "IDEMPOTENCY_REQUIRES_JSON",
4018 "SQL idempotency supports buffered JSON responses only",
4019 false,
4020 ));
4021 }
4022 if session_in_transaction {
4023 return Err(registered_sql_error_response(
4024 registration,
4025 query_id,
4026 StatusCode::CONFLICT,
4027 "IDEMPOTENCY_UNSUPPORTED_IN_TRANSACTION",
4028 "SQL idempotency cannot be used inside an open session transaction",
4029 false,
4030 ));
4031 }
4032 registration.query().set_sql_metadata(&request.sql);
4033 let binding = match sql_idempotency_binding(
4034 request,
4035 output_limits,
4036 session_id,
4037 state.sql_idempotency.expires_after_ms(),
4038 ) {
4039 Ok(binding) => binding,
4040 Err(_) => {
4041 return Err(registered_sql_error_response(
4042 registration,
4043 query_id,
4044 StatusCode::INTERNAL_SERVER_ERROR,
4045 "SERIALIZATION_FAILED",
4046 "failed to serialize SQL idempotency request semantics",
4047 false,
4048 ))
4049 }
4050 };
4051 let begin = tokio::select! {
4052 begin = state.sql_idempotency.begin(owner, &key, binding) => begin,
4053 _ = registration.query().control().cancelled() => {
4054 return Err(tracked_query_error_response(
4055 state,
4056 &cancellation_checkpoint_error(registration.query()),
4057 Some(query_id),
4058 ));
4059 }
4060 };
4061 match begin {
4062 sql_idempotency::BeginResult::Execute(execution) => Ok((registration, Some(execution))),
4063 sql_idempotency::BeginResult::Replay {
4064 receipt,
4065 expires_at_ms,
4066 } => match restore_idempotency_replay(registration, &receipt) {
4067 Ok(()) => Err(sql_idempotency_receipt_response(
4068 query_id,
4069 &receipt,
4070 true,
4071 expires_at_ms,
4072 true,
4073 )),
4074 Err(error) => Err(tracked_query_error_response(state, &error, Some(query_id))),
4075 },
4076 sql_idempotency::BeginResult::Mismatch => Err(registered_sql_error_response(
4077 registration,
4078 query_id,
4079 StatusCode::CONFLICT,
4080 "IDEMPOTENCY_KEY_REUSE_MISMATCH",
4081 "idempotency key was already used with different SQL or request semantics",
4082 false,
4083 )),
4084 sql_idempotency::BeginResult::Indeterminate { created_at_ms } => Err(
4085 sql_idempotency_indeterminate_response(registration, query_id, created_at_ms),
4086 ),
4087 sql_idempotency::BeginResult::Full => Err(registered_sql_error_response(
4088 registration,
4089 query_id,
4090 StatusCode::SERVICE_UNAVAILABLE,
4091 "IDEMPOTENCY_STORE_FULL",
4092 "SQL idempotency receipt store is full",
4093 true,
4094 )),
4095 sql_idempotency::BeginResult::Unavailable => Err(registered_sql_error_response(
4096 registration,
4097 query_id,
4098 StatusCode::SERVICE_UNAVAILABLE,
4099 "IDEMPOTENCY_STORE_UNAVAILABLE",
4100 "could not durably reserve the SQL idempotency key",
4101 true,
4102 )),
4103 }
4104}
4105
4106fn restore_idempotency_replay(
4107 registration: RegisteredQueryGuard,
4108 receipt: &sql_idempotency::SqlDurableReceipt,
4109) -> mongreldb_query::Result<()> {
4110 use mongreldb_query::{
4111 DurableOutcome, QueryTerminalError, QueryTerminalErrorCategory, QueryTerminalState,
4112 SerializationOutcome,
4113 };
4114
4115 let invalid_receipt = |field: &str| {
4116 mongreldb_query::MongrelQueryError::InvalidQueryState(format!(
4117 "durable SQL idempotency receipt has invalid {field}"
4118 ))
4119 };
4120 let terminal_state = match receipt.status.as_str() {
4121 "completed" => QueryTerminalState::Completed,
4122 "failed_before_commit" => QueryTerminalState::FailedBeforeCommit,
4123 "cancelled_before_commit" => QueryTerminalState::CancelledBeforeCommit,
4124 "deadline_before_commit" => QueryTerminalState::DeadlineBeforeCommit,
4125 "committed" => QueryTerminalState::Committed,
4126 "committed_with_error" => QueryTerminalState::CommittedWithError,
4127 "partially_committed" => QueryTerminalState::PartiallyCommitted,
4128 "cancelled_after_commit" => QueryTerminalState::CancelledAfterCommit,
4129 "deadline_after_commit" => QueryTerminalState::DeadlineAfterCommit,
4130 _ => return Err(invalid_receipt("terminal state")),
4131 };
4132 let serialization = match receipt.outcome.serialization.as_str() {
4133 "not_started" => SerializationOutcome::NotStarted,
4134 "in_progress" => SerializationOutcome::InProgress,
4135 "succeeded" => SerializationOutcome::Succeeded,
4136 "failed" => SerializationOutcome::Failed,
4137 _ => return Err(invalid_receipt("serialization state")),
4138 };
4139 let terminal_error = match receipt.terminal_error.as_ref() {
4140 Some(error) => Some(QueryTerminalError {
4141 code: error.code.clone(),
4142 category: match error.category.as_str() {
4143 "cancellation" => QueryTerminalErrorCategory::Cancellation,
4144 "deadline" => QueryTerminalErrorCategory::Deadline,
4145 "result_limit" => QueryTerminalErrorCategory::ResultLimit,
4146 "serialization" => QueryTerminalErrorCategory::Serialization,
4147 "execution" => QueryTerminalErrorCategory::Execution,
4148 _ => return Err(invalid_receipt("terminal error category")),
4149 },
4150 }),
4151 None => None,
4152 };
4153 let cancellation_reason = CancellationReason::from_protocol_str(&receipt.cancellation_reason)
4154 .ok_or_else(|| invalid_receipt("cancellation reason"))?;
4155 let phase = match receipt.server_state.as_str() {
4156 "completed" => SqlQueryPhase::Completed,
4157 "cancelled" => SqlQueryPhase::Cancelled,
4158 "failed" => SqlQueryPhase::Failed,
4159 _ => return Err(invalid_receipt("server state")),
4160 };
4161 let query = registration.into_query();
4162 query.restore_replayed_outcome(
4163 DurableOutcome {
4164 committed: receipt.outcome.committed,
4165 committed_statements: receipt.outcome.committed_statements,
4166 last_commit_epoch: receipt.outcome.last_commit_epoch,
4167 first_commit_statement_index: receipt.outcome.first_commit_statement_index,
4168 last_commit_statement_index: receipt.outcome.last_commit_statement_index,
4169 },
4170 receipt.outcome.completed_statements,
4171 receipt.outcome.statement_index,
4172 serialization,
4173 terminal_error,
4174 terminal_state,
4175 cancellation_reason,
4176 phase,
4177 );
4178 query.try_complete()
4179}
4180
4181fn sql_idempotency_indeterminate_response(
4182 registration: RegisteredQueryGuard,
4183 query_id: QueryId,
4184 created_at_ms: Option<u64>,
4185) -> Response {
4186 registration.query().mark_outcome_unknown();
4187 registration.fail();
4188 with_query_id(
4189 (
4190 StatusCode::CONFLICT,
4191 Json(json!({
4192 "query_id": query_id.to_string(),
4193 "status": "outcome_unknown",
4194 "terminal_state": "outcome_unknown",
4195 "committed": null,
4196 "committed_statements": null,
4197 "last_commit_epoch": null,
4198 "last_commit_epoch_text": null,
4199 "first_commit_statement_index": null,
4200 "last_commit_statement_index": null,
4201 "completed_statements": null,
4202 "statement_index": null,
4203 "cancel_outcome": null,
4204 "cancellation_reason": null,
4205 "retryable": false,
4206 "server_state": "failed",
4207 "idempotency_replayed": true,
4208 "idempotency_intent_created_at_ms": created_at_ms,
4209 "outcome": {
4210 "committed": null,
4211 "committed_statements": null,
4212 "last_commit_epoch": null,
4213 "last_commit_epoch_text": null,
4214 "first_commit_statement_index": null,
4215 "last_commit_statement_index": null,
4216 "completed_statements": null,
4217 "statement_index": null,
4218 "serialization": "unknown",
4219 },
4220 "error": {
4221 "code": "QUERY_OUTCOME_UNKNOWN",
4222 "message": "a durable write intent exists without a durable receipt; the SQL was not re-executed",
4223 "query_id": query_id.to_string(),
4224 "committed": null,
4225 "retryable": false,
4226 }
4227 })),
4228 )
4229 .into_response(),
4230 query_id,
4231 )
4232}
4233
4234fn registered_sql_error_response(
4235 registration: RegisteredQueryGuard,
4236 query_id: QueryId,
4237 status: StatusCode,
4238 code: &'static str,
4239 message: impl Into<String>,
4240 retryable: bool,
4241) -> Response {
4242 let message = message.into();
4243 registration
4244 .query()
4245 .record_terminal_error(code, mongreldb_query::QueryTerminalErrorCategory::Execution);
4246 registration.fail();
4247 with_query_id(
4248 (
4249 status,
4250 Json(json!({
4251 "query_id": query_id.to_string(),
4252 "status": "failed_before_commit",
4253 "terminal_state": "failed_before_commit",
4254 "committed": false,
4255 "committed_statements": 0,
4256 "last_commit_epoch": null,
4257 "last_commit_epoch_text": null,
4258 "first_commit_statement_index": null,
4259 "last_commit_statement_index": null,
4260 "completed_statements": 0,
4261 "statement_index": 0,
4262 "cancel_outcome": null,
4263 "cancellation_reason": null,
4264 "retryable": retryable,
4265 "server_state": "failed",
4266 "outcome": {
4267 "committed": false,
4268 "committed_statements": 0,
4269 "last_commit_epoch": null,
4270 "last_commit_epoch_text": null,
4271 "first_commit_statement_index": null,
4272 "last_commit_statement_index": null,
4273 "completed_statements": 0,
4274 "statement_index": 0,
4275 "serialization": "not_started",
4276 },
4277 "error": {
4278 "code": code,
4279 "message": message,
4280 "query_id": query_id.to_string(),
4281 "committed": false,
4282 "retryable": retryable,
4283 }
4284 })),
4285 )
4286 .into_response(),
4287 query_id,
4288 )
4289}
4290
4291fn register_controlled_query(
4292 state: &AppState,
4293 session: &MongrelSession,
4294 options: SqlQueryOptions,
4295) -> std::result::Result<RegisteredSqlQuery, mongreldb_query::MongrelQueryError> {
4296 let query_id = options.query_id.ok_or_else(|| {
4297 mongreldb_query::MongrelQueryError::InvalidQueryState(
4298 "server query registration requires a query id".into(),
4299 )
4300 })?;
4301 let owner = options.owner.clone().unwrap_or_default();
4302 let session_id = options.session_id.clone();
4303 let _lifecycle = state
4304 .query_lifecycle
4305 .lock()
4306 .unwrap_or_else(|error| error.into_inner());
4307 let pre_cancel_reason = match state.pre_cancellations.lookup_for_registration(
4308 query_id,
4309 &owner,
4310 session_id.as_deref(),
4311 ) {
4312 pre_cancel::RegistrationLookup::NoReservation => None,
4313 pre_cancel::RegistrationLookup::Matching(reason) => Some(reason),
4314 pre_cancel::RegistrationLookup::ReservedByAnotherIdentity => {
4315 return Err(mongreldb_query::MongrelQueryError::QueryIdConflict { query_id });
4316 }
4317 };
4318 let query = session.register_query(options)?;
4319 let Some(reason) = pre_cancel_reason else {
4320 return Ok(query);
4321 };
4322 state
4323 .pre_cancellations
4324 .take(query_id, &owner, session_id.as_deref());
4325 query.request_cancel(reason);
4326 let error = query.checkpoint().err().unwrap_or_else(|| {
4327 mongreldb_query::MongrelQueryError::InvalidQueryState(format!(
4328 "pre-cancelled query {query_id} remained runnable"
4329 ))
4330 });
4331 query.fail();
4332 Err(error)
4333}
4334
4335async fn acquire_sql_permit(
4336 state: &AppState,
4337 session: &MongrelSession,
4338 query: &RegisteredSqlQuery,
4339) -> std::result::Result<tokio::sync::OwnedSemaphorePermit, mongreldb_query::MongrelQueryError> {
4340 session.fire_test_hook(mongreldb_query::SqlTestHookPoint::WaitingForSqlPermit);
4341 tokio::select! {
4342 permit = Arc::clone(&state.sql_semaphore).acquire_owned() => permit.map_err(|_| {
4343 mongreldb_query::MongrelQueryError::InvalidQueryState(
4344 "SQL admission semaphore closed".into(),
4345 )
4346 }),
4347 _ = query.control().cancelled() => Err(cancellation_checkpoint_error(query)),
4348 }
4349}
4350
4351fn caller_may_manage_query(
4352 state: &AppState,
4353 principal: &Option<mongreldb_core::Principal>,
4354 owner: Option<&str>,
4355) -> bool {
4356 let current = current_request_principal(state, principal);
4357 if (principal.is_some()
4358 || state.auth_token.is_some()
4359 || state.user_auth
4360 || state.db.require_auth_enabled())
4361 && current.is_none()
4362 {
4363 return false;
4364 }
4365 current.is_some_and(|principal| principal.is_admin)
4366 || owner == Some(request_owner(state, principal).as_str())
4367}
4368
4369fn query_phase_name(phase: SqlQueryPhase) -> &'static str {
4370 match phase {
4371 SqlQueryPhase::Queued => "queued",
4372 SqlQueryPhase::Planning => "planning",
4373 SqlQueryPhase::Executing => "executing",
4374 SqlQueryPhase::Streaming => "streaming",
4375 SqlQueryPhase::Serializing => "serializing",
4376 SqlQueryPhase::CommitCritical => "commit_critical",
4377 SqlQueryPhase::Cancelling => "cancelling",
4378 SqlQueryPhase::Completed => "completed",
4379 SqlQueryPhase::Failed => "failed",
4380 SqlQueryPhase::Cancelled => "cancelled",
4381 }
4382}
4383
4384fn commit_fence_outcome_name(outcome: mongreldb_query::CommitFenceOutcome) -> &'static str {
4385 match outcome {
4386 mongreldb_query::CommitFenceOutcome::NotReached => "not_reached",
4387 mongreldb_query::CommitFenceOutcome::CancelWon => "cancel_won",
4388 mongreldb_query::CommitFenceOutcome::CommitWon => "commit_won",
4389 }
4390}
4391
4392fn terminal_state_name(state: mongreldb_query::QueryTerminalState) -> &'static str {
4393 use mongreldb_query::QueryTerminalState;
4394 match state {
4395 QueryTerminalState::OutcomeUnknown => "outcome_unknown",
4396 QueryTerminalState::Completed => "completed",
4397 QueryTerminalState::FailedBeforeCommit => "failed_before_commit",
4398 QueryTerminalState::CancelledBeforeCommit => "cancelled_before_commit",
4399 QueryTerminalState::DeadlineBeforeCommit => "deadline_before_commit",
4400 QueryTerminalState::Committed => "committed",
4401 QueryTerminalState::CommittedWithError => "committed_with_error",
4402 QueryTerminalState::PartiallyCommitted => "partially_committed",
4403 QueryTerminalState::CancelledAfterCommit => "cancelled_after_commit",
4404 QueryTerminalState::DeadlineAfterCommit => "deadline_after_commit",
4405 }
4406}
4407
4408fn serialization_outcome_name(outcome: mongreldb_query::SerializationOutcome) -> &'static str {
4409 use mongreldb_query::SerializationOutcome;
4410 match outcome {
4411 SerializationOutcome::NotStarted => "not_started",
4412 SerializationOutcome::InProgress => "in_progress",
4413 SerializationOutcome::Succeeded => "succeeded",
4414 SerializationOutcome::Failed => "failed",
4415 }
4416}
4417
4418fn terminal_error_category_name(
4419 category: mongreldb_query::QueryTerminalErrorCategory,
4420) -> &'static str {
4421 use mongreldb_query::QueryTerminalErrorCategory;
4422 match category {
4423 QueryTerminalErrorCategory::Cancellation => "cancellation",
4424 QueryTerminalErrorCategory::Deadline => "deadline",
4425 QueryTerminalErrorCategory::ResultLimit => "result_limit",
4426 QueryTerminalErrorCategory::Serialization => "serialization",
4427 QueryTerminalErrorCategory::Execution => "execution",
4428 }
4429}
4430
4431fn terminal_error_retryable(error: Option<&mongreldb_query::QueryTerminalError>) -> bool {
4432 error.is_some_and(|error| {
4433 matches!(
4434 error.code.as_str(),
4435 "IDEMPOTENCY_STORE_FULL" | "IDEMPOTENCY_STORE_UNAVAILABLE"
4436 )
4437 })
4438}
4439
4440fn epoch_text(epoch: Option<u64>) -> Option<String> {
4441 epoch.map(|epoch| epoch.to_string())
4442}
4443
4444fn cancellation_reason_name(reason: CancellationReason) -> &'static str {
4445 reason.as_str()
4446}
4447
4448fn query_cancel_outcome(status: &mongreldb_query::QueryStatus) -> Option<&'static str> {
4449 match status.phase {
4450 SqlQueryPhase::CommitCritical => Some("too_late"),
4451 SqlQueryPhase::Completed | SqlQueryPhase::Failed | SqlQueryPhase::Cancelled => {
4452 Some("already_finished")
4453 }
4454 SqlQueryPhase::Cancelling => Some("accepted"),
4455 _ => None,
4456 }
4457}
4458
4459fn query_outcome_json(status: Option<&mongreldb_query::QueryStatus>) -> serde_json::Value {
4460 let Some(status) = status else {
4461 return json!({
4462 "committed": false,
4463 "committed_statements": 0,
4464 "last_commit_epoch": null,
4465 "last_commit_epoch_text": null,
4466 "first_commit_statement_index": null,
4467 "last_commit_statement_index": null,
4468 "completed_statements": 0,
4469 "statement_index": 0,
4470 "serialization": "not_started",
4471 });
4472 };
4473 if status.outcome_unknown {
4474 return json!({
4475 "committed": null,
4476 "committed_statements": null,
4477 "last_commit_epoch": null,
4478 "last_commit_epoch_text": null,
4479 "first_commit_statement_index": null,
4480 "last_commit_statement_index": null,
4481 "completed_statements": null,
4482 "statement_index": null,
4483 "serialization": "unknown",
4484 });
4485 }
4486 json!({
4487 "committed": status.durable_outcome.committed,
4488 "committed_statements": (!status.outcome_unknown).then_some(status.durable_outcome.committed_statements),
4489 "last_commit_epoch": (!status.outcome_unknown).then_some(status.durable_outcome.last_commit_epoch).flatten(),
4490 "last_commit_epoch_text": (!status.outcome_unknown).then_some(epoch_text(status.durable_outcome.last_commit_epoch)).flatten(),
4491 "first_commit_statement_index": (!status.outcome_unknown).then_some(status.durable_outcome.first_commit_statement_index).flatten(),
4492 "last_commit_statement_index": (!status.outcome_unknown).then_some(status.durable_outcome.last_commit_statement_index).flatten(),
4493 "completed_statements": (!status.outcome_unknown).then_some(status.completed_statements),
4494 "statement_index": (!status.outcome_unknown).then_some(status.statement_index),
4495 "serialization": serialization_outcome_name(status.serialization_outcome),
4496 })
4497}
4498
4499fn sql_terminal_idempotency_receipt(
4500 status: &mongreldb_query::QueryStatus,
4501) -> Option<sql_idempotency::SqlDurableReceipt> {
4502 if status.outcome_unknown {
4503 return None;
4504 }
4505 let terminal_state = status.terminal_state()?;
4506 if !status.durable_outcome.committed
4507 && terminal_state != mongreldb_query::QueryTerminalState::Completed
4508 {
4509 return None;
4510 }
4511 Some(sql_idempotency::SqlDurableReceipt {
4512 original_query_id: status.query_id.to_string(),
4513 status: status
4514 .terminal_state()
4515 .map(terminal_state_name)
4516 .unwrap_or("committed")
4517 .to_owned(),
4518 server_state: query_phase_name(status.phase).to_owned(),
4519 cancellation_reason: cancellation_reason_name(status.cancellation_reason).to_owned(),
4520 outcome: sql_idempotency::SqlReceiptOutcome {
4521 committed: status.durable_outcome.committed,
4522 committed_statements: status.durable_outcome.committed_statements,
4523 last_commit_epoch: status.durable_outcome.last_commit_epoch,
4524 last_commit_epoch_text: epoch_text(status.durable_outcome.last_commit_epoch),
4525 first_commit_statement_index: status.durable_outcome.first_commit_statement_index,
4526 last_commit_statement_index: status.durable_outcome.last_commit_statement_index,
4527 completed_statements: status.completed_statements,
4528 statement_index: status.statement_index,
4529 serialization: serialization_outcome_name(status.serialization_outcome).to_owned(),
4530 },
4531 terminal_error: status.terminal_error.as_ref().map(|error| {
4532 sql_idempotency::SqlReceiptTerminalError {
4533 code: error.code.clone(),
4534 category: terminal_error_category_name(error.category).to_owned(),
4535 }
4536 }),
4537 })
4538}
4539
4540fn sql_idempotency_receipt_response(
4541 query_id: QueryId,
4542 receipt: &sql_idempotency::SqlDurableReceipt,
4543 replayed: bool,
4544 expires_at_ms: u64,
4545 persisted: bool,
4546) -> Response {
4547 let mut response = Json(json!({
4548 "query_id": query_id.to_string(),
4549 "original_query_id": receipt.original_query_id,
4550 "status": receipt.status,
4551 "terminal_state": receipt.status,
4552 "server_state": receipt.server_state,
4553 "cancel_outcome": "already_finished",
4554 "cancellation_reason": receipt.cancellation_reason,
4555 "committed": receipt.outcome.committed,
4556 "committed_statements": receipt.outcome.committed_statements,
4557 "last_commit_epoch": receipt.outcome.last_commit_epoch,
4558 "last_commit_epoch_text": receipt.outcome.last_commit_epoch_text.as_deref(),
4559 "first_commit_statement_index": receipt.outcome.first_commit_statement_index,
4560 "last_commit_statement_index": receipt.outcome.last_commit_statement_index,
4561 "completed_statements": receipt.outcome.completed_statements,
4562 "statement_index": receipt.outcome.statement_index,
4563 "retryable": false,
4564 "idempotency_replayed": replayed,
4565 "idempotency_persisted": persisted,
4566 "idempotency_expires_at_ms": expires_at_ms,
4567 "outcome": receipt.outcome,
4568 "terminal_error": receipt.terminal_error,
4569 }))
4570 .into_response();
4571 response.headers_mut().insert(
4572 "idempotency-replayed",
4573 axum::http::HeaderValue::from_static(if replayed { "true" } else { "false" }),
4574 );
4575 response.headers_mut().insert(
4576 "idempotency-persisted",
4577 axum::http::HeaderValue::from_static(if persisted { "true" } else { "false" }),
4578 );
4579 if let Ok(value) = axum::http::HeaderValue::from_str(&receipt.original_query_id) {
4580 response
4581 .headers_mut()
4582 .insert("x-mongreldb-original-query-id", value);
4583 }
4584 with_query_id(response, query_id)
4585}
4586
4587fn terminal_server_error_response(
4588 state: &AppState,
4589 query_id: QueryId,
4590 http_status: StatusCode,
4591 base_code: &'static str,
4592 message: impl Into<String>,
4593) -> Response {
4594 let status = state.query_registry.status(query_id);
4595 let committed = status
4596 .as_ref()
4597 .is_some_and(|status| status.durable_outcome.committed);
4598 let code = if committed && base_code.starts_with("SERIALIZATION_") {
4599 "SERIALIZATION_FAILED_AFTER_COMMIT"
4600 } else {
4601 base_code
4602 };
4603 let response_status = status
4604 .as_ref()
4605 .and_then(mongreldb_query::QueryStatus::terminal_state)
4606 .map(terminal_state_name)
4607 .unwrap_or(if committed {
4608 "committed_with_error"
4609 } else {
4610 "failed_before_commit"
4611 });
4612 let outcome = query_outcome_json(status.as_ref());
4613 with_query_id(
4614 (
4615 http_status,
4616 Json(json!({
4617 "query_id": query_id.to_string(),
4618 "status": response_status,
4619 "terminal_state": response_status,
4620 "committed": committed,
4621 "committed_statements": status.as_ref().map_or(0, |status| status.durable_outcome.committed_statements),
4622 "last_commit_epoch": status.as_ref().and_then(|status| status.durable_outcome.last_commit_epoch),
4623 "last_commit_epoch_text": epoch_text(status.as_ref().and_then(|status| status.durable_outcome.last_commit_epoch)),
4624 "first_commit_statement_index": status.as_ref().and_then(|status| status.durable_outcome.first_commit_statement_index),
4625 "last_commit_statement_index": status.as_ref().and_then(|status| status.durable_outcome.last_commit_statement_index),
4626 "completed_statements": status.as_ref().map_or(0, |status| status.completed_statements),
4627 "statement_index": status.as_ref().map_or(0, |status| status.statement_index),
4628 "cancel_outcome": null,
4629 "cancellation_reason": status.as_ref().map(|status| cancellation_reason_name(status.cancellation_reason)),
4630 "retryable": false,
4631 "server_state": status.as_ref().map(|status| query_phase_name(status.phase)),
4632 "outcome": outcome,
4633 "error": {
4634 "code": code,
4635 "message": message.into(),
4636 "query_id": query_id.to_string(),
4637 "committed": committed,
4638 "retryable": false,
4639 }
4640 })),
4641 )
4642 .into_response(),
4643 query_id,
4644 )
4645}
4646
4647fn query_not_found_response(query_id: Option<QueryId>) -> Response {
4648 let mut response = (
4649 StatusCode::NOT_FOUND,
4650 Json(json!({
4651 "query_id": query_id.map(|value| value.to_string()),
4652 "status": "unknown",
4653 "terminal_state": null,
4654 "committed": null,
4655 "committed_statements": null,
4656 "last_commit_epoch": null,
4657 "last_commit_epoch_text": null,
4658 "first_commit_statement_index": null,
4659 "last_commit_statement_index": null,
4660 "completed_statements": null,
4661 "statement_index": null,
4662 "cancel_outcome": "not_found",
4663 "cancellation_reason": null,
4664 "retryable": false,
4665 "server_state": "not_found",
4666 "outcome": {
4667 "committed": null,
4668 "committed_statements": null,
4669 "last_commit_epoch": null,
4670 "last_commit_epoch_text": null,
4671 "first_commit_statement_index": null,
4672 "last_commit_statement_index": null,
4673 "completed_statements": null,
4674 "statement_index": null,
4675 "serialization": "unknown",
4676 },
4677 "error": {
4678 "code": "QUERY_NOT_FOUND",
4679 "message": "query not found",
4680 "query_id": query_id.map(|value| value.to_string()),
4681 "committed": null,
4682 "retryable": false,
4683 }
4684 })),
4685 )
4686 .into_response();
4687 if let Some(query_id) = query_id {
4688 add_query_id_header(&mut response, query_id);
4689 }
4690 response
4691}
4692
4693fn query_session_header(
4694 headers: &axum::http::HeaderMap,
4695 query_id: Option<QueryId>,
4696) -> std::result::Result<Option<String>, Box<Response>> {
4697 match headers.get("x-session-id") {
4698 Some(value) => match value.to_str() {
4699 Ok(value) if value.len() <= 256 => Ok(Some(value.to_owned())),
4700 _ => Err(Box::new(bad_query_control_request(
4701 "X-Session-ID must be valid text no longer than 256 bytes",
4702 query_id,
4703 ))),
4704 },
4705 None => Ok(None),
4706 }
4707}
4708
4709fn pre_cancelled_query_response(
4710 query_id: QueryId,
4711 reason: CancellationReason,
4712 status: StatusCode,
4713) -> Response {
4714 with_query_id(
4715 (
4716 status,
4717 Json(json!({
4718 "query_id": query_id.to_string(),
4719 "status": "cancelled_before_start",
4720 "terminal_state": "cancelled_before_start",
4721 "state": "pre_cancelled",
4722 "server_state": "pre_cancelled",
4723 "cancel_outcome": "pre_cancelled",
4724 "committed": false,
4725 "committed_statements": 0,
4726 "last_commit_epoch": null,
4727 "last_commit_epoch_text": null,
4728 "first_commit_statement_index": null,
4729 "last_commit_statement_index": null,
4730 "completed_statements": 0,
4731 "statement_index": 0,
4732 "cancellation_reason": cancellation_reason_name(reason),
4733 "outcome": {
4734 "committed": false,
4735 "committed_statements": 0,
4736 "last_commit_epoch": null,
4737 "last_commit_epoch_text": null,
4738 "first_commit_statement_index": null,
4739 "last_commit_statement_index": null,
4740 "completed_statements": 0,
4741 "statement_index": 0,
4742 "serialization": "not_started",
4743 },
4744 "terminal_error": {
4745 "code": "QUERY_CANCELLED",
4746 "category": "cancellation",
4747 },
4748 "retryable": false,
4749 })),
4750 )
4751 .into_response(),
4752 query_id,
4753 )
4754}
4755
4756fn compact_finished_query_response(status: &CompactFinishedQuery) -> Response {
4757 let query_id = status.query_id;
4758 let durable = &status.durable_outcome;
4759 let outcome_unknown =
4760 status.terminal_state == mongreldb_query::QueryTerminalState::OutcomeUnknown;
4761 let terminal_error = status.terminal_error.as_ref().map(|error| {
4762 json!({
4763 "code": error.code,
4764 "category": terminal_error_category_name(error.category),
4765 })
4766 });
4767 with_query_id(
4768 Json(json!({
4769 "detail": "compact",
4770 "query_id": query_id.to_string(),
4771 "status": terminal_state_name(status.terminal_state),
4772 "terminal_state": terminal_state_name(status.terminal_state),
4773 "state": query_phase_name(status.phase),
4774 "server_state": query_phase_name(status.phase),
4775 "cancel_outcome": "already_finished",
4776 "code": "QUERY_ALREADY_FINISHED",
4777 "committed": (!outcome_unknown).then_some(durable.committed),
4778 "committed_statements": (!outcome_unknown).then_some(durable.committed_statements),
4779 "last_commit_epoch": (!outcome_unknown).then_some(durable.last_commit_epoch).flatten(),
4780 "last_commit_epoch_text": (!outcome_unknown).then_some(epoch_text(durable.last_commit_epoch)).flatten(),
4781 "first_commit_statement_index": (!outcome_unknown).then_some(durable.first_commit_statement_index).flatten(),
4782 "last_commit_statement_index": (!outcome_unknown).then_some(durable.last_commit_statement_index).flatten(),
4783 "completed_statements": (!outcome_unknown).then_some(status.completed_statements),
4784 "statement_index": (!outcome_unknown).then_some(status.statement_index),
4785 "cancellation_reason": cancellation_reason_name(status.cancellation_reason),
4786 "outcome": {
4787 "committed": (!outcome_unknown).then_some(durable.committed),
4788 "committed_statements": (!outcome_unknown).then_some(durable.committed_statements),
4789 "last_commit_epoch": (!outcome_unknown).then_some(durable.last_commit_epoch).flatten(),
4790 "last_commit_epoch_text": (!outcome_unknown).then_some(epoch_text(durable.last_commit_epoch)).flatten(),
4791 "first_commit_statement_index": (!outcome_unknown).then_some(durable.first_commit_statement_index).flatten(),
4792 "last_commit_statement_index": (!outcome_unknown).then_some(durable.last_commit_statement_index).flatten(),
4793 "completed_statements": (!outcome_unknown).then_some(status.completed_statements),
4794 "statement_index": (!outcome_unknown).then_some(status.statement_index),
4795 "serialization": serialization_outcome_name(status.serialization_outcome),
4796 },
4797 "terminal_error": terminal_error,
4798 "retryable": terminal_error_retryable(status.terminal_error.as_ref()),
4799 }))
4800 .into_response(),
4801 query_id,
4802 )
4803}
4804
4805async fn query_status(
4806 State(state): State<Arc<AppState>>,
4807 OptionalPrincipal(principal): OptionalPrincipal,
4808 Path(query_id): Path<String>,
4809 headers: axum::http::HeaderMap,
4810) -> Response {
4811 let Ok(query_id) = query_id.parse::<QueryId>() else {
4812 return query_not_found_response(None);
4813 };
4814 if !request_identity_is_current(&state, &principal) {
4815 return query_not_found_response(Some(query_id));
4816 }
4817 let requested_session = match query_session_header(&headers, Some(query_id)) {
4818 Ok(session_id) => session_id,
4819 Err(response) => return *response,
4820 };
4821 let owner = request_owner(&state, &principal);
4822 let is_admin =
4823 current_request_principal(&state, &principal).is_some_and(|principal| principal.is_admin);
4824 let _lifecycle = state
4825 .query_lifecycle
4826 .lock()
4827 .unwrap_or_else(|error| error.into_inner());
4828 let Some(status) = state.query_registry.status(query_id) else {
4829 if let Some(finished) = state.query_registry.compact_finished_status(query_id) {
4830 if !caller_may_manage_query(&state, &principal, finished.owner.as_deref())
4831 || requested_session
4832 .as_deref()
4833 .is_some_and(|session| finished.session_id.as_deref() != Some(session))
4834 {
4835 return query_not_found_response(Some(query_id));
4836 }
4837 return compact_finished_query_response(&finished);
4838 }
4839 let reason = state
4840 .pre_cancellations
4841 .reason(query_id, &owner, requested_session.as_deref())
4842 .or_else(|| {
4843 is_admin.then(|| match requested_session.as_deref() {
4844 Some(session_id) => state
4845 .pre_cancellations
4846 .reason_for_query_in_session(query_id, session_id),
4847 None => state.pre_cancellations.reason_for_query(query_id),
4848 })?
4849 });
4850 if let Some(reason) = reason {
4851 return pre_cancelled_query_response(query_id, reason, StatusCode::OK);
4852 }
4853 return query_not_found_response(Some(query_id));
4854 };
4855 if !caller_may_manage_query(&state, &principal, status.owner.as_deref())
4856 || requested_session
4857 .as_deref()
4858 .is_some_and(|session| status.session_id.as_deref() != Some(session))
4859 {
4860 return query_not_found_response(Some(query_id));
4861 }
4862 let terminal_status = status.terminal_state().map(terminal_state_name);
4863 let terminal_error = status.terminal_error.as_ref().map(|error| {
4864 json!({
4865 "code": error.code,
4866 "category": terminal_error_category_name(error.category),
4867 })
4868 });
4869 let retryable = terminal_error_retryable(status.terminal_error.as_ref());
4870 let cancel_outcome = query_cancel_outcome(&status);
4871 let outcome = query_outcome_json(Some(&status));
4872 let response = Json(json!({
4873 "query_id": query_id.to_string(),
4874 "status": terminal_status.unwrap_or(if status.durable_outcome.committed {
4875 "committed"
4876 } else {
4877 "running"
4878 }),
4879 "terminal_state": terminal_status,
4880 "state": query_phase_name(status.phase),
4881 "server_state": query_phase_name(status.phase),
4882 "started_ms_ago": status.started_at.elapsed().as_millis(),
4883 "deadline_ms_remaining": status.deadline.map(|deadline| {
4884 deadline.saturating_duration_since(std::time::Instant::now()).as_millis()
4885 }),
4886 "session_id": status.session_id,
4887 "operation": status.operation,
4888 "committed": (!status.outcome_unknown).then_some(status.committed),
4889 "committed_statements": (!status.outcome_unknown).then_some(status.durable_outcome.committed_statements),
4890 "last_commit_epoch": (!status.outcome_unknown).then_some(status.durable_outcome.last_commit_epoch).flatten(),
4891 "last_commit_epoch_text": (!status.outcome_unknown).then_some(epoch_text(status.durable_outcome.last_commit_epoch)).flatten(),
4892 "first_commit_statement_index": (!status.outcome_unknown).then_some(status.durable_outcome.first_commit_statement_index).flatten(),
4893 "last_commit_statement_index": (!status.outcome_unknown).then_some(status.durable_outcome.last_commit_statement_index).flatten(),
4894 "cancellation_reason": cancellation_reason_name(status.cancellation_reason),
4895 "completed_statements": (!status.outcome_unknown).then_some(status.completed_statements),
4896 "statement_index": (!status.outcome_unknown).then_some(status.statement_index),
4897 "cancel_outcome": cancel_outcome,
4898 "retryable": retryable,
4899 "outcome": outcome,
4900 "terminal_error": terminal_error,
4901 "trace": {
4902 "queue_duration_us": status.queue_duration.as_micros(),
4903 "planning_duration_us": status.planning_duration.as_micros(),
4904 "execution_duration_us": status.execution_duration.as_micros(),
4905 "serialization_duration_us": status.serialization_duration.as_micros(),
4906 "cancel_requested_phase": status.cancel_requested_phase.map(query_phase_name),
4907 "cancel_observed_phase": status.cancel_observed_phase.map(query_phase_name),
4908 "commit_fence_outcome": commit_fence_outcome_name(status.commit_fence_outcome),
4909 },
4910 }))
4911 .into_response();
4912 with_query_id(response, query_id)
4913}
4914
4915async fn cancel_query(
4916 State(state): State<Arc<AppState>>,
4917 OptionalPrincipal(principal): OptionalPrincipal,
4918 Path(query_id): Path<String>,
4919 headers: axum::http::HeaderMap,
4920) -> Response {
4921 let Ok(query_id) = query_id.parse::<QueryId>() else {
4922 return query_not_found_response(None);
4923 };
4924 if !request_identity_is_current(&state, &principal) {
4925 return query_not_found_response(Some(query_id));
4926 }
4927 let requested_session = match query_session_header(&headers, Some(query_id)) {
4928 Ok(session_id) => session_id,
4929 Err(response) => return *response,
4930 };
4931 let owner = request_owner(&state, &principal);
4932 let _lifecycle = state
4933 .query_lifecycle
4934 .lock()
4935 .unwrap_or_else(|error| error.into_inner());
4936 state.metrics.inc_sql_cancel_requests();
4937 let Some(status) = state.query_registry.status(query_id) else {
4938 if let Some(finished) = state.query_registry.compact_finished_status(query_id) {
4939 if !caller_may_manage_query(&state, &principal, finished.owner.as_deref())
4940 || requested_session
4941 .as_deref()
4942 .is_some_and(|session| finished.session_id.as_deref() != Some(session))
4943 {
4944 return query_not_found_response(Some(query_id));
4945 }
4946 return compact_finished_query_response(&finished);
4947 }
4948 return match state.pre_cancellations.insert(
4949 query_id,
4950 &owner,
4951 requested_session.as_deref(),
4952 CancellationReason::ClientRequest,
4953 ) {
4954 Ok(()) => {
4955 state.metrics.inc_sql_commit_cancel_winner_cancel();
4956 pre_cancelled_query_response(
4957 query_id,
4958 CancellationReason::ClientRequest,
4959 StatusCode::ACCEPTED,
4960 )
4961 }
4962 Err(pre_cancel::InsertError::MetadataTooLarge) => bad_query_control_request(
4963 "query owner or session metadata exceeds 256 bytes",
4964 Some(query_id),
4965 ),
4966 Err(
4967 pre_cancel::InsertError::Full
4968 | pre_cancel::InsertError::OwnerLimit
4969 | pre_cancel::InsertError::RateLimited,
4970 ) => with_query_id(
4971 (
4972 StatusCode::TOO_MANY_REQUESTS,
4973 Json(json!({
4974 "query_id": query_id.to_string(),
4975 "status": "failed_before_commit",
4976 "terminal_state": "failed_before_commit",
4977 "server_state": "failed",
4978 "cancel_outcome": null,
4979 "cancellation_reason": null,
4980 "committed": false,
4981 "committed_statements": 0,
4982 "last_commit_epoch": null,
4983 "last_commit_epoch_text": null,
4984 "first_commit_statement_index": null,
4985 "last_commit_statement_index": null,
4986 "completed_statements": 0,
4987 "statement_index": 0,
4988 "retryable": true,
4989 "outcome": {
4990 "committed": false,
4991 "committed_statements": 0,
4992 "last_commit_epoch": null,
4993 "last_commit_epoch_text": null,
4994 "first_commit_statement_index": null,
4995 "last_commit_statement_index": null,
4996 "completed_statements": 0,
4997 "statement_index": 0,
4998 "serialization": "not_started",
4999 },
5000 "error": {
5001 "code": "QUERY_REGISTRY_FULL",
5002 "message": "pre-registration cancellation limit reached",
5003 "query_id": query_id.to_string(),
5004 "committed": false,
5005 "retryable": true,
5006 }
5007 })),
5008 )
5009 .into_response(),
5010 query_id,
5011 ),
5012 };
5013 };
5014 if !caller_may_manage_query(&state, &principal, status.owner.as_deref())
5015 || requested_session
5016 .as_deref()
5017 .is_some_and(|session| status.session_id.as_deref() != Some(session))
5018 {
5019 return query_not_found_response(Some(query_id));
5020 }
5021 let (http_status, mut body) = match state.query_registry.cancel(query_id) {
5022 CancelOutcome::Accepted => (
5023 {
5024 state.metrics.inc_sql_commit_cancel_winner_cancel();
5025 StatusCode::ACCEPTED
5026 },
5027 json!({
5028 "query_id": query_id.to_string(),
5029 "state": "cancellation_requested",
5030 "cancel_outcome": "accepted",
5031 }),
5032 ),
5033 CancelOutcome::AlreadyCancelling => (
5034 StatusCode::OK,
5035 json!({
5036 "query_id": query_id.to_string(),
5037 "state": "cancelling",
5038 "cancel_outcome": "already_cancelling",
5039 }),
5040 ),
5041 CancelOutcome::TooLate => (
5042 {
5043 state.metrics.inc_sql_commit_cancel_winner_commit();
5044 StatusCode::CONFLICT
5045 },
5046 json!({
5047 "query_id": query_id.to_string(),
5048 "state": "commit_critical",
5049 "cancel_outcome": "too_late",
5050 "committed": status.durable_outcome.committed,
5051 "outcome": query_outcome_json(Some(&status)),
5052 "retryable": false,
5053 "error": {
5054 "code": "CANCEL_TOO_LATE",
5055 "message": "the query has entered its durable commit phase",
5056 "committed": status.durable_outcome.committed,
5057 "retryable": false,
5058 }
5059 }),
5060 ),
5061 CancelOutcome::AlreadyFinished => (
5062 StatusCode::OK,
5063 json!({
5064 "query_id": query_id.to_string(),
5065 "state": "finished",
5066 "status": status.terminal_state().map(terminal_state_name),
5067 "cancel_outcome": "already_finished",
5068 "code": "QUERY_ALREADY_FINISHED",
5069 "committed": status.durable_outcome.committed,
5070 "outcome": query_outcome_json(Some(&status)),
5071 "retryable": false,
5072 }),
5073 ),
5074 CancelOutcome::NotFound => return query_not_found_response(Some(query_id)),
5075 };
5076 let status = state.query_registry.status(query_id).unwrap_or(status);
5077 let response_status = status.terminal_state().map(terminal_state_name).unwrap_or(
5078 if status.durable_outcome.committed {
5079 "committed"
5080 } else {
5081 "running"
5082 },
5083 );
5084 if let Some(body) = body.as_object_mut() {
5085 body.insert("status".into(), json!(response_status));
5086 body.insert(
5087 "terminal_state".into(),
5088 json!(status.terminal_state().map(terminal_state_name)),
5089 );
5090 body.insert(
5091 "committed".into(),
5092 json!((!status.outcome_unknown).then_some(status.durable_outcome.committed)),
5093 );
5094 body.insert(
5095 "committed_statements".into(),
5096 json!((!status.outcome_unknown).then_some(status.durable_outcome.committed_statements)),
5097 );
5098 body.insert(
5099 "last_commit_epoch".into(),
5100 json!((!status.outcome_unknown)
5101 .then_some(status.durable_outcome.last_commit_epoch)
5102 .flatten()),
5103 );
5104 body.insert(
5105 "last_commit_epoch_text".into(),
5106 json!((!status.outcome_unknown)
5107 .then_some(epoch_text(status.durable_outcome.last_commit_epoch))
5108 .flatten()),
5109 );
5110 body.insert(
5111 "first_commit_statement_index".into(),
5112 json!((!status.outcome_unknown)
5113 .then_some(status.durable_outcome.first_commit_statement_index)
5114 .flatten()),
5115 );
5116 body.insert(
5117 "last_commit_statement_index".into(),
5118 json!((!status.outcome_unknown)
5119 .then_some(status.durable_outcome.last_commit_statement_index)
5120 .flatten()),
5121 );
5122 body.insert(
5123 "completed_statements".into(),
5124 json!((!status.outcome_unknown).then_some(status.completed_statements)),
5125 );
5126 body.insert(
5127 "statement_index".into(),
5128 json!((!status.outcome_unknown).then_some(status.statement_index)),
5129 );
5130 body.insert(
5131 "cancellation_reason".into(),
5132 json!(cancellation_reason_name(status.cancellation_reason)),
5133 );
5134 body.insert("retryable".into(), json!(false));
5135 body.insert("server_state".into(), json!(query_phase_name(status.phase)));
5136 body.insert("outcome".into(), query_outcome_json(Some(&status)));
5137 }
5138 with_query_id((http_status, Json(body)).into_response(), query_id)
5139}
5140
5141#[derive(Deserialize)]
5142#[serde(deny_unknown_fields)]
5143struct SqlContinuationRequest {
5144 cursor: String,
5145 #[serde(default)]
5146 operation_id: Option<QueryId>,
5147 #[serde(default)]
5148 timeout_ms: Option<u64>,
5149}
5150
5151fn register_page_operation(
5152 state: &AppState,
5153 options: SqlQueryOptions,
5154) -> mongreldb_query::Result<RegisteredSqlQuery> {
5155 let query_id = options.query_id.ok_or_else(|| {
5156 mongreldb_query::MongrelQueryError::InvalidQueryState(
5157 "page operation registration requires an operation id".into(),
5158 )
5159 })?;
5160 let owner = options.owner.clone().unwrap_or_default();
5161 let session_id = options.session_id.clone();
5162 let _lifecycle = state
5163 .query_lifecycle
5164 .lock()
5165 .unwrap_or_else(|error| error.into_inner());
5166 let reason = match state.pre_cancellations.lookup_for_registration(
5167 query_id,
5168 &owner,
5169 session_id.as_deref(),
5170 ) {
5171 pre_cancel::RegistrationLookup::NoReservation => None,
5172 pre_cancel::RegistrationLookup::Matching(reason) => Some(reason),
5173 pre_cancel::RegistrationLookup::ReservedByAnotherIdentity => {
5174 return Err(mongreldb_query::MongrelQueryError::QueryIdConflict { query_id });
5175 }
5176 };
5177 let query = state.query_registry.register(options)?;
5178 if let Some(reason) = reason {
5179 state
5180 .pre_cancellations
5181 .take(query_id, &owner, session_id.as_deref());
5182 query.request_cancel(reason);
5183 let error = cancellation_checkpoint_error(&query);
5184 query.fail();
5185 return Err(error);
5186 }
5187 Ok(query)
5188}
5189
5190async fn continue_sql_page(
5191 State(state): State<Arc<AppState>>,
5192 OptionalPrincipal(principal): OptionalPrincipal,
5193 headers: axum::http::HeaderMap,
5194 Json(request): Json<SqlContinuationRequest>,
5195) -> Response {
5196 let query_id = match request.operation_id {
5197 Some(query_id) => query_id,
5198 None => match QueryId::random() {
5199 Ok(query_id) => query_id,
5200 Err(error) => return query_error_response(&error, None),
5201 },
5202 };
5203 if !request_identity_is_current(&state, &principal) {
5204 return with_query_id(
5205 sql_cursor_error_response(
5206 StatusCode::NOT_FOUND,
5207 "SQL_CURSOR_NOT_FOUND",
5208 "SQL continuation result is unavailable",
5209 query_id,
5210 ),
5211 query_id,
5212 );
5213 }
5214 let owner = request_owner(&state, &principal);
5215 let session_id = match query_session_header(&headers, Some(query_id)) {
5216 Ok(session_id) => session_id,
5217 Err(response) => return *response,
5218 };
5219 let timeout_ms = request.timeout_ms.unwrap_or_else(|| {
5220 state
5221 .sql_page_default_timeout
5222 .as_millis()
5223 .min(u128::from(u64::MAX)) as u64
5224 });
5225 if timeout_ms == 0 || Duration::from_millis(timeout_ms) > state.sql_page_max_timeout {
5226 return bad_query_control_request(
5227 format!(
5228 "timeout_ms must be positive and no greater than {}",
5229 state.sql_page_max_timeout.as_millis()
5230 ),
5231 Some(query_id),
5232 );
5233 }
5234 let query = match register_page_operation(
5235 &state,
5236 SqlQueryOptions {
5237 query_id: Some(query_id),
5238 timeout: Some(Duration::from_millis(timeout_ms)),
5239 owner: Some(owner.clone()),
5240 session_id,
5241 parent_control: None,
5242 },
5243 ) {
5244 Ok(query) => query,
5245 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
5246 };
5247 query.set_sql_metadata("CONTINUE SQL PAGE");
5248 let _permit = match tokio::select! {
5249 permit = Arc::clone(&state.sql_page_semaphore).acquire_owned() => permit.map_err(|_| {
5250 mongreldb_query::MongrelQueryError::InvalidQueryState(
5251 "SQL page admission semaphore closed".into(),
5252 )
5253 }),
5254 _ = query.control().cancelled() => Err(cancellation_checkpoint_error(&query)),
5255 } {
5256 Ok(permit) => permit,
5257 Err(error) => {
5258 query.fail();
5259 return tracked_query_error_response(&state, &error, Some(query_id));
5260 }
5261 };
5262 if let Err(error) = query.transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing) {
5263 query.fail();
5264 return tracked_query_error_response(&state, &error, Some(query_id));
5265 }
5266 let fail = |status, code, message| {
5267 query.record_terminal_error(code, mongreldb_query::QueryTerminalErrorCategory::Execution);
5268 query.fail();
5269 with_query_id(
5270 sql_cursor_error_response(status, code, message, query_id),
5271 query_id,
5272 )
5273 };
5274 if request.cursor.is_empty() || request.cursor.len() > 2_048 {
5275 return fail(
5276 StatusCode::BAD_REQUEST,
5277 "INVALID_SQL_CURSOR",
5278 "invalid SQL continuation cursor",
5279 );
5280 }
5281 if let Err(error) = query.checkpoint() {
5282 query.fail();
5283 return tracked_query_error_response(&state, &error, Some(query_id));
5284 }
5285 let cursor_mac_key = match state.cursor_mac_key.get() {
5286 Ok(key) => key,
5287 Err(_) => {
5288 return fail(
5289 StatusCode::INTERNAL_SERVER_ERROR,
5290 "ENTROPY_UNAVAILABLE",
5291 "OS CSPRNG unavailable",
5292 );
5293 }
5294 };
5295 match state.sql_pages.continue_page_with_control(
5296 &request.cursor,
5297 &owner,
5298 &cursor_mac_key,
5299 sql_pages::SqlPageBinding {
5300 security_version: state.db.security_version(),
5301 catalog_epoch: state.db.catalog_snapshot().db_epoch,
5302 },
5303 &query,
5304 ) {
5305 Ok(page) => {
5306 let page_byte_count = page.byte_count;
5307 if let Err(error) = query.begin_serialization() {
5308 query.fail();
5309 return tracked_query_error_response(&state, &error, Some(query_id));
5310 }
5311 let serialization_query = query.clone();
5312 match tokio::task::spawn_blocking(move || {
5313 serialize_sql_page_controlled(page, &serialization_query)
5314 })
5315 .await
5316 {
5317 Ok(Ok(body)) => {
5318 if let Err(error) = query.try_complete() {
5319 return tracked_query_error_response(&state, &error, Some(query_id));
5320 }
5321 state.metrics.add_sql_output_bytes(page_byte_count);
5322 with_query_id(sql_page_response(body), query_id)
5323 }
5324 Ok(Err(ControlledPageSerializationError::Query(error))) => {
5325 query.fail();
5326 tracked_query_error_response(&state, &error, Some(query_id))
5327 }
5328 Ok(Err(ControlledPageSerializationError::Encoding)) => fail(
5329 StatusCode::INTERNAL_SERVER_ERROR,
5330 "SERIALIZATION_FAILED",
5331 "failed to serialize SQL continuation page",
5332 ),
5333 Err(_) => fail(
5334 StatusCode::INTERNAL_SERVER_ERROR,
5335 "SERIALIZATION_WORKER_FAILED",
5336 "SQL continuation serialization worker failed",
5337 ),
5338 }
5339 }
5340 Err(sql_pages::CursorError::Cancelled) => {
5341 let error = cancellation_checkpoint_error(&query);
5342 query.fail();
5343 tracked_query_error_response(&state, &error, Some(query_id))
5344 }
5345 Err(sql_pages::CursorError::Invalid) => fail(
5346 StatusCode::BAD_REQUEST,
5347 "INVALID_SQL_CURSOR",
5348 "invalid SQL continuation cursor",
5349 ),
5350 Err(sql_pages::CursorError::Expired) => fail(
5351 StatusCode::GONE,
5352 "SQL_CURSOR_EXPIRED",
5353 "SQL continuation cursor expired",
5354 ),
5355 Err(sql_pages::CursorError::NotFound) => fail(
5356 StatusCode::NOT_FOUND,
5357 "SQL_CURSOR_NOT_FOUND",
5358 "SQL continuation result is unavailable",
5359 ),
5360 Err(sql_pages::CursorError::PageLimit) => fail(
5361 StatusCode::PAYLOAD_TOO_LARGE,
5362 "RESULT_LIMIT_EXCEEDED",
5363 "one projected row exceeds the page byte or token limit",
5364 ),
5365 }
5366}
5367
5368fn sql_cursor_error_response(
5369 status: StatusCode,
5370 code: &'static str,
5371 message: &'static str,
5372 query_id: QueryId,
5373) -> Response {
5374 (
5375 status,
5376 Json(json!({
5377 "query_id": query_id.to_string(),
5378 "status": "failed_before_commit",
5379 "terminal_state": "failed_before_commit",
5380 "server_state": "failed",
5381 "committed": false,
5382 "committed_statements": 0,
5383 "last_commit_epoch": null,
5384 "last_commit_epoch_text": null,
5385 "first_commit_statement_index": null,
5386 "last_commit_statement_index": null,
5387 "completed_statements": 0,
5388 "statement_index": 0,
5389 "cancel_outcome": "already_finished",
5390 "cancellation_reason": "none",
5391 "retryable": false,
5392 "outcome": {
5393 "committed": false,
5394 "committed_statements": 0,
5395 "last_commit_epoch": null,
5396 "last_commit_epoch_text": null,
5397 "first_commit_statement_index": null,
5398 "last_commit_statement_index": null,
5399 "completed_statements": 0,
5400 "statement_index": 0,
5401 "serialization": "not_started",
5402 },
5403 "error": {
5404 "code": code,
5405 "message": message,
5406 "query_id": query_id.to_string(),
5407 "committed": false,
5408 "retryable": false,
5409 }
5410 })),
5411 )
5412 .into_response()
5413}
5414
5415async fn sql(
5416 State(state): State<Arc<AppState>>,
5417 OptionalPrincipal(principal): OptionalPrincipal,
5418 headers: axum::http::HeaderMap,
5419 Json(req): Json<SqlRequest>,
5420) -> Response {
5421 if !state.accepting_sql.load(Ordering::Acquire) {
5422 return (StatusCode::SERVICE_UNAVAILABLE, "server is shutting down").into_response();
5423 }
5424 if !request_identity_is_current(&state, &principal) {
5425 return StatusCode::UNAUTHORIZED.into_response();
5426 }
5427 let session_id = match query_session_header(&headers, None) {
5432 Ok(session_id) => session_id,
5433 Err(response) => return *response,
5434 };
5435
5436 let owner = request_owner(&state, &principal);
5437 if let Some(sid) = session_id {
5438 let Some(entry) = state.sessions.get(&sid, &owner) else {
5439 return (
5440 StatusCode::NOT_FOUND,
5441 "session not found or not owned by caller",
5442 )
5443 .into_response();
5444 };
5445 let (options, query_id) = match resolve_query_options(
5446 &state,
5447 &headers,
5448 req.query_id,
5449 req.timeout_ms,
5450 owner.clone(),
5451 Some(sid.clone()),
5452 ) {
5453 Ok(options) => options,
5454 Err(response) => return *response,
5455 };
5456 let query = match register_controlled_query(&state, &entry.session, options) {
5457 Ok(query) => query,
5458 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
5459 };
5460 let registration = RegisteredQueryGuard::new(query);
5461 if mongreldb_query::contains_boolean_ai_predicate(&req.sql) {
5462 registration.fail();
5463 return with_query_id(remote_boolean_ai_error(), query_id);
5464 }
5465 let output_limits = match resolve_sql_output_limits(&state, &req, query_id) {
5466 Ok(limits) => limits,
5467 Err(response) => {
5468 registration.fail();
5469 return *response;
5470 }
5471 };
5472 let (registration, pagination) =
5473 match resolve_sql_pagination(&headers, &req, output_limits, registration, query_id) {
5474 Ok(resolved) => resolved,
5475 Err(response) => return *response,
5476 };
5477 let sql_permit =
5478 match acquire_sql_permit(&state, &entry.session, registration.query()).await {
5479 Ok(permit) => permit,
5480 Err(error) => {
5481 return tracked_query_error_response(&state, &error, Some(query_id));
5482 }
5483 };
5484 let _guard = tokio::select! {
5486 guard = entry.lock.lock() => guard,
5487 _ = registration.query().control().cancelled() => {
5488 return tracked_query_error_response(
5489 &state,
5490 &cancellation_checkpoint_error(registration.query()),
5491 Some(query_id),
5492 );
5493 }
5494 };
5495 if entry.is_closed() {
5498 return (StatusCode::NOT_FOUND, "session no longer available").into_response();
5499 }
5500 if req.idempotency_key.is_some() || headers.contains_key("idempotency-key") {
5501 entry
5502 .session
5503 .fire_test_hook(mongreldb_query::SqlTestHookPoint::BeforeServerIdempotencyCheck);
5504 }
5505 let (registration, idempotency) = match begin_sql_idempotency(
5506 &state,
5507 SqlIdempotencyContext {
5508 headers: &headers,
5509 request: &req,
5510 output_limits,
5511 owner: &owner,
5512 session_id: Some(&sid),
5513 session_in_transaction: entry.session.staged_sql_operation_count().is_some(),
5514 query_id,
5515 },
5516 registration,
5517 )
5518 .await
5519 {
5520 Ok(resolved) => resolved,
5521 Err(response) => return response,
5522 };
5523 entry.touch();
5524 let query = registration.into_query();
5525 execute_sql(
5526 &state,
5527 &principal,
5528 &entry.session,
5529 ResolvedSqlRequest {
5530 request: req,
5531 output_limits,
5532 idempotency,
5533 pagination,
5534 },
5535 query,
5536 query_id,
5537 sql_permit,
5538 )
5539 .await
5540 } else {
5541 let session = match MongrelSession::open_with_external_modules_as(
5542 Arc::clone(&state.db),
5543 state.external_modules.iter().cloned(),
5544 request_principal(&state, &principal),
5545 ) {
5546 Ok(session) => session.with_query_registry(Arc::clone(&state.query_registry)),
5547 Err(e) => return (status_for_query_error(&e), e.to_string()).into_response(),
5548 };
5549 let (options, query_id) = match resolve_query_options(
5550 &state,
5551 &headers,
5552 req.query_id,
5553 req.timeout_ms,
5554 owner.clone(),
5555 None,
5556 ) {
5557 Ok(options) => options,
5558 Err(response) => return *response,
5559 };
5560 let query = match register_controlled_query(&state, &session, options) {
5561 Ok(query) => query,
5562 Err(error) => return tracked_query_error_response(&state, &error, Some(query_id)),
5563 };
5564 let registration = RegisteredQueryGuard::new(query);
5565 if mongreldb_query::contains_boolean_ai_predicate(&req.sql) {
5566 registration.fail();
5567 return with_query_id(remote_boolean_ai_error(), query_id);
5568 }
5569 let output_limits = match resolve_sql_output_limits(&state, &req, query_id) {
5570 Ok(limits) => limits,
5571 Err(response) => {
5572 registration.fail();
5573 return *response;
5574 }
5575 };
5576 let (registration, pagination) =
5577 match resolve_sql_pagination(&headers, &req, output_limits, registration, query_id) {
5578 Ok(resolved) => resolved,
5579 Err(response) => return *response,
5580 };
5581 let (registration, idempotency) = match begin_sql_idempotency(
5582 &state,
5583 SqlIdempotencyContext {
5584 headers: &headers,
5585 request: &req,
5586 output_limits,
5587 owner: &owner,
5588 session_id: None,
5589 session_in_transaction: false,
5590 query_id,
5591 },
5592 registration,
5593 )
5594 .await
5595 {
5596 Ok(resolved) => resolved,
5597 Err(response) => return response,
5598 };
5599 let sql_permit = match acquire_sql_permit(&state, &session, registration.query()).await {
5600 Ok(permit) => permit,
5601 Err(error) => {
5602 if let Some(idempotency) = idempotency {
5603 idempotency.abort();
5604 }
5605 return tracked_query_error_response(&state, &error, Some(query_id));
5606 }
5607 };
5608 let query = registration.into_query();
5609 execute_sql(
5610 &state,
5611 &principal,
5612 &session,
5613 ResolvedSqlRequest {
5614 request: req,
5615 output_limits,
5616 idempotency,
5617 pagination,
5618 },
5619 query,
5620 query_id,
5621 sql_permit,
5622 )
5623 .await
5624 }
5625}
5626
5627async fn execute_sql(
5632 state: &AppState,
5633 principal: &Option<mongreldb_core::Principal>,
5634 session: &MongrelSession,
5635 request: ResolvedSqlRequest,
5636 query: RegisteredSqlQuery,
5637 query_id: QueryId,
5638 sql_permit: tokio::sync::OwnedSemaphorePermit,
5639) -> Response {
5640 let ResolvedSqlRequest {
5641 request: req,
5642 output_limits,
5643 idempotency,
5644 pagination,
5645 } = request;
5646 let idempotency_query = idempotency.as_ref().map(|_| query.clone());
5649 state.metrics.inc_sql_queries();
5650 let audited = audit::is_audited_sql(&req.sql);
5651 let actor = request_owner(state, principal);
5652 let page_binding = sql_pages::SqlPageBinding {
5653 security_version: state.db.security_version(),
5654 catalog_epoch: state.db.catalog_snapshot().db_epoch,
5655 };
5656 let start = std::time::Instant::now();
5657 let result = if let Some(pagination) = pagination {
5663 match session
5664 .run_with_query_for_serialization_with_limits(
5665 &req.sql,
5666 query,
5667 mongreldb_query::SqlCollectionLimits::new(output_limits.0, output_limits.1),
5668 )
5669 .await
5670 {
5671 Ok(output) => Ok(dispatch_paginated_sql(
5672 state,
5673 output,
5674 query_id,
5675 &actor,
5676 pagination,
5677 output_limits,
5678 session.sql_test_hook(),
5679 page_binding,
5680 )
5681 .await),
5682 Err(error) => Err(error),
5683 }
5684 } else if req.format.as_deref() == Some("arrow-stream") {
5685 match session
5686 .run_stream_with_query_for_serialization(&req.sql, query)
5687 .await
5688 {
5689 Ok((stream, completion)) => Ok(sql_arrow_stream_response_controlled(
5690 stream,
5691 completion,
5692 sql_permit,
5693 output_limits,
5694 state,
5695 query_id,
5696 session.sql_test_hook(),
5697 )),
5698 Err(error) => Err(error),
5699 }
5700 } else {
5701 match session
5702 .run_with_query_for_serialization_with_limits(
5703 &req.sql,
5704 query,
5705 mongreldb_query::SqlCollectionLimits::new(output_limits.0, output_limits.1),
5706 )
5707 .await
5708 {
5709 Ok(output) => Ok(dispatch_buffered_sql_format(
5710 state,
5711 req.format.as_deref(),
5712 output,
5713 query_id,
5714 session.sql_test_hook(),
5715 output_limits,
5716 )
5717 .await),
5718 Err(error) => Err(error),
5719 }
5720 };
5721 let elapsed = start.elapsed();
5722 if elapsed >= state.slow_query_threshold {
5725 state.metrics.inc_slow_queries();
5726 eprintln!(
5727 "[slow-query] {}\u{00b5}s query_id={} operation={}",
5728 elapsed.as_micros(),
5729 query_id,
5730 safe_sql_operation(&req.sql)
5731 );
5732 }
5733 if audited {
5736 let (action, detail) = audit::redacted_ddl_detail(&req.sql, result.is_ok());
5737 state.audit.record(actor, action, detail);
5738 }
5739 let response = match result {
5740 Ok(response) => with_query_id(response, query_id),
5741 Err(e) => {
5742 state.metrics.inc_sql_errors();
5743 tracked_query_error_response(state, &e, Some(query_id))
5744 }
5745 };
5746 let Some(idempotency) = idempotency else {
5747 return response;
5748 };
5749 let status = idempotency_query.map(|query| query.status());
5750 if let Some(receipt) = status.as_ref().and_then(sql_terminal_idempotency_receipt) {
5751 let (expires_at_ms, persisted) = idempotency.commit(receipt.clone());
5752 return sql_idempotency_receipt_response(
5753 query_id,
5754 &receipt,
5755 false,
5756 expires_at_ms,
5757 persisted,
5758 );
5759 }
5760 if status.as_ref().is_some_and(can_abort_idempotency_intent) {
5761 idempotency.abort();
5762 }
5763 response
5764}
5765
5766fn can_abort_idempotency_intent(status: &mongreldb_query::QueryStatus) -> bool {
5767 !status.outcome_unknown
5768 && !status.durable_outcome.committed
5769 && matches!(
5770 status.terminal_state(),
5771 Some(
5772 mongreldb_query::QueryTerminalState::FailedBeforeCommit
5773 | mongreldb_query::QueryTerminalState::CancelledBeforeCommit
5774 | mongreldb_query::QueryTerminalState::DeadlineBeforeCommit
5775 )
5776 )
5777}
5778
5779fn safe_sql_operation(sql: &str) -> String {
5780 sql.split_whitespace()
5781 .next()
5782 .unwrap_or("UNKNOWN")
5783 .chars()
5784 .filter(|character| character.is_ascii_alphabetic())
5785 .take(16)
5786 .collect::<String>()
5787 .to_ascii_uppercase()
5788}
5789
5790fn remote_boolean_ai_error() -> Response {
5791 (
5792 StatusCode::BAD_REQUEST,
5793 "Boolean ANN/Sparse SQL is disabled remotely; use scored SQL functions",
5794 )
5795 .into_response()
5796}
5797
5798#[derive(Debug)]
5799struct SerializedOutput {
5800 bytes: Vec<u8>,
5801 arrow: bool,
5802}
5803
5804#[derive(Debug)]
5805enum BufferedSerializationError {
5806 Query(mongreldb_query::MongrelQueryError),
5807 Limit(String),
5808 Encoding(String),
5809}
5810
5811struct LimitedOutput {
5812 bytes: Vec<u8>,
5813 max_bytes: usize,
5814 exceeded: bool,
5815}
5816
5817impl LimitedOutput {
5818 fn new(max_bytes: usize) -> Self {
5819 Self {
5820 bytes: Vec::new(),
5821 max_bytes,
5822 exceeded: false,
5823 }
5824 }
5825}
5826
5827impl std::io::Write for LimitedOutput {
5828 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
5829 if self.bytes.len().saturating_add(bytes.len()) > self.max_bytes {
5830 self.exceeded = true;
5831 return Err(std::io::Error::other("SQL output byte limit exceeded"));
5832 }
5833 self.bytes.extend_from_slice(bytes);
5834 Ok(bytes.len())
5835 }
5836
5837 fn flush(&mut self) -> std::io::Result<()> {
5838 Ok(())
5839 }
5840}
5841
5842fn serialize_buffered_output(
5843 format: &str,
5844 batches: &[arrow::record_batch::RecordBatch],
5845 query: &RegisteredSqlQuery,
5846 max_rows: usize,
5847 max_bytes: usize,
5848 test_hook: Option<&mongreldb_query::SqlTestHook>,
5849) -> std::result::Result<SerializedOutput, BufferedSerializationError> {
5850 const ROW_CHECKPOINT_INTERVAL: usize = 256;
5851 let mut rows = 0usize;
5852 let mut writer_output = LimitedOutput::new(max_bytes);
5853
5854 if format == "arrow" {
5855 if batches.is_empty() {
5856 if let Some(hook) = test_hook {
5857 hook(mongreldb_query::SqlTestHookPoint::AfterSerialization);
5858 }
5859 return Ok(SerializedOutput {
5860 bytes: Vec::new(),
5861 arrow: true,
5862 });
5863 }
5864 let schema = batches[0].schema();
5865 let encoding_result = (|| {
5866 let mut writer =
5867 arrow::ipc::writer::FileWriter::try_new(&mut writer_output, schema.as_ref())
5868 .map_err(|error| error.to_string())?;
5869 for batch in batches {
5870 for offset in (0..batch.num_rows()).step_by(ROW_CHECKPOINT_INTERVAL) {
5871 if let Some(hook) = test_hook {
5872 hook(mongreldb_query::SqlTestHookPoint::BeforeSerializationBatch);
5873 }
5874 query.checkpoint().map_err(|error| error.to_string())?;
5875 let length = ROW_CHECKPOINT_INTERVAL.min(batch.num_rows() - offset);
5876 rows = rows.saturating_add(length);
5877 if rows > max_rows {
5878 return Err("SQL output row limit exceeded".into());
5879 }
5880 writer
5881 .write(&batch.slice(offset, length))
5882 .map_err(|error| error.to_string())?;
5883 }
5884 }
5885 writer.finish().map_err(|error| error.to_string())
5886 })();
5887 if let Err(error) = encoding_result {
5888 if let Err(query_error) = query.checkpoint() {
5889 return Err(BufferedSerializationError::Query(query_error));
5890 }
5891 if writer_output.exceeded || rows > max_rows {
5892 return Err(BufferedSerializationError::Limit(error));
5893 }
5894 return Err(BufferedSerializationError::Encoding(error));
5895 }
5896 if let Some(hook) = test_hook {
5897 hook(mongreldb_query::SqlTestHookPoint::AfterSerialization);
5898 }
5899 return Ok(SerializedOutput {
5900 bytes: writer_output.bytes,
5901 arrow: true,
5902 });
5903 }
5904
5905 let encoding_result = (|| {
5906 let mut writer = arrow::json::writer::ArrayWriter::new(&mut writer_output);
5907 for batch in batches {
5908 for offset in (0..batch.num_rows()).step_by(ROW_CHECKPOINT_INTERVAL) {
5909 if let Some(hook) = test_hook {
5910 hook(mongreldb_query::SqlTestHookPoint::BeforeSerializationBatch);
5911 }
5912 query.checkpoint().map_err(|error| error.to_string())?;
5913 let length = ROW_CHECKPOINT_INTERVAL.min(batch.num_rows() - offset);
5914 rows = rows.saturating_add(length);
5915 if rows > max_rows {
5916 return Err("SQL output row limit exceeded".into());
5917 }
5918 let slice = batch.slice(offset, length);
5919 writer
5920 .write_batches(&[&slice])
5921 .map_err(|error| error.to_string())?;
5922 }
5923 }
5924 writer.finish().map_err(|error| error.to_string())
5925 })();
5926 if let Err(error) = encoding_result {
5927 if let Err(query_error) = query.checkpoint() {
5928 return Err(BufferedSerializationError::Query(query_error));
5929 }
5930 if writer_output.exceeded || rows > max_rows {
5931 return Err(BufferedSerializationError::Limit(error));
5932 }
5933 return Err(BufferedSerializationError::Encoding(error));
5934 }
5935 if let Some(hook) = test_hook {
5936 hook(mongreldb_query::SqlTestHookPoint::AfterSerialization);
5937 }
5938 Ok(SerializedOutput {
5939 bytes: writer_output.bytes,
5940 arrow: false,
5941 })
5942}
5943
5944#[cfg(test)]
5947fn sql_arrow_stream_response(batches: mongreldb_query::MongrelRecordBatchStream) -> Response {
5948 use futures::{stream, StreamExt};
5949
5950 const STREAM_CT: &str = "application/vnd.apache.arrow.stream";
5951
5952 let schema = batches.schema();
5953 let mut writer = match arrow::ipc::writer::StreamWriter::try_new(Vec::new(), schema.as_ref()) {
5954 Ok(w) => w,
5955 Err(e) => {
5956 return (
5957 StatusCode::INTERNAL_SERVER_ERROR,
5958 format!("arrow stream init error: {e}"),
5959 )
5960 .into_response()
5961 }
5962 };
5963 let schema_chunk: Vec<u8> = std::mem::take(writer.get_mut());
5966 let batch_stream = stream::unfold(
5967 (batches, Some(writer)),
5968 |(mut batches, writer)| async move {
5969 let mut writer = writer?;
5970 match batches.next().await {
5971 Some(Ok(batch)) => match writer.write(&batch) {
5972 Ok(()) => {
5973 let chunk = std::mem::take(writer.get_mut());
5974 Some((Ok(chunk), (batches, Some(writer))))
5975 }
5976 Err(error) => Some((Err(std::io::Error::other(error)), (batches, None))),
5977 },
5978 Some(Err(error)) => Some((Err(std::io::Error::other(error)), (batches, None))),
5979 None => match writer.finish() {
5980 Ok(()) => {
5981 let chunk = std::mem::take(writer.get_mut());
5982 Some((Ok(chunk), (batches, None)))
5983 }
5984 Err(error) => Some((Err(std::io::Error::other(error)), (batches, None))),
5985 },
5986 }
5987 },
5988 );
5989
5990 let schema_item: Result<Vec<u8>, std::io::Error> = Ok(schema_chunk);
5993 let full = stream::iter([schema_item]).chain(batch_stream);
5994 let body = axum::body::Body::from_stream(full);
5995 ([(header::CONTENT_TYPE, STREAM_CT)], body).into_response()
5996}
5997
5998fn sql_arrow_stream_response_controlled(
5999 batches: mongreldb_query::MongrelRecordBatchStream,
6000 completion: SqlStreamCompletion,
6001 sql_permit: tokio::sync::OwnedSemaphorePermit,
6002 limits: (usize, usize),
6003 state: &AppState,
6004 query_id: QueryId,
6005 test_hook: Option<mongreldb_query::SqlTestHook>,
6006) -> Response {
6007 use futures::{stream, StreamExt};
6008
6009 const STREAM_CT: &str = "application/vnd.apache.arrow.stream";
6010 let (max_rows, max_bytes) = limits;
6011 let schema = batches.schema();
6012 let mut writer = match arrow::ipc::writer::StreamWriter::try_new(Vec::new(), schema.as_ref()) {
6013 Ok(writer) => writer,
6014 Err(error) => {
6015 completion.fail_serialization();
6016 drop(batches);
6017 return terminal_server_error_response(
6018 state,
6019 query_id,
6020 StatusCode::INTERNAL_SERVER_ERROR,
6021 "SERIALIZATION_FAILED",
6022 format!("arrow stream init error: {error}"),
6023 );
6024 }
6025 };
6026 let schema_chunk = std::mem::take(writer.get_mut());
6027 if schema_chunk.len() > max_bytes {
6028 completion.fail_result_limit();
6029 drop(batches);
6030 return terminal_server_error_response(
6031 state,
6032 query_id,
6033 StatusCode::PAYLOAD_TOO_LARGE,
6034 "RESULT_LIMIT_EXCEEDED",
6035 "SQL output byte limit exceeded",
6036 );
6037 }
6038 let metrics = Arc::clone(&state.metrics);
6039 metrics.add_sql_output_bytes(schema_chunk.len());
6040 let batch_stream = stream::unfold(
6041 (
6042 batches,
6043 Some(writer),
6044 completion,
6045 Some(sql_permit),
6046 0usize,
6047 schema_chunk.len(),
6048 metrics,
6049 ),
6050 move |(mut batches, writer, completion, permit, rows, bytes, metrics)| {
6051 let test_hook = test_hook.clone();
6052 async move {
6053 let mut writer = writer?;
6054 match batches.next().await {
6055 Some(Ok(batch)) => {
6056 let next_rows = rows.saturating_add(batch.num_rows());
6057 if next_rows > max_rows {
6058 completion.fail_result_limit();
6059 return Some((
6060 Err(std::io::Error::other("SQL output row limit exceeded")),
6061 (batches, None, completion, permit, next_rows, bytes, metrics),
6062 ));
6063 }
6064 match writer.write(&batch) {
6065 Ok(()) => {
6066 let chunk = std::mem::take(writer.get_mut());
6067 let next_bytes = bytes.saturating_add(chunk.len());
6068 if next_bytes > max_bytes {
6069 completion.fail_result_limit();
6070 return Some((
6071 Err(std::io::Error::other(
6072 "SQL output byte limit exceeded",
6073 )),
6074 (
6075 batches, None, completion, permit, next_rows,
6076 next_bytes, metrics,
6077 ),
6078 ));
6079 }
6080 metrics.add_sql_output_bytes(chunk.len());
6081 Some((
6082 Ok(chunk),
6083 (
6084 batches,
6085 Some(writer),
6086 completion,
6087 permit,
6088 next_rows,
6089 next_bytes,
6090 metrics,
6091 ),
6092 ))
6093 }
6094 Err(error) => {
6095 completion.fail_serialization();
6096 Some((
6097 Err(std::io::Error::other(error)),
6098 (batches, None, completion, permit, rows, bytes, metrics),
6099 ))
6100 }
6101 }
6102 }
6103 Some(Err(error)) => Some((
6104 Err(std::io::Error::other(error)),
6105 (batches, None, completion, permit, rows, bytes, metrics),
6106 )),
6107 None => match writer.finish() {
6108 Ok(()) => {
6109 let chunk = std::mem::take(writer.get_mut());
6110 let next_bytes = bytes.saturating_add(chunk.len());
6111 if next_bytes > max_bytes {
6112 completion.fail_result_limit();
6113 return Some((
6114 Err(std::io::Error::other("SQL output byte limit exceeded")),
6115 (batches, None, completion, permit, rows, next_bytes, metrics),
6116 ));
6117 }
6118 if let Some(hook) = test_hook {
6119 hook(mongreldb_query::SqlTestHookPoint::AfterSerialization);
6120 }
6121 match completion.try_complete() {
6122 Ok(()) => {
6123 metrics.add_sql_output_bytes(chunk.len());
6124 Some((
6125 Ok(chunk),
6126 (
6127 batches, None, completion, permit, rows, next_bytes,
6128 metrics,
6129 ),
6130 ))
6131 }
6132 Err(error) => {
6133 metrics.inc_sql_errors();
6134 Some((
6135 Err(std::io::Error::other(error.to_string())),
6136 (batches, None, completion, permit, rows, bytes, metrics),
6137 ))
6138 }
6139 }
6140 }
6141 Err(error) => {
6142 completion.fail_serialization();
6143 Some((
6144 Err(std::io::Error::other(error)),
6145 (batches, None, completion, permit, rows, bytes, metrics),
6146 ))
6147 }
6148 },
6149 }
6150 }
6151 },
6152 );
6153 let schema_item: Result<Vec<u8>, std::io::Error> = Ok(schema_chunk);
6154 let body = axum::body::Body::from_stream(stream::iter([schema_item]).chain(batch_stream));
6155 ([(header::CONTENT_TYPE, STREAM_CT)], body).into_response()
6156}
6157
6158#[derive(Deserialize)]
6159struct TxnOp {
6160 table: String,
6161 op: String,
6162 cells: Option<Vec<serde_json::Value>>,
6163 row_id: Option<u64>,
6164}
6165
6166#[derive(Deserialize)]
6167struct TxnRequest {
6168 ops: Vec<TxnOp>,
6169}
6170
6171async fn txn(
6172 State(state): State<Arc<AppState>>,
6173 OptionalPrincipal(principal): OptionalPrincipal,
6174 Json(req): Json<TxnRequest>,
6175) -> Response {
6176 let mut parsed: Vec<(String, TxnAction)> = Vec::with_capacity(req.ops.len());
6180 for op in &req.ops {
6181 match op.op.as_str() {
6182 "put" => {
6183 let cells_json = match op.cells.as_ref() {
6184 Some(c) if !c.is_empty() => c,
6185 _ => {
6186 return (StatusCode::BAD_REQUEST, "put op requires non-empty cells")
6187 .into_response()
6188 }
6189 };
6190 let handle = match state.db.table(&op.table) {
6191 Ok(h) => h,
6192 Err(e) => return (StatusCode::NOT_FOUND, e.to_string()).into_response(),
6193 };
6194 let schema = handle.lock().schema().clone();
6195 let cells = match parse_cells(cells_json, &schema) {
6196 Ok(c) => c,
6197 Err(msg) => return (StatusCode::BAD_REQUEST, msg).into_response(),
6198 };
6199 parsed.push((op.table.clone(), TxnAction::Put(cells)));
6200 }
6201 "delete" => {
6202 let rid = match op.row_id {
6203 Some(r) => r,
6204 None => {
6205 return (StatusCode::BAD_REQUEST, "delete op requires row_id")
6206 .into_response()
6207 }
6208 };
6209 parsed.push((op.table.clone(), TxnAction::Delete(rid)));
6210 }
6211 other => {
6212 return (StatusCode::BAD_REQUEST, format!("unknown op: {other}")).into_response()
6213 }
6214 }
6215 }
6216
6217 state.metrics.inc_txns();
6218 let mut transaction = state.db.begin_as(request_principal(&state, &principal));
6219 let result = (|| {
6220 for (table, action) in &parsed {
6221 match action {
6222 TxnAction::Put(cells) => {
6223 transaction.put(table, cells.clone())?;
6224 }
6225 TxnAction::Delete(rid) => {
6226 transaction.delete(table, mongreldb_core::RowId(*rid))?;
6227 }
6228 }
6229 }
6230 transaction.commit()
6231 })();
6232 match result {
6233 Ok(epoch) => Json(json!({
6234 "status": "committed",
6235 "epoch": epoch.0,
6236 "epoch_text": epoch.0.to_string()
6237 }))
6238 .into_response(),
6239 Err(error) => crate::kit::durable_core_error_response(&error)
6240 .unwrap_or_else(|| (status_for_error(&error), error.to_string()).into_response()),
6241 }
6242}
6243
6244enum TxnAction {
6245 Put(Vec<(u16, Value)>),
6246 Delete(u64),
6247}
6248
6249#[cfg(test)]
6250mod auth_tests {
6251 use super::*;
6252 use mongreldb_core::Database;
6253 use tempfile::tempdir;
6254
6255 #[test]
6256 fn slow_query_operation_does_not_include_literals() {
6257 let sql = "CREATE USER alice PASSWORD 'never-log-this'";
6258 let operation = safe_sql_operation(sql);
6259 assert_eq!(operation, "CREATE");
6260 assert!(!operation.contains("never-log-this"));
6261 }
6262
6263 #[tokio::test]
6264 async fn auth_rejects_missing_token() {
6265 let dir = tempdir().unwrap();
6266 let db = Arc::new(Database::create(dir.path()).unwrap());
6267 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
6268 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6269 let addr = listener.local_addr().unwrap();
6270 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
6271 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6273
6274 let client = reqwest::Client::new();
6275 let resp = client
6276 .get(format!("http://{addr}/health"))
6277 .send()
6278 .await
6279 .unwrap();
6280 assert_eq!(resp.status(), 401);
6281 }
6282
6283 #[tokio::test]
6284 async fn auth_accepts_valid_token() {
6285 let dir = tempdir().unwrap();
6286 let db = Arc::new(Database::create(dir.path()).unwrap());
6287 let app = build_app_with_config(db, std::iter::empty(), Some("secret".into()), None);
6288 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6289 let addr = listener.local_addr().unwrap();
6290 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
6291 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6292
6293 let client = reqwest::Client::new();
6294 let resp = client
6295 .get(format!("http://{addr}/health"))
6296 .header("Authorization", "Bearer secret")
6297 .send()
6298 .await
6299 .unwrap();
6300 assert_eq!(resp.status(), 200);
6301 }
6302
6303 #[tokio::test]
6304 async fn no_auth_when_token_unset() {
6305 let dir = tempdir().unwrap();
6306 let db = Arc::new(Database::create(dir.path()).unwrap());
6307 let app = build_app_with_config(db, std::iter::empty(), None, None);
6308 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6309 let addr = listener.local_addr().unwrap();
6310 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
6311 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6312
6313 let client = reqwest::Client::new();
6314 let resp = client
6315 .get(format!("http://{addr}/health"))
6316 .send()
6317 .await
6318 .unwrap();
6319 assert_eq!(resp.status(), 200);
6320 }
6321
6322 #[tokio::test]
6323 async fn capabilities_advertise_sql_cancellation_v2() {
6324 let dir = tempdir().unwrap();
6325 let db = Arc::new(Database::create(dir.path()).unwrap());
6326 let app = build_app_with_config(db, std::iter::empty(), None, None);
6327 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6328 let addr = listener.local_addr().unwrap();
6329 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
6330
6331 let body: serde_json::Value = reqwest::Client::new()
6332 .get(format!("http://{addr}/capabilities"))
6333 .send()
6334 .await
6335 .unwrap()
6336 .json()
6337 .await
6338 .unwrap();
6339 assert_eq!(body["sql_cancellation"]["version"], 2);
6340 assert_eq!(body["sql_cancellation"]["client_query_ids"], true);
6341 assert_eq!(body["sql_cancellation"]["cancel_endpoint"], true);
6342 assert_eq!(body["sql_cancellation"]["query_status"], true);
6343 assert_eq!(body["sql_cancellation"]["pre_registration_cancel"], true);
6344 assert_eq!(body["sql_cancellation"]["stream_disconnect_cancels"], true);
6345 assert_eq!(body["sql_idempotency"]["version"], 1);
6346 assert_eq!(
6347 body["sql_idempotency"]["indeterminate_never_reexecutes"],
6348 true
6349 );
6350 assert_eq!(body["sql_pagination"]["version"], 1);
6351 assert_eq!(
6352 body["sql_pagination"]["continuation_endpoint"],
6353 "/sql/continue"
6354 );
6355 }
6356}
6357
6358#[cfg(test)]
6359mod query_response_tests {
6360 use super::*;
6361
6362 #[test]
6363 fn cancellation_reason_names_are_stable_snake_case() {
6364 assert_eq!(cancellation_reason_name(CancellationReason::None), "none");
6365 assert_eq!(
6366 cancellation_reason_name(CancellationReason::ClientRequest),
6367 "client_request"
6368 );
6369 assert_eq!(
6370 cancellation_reason_name(CancellationReason::ClientDisconnected),
6371 "client_disconnected"
6372 );
6373 assert_eq!(
6374 cancellation_reason_name(CancellationReason::SessionClosed),
6375 "session_closed"
6376 );
6377 assert_eq!(
6378 cancellation_reason_name(CancellationReason::ServerShutdown),
6379 "server_shutdown"
6380 );
6381 assert_eq!(
6382 cancellation_reason_name(CancellationReason::Deadline),
6383 "deadline"
6384 );
6385 }
6386
6387 #[test]
6388 fn unknown_outcome_never_proves_idempotency_intent_safe_to_abort() {
6389 let registry = Arc::new(SqlQueryRegistry::default());
6390 let unknown_id: QueryId = "102132435465768798a9bacbdcedfe0f".parse().unwrap();
6391 let unknown = registry
6392 .register(SqlQueryOptions {
6393 query_id: Some(unknown_id),
6394 ..SqlQueryOptions::default()
6395 })
6396 .unwrap();
6397 unknown.mark_outcome_unknown();
6398 unknown.fail();
6399 assert!(!can_abort_idempotency_intent(
6400 ®istry.status(unknown_id).unwrap()
6401 ));
6402
6403 let failed_id: QueryId = "2031425364758697a8b9cadbecfd0e1f".parse().unwrap();
6404 let failed = registry
6405 .register(SqlQueryOptions {
6406 query_id: Some(failed_id),
6407 ..SqlQueryOptions::default()
6408 })
6409 .unwrap();
6410 failed.fail();
6411 assert!(can_abort_idempotency_intent(
6412 ®istry.status(failed_id).unwrap()
6413 ));
6414 }
6415
6416 #[test]
6417 fn unknown_outcome_never_becomes_durable_receipt() {
6418 let registry = Arc::new(SqlQueryRegistry::default());
6419 let query = registry.register(SqlQueryOptions::default()).unwrap();
6420 query.record_commit(0, 42);
6421 query.mark_outcome_unknown();
6422 query.fail();
6423 let status = registry.status(query.id()).unwrap();
6424 assert!(status.durable_outcome.committed);
6425 assert!(status.outcome_unknown);
6426 assert!(sql_terminal_idempotency_receipt(&status).is_none());
6427 }
6428
6429 #[test]
6430 fn successful_noop_write_becomes_noncommitting_receipt() {
6431 let registry = Arc::new(SqlQueryRegistry::default());
6432 let query = registry.register(SqlQueryOptions::default()).unwrap();
6433 query
6434 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
6435 .unwrap();
6436 query.try_complete().unwrap();
6437 let status = registry.status(query.id()).unwrap();
6438 assert!(!status.durable_outcome.committed);
6439 assert!(!can_abort_idempotency_intent(&status));
6440 let receipt = sql_terminal_idempotency_receipt(&status).unwrap();
6441 assert_eq!(receipt.status, "completed");
6442 assert!(!receipt.outcome.committed);
6443 assert_eq!(receipt.outcome.committed_statements, 0);
6444 assert_eq!(receipt.outcome.last_commit_epoch, None);
6445 }
6446
6447 #[test]
6448 fn conflicting_idempotency_key_sources_are_rejected() {
6449 let request = SqlRequest {
6450 sql: "INSERT INTO items VALUES (1)".into(),
6451 format: None,
6452 query_id: None,
6453 timeout_ms: None,
6454 max_output_rows: None,
6455 max_output_bytes: None,
6456 idempotency_key: Some("body-key".into()),
6457 pagination: None,
6458 };
6459 let mut headers = axum::http::HeaderMap::new();
6460 headers.insert("idempotency-key", "header-key".parse().unwrap());
6461 assert_eq!(
6462 requested_sql_idempotency_key(&headers, &request),
6463 Err("body idempotency_key and Idempotency-Key header must match")
6464 );
6465
6466 headers.insert("idempotency-key", "body-key".parse().unwrap());
6467 assert_eq!(
6468 requested_sql_idempotency_key(&headers, &request),
6469 Ok(Some("body-key".into()))
6470 );
6471 }
6472
6473 #[test]
6474 fn idempotency_binding_includes_pagination_semantics() {
6475 let mut request = SqlRequest {
6476 sql: "INSERT INTO items VALUES (1)".into(),
6477 format: None,
6478 query_id: None,
6479 timeout_ms: None,
6480 max_output_rows: None,
6481 max_output_bytes: None,
6482 idempotency_key: Some("key".into()),
6483 pagination: None,
6484 };
6485 let unpaged = sql_idempotency_binding(&request, (100, 1_024), None, 60_000).unwrap();
6486 request.pagination = Some(SqlPaginationRequest {
6487 page_size_rows: 10,
6488 projection: vec!["id".into()],
6489 max_page_bytes: Some(512),
6490 max_page_tokens: Some(128),
6491 });
6492 let paged = sql_idempotency_binding(&request, (100, 1_024), None, 60_000).unwrap();
6493 assert_ne!(unpaged.request_semantics_hash, paged.request_semantics_hash);
6494 }
6495
6496 #[test]
6497 fn paginated_decode_stops_nested_heap_amplification_at_budget() {
6498 let registry = Arc::new(SqlQueryRegistry::default());
6499 let query = registry.register(SqlQueryOptions::default()).unwrap();
6500 let json = format!("[[{}]]", vec!["null"; 10_000].join(","));
6501 let mut deserializer = serde_json::Deserializer::from_slice(json.as_bytes());
6502 let mut budget = PaginatedDecodeBudget {
6503 used: 0,
6504 limit: 4 * 1024,
6505 nodes: 0,
6506 exceeded: false,
6507 query: &query,
6508 test_hook: None,
6509 };
6510 let error = serde::de::DeserializeSeed::deserialize(
6511 BudgetedJsonRowsSeed {
6512 budget: &mut budget,
6513 },
6514 &mut deserializer,
6515 )
6516 .unwrap_err();
6517 assert!(error.to_string().contains(PAGINATED_MEMORY_LIMIT_ERROR));
6518 assert!(budget.exceeded);
6519 assert!(budget.nodes < 100, "decoded {} nodes", budget.nodes);
6520 query.fail();
6521 }
6522
6523 #[tokio::test]
6524 async fn unknown_outcome_response_never_claims_no_commit() {
6525 let registry = Arc::new(SqlQueryRegistry::default());
6526 let query = registry.register(SqlQueryOptions::default()).unwrap();
6527 let query_id = query.id();
6528 query
6529 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
6530 .unwrap();
6531 let error = query.outcome_unknown_error("fenced maintenance failed");
6532 query.fail();
6533 let status = registry.status(query_id).unwrap();
6534 assert_eq!(
6535 status.terminal_state(),
6536 Some(mongreldb_query::QueryTerminalState::OutcomeUnknown)
6537 );
6538
6539 let response = query_error_response_with_status(&error, Some(query_id), Some(&status));
6540 assert_eq!(response.status(), StatusCode::CONFLICT);
6541 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
6542 .await
6543 .unwrap();
6544 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
6545 assert_eq!(body["status"], "outcome_unknown");
6546 assert_eq!(body["error"]["code"], "QUERY_OUTCOME_UNKNOWN");
6547 assert!(body["committed"].is_null());
6548 assert!(body["committed_statements"].is_null());
6549 assert!(body["last_commit_epoch"].is_null());
6550 assert!(body["completed_statements"].is_null());
6551 assert!(body["statement_index"].is_null());
6552 assert!(body["outcome"]["committed"].is_null());
6553 assert!(body["error"]["committed"].is_null());
6554 }
6555
6556 #[tokio::test]
6557 async fn retryable_idempotency_error_survives_terminal_status() {
6558 let registry = Arc::new(SqlQueryRegistry::default());
6559 let query = registry.register(SqlQueryOptions::default()).unwrap();
6560 let query_id = query.id();
6561 let response = registered_sql_error_response(
6562 RegisteredQueryGuard::new(query),
6563 query_id,
6564 StatusCode::SERVICE_UNAVAILABLE,
6565 "IDEMPOTENCY_STORE_UNAVAILABLE",
6566 "could not durably reserve the SQL idempotency key",
6567 true,
6568 );
6569 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
6570 .await
6571 .unwrap();
6572 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
6573 assert_eq!(body["retryable"], true);
6574 assert_eq!(body["error"]["retryable"], true);
6575
6576 let status = registry.status(query_id).unwrap();
6577 assert_eq!(
6578 status.terminal_error.as_ref().unwrap().code,
6579 "IDEMPOTENCY_STORE_UNAVAILABLE"
6580 );
6581 assert!(terminal_error_retryable(status.terminal_error.as_ref()));
6582 }
6583
6584 #[tokio::test]
6585 async fn committed_idempotency_terminal_error_is_a_receipt() {
6586 let query_id: QueryId = "00112233445566778899aabbccddeeff".parse().unwrap();
6587 let receipt = sql_idempotency::SqlDurableReceipt {
6588 original_query_id: query_id.to_string(),
6589 status: "committed_with_error".into(),
6590 server_state: "failed".into(),
6591 cancellation_reason: "client_disconnected".into(),
6592 outcome: sql_idempotency::SqlReceiptOutcome {
6593 committed: true,
6594 committed_statements: 1,
6595 last_commit_epoch: Some(42),
6596 last_commit_epoch_text: Some("42".into()),
6597 first_commit_statement_index: Some(0),
6598 last_commit_statement_index: Some(0),
6599 completed_statements: 1,
6600 statement_index: 0,
6601 serialization: "failed".into(),
6602 },
6603 terminal_error: Some(sql_idempotency::SqlReceiptTerminalError {
6604 code: "SERIALIZATION_FAILED_AFTER_COMMIT".into(),
6605 category: "serialization".into(),
6606 }),
6607 };
6608 let response = sql_idempotency_receipt_response(query_id, &receipt, false, 99, true);
6609 assert_eq!(response.status(), StatusCode::OK);
6610 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
6611 .await
6612 .unwrap();
6613 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
6614 assert_eq!(body["status"], "committed_with_error");
6615 assert_eq!(body["server_state"], "failed");
6616 assert_eq!(body["cancellation_reason"], "client_disconnected");
6617 assert_eq!(body["first_commit_statement_index"], 0);
6618 assert_eq!(body["last_commit_statement_index"], 0);
6619 assert_eq!(
6620 body["terminal_error"]["code"],
6621 "SERIALIZATION_FAILED_AFTER_COMMIT"
6622 );
6623 }
6624
6625 #[test]
6626 fn durable_replay_restores_terminal_status_parity() {
6627 let registry = Arc::new(SqlQueryRegistry::default());
6628 let query_id: QueryId = "11223344556677889900aabbccddeeff".parse().unwrap();
6629 let query = registry
6630 .register(SqlQueryOptions {
6631 query_id: Some(query_id),
6632 ..SqlQueryOptions::default()
6633 })
6634 .unwrap();
6635 let receipt = sql_idempotency::SqlDurableReceipt {
6636 original_query_id: "00112233445566778899aabbccddeeff".into(),
6637 status: "cancelled_after_commit".into(),
6638 server_state: "cancelled".into(),
6639 cancellation_reason: "client_disconnected".into(),
6640 outcome: sql_idempotency::SqlReceiptOutcome {
6641 committed: true,
6642 committed_statements: 1,
6643 last_commit_epoch: Some(42),
6644 last_commit_epoch_text: Some("42".into()),
6645 first_commit_statement_index: Some(0),
6646 last_commit_statement_index: Some(0),
6647 completed_statements: 1,
6648 statement_index: 1,
6649 serialization: "failed".into(),
6650 },
6651 terminal_error: Some(sql_idempotency::SqlReceiptTerminalError {
6652 code: "QUERY_CANCELLED_AFTER_COMMIT".into(),
6653 category: "cancellation".into(),
6654 }),
6655 };
6656 restore_idempotency_replay(RegisteredQueryGuard::new(query), &receipt).unwrap();
6657 let status = registry.status(query_id).unwrap();
6658 assert_eq!(status.phase, SqlQueryPhase::Cancelled);
6659 assert_eq!(
6660 status.terminal_state(),
6661 Some(mongreldb_query::QueryTerminalState::CancelledAfterCommit)
6662 );
6663 assert_eq!(
6664 status.cancellation_reason,
6665 CancellationReason::ClientDisconnected
6666 );
6667 assert_eq!(status.durable_outcome.committed_statements, 1);
6668 assert_eq!(status.durable_outcome.last_commit_epoch, Some(42));
6669 assert_eq!(
6670 status.terminal_error.unwrap().code,
6671 "QUERY_CANCELLED_AFTER_COMMIT"
6672 );
6673 }
6674
6675 #[test]
6676 fn durable_replay_rejects_invalid_authenticated_state() {
6677 let registry = Arc::new(SqlQueryRegistry::default());
6678 let query = registry.register(SqlQueryOptions::default()).unwrap();
6679 let receipt = sql_idempotency::SqlDurableReceipt {
6680 original_query_id: query.id().to_string(),
6681 status: "invented_terminal_state".into(),
6682 server_state: "completed".into(),
6683 cancellation_reason: "none".into(),
6684 outcome: sql_idempotency::SqlReceiptOutcome {
6685 committed: true,
6686 committed_statements: 1,
6687 last_commit_epoch: Some(42),
6688 last_commit_epoch_text: Some("42".into()),
6689 first_commit_statement_index: Some(0),
6690 last_commit_statement_index: Some(0),
6691 completed_statements: 1,
6692 statement_index: 0,
6693 serialization: "succeeded".into(),
6694 },
6695 terminal_error: None,
6696 };
6697 let error =
6698 restore_idempotency_replay(RegisteredQueryGuard::new(query), &receipt).unwrap_err();
6699 assert!(error.to_string().contains("invalid terminal state"));
6700 }
6701
6702 #[test]
6703 fn durable_replay_cancel_wins_before_receipt_response() {
6704 let registry = Arc::new(SqlQueryRegistry::default());
6705 let query_id: QueryId = "22334455667788990011aabbccddeeff".parse().unwrap();
6706 let query = registry
6707 .register(SqlQueryOptions {
6708 query_id: Some(query_id),
6709 ..SqlQueryOptions::default()
6710 })
6711 .unwrap();
6712 assert_eq!(
6713 query.request_cancel(CancellationReason::ClientRequest),
6714 CancelOutcome::Accepted
6715 );
6716 let receipt = sql_idempotency::SqlDurableReceipt {
6717 original_query_id: "00112233445566778899aabbccddeeff".into(),
6718 status: "completed".into(),
6719 server_state: "completed".into(),
6720 cancellation_reason: "none".into(),
6721 outcome: sql_idempotency::SqlReceiptOutcome {
6722 committed: true,
6723 committed_statements: 1,
6724 last_commit_epoch: Some(42),
6725 last_commit_epoch_text: Some("42".into()),
6726 first_commit_statement_index: Some(0),
6727 last_commit_statement_index: Some(0),
6728 completed_statements: 1,
6729 statement_index: 0,
6730 serialization: "succeeded".into(),
6731 },
6732 terminal_error: None,
6733 };
6734 let error = restore_idempotency_replay(RegisteredQueryGuard::new(query), &receipt)
6735 .expect_err("accepted cancellation must suppress replay success");
6736 assert!(matches!(
6737 error,
6738 mongreldb_query::MongrelQueryError::QueryCancelled { .. }
6739 ));
6740 let status = registry.status(query_id).unwrap();
6741 assert_eq!(status.phase, SqlQueryPhase::Cancelled);
6742 assert!(status.durable_outcome.committed);
6743 }
6744
6745 #[test]
6746 fn direct_query_handle_preserves_receipt_after_tombstone_eviction() {
6747 let registry = Arc::new(SqlQueryRegistry::new(
6748 1,
6749 1,
6750 usize::MAX,
6751 std::time::Duration::from_secs(60),
6752 ));
6753 let first = registry.register(SqlQueryOptions::default()).unwrap();
6754 first
6755 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
6756 .unwrap();
6757 first.record_commit(0, 42);
6758 first.complete_current_statement();
6759 first.try_complete().unwrap();
6760
6761 let second = registry.register(SqlQueryOptions::default()).unwrap();
6762 second
6763 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
6764 .unwrap();
6765 second.try_complete().unwrap();
6766
6767 assert!(registry.status(first.id()).is_none());
6768 let receipt = sql_terminal_idempotency_receipt(&first.status()).unwrap();
6769 assert_eq!(receipt.outcome.committed_statements, 1);
6770 assert_eq!(receipt.outcome.last_commit_epoch, Some(42));
6771 }
6772
6773 #[test]
6774 fn cancellation_checkpoint_mismatch_returns_typed_error() {
6775 let registry = Arc::new(SqlQueryRegistry::default());
6776 let query = registry.register(SqlQueryOptions::default()).unwrap();
6777 assert!(matches!(
6778 cancellation_checkpoint_error(&query),
6779 mongreldb_query::MongrelQueryError::InvalidQueryState(_)
6780 ));
6781 assert_eq!(
6782 query.request_cancel(CancellationReason::ClientRequest),
6783 CancelOutcome::Accepted
6784 );
6785 assert!(matches!(
6786 cancellation_checkpoint_error(&query),
6787 mongreldb_query::MongrelQueryError::QueryCancelled { .. }
6788 ));
6789 }
6790
6791 #[tokio::test]
6792 async fn cancellation_after_commit_reports_durable_outcome() {
6793 let registry = Arc::new(SqlQueryRegistry::default());
6794 let query = registry.register(SqlQueryOptions::default()).unwrap();
6795 let query_id = query.id();
6796 query
6797 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
6798 .unwrap();
6799 query.record_commit(0, 42);
6800 assert_eq!(
6801 query.request_cancel(CancellationReason::ClientRequest),
6802 CancelOutcome::Accepted
6803 );
6804 let error = query.checkpoint().unwrap_err();
6805 query.fail();
6806 let status = registry.status(query_id).unwrap();
6807
6808 let response = query_error_response_with_status(&error, Some(query_id), Some(&status));
6809 assert_eq!(response.status(), StatusCode::CONFLICT);
6810 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
6811 .await
6812 .unwrap();
6813 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
6814 assert_eq!(body["status"], "cancelled_after_commit");
6815 assert_eq!(body["error"]["code"], "QUERY_CANCELLED_AFTER_COMMIT");
6816 assert_eq!(body["committed"], true);
6817 assert_eq!(body["outcome"]["committed_statements"], 1);
6818 assert_eq!(body["outcome"]["last_commit_epoch"], 42);
6819 assert_eq!(body["outcome"]["last_commit_epoch_text"], "42");
6820 assert_eq!(body["first_commit_statement_index"], 0);
6821 assert_eq!(body["last_commit_statement_index"], 0);
6822 assert_eq!(body["outcome"]["first_commit_statement_index"], 0);
6823 assert_eq!(body["outcome"]["last_commit_statement_index"], 0);
6824
6825 let response = query_error_response_with_status(&error, Some(query_id), None);
6826 assert_eq!(response.status(), StatusCode::CONFLICT);
6827 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
6828 .await
6829 .unwrap();
6830 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
6831 assert_eq!(body["status"], "cancelled_after_commit");
6832 assert_eq!(body["error"]["code"], "QUERY_CANCELLED_AFTER_COMMIT");
6833 assert_eq!(body["committed"], true);
6834 assert_eq!(body["committed_statements"], 1);
6835 assert_eq!(body["last_commit_epoch"], 42);
6836 assert_eq!(body["last_commit_epoch_text"], "42");
6837 assert_eq!(body["first_commit_statement_index"], 0);
6838 assert_eq!(body["last_commit_statement_index"], 0);
6839 assert_eq!(body["outcome"]["committed"], true);
6840 assert_eq!(body["outcome"]["committed_statements"], 1);
6841 assert_eq!(body["outcome"]["last_commit_epoch"], 42);
6842 assert_eq!(body["outcome"]["last_commit_epoch_text"], "42");
6843 assert_eq!(body["outcome"]["first_commit_statement_index"], 0);
6844 assert_eq!(body["outcome"]["last_commit_statement_index"], 0);
6845 }
6846
6847 #[tokio::test]
6848 async fn commit_outcome_fallback_preserves_exact_progress() {
6849 let query_id: QueryId = "33445566778899001122aabbccddeeff".parse().unwrap();
6850 let error = mongreldb_query::MongrelQueryError::CommitOutcome {
6851 query_id,
6852 committed: true,
6853 committed_statements: 3,
6854 last_commit_epoch: Some(77),
6855 first_commit_statement_index: Some(1),
6856 last_commit_statement_index: Some(4),
6857 completed_statements: 4,
6858 statement_index: 5,
6859 message: "durable outcome retained".into(),
6860 };
6861 let response = query_error_response_with_status(&error, Some(query_id), None);
6862 assert_eq!(response.status(), StatusCode::CONFLICT);
6863 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
6864 .await
6865 .unwrap();
6866 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
6867 assert_eq!(body["status"], "committed_with_error");
6868 assert_eq!(body["committed"], true);
6869 assert_eq!(body["committed_statements"], 3);
6870 assert_eq!(body["last_commit_epoch"], 77);
6871 assert_eq!(body["last_commit_epoch_text"], "77");
6872 assert_eq!(body["first_commit_statement_index"], 1);
6873 assert_eq!(body["last_commit_statement_index"], 4);
6874 assert_eq!(body["completed_statements"], 4);
6875 assert_eq!(body["statement_index"], 5);
6876 assert_eq!(body["outcome"]["committed_statements"], 3);
6877 assert_eq!(body["outcome"]["last_commit_epoch"], 77);
6878 assert_eq!(body["outcome"]["first_commit_statement_index"], 1);
6879 assert_eq!(body["outcome"]["last_commit_statement_index"], 4);
6880 assert_eq!(body["outcome"]["completed_statements"], 4);
6881 assert_eq!(body["outcome"]["statement_index"], 5);
6882 }
6883
6884 #[test]
6885 fn status_cancel_outcome_matches_cancel_endpoint_state() {
6886 let registry = Arc::new(SqlQueryRegistry::default());
6887 let commit = registry.register(SqlQueryOptions::default()).unwrap();
6888 commit
6889 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
6890 .unwrap();
6891 commit.enter_commit_critical().unwrap();
6892 assert_eq!(
6893 query_cancel_outcome(®istry.status(commit.id()).unwrap()),
6894 Some("too_late")
6895 );
6896
6897 let completed = registry.register(SqlQueryOptions::default()).unwrap();
6898 completed
6899 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
6900 .unwrap();
6901 completed.try_complete().unwrap();
6902 assert_eq!(
6903 query_cancel_outcome(®istry.status(completed.id()).unwrap()),
6904 Some("already_finished")
6905 );
6906 }
6907}
6908
6909#[cfg(test)]
6910mod wal_stream_tests {
6911 use super::*;
6912 use mongreldb_client::ReplicationFollower;
6913 use mongreldb_core::Database;
6914 use tempfile::tempdir;
6915
6916 #[tokio::test]
6917 async fn wal_stream_returns_records_after_commit() {
6918 let dir = tempdir().unwrap();
6919 let db = Arc::new(Database::create(dir.path()).unwrap());
6920 let table_schema = mongreldb_core::schema::Schema {
6921 schema_id: 1,
6922 columns: vec![mongreldb_core::schema::ColumnDef {
6923 id: 1,
6924 name: "id".into(),
6925 ty: TypeId::Int64,
6926 flags: mongreldb_core::schema::ColumnFlags::empty()
6927 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
6928 default_value: None,
6929 }],
6930 indexes: vec![],
6931 colocation: vec![],
6932 constraints: Default::default(),
6933 clustered: false,
6934 };
6935 db.create_table("items", table_schema).unwrap();
6936 let handle = db.table("items").unwrap();
6938 handle.lock().put(vec![(1, Value::Int64(1))]).unwrap();
6939 handle.lock().flush().unwrap();
6940
6941 let app = build_app(db);
6942 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6943 let addr = listener.local_addr().unwrap();
6944 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
6945 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6946
6947 let resp = reqwest::get(format!("http://{addr}/wal/stream"))
6948 .await
6949 .unwrap();
6950 assert_eq!(resp.status(), 200);
6951 let body = resp.text().await.unwrap();
6952 assert!(!body.is_empty(), "wal_stream should return records");
6954 assert!(body.contains("seq"), "response should contain seq field");
6955 }
6956
6957 #[tokio::test]
6958 async fn follower_bootstraps_and_applies_incremental_commit() {
6959 let leader_dir = tempdir().unwrap();
6960 let follower_dir = tempdir().unwrap();
6961 let follower_path = follower_dir.path().join("copy");
6962 let db = Arc::new(Database::create(leader_dir.path()).unwrap());
6963 db.create_table(
6964 "items",
6965 mongreldb_core::schema::Schema {
6966 schema_id: 1,
6967 columns: vec![mongreldb_core::schema::ColumnDef {
6968 id: 1,
6969 name: "id".into(),
6970 ty: TypeId::Int64,
6971 flags: mongreldb_core::schema::ColumnFlags::empty()
6972 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
6973 default_value: None,
6974 }],
6975 indexes: vec![],
6976 colocation: vec![],
6977 constraints: Default::default(),
6978 clustered: false,
6979 },
6980 )
6981 .unwrap();
6982 let handle = db.table("items").unwrap();
6983 handle.lock().put(vec![(1, Value::Int64(1))]).unwrap();
6984 handle.lock().commit().unwrap();
6985
6986 let app = build_app_with_config(
6987 Arc::clone(&db),
6988 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
6989 Some("replication-secret".into()),
6990 None,
6991 );
6992 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
6993 let addr = listener.local_addr().unwrap();
6994 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
6995 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6996
6997 let leader_url = format!("http://{addr}");
6998 let first_path = follower_path.clone();
6999 let (mut follower, initial) = tokio::task::spawn_blocking(move || {
7000 let mut follower = ReplicationFollower::new(&leader_url, first_path)
7001 .unwrap()
7002 .with_bearer_token("replication-secret");
7003 let applied = follower.sync().unwrap();
7004 (follower, applied)
7005 })
7006 .await
7007 .unwrap();
7008 assert_eq!(initial, 0);
7009
7010 handle.lock().put(vec![(1, Value::Int64(2))]).unwrap();
7011 handle.lock().commit().unwrap();
7012 let applied = tokio::task::spawn_blocking(move || {
7013 let count = follower.sync().unwrap();
7014 (follower, count)
7015 })
7016 .await
7017 .unwrap();
7018 follower = applied.0;
7019 assert!(applied.1 > 0);
7020 assert!(follower.last_epoch() > 0);
7021
7022 let replica = Database::open(&follower_path).unwrap();
7023 assert_eq!(replica.table("items").unwrap().lock().count(), 2);
7024 drop(replica);
7025
7026 db.set_spill_threshold(1);
7027 db.transaction(|txn| {
7028 txn.put("items", vec![(1, Value::Int64(3))])?;
7029 Ok(())
7030 })
7031 .unwrap();
7032 let (follower_after_bootstrap, applied) = tokio::task::spawn_blocking(move || {
7033 let count = follower.sync().unwrap();
7034 (follower, count)
7035 })
7036 .await
7037 .unwrap();
7038 assert_eq!(applied, 0, "spilled run should trigger safe rebootstrap");
7039 assert!(follower_after_bootstrap.last_epoch() > 0);
7040 let replica = Database::open(&follower_path).unwrap();
7041 assert_eq!(replica.table("items").unwrap().lock().count(), 3);
7042 }
7043}
7044
7045#[cfg(test)]
7046mod metrics_tests {
7047 use super::*;
7048 use mongreldb_core::Database;
7049 use tempfile::tempdir;
7050
7051 async fn setup() -> (tempfile::TempDir, std::net::SocketAddr) {
7054 let dir = tempdir().unwrap();
7055 let db = Arc::new(Database::create(dir.path()).unwrap());
7056 let table_schema = mongreldb_core::schema::Schema {
7057 schema_id: 1,
7058 columns: vec![mongreldb_core::schema::ColumnDef {
7059 id: 1,
7060 name: "id".into(),
7061 ty: TypeId::Int64,
7062 flags: mongreldb_core::schema::ColumnFlags::empty()
7063 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
7064 default_value: None,
7065 }],
7066 indexes: vec![],
7067 colocation: vec![],
7068 constraints: Default::default(),
7069 clustered: false,
7070 };
7071 db.create_table("items", table_schema).unwrap();
7072 let app = build_app(db);
7073 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
7074 let addr = listener.local_addr().unwrap();
7075 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
7076 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7077 (dir, addr)
7078 }
7079
7080 #[tokio::test]
7081 async fn metrics_endpoint_returns_prometheus_text() {
7082 let (_dir, addr) = setup().await;
7083 let client = reqwest::Client::new();
7084
7085 let _ = client
7087 .post(format!("http://{addr}/tables/items/put"))
7088 .json(&json!({ "row": [1, 1] }))
7089 .send()
7090 .await
7091 .unwrap();
7092 let _ = client
7093 .post(format!("http://{addr}/sql"))
7094 .json(&json!({ "sql": "SELECT count(*) FROM items" }))
7095 .send()
7096 .await
7097 .unwrap();
7098
7099 let resp = client
7100 .get(format!("http://{addr}/metrics"))
7101 .send()
7102 .await
7103 .unwrap();
7104 assert_eq!(resp.status(), 200);
7105 let ct = resp
7106 .headers()
7107 .get("content-type")
7108 .and_then(|v| v.to_str().ok())
7109 .unwrap_or_default()
7110 .to_string();
7111 assert!(
7112 ct.contains("text/plain"),
7113 "content-type is prometheus text: {ct}"
7114 );
7115 let body = resp.text().await.unwrap();
7116 assert!(body.contains("# TYPE mongreldb_sql_queries_total counter"));
7118 assert!(body.contains("# TYPE mongreldb_puts_total counter"));
7119 assert!(body.contains("# TYPE mongreldb_tables gauge"));
7120 assert!(
7122 body.contains("mongreldb_sql_queries_total 1"),
7123 "sql_queries counter should reflect the /sql call: {body}"
7124 );
7125 assert!(
7126 body.contains("mongreldb_puts_total 1"),
7127 "puts counter should reflect the put call: {body}"
7128 );
7129 assert!(body.contains("mongreldb_tables 1"));
7131 }
7132
7133 #[tokio::test]
7134 async fn metrics_error_counter_increments_on_bad_sql() {
7135 let (_dir, addr) = setup().await;
7136 let client = reqwest::Client::new();
7137 let _ = client
7139 .post(format!("http://{addr}/sql"))
7140 .json(&json!({ "sql": "SELECT * FROM does_not_exist" }))
7141 .send()
7142 .await
7143 .unwrap();
7144 let body = client
7145 .get(format!("http://{addr}/metrics"))
7146 .send()
7147 .await
7148 .unwrap()
7149 .text()
7150 .await
7151 .unwrap();
7152 assert!(
7153 body.contains("mongreldb_sql_errors_total 1"),
7154 "sql_errors should increment on a failed query: {body}"
7155 );
7156 }
7157
7158 #[tokio::test]
7159 async fn arrow_stream_returns_ipc_stream_bytes() {
7160 let (_dir, addr) = setup().await;
7161 let client = reqwest::Client::new();
7162 for i in 1..=3 {
7165 let resp = client
7166 .post(format!("http://{addr}/tables/items/put"))
7167 .json(&json!({ "row": [1, i] }))
7168 .send()
7169 .await
7170 .unwrap();
7171 assert_eq!(resp.status(), 200, "put should succeed");
7172 }
7173 let _ = client
7174 .post(format!("http://{addr}/tables/items/commit"))
7175 .send()
7176 .await
7177 .unwrap();
7178 let count_body = client
7180 .get(format!("http://{addr}/tables/items/count"))
7181 .send()
7182 .await
7183 .unwrap()
7184 .text()
7185 .await
7186 .unwrap();
7187 assert!(
7188 count_body.contains("\"count\":3"),
7189 "expected 3 visible rows, got: {count_body}"
7190 );
7191 let resp = client
7192 .post(format!("http://{addr}/sql"))
7193 .json(&json!({ "sql": "SELECT count(*) FROM items", "format": "arrow-stream" }))
7194 .send()
7195 .await
7196 .unwrap();
7197 assert_eq!(resp.status(), 200, "streaming query should succeed");
7198 let ct = resp
7199 .headers()
7200 .get("content-type")
7201 .and_then(|v| v.to_str().ok())
7202 .unwrap_or_default()
7203 .to_string();
7204 assert!(
7205 ct.contains("application/vnd.apache.arrow.stream"),
7206 "content-type should be the arrow stream format: {ct}"
7207 );
7208 let bytes = resp.bytes().await.unwrap();
7209 assert!(
7213 !bytes.is_empty(),
7214 "arrow stream body should contain schema + batch + EOS"
7215 );
7216 assert!(
7217 bytes.starts_with(&0xFFFFFFFFu32.to_le_bytes()),
7218 "arrow stream must begin with the IPC continuation marker"
7219 );
7220 assert!(
7221 bytes.ends_with(&[0u8, 0, 0, 0]),
7222 "arrow stream should end with the EOS marker (trailing zero length)"
7223 );
7224 }
7225}
7226
7227#[cfg(test)]
7228mod streaming_tests {
7229 use super::*;
7230 use arrow::array::Int64Array;
7231 use arrow::datatypes::{DataType, Field, Schema};
7232 use arrow::record_batch::RecordBatch;
7233 use datafusion::common::DataFusionError;
7234 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
7235 use futures::StreamExt;
7236 use std::sync::Arc as StdArc;
7237
7238 fn batch_stream(batches: Vec<RecordBatch>) -> mongreldb_query::MongrelRecordBatchStream {
7239 let schema = batches
7240 .first()
7241 .map(RecordBatch::schema)
7242 .unwrap_or_else(|| StdArc::new(Schema::empty()));
7243 let batches =
7244 futures::stream::iter(batches.into_iter().map(Ok::<RecordBatch, DataFusionError>));
7245 Box::pin(RecordBatchStreamAdapter::new(schema, batches))
7246 }
7247
7248 #[tokio::test]
7253 async fn arrow_stream_serializes_multiple_batches_roundtrip() {
7254 let schema = StdArc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)]));
7255 let b1 = RecordBatch::try_new(
7256 schema.clone(),
7257 vec![StdArc::new(Int64Array::from(vec![1, 2]))],
7258 )
7259 .unwrap();
7260 let b2 = RecordBatch::try_new(
7261 schema.clone(),
7262 vec![StdArc::new(Int64Array::from(vec![3, 4, 5]))],
7263 )
7264 .unwrap();
7265
7266 let resp = sql_arrow_stream_response(batch_stream(vec![b1, b2]));
7267 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
7268 .await
7269 .unwrap();
7270
7271 assert!(bytes.starts_with(&0xFFFFFFFFu32.to_le_bytes()));
7273
7274 let slice: &[u8] = bytes.as_ref();
7277 let mut reader = arrow::ipc::reader::StreamReader::try_new(slice, None).unwrap();
7278 let mut total_rows = 0;
7279 for batch in reader.by_ref() {
7280 let batch = batch.expect("each IPC message should decode");
7281 total_rows += batch.num_rows();
7282 }
7283 assert_eq!(
7284 total_rows, 5,
7285 "all rows should round-trip through the stream"
7286 );
7287 }
7288
7289 #[tokio::test]
7290 async fn arrow_stream_emits_schema_before_first_batch() {
7291 let schema = StdArc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)]));
7292 let pending = futures::stream::pending::<Result<RecordBatch, DataFusionError>>();
7293 let batches = Box::pin(RecordBatchStreamAdapter::new(schema, pending));
7294 let mut body = sql_arrow_stream_response(batches)
7295 .into_body()
7296 .into_data_stream();
7297
7298 let chunk = tokio::time::timeout(std::time::Duration::from_millis(100), body.next())
7299 .await
7300 .expect("schema chunk should not wait for a query batch")
7301 .unwrap()
7302 .unwrap();
7303 assert!(chunk.starts_with(&0xFFFFFFFFu32.to_le_bytes()));
7304 }
7305
7306 #[tokio::test]
7307 async fn arrow_stream_empty_query_is_valid_ipc() {
7308 let resp = sql_arrow_stream_response(batch_stream(Vec::new()));
7309 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
7310 .await
7311 .unwrap();
7312 let slice: &[u8] = bytes.as_ref();
7313 let reader = arrow::ipc::reader::StreamReader::try_new(slice, None).unwrap();
7314 assert_eq!(reader.count(), 0);
7315 }
7316
7317 #[tokio::test]
7318 async fn buffered_output_limits_are_typed() {
7319 let dir = tempfile::tempdir().unwrap();
7320 let db = StdArc::new(mongreldb_core::Database::create(dir.path()).unwrap());
7321 let session = MongrelSession::open(db).unwrap();
7322 let query = session.register_query(SqlQueryOptions::default()).unwrap();
7323 let output = session
7324 .run_with_query_for_serialization("SELECT 1", query)
7325 .await
7326 .unwrap();
7327
7328 let row_error =
7329 serialize_buffered_output("json", output.batches(), output.query(), 0, 1024, None)
7330 .unwrap_err();
7331 assert!(matches!(row_error, BufferedSerializationError::Limit(_)));
7332 let byte_error =
7333 serialize_buffered_output("json", output.batches(), output.query(), 10, 1, None)
7334 .unwrap_err();
7335 assert!(matches!(byte_error, BufferedSerializationError::Limit(_)));
7336 output.fail();
7337 }
7338}
7339
7340#[cfg(test)]
7341mod audit_tests {
7342 use super::*;
7343 use mongreldb_core::Database;
7344 use tempfile::tempdir;
7345
7346 async fn auth_setup(password: &str) -> std::net::SocketAddr {
7348 let dir = tempdir().unwrap();
7349 let db = Arc::new(Database::create(dir.path()).unwrap());
7350 db.create_user("alice", password).unwrap();
7351 db.set_user_admin("alice", true).unwrap();
7352 let app = build_app_full(
7353 db,
7354 std::iter::empty::<Arc<dyn ExternalTableModule>>(),
7355 None,
7356 None,
7357 true,
7358 );
7359 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
7360 let addr = listener.local_addr().unwrap();
7361 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
7362 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7363 addr
7364 }
7365
7366 #[tokio::test]
7367 async fn audit_records_login_success_and_failure() {
7368 let addr = auth_setup("s3cret").await;
7369 let client = reqwest::Client::new();
7370
7371 let resp = client
7373 .get(format!("http://{addr}/health"))
7374 .header("Authorization", basic("alice", "s3cret"))
7375 .send()
7376 .await
7377 .unwrap();
7378 assert_eq!(resp.status(), 200);
7379
7380 let resp = client
7382 .get(format!("http://{addr}/health"))
7383 .header("Authorization", basic("alice", "wrong"))
7384 .send()
7385 .await
7386 .unwrap();
7387 assert_eq!(resp.status(), 401);
7388
7389 let body = client
7390 .get(format!("http://{addr}/audit"))
7391 .header("Authorization", basic("alice", "s3cret"))
7392 .send()
7393 .await
7394 .unwrap()
7395 .text()
7396 .await
7397 .unwrap();
7398 assert!(
7399 body.contains("\"action\":\"login.ok\""),
7400 "audit should record the successful login: {body}"
7401 );
7402 assert!(
7403 body.contains("\"action\":\"login.fail\""),
7404 "audit should record the failed login: {body}"
7405 );
7406 assert!(
7407 body.contains("\"principal\":\"alice\""),
7408 "audit should attribute events to alice: {body}"
7409 );
7410 }
7411
7412 #[tokio::test]
7413 async fn audit_records_ddl_sql() {
7414 let dir = tempdir().unwrap();
7415 let db = Arc::new(Database::create(dir.path()).unwrap());
7416 let app = build_app(db);
7417 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
7418 let addr = listener.local_addr().unwrap();
7419 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
7420 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7421
7422 let client = reqwest::Client::new();
7423 let _ = client
7424 .post(format!("http://{addr}/sql"))
7425 .json(&json!({ "sql": "CREATE TABLE t (id BIGINT PRIMARY KEY)" }))
7426 .send()
7427 .await
7428 .unwrap();
7429 let _ = client
7431 .post(format!("http://{addr}/sql"))
7432 .json(&json!({ "sql": "SELECT 1" }))
7433 .send()
7434 .await
7435 .unwrap();
7436
7437 let body = client
7438 .get(format!("http://{addr}/audit"))
7439 .send()
7440 .await
7441 .unwrap()
7442 .text()
7443 .await
7444 .unwrap();
7445 assert!(
7446 body.contains("\"action\":\"ddl.ok\""),
7447 "audit should record the successful DDL statement: {body}"
7448 );
7449 assert!(
7450 body.contains("CREATE TABLE"),
7451 "audit detail should carry the DDL snippet: {body}"
7452 );
7453 assert!(
7455 !body.contains("SELECT 1"),
7456 "non-DDL reads should not be audited: {body}"
7457 );
7458 }
7459
7460 #[tokio::test]
7461 async fn audit_redacts_credential_passwords() {
7462 let dir = tempdir().unwrap();
7463 let db = Arc::new(Database::create(dir.path()).unwrap());
7464 let app = build_app(db);
7465 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
7466 let addr = listener.local_addr().unwrap();
7467 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
7468 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7469
7470 let client = reqwest::Client::new();
7471 let _ = client
7474 .post(format!("http://{addr}/sql"))
7475 .json(&json!({ "sql": "CREATE USER alice WITH PASSWORD 'topsecret'" }))
7476 .send()
7477 .await
7478 .unwrap();
7479
7480 let body = client
7481 .get(format!("http://{addr}/audit"))
7482 .send()
7483 .await
7484 .unwrap()
7485 .text()
7486 .await
7487 .unwrap();
7488 assert!(
7489 !body.contains("topsecret"),
7490 "password must never appear in the audit log: {body}"
7491 );
7492 assert!(
7493 body.contains("redacted credential statement"),
7494 "credential DDL should be recorded as redacted: {body}"
7495 );
7496 }
7497
7498 fn basic(user: &str, pass: &str) -> String {
7499 let raw = format!("{user}:{pass}");
7500 format!("Basic {}", base64_encode(raw.as_bytes()))
7501 }
7502
7503 fn base64_encode(input: &[u8]) -> String {
7504 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7505 let mut out = String::new();
7506 let mut buf = 0u32;
7507 let mut bits = 0u32;
7508 for &b in input {
7509 buf = (buf << 8) | b as u32;
7510 bits += 8;
7511 while bits >= 6 {
7512 bits -= 6;
7513 out.push(TABLE[((buf >> bits) & 0x3F) as usize] as char);
7514 }
7515 }
7516 if bits > 0 {
7517 out.push(TABLE[((buf << (6 - bits)) & 0x3F) as usize] as char);
7518 }
7519 while !out.len().is_multiple_of(4) {
7520 out.push('=');
7521 }
7522 out
7523 }
7524}
7525
7526#[cfg(test)]
7527mod session_tests {
7528 use super::*;
7529 use mongreldb_core::Database;
7530 use tempfile::tempdir;
7531
7532 async fn setup() -> (tempfile::TempDir, std::net::SocketAddr) {
7536 let dir = tempdir().unwrap();
7537 let db = Arc::new(Database::create(dir.path()).unwrap());
7538 let table_schema = mongreldb_core::schema::Schema {
7539 schema_id: 1,
7540 columns: vec![mongreldb_core::schema::ColumnDef {
7541 id: 1,
7542 name: "id".into(),
7543 ty: TypeId::Int64,
7544 flags: mongreldb_core::schema::ColumnFlags::empty()
7545 .with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
7546 default_value: None,
7547 }],
7548 indexes: vec![],
7549 colocation: vec![],
7550 constraints: Default::default(),
7551 clustered: false,
7552 };
7553 db.create_table("items", table_schema).unwrap();
7554 let app = build_app(db);
7555 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
7556 let addr = listener.local_addr().unwrap();
7557 tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
7558 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7559 (dir, addr)
7560 }
7561
7562 async fn open_session(client: &reqwest::Client, addr: &std::net::SocketAddr) -> String {
7564 let resp = client
7565 .post(format!("http://{addr}/sessions"))
7566 .send()
7567 .await
7568 .unwrap();
7569 assert_eq!(resp.status(), 200);
7570 resp.json::<serde_json::Value>()
7571 .await
7572 .unwrap()
7573 .get("session_id")
7574 .unwrap()
7575 .as_str()
7576 .unwrap()
7577 .to_string()
7578 }
7579
7580 async fn sql_on(
7581 client: &reqwest::Client,
7582 addr: &std::net::SocketAddr,
7583 session: &str,
7584 sql: &str,
7585 ) -> reqwest::Response {
7586 client
7587 .post(format!("http://{addr}/sql"))
7588 .header("X-Session-ID", session)
7589 .json(&json!({ "sql": sql }))
7590 .send()
7591 .await
7592 .unwrap()
7593 }
7594
7595 async fn count_items(client: &reqwest::Client, addr: &std::net::SocketAddr) -> u64 {
7596 client
7597 .get(format!("http://{addr}/tables/items/count"))
7598 .send()
7599 .await
7600 .unwrap()
7601 .json::<serde_json::Value>()
7602 .await
7603 .unwrap()
7604 .get("count")
7605 .unwrap()
7606 .as_u64()
7607 .unwrap()
7608 }
7609
7610 #[tokio::test]
7611 async fn cross_request_transaction_commits() {
7612 let (_dir, addr) = setup().await;
7613 let client = reqwest::Client::new();
7614 let session = open_session(&client, &addr).await;
7615
7616 let r = sql_on(&client, &addr, &session, "BEGIN").await;
7618 assert_eq!(r.status(), 200);
7619 let r = sql_on(
7620 &client,
7621 &addr,
7622 &session,
7623 "INSERT INTO items (id) VALUES (1)",
7624 )
7625 .await;
7626 assert_eq!(r.status(), 200, "INSERT should stage successfully");
7627 assert_eq!(count_items(&client, &addr).await, 0);
7629 let r = sql_on(&client, &addr, &session, "COMMIT").await;
7630 assert_eq!(r.status(), 200);
7631
7632 assert_eq!(count_items(&client, &addr).await, 1);
7634 }
7635
7636 #[tokio::test]
7637 async fn cross_request_transaction_rolls_back() {
7638 let (_dir, addr) = setup().await;
7639 let client = reqwest::Client::new();
7640 let session = open_session(&client, &addr).await;
7641
7642 sql_on(&client, &addr, &session, "BEGIN").await;
7643 sql_on(
7644 &client,
7645 &addr,
7646 &session,
7647 "INSERT INTO items (id) VALUES (5)",
7648 )
7649 .await;
7650 let r = sql_on(&client, &addr, &session, "ROLLBACK").await;
7652 assert_eq!(r.status(), 200);
7653 assert_eq!(
7654 count_items(&client, &addr).await,
7655 0,
7656 "rollback discards staged writes"
7657 );
7658 }
7659
7660 #[tokio::test]
7661 async fn unknown_session_id_is_404() {
7662 let (_dir, addr) = setup().await;
7663 let client = reqwest::Client::new();
7664 let resp = client
7665 .post(format!("http://{addr}/sql"))
7666 .header("X-Session-ID", "does-not-exist")
7667 .json(&json!({ "sql": "SELECT 1" }))
7668 .send()
7669 .await
7670 .unwrap();
7671 assert_eq!(resp.status(), 404);
7672 }
7673
7674 #[tokio::test]
7675 async fn invalid_session_headers_do_not_autocommit() {
7676 let (_dir, addr) = setup().await;
7677 let client = reqwest::Client::new();
7678
7679 let resp = client
7680 .post(format!("http://{addr}/sql"))
7681 .header(
7682 "X-Session-ID",
7683 reqwest::header::HeaderValue::from_bytes(&[0xff]).unwrap(),
7684 )
7685 .json(&json!({ "sql": "INSERT INTO items (id) VALUES (1)" }))
7686 .send()
7687 .await
7688 .unwrap();
7689 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
7690
7691 let resp = client
7692 .post(format!("http://{addr}/sql"))
7693 .header("X-Session-ID", "x".repeat(257))
7694 .json(&json!({ "sql": "INSERT INTO items (id) VALUES (2)" }))
7695 .send()
7696 .await
7697 .unwrap();
7698 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
7699 assert_eq!(count_items(&client, &addr).await, 0);
7700 }
7701
7702 #[tokio::test]
7703 async fn close_session_ends_cross_request_state() {
7704 let (_dir, addr) = setup().await;
7705 let client = reqwest::Client::new();
7706 let session = open_session(&client, &addr).await;
7707
7708 sql_on(&client, &addr, &session, "BEGIN").await;
7710 let r = client
7711 .delete(format!("http://{addr}/sessions/{session}"))
7712 .send()
7713 .await
7714 .unwrap();
7715 assert_eq!(r.status(), 200);
7716
7717 let resp = sql_on(&client, &addr, &session, "COMMIT").await;
7719 assert_eq!(resp.status(), 404, "closed session is no longer usable");
7720 }
7721
7722 #[tokio::test]
7723 async fn no_session_header_uses_fresh_ephemeral_session() {
7724 let (_dir, addr) = setup().await;
7727 let client = reqwest::Client::new();
7728 let resp = client
7729 .post(format!("http://{addr}/sql"))
7730 .json(&json!({ "sql": "INSERT INTO items (id) VALUES (42)" }))
7731 .send()
7732 .await
7733 .unwrap();
7734 assert_eq!(resp.status(), 200);
7735 assert_eq!(count_items(&client, &addr).await, 1);
7736 }
7737
7738 #[tokio::test]
7739 async fn prepared_statement_prepare_execute_and_reuse() {
7740 let (_dir, addr) = setup().await;
7741 let client = reqwest::Client::new();
7742 let session = open_session(&client, &addr).await;
7743 sql_on(
7744 &client,
7745 &addr,
7746 &session,
7747 "INSERT INTO items (id) VALUES (1), (2), (3), (4)",
7748 )
7749 .await;
7750
7751 let resp = client
7753 .post(format!("http://{addr}/sessions/{session}/prepare"))
7754 .json(&json!({"name":"gt","sql":"SELECT id FROM items WHERE id > $1"}))
7755 .send()
7756 .await
7757 .unwrap();
7758 assert_eq!(resp.status(), 200);
7759
7760 let resp = client
7762 .post(format!("http://{addr}/sessions/{session}/execute"))
7763 .json(&json!({"name":"gt","params":[2]}))
7764 .send()
7765 .await
7766 .unwrap();
7767 assert_eq!(resp.status(), 200);
7768 let body = resp.json::<serde_json::Value>().await.unwrap();
7769 let arr = body
7770 .as_array()
7771 .expect("execute returns a JSON array of rows");
7772 assert_eq!(arr.len(), 2, "ids > 2 are {{3,4}}: {body}");
7773
7774 let resp = client
7776 .post(format!("http://{addr}/sessions/{session}/execute"))
7777 .json(&json!({"name":"gt","params":[3]}))
7778 .send()
7779 .await
7780 .unwrap();
7781 let body = resp.json::<serde_json::Value>().await.unwrap();
7782 assert_eq!(body.as_array().unwrap().len(), 1, "ids > 3 is {{4}}");
7783 }
7784
7785 #[tokio::test]
7786 async fn prepared_statement_deallocate_then_execute_fails() {
7787 let (_dir, addr) = setup().await;
7788 let client = reqwest::Client::new();
7789 let session = open_session(&client, &addr).await;
7790 let _ = client
7791 .post(format!("http://{addr}/sessions/{session}/prepare"))
7792 .json(&json!({"name":"p","sql":"SELECT $1"}))
7793 .send()
7794 .await
7795 .unwrap();
7796 let deallocate_query_id = "dadadadadadadadadadadadadadadada";
7797 let resp = client
7798 .delete(format!("http://{addr}/sessions/{session}/statements/p"))
7799 .header("X-MongrelDB-Query-ID", deallocate_query_id)
7800 .header("X-MongrelDB-Timeout-Ms", "10000")
7801 .send()
7802 .await
7803 .unwrap();
7804 assert_eq!(resp.status(), 200);
7805 assert_eq!(
7806 resp.headers()
7807 .get("X-MongrelDB-Query-ID")
7808 .unwrap()
7809 .to_str()
7810 .unwrap(),
7811 deallocate_query_id
7812 );
7813 let status = client
7814 .get(format!("http://{addr}/queries/{deallocate_query_id}"))
7815 .send()
7816 .await
7817 .unwrap()
7818 .json::<serde_json::Value>()
7819 .await
7820 .unwrap();
7821 assert_eq!(status["state"], "completed");
7822 assert_eq!(status["operation"], "DEALLOCATE");
7823 let resp = client
7825 .post(format!("http://{addr}/sessions/{session}/execute"))
7826 .json(&json!({"name":"p","params":[1]}))
7827 .send()
7828 .await
7829 .unwrap();
7830 assert_ne!(resp.status(), 200, "execute after DEALLOCATE must fail");
7831 }
7832
7833 #[tokio::test]
7834 async fn prepared_statement_deallocate_honors_pre_registration_cancel() {
7835 let (_dir, addr) = setup().await;
7836 let client = reqwest::Client::new();
7837 let session = open_session(&client, &addr).await;
7838 let prepared = client
7839 .post(format!("http://{addr}/sessions/{session}/prepare"))
7840 .json(&json!({"name":"p","sql":"SELECT $1"}))
7841 .send()
7842 .await
7843 .unwrap();
7844 assert_eq!(prepared.status(), StatusCode::OK);
7845
7846 let query_id = "dbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdb";
7847 let cancel = client
7848 .post(format!("http://{addr}/queries/{query_id}/cancel"))
7849 .header("X-Session-ID", &session)
7850 .send()
7851 .await
7852 .unwrap();
7853 assert_eq!(cancel.status(), StatusCode::ACCEPTED);
7854 let deallocate = client
7855 .delete(format!("http://{addr}/sessions/{session}/statements/p"))
7856 .header("X-MongrelDB-Query-ID", query_id)
7857 .send()
7858 .await
7859 .unwrap();
7860 assert_eq!(deallocate.status().as_u16(), 499);
7861 assert_eq!(
7862 deallocate.json::<serde_json::Value>().await.unwrap()["error"]["code"],
7863 "QUERY_CANCELLED"
7864 );
7865
7866 let execute = client
7867 .post(format!("http://{addr}/sessions/{session}/execute"))
7868 .json(&json!({"name":"p","params":[1]}))
7869 .send()
7870 .await
7871 .unwrap();
7872 assert_eq!(execute.status(), StatusCode::OK);
7873 }
7874
7875 #[tokio::test]
7876 async fn prepared_statement_rejects_bad_name() {
7877 let (_dir, addr) = setup().await;
7878 let client = reqwest::Client::new();
7879 let session = open_session(&client, &addr).await;
7880 let resp = client
7881 .post(format!("http://{addr}/sessions/{session}/prepare"))
7882 .json(&json!({"name":"1bad","sql":"SELECT 1"}))
7883 .send()
7884 .await
7885 .unwrap();
7886 assert_eq!(
7887 resp.status(),
7888 400,
7889 "statement name starting with a digit must be rejected"
7890 );
7891 }
7892}