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