1pub mod native;
14
15use std::io::{Cursor, Read};
16
17use arrow::ipc::reader::FileReader;
18use arrow::record_batch::RecordBatch;
19use secrecy::ExposeSecret;
20pub use secrecy::SecretString;
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23use zeroize::Zeroizing;
24
25#[derive(Debug, Error)]
29pub enum ClientError {
30 #[error("io/transport error: {0}")]
31 Transport(String),
32 #[error("http {status}: {body}")]
33 Http { status: u16, body: String },
34 #[error("decode error: {0}")]
35 Decode(String),
36 #[error("kit error: {code}: {message}")]
37 Kit {
38 code: KitErrorCode,
39 message: String,
40 op_index: Option<usize>,
41 status: u16,
42 committed: Option<bool>,
43 epoch: Option<u64>,
44 epoch_text: Option<String>,
45 retryable: Option<bool>,
46 },
47 #[error("remote query {code}: {message}")]
48 Query {
49 status: u16,
50 code: RemoteQueryErrorCode,
51 message: String,
52 response: Box<RemoteQueryErrorResponse>,
53 },
54 #[error("query {query_id} outcome unknown after transport loss: {message}")]
55 QueryOutcomeUnknown {
56 query_id: String,
57 message: String,
58 status: Option<Box<RemoteQueryStatus>>,
59 cancel_outcome: Option<RemoteCancelOutcome>,
60 },
61 #[error("native RPC {code}: {category}: {message}")]
62 Native {
63 code: String,
64 category_code: Option<u32>,
65 category: String,
66 message: String,
67 retryable: bool,
68 },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum KitErrorCode {
74 UniqueViolation,
75 FkViolation,
76 CheckViolation,
77 ProcedureNotFound,
78 ProcedureValidation,
79 ProcedureExecution,
80 TriggerNotFound,
81 TriggerValidation,
82 Conflict,
83 AuthRequired,
84 PermissionDenied,
85 DeadlineExceeded,
86 WorkBudgetExceeded,
87 Cancelled,
88 CommitOutcome,
89 QueryOutcomeUnknown,
90 IdempotencyKeyReuseMismatch,
91 IdempotencyStoreFull,
92 IdempotencyStoreUnavailable,
93 InvalidIdempotencyKey,
94 BadRequest,
95 NotFound,
96 Internal,
97 Other,
98}
99
100impl std::fmt::Display for KitErrorCode {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.write_str(self.as_str())
103 }
104}
105
106impl KitErrorCode {
107 fn as_str(&self) -> &'static str {
108 match self {
109 KitErrorCode::UniqueViolation => "UNIQUE_VIOLATION",
110 KitErrorCode::FkViolation => "FK_VIOLATION",
111 KitErrorCode::CheckViolation => "CHECK_VIOLATION",
112 KitErrorCode::ProcedureNotFound => "PROCEDURE_NOT_FOUND",
113 KitErrorCode::ProcedureValidation => "PROCEDURE_VALIDATION",
114 KitErrorCode::ProcedureExecution => "PROCEDURE_EXECUTION",
115 KitErrorCode::TriggerNotFound => "TRIGGER_NOT_FOUND",
116 KitErrorCode::TriggerValidation => "TRIGGER_VALIDATION",
117 KitErrorCode::Conflict => "CONFLICT",
118 KitErrorCode::AuthRequired => "AUTH_REQUIRED",
119 KitErrorCode::PermissionDenied => "PERMISSION_DENIED",
120 KitErrorCode::DeadlineExceeded => "DEADLINE_EXCEEDED",
121 KitErrorCode::WorkBudgetExceeded => "WORK_BUDGET_EXCEEDED",
122 KitErrorCode::Cancelled => "CANCELLED",
123 KitErrorCode::CommitOutcome => "COMMIT_OUTCOME",
124 KitErrorCode::QueryOutcomeUnknown => "QUERY_OUTCOME_UNKNOWN",
125 KitErrorCode::IdempotencyKeyReuseMismatch => "IDEMPOTENCY_KEY_REUSE_MISMATCH",
126 KitErrorCode::IdempotencyStoreFull => "IDEMPOTENCY_STORE_FULL",
127 KitErrorCode::IdempotencyStoreUnavailable => "IDEMPOTENCY_STORE_UNAVAILABLE",
128 KitErrorCode::InvalidIdempotencyKey => "INVALID_IDEMPOTENCY_KEY",
129 KitErrorCode::BadRequest => "BAD_REQUEST",
130 KitErrorCode::NotFound => "NOT_FOUND",
131 KitErrorCode::Internal => "INTERNAL",
132 KitErrorCode::Other => "OTHER",
133 }
134 }
135
136 fn from_str(s: &str) -> Self {
137 match s {
138 "UNIQUE_VIOLATION" => KitErrorCode::UniqueViolation,
139 "FK_VIOLATION" => KitErrorCode::FkViolation,
140 "CHECK_VIOLATION" => KitErrorCode::CheckViolation,
141 "PROCEDURE_NOT_FOUND" => KitErrorCode::ProcedureNotFound,
142 "PROCEDURE_VALIDATION" => KitErrorCode::ProcedureValidation,
143 "PROCEDURE_EXECUTION" => KitErrorCode::ProcedureExecution,
144 "TRIGGER_NOT_FOUND" => KitErrorCode::TriggerNotFound,
145 "TRIGGER_VALIDATION" => KitErrorCode::TriggerValidation,
146 "CONFLICT" => KitErrorCode::Conflict,
147 "AUTH_REQUIRED" => KitErrorCode::AuthRequired,
148 "PERMISSION_DENIED" => KitErrorCode::PermissionDenied,
149 "DEADLINE_EXCEEDED" => KitErrorCode::DeadlineExceeded,
150 "WORK_BUDGET_EXCEEDED" => KitErrorCode::WorkBudgetExceeded,
151 "CANCELLED" => KitErrorCode::Cancelled,
152 "COMMIT_OUTCOME" => KitErrorCode::CommitOutcome,
153 "QUERY_OUTCOME_UNKNOWN" => KitErrorCode::QueryOutcomeUnknown,
154 "IDEMPOTENCY_KEY_REUSE_MISMATCH" => KitErrorCode::IdempotencyKeyReuseMismatch,
155 "IDEMPOTENCY_STORE_FULL" => KitErrorCode::IdempotencyStoreFull,
156 "IDEMPOTENCY_STORE_UNAVAILABLE" => KitErrorCode::IdempotencyStoreUnavailable,
157 "INVALID_IDEMPOTENCY_KEY" => KitErrorCode::InvalidIdempotencyKey,
158 "BAD_REQUEST" => KitErrorCode::BadRequest,
159 "NOT_FOUND" => KitErrorCode::NotFound,
160 "INTERNAL" => KitErrorCode::Internal,
161 _ => KitErrorCode::Other,
162 }
163 }
164}
165
166impl From<reqwest::Error> for ClientError {
167 fn from(e: reqwest::Error) -> Self {
168 ClientError::Transport(e.to_string())
169 }
170}
171
172impl From<std::io::Error> for ClientError {
173 fn from(e: std::io::Error) -> Self {
174 ClientError::Transport(e.to_string())
175 }
176}
177
178pub type ClientResult<T> = std::result::Result<T, ClientError>;
179
180const MAX_CONTROL_RESPONSE_BYTES: u64 = 1024 * 1024;
181const MAX_SQL_RESPONSE_BYTES: u64 = 65 * 1024 * 1024;
184const MAX_REPLICATION_WAL_RESPONSE_BYTES: u64 = 256 * 1024 * 1024;
185const MAX_REPLICATION_SNAPSHOT_BYTES: u64 = 512 * 1024 * 1024;
186
187struct StrictJsonValue(serde_json::Value);
188
189impl<'de> Deserialize<'de> for StrictJsonValue {
190 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191 where
192 D: serde::Deserializer<'de>,
193 {
194 struct Visitor;
195
196 impl<'de> serde::de::Visitor<'de> for Visitor {
197 type Value = StrictJsonValue;
198
199 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 formatter.write_str("a JSON value without duplicate object keys")
201 }
202
203 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
204 Ok(StrictJsonValue(value.into()))
205 }
206
207 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
208 Ok(StrictJsonValue(value.into()))
209 }
210
211 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
212 Ok(StrictJsonValue(value.into()))
213 }
214
215 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
216 where
217 E: serde::de::Error,
218 {
219 serde_json::Number::from_f64(value)
220 .map(serde_json::Value::Number)
221 .map(StrictJsonValue)
222 .ok_or_else(|| E::custom("non-finite JSON number"))
223 }
224
225 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
226 Ok(StrictJsonValue(value.into()))
227 }
228
229 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
230 Ok(StrictJsonValue(value.into()))
231 }
232
233 fn visit_none<E>(self) -> Result<Self::Value, E> {
234 Ok(StrictJsonValue(serde_json::Value::Null))
235 }
236
237 fn visit_unit<E>(self) -> Result<Self::Value, E> {
238 Ok(StrictJsonValue(serde_json::Value::Null))
239 }
240
241 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
242 where
243 D: serde::Deserializer<'de>,
244 {
245 <StrictJsonValue as Deserialize>::deserialize(deserializer)
246 }
247
248 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
249 where
250 A: serde::de::SeqAccess<'de>,
251 {
252 let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
253 while let Some(value) = sequence.next_element::<StrictJsonValue>()? {
254 values.push(value.0);
255 }
256 Ok(StrictJsonValue(serde_json::Value::Array(values)))
257 }
258
259 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
260 where
261 A: serde::de::MapAccess<'de>,
262 {
263 let mut values = serde_json::Map::with_capacity(map.size_hint().unwrap_or(0));
264 while let Some(key) = map.next_key::<String>()? {
265 if values.contains_key(&key) {
266 return Err(serde::de::Error::custom(format!(
267 "duplicate JSON object key {key:?}"
268 )));
269 }
270 let value = map.next_value::<StrictJsonValue>()?;
271 values.insert(key, value.0);
272 }
273 Ok(StrictJsonValue(serde_json::Value::Object(values)))
274 }
275 }
276
277 deserializer.deserialize_any(Visitor)
278 }
279}
280
281fn strict_json_value(bytes: &[u8]) -> Result<serde_json::Value, String> {
282 let mut deserializer = serde_json::Deserializer::from_slice(bytes);
283 let value =
284 StrictJsonValue::deserialize(&mut deserializer).map_err(|error| error.to_string())?;
285 deserializer.end().map_err(|error| error.to_string())?;
286 Ok(value.0)
287}
288
289fn strict_json<T: serde::de::DeserializeOwned>(bytes: &[u8], context: &str) -> ClientResult<T> {
290 let value = strict_json_value(bytes)
291 .map_err(|error| ClientError::Decode(format!("invalid {context} response: {error}")))?;
292 serde_json::from_value(value)
293 .map_err(|error| ClientError::Decode(format!("invalid {context} response: {error}")))
294}
295
296fn strict_roundtrip_json<T>(bytes: &[u8], context: &str) -> ClientResult<T>
297where
298 T: serde::de::DeserializeOwned + Serialize,
299{
300 let value = strict_json_value(bytes)
301 .map_err(|error| ClientError::Decode(format!("invalid {context}: {error}")))?;
302 let parsed: T = serde_json::from_value(value.clone())
303 .map_err(|error| ClientError::Decode(format!("invalid {context}: {error}")))?;
304 if serde_json::to_value(&parsed)
305 .map_err(|error| ClientError::Decode(format!("invalid {context}: {error}")))?
306 != value
307 {
308 return Err(ClientError::Decode(format!(
309 "invalid {context}: unknown or non-canonical fields"
310 )));
311 }
312 Ok(parsed)
313}
314
315fn bounded_blocking_bytes(
316 mut response: reqwest::blocking::Response,
317 limit: u64,
318) -> Result<Vec<u8>, String> {
319 if response
320 .content_length()
321 .is_some_and(|length| length > limit)
322 {
323 return Err(format!("response exceeds {limit} bytes"));
324 }
325 let mut bytes = Vec::new();
326 response
327 .by_ref()
328 .take(limit.saturating_add(1))
329 .read_to_end(&mut bytes)
330 .map_err(|error| error.to_string())?;
331 if bytes.len() as u64 > limit {
332 return Err(format!("response exceeds {limit} bytes"));
333 }
334 Ok(bytes)
335}
336
337async fn bounded_async_bytes(
338 mut response: reqwest::Response,
339 limit: u64,
340) -> Result<Vec<u8>, String> {
341 if response
342 .content_length()
343 .is_some_and(|length| length > limit)
344 {
345 return Err(format!("response exceeds {limit} bytes"));
346 }
347 let mut bytes = Vec::new();
348 while let Some(chunk) = response.chunk().await.map_err(|error| error.to_string())? {
349 if (bytes.len() as u64).saturating_add(chunk.len() as u64) > limit {
350 return Err(format!("response exceeds {limit} bytes"));
351 }
352 bytes.extend_from_slice(&chunk);
353 }
354 Ok(bytes)
355}
356
357fn decode_blocking_json<T: serde::de::DeserializeOwned>(
358 response: reqwest::blocking::Response,
359 limit: u64,
360 context: &str,
361) -> ClientResult<T> {
362 let bytes = bounded_blocking_bytes(response, limit)
363 .map_err(|error| ClientError::Decode(format!("invalid {context} response: {error}")))?;
364 strict_json(&bytes, context)
365}
366
367async fn decode_async_json<T: serde::de::DeserializeOwned>(
368 response: reqwest::Response,
369 limit: u64,
370 context: &str,
371) -> ClientResult<T> {
372 let bytes = bounded_async_bytes(response, limit)
373 .await
374 .map_err(|error| ClientError::Decode(format!("invalid {context} response: {error}")))?;
375 strict_json(&bytes, context)
376}
377
378#[derive(Clone)]
379pub enum RemoteAuth {
380 Bearer(SecretString),
381 Basic {
382 username: String,
383 password: SecretString,
384 },
385}
386
387impl std::fmt::Debug for RemoteAuth {
388 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 match self {
390 Self::Bearer(_) => formatter.write_str("Bearer([REDACTED])"),
391 Self::Basic { username, .. } => formatter
392 .debug_struct("Basic")
393 .field("username", username)
394 .field("password", &"[REDACTED]")
395 .finish(),
396 }
397 }
398}
399
400#[derive(Debug, Clone, Default)]
401pub struct RemoteOptions {
402 pub auth: Option<RemoteAuth>,
403 pub transport_timeout: Option<std::time::Duration>,
404}
405
406#[derive(Clone)]
407pub struct MongrelClient {
408 base_url: String,
409 client: reqwest::blocking::Client,
410 transport: ClientTransportOptions,
411}
412
413#[derive(Clone, Copy, Default)]
414struct ClientTransportOptions {
415 connect_timeout: Option<std::time::Duration>,
416 request_timeout: Option<std::time::Duration>,
417 pool_idle_timeout: Option<std::time::Duration>,
418}
419
420pub struct MongrelClientBuilder {
423 base_url: String,
424 invalid_base_url: bool,
425 authorization: Option<reqwest::header::HeaderValue>,
426 invalid_authorization: bool,
427 connect_timeout: Option<std::time::Duration>,
428 request_timeout: Option<std::time::Duration>,
429 pool_idle_timeout: Option<std::time::Duration>,
430}
431
432pub struct AsyncMongrelClientBuilder {
434 base_url: String,
435 invalid_base_url: bool,
436 authorization: Option<reqwest::header::HeaderValue>,
437 invalid_authorization: bool,
438 connect_timeout: Option<std::time::Duration>,
439 request_timeout: Option<std::time::Duration>,
440 pool_idle_timeout: Option<std::time::Duration>,
441}
442
443fn bearer_header(token: &str) -> ClientResult<reqwest::header::HeaderValue> {
444 let value = Zeroizing::new(format!("Bearer {token}"));
445 sensitive_header(value.as_str())
446}
447
448fn basic_header(username: &str, password: &str) -> ClientResult<reqwest::header::HeaderValue> {
449 let credentials = Zeroizing::new(format!("{username}:{password}"));
450 let encoded = Zeroizing::new(base64::Engine::encode(
451 &base64::engine::general_purpose::STANDARD,
452 credentials.as_bytes(),
453 ));
454 let value = Zeroizing::new(format!("Basic {}", encoded.as_str()));
455 sensitive_header(value.as_str())
456}
457
458fn sensitive_header(value: &str) -> ClientResult<reqwest::header::HeaderValue> {
459 let mut value = reqwest::header::HeaderValue::from_str(value)
460 .map_err(|_| ClientError::Transport("invalid authorization credentials".into()))?;
461 value.set_sensitive(true);
462 Ok(value)
463}
464
465fn default_headers(
466 authorization: Option<reqwest::header::HeaderValue>,
467) -> reqwest::header::HeaderMap {
468 let mut headers = reqwest::header::HeaderMap::new();
469 if let Some(value) = authorization {
470 headers.insert(reqwest::header::AUTHORIZATION, value);
471 }
472 headers
473}
474
475fn url_with_segments(base_url: &str, segments: &[&str]) -> ClientResult<String> {
476 let mut url = reqwest::Url::parse(base_url)
477 .map_err(|_| ClientError::Transport("invalid client base URL".into()))?;
478 {
479 let mut path = url
480 .path_segments_mut()
481 .map_err(|_| ClientError::Transport("client base URL cannot contain a path".into()))?;
482 path.pop_if_empty();
483 path.extend(segments.iter().copied());
484 }
485 Ok(url.into())
486}
487
488fn blocking_http_client(
489 authorization: Option<reqwest::header::HeaderValue>,
490 transport: ClientTransportOptions,
491) -> ClientResult<reqwest::blocking::Client> {
492 let mut builder =
493 reqwest::blocking::Client::builder().default_headers(default_headers(authorization));
494 if let Some(timeout) = transport.connect_timeout {
495 builder = builder.connect_timeout(timeout);
496 }
497 if let Some(timeout) = transport.request_timeout {
498 builder = builder.timeout(timeout);
499 }
500 if let Some(timeout) = transport.pool_idle_timeout {
501 builder = builder.pool_idle_timeout(timeout);
502 }
503 Ok(builder.build()?)
504}
505
506fn async_http_client(
507 authorization: Option<reqwest::header::HeaderValue>,
508 transport: ClientTransportOptions,
509) -> ClientResult<reqwest::Client> {
510 let mut builder = reqwest::Client::builder().default_headers(default_headers(authorization));
511 if let Some(timeout) = transport.connect_timeout {
512 builder = builder.connect_timeout(timeout);
513 }
514 if let Some(timeout) = transport.request_timeout {
515 builder = builder.timeout(timeout);
516 }
517 if let Some(timeout) = transport.pool_idle_timeout {
518 builder = builder.pool_idle_timeout(timeout);
519 }
520 Ok(builder.build()?)
521}
522
523fn sanitized_base_url(value: &str) -> Option<String> {
524 let url = reqwest::Url::parse(value).ok()?;
525 if !matches!(url.scheme(), "http" | "https")
526 || url.host_str().is_none()
527 || !url.username().is_empty()
528 || url.password().is_some()
529 || url.query().is_some()
530 || url.fragment().is_some()
531 {
532 return None;
533 }
534 Some(url.as_str().trim_end_matches('/').to_owned())
535}
536
537impl MongrelClientBuilder {
538 pub fn connect_timeout(mut self, timeout: std::time::Duration) -> Self {
539 self.connect_timeout = Some(timeout);
540 self
541 }
542
543 pub fn request_timeout(mut self, timeout: std::time::Duration) -> Self {
544 self.request_timeout = Some(timeout);
545 self
546 }
547
548 pub fn pool_idle_timeout(mut self, timeout: std::time::Duration) -> Self {
549 self.pool_idle_timeout = Some(timeout);
550 self
551 }
552
553 pub fn bearer_token(mut self, token: impl AsRef<str>) -> Self {
554 match bearer_header(token.as_ref()) {
555 Ok(value) => self.authorization = Some(value),
556 Err(_) => self.invalid_authorization = true,
557 }
558 self
559 }
560
561 pub fn basic_auth(mut self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
562 match basic_header(username.as_ref(), password.as_ref()) {
563 Ok(value) => self.authorization = Some(value),
564 Err(_) => self.invalid_authorization = true,
565 }
566 self
567 }
568
569 pub fn build(self) -> ClientResult<MongrelClient> {
570 if self.invalid_base_url {
571 return Err(ClientError::Transport(
572 "invalid MongrelDB server URL; use HTTP(S) without credentials, query, or fragment"
573 .into(),
574 ));
575 }
576 if self.invalid_authorization {
577 return Err(ClientError::Transport(
578 "invalid authorization credentials".into(),
579 ));
580 }
581 let transport = ClientTransportOptions {
582 connect_timeout: self.connect_timeout,
583 request_timeout: self.request_timeout,
584 pool_idle_timeout: self.pool_idle_timeout,
585 };
586 let client = blocking_http_client(self.authorization, transport)?;
587 Ok(MongrelClient {
588 base_url: self.base_url,
589 client,
590 transport,
591 })
592 }
593}
594
595impl AsyncMongrelClientBuilder {
596 pub fn connect_timeout(mut self, timeout: std::time::Duration) -> Self {
597 self.connect_timeout = Some(timeout);
598 self
599 }
600
601 pub fn request_timeout(mut self, timeout: std::time::Duration) -> Self {
602 self.request_timeout = Some(timeout);
603 self
604 }
605
606 pub fn pool_idle_timeout(mut self, timeout: std::time::Duration) -> Self {
607 self.pool_idle_timeout = Some(timeout);
608 self
609 }
610
611 pub fn bearer_token(mut self, token: impl AsRef<str>) -> Self {
612 match bearer_header(token.as_ref()) {
613 Ok(value) => self.authorization = Some(value),
614 Err(_) => self.invalid_authorization = true,
615 }
616 self
617 }
618
619 pub fn basic_auth(mut self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
620 match basic_header(username.as_ref(), password.as_ref()) {
621 Ok(value) => self.authorization = Some(value),
622 Err(_) => self.invalid_authorization = true,
623 }
624 self
625 }
626
627 pub fn build(self) -> ClientResult<AsyncMongrelClient> {
628 if self.invalid_base_url {
629 return Err(ClientError::Transport(
630 "invalid MongrelDB server URL; use HTTP(S) without credentials, query, or fragment"
631 .into(),
632 ));
633 }
634 if self.invalid_authorization {
635 return Err(ClientError::Transport(
636 "invalid authorization credentials".into(),
637 ));
638 }
639 let transport = ClientTransportOptions {
640 connect_timeout: self.connect_timeout,
641 request_timeout: self.request_timeout,
642 pool_idle_timeout: self.pool_idle_timeout,
643 };
644 let client = async_http_client(self.authorization, transport)?;
645 Ok(AsyncMongrelClient {
646 base_url: self.base_url,
647 client,
648 transport,
649 })
650 }
651}
652
653impl MongrelClient {
654 pub fn try_with_bearer_token(mut self, token: impl AsRef<str>) -> ClientResult<Self> {
655 self.client = blocking_http_client(Some(bearer_header(token.as_ref())?), self.transport)?;
656 Ok(self)
657 }
658
659 pub fn with_bearer_token(self, token: impl AsRef<str>) -> ClientResult<Self> {
660 self.try_with_bearer_token(token)
661 }
662
663 pub fn try_with_basic_auth(
664 mut self,
665 username: impl AsRef<str>,
666 password: impl AsRef<str>,
667 ) -> ClientResult<Self> {
668 self.client = blocking_http_client(
669 Some(basic_header(username.as_ref(), password.as_ref())?),
670 self.transport,
671 )?;
672 Ok(self)
673 }
674
675 pub fn with_basic_auth(
676 self,
677 username: impl AsRef<str>,
678 password: impl AsRef<str>,
679 ) -> ClientResult<Self> {
680 self.try_with_basic_auth(username, password)
681 }
682}
683
684#[derive(Clone)]
686pub struct AsyncMongrelClient {
687 base_url: String,
688 client: reqwest::Client,
689 transport: ClientTransportOptions,
690}
691
692const SQL_RECOVERY_WINDOW: std::time::Duration = std::time::Duration::from_secs(2);
693const SQL_RECOVERY_REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(250);
694const SQL_RECOVERY_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(25);
695
696#[derive(Debug, Clone, Default)]
697pub struct SqlClientOptions {
698 pub query_id: Option<mongreldb_query::QueryId>,
699 pub timeout: Option<std::time::Duration>,
700}
701
702pub type RemoteSqlControlOptions = SqlClientOptions;
703
704#[derive(Debug, Clone)]
705pub struct SqlPageOptions {
706 pub query_id: Option<mongreldb_query::QueryId>,
707 pub timeout: Option<std::time::Duration>,
708 pub max_output_rows: Option<u64>,
709 pub max_output_bytes: Option<u64>,
710 pub page_size_rows: u64,
711 pub projection: Vec<String>,
712 pub max_page_bytes: Option<u64>,
713 pub max_page_tokens: Option<u64>,
714}
715
716impl SqlPageOptions {
717 pub fn new(page_size_rows: u64, projection: Vec<String>) -> Self {
718 Self {
719 query_id: None,
720 timeout: None,
721 max_output_rows: None,
722 max_output_bytes: None,
723 page_size_rows,
724 projection,
725 max_page_bytes: None,
726 max_page_tokens: None,
727 }
728 }
729}
730
731fn validate_sql_page_options(options: &SqlPageOptions) -> ClientResult<()> {
732 if options.page_size_rows == 0
733 || options.max_output_rows == Some(0)
734 || options.max_output_bytes == Some(0)
735 || options.max_page_bytes == Some(0)
736 || options.max_page_tokens == Some(0)
737 {
738 return Err(ClientError::Decode(
739 "SQL pagination row, byte, and token limits must be positive".into(),
740 ));
741 }
742 if options.projection.is_empty() || options.projection.len() > 128 {
743 return Err(ClientError::Decode(
744 "SQL pagination projection must contain 1 to 128 columns".into(),
745 ));
746 }
747 let mut seen = std::collections::HashSet::new();
748 let projection_bytes = options
749 .projection
750 .iter()
751 .map(String::len)
752 .fold(0usize, usize::saturating_add);
753 if projection_bytes > 16 * 1024
754 || options.projection.iter().any(|column| {
755 column.is_empty()
756 || column == "*"
757 || column.len() > 256
758 || !seen.insert(column.as_str())
759 })
760 {
761 return Err(ClientError::Decode(
762 "SQL pagination projection requires unique explicit column names of at most 256 bytes"
763 .into(),
764 ));
765 }
766 Ok(())
767}
768
769fn validate_remote_sql_page(
770 page: RemoteSqlPage,
771 initial_options: Option<&SqlPageOptions>,
772) -> Result<RemoteSqlPage, String> {
773 let metadata = &page.page;
774 let end = metadata
775 .offset
776 .checked_add(metadata.row_count)
777 .ok_or_else(|| "SQL page offset overflowed".to_owned())?;
778 if page.status != "completed" {
779 return Err("SQL page status is not completed".into());
780 }
781 if metadata.row_count != page.rows.len() {
782 return Err("SQL page row_count does not match rows".into());
783 }
784 if page.rows.iter().any(|row| !row.is_object()) {
785 return Err("SQL page rows must be JSON objects".into());
786 }
787 if metadata.projection.is_empty()
788 || metadata.projection.iter().any(|column| column.is_empty())
789 || metadata
790 .projection
791 .iter()
792 .collect::<std::collections::HashSet<_>>()
793 .len()
794 != metadata.projection.len()
795 || page.rows.iter().any(|row| match row.as_object() {
796 Some(object) => {
797 object.len() != metadata.projection.len()
798 || metadata
799 .projection
800 .iter()
801 .any(|column| !object.contains_key(column))
802 }
803 None => true,
804 })
805 {
806 return Err("SQL page rows do not exactly match the projection".into());
807 }
808 let byte_count = page.rows.iter().try_fold(2_usize, |bytes, row| {
809 serde_json::to_vec(row)
810 .map(|encoded| {
811 bytes
812 .saturating_add(usize::from(bytes > 2))
813 .saturating_add(encoded.len())
814 })
815 .map_err(|error| error.to_string())
816 })?;
817 if metadata.byte_count != byte_count
818 || metadata.estimated_tokens != byte_count.saturating_add(3) / 4
819 {
820 return Err("SQL page byte or token estimate is invalid".into());
821 }
822 if metadata.offset > metadata.total_rows || end > metadata.total_rows {
823 return Err("SQL page offset or row_count exceeds total_rows".into());
824 }
825 if metadata.limits.rows == 0
826 || metadata.limits.bytes == 0
827 || metadata.limits.tokens == 0
828 || metadata.row_count > metadata.limits.rows
829 || metadata.byte_count > metadata.limits.bytes
830 || metadata.estimated_tokens > metadata.limits.tokens
831 {
832 return Err("SQL page exceeds its declared limits".into());
833 }
834 if metadata.expires_at_ms == 0
835 || metadata.snapshot != "retained_result"
836 || metadata.token_estimate != "ceil(projected_json_bytes/4)"
837 {
838 return Err("SQL page metadata is invalid".into());
839 }
840 let has_more = end < metadata.total_rows;
841 if (has_more && metadata.row_count == 0)
842 || has_more != page.next_cursor.is_some()
843 || page
844 .next_cursor
845 .as_ref()
846 .is_some_and(|cursor| cursor.is_empty() || cursor.len() > 2_048)
847 {
848 return Err("SQL page continuation cursor is inconsistent".into());
849 }
850 if let Some(options) = initial_options {
851 if metadata.offset != 0
852 || metadata.projection != options.projection
853 || metadata.limits.rows as u64 > options.page_size_rows
854 || options
855 .max_page_bytes
856 .is_some_and(|limit| metadata.limits.bytes as u64 > limit)
857 || options
858 .max_page_tokens
859 .is_some_and(|limit| metadata.limits.tokens as u64 > limit)
860 || options
861 .max_output_rows
862 .is_some_and(|limit| metadata.total_rows as u64 > limit)
863 || options
864 .max_output_bytes
865 .is_some_and(|limit| metadata.byte_count as u64 > limit)
866 {
867 return Err("SQL page does not match the requested pagination options".into());
868 }
869 }
870 Ok(page)
871}
872
873#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
874#[serde(rename_all = "snake_case")]
875pub enum RemoteCancelOutcome {
876 Accepted,
877 AlreadyCancelling,
878 TooLate,
879 AlreadyFinished,
880 NotFound,
881 PreCancelled,
882}
883
884fn cancel_outcome_from_wire(value: Option<&str>) -> Option<RemoteCancelOutcome> {
885 match value {
886 Some("accepted" | "cancellation_requested") => Some(RemoteCancelOutcome::Accepted),
887 Some("already_cancelling" | "cancelling") => Some(RemoteCancelOutcome::AlreadyCancelling),
888 Some("too_late" | "commit_critical") => Some(RemoteCancelOutcome::TooLate),
889 Some("already_finished" | "finished") => Some(RemoteCancelOutcome::AlreadyFinished),
890 Some("pre_cancelled") => Some(RemoteCancelOutcome::PreCancelled),
891 Some("not_found") => Some(RemoteCancelOutcome::NotFound),
892 _ => None,
893 }
894}
895
896fn decode_cancel_outcome(
897 body: &serde_json::Value,
898 expected_query_id: mongreldb_query::QueryId,
899 http_status: u16,
900) -> ClientResult<RemoteCancelOutcome> {
901 const FIELDS: &[&str] = &[
902 "query_id",
903 "status",
904 "terminal_state",
905 "state",
906 "server_state",
907 "code",
908 "operation",
909 "committed",
910 "committed_statements",
911 "last_commit_epoch",
912 "last_commit_epoch_text",
913 "first_commit_statement_index",
914 "last_commit_statement_index",
915 "completed_statements",
916 "statement_index",
917 "cancel_outcome",
918 "cancellation_reason",
919 "retryable",
920 "outcome",
921 "terminal_error",
922 "error",
923 "trace",
924 "started_ms_ago",
925 "deadline_ms_remaining",
926 "session_id",
927 ];
928 let object = body
929 .as_object()
930 .ok_or_else(|| ClientError::Decode("cancellation response is not an object".into()))?;
931 if let Some(field) = object
932 .keys()
933 .find(|field| !FIELDS.contains(&field.as_str()))
934 {
935 return Err(ClientError::Decode(format!(
936 "cancellation response contains unknown field {field:?}"
937 )));
938 }
939 if let Some(outcome) = object.get("outcome") {
940 serde_json::from_value::<RemoteQueryOutcome>(outcome.clone()).map_err(|error| {
941 ClientError::Decode(format!("invalid cancellation outcome: {error}"))
942 })?;
943 }
944 if let Some(error) = object.get("error") {
945 serde_json::from_value::<RemoteQueryErrorBody>(error.clone())
946 .map_err(|error| ClientError::Decode(format!("invalid cancellation error: {error}")))?;
947 }
948 if let Some(error) = object
949 .get("terminal_error")
950 .filter(|value| !value.is_null())
951 {
952 serde_json::from_value::<RemoteTerminalError>(error.clone()).map_err(|error| {
953 ClientError::Decode(format!("invalid cancellation terminal error: {error}"))
954 })?;
955 }
956 if body.get("query_id").and_then(serde_json::Value::as_str)
957 != Some(expected_query_id.to_string().as_str())
958 {
959 return Err(ClientError::Decode(
960 "cancellation response query_id does not match the request".into(),
961 ));
962 }
963 let outcome = cancel_outcome_from_wire(
964 body.get("cancel_outcome")
965 .and_then(serde_json::Value::as_str),
966 );
967 let state = cancel_outcome_from_wire(body.get("state").and_then(serde_json::Value::as_str));
968 if outcome.is_some() && state.is_some() && outcome != state {
969 return Err(ClientError::Decode(
970 "cancellation response state and cancel_outcome disagree".into(),
971 ));
972 }
973 let outcome = outcome
974 .or(state)
975 .ok_or_else(|| ClientError::Decode("cancellation response has no valid outcome".into()))?;
976 let compatible = matches!(
977 (http_status, outcome),
978 (
979 202,
980 RemoteCancelOutcome::Accepted | RemoteCancelOutcome::PreCancelled
981 ) | (
982 200,
983 RemoteCancelOutcome::AlreadyCancelling | RemoteCancelOutcome::AlreadyFinished
984 ) | (409, RemoteCancelOutcome::TooLate)
985 | (404, RemoteCancelOutcome::NotFound)
986 );
987 if !compatible {
988 return Err(ClientError::Decode(
989 "cancellation HTTP status and outcome disagree".into(),
990 ));
991 }
992 Ok(outcome)
993}
994
995#[derive(Debug, Clone, PartialEq, Eq)]
996pub enum RemoteQueryErrorCode {
997 QueryCancelled,
998 DeadlineExceeded,
999 QueryIdConflict,
1000 QueryRegistryFull,
1001 CancelTooLate,
1002 QueryAlreadyFinished,
1003 QueryNotFound,
1004 TransactionAborted,
1005 NoSqlTransaction,
1006 SavepointNotFound,
1007 CommitOutcome,
1008 QueryFailed,
1009 QueryCancelledAfterCommit,
1010 DeadlineAfterCommit,
1011 ResultLimitExceeded,
1012 SerializationFailed,
1013 SerializationFailedAfterCommit,
1014 SerializationWorkerFailed,
1015 CapabilityUnsupported,
1016 QueryOutcomeUnknown,
1017 InvalidQueryOptions,
1018 IncompatibleSqlControls,
1019 InvalidIdempotencyKey,
1020 IdempotencyKeyReuseMismatch,
1021 IdempotencyRequiresJson,
1022 IdempotencyRequiresSingleWrite,
1023 IdempotencyStoreFull,
1024 IdempotencyUnsupportedInTransaction,
1025 IdempotencyStoreUnavailable,
1026 PaginationRequiresJson,
1027 PaginationRequiresSingleReadQuery,
1028 InvalidPaginationOptions,
1029 InvalidSqlCursor,
1030 SqlCursorExpired,
1031 SqlCursorNotFound,
1032 InvalidSqlProjection,
1033 InvalidPageOffset,
1034 SqlPageStoreFull,
1035 SqlAdmissionClosed,
1036 EntropyUnavailable,
1037 Other(String),
1038}
1039
1040impl RemoteQueryErrorCode {
1041 pub fn as_str(&self) -> &str {
1042 match self {
1043 Self::QueryCancelled => "QUERY_CANCELLED",
1044 Self::DeadlineExceeded => "DEADLINE_EXCEEDED",
1045 Self::QueryIdConflict => "QUERY_ID_CONFLICT",
1046 Self::QueryRegistryFull => "QUERY_REGISTRY_FULL",
1047 Self::CancelTooLate => "CANCEL_TOO_LATE",
1048 Self::QueryAlreadyFinished => "QUERY_ALREADY_FINISHED",
1049 Self::QueryNotFound => "QUERY_NOT_FOUND",
1050 Self::TransactionAborted => "TRANSACTION_ABORTED",
1051 Self::NoSqlTransaction => "NO_SQL_TRANSACTION",
1052 Self::SavepointNotFound => "SAVEPOINT_NOT_FOUND",
1053 Self::CommitOutcome => "COMMIT_OUTCOME",
1054 Self::QueryFailed => "QUERY_FAILED",
1055 Self::QueryCancelledAfterCommit => "QUERY_CANCELLED_AFTER_COMMIT",
1056 Self::DeadlineAfterCommit => "DEADLINE_AFTER_COMMIT",
1057 Self::ResultLimitExceeded => "RESULT_LIMIT_EXCEEDED",
1058 Self::SerializationFailed => "SERIALIZATION_FAILED",
1059 Self::SerializationFailedAfterCommit => "SERIALIZATION_FAILED_AFTER_COMMIT",
1060 Self::SerializationWorkerFailed => "SERIALIZATION_WORKER_FAILED",
1061 Self::CapabilityUnsupported => "CAPABILITY_UNSUPPORTED",
1062 Self::QueryOutcomeUnknown => "QUERY_OUTCOME_UNKNOWN",
1063 Self::InvalidQueryOptions => "INVALID_QUERY_OPTIONS",
1064 Self::IncompatibleSqlControls => "INCOMPATIBLE_SQL_CONTROLS",
1065 Self::InvalidIdempotencyKey => "INVALID_IDEMPOTENCY_KEY",
1066 Self::IdempotencyKeyReuseMismatch => "IDEMPOTENCY_KEY_REUSE_MISMATCH",
1067 Self::IdempotencyRequiresJson => "IDEMPOTENCY_REQUIRES_JSON",
1068 Self::IdempotencyRequiresSingleWrite => "IDEMPOTENCY_REQUIRES_SINGLE_WRITE",
1069 Self::IdempotencyStoreFull => "IDEMPOTENCY_STORE_FULL",
1070 Self::IdempotencyUnsupportedInTransaction => "IDEMPOTENCY_UNSUPPORTED_IN_TRANSACTION",
1071 Self::IdempotencyStoreUnavailable => "IDEMPOTENCY_STORE_UNAVAILABLE",
1072 Self::PaginationRequiresJson => "PAGINATION_REQUIRES_JSON",
1073 Self::PaginationRequiresSingleReadQuery => "PAGINATION_REQUIRES_SINGLE_READ_QUERY",
1074 Self::InvalidPaginationOptions => "INVALID_PAGINATION_OPTIONS",
1075 Self::InvalidSqlCursor => "INVALID_SQL_CURSOR",
1076 Self::SqlCursorExpired => "SQL_CURSOR_EXPIRED",
1077 Self::SqlCursorNotFound => "SQL_CURSOR_NOT_FOUND",
1078 Self::InvalidSqlProjection => "INVALID_SQL_PROJECTION",
1079 Self::InvalidPageOffset => "INVALID_PAGE_OFFSET",
1080 Self::SqlPageStoreFull => "SQL_PAGE_STORE_FULL",
1081 Self::SqlAdmissionClosed => "SQL_ADMISSION_CLOSED",
1082 Self::EntropyUnavailable => "ENTROPY_UNAVAILABLE",
1083 Self::Other(code) => code,
1084 }
1085 }
1086
1087 fn from_code(code: &str) -> Self {
1088 match code {
1089 "QUERY_CANCELLED" => Self::QueryCancelled,
1090 "DEADLINE_EXCEEDED" => Self::DeadlineExceeded,
1091 "QUERY_ID_CONFLICT" => Self::QueryIdConflict,
1092 "QUERY_REGISTRY_FULL" => Self::QueryRegistryFull,
1093 "CANCEL_TOO_LATE" => Self::CancelTooLate,
1094 "QUERY_ALREADY_FINISHED" => Self::QueryAlreadyFinished,
1095 "QUERY_NOT_FOUND" => Self::QueryNotFound,
1096 "TRANSACTION_ABORTED" => Self::TransactionAborted,
1097 "NO_SQL_TRANSACTION" => Self::NoSqlTransaction,
1098 "SAVEPOINT_NOT_FOUND" => Self::SavepointNotFound,
1099 "COMMIT_OUTCOME" => Self::CommitOutcome,
1100 "QUERY_FAILED" => Self::QueryFailed,
1101 "QUERY_CANCELLED_AFTER_COMMIT" => Self::QueryCancelledAfterCommit,
1102 "DEADLINE_AFTER_COMMIT" => Self::DeadlineAfterCommit,
1103 "RESULT_LIMIT_EXCEEDED" => Self::ResultLimitExceeded,
1104 "SERIALIZATION_FAILED" => Self::SerializationFailed,
1105 "SERIALIZATION_FAILED_AFTER_COMMIT" => Self::SerializationFailedAfterCommit,
1106 "SERIALIZATION_WORKER_FAILED" => Self::SerializationWorkerFailed,
1107 "CAPABILITY_UNSUPPORTED" => Self::CapabilityUnsupported,
1108 "QUERY_OUTCOME_UNKNOWN" => Self::QueryOutcomeUnknown,
1109 "INVALID_QUERY_OPTIONS" => Self::InvalidQueryOptions,
1110 "INCOMPATIBLE_SQL_CONTROLS" => Self::IncompatibleSqlControls,
1111 "INVALID_IDEMPOTENCY_KEY" => Self::InvalidIdempotencyKey,
1112 "IDEMPOTENCY_KEY_REUSE_MISMATCH" => Self::IdempotencyKeyReuseMismatch,
1113 "IDEMPOTENCY_REQUIRES_JSON" => Self::IdempotencyRequiresJson,
1114 "IDEMPOTENCY_REQUIRES_SINGLE_WRITE" => Self::IdempotencyRequiresSingleWrite,
1115 "IDEMPOTENCY_STORE_FULL" => Self::IdempotencyStoreFull,
1116 "IDEMPOTENCY_UNSUPPORTED_IN_TRANSACTION" => Self::IdempotencyUnsupportedInTransaction,
1117 "IDEMPOTENCY_STORE_UNAVAILABLE" => Self::IdempotencyStoreUnavailable,
1118 "PAGINATION_REQUIRES_JSON" => Self::PaginationRequiresJson,
1119 "PAGINATION_REQUIRES_SINGLE_READ_QUERY" => Self::PaginationRequiresSingleReadQuery,
1120 "INVALID_PAGINATION_OPTIONS" => Self::InvalidPaginationOptions,
1121 "INVALID_SQL_CURSOR" => Self::InvalidSqlCursor,
1122 "SQL_CURSOR_EXPIRED" => Self::SqlCursorExpired,
1123 "SQL_CURSOR_NOT_FOUND" => Self::SqlCursorNotFound,
1124 "INVALID_SQL_PROJECTION" => Self::InvalidSqlProjection,
1125 "INVALID_PAGE_OFFSET" => Self::InvalidPageOffset,
1126 "SQL_PAGE_STORE_FULL" => Self::SqlPageStoreFull,
1127 "SQL_ADMISSION_CLOSED" => Self::SqlAdmissionClosed,
1128 "ENTROPY_UNAVAILABLE" => Self::EntropyUnavailable,
1129 other => Self::Other(other.into()),
1130 }
1131 }
1132}
1133
1134impl std::fmt::Display for RemoteQueryErrorCode {
1135 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1136 formatter.write_str(self.as_str())
1137 }
1138}
1139
1140impl<'de> Deserialize<'de> for RemoteQueryErrorCode {
1141 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1142 where
1143 D: serde::Deserializer<'de>,
1144 {
1145 let code = String::deserialize(deserializer)?;
1146 Ok(Self::from_code(&code))
1147 }
1148}
1149
1150#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
1152#[serde(deny_unknown_fields)]
1153pub struct RemoteCommitHlc {
1154 pub physical_micros: u64,
1155 #[serde(default)]
1156 pub logical: u32,
1157 #[serde(default)]
1158 pub node_tiebreaker: u32,
1159}
1160
1161#[derive(Debug, Clone, Default, Deserialize)]
1162#[serde(deny_unknown_fields)]
1163pub struct RemoteQueryOutcome {
1164 #[serde(default)]
1165 pub committed: Option<bool>,
1166 #[serde(default)]
1167 pub committed_statements: Option<usize>,
1168 #[serde(default)]
1169 pub last_commit_epoch: Option<u64>,
1170 #[serde(default)]
1171 pub last_commit_epoch_text: Option<String>,
1172 #[serde(default)]
1174 pub last_commit_hlc: Option<RemoteCommitHlc>,
1175 #[serde(default)]
1176 pub first_commit_statement_index: Option<usize>,
1177 #[serde(default)]
1178 pub last_commit_statement_index: Option<usize>,
1179 #[serde(default)]
1180 pub completed_statements: Option<usize>,
1181 #[serde(default)]
1182 pub statement_index: Option<usize>,
1183 #[serde(default)]
1184 pub serialization: String,
1185 #[serde(default)]
1187 pub serialization_state: Option<String>,
1188 #[serde(default)]
1189 pub terminal_state: Option<String>,
1190}
1191
1192#[derive(Debug, Clone, Deserialize)]
1193#[serde(deny_unknown_fields)]
1194pub struct RemoteTerminalError {
1195 pub code: RemoteQueryErrorCode,
1196 pub category: String,
1197}
1198
1199#[derive(Debug, Clone, Deserialize)]
1200#[serde(deny_unknown_fields)]
1201pub struct RemoteQueryErrorBody {
1202 pub code: RemoteQueryErrorCode,
1203 pub message: String,
1204 #[serde(default)]
1205 pub query_id: Option<String>,
1206 #[serde(default)]
1207 pub committed: Option<bool>,
1208 #[serde(default)]
1209 pub retryable: bool,
1210}
1211
1212#[derive(Debug, Clone, Deserialize)]
1213#[serde(deny_unknown_fields)]
1214pub struct RemoteQueryErrorResponse {
1215 #[serde(default)]
1216 pub query_id: Option<String>,
1217 #[serde(default)]
1218 pub status: String,
1219 #[serde(default)]
1220 pub terminal_state: Option<String>,
1221 #[serde(default)]
1222 pub committed: Option<bool>,
1223 #[serde(default)]
1224 pub committed_statements: Option<usize>,
1225 #[serde(default)]
1226 pub last_commit_epoch: Option<u64>,
1227 #[serde(default)]
1228 pub last_commit_epoch_text: Option<String>,
1229 #[serde(default)]
1230 pub first_commit_statement_index: Option<usize>,
1231 #[serde(default)]
1232 pub last_commit_statement_index: Option<usize>,
1233 #[serde(default)]
1234 pub completed_statements: Option<usize>,
1235 #[serde(default)]
1236 pub statement_index: Option<usize>,
1237 #[serde(default)]
1238 pub cancel_outcome: Option<RemoteCancelOutcome>,
1239 #[serde(default)]
1240 pub cancellation_reason: Option<String>,
1241 #[serde(default)]
1242 pub retryable: bool,
1243 #[serde(default)]
1244 pub server_state: Option<String>,
1245 #[serde(default)]
1246 pub outcome: RemoteQueryOutcome,
1247 pub error: RemoteQueryErrorBody,
1248}
1249
1250#[derive(Debug, Clone, Deserialize)]
1251#[serde(deny_unknown_fields)]
1252pub struct SqlCancellationCapabilities {
1253 pub version: u8,
1254 pub client_query_ids: bool,
1255 pub cancel_endpoint: bool,
1256 pub query_status: bool,
1257 #[serde(default)]
1258 pub pre_registration_cancel: bool,
1259 pub stream_disconnect_cancels: bool,
1260}
1261
1262#[derive(Debug, Clone, Deserialize)]
1263#[serde(deny_unknown_fields)]
1264pub struct SqlIdempotencyCapabilities {
1265 pub version: u8,
1266 pub durable_pre_execution_intent: bool,
1267 pub replay_committed_receipt: bool,
1268 pub indeterminate_never_reexecutes: bool,
1269}
1270
1271#[derive(Debug, Clone, Deserialize)]
1272#[serde(deny_unknown_fields)]
1273pub struct SqlPaginationCapabilities {
1274 pub version: u8,
1275 pub continuation_endpoint: String,
1276 pub retained_snapshot: bool,
1277 pub projection_required: bool,
1278 pub byte_and_token_hints: bool,
1279}
1280
1281#[derive(Debug, Clone, Deserialize)]
1282#[serde(deny_unknown_fields)]
1283pub struct ServerCapabilities {
1284 pub sql_cancellation: SqlCancellationCapabilities,
1285 #[serde(default)]
1286 pub sql_idempotency: Option<SqlIdempotencyCapabilities>,
1287 #[serde(default)]
1288 pub sql_pagination: Option<SqlPaginationCapabilities>,
1289}
1290
1291#[derive(Debug, Clone, Deserialize)]
1292#[serde(deny_unknown_fields)]
1293pub struct RemoteQueryStatus {
1294 pub query_id: String,
1295 #[serde(default)]
1296 pub detail: Option<String>,
1297 #[serde(default)]
1298 pub status: String,
1299 #[serde(default)]
1300 pub state: String,
1301 #[serde(default)]
1302 pub server_state: String,
1303 #[serde(default)]
1304 pub terminal_state: Option<String>,
1305 #[serde(default)]
1306 pub operation: String,
1307 #[serde(default)]
1308 pub started_ms_ago: Option<u64>,
1309 #[serde(default)]
1310 pub deadline_ms_remaining: Option<u64>,
1311 #[serde(default)]
1312 pub session_id: Option<String>,
1313 #[serde(default)]
1314 pub code: Option<RemoteQueryErrorCode>,
1315 #[serde(default)]
1316 pub committed: Option<bool>,
1317 #[serde(default)]
1318 pub cancellation_reason: String,
1319 #[serde(default)]
1320 pub committed_statements: Option<usize>,
1321 #[serde(default)]
1322 pub last_commit_epoch: Option<u64>,
1323 #[serde(default)]
1324 pub last_commit_epoch_text: Option<String>,
1325 #[serde(default)]
1326 pub last_commit_hlc: Option<RemoteCommitHlc>,
1327 #[serde(default)]
1328 pub first_commit_statement_index: Option<usize>,
1329 #[serde(default)]
1330 pub last_commit_statement_index: Option<usize>,
1331 #[serde(default)]
1332 pub completed_statements: Option<usize>,
1333 #[serde(default)]
1334 pub statement_index: Option<usize>,
1335 #[serde(default)]
1336 pub cancel_outcome: Option<RemoteCancelOutcome>,
1337 #[serde(default)]
1338 pub retryable: bool,
1339 #[serde(default)]
1340 pub outcome: RemoteQueryOutcome,
1341 #[serde(default)]
1343 pub durable: Option<RemoteQueryOutcome>,
1344 #[serde(default)]
1345 pub terminal_error: Option<RemoteTerminalError>,
1346 #[serde(default)]
1347 pub trace: serde_json::Value,
1348}
1349
1350impl RemoteQueryStatus {
1351 pub fn is_terminal(&self) -> bool {
1352 matches!(
1353 self.server_state_or_state(),
1354 "completed" | "failed" | "cancelled" | "pre_cancelled" | "finished"
1355 )
1356 }
1357
1358 pub fn durably_committed(&self) -> Option<bool> {
1359 match (self.committed, self.outcome.committed) {
1360 (Some(true), _) | (_, Some(true)) => Some(true),
1361 (Some(false), _) | (_, Some(false)) => Some(false),
1362 (None, None) => None,
1363 }
1364 }
1365
1366 pub fn last_commit_hlc(&self) -> Option<&RemoteCommitHlc> {
1368 self.durable
1369 .as_ref()
1370 .and_then(|d| d.last_commit_hlc.as_ref())
1371 .or(self.outcome.last_commit_hlc.as_ref())
1372 .or(self.last_commit_hlc.as_ref())
1373 }
1374
1375 pub fn server_state_or_state(&self) -> &str {
1376 if self.server_state.is_empty() {
1377 &self.state
1378 } else {
1379 &self.server_state
1380 }
1381 }
1382}
1383
1384#[derive(Debug, Clone, Deserialize)]
1385#[serde(deny_unknown_fields)]
1386pub struct RemoteSqlReceipt {
1387 pub query_id: String,
1388 pub original_query_id: String,
1389 pub status: String,
1390 #[serde(default)]
1391 pub terminal_state: Option<String>,
1392 #[serde(default)]
1393 pub server_state: String,
1394 #[serde(default)]
1395 pub cancel_outcome: Option<RemoteCancelOutcome>,
1396 #[serde(default)]
1397 pub cancellation_reason: String,
1398 pub committed: bool,
1399 pub committed_statements: usize,
1400 pub last_commit_epoch: Option<u64>,
1401 #[serde(default)]
1402 pub last_commit_epoch_text: Option<String>,
1403 #[serde(default)]
1404 pub first_commit_statement_index: Option<usize>,
1405 #[serde(default)]
1406 pub last_commit_statement_index: Option<usize>,
1407 #[serde(default)]
1408 pub completed_statements: usize,
1409 #[serde(default)]
1410 pub statement_index: usize,
1411 pub retryable: bool,
1412 pub idempotency_replayed: bool,
1413 pub idempotency_persisted: bool,
1414 pub idempotency_expires_at_ms: u64,
1415 pub outcome: RemoteQueryOutcome,
1416 #[serde(default)]
1417 pub terminal_error: Option<RemoteTerminalError>,
1418 #[serde(default)]
1419 pub commit_receipt: Option<RemoteCommitReceipt>,
1420}
1421
1422#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
1423#[serde(deny_unknown_fields)]
1424pub struct RemoteCommitReceipt {
1425 pub transaction_id: String,
1426 pub commit_ts_physical_micros: u64,
1427 pub commit_ts_logical: u32,
1428 pub commit_ts_node_tiebreaker: u32,
1429 pub log_term: u64,
1430 pub log_index: u64,
1431 pub durability: String,
1432}
1433
1434fn exact_epoch(text: Option<&str>, numeric: Option<u64>) -> Result<Option<u64>, String> {
1435 match text {
1436 Some(text) => {
1437 let epoch = text
1438 .parse::<u64>()
1439 .map_err(|_| "last_commit_epoch_text is not an unsigned integer".to_owned())?;
1440 if epoch.to_string() != text {
1441 return Err("last_commit_epoch_text is not canonical".into());
1442 }
1443 if numeric.is_some_and(|numeric| numeric != epoch) {
1444 return Err("last_commit_epoch and last_commit_epoch_text disagree".into());
1445 }
1446 Ok(Some(epoch))
1447 }
1448 None => Ok(numeric),
1449 }
1450}
1451
1452fn validate_remote_query_status(
1453 mut status: RemoteQueryStatus,
1454 expected_query_id: mongreldb_query::QueryId,
1455) -> Result<RemoteQueryStatus, String> {
1456 const STATES: &[&str] = &[
1457 "queued",
1458 "planning",
1459 "executing",
1460 "streaming",
1461 "serializing",
1462 "commit_critical",
1463 "cancelling",
1464 "completed",
1465 "failed",
1466 "cancelled",
1467 "pre_cancelled",
1468 "finished",
1469 ];
1470 const STATUSES: &[&str] = &[
1471 "running",
1472 "outcome_unknown",
1473 "completed",
1474 "failed_before_commit",
1475 "cancelled_before_commit",
1476 "deadline_before_commit",
1477 "cancelled_before_start",
1478 "committed",
1479 "committed_with_error",
1480 "partially_committed",
1481 "cancelled_after_commit",
1482 "deadline_after_commit",
1483 "finished",
1484 ];
1485 if status.query_id != expected_query_id.to_string() {
1486 return Err("query status query_id does not match the request".into());
1487 }
1488 if status
1489 .detail
1490 .as_deref()
1491 .is_some_and(|detail| detail != "compact")
1492 {
1493 return Err("query status detail is invalid".into());
1494 }
1495 if !STATUSES.contains(&status.status.as_str())
1496 || status.state.is_empty()
1497 || !STATES.contains(&status.state.as_str())
1498 || (!status.server_state.is_empty()
1499 && (!STATES.contains(&status.server_state.as_str())
1500 || status.server_state != status.state))
1501 || status
1502 .terminal_state
1503 .as_ref()
1504 .is_some_and(|terminal| terminal != &status.status)
1505 {
1506 return Err("query status state or status is invalid".into());
1507 }
1508 let state_matches_status = match status.status.as_str() {
1509 "running" => matches!(
1510 status.state.as_str(),
1511 "queued"
1512 | "planning"
1513 | "executing"
1514 | "streaming"
1515 | "serializing"
1516 | "commit_critical"
1517 | "cancelling"
1518 ),
1519 "committed" => !matches!(
1520 status.state.as_str(),
1521 "failed" | "cancelled" | "pre_cancelled" | "finished"
1522 ),
1523 "completed" => status.state == "completed",
1524 "failed_before_commit"
1525 | "committed_with_error"
1526 | "partially_committed"
1527 | "outcome_unknown" => status.state == "failed",
1528 "cancelled_before_commit"
1529 | "deadline_before_commit"
1530 | "cancelled_after_commit"
1531 | "deadline_after_commit" => status.state == "cancelled",
1532 "cancelled_before_start" => status.state == "pre_cancelled",
1533 "finished" => status.state == "finished",
1534 _ => false,
1535 };
1536 if !state_matches_status {
1537 return Err("query status state and status disagree".into());
1538 }
1539 let top_epoch = exact_epoch(
1540 status.last_commit_epoch_text.as_deref(),
1541 status.last_commit_epoch,
1542 )?;
1543 let outcome_epoch = exact_epoch(
1544 status.outcome.last_commit_epoch_text.as_deref(),
1545 status.outcome.last_commit_epoch,
1546 )?;
1547 if status
1548 .last_commit_epoch
1549 .is_some_and(|numeric| Some(numeric) != top_epoch)
1550 || status
1551 .outcome
1552 .last_commit_epoch
1553 .is_some_and(|numeric| Some(numeric) != outcome_epoch)
1554 || top_epoch != outcome_epoch
1555 || status.committed != status.outcome.committed
1556 || status.committed_statements != status.outcome.committed_statements
1557 || status.first_commit_statement_index != status.outcome.first_commit_statement_index
1558 || status.last_commit_statement_index != status.outcome.last_commit_statement_index
1559 || status.completed_statements != status.outcome.completed_statements
1560 || status.statement_index != status.outcome.statement_index
1561 {
1562 return Err("query status top-level and outcome fields disagree".into());
1563 }
1564 match status.committed {
1565 Some(true) => {
1566 if status.committed_statements == Some(0)
1567 || status.committed_statements.is_none()
1568 || top_epoch.is_none()
1569 || status.last_commit_epoch_text.is_none()
1570 || status.outcome.last_commit_epoch_text.is_none()
1571 || status.first_commit_statement_index.is_none()
1572 || status.last_commit_statement_index.is_none()
1573 || status.completed_statements.is_none()
1574 || status.statement_index.is_none()
1575 || !matches!(
1576 status.status.as_str(),
1577 "committed"
1578 | "committed_with_error"
1579 | "partially_committed"
1580 | "cancelled_after_commit"
1581 | "deadline_after_commit"
1582 )
1583 {
1584 return Err("committed query status has invalid durable metadata".into());
1585 }
1586 }
1587 Some(false) => {
1588 if status.committed_statements != Some(0)
1589 || top_epoch.is_some()
1590 || status.first_commit_statement_index.is_some()
1591 || status.last_commit_statement_index.is_some()
1592 || status.completed_statements.is_none()
1593 || status.statement_index.is_none()
1594 || matches!(
1595 status.status.as_str(),
1596 "committed"
1597 | "committed_with_error"
1598 | "partially_committed"
1599 | "cancelled_after_commit"
1600 | "deadline_after_commit"
1601 | "outcome_unknown"
1602 | "finished"
1603 )
1604 {
1605 return Err("non-committed query status has invalid durable metadata".into());
1606 }
1607 }
1608 None => {
1609 if status.committed_statements.is_some()
1610 || top_epoch.is_some()
1611 || status.first_commit_statement_index.is_some()
1612 || status.last_commit_statement_index.is_some()
1613 || status.completed_statements.is_some()
1614 || status.statement_index.is_some()
1615 || !matches!(status.status.as_str(), "outcome_unknown" | "finished")
1616 {
1617 return Err("unknown query status contains durable metadata".into());
1618 }
1619 }
1620 }
1621 if let (Some(first), Some(last), Some(committed)) = (
1622 status.first_commit_statement_index,
1623 status.last_commit_statement_index,
1624 status.committed_statements,
1625 ) {
1626 if first > last
1627 || committed > last.saturating_sub(first).saturating_add(1)
1628 || status
1629 .statement_index
1630 .is_some_and(|statement| last > statement)
1631 {
1632 return Err("query status commit statement indexes are invalid".into());
1633 }
1634 }
1635 if let (Some(completed), Some(statement)) =
1636 (status.completed_statements, status.statement_index)
1637 {
1638 if statement > completed || completed > statement.saturating_add(1) {
1639 return Err("query status statement index and completed count disagree".into());
1640 }
1641 }
1642 let terminal_error = status.terminal_error.as_ref();
1643 if terminal_error.is_some_and(|error| {
1644 error.code.as_str().trim().is_empty()
1645 || !matches!(
1646 error.category.as_str(),
1647 "cancellation" | "deadline" | "result_limit" | "serialization" | "execution"
1648 )
1649 }) {
1650 return Err("query status terminal error fields are invalid".into());
1651 }
1652 let terminal_error_matches = match status.status.as_str() {
1653 "running" | "completed" | "committed" | "finished" => terminal_error.is_none(),
1654 "outcome_unknown" => terminal_error.is_some_and(|error| {
1655 error.code.as_str() == "QUERY_OUTCOME_UNKNOWN" && error.category == "execution"
1656 }),
1657 "cancelled_before_start" | "cancelled_before_commit" => {
1658 terminal_error.is_some_and(|error| {
1659 error.code.as_str() == "QUERY_CANCELLED" && error.category == "cancellation"
1660 })
1661 }
1662 "cancelled_after_commit" => terminal_error.is_some_and(|error| {
1663 error.code.as_str() == "QUERY_CANCELLED_AFTER_COMMIT"
1664 && error.category == "cancellation"
1665 }),
1666 "deadline_before_commit" => terminal_error.is_some_and(|error| {
1667 error.code.as_str() == "DEADLINE_EXCEEDED" && error.category == "deadline"
1668 }),
1669 "deadline_after_commit" => terminal_error.is_some_and(|error| {
1670 error.code.as_str() == "DEADLINE_AFTER_COMMIT" && error.category == "deadline"
1671 }),
1672 "failed_before_commit" | "committed_with_error" | "partially_committed" => {
1673 terminal_error.is_some()
1674 }
1675 _ => false,
1676 };
1677 if !terminal_error_matches {
1678 return Err("query status terminal error disagrees with status".into());
1679 }
1680 if terminal_error.is_some_and(|error| {
1681 (error.category == "cancellation")
1682 != matches!(
1683 error.code.as_str(),
1684 "QUERY_CANCELLED" | "QUERY_CANCELLED_AFTER_COMMIT"
1685 )
1686 || (error.category == "deadline")
1687 != matches!(
1688 error.code.as_str(),
1689 "DEADLINE_EXCEEDED" | "DEADLINE_AFTER_COMMIT"
1690 )
1691 }) {
1692 return Err("query status terminal error code and category disagree".into());
1693 }
1694 let retryable = terminal_error.is_some_and(|error| {
1695 matches!(
1696 error.code.as_str(),
1697 "IDEMPOTENCY_STORE_FULL" | "IDEMPOTENCY_STORE_UNAVAILABLE"
1698 )
1699 });
1700 if status.retryable != retryable {
1701 return Err("query status retryable flag disagrees with terminal error".into());
1702 }
1703 let expected_cancel_outcome = match status.state.as_str() {
1704 "commit_critical" => Some(RemoteCancelOutcome::TooLate),
1705 "cancelling" => Some(RemoteCancelOutcome::Accepted),
1706 "completed" | "failed" | "cancelled" | "finished" => {
1707 Some(RemoteCancelOutcome::AlreadyFinished)
1708 }
1709 "pre_cancelled" => Some(RemoteCancelOutcome::PreCancelled),
1710 _ => None,
1711 };
1712 if status.cancel_outcome != expected_cancel_outcome {
1713 return Err("query status cancel_outcome disagrees with state".into());
1714 }
1715 const CANCELLATION_REASONS: &[&str] = &[
1716 "none",
1717 "client_request",
1718 "client_disconnected",
1719 "session_closed",
1720 "server_shutdown",
1721 "deadline",
1722 ];
1723 let valid_reason = match status.status.as_str() {
1724 "finished" => status.cancellation_reason == "none",
1725 "cancelled_before_start" | "cancelled_before_commit" | "cancelled_after_commit" => {
1726 CANCELLATION_REASONS.contains(&status.cancellation_reason.as_str())
1727 && !matches!(status.cancellation_reason.as_str(), "none" | "deadline")
1728 }
1729 "deadline_before_commit" | "deadline_after_commit" => {
1730 status.cancellation_reason == "deadline"
1731 }
1732 "running" | "committed" if status.state == "cancelling" => {
1733 CANCELLATION_REASONS.contains(&status.cancellation_reason.as_str())
1734 && status.cancellation_reason != "none"
1735 }
1736 _ => status.cancellation_reason == "none",
1737 };
1738 if !valid_reason {
1739 return Err("query status cancellation_reason disagrees with status".into());
1740 }
1741 let serialization = status.outcome.serialization.as_str();
1742 let valid_serialization = match status.status.as_str() {
1743 "finished" | "outcome_unknown" => serialization == "unknown",
1744 "completed" => serialization == "succeeded",
1745 "running" | "committed" => match status.state.as_str() {
1746 "serializing" => serialization == "in_progress",
1747 "cancelling" => matches!(serialization, "not_started" | "in_progress"),
1748 "completed" => serialization == "succeeded",
1749 _ => serialization == "not_started",
1750 },
1751 _ => matches!(serialization, "not_started" | "failed"),
1752 };
1753 if !valid_serialization {
1754 return Err("query status serialization state is invalid".into());
1755 }
1756 let terminal_state_valid = match status.status.as_str() {
1757 "running" | "finished" => status.terminal_state.is_none(),
1758 "committed" if status.state != "completed" => status.terminal_state.is_none(),
1759 _ => status.terminal_state.as_deref() == Some(status.status.as_str()),
1760 };
1761 if !terminal_state_valid {
1762 return Err("query status terminal_state is invalid".into());
1763 }
1764 if matches!(status.state.as_str(), "pre_cancelled" | "finished") {
1765 if !status.operation.is_empty() {
1766 return Err("synthetic query status unexpectedly names an operation".into());
1767 }
1768 } else if status.operation.is_empty() {
1769 return Err("live query status lacks operation".into());
1770 }
1771 status.last_commit_epoch = top_epoch;
1772 status.outcome.last_commit_epoch = outcome_epoch;
1773 Ok(status)
1774}
1775
1776fn validate_remote_query_error(
1777 mut response: RemoteQueryErrorResponse,
1778 expected_query_id: mongreldb_query::QueryId,
1779) -> Result<RemoteQueryErrorResponse, String> {
1780 let expected = expected_query_id.to_string();
1781 if response.query_id.as_deref() != Some(&expected)
1782 || response.error.query_id.as_deref() != Some(&expected)
1783 || response.terminal_state.as_deref() != Some(response.status.as_str())
1784 || response.error.message.is_empty()
1785 {
1786 return Err("query error query_id does not match the request".into());
1787 }
1788 if response.committed != response.outcome.committed
1789 || response.committed != response.error.committed
1790 || response.committed_statements != response.outcome.committed_statements
1791 || response.first_commit_statement_index != response.outcome.first_commit_statement_index
1792 || response.last_commit_statement_index != response.outcome.last_commit_statement_index
1793 || response.completed_statements != response.outcome.completed_statements
1794 || response.statement_index != response.outcome.statement_index
1795 || response.retryable != response.error.retryable
1796 {
1797 return Err("query error top-level, outcome, and error fields disagree".into());
1798 }
1799 let top_epoch = exact_epoch(
1800 response.last_commit_epoch_text.as_deref(),
1801 response.last_commit_epoch,
1802 )?;
1803 let outcome_epoch = exact_epoch(
1804 response.outcome.last_commit_epoch_text.as_deref(),
1805 response.outcome.last_commit_epoch,
1806 )?;
1807 if response
1808 .last_commit_epoch
1809 .is_some_and(|numeric| Some(numeric) != top_epoch)
1810 || response
1811 .outcome
1812 .last_commit_epoch
1813 .is_some_and(|numeric| Some(numeric) != outcome_epoch)
1814 || top_epoch != outcome_epoch
1815 {
1816 return Err("query error top-level and outcome commit epochs disagree".into());
1817 }
1818 let outcome_unknown = response.error.code == RemoteQueryErrorCode::QueryOutcomeUnknown;
1819 match response.committed {
1820 Some(true) => {
1821 if outcome_unknown
1822 || response.committed_statements == Some(0)
1823 || response.committed_statements.is_none()
1824 || top_epoch.is_none()
1825 || response.last_commit_epoch_text.is_none()
1826 || response.outcome.last_commit_epoch_text.is_none()
1827 || response.first_commit_statement_index.is_none()
1828 || response.last_commit_statement_index.is_none()
1829 || response.completed_statements.is_none()
1830 || response.statement_index.is_none()
1831 || !matches!(
1832 response.status.as_str(),
1833 "committed"
1834 | "committed_with_error"
1835 | "partially_committed"
1836 | "cancelled_after_commit"
1837 | "deadline_after_commit"
1838 )
1839 {
1840 return Err("committed query error has invalid durable metadata".into());
1841 }
1842 }
1843 Some(false) => {
1844 if outcome_unknown
1845 || response.committed_statements != Some(0)
1846 || top_epoch.is_some()
1847 || response.first_commit_statement_index.is_some()
1848 || response.last_commit_statement_index.is_some()
1849 || response.completed_statements.is_none()
1850 || response.statement_index.is_none()
1851 || !matches!(
1852 response.status.as_str(),
1853 "failed_before_commit"
1854 | "cancelled_before_commit"
1855 | "deadline_before_commit"
1856 | "cancelled_before_start"
1857 )
1858 {
1859 return Err("non-committed query error has invalid durable metadata".into());
1860 }
1861 }
1862 None => {
1863 if !outcome_unknown
1864 || response.status != "outcome_unknown"
1865 || response.committed_statements.is_some()
1866 || top_epoch.is_some()
1867 || response.first_commit_statement_index.is_some()
1868 || response.last_commit_statement_index.is_some()
1869 || response.completed_statements.is_some()
1870 || response.statement_index.is_some()
1871 || response.retryable
1872 {
1873 return Err("unknown query error contains contradictory metadata".into());
1874 }
1875 }
1876 }
1877 if outcome_unknown && response.status != "outcome_unknown" {
1878 return Err("outcome-unknown error has the wrong status".into());
1879 }
1880 if response.retryable
1881 && (response.committed != Some(false)
1882 || !matches!(
1883 response.error.code.as_str(),
1884 "QUERY_REGISTRY_FULL" | "IDEMPOTENCY_STORE_FULL" | "IDEMPOTENCY_STORE_UNAVAILABLE"
1885 ))
1886 {
1887 return Err("query error retryable flag is unsafe".into());
1888 }
1889 if let (Some(first), Some(last), Some(committed)) = (
1890 response.first_commit_statement_index,
1891 response.last_commit_statement_index,
1892 response.committed_statements,
1893 ) {
1894 if first > last
1895 || committed > last.saturating_sub(first).saturating_add(1)
1896 || response
1897 .statement_index
1898 .is_some_and(|statement| last > statement)
1899 {
1900 return Err("query error commit statement indexes are invalid".into());
1901 }
1902 }
1903 if let (Some(completed), Some(statement)) =
1904 (response.completed_statements, response.statement_index)
1905 {
1906 if statement > completed || completed > statement.saturating_add(1) {
1907 return Err("query error statement index and completed count disagree".into());
1908 }
1909 }
1910 let code_matches = match response.error.code {
1911 RemoteQueryErrorCode::QueryOutcomeUnknown => response.status == "outcome_unknown",
1912 RemoteQueryErrorCode::QueryCancelledAfterCommit => {
1913 response.status == "cancelled_after_commit" && response.committed == Some(true)
1914 }
1915 RemoteQueryErrorCode::DeadlineAfterCommit => {
1916 response.status == "deadline_after_commit" && response.committed == Some(true)
1917 }
1918 RemoteQueryErrorCode::QueryCancelled => matches!(
1919 response.status.as_str(),
1920 "cancelled_before_commit" | "cancelled_before_start"
1921 ),
1922 RemoteQueryErrorCode::DeadlineExceeded => response.status == "deadline_before_commit",
1923 RemoteQueryErrorCode::CommitOutcome
1924 | RemoteQueryErrorCode::SerializationFailedAfterCommit => response.committed == Some(true),
1925 RemoteQueryErrorCode::SerializationFailed => response.committed == Some(false),
1926 _ => true,
1927 };
1928 if !code_matches {
1929 return Err("query error code and status disagree".into());
1930 }
1931 let status_matches_code = match response.status.as_str() {
1932 "outcome_unknown" => response.error.code == RemoteQueryErrorCode::QueryOutcomeUnknown,
1933 "cancelled_after_commit" => {
1934 response.error.code == RemoteQueryErrorCode::QueryCancelledAfterCommit
1935 }
1936 "deadline_after_commit" => response.error.code == RemoteQueryErrorCode::DeadlineAfterCommit,
1937 "cancelled_before_commit" | "cancelled_before_start" => {
1938 response.error.code == RemoteQueryErrorCode::QueryCancelled
1939 }
1940 "deadline_before_commit" => response.error.code == RemoteQueryErrorCode::DeadlineExceeded,
1941 _ => true,
1942 };
1943 if !status_matches_code {
1944 return Err("query error status and code disagree".into());
1945 }
1946 const CANCELLATION_REASONS: &[&str] = &[
1947 "none",
1948 "client_request",
1949 "client_disconnected",
1950 "session_closed",
1951 "server_shutdown",
1952 "deadline",
1953 ];
1954 let cancellation_error = matches!(
1955 response.error.code,
1956 RemoteQueryErrorCode::QueryCancelled | RemoteQueryErrorCode::QueryCancelledAfterCommit
1957 );
1958 let deadline_error = matches!(
1959 response.error.code,
1960 RemoteQueryErrorCode::DeadlineExceeded | RemoteQueryErrorCode::DeadlineAfterCommit
1961 );
1962 let expected_cancel_outcome = if cancellation_error || deadline_error {
1963 Some(RemoteCancelOutcome::Accepted)
1964 } else {
1965 match response.server_state.as_deref() {
1966 Some("commit_critical") => Some(RemoteCancelOutcome::TooLate),
1967 Some("cancelling") => Some(RemoteCancelOutcome::Accepted),
1968 Some("completed" | "failed" | "cancelled") => {
1969 Some(RemoteCancelOutcome::AlreadyFinished)
1970 }
1971 Some(_) | None => None,
1972 }
1973 };
1974 if response.cancel_outcome != expected_cancel_outcome {
1975 return Err("query error cancel_outcome disagrees with its phase".into());
1976 }
1977 let valid_reason = match response.cancellation_reason.as_deref() {
1978 Some("deadline") => deadline_error,
1979 Some(reason) if CANCELLATION_REASONS.contains(&reason) => {
1980 if cancellation_error {
1981 reason != "none"
1982 } else {
1983 response.server_state.is_some() && reason == "none"
1984 }
1985 }
1986 None => response.server_state.is_none() && !cancellation_error && !deadline_error,
1987 Some(_) => false,
1988 };
1989 if !valid_reason {
1990 return Err("query error cancellation_reason is invalid".into());
1991 }
1992 let valid_server_state = match response.server_state.as_deref() {
1993 None => true,
1994 Some("cancelled") => cancellation_error || deadline_error,
1995 Some("failed") => !cancellation_error && !deadline_error,
1996 Some(_) => false,
1997 };
1998 if !valid_server_state {
1999 return Err("query error server_state disagrees with its status".into());
2000 }
2001 let valid_serialization = match response.server_state.as_deref() {
2002 None => response.outcome.serialization == "unknown",
2003 Some(_) if outcome_unknown => response.outcome.serialization == "unknown",
2004 Some("failed" | "cancelled") => {
2005 matches!(
2006 response.outcome.serialization.as_str(),
2007 "not_started" | "failed"
2008 )
2009 }
2010 Some(_) => false,
2011 };
2012 if !valid_serialization {
2013 return Err("query error serialization state is invalid".into());
2014 }
2015 response.last_commit_epoch = top_epoch;
2016 response.outcome.last_commit_epoch = outcome_epoch;
2017 Ok(response)
2018}
2019
2020fn validate_queryless_sql_error(
2021 mut response: RemoteQueryErrorResponse,
2022) -> Result<RemoteQueryErrorResponse, String> {
2023 if response.query_id.is_some()
2024 || response.error.query_id.is_some()
2025 || response.status != "failed_before_commit"
2026 || response.terminal_state.as_deref() != Some("failed_before_commit")
2027 || response.server_state.as_deref() != Some("failed")
2028 || response.committed != Some(false)
2029 || response.error.committed != Some(false)
2030 || response.outcome.committed != Some(false)
2031 || response.committed_statements != Some(0)
2032 || response.outcome.committed_statements != Some(0)
2033 || response.last_commit_epoch.is_some()
2034 || response.last_commit_epoch_text.is_some()
2035 || response.outcome.last_commit_epoch.is_some()
2036 || response.outcome.last_commit_epoch_text.is_some()
2037 || response.first_commit_statement_index.is_some()
2038 || response.last_commit_statement_index.is_some()
2039 || response.outcome.first_commit_statement_index.is_some()
2040 || response.outcome.last_commit_statement_index.is_some()
2041 || response.completed_statements != Some(0)
2042 || response.outcome.completed_statements != Some(0)
2043 || response.statement_index != Some(0)
2044 || response.outcome.statement_index != Some(0)
2045 || response.cancel_outcome.is_some()
2046 || response.cancellation_reason.is_some()
2047 || response.retryable
2048 || response.error.retryable
2049 || response.outcome.serialization != "not_started"
2050 || response.error.message.is_empty()
2051 || !matches!(
2052 response.error.code,
2053 RemoteQueryErrorCode::InvalidSqlCursor
2054 | RemoteQueryErrorCode::SqlCursorExpired
2055 | RemoteQueryErrorCode::SqlCursorNotFound
2056 | RemoteQueryErrorCode::ResultLimitExceeded
2057 | RemoteQueryErrorCode::SerializationFailed
2058 | RemoteQueryErrorCode::SerializationWorkerFailed
2059 | RemoteQueryErrorCode::SqlAdmissionClosed
2060 | RemoteQueryErrorCode::EntropyUnavailable
2061 )
2062 {
2063 return Err("queryless SQL error metadata is invalid".into());
2064 }
2065 response.last_commit_epoch = None;
2066 response.outcome.last_commit_epoch = None;
2067 Ok(response)
2068}
2069
2070fn validate_remote_sql_receipt(
2071 mut receipt: RemoteSqlReceipt,
2072 expected_query_id: mongreldb_query::QueryId,
2073 expected_original_query_id: Option<mongreldb_query::QueryId>,
2074) -> Result<RemoteSqlReceipt, String> {
2075 if receipt.query_id != expected_query_id.to_string()
2076 || receipt.terminal_state.as_deref() != Some(receipt.status.as_str())
2077 {
2078 return Err("receipt query_id does not match the request".into());
2079 }
2080 if receipt
2081 .original_query_id
2082 .parse::<mongreldb_query::QueryId>()
2083 .is_err()
2084 {
2085 return Err("receipt original_query_id is invalid".into());
2086 }
2087 let status_committed = match receipt.status.as_str() {
2088 "completed" => false,
2089 "committed"
2090 | "committed_with_error"
2091 | "partially_committed"
2092 | "cancelled_after_commit"
2093 | "deadline_after_commit" => true,
2094 _ => return Err("receipt status is invalid".into()),
2095 };
2096 let expected_server_state = match receipt.status.as_str() {
2097 "completed" | "committed" => "completed",
2098 "committed_with_error" | "partially_committed" => "failed",
2099 "cancelled_after_commit" | "deadline_after_commit" => "cancelled",
2100 _ => return Err("receipt status is invalid".into()),
2101 };
2102 if receipt.server_state != expected_server_state
2103 || receipt.cancel_outcome != Some(RemoteCancelOutcome::AlreadyFinished)
2104 {
2105 return Err("receipt terminal state metadata is invalid".into());
2106 }
2107 let terminal_error = receipt.terminal_error.as_ref();
2108 let terminal_error_matches = match receipt.status.as_str() {
2109 "completed" | "committed" => terminal_error.is_none(),
2110 "cancelled_after_commit" => terminal_error.is_some_and(|error| {
2111 error.code.as_str() == "QUERY_CANCELLED_AFTER_COMMIT"
2112 && error.category == "cancellation"
2113 }),
2114 "deadline_after_commit" => terminal_error.is_some_and(|error| {
2115 error.code.as_str() == "DEADLINE_AFTER_COMMIT" && error.category == "deadline"
2116 }),
2117 "committed_with_error" | "partially_committed" => terminal_error.is_some(),
2118 _ => false,
2119 };
2120 if !terminal_error_matches
2121 || terminal_error.is_some_and(|error| {
2122 !matches!(
2123 error.category.as_str(),
2124 "cancellation" | "deadline" | "result_limit" | "serialization" | "execution"
2125 ) || (error.category == "cancellation")
2126 != (error.code.as_str() == "QUERY_CANCELLED_AFTER_COMMIT")
2127 || (error.category == "deadline")
2128 != (error.code.as_str() == "DEADLINE_AFTER_COMMIT")
2129 || (error.category == "serialization")
2130 != (error.code.as_str() == "SERIALIZATION_FAILED_AFTER_COMMIT")
2131 })
2132 {
2133 return Err("receipt terminal error disagrees with status".into());
2134 }
2135 let valid_reason = match receipt.status.as_str() {
2136 "cancelled_after_commit" => matches!(
2137 receipt.cancellation_reason.as_str(),
2138 "client_request" | "client_disconnected" | "session_closed" | "server_shutdown"
2139 ),
2140 "deadline_after_commit" => receipt.cancellation_reason == "deadline",
2141 _ => receipt.cancellation_reason == "none",
2142 };
2143 let serialization = receipt.outcome.serialization.as_str();
2144 let valid_serialization = matches!(serialization, "not_started" | "succeeded" | "failed")
2145 && match receipt.status.as_str() {
2146 "completed" | "committed" => serialization == "succeeded",
2147 _ if terminal_error.is_some_and(|error| error.category == "serialization") => {
2148 serialization == "failed"
2149 }
2150 _ => serialization != "succeeded",
2151 };
2152 if !valid_reason || !valid_serialization {
2153 return Err("receipt cancellation or serialization metadata is invalid".into());
2154 }
2155 if receipt.committed != status_committed
2156 || receipt.outcome.committed != Some(receipt.committed)
2157 || receipt.outcome.committed_statements != Some(receipt.committed_statements)
2158 || receipt.outcome.first_commit_statement_index != receipt.first_commit_statement_index
2159 || receipt.outcome.last_commit_statement_index != receipt.last_commit_statement_index
2160 || receipt.outcome.completed_statements != Some(receipt.completed_statements)
2161 || receipt.outcome.statement_index != Some(receipt.statement_index)
2162 {
2163 return Err("receipt top-level and outcome fields disagree".into());
2164 }
2165 let top_epoch = exact_epoch(
2166 receipt.last_commit_epoch_text.as_deref(),
2167 receipt.last_commit_epoch,
2168 )?;
2169 let outcome_epoch = exact_epoch(
2170 receipt.outcome.last_commit_epoch_text.as_deref(),
2171 receipt.outcome.last_commit_epoch,
2172 )?;
2173 if receipt
2174 .last_commit_epoch
2175 .is_some_and(|numeric| Some(numeric) != top_epoch)
2176 || receipt
2177 .outcome
2178 .last_commit_epoch
2179 .is_some_and(|numeric| Some(numeric) != outcome_epoch)
2180 || top_epoch != outcome_epoch
2181 {
2182 return Err("receipt top-level and outcome commit epochs disagree".into());
2183 }
2184 if receipt.committed {
2185 if receipt.committed_statements == 0
2186 || top_epoch.is_none()
2187 || receipt.last_commit_epoch_text.is_none()
2188 || receipt.outcome.last_commit_epoch_text.is_none()
2189 || receipt.first_commit_statement_index.is_none()
2190 || receipt.last_commit_statement_index.is_none()
2191 {
2192 return Err("committed receipt has no durable commit metadata".into());
2193 }
2194 } else if receipt.committed_statements != 0
2195 || top_epoch.is_some()
2196 || receipt.first_commit_statement_index.is_some()
2197 || receipt.last_commit_statement_index.is_some()
2198 {
2199 return Err("non-committed receipt contains commit metadata".into());
2200 }
2201 if let (Some(first), Some(last)) = (
2202 receipt.first_commit_statement_index,
2203 receipt.last_commit_statement_index,
2204 ) {
2205 if first > last
2206 || receipt.committed_statements > last.saturating_sub(first).saturating_add(1)
2207 || last > receipt.statement_index
2208 {
2209 return Err("receipt commit statement indexes are invalid".into());
2210 }
2211 }
2212 if receipt.statement_index > receipt.completed_statements
2213 || receipt.completed_statements > receipt.statement_index.saturating_add(1)
2214 {
2215 return Err("receipt statement index and completed count disagree".into());
2216 }
2217 let idempotency_identity_valid = match expected_original_query_id {
2218 Some(original) if receipt.idempotency_replayed => {
2219 receipt.original_query_id == original.to_string()
2220 }
2221 Some(_) => receipt.original_query_id == expected_query_id.to_string(),
2222 None if receipt.idempotency_replayed => true,
2223 None => receipt.original_query_id == expected_query_id.to_string(),
2224 };
2225 if !receipt.idempotency_persisted
2226 || receipt.idempotency_expires_at_ms == 0
2227 || receipt.retryable
2228 || !idempotency_identity_valid
2229 {
2230 return Err("receipt idempotency metadata is invalid".into());
2231 }
2232 receipt.last_commit_epoch = top_epoch;
2233 receipt.outcome.last_commit_epoch = outcome_epoch;
2234 Ok(receipt)
2235}
2236
2237#[derive(Clone)]
2238struct SqlReceiptCommitProof {
2239 epoch: u64,
2240 epoch_text: String,
2241 committed_statements: usize,
2242}
2243
2244fn sql_receipt_commit_proof(
2245 value: &serde_json::Value,
2246 expected_query_id: mongreldb_query::QueryId,
2247 expected_original_query_id: Option<mongreldb_query::QueryId>,
2248) -> Option<SqlReceiptCommitProof> {
2249 let object = value.as_object()?;
2250 let status = object.get("status")?.as_str()?;
2251 if !matches!(
2252 status,
2253 "committed"
2254 | "committed_with_error"
2255 | "partially_committed"
2256 | "cancelled_after_commit"
2257 | "deadline_after_commit"
2258 ) || object.get("query_id")?.as_str()? != expected_query_id.to_string()
2259 || !object.get("committed")?.as_bool()?
2260 || object.get("retryable")?.as_bool()?
2261 || !object.get("idempotency_persisted")?.as_bool()?
2262 {
2263 return None;
2264 }
2265 let original = object.get("original_query_id")?.as_str()?;
2266 let replayed = object.get("idempotency_replayed")?.as_bool()?;
2267 let identity_valid = match expected_original_query_id {
2268 Some(expected) if replayed => original == expected.to_string(),
2269 Some(_) => original == expected_query_id.to_string(),
2270 None if replayed => original.parse::<mongreldb_query::QueryId>().is_ok(),
2271 None => original == expected_query_id.to_string(),
2272 };
2273 if !identity_valid {
2274 return None;
2275 }
2276 let committed_statements =
2277 usize::try_from(object.get("committed_statements")?.as_u64()?).ok()?;
2278 if committed_statements == 0 {
2279 return None;
2280 }
2281 let epoch_text = object.get("last_commit_epoch_text")?.as_str()?.to_owned();
2282 let numeric_epoch = object.get("last_commit_epoch")?.as_u64();
2283 let epoch = exact_epoch(Some(&epoch_text), numeric_epoch).ok()??;
2284 if numeric_epoch.is_some_and(|numeric| numeric != epoch) {
2285 return None;
2286 }
2287 let outcome = object.get("outcome")?.as_object()?;
2288 let outcome_numeric_epoch = outcome
2289 .get("last_commit_epoch")
2290 .and_then(serde_json::Value::as_u64);
2291 let outcome_epoch = exact_epoch(
2292 outcome
2293 .get("last_commit_epoch_text")
2294 .and_then(serde_json::Value::as_str),
2295 outcome_numeric_epoch,
2296 )
2297 .ok()??;
2298 if !outcome.get("committed")?.as_bool()?
2299 || usize::try_from(outcome.get("committed_statements")?.as_u64()?).ok()?
2300 != committed_statements
2301 || outcome.get("last_commit_epoch_text")?.as_str()? != epoch_text
2302 || outcome_epoch != epoch
2303 || outcome_numeric_epoch.is_some_and(|numeric| numeric != outcome_epoch)
2304 {
2305 return None;
2306 }
2307 Some(SqlReceiptCommitProof {
2308 epoch,
2309 epoch_text,
2310 committed_statements,
2311 })
2312}
2313
2314fn committed_sql_receipt_decode_error(
2315 query_id: mongreldb_query::QueryId,
2316 proof: SqlReceiptCommitProof,
2317 message: impl Into<String>,
2318) -> ClientError {
2319 let message = message.into();
2320 let query_id = query_id.to_string();
2321 let code = RemoteQueryErrorCode::CommitOutcome;
2322 let response = RemoteQueryErrorResponse {
2323 query_id: Some(query_id.clone()),
2324 status: "committed_with_error".into(),
2325 terminal_state: Some("committed_with_error".into()),
2326 committed: Some(true),
2327 committed_statements: Some(proof.committed_statements),
2328 last_commit_epoch: Some(proof.epoch),
2329 last_commit_epoch_text: Some(proof.epoch_text.clone()),
2330 first_commit_statement_index: None,
2331 last_commit_statement_index: None,
2332 completed_statements: None,
2333 statement_index: None,
2334 cancel_outcome: Some(RemoteCancelOutcome::AlreadyFinished),
2335 cancellation_reason: Some("none".into()),
2336 retryable: false,
2337 server_state: Some("failed".into()),
2338 outcome: RemoteQueryOutcome {
2339 committed: Some(true),
2340 committed_statements: Some(proof.committed_statements),
2341 last_commit_epoch: Some(proof.epoch),
2342 last_commit_epoch_text: Some(proof.epoch_text),
2343 serialization: "unknown".into(),
2344 ..RemoteQueryOutcome::default()
2345 },
2346 error: RemoteQueryErrorBody {
2347 code: code.clone(),
2348 message: message.clone(),
2349 query_id: Some(query_id),
2350 committed: Some(true),
2351 retryable: false,
2352 },
2353 };
2354 ClientError::Query {
2355 status: 0,
2356 code,
2357 message,
2358 response: Box::new(response),
2359 }
2360}
2361
2362fn sql_error_commit_proof(
2363 value: &serde_json::Value,
2364 expected_query_id: mongreldb_query::QueryId,
2365) -> Option<SqlReceiptCommitProof> {
2366 let object = value.as_object()?;
2367 let expected = expected_query_id.to_string();
2368 if object.get("query_id")?.as_str()? != expected
2369 || !object.get("committed")?.as_bool()?
2370 || object.get("retryable")?.as_bool()?
2371 || !matches!(
2372 object.get("status")?.as_str()?,
2373 "committed"
2374 | "committed_with_error"
2375 | "partially_committed"
2376 | "cancelled_after_commit"
2377 | "deadline_after_commit"
2378 )
2379 {
2380 return None;
2381 }
2382 let committed_statements =
2383 usize::try_from(object.get("committed_statements")?.as_u64()?).ok()?;
2384 if committed_statements == 0 {
2385 return None;
2386 }
2387 let epoch_text = object.get("last_commit_epoch_text")?.as_str()?.to_owned();
2388 let numeric_epoch = object.get("last_commit_epoch")?.as_u64()?;
2389 let epoch = exact_epoch(Some(&epoch_text), Some(numeric_epoch)).ok()??;
2390 if numeric_epoch != epoch {
2391 return None;
2392 }
2393 let outcome = object.get("outcome")?.as_object()?;
2394 let outcome_epoch_text = outcome.get("last_commit_epoch_text")?.as_str()?;
2395 let outcome_numeric_epoch = outcome.get("last_commit_epoch")?.as_u64()?;
2396 let outcome_epoch =
2397 exact_epoch(Some(outcome_epoch_text), Some(outcome_numeric_epoch)).ok()??;
2398 if !outcome.get("committed")?.as_bool()?
2399 || usize::try_from(outcome.get("committed_statements")?.as_u64()?).ok()?
2400 != committed_statements
2401 || outcome_epoch_text != epoch_text
2402 || outcome_epoch != epoch
2403 || outcome_numeric_epoch != outcome_epoch
2404 {
2405 return None;
2406 }
2407 let error = object.get("error")?.as_object()?;
2408 if error.get("query_id")?.as_str()? != expected
2409 || !error.get("committed")?.as_bool()?
2410 || error.get("retryable")?.as_bool()?
2411 {
2412 return None;
2413 }
2414 Some(SqlReceiptCommitProof {
2415 epoch,
2416 epoch_text,
2417 committed_statements,
2418 })
2419}
2420
2421enum SqlReceiptDecodeError {
2422 KnownCommit(ClientError),
2423 Unknown(String),
2424}
2425
2426fn decode_remote_sql_receipt(
2427 bytes: &[u8],
2428 query_id: mongreldb_query::QueryId,
2429 expected_original_query_id: Option<mongreldb_query::QueryId>,
2430) -> Result<RemoteSqlReceipt, SqlReceiptDecodeError> {
2431 let value = strict_json_value(bytes)
2432 .map_err(|error| SqlReceiptDecodeError::Unknown(format!("invalid SQL receipt: {error}")))?;
2433 let proof = sql_receipt_commit_proof(&value, query_id, expected_original_query_id);
2434 let receipt = serde_json::from_value::<RemoteSqlReceipt>(value).map_err(|error| {
2435 proof.clone().map_or_else(
2436 || SqlReceiptDecodeError::Unknown(format!("invalid SQL receipt: {error}")),
2437 |proof| {
2438 SqlReceiptDecodeError::KnownCommit(committed_sql_receipt_decode_error(
2439 query_id,
2440 proof,
2441 format!("SQL committed but its receipt was invalid: {error}"),
2442 ))
2443 },
2444 )
2445 })?;
2446 validate_remote_sql_receipt(receipt, query_id, expected_original_query_id).map_err(|error| {
2447 proof.map_or_else(
2448 || SqlReceiptDecodeError::Unknown(error.clone()),
2449 |proof| {
2450 SqlReceiptDecodeError::KnownCommit(committed_sql_receipt_decode_error(
2451 query_id,
2452 proof,
2453 format!("SQL committed but its receipt was invalid: {error}"),
2454 ))
2455 },
2456 )
2457 })
2458}
2459
2460#[derive(Debug, Clone, Deserialize, PartialEq)]
2461#[serde(deny_unknown_fields)]
2462pub struct RemoteSqlPageLimits {
2463 pub rows: usize,
2464 pub bytes: usize,
2465 pub tokens: usize,
2466}
2467
2468#[derive(Debug, Clone, Deserialize, PartialEq)]
2469#[serde(deny_unknown_fields)]
2470pub struct RemoteSqlPageMetadata {
2471 pub offset: usize,
2472 pub row_count: usize,
2473 pub total_rows: usize,
2474 pub byte_count: usize,
2475 pub estimated_tokens: usize,
2476 pub limits: RemoteSqlPageLimits,
2477 pub projection: Vec<String>,
2478 pub expires_at_ms: u64,
2479 pub snapshot: String,
2480 pub token_estimate: String,
2481}
2482
2483#[derive(Debug, Clone, Deserialize, PartialEq)]
2484#[serde(deny_unknown_fields)]
2485pub struct RemoteSqlPage {
2486 pub status: String,
2487 pub rows: Vec<serde_json::Value>,
2488 pub next_cursor: Option<String>,
2489 pub page: RemoteSqlPageMetadata,
2490}
2491
2492pub struct RemoteSqlQueryHandle {
2493 query_id: mongreldb_query::QueryId,
2494 client: MongrelClient,
2495 result: std::thread::JoinHandle<ClientResult<Vec<RecordBatch>>>,
2496}
2497
2498impl RemoteSqlQueryHandle {
2499 pub fn id(&self) -> mongreldb_query::QueryId {
2500 self.query_id
2501 }
2502
2503 pub fn cancel(&self) -> ClientResult<RemoteCancelOutcome> {
2504 self.client.cancel_sql(self.query_id)
2505 }
2506
2507 pub fn status(&self) -> ClientResult<RemoteQueryStatus> {
2508 self.client.query_status(self.query_id)
2509 }
2510
2511 pub fn wait(self) -> ClientResult<Vec<RecordBatch>> {
2512 match self.result.join() {
2513 Ok(result) => result,
2514 Err(_) => Err(self
2515 .client
2516 .recover_after_transport_loss(self.query_id, "SQL worker panicked".into())),
2517 }
2518 }
2519}
2520
2521#[derive(Serialize)]
2522struct SqlReq {
2523 sql: String,
2524 #[serde(skip_serializing_if = "Option::is_none")]
2525 format: Option<&'static str>,
2526 query_id: mongreldb_query::QueryId,
2527 #[serde(skip_serializing_if = "Option::is_none")]
2528 timeout_ms: Option<u64>,
2529 #[serde(skip_serializing_if = "Option::is_none")]
2530 max_output_rows: Option<u64>,
2531 #[serde(skip_serializing_if = "Option::is_none")]
2532 max_output_bytes: Option<u64>,
2533 #[serde(skip_serializing_if = "Option::is_none")]
2534 idempotency_key: Option<String>,
2535 #[serde(skip_serializing_if = "Option::is_none")]
2536 pagination: Option<SqlPaginationReq>,
2537}
2538
2539#[derive(Debug, Clone, Serialize)]
2540struct SqlPaginationReq {
2541 page_size_rows: u64,
2542 projection: Vec<String>,
2543 #[serde(skip_serializing_if = "Option::is_none")]
2544 max_page_bytes: Option<u64>,
2545 #[serde(skip_serializing_if = "Option::is_none")]
2546 max_page_tokens: Option<u64>,
2547}
2548
2549#[derive(Deserialize)]
2550#[serde(deny_unknown_fields)]
2551struct CountResp {
2552 count: u64,
2553}
2554
2555#[derive(Deserialize)]
2556#[serde(deny_unknown_fields)]
2557struct TableIdResponse {
2558 table_id: u64,
2559 table_id_text: String,
2560}
2561
2562#[derive(Deserialize)]
2563#[serde(deny_unknown_fields)]
2564struct RowIdResponse {
2565 row_id: String,
2566}
2567
2568#[derive(Deserialize)]
2569#[serde(deny_unknown_fields)]
2570struct EpochResponse {
2571 epoch: u64,
2572 epoch_text: String,
2573}
2574
2575#[derive(Deserialize)]
2576#[serde(deny_unknown_fields)]
2577struct CommittedWriteResponse {
2578 status: String,
2579 epoch: u64,
2580 epoch_text: String,
2581}
2582
2583#[derive(Deserialize)]
2584#[serde(deny_unknown_fields)]
2585struct TriggerDropResponse {
2586 status: String,
2587 epoch: u64,
2588 epoch_text: String,
2589 dropped_trigger: mongreldb_core::StoredTrigger,
2590 resource_tables: Vec<TriggerTableBindingResponse>,
2591}
2592
2593#[derive(Deserialize)]
2594#[serde(deny_unknown_fields)]
2595struct TriggerTableBindingResponse {
2596 name: String,
2597 table_id: u64,
2598 schema_id: u64,
2599}
2600
2601#[derive(Debug, Clone, Deserialize)]
2602#[serde(deny_unknown_fields)]
2603pub struct HistoryRetention {
2604 pub history_retention_epochs: u64,
2605 pub earliest_retained_epoch: u64,
2606}
2607
2608#[derive(Debug, Clone, Deserialize)]
2610#[serde(deny_unknown_fields)]
2611pub struct TableSchemaInfo {
2612 pub schema_id: u64,
2613 pub columns: Vec<ColumnMeta>,
2614 #[serde(default)]
2615 pub indexes: Vec<IndexMeta>,
2616 pub constraints: ConstraintMeta,
2617}
2618
2619#[derive(Debug, Clone, Deserialize)]
2620#[serde(deny_unknown_fields)]
2621pub struct IndexMeta {
2622 pub name: String,
2623 pub column_id: u16,
2624 pub kind: String,
2625 pub predicate: Option<String>,
2626 #[serde(default)]
2627 pub options: mongreldb_core::schema::IndexOptions,
2628}
2629
2630#[derive(Debug, Clone, Deserialize)]
2631#[serde(deny_unknown_fields)]
2632pub struct ColumnMeta {
2633 pub id: u16,
2634 pub name: String,
2635 pub ty: String,
2636 pub primary_key: bool,
2637 pub nullable: bool,
2638 pub auto_increment: bool,
2639 #[serde(default)]
2640 pub embedding_source: Option<mongreldb_core::EmbeddingSource>,
2641}
2642
2643#[derive(Debug, Clone, Default, Deserialize)]
2644#[serde(deny_unknown_fields)]
2645pub struct ConstraintMeta {
2646 #[serde(default)]
2647 pub uniques: Vec<serde_json::Value>,
2648 #[serde(default)]
2649 pub foreign_keys: Vec<serde_json::Value>,
2650 #[serde(default)]
2651 pub checks: Vec<serde_json::Value>,
2652}
2653
2654#[derive(Debug, Clone, Serialize)]
2658pub struct KitTxnRequest {
2659 #[serde(skip_serializing_if = "Option::is_none")]
2660 pub idempotency_key: Option<String>,
2661 pub ops: Vec<KitOp>,
2662}
2663
2664impl KitTxnRequest {
2665 pub fn new(ops: Vec<KitOp>) -> Self {
2666 Self {
2667 idempotency_key: None,
2668 ops,
2669 }
2670 }
2671 pub fn with_idempotency_key(mut self, key: impl Into<String>) -> Self {
2672 self.idempotency_key = Some(key.into());
2673 self
2674 }
2675}
2676
2677#[derive(Debug, Clone, Serialize)]
2679#[serde(rename_all = "snake_case")]
2680pub enum KitOp {
2681 Put {
2682 table: String,
2683 cells: Vec<serde_json::Value>,
2684 returning: bool,
2685 },
2686 Upsert {
2687 table: String,
2688 cells: Vec<serde_json::Value>,
2689 #[serde(skip_serializing_if = "Option::is_none")]
2690 update_cells: Option<Vec<serde_json::Value>>,
2691 returning: bool,
2692 },
2693 Delete {
2694 table: String,
2695 row_id: u64,
2696 },
2697 DeleteByPk {
2698 table: String,
2699 pk: serde_json::Value,
2700 },
2701}
2702
2703impl KitOp {
2704 pub fn put(table: impl Into<String>, cells: Vec<serde_json::Value>) -> Self {
2705 KitOp::Put {
2706 table: table.into(),
2707 cells,
2708 returning: false,
2709 }
2710 }
2711 pub fn put_returning(table: impl Into<String>, cells: Vec<serde_json::Value>) -> Self {
2712 KitOp::Put {
2713 table: table.into(),
2714 cells,
2715 returning: true,
2716 }
2717 }
2718 pub fn upsert(table: impl Into<String>, cells: Vec<serde_json::Value>) -> Self {
2719 KitOp::Upsert {
2720 table: table.into(),
2721 cells,
2722 update_cells: None,
2723 returning: false,
2724 }
2725 }
2726 pub fn delete_by_pk(table: impl Into<String>, pk: serde_json::Value) -> Self {
2727 KitOp::DeleteByPk {
2728 table: table.into(),
2729 pk,
2730 }
2731 }
2732}
2733
2734#[derive(Debug, Clone, Deserialize)]
2736#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "kind")]
2737pub enum KitOpResult {
2738 Put {
2739 row_id: Option<String>,
2740 auto_inc: Option<i64>,
2741 #[serde(default)]
2742 row: Option<Vec<serde_json::Value>>,
2743 },
2744 Upsert {
2745 action: String,
2746 auto_inc: Option<i64>,
2747 #[serde(default)]
2748 row: Option<Vec<serde_json::Value>>,
2749 },
2750 Deleted,
2751 NotFound,
2752}
2753
2754#[derive(Debug, Clone, Deserialize)]
2755#[serde(deny_unknown_fields)]
2756pub struct KitTxnResponse {
2757 pub status: String,
2758 pub epoch: u64,
2759 #[serde(default)]
2760 pub epoch_text: Option<String>,
2761 pub results: Vec<KitOpResult>,
2762}
2763
2764fn validate_kit_txn_response(
2765 response: KitTxnResponse,
2766 request: &KitTxnRequest,
2767) -> ClientResult<KitTxnResponse> {
2768 if response.status != "committed" {
2769 return Err(ClientError::Decode(
2770 "Kit transaction success status is not committed".into(),
2771 ));
2772 }
2773 if response.epoch_text.is_none()
2774 || exact_kit_epoch(response.epoch_text.as_deref(), Some(response.epoch))
2775 .map_err(ClientError::Decode)?
2776 .is_none()
2777 {
2778 return Err(ClientError::Decode(
2779 "Kit transaction response has no exact epoch".into(),
2780 ));
2781 }
2782 validate_kit_results(&response.results, request)?;
2783 Ok(response)
2784}
2785
2786fn kit_txn_outcome_unknown(message: impl Into<String>) -> ClientError {
2787 ClientError::Kit {
2788 code: KitErrorCode::QueryOutcomeUnknown,
2789 message: message.into(),
2790 op_index: None,
2791 status: 0,
2792 committed: None,
2793 epoch: None,
2794 epoch_text: None,
2795 retryable: Some(false),
2796 }
2797}
2798
2799fn kit_txn_auth_error(status: u16) -> ClientError {
2800 let (code, message) = if status == 401 {
2801 (KitErrorCode::AuthRequired, "authentication required")
2802 } else {
2803 (KitErrorCode::PermissionDenied, "permission denied")
2804 };
2805 ClientError::Kit {
2806 code,
2807 message: message.into(),
2808 op_index: None,
2809 status,
2810 committed: Some(false),
2811 epoch: None,
2812 epoch_text: None,
2813 retryable: Some(false),
2814 }
2815}
2816
2817fn committed_kit_txn_decode_error(
2818 status: u16,
2819 epoch: u64,
2820 epoch_text: String,
2821 message: impl Into<String>,
2822) -> ClientError {
2823 ClientError::Kit {
2824 code: KitErrorCode::CommitOutcome,
2825 message: message.into(),
2826 op_index: None,
2827 status,
2828 committed: Some(true),
2829 epoch: Some(epoch),
2830 epoch_text: Some(epoch_text),
2831 retryable: Some(false),
2832 }
2833}
2834
2835fn decode_kit_txn_success(
2836 body: &[u8],
2837 request: &KitTxnRequest,
2838 status: u16,
2839) -> ClientResult<KitTxnResponse> {
2840 let value = strict_json_value(body).map_err(|error| {
2841 kit_txn_outcome_unknown(format!("invalid Kit transaction success response: {error}"))
2842 })?;
2843 let epoch = value.get("epoch").and_then(serde_json::Value::as_u64);
2844 let epoch_text = value
2845 .get("epoch_text")
2846 .and_then(serde_json::Value::as_str)
2847 .map(str::to_owned);
2848 let proven_commit = value.get("status").and_then(serde_json::Value::as_str)
2849 == Some("committed")
2850 && epoch.is_some()
2851 && epoch_text
2852 .as_deref()
2853 .is_some_and(|text| exact_kit_epoch(Some(text), epoch).ok().flatten() == epoch);
2854 let (Some(epoch), Some(epoch_text)) = (epoch, epoch_text) else {
2855 return Err(kit_txn_outcome_unknown(
2856 "Kit transaction success response does not prove a commit",
2857 ));
2858 };
2859 if !proven_commit {
2860 return Err(kit_txn_outcome_unknown(
2861 "Kit transaction success response does not prove a commit",
2862 ));
2863 }
2864 if let Err(error) = exact_json_object_fields(
2865 &value,
2866 &["status", "epoch", "epoch_text", "results"],
2867 &["status", "epoch", "epoch_text", "results"],
2868 "Kit transaction success response",
2869 ) {
2870 return Err(committed_kit_txn_decode_error(
2871 status,
2872 epoch,
2873 epoch_text,
2874 format!("transaction committed but its response was invalid: {error}"),
2875 ));
2876 }
2877 if !value["results"].is_array() {
2878 return Err(committed_kit_txn_decode_error(
2879 status,
2880 epoch,
2881 epoch_text,
2882 "transaction committed but its results were not an array",
2883 ));
2884 }
2885 let response = serde_json::from_value::<KitTxnResponse>(value).map_err(|error| {
2886 committed_kit_txn_decode_error(
2887 status,
2888 epoch,
2889 epoch_text.clone(),
2890 format!("transaction committed but its result could not be decoded: {error}"),
2891 )
2892 })?;
2893 validate_kit_txn_response(response, request).map_err(|error| {
2894 committed_kit_txn_decode_error(
2895 status,
2896 epoch,
2897 epoch_text,
2898 format!("transaction committed but its result was invalid: {error}"),
2899 )
2900 })
2901}
2902
2903fn validate_kit_results(results: &[KitOpResult], request: &KitTxnRequest) -> ClientResult<()> {
2904 if results.len() != request.ops.len() {
2905 return Err(ClientError::Decode(
2906 "Kit transaction result count does not match request".into(),
2907 ));
2908 }
2909 for (index, (operation, result)) in request.ops.iter().zip(results).enumerate() {
2910 let matches = match (operation, result) {
2911 (KitOp::Put { returning, .. }, KitOpResult::Put { row_id, row, .. }) => {
2912 row_id.is_none()
2913 && row.is_some() == *returning
2914 && row.as_deref().is_none_or(valid_flat_kit_cells)
2915 }
2916 (KitOp::Upsert { returning, .. }, KitOpResult::Upsert { action, row, .. }) => {
2917 matches!(action.as_str(), "inserted" | "updated" | "unchanged")
2918 && row.is_some() == *returning
2919 && row.as_deref().is_none_or(valid_flat_kit_cells)
2920 }
2921 (KitOp::Delete { .. }, KitOpResult::Deleted) => true,
2922 (KitOp::DeleteByPk { .. }, KitOpResult::Deleted | KitOpResult::NotFound) => true,
2923 _ => false,
2924 };
2925 if !matches {
2926 return Err(ClientError::Decode(format!(
2927 "Kit transaction result {index} does not match request operation"
2928 )));
2929 }
2930 }
2931 Ok(())
2932}
2933
2934fn valid_flat_kit_cells(cells: &[serde_json::Value]) -> bool {
2935 if !cells.len().is_multiple_of(2) {
2936 return false;
2937 }
2938 let mut column_ids = std::collections::HashSet::new();
2939 cells.chunks_exact(2).all(|pair| {
2940 pair[0]
2941 .as_u64()
2942 .and_then(|column_id| u16::try_from(column_id).ok())
2943 .is_some_and(|column_id| column_ids.insert(column_id))
2944 })
2945}
2946
2947#[derive(Debug, Clone, Serialize)]
2954pub struct KitQueryRequest {
2955 pub table: String,
2956 #[serde(skip_serializing_if = "Vec::is_empty")]
2957 pub conditions: Vec<serde_json::Value>,
2958 #[serde(skip_serializing_if = "Option::is_none")]
2959 pub projection: Option<Vec<u16>>,
2960 #[serde(skip_serializing_if = "Option::is_none")]
2961 pub limit: Option<usize>,
2962 #[serde(skip_serializing_if = "Option::is_none")]
2963 pub offset: Option<usize>,
2964 #[serde(skip_serializing_if = "Option::is_none")]
2965 pub cursor: Option<String>,
2966}
2967
2968#[derive(Debug, Clone, Deserialize)]
2969#[serde(deny_unknown_fields)]
2970pub struct KitQueryResponse {
2971 pub rows: Vec<KitQueryRow>,
2972 pub truncated: bool,
2973 #[serde(default)]
2974 pub next_cursor: Option<String>,
2975}
2976
2977#[derive(Debug, Clone, Deserialize)]
2978#[serde(deny_unknown_fields)]
2979pub struct KitQueryRow {
2980 pub row_id: String,
2981 pub cells: Vec<serde_json::Value>,
2983}
2984
2985fn validate_kit_query_response(
2986 response: KitQueryResponse,
2987 request: &KitQueryRequest,
2988) -> ClientResult<KitQueryResponse> {
2989 if response.truncated != response.next_cursor.is_some()
2990 || response
2991 .next_cursor
2992 .as_ref()
2993 .is_some_and(|cursor| cursor.is_empty() || cursor.len() > 2_048)
2994 {
2995 return Err(ClientError::Decode(
2996 "Kit query continuation cursor is inconsistent".into(),
2997 ));
2998 }
2999 let limit = request
3000 .limit
3001 .unwrap_or(mongreldb_core::query::MAX_FINAL_LIMIT);
3002 if response.rows.len() > limit {
3003 return Err(ClientError::Decode(
3004 "Kit query returned more rows than requested".into(),
3005 ));
3006 }
3007 let projection = request.projection.as_ref().map(|columns| {
3008 columns
3009 .iter()
3010 .copied()
3011 .collect::<std::collections::HashSet<_>>()
3012 });
3013 let mut row_ids = std::collections::HashSet::new();
3014 for row in &response.rows {
3015 if row
3016 .row_id
3017 .parse::<u64>()
3018 .ok()
3019 .is_none_or(|row_id| row_id.to_string() != row.row_id)
3020 || !row_ids.insert(row.row_id.as_str())
3021 || !valid_flat_kit_cells(&row.cells)
3022 || projection.as_ref().is_some_and(|projection| {
3023 row.cells.chunks_exact(2).any(|pair| {
3024 pair[0]
3025 .as_u64()
3026 .and_then(|column_id| u16::try_from(column_id).ok())
3027 .is_none_or(|column_id| !projection.contains(&column_id))
3028 })
3029 })
3030 {
3031 return Err(ClientError::Decode(
3032 "Kit query row identifier or cell layout is invalid".into(),
3033 ));
3034 }
3035 }
3036 Ok(response)
3037}
3038
3039#[derive(Debug, Clone, Copy, Default, Serialize)]
3041pub struct AiExecutionOptions {
3042 #[serde(skip_serializing_if = "Option::is_none")]
3043 pub deadline_ms: Option<u64>,
3044 #[serde(skip_serializing_if = "Option::is_none")]
3045 pub max_work: Option<usize>,
3046}
3047
3048#[derive(Serialize)]
3049struct WithAiExecutionOptions<'a, T> {
3050 #[serde(flatten)]
3051 request: &'a T,
3052 #[serde(flatten)]
3053 options: &'a AiExecutionOptions,
3054}
3055
3056#[derive(Debug, Clone, Serialize)]
3057pub struct KitRetrieveRequest {
3058 pub table: String,
3059 pub retriever: serde_json::Value,
3060}
3061
3062#[derive(Debug, Clone, Deserialize)]
3063#[serde(deny_unknown_fields)]
3064pub struct KitRetrieveResponse {
3065 pub hits: Vec<KitRetrieverHit>,
3066}
3067
3068#[derive(Debug, Clone, Serialize)]
3070pub struct KitRetrieveTextRequest {
3071 pub table: String,
3072 pub embedding_column: u16,
3073 pub text: String,
3074 #[serde(skip_serializing_if = "Option::is_none")]
3075 pub k: Option<usize>,
3076}
3077
3078#[derive(Debug, Clone, Deserialize)]
3079#[serde(deny_unknown_fields)]
3080pub struct KitRetrieveTextResponse {
3081 pub hits: Vec<KitRetrieverHit>,
3082 pub provenance: KitTextRetrieveProvenance,
3083}
3084
3085#[derive(Debug, Clone, Deserialize)]
3086#[serde(deny_unknown_fields)]
3087pub struct KitTextRetrieveProvenance {
3088 pub embedding_column: u16,
3089 pub provider_registry_generation: u64,
3090 pub query_source_fingerprint: String,
3091 pub semantic_identity: serde_json::Value,
3092}
3093
3094#[derive(Debug, Clone, Serialize)]
3095pub struct KitAnnRerankRequest {
3096 pub table: String,
3097 pub column_id: u16,
3098 pub query: Vec<f32>,
3099 pub candidate_k: usize,
3100 pub limit: usize,
3101 pub metric: KitVectorMetric,
3102}
3103
3104#[derive(Debug, Clone, Copy, Serialize)]
3105#[serde(rename_all = "snake_case")]
3106pub enum KitVectorMetric {
3107 Cosine,
3108 DotProduct,
3109 Euclidean,
3110}
3111
3112#[derive(Debug, Clone, Deserialize)]
3113#[serde(deny_unknown_fields)]
3114pub struct KitAnnRerankResponse {
3115 pub hits: Vec<KitAnnRerankHit>,
3116}
3117
3118#[derive(Debug, Clone, Deserialize)]
3119#[serde(deny_unknown_fields)]
3120pub struct KitAnnRerankHit {
3121 pub row_id: String,
3122 pub candidate_distance: KitAnnCandidateDistance,
3123 pub exact_score: f32,
3124}
3125
3126#[derive(Debug, Clone, Deserialize)]
3131#[serde(deny_unknown_fields)]
3132pub struct KitAnnCandidateDistance {
3133 pub kind: String,
3134 pub value: f64,
3135}
3136
3137#[derive(Debug, Clone, Deserialize)]
3138#[serde(deny_unknown_fields)]
3139pub struct KitRetrieverHit {
3140 pub row_id: String,
3141 pub rank: usize,
3142 pub score: KitScore,
3143}
3144
3145#[derive(Debug, Clone, Deserialize)]
3146#[serde(deny_unknown_fields)]
3147pub struct KitScore {
3148 pub kind: String,
3149 pub value: f64,
3150}
3151
3152#[derive(Debug, Clone, Serialize)]
3153pub struct KitSetSimilarityRequest {
3154 pub table: String,
3155 pub column_id: u16,
3156 pub members: Vec<serde_json::Value>,
3157 pub candidate_k: usize,
3158 pub min_jaccard: f32,
3159 pub limit: usize,
3160}
3161
3162#[derive(Debug, Clone, Deserialize)]
3163#[serde(deny_unknown_fields)]
3164pub struct KitSetSimilarityResponse {
3165 pub hits: Vec<KitSetSimilarityHit>,
3166}
3167
3168#[derive(Debug, Clone, Deserialize)]
3169#[serde(deny_unknown_fields)]
3170pub struct KitSetSimilarityHit {
3171 pub row_id: String,
3172 pub estimated_jaccard: f32,
3173 pub exact_jaccard: f32,
3174}
3175
3176#[derive(Debug, Clone, Serialize)]
3177pub struct KitSearchRequest {
3178 pub table: String,
3179 #[serde(skip_serializing_if = "Vec::is_empty")]
3180 pub must: Vec<serde_json::Value>,
3181 pub retrievers: Vec<serde_json::Value>,
3182 pub fusion: serde_json::Value,
3183 #[serde(skip_serializing_if = "Option::is_none")]
3184 pub rerank: Option<serde_json::Value>,
3185 pub limit: usize,
3186 #[serde(skip_serializing_if = "Option::is_none")]
3187 pub projection: Option<Vec<u16>>,
3188 #[serde(skip_serializing_if = "Option::is_none")]
3189 pub deadline_ms: Option<u64>,
3190 #[serde(skip_serializing_if = "Option::is_none")]
3191 pub max_work: Option<usize>,
3192 #[serde(default)]
3193 pub explain: bool,
3194 #[serde(skip_serializing_if = "Option::is_none")]
3195 pub cursor: Option<String>,
3196}
3197
3198#[derive(Debug, Clone, Deserialize)]
3199#[serde(deny_unknown_fields)]
3200pub struct KitSearchResponse {
3201 pub hits: Vec<KitSearchHit>,
3202 #[serde(default)]
3203 pub trace: Option<serde_json::Value>,
3204 #[serde(default)]
3205 pub next_cursor: Option<String>,
3206}
3207
3208#[derive(Debug, Clone, Deserialize)]
3209#[serde(deny_unknown_fields)]
3210pub struct KitSearchHit {
3211 pub row_id: String,
3212 pub cells: Vec<serde_json::Value>,
3213 pub components: Vec<KitComponentScore>,
3214 pub fused_score: f64,
3215 #[serde(default)]
3216 pub exact_rerank_score: Option<f32>,
3217 #[serde(default)]
3218 pub final_score: Option<f64>,
3219 #[serde(default)]
3220 pub final_rank: Option<usize>,
3221}
3222
3223#[derive(Debug, Clone, Deserialize)]
3224#[serde(deny_unknown_fields)]
3225pub struct KitComponentScore {
3226 pub retriever_name: String,
3227 pub rank: usize,
3228 pub raw_score: KitScore,
3229 pub contribution: f64,
3230}
3231
3232#[derive(Debug, Clone, Serialize)]
3233pub struct ProcedureRequest {
3234 pub procedure: mongreldb_core::StoredProcedure,
3235}
3236
3237#[derive(Debug, Clone, Serialize)]
3238pub struct TriggerRequest {
3239 pub trigger: mongreldb_core::StoredTrigger,
3240 #[serde(skip_serializing_if = "Option::is_none")]
3241 pub idempotency_key: Option<String>,
3242}
3243
3244#[derive(Debug, Clone, Deserialize)]
3245#[serde(deny_unknown_fields)]
3246pub struct ProcedureResponse {
3247 #[serde(default)]
3248 pub status: String,
3249 pub procedure: mongreldb_core::StoredProcedure,
3250}
3251
3252#[derive(Debug, Clone, Deserialize)]
3253#[serde(deny_unknown_fields)]
3254pub struct TriggerResponse {
3255 #[serde(default)]
3256 pub status: Option<String>,
3257 pub trigger: mongreldb_core::StoredTrigger,
3258}
3259
3260#[derive(Debug, Clone, Deserialize)]
3261#[serde(deny_unknown_fields)]
3262pub struct ProceduresResponse {
3263 pub procedures: Vec<mongreldb_core::StoredProcedure>,
3264}
3265
3266#[derive(Debug, Clone, Deserialize)]
3267#[serde(deny_unknown_fields)]
3268pub struct TriggersResponse {
3269 pub triggers: Vec<mongreldb_core::StoredTrigger>,
3270}
3271
3272#[derive(Debug, Clone, Serialize)]
3273pub struct ProcedureCallRequest {
3274 #[serde(default)]
3275 pub args: serde_json::Map<String, serde_json::Value>,
3276 #[serde(skip_serializing_if = "Option::is_none")]
3277 pub idempotency_key: Option<String>,
3278}
3279
3280#[derive(Debug, Clone, Deserialize)]
3281#[serde(deny_unknown_fields)]
3282pub struct ProcedureCallResponse {
3283 pub status: String,
3284 pub committed: bool,
3285 #[serde(default)]
3286 pub epoch: Option<u64>,
3287 #[serde(default)]
3288 pub epoch_text: Option<String>,
3289 pub result: serde_json::Value,
3290}
3291
3292fn validate_procedure_call_response(
3293 response: ProcedureCallResponse,
3294) -> ClientResult<ProcedureCallResponse> {
3295 if response.status != "ok"
3296 || response.committed != response.epoch.is_some()
3297 || response.committed != response.epoch_text.is_some()
3298 || exact_kit_epoch(response.epoch_text.as_deref(), response.epoch)
3299 .map_err(ClientError::Decode)?
3300 != response.epoch
3301 {
3302 return Err(ClientError::Decode(
3303 "procedure call response has invalid durable metadata".into(),
3304 ));
3305 }
3306 Ok(response)
3307}
3308
3309fn procedure_definition_matches(
3310 actual: &mongreldb_core::StoredProcedure,
3311 requested: &mongreldb_core::StoredProcedure,
3312) -> bool {
3313 actual.mode == requested.mode
3314 && actual.params == requested.params
3315 && actual.body == requested.body
3316}
3317
3318fn trigger_definition_matches(
3319 actual: &mongreldb_core::StoredTrigger,
3320 requested: &mongreldb_core::StoredTrigger,
3321) -> bool {
3322 actual.target == requested.target
3323 && actual.timing == requested.timing
3324 && actual.event == requested.event
3325 && actual.update_of == requested.update_of
3326 && actual.target_columns == requested.target_columns
3327 && actual.when == requested.when
3328 && actual.program == requested.program
3329 && actual.enabled
3330}
3331
3332#[derive(Debug, Deserialize)]
3334#[serde(deny_unknown_fields)]
3335struct KitErrorEnvelope {
3336 #[allow(dead_code)]
3337 status: String,
3338 #[serde(default)]
3339 committed: Option<bool>,
3340 #[serde(default)]
3341 epoch: Option<u64>,
3342 #[serde(default)]
3343 epoch_text: Option<String>,
3344 #[serde(default)]
3345 retryable: Option<bool>,
3346 #[serde(default)]
3347 results: Option<Vec<KitOpResult>>,
3348 error: KitErrorBody,
3349}
3350
3351#[derive(Debug, Deserialize)]
3352#[serde(deny_unknown_fields)]
3353struct KitErrorBody {
3354 code: String,
3355 message: String,
3356 #[serde(default)]
3357 op_index: Option<usize>,
3358}
3359
3360fn exact_json_object_fields(
3361 value: &serde_json::Value,
3362 allowed: &[&str],
3363 required: &[&str],
3364 context: &str,
3365) -> Result<(), String> {
3366 let object = value
3367 .as_object()
3368 .ok_or_else(|| format!("{context} is not an object"))?;
3369 if let Some(field) = object
3370 .keys()
3371 .find(|field| !allowed.contains(&field.as_str()))
3372 {
3373 return Err(format!("{context} contains unknown field {field:?}"));
3374 }
3375 if let Some(field) = required.iter().find(|field| !object.contains_key(**field)) {
3376 return Err(format!("{context} lacks required field {field:?}"));
3377 }
3378 Ok(())
3379}
3380
3381fn is_exact_query_not_found_response(
3382 body: &[u8],
3383 expected_query_id: mongreldb_query::QueryId,
3384) -> bool {
3385 const FIELDS: &[&str] = &[
3386 "query_id",
3387 "status",
3388 "terminal_state",
3389 "committed",
3390 "committed_statements",
3391 "last_commit_epoch",
3392 "last_commit_epoch_text",
3393 "first_commit_statement_index",
3394 "last_commit_statement_index",
3395 "completed_statements",
3396 "statement_index",
3397 "cancel_outcome",
3398 "cancellation_reason",
3399 "retryable",
3400 "server_state",
3401 "outcome",
3402 "error",
3403 ];
3404 const OUTCOME_FIELDS: &[&str] = &[
3405 "committed",
3406 "committed_statements",
3407 "last_commit_epoch",
3408 "last_commit_epoch_text",
3409 "first_commit_statement_index",
3410 "last_commit_statement_index",
3411 "completed_statements",
3412 "statement_index",
3413 "serialization",
3414 ];
3415 const NULL_FIELDS: &[&str] = &[
3416 "terminal_state",
3417 "committed",
3418 "committed_statements",
3419 "last_commit_epoch",
3420 "last_commit_epoch_text",
3421 "first_commit_statement_index",
3422 "last_commit_statement_index",
3423 "completed_statements",
3424 "statement_index",
3425 "cancellation_reason",
3426 ];
3427 let Ok(value) = strict_json_value(body) else {
3428 return false;
3429 };
3430 if exact_json_object_fields(&value, FIELDS, FIELDS, "query-not-found response").is_err() {
3431 return false;
3432 }
3433 let expected_query_id = expected_query_id.to_string();
3434 if value["query_id"].as_str() != Some(expected_query_id.as_str())
3435 || value["status"].as_str() != Some("unknown")
3436 || value["cancel_outcome"].as_str() != Some("not_found")
3437 || value["server_state"].as_str() != Some("not_found")
3438 || value["retryable"].as_bool() != Some(false)
3439 || NULL_FIELDS.iter().any(|field| !value[*field].is_null())
3440 {
3441 return false;
3442 }
3443 let outcome = &value["outcome"];
3444 if exact_json_object_fields(
3445 outcome,
3446 OUTCOME_FIELDS,
3447 OUTCOME_FIELDS,
3448 "query-not-found outcome",
3449 )
3450 .is_err()
3451 || outcome["serialization"].as_str() != Some("unknown")
3452 || OUTCOME_FIELDS
3453 .iter()
3454 .filter(|field| **field != "serialization")
3455 .any(|field| !outcome[*field].is_null())
3456 {
3457 return false;
3458 }
3459 let error = &value["error"];
3460 exact_json_object_fields(
3461 error,
3462 &["code", "message", "query_id", "committed", "retryable"],
3463 &["code", "message", "query_id", "committed", "retryable"],
3464 "query-not-found error",
3465 )
3466 .is_ok()
3467 && error["code"].as_str() == Some("QUERY_NOT_FOUND")
3468 && error["message"].as_str().is_some()
3469 && error["query_id"].as_str() == Some(expected_query_id.as_str())
3470 && error["committed"].is_null()
3471 && error["retryable"].as_bool() == Some(false)
3472}
3473
3474fn validate_kit_txn_error_json(value: &serde_json::Value) -> Result<(), String> {
3475 let error = value
3476 .get("error")
3477 .ok_or_else(|| "Kit transaction error response lacks error".to_owned())?;
3478 exact_json_object_fields(
3479 error,
3480 &["code", "message", "op_index"],
3481 &["code", "message"],
3482 "Kit transaction error",
3483 )?;
3484 let status = value
3485 .get("status")
3486 .and_then(serde_json::Value::as_str)
3487 .ok_or_else(|| "Kit transaction error response lacks status".to_owned())?;
3488 if matches!(status, "committed" | "outcome_unknown")
3489 && error
3490 .as_object()
3491 .is_some_and(|error| error.contains_key("op_index"))
3492 {
3493 return Err("durable Kit transaction error must omit op_index".into());
3494 }
3495 match status {
3496 "aborted" => {
3497 let enriched = value.get("committed").is_some() || value.get("retryable").is_some();
3498 if enriched {
3499 exact_json_object_fields(
3500 value,
3501 &["status", "committed", "retryable", "error"],
3502 &["status", "committed", "retryable", "error"],
3503 "Kit transaction error response",
3504 )?;
3505 } else {
3506 exact_json_object_fields(
3507 value,
3508 &["status", "error"],
3509 &["status", "error"],
3510 "Kit transaction error response",
3511 )?;
3512 }
3513 }
3514 "committed" => exact_json_object_fields(
3515 value,
3516 &[
3517 "status",
3518 "committed",
3519 "epoch",
3520 "epoch_text",
3521 "results",
3522 "retryable",
3523 "error",
3524 ],
3525 &[
3526 "status",
3527 "committed",
3528 "epoch",
3529 "epoch_text",
3530 "retryable",
3531 "error",
3532 ],
3533 "Kit transaction error response",
3534 )?,
3535 "outcome_unknown" => exact_json_object_fields(
3536 value,
3537 &[
3538 "status",
3539 "committed",
3540 "epoch",
3541 "epoch_text",
3542 "retryable",
3543 "error",
3544 ],
3545 &[
3546 "status",
3547 "committed",
3548 "epoch",
3549 "epoch_text",
3550 "retryable",
3551 "error",
3552 ],
3553 "Kit transaction error response",
3554 )?,
3555 _ => return Err("Kit transaction error response status is invalid".into()),
3556 }
3557 Ok(())
3558}
3559
3560fn proven_kit_commit_metadata(value: &serde_json::Value) -> Option<(u64, String)> {
3561 if value.get("status")?.as_str()? != "committed"
3562 || !value.get("committed")?.as_bool()?
3563 || value.get("retryable")?.as_bool()?
3564 {
3565 return None;
3566 }
3567 let error = value.get("error")?.as_object()?;
3568 if error.get("code")?.as_str()? != "COMMIT_OUTCOME"
3569 || error.get("message")?.as_str()?.is_empty()
3570 || error.get("op_index").is_some_and(|index| {
3571 !index.is_null()
3572 && index
3573 .as_u64()
3574 .and_then(|index| usize::try_from(index).ok())
3575 .is_none()
3576 })
3577 {
3578 return None;
3579 }
3580 let epoch = value.get("epoch")?.as_u64()?;
3581 let epoch_text = value.get("epoch_text")?.as_str()?.to_owned();
3582 (exact_kit_epoch(Some(&epoch_text), Some(epoch))
3583 .ok()
3584 .flatten()
3585 == Some(epoch))
3586 .then_some((epoch, epoch_text))
3587}
3588
3589fn decode_http_error(status: u16, body: &[u8]) -> ClientError {
3590 let value = match strict_json_value(body) {
3591 Ok(value) => value,
3592 Err(error) => {
3593 if body
3594 .iter()
3595 .copied()
3596 .find(|byte| !byte.is_ascii_whitespace())
3597 .is_some_and(|byte| matches!(byte, b'{' | b'['))
3598 {
3599 return ClientError::Decode(format!("invalid HTTP error response: {error}"));
3600 }
3601 return ClientError::Http {
3602 status,
3603 body: format!("non-JSON error response ({} bytes)", body.len()),
3604 };
3605 }
3606 };
3607 if status == 404
3608 && value
3609 .get("query_id")
3610 .and_then(serde_json::Value::as_str)
3611 .and_then(|query_id| query_id.parse::<mongreldb_query::QueryId>().ok())
3612 .is_some_and(|query_id| is_exact_query_not_found_response(body, query_id))
3613 {
3614 return match serde_json::from_value::<RemoteQueryErrorResponse>(value) {
3615 Ok(response) => ClientError::Query {
3616 status,
3617 code: RemoteQueryErrorCode::QueryNotFound,
3618 message: response.error.message.clone(),
3619 response: Box::new(response),
3620 },
3621 Err(error) => ClientError::Decode(format!("invalid query-not-found response: {error}")),
3622 };
3623 }
3624 let queryless_sql_response = value.get("query_id").is_none()
3625 && value
3626 .get("error")
3627 .and_then(|error| error.get("query_id"))
3628 .is_none()
3629 && value.get("outcome").is_some();
3630 if queryless_sql_response {
3631 return match serde_json::from_value::<RemoteQueryErrorResponse>(value) {
3632 Ok(response) => match validate_queryless_sql_error(response) {
3633 Ok(response) => ClientError::Query {
3634 status,
3635 code: response.error.code.clone(),
3636 message: response.error.message.clone(),
3637 response: Box::new(response),
3638 },
3639 Err(error) => {
3640 ClientError::Decode(format!("invalid queryless SQL error response: {error}"))
3641 }
3642 },
3643 Err(error) => {
3644 ClientError::Decode(format!("invalid queryless SQL error response: {error}"))
3645 }
3646 };
3647 }
3648 let query_response = value.get("query_id").is_some()
3649 || value.get("outcome").is_some()
3650 || value.get("server_state").is_some()
3651 || value
3652 .get("error")
3653 .and_then(|error| error.get("query_id"))
3654 .is_some();
3655 if query_response {
3656 return match serde_json::from_value::<RemoteQueryErrorResponse>(value) {
3657 Ok(response) => {
3658 let query_id = response
3659 .query_id
3660 .as_deref()
3661 .or(response.error.query_id.as_deref())
3662 .and_then(|query_id| query_id.parse::<mongreldb_query::QueryId>().ok());
3663 match query_id
3664 .ok_or_else(|| "query error response lacks a valid query_id".to_owned())
3665 .and_then(|query_id| validate_remote_query_error(response, query_id))
3666 {
3667 Ok(response) => ClientError::Query {
3668 status,
3669 code: response.error.code.clone(),
3670 message: response.error.message.clone(),
3671 response: Box::new(response),
3672 },
3673 Err(error) => {
3674 ClientError::Decode(format!("invalid query error response: {error}"))
3675 }
3676 }
3677 }
3678 Err(error) => ClientError::Decode(format!("invalid query error response: {error}")),
3679 };
3680 }
3681 match serde_json::from_value::<KitErrorEnvelope>(value) {
3682 Ok(env) => match validate_kit_error_envelope(&env) {
3683 Ok(epoch) => ClientError::Kit {
3684 code: KitErrorCode::from_str(&env.error.code),
3685 message: env.error.message,
3686 op_index: env.error.op_index,
3687 status,
3688 committed: env.committed,
3689 epoch,
3690 epoch_text: env.epoch_text,
3691 retryable: env.retryable,
3692 },
3693 Err(message) => ClientError::Decode(message),
3694 },
3695 Err(error) => ClientError::Decode(format!("invalid Kit error response: {error}")),
3696 }
3697}
3698
3699fn validate_kit_error_envelope(env: &KitErrorEnvelope) -> Result<Option<u64>, String> {
3700 if env.status.is_empty()
3701 || env.error.code.is_empty()
3702 || env.error.message.is_empty()
3703 || env.retryable == Some(true)
3704 && !matches!(
3705 env.error.code.as_str(),
3706 "IDEMPOTENCY_STORE_FULL" | "IDEMPOTENCY_STORE_UNAVAILABLE"
3707 )
3708 {
3709 return Err("invalid Kit error metadata".into());
3710 }
3711 let epoch = exact_kit_epoch(env.epoch_text.as_deref(), env.epoch)?;
3712 match env.error.code.as_str() {
3713 "COMMIT_OUTCOME" => {
3714 if env.status != "committed"
3715 || env.committed != Some(true)
3716 || epoch.is_none()
3717 || env.epoch_text.is_none()
3718 || env.retryable != Some(false)
3719 || env.error.op_index.is_some()
3720 {
3721 return Err("invalid committed Kit error metadata".into());
3722 }
3723 }
3724 "QUERY_OUTCOME_UNKNOWN" => {
3725 if env.status != "outcome_unknown"
3726 || env.committed.is_some()
3727 || env.results.is_some()
3728 || env.retryable != Some(false)
3729 || env.error.op_index.is_some()
3730 {
3731 return Err("invalid unknown Kit outcome metadata".into());
3732 }
3733 }
3734 _ => {
3735 let metadata_valid = match env.status.as_str() {
3736 "aborted" => matches!(
3737 (env.committed, env.retryable),
3738 (None, None) | (Some(false), Some(_))
3739 ),
3740 "error" => env.committed.is_none() && env.retryable.is_none(),
3741 _ => false,
3742 };
3743 if !metadata_valid || epoch.is_some() || env.results.is_some() {
3744 return Err("invalid aborted Kit error metadata".into());
3745 }
3746 }
3747 }
3748 Ok(epoch)
3749}
3750
3751fn decode_kit_txn_http_error(status: u16, body: &[u8], request: &KitTxnRequest) -> ClientError {
3752 let value = match strict_json_value(body) {
3753 Ok(value) => value,
3754 Err(error) => {
3755 return kit_txn_outcome_unknown(format!(
3756 "invalid Kit transaction error response: {error}"
3757 ))
3758 }
3759 };
3760 let proven_commit = proven_kit_commit_metadata(&value);
3761 if let Err(error) = validate_kit_txn_error_json(&value) {
3762 if let Some((epoch, epoch_text)) = proven_commit.as_ref() {
3763 return committed_kit_txn_decode_error(status, *epoch, epoch_text.clone(), error);
3764 }
3765 return kit_txn_outcome_unknown(error);
3766 }
3767 let env = match serde_json::from_value::<KitErrorEnvelope>(value) {
3768 Ok(env) => env,
3769 Err(error) => {
3770 if let Some((epoch, epoch_text)) = proven_commit {
3771 return committed_kit_txn_decode_error(
3772 status,
3773 epoch,
3774 epoch_text,
3775 format!(
3776 "transaction committed but its error result could not be decoded: {error}"
3777 ),
3778 );
3779 }
3780 return kit_txn_outcome_unknown(format!(
3781 "invalid Kit transaction error response: {error}"
3782 ));
3783 }
3784 };
3785 let epoch = match validate_kit_error_envelope(&env) {
3786 Ok(epoch) => epoch,
3787 Err(error) => {
3788 if let Some((epoch, epoch_text)) = proven_commit {
3789 return committed_kit_txn_decode_error(status, epoch, epoch_text, error);
3790 }
3791 return kit_txn_outcome_unknown(error);
3792 }
3793 };
3794 if env
3795 .error
3796 .op_index
3797 .is_some_and(|op_index| op_index >= request.ops.len())
3798 {
3799 return kit_txn_outcome_unknown("Kit transaction error op_index is out of bounds");
3800 }
3801 if env.committed == Some(true) {
3802 if let Some(results) = env.results.as_deref() {
3803 if let Err(error) = validate_kit_results(results, request) {
3804 let (Some(epoch), Some(epoch_text)) = (epoch, env.epoch_text.clone()) else {
3805 return kit_txn_outcome_unknown(
3806 "committed Kit transaction response lost validated epoch metadata",
3807 );
3808 };
3809 return committed_kit_txn_decode_error(
3810 status,
3811 epoch,
3812 epoch_text,
3813 format!("transaction committed but its result was invalid: {error}"),
3814 );
3815 }
3816 }
3817 }
3818 ClientError::Kit {
3819 code: KitErrorCode::from_str(&env.error.code),
3820 message: env.error.message,
3821 op_index: env.error.op_index,
3822 status,
3823 committed: env.committed,
3824 epoch,
3825 epoch_text: env.epoch_text,
3826 retryable: env.retryable,
3827 }
3828}
3829
3830fn decode_sql_http_error(
3831 status: u16,
3832 body: &[u8],
3833 expected_query_id: mongreldb_query::QueryId,
3834) -> Result<ClientError, String> {
3835 let value =
3836 strict_json_value(body).map_err(|error| format!("invalid SQL error response: {error}"))?;
3837 if status == 404 && is_exact_query_not_found_response(body, expected_query_id) {
3838 let response = serde_json::from_value::<RemoteQueryErrorResponse>(value)
3839 .map_err(|error| format!("invalid query-not-found response: {error}"))?;
3840 return Ok(ClientError::Query {
3841 status,
3842 code: RemoteQueryErrorCode::QueryNotFound,
3843 message: response.error.message.clone(),
3844 response: Box::new(response),
3845 });
3846 }
3847 let proof = sql_error_commit_proof(&value, expected_query_id);
3848 let response = match serde_json::from_value::<RemoteQueryErrorResponse>(value) {
3849 Ok(response) => response,
3850 Err(error) => {
3851 if let Some(proof) = proof.clone() {
3852 return Ok(committed_sql_receipt_decode_error(
3853 expected_query_id,
3854 proof,
3855 format!("SQL committed but its error response was invalid: {error}"),
3856 ));
3857 }
3858 return Err(format!("invalid SQL error response: {error}"));
3859 }
3860 };
3861 let response = match validate_remote_query_error(response, expected_query_id) {
3862 Ok(response) => response,
3863 Err(error) => {
3864 if let Some(proof) = proof {
3865 return Ok(committed_sql_receipt_decode_error(
3866 expected_query_id,
3867 proof,
3868 format!("SQL committed but its error response was invalid: {error}"),
3869 ));
3870 }
3871 return Err(error);
3872 }
3873 };
3874 Ok(ClientError::Query {
3875 status,
3876 code: response.error.code.clone(),
3877 message: response.error.message.clone(),
3878 response: Box::new(response),
3879 })
3880}
3881
3882fn validate_sql_query_id_header(
3883 headers: &reqwest::header::HeaderMap,
3884 expected_query_id: mongreldb_query::QueryId,
3885) -> Result<(), String> {
3886 let value = headers
3887 .get("x-mongreldb-query-id")
3888 .ok_or_else(|| "SQL response is missing x-mongreldb-query-id".to_owned())?
3889 .to_str()
3890 .map_err(|_| "SQL response has a non-UTF-8 x-mongreldb-query-id".to_owned())?;
3891 if value != expected_query_id.to_string() {
3892 return Err("SQL response x-mongreldb-query-id does not match the request".into());
3893 }
3894 Ok(())
3895}
3896
3897fn client_error_proves_commit(error: &ClientError) -> bool {
3898 matches!(
3899 error,
3900 ClientError::Query { response, .. }
3901 if response.committed == Some(true)
3902 && response.committed_statements.is_some_and(|count| count > 0)
3903 && response.last_commit_epoch.is_some()
3904 && response.last_commit_epoch_text.is_some()
3905 )
3906}
3907
3908fn exact_kit_epoch(text: Option<&str>, numeric: Option<u64>) -> Result<Option<u64>, String> {
3909 let Some(text) = text else {
3910 return Ok(numeric);
3911 };
3912 let exact = text
3913 .parse::<u64>()
3914 .map_err(|_| "epoch_text is not an unsigned integer".to_owned())?;
3915 if exact.to_string() != text {
3916 return Err("epoch_text is not canonical".into());
3917 }
3918 if numeric.is_some_and(|numeric| numeric != exact) {
3919 return Err("epoch and epoch_text disagree".into());
3920 }
3921 Ok(Some(exact))
3922}
3923
3924fn exact_required_u64(field: &str, numeric: u64, text: &str) -> ClientResult<u64> {
3925 let exact = text
3926 .parse::<u64>()
3927 .map_err(|_| ClientError::Decode(format!("{field}_text is not an unsigned integer")))?;
3928 if exact.to_string() != text {
3929 return Err(ClientError::Decode(format!(
3930 "{field}_text is not canonical"
3931 )));
3932 }
3933 if exact != numeric {
3934 return Err(ClientError::Decode(format!(
3935 "{field} and {field}_text disagree"
3936 )));
3937 }
3938 Ok(exact)
3939}
3940
3941fn parse_required_u64_text(field: &str, text: &str) -> ClientResult<u64> {
3942 let value = text
3943 .parse::<u64>()
3944 .map_err(|_| ClientError::Decode(format!("{field} is not an unsigned integer")))?;
3945 if value.to_string() != text {
3946 return Err(ClientError::Decode(format!("{field} is not canonical")));
3947 }
3948 Ok(value)
3949}
3950
3951fn decode_required_json<T: serde::de::DeserializeOwned>(
3952 response: reqwest::blocking::Response,
3953 context: &str,
3954) -> ClientResult<T> {
3955 decode_blocking_json(response, MAX_CONTROL_RESPONSE_BYTES, context)
3956}
3957
3958fn write_outcome_unknown(context: &str, error: impl std::fmt::Display) -> ClientError {
3959 kit_txn_outcome_unknown(format!(
3960 "{context} outcome is unknown because its response could not be confirmed: {error}"
3961 ))
3962}
3963
3964fn decode_write_json<T: serde::de::DeserializeOwned>(
3965 response: reqwest::blocking::Response,
3966 context: &str,
3967) -> ClientResult<T> {
3968 decode_required_json(response, context).map_err(|error| write_outcome_unknown(context, error))
3969}
3970
3971fn validate_committed_write(response: &CommittedWriteResponse, context: &str) -> ClientResult<u64> {
3972 if response.status != "committed" {
3973 return Err(write_outcome_unknown(
3974 context,
3975 "success response does not prove a commit",
3976 ));
3977 }
3978 exact_required_u64("epoch", response.epoch, &response.epoch_text)
3979 .map_err(|error| write_outcome_unknown(context, error))
3980}
3981
3982fn validate_trigger_drop_response(
3983 response: &TriggerDropResponse,
3984 expected_name: &str,
3985) -> ClientResult<()> {
3986 validate_committed_write(
3987 &CommittedWriteResponse {
3988 status: response.status.clone(),
3989 epoch: response.epoch,
3990 epoch_text: response.epoch_text.clone(),
3991 },
3992 "drop trigger",
3993 )?;
3994 let mut names = std::collections::HashSet::new();
3995 let mut identities = std::collections::HashSet::new();
3996 if response.dropped_trigger.name != expected_name
3997 || response.resource_tables.iter().any(|binding| {
3998 binding.name.is_empty()
3999 || !names.insert(&binding.name)
4000 || !identities.insert((binding.table_id, binding.schema_id))
4001 })
4002 {
4003 return Err(write_outcome_unknown(
4004 "drop trigger",
4005 "success response has an invalid resource binding",
4006 ));
4007 }
4008 Ok(())
4009}
4010
4011fn capability_unsupported(message: impl Into<String>) -> ClientError {
4012 let message = message.into();
4013 let response = RemoteQueryErrorResponse {
4014 query_id: None,
4015 status: "failed_before_commit".into(),
4016 terminal_state: Some("failed_before_commit".into()),
4017 committed: Some(false),
4018 committed_statements: Some(0),
4019 last_commit_epoch: None,
4020 last_commit_epoch_text: None,
4021 first_commit_statement_index: None,
4022 last_commit_statement_index: None,
4023 completed_statements: Some(0),
4024 statement_index: Some(0),
4025 cancel_outcome: None,
4026 cancellation_reason: None,
4027 retryable: false,
4028 server_state: None,
4029 outcome: RemoteQueryOutcome::default(),
4030 error: RemoteQueryErrorBody {
4031 code: RemoteQueryErrorCode::CapabilityUnsupported,
4032 message: message.clone(),
4033 query_id: None,
4034 committed: Some(false),
4035 retryable: false,
4036 },
4037 };
4038 ClientError::Query {
4039 status: 0,
4040 code: RemoteQueryErrorCode::CapabilityUnsupported,
4041 message,
4042 response: Box::new(response),
4043 }
4044}
4045
4046fn client_serialization_error(
4047 query_id: Option<mongreldb_query::QueryId>,
4048 message: impl Into<String>,
4049) -> ClientError {
4050 let message = message.into();
4051 let query_id = query_id.map(|query_id| query_id.to_string());
4052 let response = RemoteQueryErrorResponse {
4053 query_id: query_id.clone(),
4054 status: "failed_before_commit".into(),
4055 terminal_state: Some("failed_before_commit".into()),
4056 committed: Some(false),
4057 committed_statements: Some(0),
4058 last_commit_epoch: None,
4059 last_commit_epoch_text: None,
4060 first_commit_statement_index: None,
4061 last_commit_statement_index: None,
4062 completed_statements: Some(0),
4063 statement_index: Some(0),
4064 cancel_outcome: None,
4065 cancellation_reason: None,
4066 retryable: false,
4067 server_state: Some("failed".into()),
4068 outcome: RemoteQueryOutcome {
4069 committed: Some(false),
4070 committed_statements: Some(0),
4071 completed_statements: Some(0),
4072 statement_index: Some(0),
4073 serialization: "failed".into(),
4074 ..RemoteQueryOutcome::default()
4075 },
4076 error: RemoteQueryErrorBody {
4077 code: RemoteQueryErrorCode::SerializationFailed,
4078 message: message.clone(),
4079 query_id,
4080 committed: Some(false),
4081 retryable: false,
4082 },
4083 };
4084 ClientError::Query {
4085 status: 0,
4086 code: RemoteQueryErrorCode::SerializationFailed,
4087 message,
4088 response: Box::new(response),
4089 }
4090}
4091
4092struct IdempotentAttemptError {
4093 error: ClientError,
4094 replay: bool,
4095}
4096
4097impl IdempotentAttemptError {
4098 fn final_error(error: ClientError) -> Self {
4099 Self {
4100 error,
4101 replay: false,
4102 }
4103 }
4104}
4105
4106fn fresh_query_id(previous: mongreldb_query::QueryId) -> ClientResult<mongreldb_query::QueryId> {
4107 let query_id = mongreldb_query::QueryId::random()
4108 .map_err(|error| ClientError::Transport(error.to_string()))?;
4109 if query_id == previous {
4110 return Err(ClientError::Transport(
4111 "generated duplicate SQL query ID".into(),
4112 ));
4113 }
4114 Ok(query_id)
4115}
4116
4117fn max_known(left: Option<usize>, right: Option<usize>) -> Option<usize> {
4118 left.into_iter().chain(right).max()
4119}
4120
4121fn recovered_query_error(status: RemoteQueryStatus, message: String) -> ClientError {
4122 let committed = status.durably_committed();
4123 let code = status
4124 .terminal_error
4125 .as_ref()
4126 .map(|error| error.code.clone())
4127 .unwrap_or_else(|| {
4128 if committed == Some(true) {
4129 RemoteQueryErrorCode::CommitOutcome
4130 } else {
4131 RemoteQueryErrorCode::SerializationFailed
4132 }
4133 });
4134 let response = RemoteQueryErrorResponse {
4135 query_id: Some(status.query_id.clone()),
4136 status: status.status.clone(),
4137 terminal_state: status.terminal_state.clone(),
4138 committed,
4139 committed_statements: max_known(
4140 status.committed_statements,
4141 status.outcome.committed_statements,
4142 ),
4143 last_commit_epoch: status
4144 .last_commit_epoch
4145 .or(status.outcome.last_commit_epoch),
4146 last_commit_epoch_text: status
4147 .last_commit_epoch_text
4148 .clone()
4149 .or_else(|| status.outcome.last_commit_epoch_text.clone()),
4150 first_commit_statement_index: status
4151 .first_commit_statement_index
4152 .or(status.outcome.first_commit_statement_index),
4153 last_commit_statement_index: status
4154 .last_commit_statement_index
4155 .or(status.outcome.last_commit_statement_index),
4156 completed_statements: max_known(
4157 status.completed_statements,
4158 status.outcome.completed_statements,
4159 ),
4160 statement_index: max_known(status.statement_index, status.outcome.statement_index),
4161 cancel_outcome: status.cancel_outcome,
4162 cancellation_reason: (!status.cancellation_reason.is_empty())
4163 .then_some(status.cancellation_reason.clone()),
4164 retryable: status.retryable,
4165 server_state: Some(status.server_state_or_state().to_owned()),
4166 outcome: status.outcome,
4167 error: RemoteQueryErrorBody {
4168 code: code.clone(),
4169 message: message.clone(),
4170 query_id: Some(status.query_id),
4171 committed,
4172 retryable: status.retryable,
4173 },
4174 };
4175 ClientError::Query {
4176 status: 0,
4177 code,
4178 message,
4179 response: Box::new(response),
4180 }
4181}
4182
4183fn recovery_status_is_decisive(status: &RemoteQueryStatus) -> bool {
4184 status.durably_committed() == Some(true)
4185 || status.is_terminal()
4186 && (status.durably_committed().is_some() || status.terminal_error.is_some())
4187}
4188
4189impl MongrelClient {
4190 pub fn builder(url: impl AsRef<str>) -> MongrelClientBuilder {
4191 let base_url = sanitized_base_url(url.as_ref());
4192 MongrelClientBuilder {
4193 invalid_base_url: base_url.is_none(),
4194 base_url: base_url.unwrap_or_default(),
4195 authorization: None,
4196 invalid_authorization: false,
4197 connect_timeout: None,
4198 request_timeout: None,
4199 pool_idle_timeout: None,
4200 }
4201 }
4202
4203 pub fn new(url: &str) -> ClientResult<Self> {
4204 Self::builder(url).build()
4205 }
4206
4207 pub fn with_options(url: impl AsRef<str>, options: RemoteOptions) -> ClientResult<Self> {
4208 let mut builder = Self::builder(url);
4209 if let Some(timeout) = options.transport_timeout {
4210 builder = builder.request_timeout(timeout);
4211 }
4212 builder = match options.auth {
4213 Some(RemoteAuth::Bearer(token)) => builder.bearer_token(token.expose_secret()),
4214 Some(RemoteAuth::Basic { username, password }) => {
4215 builder.basic_auth(username, password.expose_secret())
4216 }
4217 None => builder,
4218 };
4219 builder.build()
4220 }
4221
4222 fn url(&self, path: &str) -> String {
4223 format!("{}{path}", self.base_url)
4224 }
4225
4226 fn url_segments(&self, segments: &[&str]) -> ClientResult<String> {
4227 url_with_segments(&self.base_url, segments)
4228 }
4229
4230 fn check(
4233 &self,
4234 resp: reqwest::blocking::Response,
4235 ) -> ClientResult<reqwest::blocking::Response> {
4236 let status = resp.status();
4237 if status.is_success() {
4238 return Ok(resp);
4239 }
4240 let status_u16 = status.as_u16();
4241 let body = bounded_blocking_bytes(resp, MAX_CONTROL_RESPONSE_BYTES).map_err(|error| {
4242 ClientError::Decode(format!("invalid HTTP error response: {error}"))
4243 })?;
4244 Err(decode_http_error(status_u16, &body))
4245 }
4246
4247 fn write_response(
4248 &self,
4249 response: Result<reqwest::blocking::Response, reqwest::Error>,
4250 context: &str,
4251 ) -> ClientResult<reqwest::blocking::Response> {
4252 let response = response.map_err(|error| write_outcome_unknown(context, error))?;
4253 let status = response.status();
4254 if status.is_success() {
4255 return Ok(response);
4256 }
4257 let body = bounded_blocking_bytes(response, MAX_CONTROL_RESPONSE_BYTES)
4258 .map_err(|error| write_outcome_unknown(context, error))?;
4259 let error = decode_http_error(status.as_u16(), &body);
4260 match error {
4261 ClientError::Decode(message) => Err(write_outcome_unknown(context, message)),
4262 error => Err(error),
4263 }
4264 }
4265
4266 fn check_sql_response(
4267 &self,
4268 response: reqwest::blocking::Response,
4269 query_id: mongreldb_query::QueryId,
4270 ) -> ClientResult<reqwest::blocking::Response> {
4271 let status = response.status();
4272 let pre_handler_auth = matches!(
4273 status,
4274 reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
4275 );
4276 if status.is_success() {
4277 return Ok(response);
4278 }
4279 let header_error = (!pre_handler_auth)
4280 .then(|| validate_sql_query_id_header(response.headers(), query_id).err())
4281 .flatten();
4282 let status = status.as_u16();
4283 let body =
4284 bounded_blocking_bytes(response, MAX_CONTROL_RESPONSE_BYTES).map_err(|error| {
4285 if pre_handler_auth {
4286 ClientError::Http {
4287 status,
4288 body: format!("unreadable authentication error response: {error}"),
4289 }
4290 } else {
4291 self.recover_after_transport_loss(query_id, error.to_string())
4292 }
4293 })?;
4294 if pre_handler_auth {
4295 return Err(decode_http_error(status, &body));
4296 }
4297 match decode_sql_http_error(status, &body, query_id) {
4298 Ok(error) if client_error_proves_commit(&error) => Err(error),
4299 Ok(error) if header_error.is_none() => Err(error),
4300 Ok(_) => Err(self.recover_after_transport_loss(
4301 query_id,
4302 header_error.unwrap_or_else(|| "SQL response identity is invalid".into()),
4303 )),
4304 Err(error) => Err(self.recover_after_transport_loss(query_id, error)),
4305 }
4306 }
4307
4308 fn check_query_status_response(
4309 &self,
4310 response: reqwest::blocking::Response,
4311 query_id: mongreldb_query::QueryId,
4312 ) -> ClientResult<reqwest::blocking::Response> {
4313 let status = response.status();
4314 if status.is_success() {
4315 return Ok(response);
4316 }
4317 let status = status.as_u16();
4318 let body = bounded_blocking_bytes(response, MAX_CONTROL_RESPONSE_BYTES)
4319 .map_err(ClientError::Decode)?;
4320 if matches!(status, 401 | 403) {
4321 return Err(decode_http_error(status, &body));
4322 }
4323 match decode_sql_http_error(status, &body, query_id) {
4324 Ok(error) => Err(error),
4325 Err(error) => Err(ClientError::Decode(error)),
4326 }
4327 }
4328
4329 pub fn health(&self) -> ClientResult<String> {
4330 let resp = self.client.get(self.url("/health")).send()?;
4331 let bytes = bounded_blocking_bytes(self.check(resp)?, MAX_CONTROL_RESPONSE_BYTES)
4332 .map_err(ClientError::Transport)?;
4333 String::from_utf8(bytes)
4334 .map_err(|_| ClientError::Decode("invalid health response: non-UTF-8 body".into()))
4335 }
4336
4337 pub fn capabilities(&self) -> ClientResult<ServerCapabilities> {
4338 let response = self.client.get(self.url("/capabilities")).send()?;
4339 decode_blocking_json(
4340 self.check(response)?,
4341 MAX_CONTROL_RESPONSE_BYTES,
4342 "capabilities",
4343 )
4344 }
4345
4346 pub fn sql_cancellation_capabilities(&self) -> ClientResult<SqlCancellationCapabilities> {
4347 let capabilities = self.capabilities()?.sql_cancellation;
4348 if capabilities.version < 2
4349 || !capabilities.client_query_ids
4350 || !capabilities.cancel_endpoint
4351 || !capabilities.query_status
4352 || !capabilities.pre_registration_cancel
4353 {
4354 return Err(capability_unsupported(
4355 "server does not support SQL cancellation capability version 2",
4356 ));
4357 }
4358 Ok(capabilities)
4359 }
4360
4361 pub fn sql_idempotency_capabilities(&self) -> ClientResult<SqlIdempotencyCapabilities> {
4362 let capabilities = self
4363 .capabilities()?
4364 .sql_idempotency
4365 .ok_or_else(|| capability_unsupported("server does not advertise SQL idempotency"))?;
4366 if capabilities.version < 1
4367 || !capabilities.durable_pre_execution_intent
4368 || !capabilities.replay_committed_receipt
4369 || !capabilities.indeterminate_never_reexecutes
4370 {
4371 return Err(capability_unsupported(
4372 "server does not support safe SQL idempotency capability version 1",
4373 ));
4374 }
4375 Ok(capabilities)
4376 }
4377
4378 pub fn sql_pagination_capabilities(&self) -> ClientResult<SqlPaginationCapabilities> {
4379 let capabilities = self
4380 .capabilities()?
4381 .sql_pagination
4382 .ok_or_else(|| capability_unsupported("server does not advertise SQL pagination"))?;
4383 if capabilities.version < 1
4384 || capabilities.continuation_endpoint != "/sql/continue"
4385 || !capabilities.retained_snapshot
4386 || !capabilities.projection_required
4387 || !capabilities.byte_and_token_hints
4388 {
4389 return Err(capability_unsupported(
4390 "server does not support SQL pagination capability version 1",
4391 ));
4392 }
4393 Ok(capabilities)
4394 }
4395
4396 pub fn set_history_retention_epochs(&self, epochs: u64) -> ClientResult<HistoryRetention> {
4397 let resp = self
4398 .client
4399 .put(self.url("/history/retention"))
4400 .json(&serde_json::json!({"history_retention_epochs": epochs}))
4401 .send()?;
4402 decode_blocking_json(
4403 self.check(resp)?,
4404 MAX_CONTROL_RESPONSE_BYTES,
4405 "history retention",
4406 )
4407 }
4408
4409 pub fn history_retention_epochs(&self) -> ClientResult<u64> {
4410 Ok(self.history_retention()?.history_retention_epochs)
4411 }
4412
4413 pub fn earliest_retained_epoch(&self) -> ClientResult<u64> {
4414 Ok(self.history_retention()?.earliest_retained_epoch)
4415 }
4416
4417 fn history_retention(&self) -> ClientResult<HistoryRetention> {
4418 let resp = self.client.get(self.url("/history/retention")).send()?;
4419 decode_blocking_json(
4420 self.check(resp)?,
4421 MAX_CONTROL_RESPONSE_BYTES,
4422 "history retention",
4423 )
4424 }
4425
4426 pub fn list_tables(&self) -> ClientResult<Vec<String>> {
4429 let resp = self.client.get(self.url("/tables")).send()?;
4430 decode_blocking_json(self.check(resp)?, MAX_CONTROL_RESPONSE_BYTES, "table list")
4431 }
4432
4433 pub fn create_table(&self, name: &str, columns: Vec<ColumnDefJson>) -> ClientResult<u64> {
4434 let response = self.write_response(
4435 self.client
4436 .post(self.url("/tables"))
4437 .json(&serde_json::json!({ "name": name, "columns": columns }))
4438 .send(),
4439 "create table",
4440 )?;
4441 let response: TableIdResponse = decode_write_json(response, "create table")?;
4442 exact_required_u64("table_id", response.table_id, &response.table_id_text)
4443 .map_err(|error| write_outcome_unknown("create table", error))
4444 }
4445
4446 pub fn drop_table(&self, name: &str) -> ClientResult<()> {
4447 let response = self.write_response(
4448 self.client
4449 .delete(self.url_segments(&["tables", name])?)
4450 .send(),
4451 "drop table",
4452 )?;
4453 let response: CommittedWriteResponse = decode_write_json(response, "drop table")?;
4454 validate_committed_write(&response, "drop table")?;
4455 Ok(())
4456 }
4457
4458 pub fn count(&self, table: &str) -> ClientResult<u64> {
4461 let resp = self
4462 .client
4463 .get(self.url_segments(&["tables", table, "count"])?)
4464 .send()?;
4465 let resp = self.check(resp)?;
4466 let cr: CountResp = decode_blocking_json(resp, MAX_CONTROL_RESPONSE_BYTES, "count")?;
4467 Ok(cr.count)
4468 }
4469
4470 pub fn put(&self, table: &str, row: Vec<(u16, mongreldb_core::Value)>) -> ClientResult<u64> {
4471 let mut json_row = Vec::with_capacity(row.len() * 2);
4472 for (id, value) in &row {
4473 json_row.push(serde_json::json!(id));
4474 json_row.push(value_to_json(value)?);
4475 }
4476 let response = self.write_response(
4477 self.client
4478 .post(self.url_segments(&["tables", table, "put"])?)
4479 .json(&serde_json::json!({ "row": json_row }))
4480 .send(),
4481 "put",
4482 )?;
4483 let response: RowIdResponse = decode_write_json(response, "put")?;
4484 parse_required_u64_text("row_id", &response.row_id)
4485 .map_err(|error| write_outcome_unknown("put", error))
4486 }
4487
4488 pub fn commit(&self, table: &str) -> ClientResult<u64> {
4489 let response = self.write_response(
4490 self.client
4491 .post(self.url_segments(&["tables", table, "commit"])?)
4492 .send(),
4493 "commit",
4494 )?;
4495 let response: EpochResponse = decode_write_json(response, "commit")?;
4496 exact_required_u64("epoch", response.epoch, &response.epoch_text)
4497 .map_err(|error| write_outcome_unknown("commit", error))
4498 }
4499
4500 pub fn sql(&self, sql: &str) -> ClientResult<Vec<RecordBatch>> {
4503 self.sql_with_options(sql, SqlClientOptions::default())
4504 }
4505
4506 pub fn sql_with_options(
4507 &self,
4508 sql: &str,
4509 mut options: SqlClientOptions,
4510 ) -> ClientResult<Vec<RecordBatch>> {
4511 if options.query_id.is_some() || options.timeout.is_some() {
4512 self.sql_cancellation_capabilities()?;
4513 }
4514 let query_id = match options.query_id.take() {
4515 Some(query_id) => query_id,
4516 None => mongreldb_query::QueryId::random()
4517 .map_err(|error| ClientError::Transport(error.to_string()))?,
4518 };
4519 let timeout_ms = options
4520 .timeout
4521 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
4522 let response = self
4523 .client
4524 .post(self.url("/sql"))
4525 .json(&SqlReq {
4526 sql: sql.to_string(),
4527 format: Some("arrow"), query_id,
4529 timeout_ms,
4530 max_output_rows: None,
4531 max_output_bytes: None,
4532 idempotency_key: None,
4533 pagination: None,
4534 })
4535 .send();
4536 let response = match response {
4537 Ok(response) => response,
4538 Err(error) => {
4539 return Err(self.recover_after_transport_loss(query_id, error.to_string()));
4540 }
4541 };
4542 let resp = self.check_sql_response(response, query_id)?;
4543 if let Err(error) = validate_sql_query_id_header(resp.headers(), query_id) {
4544 return Err(self.recover_after_transport_loss(query_id, error));
4545 }
4546 let bytes = bounded_blocking_bytes(resp, MAX_SQL_RESPONSE_BYTES)
4547 .map_err(|error| self.recover_after_transport_loss(query_id, error.to_string()))?;
4548 read_arrow_ipc(&bytes)
4549 .map_err(|error| self.recover_after_transport_loss(query_id, error.to_string()))
4550 }
4551
4552 pub fn sql_write_idempotent(
4553 &self,
4554 sql: &str,
4555 idempotency_key: impl Into<String>,
4556 ) -> ClientResult<RemoteSqlReceipt> {
4557 self.sql_write_idempotent_with_options(sql, idempotency_key, SqlClientOptions::default())
4558 }
4559
4560 pub fn sql_write_idempotent_with_options(
4561 &self,
4562 sql: &str,
4563 idempotency_key: impl Into<String>,
4564 mut options: SqlClientOptions,
4565 ) -> ClientResult<RemoteSqlReceipt> {
4566 let idempotency_key = idempotency_key.into();
4567 if idempotency_key.is_empty() || idempotency_key.len() > 256 {
4568 return Err(ClientError::Decode(
4569 "SQL idempotency key must contain 1 to 256 bytes".into(),
4570 ));
4571 }
4572 self.sql_idempotency_capabilities()?;
4573 let query_id = match options.query_id.take() {
4574 Some(query_id) => query_id,
4575 None => mongreldb_query::QueryId::random()
4576 .map_err(|error| ClientError::Transport(error.to_string()))?,
4577 };
4578 let result =
4579 self.sql_write_idempotent_once(sql, &idempotency_key, &options, query_id, None);
4580 if result.as_ref().is_err_and(|error| error.replay) {
4581 self.sql_idempotency_capabilities()?;
4582 return self
4583 .sql_write_idempotent_once(
4584 sql,
4585 &idempotency_key,
4586 &options,
4587 fresh_query_id(query_id)?,
4588 Some(query_id),
4589 )
4590 .map_err(|error| error.error);
4591 }
4592 result.map_err(|error| error.error)
4593 }
4594
4595 fn sql_write_idempotent_once(
4596 &self,
4597 sql: &str,
4598 idempotency_key: &str,
4599 options: &SqlClientOptions,
4600 query_id: mongreldb_query::QueryId,
4601 expected_original_query_id: Option<mongreldb_query::QueryId>,
4602 ) -> Result<RemoteSqlReceipt, IdempotentAttemptError> {
4603 let timeout_ms = options
4604 .timeout
4605 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
4606 let response = self
4607 .client
4608 .post(self.url("/sql"))
4609 .json(&SqlReq {
4610 sql: sql.to_owned(),
4611 format: None,
4612 query_id,
4613 timeout_ms,
4614 max_output_rows: None,
4615 max_output_bytes: None,
4616 idempotency_key: Some(idempotency_key.to_owned()),
4617 pagination: None,
4618 })
4619 .send();
4620 let response = match response {
4621 Ok(response) => response,
4622 Err(error) => {
4623 return Err(self.idempotent_attempt_loss(query_id, error.to_string()));
4624 }
4625 };
4626 let response = self
4627 .check_sql_response(response, query_id)
4628 .map_err(IdempotentAttemptError::final_error)?;
4629 let header_error = validate_sql_query_id_header(response.headers(), query_id).err();
4630 let bytes = bounded_blocking_bytes(response, MAX_CONTROL_RESPONSE_BYTES)
4631 .map_err(|error| self.idempotent_attempt_loss(query_id, error))?;
4632 match decode_remote_sql_receipt(&bytes, query_id, expected_original_query_id) {
4633 Ok(receipt) if header_error.is_none() => Ok(receipt),
4634 Ok(_) => {
4635 let proof = strict_json_value(&bytes).ok().and_then(|value| {
4636 sql_receipt_commit_proof(&value, query_id, expected_original_query_id)
4637 });
4638 match proof {
4639 Some(proof) => Err(IdempotentAttemptError::final_error(
4640 committed_sql_receipt_decode_error(
4641 query_id,
4642 proof,
4643 header_error
4644 .clone()
4645 .unwrap_or_else(|| "SQL response identity is invalid".into()),
4646 ),
4647 )),
4648 None => Err(self.idempotent_attempt_loss(
4649 query_id,
4650 header_error.unwrap_or_else(|| "SQL response identity is invalid".into()),
4651 )),
4652 }
4653 }
4654 Err(SqlReceiptDecodeError::KnownCommit(error)) => {
4655 Err(IdempotentAttemptError::final_error(error))
4656 }
4657 Err(SqlReceiptDecodeError::Unknown(error)) => {
4658 Err(self.idempotent_attempt_loss(query_id, error))
4659 }
4660 }
4661 }
4662
4663 fn idempotent_attempt_loss(
4664 &self,
4665 query_id: mongreldb_query::QueryId,
4666 message: String,
4667 ) -> IdempotentAttemptError {
4668 let missing = self
4669 .client
4670 .get(self.url(&format!("/queries/{query_id}")))
4671 .timeout(SQL_RECOVERY_REQUEST_TIMEOUT)
4672 .send()
4673 .ok()
4674 .filter(|response| response.status() == reqwest::StatusCode::NOT_FOUND)
4675 .and_then(|response| bounded_blocking_bytes(response, MAX_CONTROL_RESPONSE_BYTES).ok())
4676 .is_some_and(|body| is_exact_query_not_found_response(&body, query_id));
4677 if missing {
4678 return IdempotentAttemptError {
4679 error: ClientError::QueryOutcomeUnknown {
4680 query_id: query_id.to_string(),
4681 message,
4682 status: None,
4683 cancel_outcome: None,
4684 },
4685 replay: true,
4686 };
4687 }
4688 IdempotentAttemptError::final_error(self.recover_after_transport_loss(query_id, message))
4689 }
4690
4691 pub fn sql_page(&self, sql: &str, mut options: SqlPageOptions) -> ClientResult<RemoteSqlPage> {
4692 validate_sql_page_options(&options)?;
4693 self.sql_pagination_capabilities()?;
4694 let query_id = match options.query_id.take() {
4695 Some(query_id) => query_id,
4696 None => mongreldb_query::QueryId::random()
4697 .map_err(|error| ClientError::Transport(error.to_string()))?,
4698 };
4699 let timeout_ms = options
4700 .timeout
4701 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
4702 let response = self
4703 .client
4704 .post(self.url("/sql"))
4705 .json(&SqlReq {
4706 sql: sql.to_owned(),
4707 format: None,
4708 query_id,
4709 timeout_ms,
4710 max_output_rows: options.max_output_rows,
4711 max_output_bytes: options.max_output_bytes,
4712 idempotency_key: None,
4713 pagination: Some(SqlPaginationReq {
4714 page_size_rows: options.page_size_rows,
4715 projection: options.projection.clone(),
4716 max_page_bytes: options.max_page_bytes,
4717 max_page_tokens: options.max_page_tokens,
4718 }),
4719 })
4720 .send();
4721 let response = match response {
4722 Ok(response) => response,
4723 Err(error) => {
4724 return Err(self.recover_after_transport_loss(query_id, error.to_string()));
4725 }
4726 };
4727 let page = bounded_blocking_bytes(
4728 {
4729 let response = self.check_sql_response(response, query_id)?;
4730 if let Err(error) = validate_sql_query_id_header(response.headers(), query_id) {
4731 return Err(self.recover_after_transport_loss(query_id, error));
4732 }
4733 response
4734 },
4735 MAX_SQL_RESPONSE_BYTES,
4736 )
4737 .and_then(|bytes| {
4738 strict_json::<RemoteSqlPage>(&bytes, "SQL page").map_err(|error| error.to_string())
4739 })
4740 .and_then(|page| validate_remote_sql_page(page, Some(&options)));
4741 page.map_err(|error| self.recover_after_transport_loss(query_id, error))
4742 }
4743
4744 pub fn continue_sql_page(
4745 &self,
4746 cursor: &str,
4747 mut options: RemoteSqlControlOptions,
4748 ) -> ClientResult<RemoteSqlPage> {
4749 if cursor.is_empty() || cursor.len() > 2_048 {
4750 return Err(ClientError::Decode(
4751 "SQL continuation cursor must contain 1 to 2048 bytes".into(),
4752 ));
4753 }
4754 self.sql_pagination_capabilities()?;
4755 let query_id = match options.query_id.take() {
4756 Some(query_id) => query_id,
4757 None => mongreldb_query::QueryId::random()
4758 .map_err(|error| ClientError::Transport(error.to_string()))?,
4759 };
4760 if options.timeout == Some(std::time::Duration::ZERO) {
4761 return Err(ClientError::Decode("timeout must be positive".into()));
4762 }
4763 let timeout_ms = options
4764 .timeout
4765 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
4766 let response = match self
4767 .client
4768 .post(self.url("/sql/continue"))
4769 .json(&serde_json::json!({
4770 "cursor": cursor,
4771 "operation_id": query_id,
4772 "timeout_ms": timeout_ms,
4773 }))
4774 .send()
4775 {
4776 Ok(response) => response,
4777 Err(error) => {
4778 return Err(self.recover_after_transport_loss(query_id, error.to_string()))
4779 }
4780 };
4781 let response = self.check_sql_response(response, query_id)?;
4782 validate_sql_query_id_header(response.headers(), query_id)
4783 .map_err(|error| client_serialization_error(Some(query_id), error))?;
4784 bounded_blocking_bytes(response, MAX_SQL_RESPONSE_BYTES)
4785 .and_then(|bytes| {
4786 strict_json::<RemoteSqlPage>(&bytes, "SQL page").map_err(|error| error.to_string())
4787 })
4788 .and_then(|page| validate_remote_sql_page(page, None))
4789 .map_err(|error| client_serialization_error(Some(query_id), error))
4790 }
4791
4792 pub fn start_sql(
4793 &self,
4794 sql: impl Into<String>,
4795 mut options: SqlClientOptions,
4796 ) -> ClientResult<RemoteSqlQueryHandle> {
4797 self.sql_cancellation_capabilities()?;
4798 let query_id = match options.query_id {
4799 Some(query_id) => query_id,
4800 None => mongreldb_query::QueryId::random()
4801 .map_err(|error| ClientError::Transport(error.to_string()))?,
4802 };
4803 options.query_id = Some(query_id);
4804 let client = self.clone();
4805 let cancel_client = self.clone();
4806 let sql = sql.into();
4807 let result = std::thread::Builder::new()
4808 .name(format!("mongreldb-sql-{query_id}"))
4809 .spawn(move || client.sql_with_options(&sql, options))
4810 .map_err(|error| ClientError::Transport(error.to_string()))?;
4811 Ok(RemoteSqlQueryHandle {
4812 query_id,
4813 client: cancel_client,
4814 result,
4815 })
4816 }
4817
4818 pub fn cancel_sql(
4819 &self,
4820 query_id: mongreldb_query::QueryId,
4821 ) -> ClientResult<RemoteCancelOutcome> {
4822 self.sql_cancellation_capabilities()?;
4823 let response = self
4824 .client
4825 .post(self.url(&format!("/queries/{query_id}/cancel")))
4826 .send()?;
4827 let status = response.status();
4828 if !matches!(
4829 status,
4830 reqwest::StatusCode::OK
4831 | reqwest::StatusCode::ACCEPTED
4832 | reqwest::StatusCode::CONFLICT
4833 | reqwest::StatusCode::NOT_FOUND
4834 ) {
4835 return match self.check(response) {
4836 Err(error) => Err(error),
4837 Ok(_) => Err(ClientError::Decode(
4838 "unexpected successful cancellation response".into(),
4839 )),
4840 };
4841 }
4842 let body: serde_json::Value =
4843 decode_blocking_json(response, MAX_CONTROL_RESPONSE_BYTES, "cancellation")?;
4844 decode_cancel_outcome(&body, query_id, status.as_u16())
4845 }
4846
4847 pub fn query_status(
4848 &self,
4849 query_id: mongreldb_query::QueryId,
4850 ) -> ClientResult<RemoteQueryStatus> {
4851 self.sql_cancellation_capabilities()?;
4852 let response = self
4853 .client
4854 .get(self.url(&format!("/queries/{query_id}")))
4855 .send()?;
4856 let status = decode_blocking_json(
4857 self.check_query_status_response(response, query_id)?,
4858 MAX_CONTROL_RESPONSE_BYTES,
4859 "query status",
4860 )?;
4861 validate_remote_query_status(status, query_id).map_err(ClientError::Decode)
4862 }
4863
4864 fn query_status_optional(
4865 &self,
4866 query_id: mongreldb_query::QueryId,
4867 timeout: std::time::Duration,
4868 ) -> Option<RemoteQueryStatus> {
4869 let response = self
4870 .client
4871 .get(self.url(&format!("/queries/{query_id}")))
4872 .timeout(timeout)
4873 .send()
4874 .ok()?;
4875 if !response.status().is_success() {
4876 return None;
4877 }
4878 let status =
4879 decode_blocking_json(response, MAX_CONTROL_RESPONSE_BYTES, "query status").ok()?;
4880 validate_remote_query_status(status, query_id).ok()
4881 }
4882
4883 fn cancel_sql_for_recovery(
4884 &self,
4885 query_id: mongreldb_query::QueryId,
4886 timeout: std::time::Duration,
4887 ) -> Option<RemoteCancelOutcome> {
4888 let response = self
4889 .client
4890 .post(self.url(&format!("/queries/{query_id}/cancel")))
4891 .timeout(timeout)
4892 .send()
4893 .ok()?;
4894 let status = response.status();
4895 if !matches!(
4896 status,
4897 reqwest::StatusCode::OK
4898 | reqwest::StatusCode::ACCEPTED
4899 | reqwest::StatusCode::CONFLICT
4900 | reqwest::StatusCode::NOT_FOUND
4901 ) {
4902 return None;
4903 }
4904 let body =
4905 decode_blocking_json(response, MAX_CONTROL_RESPONSE_BYTES, "cancellation").ok()?;
4906 decode_cancel_outcome(&body, query_id, status.as_u16()).ok()
4907 }
4908
4909 fn recover_after_transport_loss(
4910 &self,
4911 query_id: mongreldb_query::QueryId,
4912 message: String,
4913 ) -> ClientError {
4914 let deadline = std::time::Instant::now() + SQL_RECOVERY_WINDOW;
4915 let mut status = self.query_status_optional(query_id, SQL_RECOVERY_REQUEST_TIMEOUT);
4916 if let Some(decisive) = status
4917 .as_ref()
4918 .filter(|status| recovery_status_is_decisive(status))
4919 {
4920 return recovered_query_error(decisive.clone(), message);
4921 }
4922 let cancel_outcome = self.cancel_sql_for_recovery(
4923 query_id,
4924 deadline
4925 .saturating_duration_since(std::time::Instant::now())
4926 .min(SQL_RECOVERY_REQUEST_TIMEOUT),
4927 );
4928 if status.is_none() && cancel_outcome == Some(RemoteCancelOutcome::NotFound) {
4929 return ClientError::QueryOutcomeUnknown {
4930 query_id: query_id.to_string(),
4931 message,
4932 status: None,
4933 cancel_outcome,
4934 };
4935 }
4936 while !status.as_ref().is_some_and(recovery_status_is_decisive)
4937 && std::time::Instant::now() < deadline
4938 {
4939 std::thread::sleep(
4940 deadline
4941 .saturating_duration_since(std::time::Instant::now())
4942 .min(SQL_RECOVERY_POLL_INTERVAL),
4943 );
4944 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
4945 if remaining.is_zero() {
4946 break;
4947 }
4948 status = self
4949 .query_status_optional(query_id, remaining.min(SQL_RECOVERY_REQUEST_TIMEOUT))
4950 .or(status);
4951 }
4952 if let Some(decisive) = status
4953 .as_ref()
4954 .filter(|status| recovery_status_is_decisive(status))
4955 {
4956 return recovered_query_error(decisive.clone(), message);
4957 }
4958 ClientError::QueryOutcomeUnknown {
4959 query_id: query_id.to_string(),
4960 message,
4961 status: status.map(Box::new),
4962 cancel_outcome,
4963 }
4964 }
4965
4966 pub fn txn(&self, ops: Vec<TxnOp>) -> ClientResult<()> {
4969 let response = self.write_response(
4970 self.client
4971 .post(self.url("/txn"))
4972 .json(&serde_json::json!({ "ops": ops }))
4973 .send(),
4974 "transaction",
4975 )?;
4976 let response: CommittedWriteResponse = decode_write_json(response, "transaction")?;
4977 validate_committed_write(&response, "transaction")?;
4978 Ok(())
4979 }
4980
4981 pub fn kit_schema(&self, table: &str) -> ClientResult<TableSchemaInfo> {
4985 let resp = self
4986 .client
4987 .get(self.url_segments(&["kit", "schema", table])?)
4988 .send()?;
4989 decode_blocking_json(self.check(resp)?, MAX_CONTROL_RESPONSE_BYTES, "Kit schema")
4990 }
4991
4992 pub fn kit_txn(&self, req: &KitTxnRequest) -> ClientResult<KitTxnResponse> {
4995 let resp = self
4996 .client
4997 .post(self.url("/kit/txn"))
4998 .json(req)
4999 .send()
5000 .map_err(|error| {
5001 kit_txn_outcome_unknown(format!(
5002 "Kit transaction transport failed before outcome confirmation: {error}"
5003 ))
5004 })?;
5005 if !resp.status().is_success() {
5006 let status = resp.status().as_u16();
5007 if matches!(status, 401 | 403) {
5008 return Err(kit_txn_auth_error(status));
5009 }
5010 let body = bounded_blocking_bytes(resp, MAX_SQL_RESPONSE_BYTES).map_err(|error| {
5011 kit_txn_outcome_unknown(format!(
5012 "Kit transaction error response could not be read: {error}"
5013 ))
5014 })?;
5015 return Err(decode_kit_txn_http_error(status, &body, req));
5016 }
5017 let status = resp.status().as_u16();
5018 let body = bounded_blocking_bytes(resp, MAX_SQL_RESPONSE_BYTES).map_err(|error| {
5019 kit_txn_outcome_unknown(format!(
5020 "Kit transaction success response could not be read: {error}"
5021 ))
5022 })?;
5023 decode_kit_txn_success(&body, req, status)
5024 }
5025
5026 pub fn kit_query(&self, req: &KitQueryRequest) -> ClientResult<KitQueryResponse> {
5030 let resp = self.client.post(self.url("/kit/query")).json(req).send()?;
5031 let response =
5032 decode_blocking_json(self.check(resp)?, MAX_SQL_RESPONSE_BYTES, "Kit query")?;
5033 validate_kit_query_response(response, req)
5034 }
5035
5036 pub fn kit_retrieve(&self, req: &KitRetrieveRequest) -> ClientResult<KitRetrieveResponse> {
5037 self.kit_retrieve_with_options(req, &AiExecutionOptions::default())
5038 }
5039
5040 pub fn kit_retrieve_with_options(
5041 &self,
5042 req: &KitRetrieveRequest,
5043 options: &AiExecutionOptions,
5044 ) -> ClientResult<KitRetrieveResponse> {
5045 let resp = self
5046 .client
5047 .post(self.url("/kit/retrieve"))
5048 .json(&WithAiExecutionOptions {
5049 request: req,
5050 options,
5051 })
5052 .send()?;
5053 decode_blocking_json(self.check(resp)?, MAX_SQL_RESPONSE_BYTES, "Kit retrieve")
5054 }
5055
5056 pub fn kit_retrieve_text(
5058 &self,
5059 req: &KitRetrieveTextRequest,
5060 ) -> ClientResult<KitRetrieveTextResponse> {
5061 self.kit_retrieve_text_with_options(req, &AiExecutionOptions::default())
5062 }
5063
5064 pub fn kit_retrieve_text_with_options(
5065 &self,
5066 req: &KitRetrieveTextRequest,
5067 options: &AiExecutionOptions,
5068 ) -> ClientResult<KitRetrieveTextResponse> {
5069 let resp = self
5070 .client
5071 .post(self.url("/kit/retrieve_text"))
5072 .json(&WithAiExecutionOptions {
5073 request: req,
5074 options,
5075 })
5076 .send()?;
5077 decode_blocking_json(
5078 self.check(resp)?,
5079 MAX_SQL_RESPONSE_BYTES,
5080 "Kit retrieve_text",
5081 )
5082 }
5083
5084 pub fn kit_ann_rerank(&self, req: &KitAnnRerankRequest) -> ClientResult<KitAnnRerankResponse> {
5085 self.kit_ann_rerank_with_options(req, &AiExecutionOptions::default())
5086 }
5087
5088 pub fn kit_ann_rerank_with_options(
5089 &self,
5090 req: &KitAnnRerankRequest,
5091 options: &AiExecutionOptions,
5092 ) -> ClientResult<KitAnnRerankResponse> {
5093 let resp = self
5094 .client
5095 .post(self.url("/kit/ann_rerank"))
5096 .json(&WithAiExecutionOptions {
5097 request: req,
5098 options,
5099 })
5100 .send()?;
5101 decode_blocking_json(self.check(resp)?, MAX_SQL_RESPONSE_BYTES, "Kit ANN rerank")
5102 }
5103
5104 pub fn kit_set_similarity(
5105 &self,
5106 req: &KitSetSimilarityRequest,
5107 ) -> ClientResult<KitSetSimilarityResponse> {
5108 self.kit_set_similarity_with_options(req, &AiExecutionOptions::default())
5109 }
5110
5111 pub fn kit_set_similarity_with_options(
5112 &self,
5113 req: &KitSetSimilarityRequest,
5114 options: &AiExecutionOptions,
5115 ) -> ClientResult<KitSetSimilarityResponse> {
5116 let resp = self
5117 .client
5118 .post(self.url("/kit/set_similarity"))
5119 .json(&WithAiExecutionOptions {
5120 request: req,
5121 options,
5122 })
5123 .send()?;
5124 decode_blocking_json(
5125 self.check(resp)?,
5126 MAX_SQL_RESPONSE_BYTES,
5127 "Kit set similarity",
5128 )
5129 }
5130
5131 pub fn kit_search(&self, req: &KitSearchRequest) -> ClientResult<KitSearchResponse> {
5132 let resp = self.client.post(self.url("/kit/search")).json(req).send()?;
5133 decode_blocking_json(self.check(resp)?, MAX_SQL_RESPONSE_BYTES, "Kit search")
5134 }
5135
5136 pub fn kit_ai_metrics(&self) -> ClientResult<serde_json::Value> {
5137 let resp = self.client.get(self.url("/kit/ai/metrics")).send()?;
5138 decode_blocking_json(
5139 self.check(resp)?,
5140 MAX_CONTROL_RESPONSE_BYTES,
5141 "Kit AI metrics",
5142 )
5143 }
5144
5145 pub fn kit_create_table(&self, body: &serde_json::Value) -> ClientResult<u64> {
5150 let response = self.write_response(
5151 self.client
5152 .post(self.url("/kit/create_table"))
5153 .json(body)
5154 .send(),
5155 "Kit create table",
5156 )?;
5157 let response: TableIdResponse = decode_write_json(response, "Kit create table")?;
5158 exact_required_u64("table_id", response.table_id, &response.table_id_text)
5159 .map_err(|error| write_outcome_unknown("Kit create table", error))
5160 }
5161
5162 pub fn procedures(&self) -> ClientResult<Vec<mongreldb_core::StoredProcedure>> {
5163 let resp = self.client.get(self.url("/procedures")).send()?;
5164 let response: ProceduresResponse = decode_blocking_json(
5165 self.check(resp)?,
5166 MAX_CONTROL_RESPONSE_BYTES,
5167 "procedure list",
5168 )?;
5169 Ok(response.procedures)
5170 }
5171
5172 pub fn procedure(&self, name: &str) -> ClientResult<mongreldb_core::StoredProcedure> {
5173 let resp = self
5174 .client
5175 .get(self.url_segments(&["procedures", name])?)
5176 .send()?;
5177 let response: ProcedureResponse =
5178 decode_blocking_json(self.check(resp)?, MAX_CONTROL_RESPONSE_BYTES, "procedure")?;
5179 Ok(response.procedure)
5180 }
5181
5182 pub fn create_procedure(
5183 &self,
5184 procedure: mongreldb_core::StoredProcedure,
5185 ) -> ClientResult<mongreldb_core::StoredProcedure> {
5186 let expected_name = procedure.name.clone();
5187 let expected_definition = procedure.clone();
5188 let response = self.write_response(
5189 self.client
5190 .post(self.url("/procedures"))
5191 .json(&ProcedureRequest { procedure })
5192 .send(),
5193 "create procedure",
5194 )?;
5195 let response: ProcedureResponse = decode_write_json(response, "create procedure")?;
5196 if response.status != "ok"
5197 || response.procedure.name != expected_name
5198 || response.procedure.version == 0
5199 || response.procedure.created_epoch == 0
5200 || response.procedure.updated_epoch < response.procedure.created_epoch
5201 || response.procedure.checksum.is_empty()
5202 || response.procedure.validate().is_err()
5203 || !procedure_definition_matches(&response.procedure, &expected_definition)
5204 {
5205 return Err(write_outcome_unknown(
5206 "create procedure",
5207 "success response does not match the requested procedure",
5208 ));
5209 }
5210 Ok(response.procedure)
5211 }
5212
5213 pub fn replace_procedure(
5214 &self,
5215 name: &str,
5216 procedure: mongreldb_core::StoredProcedure,
5217 ) -> ClientResult<mongreldb_core::StoredProcedure> {
5218 let expected_definition = procedure.clone();
5219 let response = self.write_response(
5220 self.client
5221 .put(self.url_segments(&["procedures", name])?)
5222 .json(&ProcedureRequest { procedure })
5223 .send(),
5224 "replace procedure",
5225 )?;
5226 let response: ProcedureResponse = decode_write_json(response, "replace procedure")?;
5227 if response.status != "ok"
5228 || response.procedure.name != name
5229 || response.procedure.version == 0
5230 || response.procedure.created_epoch == 0
5231 || response.procedure.updated_epoch < response.procedure.created_epoch
5232 || response.procedure.checksum.is_empty()
5233 || response.procedure.validate().is_err()
5234 || !procedure_definition_matches(&response.procedure, &expected_definition)
5235 {
5236 return Err(write_outcome_unknown(
5237 "replace procedure",
5238 "success response does not match the requested procedure",
5239 ));
5240 }
5241 Ok(response.procedure)
5242 }
5243
5244 pub fn drop_procedure(&self, name: &str) -> ClientResult<()> {
5245 let response = self.write_response(
5246 self.client
5247 .delete(self.url_segments(&["procedures", name])?)
5248 .send(),
5249 "drop procedure",
5250 )?;
5251 let response: CommittedWriteResponse = decode_write_json(response, "drop procedure")?;
5252 validate_committed_write(&response, "drop procedure")?;
5253 Ok(())
5254 }
5255
5256 pub fn call_procedure(
5257 &self,
5258 name: &str,
5259 req: &ProcedureCallRequest,
5260 ) -> ClientResult<ProcedureCallResponse> {
5261 let response = self.write_response(
5262 self.client
5263 .post(self.url_segments(&["procedures", name, "call"])?)
5264 .json(req)
5265 .send(),
5266 "procedure call",
5267 )?;
5268 let response = decode_write_json(response, "procedure call")?;
5269 validate_procedure_call_response(response)
5270 .map_err(|error| write_outcome_unknown("procedure call", error))
5271 }
5272
5273 pub fn kit_call_procedure(
5274 &self,
5275 name: &str,
5276 req: &ProcedureCallRequest,
5277 ) -> ClientResult<ProcedureCallResponse> {
5278 let response = self.write_response(
5279 self.client
5280 .post(self.url_segments(&["kit", "procedures", name, "call"])?)
5281 .json(req)
5282 .send(),
5283 "Kit procedure call",
5284 )?;
5285 let response = decode_write_json(response, "Kit procedure call")?;
5286 validate_procedure_call_response(response)
5287 .map_err(|error| write_outcome_unknown("Kit procedure call", error))
5288 }
5289
5290 pub fn triggers(&self) -> ClientResult<Vec<mongreldb_core::StoredTrigger>> {
5291 let resp = self.client.get(self.url("/triggers")).send()?;
5292 let response: TriggersResponse = decode_blocking_json(
5293 self.check(resp)?,
5294 MAX_CONTROL_RESPONSE_BYTES,
5295 "trigger list",
5296 )?;
5297 Ok(response.triggers)
5298 }
5299
5300 pub fn trigger(&self, name: &str) -> ClientResult<mongreldb_core::StoredTrigger> {
5301 let resp = self
5302 .client
5303 .get(self.url_segments(&["triggers", name])?)
5304 .send()?;
5305 let response: TriggerResponse =
5306 decode_blocking_json(self.check(resp)?, MAX_CONTROL_RESPONSE_BYTES, "trigger")?;
5307 Ok(response.trigger)
5308 }
5309
5310 pub fn create_trigger(
5311 &self,
5312 trigger: mongreldb_core::StoredTrigger,
5313 ) -> ClientResult<mongreldb_core::StoredTrigger> {
5314 self.create_trigger_with_idempotency_key(trigger, None::<String>)
5315 }
5316
5317 pub fn create_trigger_with_idempotency_key(
5318 &self,
5319 trigger: mongreldb_core::StoredTrigger,
5320 idempotency_key: Option<impl Into<String>>,
5321 ) -> ClientResult<mongreldb_core::StoredTrigger> {
5322 let expected_name = trigger.name.clone();
5323 let expected_definition = trigger.clone();
5324 let response = self.write_response(
5325 self.client
5326 .post(self.url("/triggers"))
5327 .json(&TriggerRequest {
5328 trigger,
5329 idempotency_key: idempotency_key.map(Into::into),
5330 })
5331 .send(),
5332 "create trigger",
5333 )?;
5334 let response: TriggerResponse = decode_write_json(response, "create trigger")?;
5335 if response.status.as_deref() != Some("ok")
5336 || response.trigger.name != expected_name
5337 || response.trigger.version == 0
5338 || response.trigger.created_epoch == 0
5339 || response.trigger.updated_epoch < response.trigger.created_epoch
5340 || response.trigger.checksum.is_empty()
5341 || response.trigger.validate().is_err()
5342 || !trigger_definition_matches(&response.trigger, &expected_definition)
5343 {
5344 return Err(write_outcome_unknown(
5345 "create trigger",
5346 "success response does not match the requested trigger",
5347 ));
5348 }
5349 Ok(response.trigger)
5350 }
5351
5352 pub fn replace_trigger(
5353 &self,
5354 name: &str,
5355 trigger: mongreldb_core::StoredTrigger,
5356 ) -> ClientResult<mongreldb_core::StoredTrigger> {
5357 self.replace_trigger_with_idempotency_key(name, trigger, None::<String>)
5358 }
5359
5360 pub fn replace_trigger_with_idempotency_key(
5361 &self,
5362 name: &str,
5363 trigger: mongreldb_core::StoredTrigger,
5364 idempotency_key: Option<impl Into<String>>,
5365 ) -> ClientResult<mongreldb_core::StoredTrigger> {
5366 let expected_definition = trigger.clone();
5367 let response = self.write_response(
5368 self.client
5369 .put(self.url_segments(&["triggers", name])?)
5370 .json(&TriggerRequest {
5371 trigger,
5372 idempotency_key: idempotency_key.map(Into::into),
5373 })
5374 .send(),
5375 "replace trigger",
5376 )?;
5377 let response: TriggerResponse = decode_write_json(response, "replace trigger")?;
5378 if response.status.as_deref() != Some("ok")
5379 || response.trigger.name != name
5380 || response.trigger.version == 0
5381 || response.trigger.created_epoch == 0
5382 || response.trigger.updated_epoch < response.trigger.created_epoch
5383 || response.trigger.checksum.is_empty()
5384 || response.trigger.validate().is_err()
5385 || !trigger_definition_matches(&response.trigger, &expected_definition)
5386 {
5387 return Err(write_outcome_unknown(
5388 "replace trigger",
5389 "success response does not match the requested trigger",
5390 ));
5391 }
5392 Ok(response.trigger)
5393 }
5394
5395 pub fn drop_trigger(&self, name: &str) -> ClientResult<()> {
5396 self.drop_trigger_with_idempotency_key(name, None::<String>)
5397 }
5398
5399 pub fn drop_trigger_with_idempotency_key(
5400 &self,
5401 name: &str,
5402 idempotency_key: Option<impl Into<String>>,
5403 ) -> ClientResult<()> {
5404 let mut request = self.client.delete(self.url_segments(&["triggers", name])?);
5405 if let Some(idempotency_key) = idempotency_key {
5406 request = request.header("Idempotency-Key", idempotency_key.into());
5407 }
5408 let response = self.write_response(request.send(), "drop trigger")?;
5409 let response: TriggerDropResponse = decode_write_json(response, "drop trigger")?;
5410 validate_trigger_drop_response(&response, name)?;
5411 Ok(())
5412 }
5413}
5414
5415impl AsyncMongrelClient {
5416 pub fn builder(url: impl AsRef<str>) -> AsyncMongrelClientBuilder {
5417 let base_url = sanitized_base_url(url.as_ref());
5418 AsyncMongrelClientBuilder {
5419 invalid_base_url: base_url.is_none(),
5420 base_url: base_url.unwrap_or_default(),
5421 authorization: None,
5422 invalid_authorization: false,
5423 connect_timeout: None,
5424 request_timeout: None,
5425 pool_idle_timeout: None,
5426 }
5427 }
5428
5429 pub fn new(url: &str) -> ClientResult<Self> {
5430 Self::builder(url).build()
5431 }
5432
5433 pub fn with_options(url: impl AsRef<str>, options: RemoteOptions) -> ClientResult<Self> {
5434 let mut builder = Self::builder(url);
5435 if let Some(timeout) = options.transport_timeout {
5436 builder = builder.request_timeout(timeout);
5437 }
5438 builder = match options.auth {
5439 Some(RemoteAuth::Bearer(token)) => builder.bearer_token(token.expose_secret()),
5440 Some(RemoteAuth::Basic { username, password }) => {
5441 builder.basic_auth(username, password.expose_secret())
5442 }
5443 None => builder,
5444 };
5445 builder.build()
5446 }
5447
5448 pub fn try_with_bearer_token(mut self, token: impl AsRef<str>) -> ClientResult<Self> {
5449 self.client = async_http_client(Some(bearer_header(token.as_ref())?), self.transport)?;
5450 Ok(self)
5451 }
5452
5453 pub fn with_bearer_token(self, token: impl AsRef<str>) -> ClientResult<Self> {
5454 self.try_with_bearer_token(token)
5455 }
5456
5457 pub fn try_with_basic_auth(
5458 mut self,
5459 username: impl AsRef<str>,
5460 password: impl AsRef<str>,
5461 ) -> ClientResult<Self> {
5462 self.client = async_http_client(
5463 Some(basic_header(username.as_ref(), password.as_ref())?),
5464 self.transport,
5465 )?;
5466 Ok(self)
5467 }
5468
5469 pub fn with_basic_auth(
5470 self,
5471 username: impl AsRef<str>,
5472 password: impl AsRef<str>,
5473 ) -> ClientResult<Self> {
5474 self.try_with_basic_auth(username, password)
5475 }
5476
5477 fn url(&self, path: &str) -> String {
5478 format!("{}{path}", self.base_url)
5479 }
5480
5481 async fn check(&self, response: reqwest::Response) -> ClientResult<reqwest::Response> {
5482 let status = response.status();
5483 if status.is_success() {
5484 return Ok(response);
5485 }
5486 let status_u16 = status.as_u16();
5487 let body = bounded_async_bytes(response, MAX_CONTROL_RESPONSE_BYTES)
5488 .await
5489 .map_err(|error| {
5490 ClientError::Decode(format!("invalid HTTP error response: {error}"))
5491 })?;
5492 Err(decode_http_error(status_u16, &body))
5493 }
5494
5495 async fn check_sql_response(
5496 &self,
5497 response: reqwest::Response,
5498 query_id: mongreldb_query::QueryId,
5499 ) -> ClientResult<reqwest::Response> {
5500 let status = response.status();
5501 let pre_handler_auth = matches!(
5502 status,
5503 reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
5504 );
5505 if status.is_success() {
5506 return Ok(response);
5507 }
5508 let header_error = (!pre_handler_auth)
5509 .then(|| validate_sql_query_id_header(response.headers(), query_id).err())
5510 .flatten();
5511 let status = status.as_u16();
5512 let body = match bounded_async_bytes(response, MAX_CONTROL_RESPONSE_BYTES).await {
5513 Ok(body) => body,
5514 Err(error) => {
5515 if pre_handler_auth {
5516 return Err(ClientError::Http {
5517 status,
5518 body: format!("unreadable authentication error response: {error}"),
5519 });
5520 }
5521 return Err(self
5522 .recover_after_transport_loss(query_id, error.to_string())
5523 .await);
5524 }
5525 };
5526 if pre_handler_auth {
5527 return Err(decode_http_error(status, &body));
5528 }
5529 match decode_sql_http_error(status, &body, query_id) {
5530 Ok(error) if client_error_proves_commit(&error) => Err(error),
5531 Ok(error) if header_error.is_none() => Err(error),
5532 Ok(_) => Err(self
5533 .recover_after_transport_loss(
5534 query_id,
5535 header_error.unwrap_or_else(|| "SQL response identity is invalid".into()),
5536 )
5537 .await),
5538 Err(error) => Err(self.recover_after_transport_loss(query_id, error).await),
5539 }
5540 }
5541
5542 async fn check_query_status_response(
5543 &self,
5544 response: reqwest::Response,
5545 query_id: mongreldb_query::QueryId,
5546 ) -> ClientResult<reqwest::Response> {
5547 let status = response.status();
5548 if status.is_success() {
5549 return Ok(response);
5550 }
5551 let status = status.as_u16();
5552 let body = bounded_async_bytes(response, MAX_CONTROL_RESPONSE_BYTES)
5553 .await
5554 .map_err(ClientError::Decode)?;
5555 if matches!(status, 401 | 403) {
5556 return Err(decode_http_error(status, &body));
5557 }
5558 match decode_sql_http_error(status, &body, query_id) {
5559 Ok(error) => Err(error),
5560 Err(error) => Err(ClientError::Decode(error)),
5561 }
5562 }
5563
5564 pub async fn health(&self) -> ClientResult<String> {
5565 let response = self.client.get(self.url("/health")).send().await?;
5566 let bytes = bounded_async_bytes(self.check(response).await?, MAX_CONTROL_RESPONSE_BYTES)
5567 .await
5568 .map_err(ClientError::Transport)?;
5569 String::from_utf8(bytes)
5570 .map_err(|_| ClientError::Decode("invalid health response: non-UTF-8 body".into()))
5571 }
5572
5573 pub async fn capabilities(&self) -> ClientResult<ServerCapabilities> {
5574 let response = self.client.get(self.url("/capabilities")).send().await?;
5575 decode_async_json(
5576 self.check(response).await?,
5577 MAX_CONTROL_RESPONSE_BYTES,
5578 "capabilities",
5579 )
5580 .await
5581 }
5582
5583 pub async fn sql_cancellation_capabilities(&self) -> ClientResult<SqlCancellationCapabilities> {
5584 let capabilities = self.capabilities().await?.sql_cancellation;
5585 if capabilities.version < 2
5586 || !capabilities.client_query_ids
5587 || !capabilities.cancel_endpoint
5588 || !capabilities.query_status
5589 || !capabilities.pre_registration_cancel
5590 {
5591 return Err(capability_unsupported(
5592 "server does not support SQL cancellation capability version 2",
5593 ));
5594 }
5595 Ok(capabilities)
5596 }
5597
5598 pub async fn sql_idempotency_capabilities(&self) -> ClientResult<SqlIdempotencyCapabilities> {
5599 let capabilities =
5600 self.capabilities().await?.sql_idempotency.ok_or_else(|| {
5601 capability_unsupported("server does not advertise SQL idempotency")
5602 })?;
5603 if capabilities.version < 1
5604 || !capabilities.durable_pre_execution_intent
5605 || !capabilities.replay_committed_receipt
5606 || !capabilities.indeterminate_never_reexecutes
5607 {
5608 return Err(capability_unsupported(
5609 "server does not support safe SQL idempotency capability version 1",
5610 ));
5611 }
5612 Ok(capabilities)
5613 }
5614
5615 pub async fn sql_pagination_capabilities(&self) -> ClientResult<SqlPaginationCapabilities> {
5616 let capabilities =
5617 self.capabilities().await?.sql_pagination.ok_or_else(|| {
5618 capability_unsupported("server does not advertise SQL pagination")
5619 })?;
5620 if capabilities.version < 1
5621 || capabilities.continuation_endpoint != "/sql/continue"
5622 || !capabilities.retained_snapshot
5623 || !capabilities.projection_required
5624 || !capabilities.byte_and_token_hints
5625 {
5626 return Err(capability_unsupported(
5627 "server does not support SQL pagination capability version 1",
5628 ));
5629 }
5630 Ok(capabilities)
5631 }
5632
5633 pub async fn sql(&self, sql: &str) -> ClientResult<Vec<RecordBatch>> {
5634 self.sql_with_options(sql, SqlClientOptions::default())
5635 .await
5636 }
5637
5638 pub async fn sql_with_options(
5639 &self,
5640 sql: &str,
5641 mut options: SqlClientOptions,
5642 ) -> ClientResult<Vec<RecordBatch>> {
5643 if options.query_id.is_some() || options.timeout.is_some() {
5644 self.sql_cancellation_capabilities().await?;
5645 }
5646 let query_id = match options.query_id.take() {
5647 Some(query_id) => query_id,
5648 None => mongreldb_query::QueryId::random()
5649 .map_err(|error| ClientError::Transport(error.to_string()))?,
5650 };
5651 let timeout_ms = options
5652 .timeout
5653 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
5654 let response = self
5655 .client
5656 .post(self.url("/sql"))
5657 .json(&SqlReq {
5658 sql: sql.to_string(),
5659 format: Some("arrow"),
5660 query_id,
5661 timeout_ms,
5662 max_output_rows: None,
5663 max_output_bytes: None,
5664 idempotency_key: None,
5665 pagination: None,
5666 })
5667 .send()
5668 .await;
5669 let response = match response {
5670 Ok(response) => response,
5671 Err(error) => {
5672 return Err(self
5673 .recover_after_transport_loss(query_id, error.to_string())
5674 .await);
5675 }
5676 };
5677 let response = self.check_sql_response(response, query_id).await?;
5678 if let Err(error) = validate_sql_query_id_header(response.headers(), query_id) {
5679 return Err(self.recover_after_transport_loss(query_id, error).await);
5680 }
5681 let bytes = match bounded_async_bytes(response, MAX_SQL_RESPONSE_BYTES).await {
5682 Ok(bytes) => bytes,
5683 Err(error) => {
5684 return Err(self
5685 .recover_after_transport_loss(query_id, error.to_string())
5686 .await)
5687 }
5688 };
5689 match read_arrow_ipc(&bytes) {
5690 Ok(batches) => Ok(batches),
5691 Err(error) => Err(self
5692 .recover_after_transport_loss(query_id, error.to_string())
5693 .await),
5694 }
5695 }
5696
5697 pub async fn sql_write_idempotent(
5698 &self,
5699 sql: &str,
5700 idempotency_key: impl Into<String>,
5701 ) -> ClientResult<RemoteSqlReceipt> {
5702 self.sql_write_idempotent_with_options(sql, idempotency_key, SqlClientOptions::default())
5703 .await
5704 }
5705
5706 pub async fn sql_write_idempotent_with_options(
5707 &self,
5708 sql: &str,
5709 idempotency_key: impl Into<String>,
5710 mut options: SqlClientOptions,
5711 ) -> ClientResult<RemoteSqlReceipt> {
5712 let idempotency_key = idempotency_key.into();
5713 if idempotency_key.is_empty() || idempotency_key.len() > 256 {
5714 return Err(ClientError::Decode(
5715 "SQL idempotency key must contain 1 to 256 bytes".into(),
5716 ));
5717 }
5718 self.sql_idempotency_capabilities().await?;
5719 let query_id = match options.query_id.take() {
5720 Some(query_id) => query_id,
5721 None => mongreldb_query::QueryId::random()
5722 .map_err(|error| ClientError::Transport(error.to_string()))?,
5723 };
5724 let result = self
5725 .sql_write_idempotent_once(sql, &idempotency_key, &options, query_id, None)
5726 .await;
5727 if result.as_ref().is_err_and(|error| error.replay) {
5728 self.sql_idempotency_capabilities().await?;
5729 return self
5730 .sql_write_idempotent_once(
5731 sql,
5732 &idempotency_key,
5733 &options,
5734 fresh_query_id(query_id)?,
5735 Some(query_id),
5736 )
5737 .await
5738 .map_err(|error| error.error);
5739 }
5740 result.map_err(|error| error.error)
5741 }
5742
5743 async fn sql_write_idempotent_once(
5744 &self,
5745 sql: &str,
5746 idempotency_key: &str,
5747 options: &SqlClientOptions,
5748 query_id: mongreldb_query::QueryId,
5749 expected_original_query_id: Option<mongreldb_query::QueryId>,
5750 ) -> Result<RemoteSqlReceipt, IdempotentAttemptError> {
5751 let timeout_ms = options
5752 .timeout
5753 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
5754 let response = self
5755 .client
5756 .post(self.url("/sql"))
5757 .json(&SqlReq {
5758 sql: sql.to_owned(),
5759 format: None,
5760 query_id,
5761 timeout_ms,
5762 max_output_rows: None,
5763 max_output_bytes: None,
5764 idempotency_key: Some(idempotency_key.to_owned()),
5765 pagination: None,
5766 })
5767 .send()
5768 .await;
5769 let response = match response {
5770 Ok(response) => response,
5771 Err(error) => {
5772 return Err(self
5773 .idempotent_attempt_loss(query_id, error.to_string())
5774 .await);
5775 }
5776 };
5777 let response = self
5778 .check_sql_response(response, query_id)
5779 .await
5780 .map_err(IdempotentAttemptError::final_error)?;
5781 let header_error = validate_sql_query_id_header(response.headers(), query_id).err();
5782 let bytes = match bounded_async_bytes(response, MAX_CONTROL_RESPONSE_BYTES).await {
5783 Ok(bytes) => bytes,
5784 Err(error) => return Err(self.idempotent_attempt_loss(query_id, error).await),
5785 };
5786 match decode_remote_sql_receipt(&bytes, query_id, expected_original_query_id) {
5787 Ok(receipt) if header_error.is_none() => Ok(receipt),
5788 Ok(_) => {
5789 let proof = strict_json_value(&bytes).ok().and_then(|value| {
5790 sql_receipt_commit_proof(&value, query_id, expected_original_query_id)
5791 });
5792 match proof {
5793 Some(proof) => Err(IdempotentAttemptError::final_error(
5794 committed_sql_receipt_decode_error(
5795 query_id,
5796 proof,
5797 header_error
5798 .clone()
5799 .unwrap_or_else(|| "SQL response identity is invalid".into()),
5800 ),
5801 )),
5802 None => Err(self
5803 .idempotent_attempt_loss(
5804 query_id,
5805 header_error
5806 .unwrap_or_else(|| "SQL response identity is invalid".into()),
5807 )
5808 .await),
5809 }
5810 }
5811 Err(SqlReceiptDecodeError::KnownCommit(error)) => {
5812 Err(IdempotentAttemptError::final_error(error))
5813 }
5814 Err(SqlReceiptDecodeError::Unknown(error)) => {
5815 Err(self.idempotent_attempt_loss(query_id, error).await)
5816 }
5817 }
5818 }
5819
5820 async fn idempotent_attempt_loss(
5821 &self,
5822 query_id: mongreldb_query::QueryId,
5823 message: String,
5824 ) -> IdempotentAttemptError {
5825 let missing = match self
5826 .client
5827 .get(self.url(&format!("/queries/{query_id}")))
5828 .timeout(SQL_RECOVERY_REQUEST_TIMEOUT)
5829 .send()
5830 .await
5831 {
5832 Ok(response) if response.status() == reqwest::StatusCode::NOT_FOUND => {
5833 bounded_async_bytes(response, MAX_CONTROL_RESPONSE_BYTES)
5834 .await
5835 .ok()
5836 .is_some_and(|body| is_exact_query_not_found_response(&body, query_id))
5837 }
5838 _ => false,
5839 };
5840 if missing {
5841 return IdempotentAttemptError {
5842 error: ClientError::QueryOutcomeUnknown {
5843 query_id: query_id.to_string(),
5844 message,
5845 status: None,
5846 cancel_outcome: None,
5847 },
5848 replay: true,
5849 };
5850 }
5851 IdempotentAttemptError::final_error(
5852 self.recover_after_transport_loss(query_id, message).await,
5853 )
5854 }
5855
5856 pub async fn sql_page(
5857 &self,
5858 sql: &str,
5859 mut options: SqlPageOptions,
5860 ) -> ClientResult<RemoteSqlPage> {
5861 validate_sql_page_options(&options)?;
5862 self.sql_pagination_capabilities().await?;
5863 let query_id = match options.query_id.take() {
5864 Some(query_id) => query_id,
5865 None => mongreldb_query::QueryId::random()
5866 .map_err(|error| ClientError::Transport(error.to_string()))?,
5867 };
5868 let timeout_ms = options
5869 .timeout
5870 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
5871 let response = self
5872 .client
5873 .post(self.url("/sql"))
5874 .json(&SqlReq {
5875 sql: sql.to_owned(),
5876 format: None,
5877 query_id,
5878 timeout_ms,
5879 max_output_rows: options.max_output_rows,
5880 max_output_bytes: options.max_output_bytes,
5881 idempotency_key: None,
5882 pagination: Some(SqlPaginationReq {
5883 page_size_rows: options.page_size_rows,
5884 projection: options.projection.clone(),
5885 max_page_bytes: options.max_page_bytes,
5886 max_page_tokens: options.max_page_tokens,
5887 }),
5888 })
5889 .send()
5890 .await;
5891 let response = match response {
5892 Ok(response) => response,
5893 Err(error) => {
5894 return Err(self
5895 .recover_after_transport_loss(query_id, error.to_string())
5896 .await);
5897 }
5898 };
5899 let response = self.check_sql_response(response, query_id).await?;
5900 if let Err(error) = validate_sql_query_id_header(response.headers(), query_id) {
5901 return Err(self.recover_after_transport_loss(query_id, error).await);
5902 }
5903 let page = bounded_async_bytes(response, MAX_SQL_RESPONSE_BYTES)
5904 .await
5905 .and_then(|bytes| {
5906 strict_json::<RemoteSqlPage>(&bytes, "SQL page").map_err(|error| error.to_string())
5907 })
5908 .and_then(|page| validate_remote_sql_page(page, Some(&options)));
5909 match page {
5910 Ok(page) => Ok(page),
5911 Err(error) => Err(self.recover_after_transport_loss(query_id, error).await),
5912 }
5913 }
5914
5915 pub async fn continue_sql_page(
5916 &self,
5917 cursor: &str,
5918 mut options: RemoteSqlControlOptions,
5919 ) -> ClientResult<RemoteSqlPage> {
5920 if cursor.is_empty() || cursor.len() > 2_048 {
5921 return Err(ClientError::Decode(
5922 "SQL continuation cursor must contain 1 to 2048 bytes".into(),
5923 ));
5924 }
5925 self.sql_pagination_capabilities().await?;
5926 let query_id = match options.query_id.take() {
5927 Some(query_id) => query_id,
5928 None => mongreldb_query::QueryId::random()
5929 .map_err(|error| ClientError::Transport(error.to_string()))?,
5930 };
5931 if options.timeout == Some(std::time::Duration::ZERO) {
5932 return Err(ClientError::Decode("timeout must be positive".into()));
5933 }
5934 let timeout_ms = options
5935 .timeout
5936 .map(|timeout| timeout.as_millis().min(u128::from(u64::MAX)) as u64);
5937 let response = self
5938 .client
5939 .post(self.url("/sql/continue"))
5940 .json(&serde_json::json!({
5941 "cursor": cursor,
5942 "operation_id": query_id,
5943 "timeout_ms": timeout_ms,
5944 }))
5945 .send()
5946 .await
5947 .map_err(|error| ClientError::Transport(error.to_string()))?;
5948 let response = self.check_sql_response(response, query_id).await?;
5949 validate_sql_query_id_header(response.headers(), query_id)
5950 .map_err(|error| client_serialization_error(Some(query_id), error))?;
5951 bounded_async_bytes(response, MAX_SQL_RESPONSE_BYTES)
5952 .await
5953 .and_then(|bytes| {
5954 strict_json::<RemoteSqlPage>(&bytes, "SQL page").map_err(|error| error.to_string())
5955 })
5956 .and_then(|page| validate_remote_sql_page(page, None))
5957 .map_err(|error| client_serialization_error(Some(query_id), error))
5958 }
5959
5960 pub async fn cancel_sql(
5961 &self,
5962 query_id: mongreldb_query::QueryId,
5963 ) -> ClientResult<RemoteCancelOutcome> {
5964 self.sql_cancellation_capabilities().await?;
5965 let response = self
5966 .client
5967 .post(self.url(&format!("/queries/{query_id}/cancel")))
5968 .send()
5969 .await?;
5970 let status = response.status();
5971 if !matches!(
5972 status,
5973 reqwest::StatusCode::OK
5974 | reqwest::StatusCode::ACCEPTED
5975 | reqwest::StatusCode::CONFLICT
5976 | reqwest::StatusCode::NOT_FOUND
5977 ) {
5978 return match self.check(response).await {
5979 Err(error) => Err(error),
5980 Ok(_) => Err(ClientError::Decode(
5981 "unexpected successful cancellation response".into(),
5982 )),
5983 };
5984 }
5985 let body: serde_json::Value =
5986 decode_async_json(response, MAX_CONTROL_RESPONSE_BYTES, "cancellation").await?;
5987 decode_cancel_outcome(&body, query_id, status.as_u16())
5988 }
5989
5990 pub async fn query_status(
5991 &self,
5992 query_id: mongreldb_query::QueryId,
5993 ) -> ClientResult<RemoteQueryStatus> {
5994 self.sql_cancellation_capabilities().await?;
5995 let response = self
5996 .client
5997 .get(self.url(&format!("/queries/{query_id}")))
5998 .send()
5999 .await?;
6000 let status = decode_async_json(
6001 self.check_query_status_response(response, query_id).await?,
6002 MAX_CONTROL_RESPONSE_BYTES,
6003 "query status",
6004 )
6005 .await?;
6006 validate_remote_query_status(status, query_id).map_err(ClientError::Decode)
6007 }
6008
6009 async fn query_status_optional(
6010 &self,
6011 query_id: mongreldb_query::QueryId,
6012 timeout: std::time::Duration,
6013 ) -> Option<RemoteQueryStatus> {
6014 let response = self
6015 .client
6016 .get(self.url(&format!("/queries/{query_id}")))
6017 .timeout(timeout)
6018 .send()
6019 .await
6020 .ok()?;
6021 if !response.status().is_success() {
6022 return None;
6023 }
6024 let status = decode_async_json(response, MAX_CONTROL_RESPONSE_BYTES, "query status")
6025 .await
6026 .ok()?;
6027 validate_remote_query_status(status, query_id).ok()
6028 }
6029
6030 async fn cancel_sql_for_recovery(
6031 &self,
6032 query_id: mongreldb_query::QueryId,
6033 timeout: std::time::Duration,
6034 ) -> Option<RemoteCancelOutcome> {
6035 let response = self
6036 .client
6037 .post(self.url(&format!("/queries/{query_id}/cancel")))
6038 .timeout(timeout)
6039 .send()
6040 .await
6041 .ok()?;
6042 let status = response.status();
6043 if !matches!(
6044 status,
6045 reqwest::StatusCode::OK
6046 | reqwest::StatusCode::ACCEPTED
6047 | reqwest::StatusCode::CONFLICT
6048 | reqwest::StatusCode::NOT_FOUND
6049 ) {
6050 return None;
6051 }
6052 let body = decode_async_json(response, MAX_CONTROL_RESPONSE_BYTES, "cancellation")
6053 .await
6054 .ok()?;
6055 decode_cancel_outcome(&body, query_id, status.as_u16()).ok()
6056 }
6057
6058 async fn recover_after_transport_loss(
6059 &self,
6060 query_id: mongreldb_query::QueryId,
6061 message: String,
6062 ) -> ClientError {
6063 let deadline = tokio::time::Instant::now() + SQL_RECOVERY_WINDOW;
6064 let mut status = self
6065 .query_status_optional(query_id, SQL_RECOVERY_REQUEST_TIMEOUT)
6066 .await;
6067 if let Some(decisive) = status
6068 .as_ref()
6069 .filter(|status| recovery_status_is_decisive(status))
6070 {
6071 return recovered_query_error(decisive.clone(), message);
6072 }
6073 let cancel_outcome = self
6074 .cancel_sql_for_recovery(
6075 query_id,
6076 deadline
6077 .saturating_duration_since(tokio::time::Instant::now())
6078 .min(SQL_RECOVERY_REQUEST_TIMEOUT),
6079 )
6080 .await;
6081 if status.is_none() && cancel_outcome == Some(RemoteCancelOutcome::NotFound) {
6082 return ClientError::QueryOutcomeUnknown {
6083 query_id: query_id.to_string(),
6084 message,
6085 status: None,
6086 cancel_outcome,
6087 };
6088 }
6089 while !status.as_ref().is_some_and(recovery_status_is_decisive)
6090 && tokio::time::Instant::now() < deadline
6091 {
6092 tokio::time::sleep(
6093 deadline
6094 .saturating_duration_since(tokio::time::Instant::now())
6095 .min(SQL_RECOVERY_POLL_INTERVAL),
6096 )
6097 .await;
6098 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
6099 if remaining.is_zero() {
6100 break;
6101 }
6102 status = self
6103 .query_status_optional(query_id, remaining.min(SQL_RECOVERY_REQUEST_TIMEOUT))
6104 .await
6105 .or(status);
6106 }
6107 if let Some(decisive) = status
6108 .as_ref()
6109 .filter(|status| recovery_status_is_decisive(status))
6110 {
6111 return recovered_query_error(decisive.clone(), message);
6112 }
6113 ClientError::QueryOutcomeUnknown {
6114 query_id: query_id.to_string(),
6115 message,
6116 status: status.map(Box::new),
6117 cancel_outcome,
6118 }
6119 }
6120
6121 pub async fn kit_schema(&self, table: &str) -> ClientResult<TableSchemaInfo> {
6123 let response = self
6124 .client
6125 .get(url_with_segments(
6126 &self.base_url,
6127 &["kit", "schema", table],
6128 )?)
6129 .send()
6130 .await?;
6131 decode_async_json(
6132 self.check(response).await?,
6133 MAX_CONTROL_RESPONSE_BYTES,
6134 "Kit schema",
6135 )
6136 .await
6137 }
6138
6139 pub async fn kit_txn(&self, req: &KitTxnRequest) -> ClientResult<KitTxnResponse> {
6141 let response = self
6142 .client
6143 .post(self.url("/kit/txn"))
6144 .json(req)
6145 .send()
6146 .await
6147 .map_err(|error| {
6148 kit_txn_outcome_unknown(format!(
6149 "Kit transaction transport failed before outcome confirmation: {error}"
6150 ))
6151 })?;
6152 if !response.status().is_success() {
6153 let status = response.status().as_u16();
6154 if matches!(status, 401 | 403) {
6155 return Err(kit_txn_auth_error(status));
6156 }
6157 let body = bounded_async_bytes(response, MAX_SQL_RESPONSE_BYTES)
6158 .await
6159 .map_err(|error| {
6160 kit_txn_outcome_unknown(format!(
6161 "Kit transaction error response could not be read: {error}"
6162 ))
6163 })?;
6164 return Err(decode_kit_txn_http_error(status, &body, req));
6165 }
6166 let status = response.status().as_u16();
6167 let body = bounded_async_bytes(response, MAX_SQL_RESPONSE_BYTES)
6168 .await
6169 .map_err(|error| {
6170 kit_txn_outcome_unknown(format!(
6171 "Kit transaction success response could not be read: {error}"
6172 ))
6173 })?;
6174 decode_kit_txn_success(&body, req, status)
6175 }
6176
6177 pub async fn kit_query(&self, req: &KitQueryRequest) -> ClientResult<KitQueryResponse> {
6179 let response = self
6180 .client
6181 .post(self.url("/kit/query"))
6182 .json(req)
6183 .send()
6184 .await?;
6185 let response = decode_async_json(
6186 self.check(response).await?,
6187 MAX_SQL_RESPONSE_BYTES,
6188 "Kit query",
6189 )
6190 .await?;
6191 validate_kit_query_response(response, req)
6192 }
6193
6194 pub async fn kit_retrieve(
6195 &self,
6196 req: &KitRetrieveRequest,
6197 ) -> ClientResult<KitRetrieveResponse> {
6198 self.kit_retrieve_with_options(req, &AiExecutionOptions::default())
6199 .await
6200 }
6201
6202 pub async fn kit_retrieve_with_options(
6203 &self,
6204 req: &KitRetrieveRequest,
6205 options: &AiExecutionOptions,
6206 ) -> ClientResult<KitRetrieveResponse> {
6207 let response = self
6208 .client
6209 .post(self.url("/kit/retrieve"))
6210 .json(&WithAiExecutionOptions {
6211 request: req,
6212 options,
6213 })
6214 .send()
6215 .await?;
6216 decode_async_json(
6217 self.check(response).await?,
6218 MAX_SQL_RESPONSE_BYTES,
6219 "Kit retrieve",
6220 )
6221 .await
6222 }
6223
6224 pub async fn kit_retrieve_text(
6225 &self,
6226 req: &KitRetrieveTextRequest,
6227 ) -> ClientResult<KitRetrieveTextResponse> {
6228 self.kit_retrieve_text_with_options(req, &AiExecutionOptions::default())
6229 .await
6230 }
6231
6232 pub async fn kit_retrieve_text_with_options(
6233 &self,
6234 req: &KitRetrieveTextRequest,
6235 options: &AiExecutionOptions,
6236 ) -> ClientResult<KitRetrieveTextResponse> {
6237 let response = self
6238 .client
6239 .post(self.url("/kit/retrieve_text"))
6240 .json(&WithAiExecutionOptions {
6241 request: req,
6242 options,
6243 })
6244 .send()
6245 .await?;
6246 decode_async_json(
6247 self.check(response).await?,
6248 MAX_SQL_RESPONSE_BYTES,
6249 "Kit retrieve_text",
6250 )
6251 .await
6252 }
6253
6254 pub async fn kit_ann_rerank(
6255 &self,
6256 req: &KitAnnRerankRequest,
6257 ) -> ClientResult<KitAnnRerankResponse> {
6258 self.kit_ann_rerank_with_options(req, &AiExecutionOptions::default())
6259 .await
6260 }
6261
6262 pub async fn kit_ann_rerank_with_options(
6263 &self,
6264 req: &KitAnnRerankRequest,
6265 options: &AiExecutionOptions,
6266 ) -> ClientResult<KitAnnRerankResponse> {
6267 let response = self
6268 .client
6269 .post(self.url("/kit/ann_rerank"))
6270 .json(&WithAiExecutionOptions {
6271 request: req,
6272 options,
6273 })
6274 .send()
6275 .await?;
6276 decode_async_json(
6277 self.check(response).await?,
6278 MAX_SQL_RESPONSE_BYTES,
6279 "Kit ANN rerank",
6280 )
6281 .await
6282 }
6283
6284 pub async fn kit_set_similarity(
6285 &self,
6286 req: &KitSetSimilarityRequest,
6287 ) -> ClientResult<KitSetSimilarityResponse> {
6288 self.kit_set_similarity_with_options(req, &AiExecutionOptions::default())
6289 .await
6290 }
6291
6292 pub async fn kit_set_similarity_with_options(
6293 &self,
6294 req: &KitSetSimilarityRequest,
6295 options: &AiExecutionOptions,
6296 ) -> ClientResult<KitSetSimilarityResponse> {
6297 let response = self
6298 .client
6299 .post(self.url("/kit/set_similarity"))
6300 .json(&WithAiExecutionOptions {
6301 request: req,
6302 options,
6303 })
6304 .send()
6305 .await?;
6306 decode_async_json(
6307 self.check(response).await?,
6308 MAX_SQL_RESPONSE_BYTES,
6309 "Kit set similarity",
6310 )
6311 .await
6312 }
6313
6314 pub async fn kit_search(&self, req: &KitSearchRequest) -> ClientResult<KitSearchResponse> {
6315 let response = self
6316 .client
6317 .post(self.url("/kit/search"))
6318 .json(req)
6319 .send()
6320 .await?;
6321 decode_async_json(
6322 self.check(response).await?,
6323 MAX_SQL_RESPONSE_BYTES,
6324 "Kit search",
6325 )
6326 .await
6327 }
6328
6329 pub async fn kit_ai_metrics(&self) -> ClientResult<serde_json::Value> {
6330 let response = self.client.get(self.url("/kit/ai/metrics")).send().await?;
6331 decode_async_json(
6332 self.check(response).await?,
6333 MAX_CONTROL_RESPONSE_BYTES,
6334 "Kit AI metrics",
6335 )
6336 .await
6337 }
6338}
6339
6340#[derive(Serialize, Clone)]
6341pub struct ColumnDefJson {
6342 pub id: u16,
6343 pub name: String,
6344 pub ty: String,
6345 pub primary_key: bool,
6346}
6347
6348#[derive(Serialize, Clone)]
6349pub struct TxnOp {
6350 pub table: String,
6351 pub op: String,
6352 #[serde(skip_serializing_if = "Option::is_none")]
6353 pub cells: Option<Vec<serde_json::Value>>,
6354 #[serde(skip_serializing_if = "Option::is_none")]
6355 pub row_id: Option<u64>,
6356}
6357
6358fn value_to_json(value: &mongreldb_core::Value) -> ClientResult<serde_json::Value> {
6359 use mongreldb_core::Value;
6360
6361 Ok(match value {
6362 Value::Null => serde_json::Value::Null,
6363 Value::Bool(value) => serde_json::Value::Bool(*value),
6364 Value::Int64(value) => serde_json::Value::Number((*value).into()),
6365 Value::Float64(value) => serde_json::Number::from_f64(*value)
6366 .map(serde_json::Value::Number)
6367 .ok_or_else(|| ClientError::Decode("legacy put rejects non-finite floats".into()))?,
6368 Value::Bytes(value) => tagged_hex_value("bytes", value),
6369 Value::Embedding(values) => serde_json::Value::Array(
6370 values
6371 .iter()
6372 .map(|value| {
6373 serde_json::Number::from_f64(f64::from(*value))
6374 .map(serde_json::Value::Number)
6375 .ok_or_else(|| {
6376 ClientError::Decode(
6377 "legacy put rejects non-finite embedding values".into(),
6378 )
6379 })
6380 })
6381 .collect::<ClientResult<Vec<_>>>()?,
6382 ),
6383 Value::GeneratedEmbedding(value) => serde_json::Value::Array(
6384 value
6385 .vector
6386 .iter()
6387 .map(|value| {
6388 serde_json::Number::from_f64(f64::from(*value))
6389 .map(serde_json::Value::Number)
6390 .ok_or_else(|| {
6391 ClientError::Decode(
6392 "legacy put rejects non-finite embedding values".into(),
6393 )
6394 })
6395 })
6396 .collect::<ClientResult<Vec<_>>>()?,
6397 ),
6398 Value::Decimal(value) => serde_json::json!({
6399 "$mongreldb_type": "decimal",
6400 "unscaled": value.to_string(),
6401 }),
6402 Value::Interval {
6403 months,
6404 days,
6405 nanos,
6406 } => serde_json::json!({
6407 "$mongreldb_type": "interval",
6408 "months": months.to_string(),
6409 "days": days.to_string(),
6410 "nanos": nanos.to_string(),
6411 }),
6412 Value::Uuid(value) => tagged_hex_value("uuid", value),
6413 Value::Json(value) => {
6414 std::str::from_utf8(value)
6415 .map_err(|_| ClientError::Decode("legacy put JSON is not UTF-8".into()))?;
6416 serde_json::from_slice::<serde_json::Value>(value).map_err(|error| {
6417 ClientError::Decode(format!("legacy put JSON is invalid: {error}"))
6418 })?;
6419 tagged_hex_value("json", value)
6420 }
6421 })
6422}
6423
6424fn tagged_hex_value(kind: &str, bytes: &[u8]) -> serde_json::Value {
6425 serde_json::json!({
6426 "$mongreldb_type": kind,
6427 "hex": hex_bytes(bytes),
6428 })
6429}
6430
6431fn hex_bytes(bytes: &[u8]) -> String {
6432 const HEX: &[u8; 16] = b"0123456789abcdef";
6433 let mut encoded = String::with_capacity(bytes.len() * 2);
6434 for byte in bytes {
6435 encoded.push(HEX[(byte >> 4) as usize] as char);
6436 encoded.push(HEX[(byte & 0x0f) as usize] as char);
6437 }
6438 encoded
6439}
6440
6441fn read_arrow_ipc(bytes: &[u8]) -> ClientResult<Vec<RecordBatch>> {
6442 if bytes.is_empty() {
6443 return Ok(Vec::new());
6444 }
6445 let cursor = Cursor::new(bytes);
6446 let reader = FileReader::try_new(cursor, None)
6447 .map_err(|e| ClientError::Decode(format!("arrow ipc: {e}")))?;
6448 reader
6449 .into_iter()
6450 .collect::<std::result::Result<Vec<_>, _>>()
6451 .map_err(|e| ClientError::Decode(format!("arrow read: {e}")))
6452}
6453
6454pub struct ReplicationFollower {
6466 leader_url: String,
6467 local_path: std::path::PathBuf,
6468 client: reqwest::blocking::Client,
6469 last_epoch: u64,
6470 bearer_token: Option<SecretString>,
6471 basic_auth: Option<(String, SecretString)>,
6472 local_passphrase: Option<SecretString>,
6473 local_credentials: Option<(String, SecretString)>,
6474}
6475
6476impl std::fmt::Debug for ReplicationFollower {
6477 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6478 formatter
6479 .debug_struct("ReplicationFollower")
6480 .field("leader_url", &self.leader_url)
6481 .field("local_path", &self.local_path)
6482 .field("last_epoch", &self.last_epoch)
6483 .field(
6484 "bearer_token",
6485 &self.bearer_token.as_ref().map(|_| "[REDACTED]"),
6486 )
6487 .field(
6488 "basic_auth",
6489 &self
6490 .basic_auth
6491 .as_ref()
6492 .map(|(username, _)| (username, "[REDACTED]")),
6493 )
6494 .field(
6495 "local_passphrase",
6496 &self.local_passphrase.as_ref().map(|_| "[REDACTED]"),
6497 )
6498 .field(
6499 "local_credentials",
6500 &self
6501 .local_credentials
6502 .as_ref()
6503 .map(|(username, _)| (username, "[REDACTED]")),
6504 )
6505 .finish()
6506 }
6507}
6508
6509impl ReplicationFollower {
6510 pub fn new(leader_url: &str, local_path: impl AsRef<std::path::Path>) -> ClientResult<Self> {
6513 let leader_url = sanitized_base_url(leader_url).ok_or_else(|| {
6514 ClientError::Transport(
6515 "invalid leader URL: expected http(s) URL without credentials, query, or fragment"
6516 .into(),
6517 )
6518 })?;
6519 let local_path = local_path.as_ref().to_path_buf();
6520 let last_epoch = mongreldb_core::replica_epoch(&local_path).unwrap_or(0);
6521 Ok(Self {
6522 leader_url,
6523 local_path,
6524 client: reqwest::blocking::Client::new(),
6525 last_epoch,
6526 bearer_token: None,
6527 basic_auth: None,
6528 local_passphrase: None,
6529 local_credentials: None,
6530 })
6531 }
6532
6533 pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
6534 self.bearer_token = Some(token.into().into());
6535 self
6536 }
6537
6538 pub fn with_basic_auth(
6539 mut self,
6540 username: impl Into<String>,
6541 password: impl Into<String>,
6542 ) -> Self {
6543 self.basic_auth = Some((username.into(), password.into().into()));
6544 self
6545 }
6546
6547 pub fn with_local_encryption_passphrase(mut self, passphrase: impl Into<String>) -> Self {
6548 self.local_passphrase = Some(passphrase.into().into());
6549 self
6550 }
6551
6552 pub fn with_local_credentials(
6553 mut self,
6554 username: impl Into<String>,
6555 password: impl Into<String>,
6556 ) -> Self {
6557 self.local_credentials = Some((username.into(), password.into().into()));
6558 self
6559 }
6560
6561 pub fn sync(&mut self) -> Result<usize, String> {
6564 let legacy_replica_needs_snapshot = mongreldb_core::is_replica(&self.local_path)
6565 && !self.local_path.join("_meta/replication_source_id").exists();
6566 if !self.local_path.join("CATALOG").exists() || legacy_replica_needs_snapshot {
6567 self.ensure_bootstrap_destination_is_owned_or_empty()?;
6568 self.bootstrap()?;
6569 } else if !mongreldb_core::is_replica(&self.local_path) {
6570 return Err(format!(
6571 "refusing to overwrite non-replica database at {}",
6572 self.local_path.display()
6573 ));
6574 }
6575
6576 let mut resp = self.fetch_wal()?;
6577 if resp.status() == reqwest::StatusCode::CONFLICT {
6578 self.validate_snapshot_required_response(resp)?;
6579 self.bootstrap()?;
6580 resp = self.fetch_wal()?;
6581 }
6582 if !resp.status().is_success() {
6583 return Err(format!("leader returned {}", resp.status()));
6584 }
6585 let from_epoch = replication_u64_header(&resp, "x-mongreldb-from-epoch")?;
6586 let leader_epoch = replication_u64_header(&resp, "x-mongreldb-current-epoch")?;
6587 let source_id = replication_digest_header(&resp, "x-mongreldb-source-id")?;
6588 let commit_count = replication_u64_header(&resp, "x-mongreldb-commit-count")?;
6589 let records_sha256 = replication_digest_header(&resp, "x-mongreldb-records-sha256")?;
6590 let earliest_epoch = resp
6591 .headers()
6592 .get("x-mongreldb-earliest-epoch")
6593 .map(|value| {
6594 value
6595 .to_str()
6596 .map_err(|error| format!("invalid x-mongreldb-earliest-epoch: {error}"))?
6597 .parse::<u64>()
6598 .map_err(|error| format!("invalid x-mongreldb-earliest-epoch: {error}"))
6599 })
6600 .transpose()?;
6601 let body = bounded_blocking_bytes(resp, MAX_REPLICATION_WAL_RESPONSE_BYTES)
6602 .map_err(|error| format!("failed to read WAL response: {error}"))?;
6603 let body = std::str::from_utf8(&body)
6604 .map_err(|_| "invalid WAL response: body is not UTF-8".to_owned())?;
6605 let mut records = Vec::new();
6606 for line in body.lines() {
6607 if line.trim().is_empty() {
6608 continue;
6609 }
6610 records.push(
6611 strict_roundtrip_json::<mongreldb_core::wal::Record>(
6612 line.as_bytes(),
6613 "WAL record from leader",
6614 )
6615 .map_err(|error| error.to_string())?,
6616 );
6617 }
6618 let record_count = records.len();
6619 let batch = mongreldb_core::ReplicationBatch::from_wire(
6620 source_id,
6621 from_epoch,
6622 leader_epoch,
6623 earliest_epoch,
6624 commit_count,
6625 records_sha256,
6626 records,
6627 );
6628
6629 let local = self.open_local()?;
6630 let applied_epoch = local
6631 .append_replication_batch(&batch)
6632 .map_err(|error| error.to_string())?;
6633 if record_count == 0 {
6634 return Ok(0);
6635 }
6636 drop(local);
6637 let recovered = self.open_local()?;
6638 if recovered.visible_epoch().0 < applied_epoch {
6639 return Err(format!(
6640 "replica recovery stopped at epoch {}, expected {applied_epoch}",
6641 recovered.visible_epoch().0
6642 ));
6643 }
6644 drop(recovered);
6645 mongreldb_core::write_replica_epoch(&self.local_path, applied_epoch)
6646 .map_err(|error| error.to_string())?;
6647 self.last_epoch = applied_epoch;
6648 Ok(record_count)
6649 }
6650
6651 pub fn bootstrap(&mut self) -> Result<(), String> {
6652 self.ensure_bootstrap_destination_is_owned_or_empty()?;
6653 let url = format!("{}/replication/snapshot", self.leader_url);
6654 let response = self
6655 .request(&url)
6656 .send()
6657 .map_err(|error| format!("failed to fetch replication snapshot: {error}"))?;
6658 if !response.status().is_success() {
6659 return Err(format!(
6660 "leader snapshot endpoint returned {}",
6661 response.status()
6662 ));
6663 }
6664 let response_source_id = replication_digest_header(&response, "x-mongreldb-source-id")?;
6665 let response_epoch = replication_u64_header(&response, "x-mongreldb-current-epoch")?;
6666 let bytes = bounded_blocking_bytes(response, MAX_REPLICATION_SNAPSHOT_BYTES)
6667 .map_err(|error| format!("failed to read replication snapshot: {error}"))?;
6668 let snapshot = mongreldb_core::ReplicationSnapshot::decode(&bytes)
6669 .map_err(|error| error.to_string())?;
6670 if snapshot.source_id() != response_source_id {
6671 return Err("replication snapshot source header does not match snapshot".into());
6672 }
6673 if snapshot.epoch() != response_epoch {
6674 return Err("replication snapshot epoch header does not match snapshot".into());
6675 }
6676 let mut minimum_epoch = self
6677 .last_epoch
6678 .max(mongreldb_core::replica_epoch(&self.local_path).unwrap_or(0));
6679 if self.local_path.join("CATALOG").exists() {
6680 let local = self.open_local()?;
6681 minimum_epoch = minimum_epoch.max(local.visible_epoch().0);
6682 }
6683 if snapshot.epoch() < minimum_epoch {
6684 return Err(format!(
6685 "refusing replication snapshot epoch {} older than local epoch {minimum_epoch}",
6686 snapshot.epoch()
6687 ));
6688 }
6689 snapshot
6690 .install_validated(&self.local_path, |stage| {
6691 let database = self
6692 .open_path(stage)
6693 .map_err(mongreldb_core::MongrelError::Other)?;
6694 drop(database);
6695 Ok(())
6696 })
6697 .map_err(|error| error.to_string())?;
6698 self.last_epoch = snapshot.epoch();
6699 Ok(())
6700 }
6701
6702 fn ensure_bootstrap_destination_is_owned_or_empty(&self) -> Result<(), String> {
6703 let metadata = match std::fs::symlink_metadata(&self.local_path) {
6704 Ok(metadata) => metadata,
6705 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
6706 Err(error) => {
6707 return Err(format!(
6708 "failed to inspect replication destination {}: {error}",
6709 self.local_path.display()
6710 ))
6711 }
6712 };
6713 if metadata.file_type().is_symlink() || !metadata.is_dir() {
6714 return Err(format!(
6715 "replication destination is not a directory: {}",
6716 self.local_path.display()
6717 ));
6718 }
6719 if mongreldb_core::is_replica(&self.local_path) {
6720 return Ok(());
6721 }
6722 let mut entries = std::fs::read_dir(&self.local_path).map_err(|error| {
6723 format!(
6724 "failed to inspect replication destination {}: {error}",
6725 self.local_path.display()
6726 )
6727 })?;
6728 if entries
6729 .next()
6730 .transpose()
6731 .map_err(|error| error.to_string())?
6732 .is_some()
6733 {
6734 return Err(format!(
6735 "refusing to overwrite non-replica directory at {}",
6736 self.local_path.display()
6737 ));
6738 }
6739 drop(entries);
6740 std::fs::remove_dir(&self.local_path).map_err(|error| {
6741 format!(
6742 "failed to claim empty replication destination {}: {error}",
6743 self.local_path.display()
6744 )
6745 })?;
6746 Ok(())
6747 }
6748
6749 fn validate_snapshot_required_response(
6750 &self,
6751 response: reqwest::blocking::Response,
6752 ) -> Result<(), String> {
6753 let from_epoch = replication_u64_header(&response, "x-mongreldb-from-epoch")?;
6754 let current_epoch = replication_u64_header(&response, "x-mongreldb-current-epoch")?;
6755 let source_id = replication_digest_header(&response, "x-mongreldb-source-id")?;
6756 let status = response
6757 .headers()
6758 .get("x-mongreldb-replication-status")
6759 .and_then(|value| value.to_str().ok());
6760 if status != Some("snapshot-required") {
6761 return Err("leader returned an unrecognized replication conflict".into());
6762 }
6763 if self.local_path.join("CATALOG").exists() {
6764 let local_source = mongreldb_core::replica_source_id(&self.local_path)
6765 .map_err(|error| error.to_string())?;
6766 if source_id != local_source {
6767 return Err("replication conflict came from a different source".into());
6768 }
6769 }
6770 replication_u64_header(&response, "x-mongreldb-commit-count")?;
6771 replication_digest_header(&response, "x-mongreldb-records-sha256")?;
6772 if from_epoch != self.last_epoch || current_epoch < self.last_epoch {
6773 return Err(format!(
6774 "invalid replication snapshot requirement: from epoch {from_epoch}, current epoch {current_epoch}, local epoch {}",
6775 self.last_epoch
6776 ));
6777 }
6778 let body = bounded_blocking_bytes(response, MAX_CONTROL_RESPONSE_BYTES)
6779 .map_err(|error| format!("failed to read replication conflict response: {error}"))?;
6780 if body != b"replication snapshot required: WAL retention gap or spilled run" {
6781 return Err("leader returned an unrecognized replication conflict".into());
6782 }
6783 Ok(())
6784 }
6785
6786 fn fetch_wal(&self) -> Result<reqwest::blocking::Response, String> {
6787 let url = format!("{}/wal/stream?since={}", self.leader_url, self.last_epoch);
6788 self.request(&url)
6789 .send()
6790 .map_err(|error| format!("failed to connect to leader: {error}"))
6791 }
6792
6793 fn request(&self, url: &str) -> reqwest::blocking::RequestBuilder {
6794 let request = self.client.get(url);
6795 if let Some(token) = &self.bearer_token {
6796 request.bearer_auth(token.expose_secret())
6797 } else if let Some((username, password)) = &self.basic_auth {
6798 request.basic_auth(username, Some(password.expose_secret()))
6799 } else {
6800 request
6801 }
6802 }
6803
6804 fn open_local(&self) -> Result<mongreldb_core::Database, String> {
6805 self.open_path(&self.local_path)
6806 }
6807
6808 fn open_path(&self, path: &std::path::Path) -> Result<mongreldb_core::Database, String> {
6809 let result = match (&self.local_passphrase, &self.local_credentials) {
6810 (Some(passphrase), Some((username, password))) => {
6811 mongreldb_core::Database::open_encrypted_with_credentials(
6812 path,
6813 passphrase.expose_secret(),
6814 username,
6815 password.expose_secret(),
6816 )
6817 }
6818 (Some(passphrase), None) => {
6819 mongreldb_core::Database::open_encrypted(path, passphrase.expose_secret())
6820 }
6821 (None, Some((username, password))) => mongreldb_core::Database::open_with_credentials(
6822 path,
6823 username,
6824 password.expose_secret(),
6825 ),
6826 (None, None) => mongreldb_core::Database::open(path),
6827 };
6828 result.map_err(|error| format!("failed to open local replica: {error}"))
6829 }
6830
6831 pub fn last_epoch(&self) -> u64 {
6833 self.last_epoch
6834 }
6835
6836 pub fn last_seq(&self) -> u64 {
6838 self.last_epoch
6839 }
6840}
6841
6842fn replication_u64_header(
6843 response: &reqwest::blocking::Response,
6844 name: &str,
6845) -> Result<u64, String> {
6846 response
6847 .headers()
6848 .get(name)
6849 .ok_or_else(|| format!("leader response missing {name}"))?
6850 .to_str()
6851 .map_err(|error| format!("invalid {name}: {error}"))?
6852 .parse()
6853 .map_err(|error| format!("invalid {name}: {error}"))
6854}
6855
6856fn replication_digest_header(
6857 response: &reqwest::blocking::Response,
6858 name: &str,
6859) -> Result<[u8; 32], String> {
6860 let value = response
6861 .headers()
6862 .get(name)
6863 .ok_or_else(|| format!("leader response missing {name}"))?
6864 .to_str()
6865 .map_err(|error| format!("invalid {name}: {error}"))?;
6866 if value.len() != 64 {
6867 return Err(format!(
6868 "invalid {name}: expected 64 hexadecimal characters"
6869 ));
6870 }
6871 let mut digest = [0_u8; 32];
6872 for (index, byte) in digest.iter_mut().enumerate() {
6873 *byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16)
6874 .map_err(|error| format!("invalid {name}: {error}"))?;
6875 }
6876 Ok(digest)
6877}