1use crate::agent::wire_id;
2use crate::codes::QUERY_OP_VERSION;
3use crate::destination::{BackendResourceId, DestinationId};
4use crate::error::InvalidError;
5use crate::limits::{
6 MAX_PAGE_SIZE, MAX_QUERY_CURSOR_BYTES, MAX_QUERY_FIELDS, MAX_QUERY_NAME_BYTES,
7 MAX_QUERY_PARAMETERS, MAX_QUERY_PREDICATES, MAX_RAW_SQL_BYTES,
8};
9use crate::schema::{Digest32, LogicalField, TypedValue, UuidValue, validate_result_fields};
10use crate::validate::Validate;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeSet;
13
14wire_id!(
15 QueryExecutionId
17);
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(tag = "kind", rename_all = "snake_case")]
22#[non_exhaustive]
23pub enum QueryTarget {
24 Operational {
25 index: String,
26 },
27 Lakehouse {
28 destination_id: DestinationId,
29 destination_generation: u64,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 snapshot: Option<SnapshotSelector>,
32 },
33}
34
35impl QueryTarget {
36 pub fn operational(index: impl Into<String>) -> Self {
37 Self::Operational {
38 index: index.into(),
39 }
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
46#[non_exhaustive]
47pub enum SnapshotSelector {
48 SnapshotId(i64),
49 TimestampMicros(i64),
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55#[non_exhaustive]
56pub enum SqlDialect {
57 DataFusion,
58 Postgres,
59 MySql,
60 Sqlite,
61}
62
63#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
65pub struct KeyMatch {
66 pub field: String,
67 pub value: TypedValue,
68}
69
70impl KeyMatch {
71 pub fn new(field: impl Into<String>, value: impl Into<TypedValue>) -> Self {
73 Self {
74 field: field.into(),
75 value: value.into(),
76 }
77 }
78}
79
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83#[cfg_attr(feature = "builders", derive(bon::Builder))]
84pub struct Query {
85 pub execution_id: QueryExecutionId,
86 pub target: QueryTarget,
87 pub deadline_micros: u64,
89 #[cfg_attr(feature = "builders", builder(default))]
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub by_key: Vec<KeyMatch>,
92 #[cfg_attr(feature = "builders", builder(into))]
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub message_type: Option<String>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub time_range: Option<(u64, u64)>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub filter: Option<Filter>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub vector: Option<VectorQuery>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub text: Option<TextQuery>,
110 #[cfg_attr(feature = "builders", builder(default))]
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub order: Vec<Sort>,
113 #[cfg_attr(feature = "builders", builder(default))]
114 #[serde(default)]
115 pub page: QueryPageRequest,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub aggregate: Option<Aggregate>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub having: Option<Filter>,
123 #[cfg_attr(feature = "builders", builder(default))]
125 #[serde(default, skip_serializing_if = "is_false")]
126 pub distinct: bool,
127 #[cfg_attr(feature = "builders", builder(default))]
128 #[serde(default)]
129 pub select: Select,
130 #[cfg_attr(feature = "builders", builder(into))]
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub fork: Option<String>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub raw_sql: Option<RawSql>,
139 #[cfg_attr(feature = "builders", builder(default))]
142 #[serde(default, skip_serializing_if = "Consistency::is_eventual")]
143 pub consistency: Consistency,
144}
145
146impl Query {
147 pub fn new(execution_id: QueryExecutionId, target: QueryTarget, deadline_micros: u64) -> Self {
149 Self {
150 execution_id,
151 target,
152 deadline_micros,
153 by_key: Vec::new(),
154 message_type: None,
155 time_range: None,
156 filter: None,
157 vector: None,
158 text: None,
159 order: Vec::new(),
160 page: QueryPageRequest::default(),
161 aggregate: None,
162 having: None,
163 distinct: false,
164 select: Select::default(),
165 fork: None,
166 raw_sql: None,
167 consistency: Consistency::Eventual,
168 }
169 }
170
171 pub fn operational(
173 execution_id: QueryExecutionId,
174 index: impl Into<String>,
175 deadline_micros: u64,
176 ) -> Self {
177 Self::new(
178 execution_id,
179 QueryTarget::operational(index),
180 deadline_micros,
181 )
182 }
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
186pub struct QueryPageRequest {
187 pub limit: u32,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub offset: Option<u64>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub cursor: Option<String>,
192 #[serde(default, skip_serializing_if = "is_false")]
193 pub want_total: bool,
194}
195
196impl Default for QueryPageRequest {
197 fn default() -> Self {
198 Self {
199 limit: 50,
200 offset: Some(0),
201 cursor: None,
202 want_total: false,
203 }
204 }
205}
206
207impl Validate for Query {
208 fn validate(&self) -> Result<(), InvalidError> {
209 if self.execution_id.as_u128() == 0 {
210 return Err(InvalidError::new("query execution id must be nonzero"));
211 }
212 if self.deadline_micros == 0 {
213 return Err(InvalidError::new("query deadline must be nonzero"));
214 }
215 match &self.target {
216 QueryTarget::Operational { index } => validate_query_name("index", index)?,
217 QueryTarget::Lakehouse {
218 destination_id,
219 destination_generation,
220 snapshot,
221 } => {
222 if destination_id.as_u128() == 0 || *destination_generation == 0 {
223 return Err(InvalidError::new(
224 "lakehouse destination identity and generation must be nonzero",
225 ));
226 }
227 if let Some(selector) = snapshot {
228 match selector {
229 SnapshotSelector::SnapshotId(value)
230 | SnapshotSelector::TimestampMicros(value)
231 if *value <= 0 =>
232 {
233 return Err(InvalidError::new(
234 "snapshot selector must contain a positive value",
235 ));
236 }
237 _ => {}
238 }
239 }
240 if self.fork.is_some() {
241 return Err(InvalidError::new(
242 "a lakehouse query cannot resolve against an operational fork",
243 ));
244 }
245 }
246 }
247 if self.page.limit == 0 || self.page.limit > MAX_PAGE_SIZE as u32 {
248 return Err(InvalidError::new(format!(
249 "query limit {} is outside 1..={MAX_PAGE_SIZE}",
250 self.page.limit
251 )));
252 }
253 for (label, count) in [
254 ("exact matches", self.by_key.len()),
255 ("sort fields", self.order.len()),
256 ("selected fields", self.select.fields.len()),
257 ] {
258 if count > MAX_QUERY_FIELDS {
259 return Err(InvalidError::new(format!(
260 "query {label} count {count} exceeds cap {MAX_QUERY_FIELDS}"
261 )));
262 }
263 }
264 if self.page.offset.is_some() && self.page.cursor.is_some() {
265 return Err(InvalidError::new(
266 "query page cannot contain both an offset and a cursor",
267 ));
268 }
269 if let Some(cursor) = &self.page.cursor
270 && (cursor.is_empty()
271 || cursor.len() > MAX_QUERY_CURSOR_BYTES
272 || cursor.chars().any(char::is_control))
273 {
274 return Err(InvalidError::new(
275 "query cursor must contain 1..=512 bytes without control characters",
276 ));
277 }
278 for key_match in &self.by_key {
279 validate_query_name("key-match field", &key_match.field)?;
280 key_match.value.validate()?;
281 }
282 if let Some(message_type) = &self.message_type {
283 validate_query_name("message type", message_type)?;
284 }
285 if let Some((start, end)) = self.time_range
286 && start >= end
287 {
288 return Err(InvalidError::new(
289 "query time range start must be before its end, and both bounds are inclusive",
290 ));
291 }
292 for sort in &self.order {
293 validate_query_name("sort field", &sort.field)?;
294 }
295 for field in &self.select.fields {
296 validate_query_name("selected field", field)?;
297 }
298 if let Some(text) = &self.text {
299 text.validate()?;
300 }
301 if let Some(vector) = &self.vector {
302 vector.validate()?;
303 }
304 validate_filter(self.filter.as_ref())?;
305 validate_filter(self.having.as_ref())?;
306 if self.having.is_some() && self.aggregate.is_none() {
307 return Err(InvalidError::new(
308 "query having filter requires an aggregate",
309 ));
310 }
311 if let Some(aggregate) = &self.aggregate {
312 aggregate.validate()?;
313 }
314 if self.aggregate.is_some() && (!self.select.fields.is_empty() || self.select.payload) {
315 return Err(InvalidError::new(
316 "aggregate queries cannot request row selection",
317 ));
318 }
319 if let Some(raw_sql) = &self.raw_sql {
320 raw_sql.validate()?;
321 if !self.by_key.is_empty()
322 || self.message_type.is_some()
323 || self.time_range.is_some()
324 || self.filter.is_some()
325 || self.vector.is_some()
326 || self.text.is_some()
327 || !self.order.is_empty()
328 || self.aggregate.is_some()
329 || self.having.is_some()
330 || self.distinct
331 || !self.select.fields.is_empty()
332 || self.select.payload
333 {
334 return Err(InvalidError::new(
335 "raw SQL cannot be combined with the structured query expression",
336 ));
337 }
338 }
339 Ok(())
340 }
341}
342
343impl Validate for Aggregate {
344 fn validate(&self) -> Result<(), InvalidError> {
345 if self.funcs.is_empty() || self.funcs.len() > MAX_QUERY_PARAMETERS {
346 return Err(InvalidError::new(format!(
347 "aggregate function count must be in 1..={MAX_QUERY_PARAMETERS}"
348 )));
349 }
350 for field in &self.group_by {
351 validate_query_name("group field", field)?;
352 }
353 if self.group_by.len() > MAX_QUERY_FIELDS {
354 return Err(InvalidError::new(format!(
355 "aggregate group field count exceeds cap {MAX_QUERY_FIELDS}"
356 )));
357 }
358 let mut aliases = BTreeSet::new();
359 for call in &self.funcs {
360 validate_query_name("aggregate alias", &call.alias)?;
361 if !aliases.insert(&call.alias) {
362 return Err(InvalidError::new(format!(
363 "aggregate repeats alias `{}`",
364 call.alias
365 )));
366 }
367 if let Some(field) = &call.field {
368 validate_query_name("aggregate field", field)?;
369 }
370 match call.func {
371 AggFunc::Count if call.arg.is_none() => {}
372 AggFunc::Percentile
373 if call.field.is_some()
374 && call.arg.is_some_and(|value| {
375 value.is_finite() && (0.0..=1.0).contains(&value)
376 }) => {}
377 AggFunc::Percentile => {
378 return Err(InvalidError::new(
379 "percentile requires a field and a finite fraction in 0..=1",
380 ));
381 }
382 _ if call.field.is_some() && call.arg.is_none() => {}
383 _ => {
384 return Err(InvalidError::new(
385 "aggregate field and argument do not match the selected function",
386 ));
387 }
388 }
389 }
390 if let Some(window) = &self.window {
391 validate_query_name("window field", &window.field)?;
392 if window.every_micros == 0 {
393 return Err(InvalidError::new("window duration must be nonzero"));
394 }
395 }
396 Ok(())
397 }
398}
399
400impl Validate for RawSql {
401 fn validate(&self) -> Result<(), InvalidError> {
402 if self.sql.trim().is_empty() || self.sql.len() > MAX_RAW_SQL_BYTES {
403 return Err(InvalidError::new(format!(
404 "raw SQL must contain 1..={MAX_RAW_SQL_BYTES} bytes"
405 )));
406 }
407 if self
408 .sql
409 .chars()
410 .any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t'))
411 {
412 return Err(InvalidError::new("raw SQL contains a control character"));
413 }
414 if self.params.len() > MAX_QUERY_PARAMETERS {
415 return Err(InvalidError::new(format!(
416 "raw SQL has {} parameters, exceeds cap {MAX_QUERY_PARAMETERS}",
417 self.params.len()
418 )));
419 }
420 for param in &self.params {
421 param.validate()?;
422 }
423 Ok(())
424 }
425}
426
427fn validate_query_name(label: &str, value: &str) -> Result<(), InvalidError> {
428 if value.is_empty() || value.len() > MAX_QUERY_NAME_BYTES {
429 return Err(InvalidError::new(format!(
430 "{label} must contain 1..={MAX_QUERY_NAME_BYTES} bytes"
431 )));
432 }
433 if value.trim() != value || value.chars().any(char::is_control) {
434 return Err(InvalidError::new(format!(
435 "{label} contains surrounding whitespace or a control character"
436 )));
437 }
438 Ok(())
439}
440
441fn validate_filter(filter: Option<&Filter>) -> Result<(), InvalidError> {
442 fn visit(filter: &Filter, depth: usize, count: &mut usize) -> Result<(), InvalidError> {
443 *count += 1;
444 if *count > MAX_QUERY_PREDICATES {
445 return Err(InvalidError::new(format!(
446 "query filter node count exceeds cap {MAX_QUERY_PREDICATES}"
447 )));
448 }
449 if depth > 64 {
450 return Err(InvalidError::new("query filter depth exceeds cap 64"));
451 }
452 match filter {
453 Filter::All(filters) | Filter::Any(filters) => {
454 if filters.is_empty() {
455 return Err(InvalidError::new(
456 "query filter conjunction must not be empty",
457 ));
458 }
459 for filter in filters {
460 visit(filter, depth + 1, count)?;
461 }
462 Ok(())
463 }
464 Filter::Not(filter) => visit(filter, depth + 1, count),
465 Filter::Pred(predicate) => {
466 validate_query_name("predicate field", &predicate.field)?;
467 predicate.value.validate()?;
468 if predicate.op == CmpOp::In
469 && !matches!(&predicate.value, TypedValue::List(values) if !values.is_empty())
470 {
471 return Err(InvalidError::new(
472 "query IN predicate requires a nonempty typed list",
473 ));
474 }
475 if predicate.op == CmpOp::Prefix
476 && !matches!(&predicate.value, TypedValue::String(_))
477 {
478 return Err(InvalidError::new(
479 "query prefix predicate requires a string value",
480 ));
481 }
482 Ok(())
483 }
484 }
485 }
486
487 let mut count = 0;
488 filter.map_or(Ok(()), |filter| visit(filter, 1, &mut count))
489}
490
491fn is_false(value: &bool) -> bool {
492 !*value
493}
494
495#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case")]
507#[non_exhaustive]
508pub enum Consistency {
509 #[default]
513 Eventual,
514 ReadYourWrites,
520 Strong,
525}
526
527impl Consistency {
528 pub fn is_eventual(&self) -> bool {
530 matches!(self, Consistency::Eventual)
531 }
532}
533
534#[derive(Clone, Copy, Debug, PartialEq, Eq)]
547pub struct ConsistencyGate {
548 pub applied: u64,
550 pub required: u64,
552}
553
554impl ConsistencyGate {
555 pub fn new(applied: u64, required: u64) -> Self {
558 Self { applied, required }
559 }
560
561 pub fn is_caught_up(&self) -> bool {
563 self.applied >= self.required
564 }
565
566 pub fn check(&self, level: Consistency, what: impl Into<String>) -> Result<(), QueryError> {
571 if level.is_eventual() || self.is_caught_up() {
572 return Ok(());
573 }
574 Err(QueryError::Stale {
575 what: what.into(),
576 applied: self.applied,
577 required: self.required,
578 })
579 }
580}
581
582#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
586#[serde(rename_all = "snake_case")]
587pub enum Filter {
588 All(Vec<Filter>),
589 Any(Vec<Filter>),
590 Not(Box<Filter>),
591 Pred(Predicate),
592}
593
594impl Filter {
595 pub fn all(filters: impl IntoIterator<Item = Filter>) -> Self {
597 Filter::All(filters.into_iter().collect())
598 }
599
600 pub fn any(filters: impl IntoIterator<Item = Filter>) -> Self {
602 Filter::Any(filters.into_iter().collect())
603 }
604
605 pub fn negate(filter: Filter) -> Self {
607 Filter::Not(Box::new(filter))
608 }
609
610 pub fn pred(field: impl Into<String>, op: CmpOp, value: impl Into<TypedValue>) -> Self {
612 Filter::Pred(Predicate {
613 field: field.into(),
614 op,
615 value: value.into(),
616 })
617 }
618}
619
620#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
622pub struct Predicate {
623 pub field: String,
624 pub op: CmpOp,
625 pub value: TypedValue,
626}
627
628#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
631pub struct RawSql {
632 pub dialect: SqlDialect,
633 pub sql: String,
634 #[serde(default, skip_serializing_if = "Vec::is_empty")]
635 pub params: Vec<TypedValue>,
636}
637
638#[derive(
640 Clone,
641 Copy,
642 Debug,
643 PartialEq,
644 Eq,
645 Serialize,
646 Deserialize,
647 strum::Display,
648 strum::EnumString,
649 strum::VariantArray,
650)]
651#[serde(rename_all = "snake_case")]
652#[strum(serialize_all = "snake_case")]
653pub enum CmpOp {
654 Eq,
655 Ne,
656 Lt,
657 Lte,
658 Gt,
659 Gte,
660 In,
661 Contains,
662 Prefix,
663}
664
665#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
667pub struct Sort {
668 pub field: String,
669 #[serde(default)]
670 pub dir: Dir,
671}
672
673#[derive(
675 Clone,
676 Copy,
677 Debug,
678 Default,
679 PartialEq,
680 Eq,
681 Serialize,
682 Deserialize,
683 strum::Display,
684 strum::EnumString,
685 strum::VariantArray,
686)]
687#[serde(rename_all = "snake_case")]
688#[strum(serialize_all = "snake_case")]
689pub enum Dir {
690 #[default]
691 Asc,
692 Desc,
693}
694
695#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
701pub struct TextQuery {
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub field: Option<String>,
704 pub query: String,
705}
706
707#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
709pub struct VectorQuery {
710 pub field: String,
711 pub embedding: Vec<f32>,
712 pub top_k: u32,
713}
714
715#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
719pub struct Aggregate {
720 #[serde(default, skip_serializing_if = "Vec::is_empty")]
721 pub group_by: Vec<String>,
722 pub funcs: Vec<AggCall>,
723 #[serde(default, skip_serializing_if = "Option::is_none")]
724 pub window: Option<Window>,
725}
726
727#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
731pub struct AggCall {
732 pub func: AggFunc,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub field: Option<String>,
735 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub arg: Option<f64>,
737 pub alias: String,
738}
739
740#[derive(
743 Clone,
744 Copy,
745 Debug,
746 PartialEq,
747 Eq,
748 Serialize,
749 Deserialize,
750 strum::Display,
751 strum::EnumString,
752 strum::VariantArray,
753)]
754#[serde(rename_all = "snake_case")]
755#[strum(serialize_all = "snake_case")]
756pub enum AggFunc {
757 Count,
758 CountDistinct,
759 Sum,
760 Avg,
761 Min,
762 Max,
763 Percentile,
764 StdDev,
765}
766
767#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
769pub struct Window {
770 pub field: String,
771 pub every_micros: u64,
772}
773
774#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
776pub struct Select {
777 #[serde(default, skip_serializing_if = "Vec::is_empty")]
779 pub fields: Vec<String>,
780 #[serde(default)]
782 pub payload: bool,
783}
784
785#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
795#[serde(untagged)]
796pub enum Value {
797 Str(String),
798 Int(i64),
799 Uint(u64),
800 Float(f64),
801 Bool(bool),
802 Null,
803 List(Vec<Value>),
804}
805
806impl From<&str> for Value {
807 fn from(value: &str) -> Self {
808 Self::Str(value.to_owned())
809 }
810}
811
812impl From<String> for Value {
813 fn from(value: String) -> Self {
814 Self::Str(value)
815 }
816}
817
818impl From<&String> for Value {
819 fn from(value: &String) -> Self {
820 Self::Str(value.clone())
821 }
822}
823
824impl From<i64> for Value {
825 fn from(value: i64) -> Self {
826 Self::Int(value)
827 }
828}
829
830impl From<u64> for Value {
831 fn from(value: u64) -> Self {
832 Self::Uint(value)
833 }
834}
835
836impl From<i32> for Value {
837 fn from(value: i32) -> Self {
838 Self::Int(value as i64)
839 }
840}
841
842impl From<u32> for Value {
843 fn from(value: u32) -> Self {
844 Self::Int(value as i64)
845 }
846}
847
848impl From<f64> for Value {
849 fn from(value: f64) -> Self {
850 Self::Float(value)
851 }
852}
853
854impl From<f32> for Value {
855 fn from(value: f32) -> Self {
856 Self::Float(value as f64)
857 }
858}
859
860impl From<bool> for Value {
861 fn from(value: bool) -> Self {
862 Self::Bool(value)
863 }
864}
865
866impl<T: Into<Value>> From<Vec<T>> for Value {
867 fn from(values: Vec<T>) -> Self {
868 Self::List(values.into_iter().map(Into::into).collect())
869 }
870}
871
872impl Value {
873 pub fn from_input(input: &str) -> Self {
884 match input {
885 "null" => return Value::Null,
886 "true" => return Value::Bool(true),
887 "false" => return Value::Bool(false),
888 _ => {}
889 }
890 if let Ok(int) = input.parse::<i64>() {
891 return Value::Int(int);
892 }
893 if let Ok(uint) = input.parse::<u64>() {
894 return Value::Uint(uint);
895 }
896 if !input.is_empty()
899 && input
900 .bytes()
901 .all(|b| b.is_ascii_digit() || b == b'.' || b == b'-' || b == b'+')
902 && let Ok(float) = input.parse::<f64>()
903 {
904 return Value::Float(float);
905 }
906 Value::Str(input.to_owned())
907 }
908}
909
910impl std::fmt::Display for Value {
911 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
915 match self {
916 Value::Str(value) => f.write_str(value),
917 Value::Int(value) => write!(f, "{value}"),
918 Value::Uint(value) => write!(f, "{value}"),
919 Value::Float(value) => write!(f, "{value}"),
920 Value::Bool(value) => write!(f, "{value}"),
921 Value::Null => f.write_str("null"),
922 Value::List(values) => {
923 f.write_str("[")?;
924 for (index, value) in values.iter().enumerate() {
925 if index > 0 {
926 f.write_str(", ")?;
927 }
928 write!(f, "{value}")?;
929 }
930 f.write_str("]")
931 }
932 }
933 }
934}
935
936impl std::str::FromStr for Value {
937 type Err = std::convert::Infallible;
938
939 fn from_str(s: &str) -> Result<Self, Self::Err> {
940 Ok(Value::from_input(s))
941 }
942}
943
944#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
946pub struct QueryResult {
947 pub fields: Vec<LogicalField>,
948 pub rows: Vec<Row>,
949 #[serde(default)]
950 pub page: Page,
951 pub context: QueryContext,
952}
953
954#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
956pub struct Row {
957 pub values: Vec<TypedValue>,
958 #[serde(default, skip_serializing_if = "Option::is_none")]
961 pub score: Option<f32>,
962}
963
964impl Validate for QueryResult {
965 fn validate(&self) -> Result<(), InvalidError> {
966 validate_result_fields(&self.fields)?;
967 for row in &self.rows {
968 if row.values.len() != self.fields.len() {
969 return Err(InvalidError::new(
970 "query row value count does not match the result schema",
971 ));
972 }
973 for (value, field) in row.values.iter().zip(&self.fields) {
974 value.validate_against(&field.field_type, field.required)?;
975 }
976 if row.score.is_some_and(|score| !score.is_finite()) {
977 return Err(InvalidError::new("query row score must be finite"));
978 }
979 }
980 if self.context.execution_id.as_u128() == 0 {
981 return Err(InvalidError::new(
982 "query result execution id must be nonzero",
983 ));
984 }
985 if self.context.row_count != self.rows.len() as u64 {
986 return Err(InvalidError::new(
987 "query result row count does not match the page rows",
988 ));
989 }
990 self.context.validate()?;
991 self.page.validate()?;
992 if self.page.has_more != self.page.next_cursor.is_some() {
993 return Err(InvalidError::new(
994 "query page has_more and next_cursor must agree",
995 ));
996 }
997 Ok(())
998 }
999}
1000
1001impl Validate for QueryContext {
1002 fn validate(&self) -> Result<(), InvalidError> {
1003 if self.execution_id.as_u128() == 0 {
1004 return Err(InvalidError::new(
1005 "query context execution id must be nonzero",
1006 ));
1007 }
1008 validate_query_name("query engine name", &self.engine.name)?;
1009 validate_query_name("query engine version", &self.engine.version)?;
1010 if self.delivered_consistency < self.requested_consistency {
1011 return Err(InvalidError::new(
1012 "delivered consistency is weaker than requested consistency",
1013 ));
1014 }
1015 match &self.resolved_target {
1016 ResolvedQueryTarget::Operational {
1017 index,
1018 backend_resource_id,
1019 backend_generation,
1020 runtime_configuration_revision,
1021 } => {
1022 validate_query_name("resolved index", index)?;
1023 if backend_resource_id.as_u128() == 0
1024 || *backend_generation == 0
1025 || *runtime_configuration_revision == 0
1026 {
1027 return Err(InvalidError::new(
1028 "operational query context is missing backend evidence",
1029 ));
1030 }
1031 if self.boundary.is_some()
1032 || self.checkpoint_revision.is_some()
1033 || self.global_state_revision.is_some()
1034 {
1035 return Err(InvalidError::new(
1036 "operational query context cannot carry lakehouse checkpoint evidence",
1037 ));
1038 }
1039 }
1040 ResolvedQueryTarget::Lakehouse {
1041 destination_id,
1042 destination_generation,
1043 backend_resource_id,
1044 backend_generation,
1045 runtime_configuration_revision,
1046 table_uuid,
1047 namespace,
1048 table,
1049 snapshot_id,
1050 schema_id,
1051 partition_spec_id,
1052 } => {
1053 if destination_id.as_u128() == 0
1054 || *destination_generation == 0
1055 || backend_resource_id.as_u128() == 0
1056 || *backend_generation == 0
1057 || *runtime_configuration_revision == 0
1058 || table_uuid.as_bytes().len() != UuidValue::BYTES
1059 || namespace.is_empty()
1060 || *snapshot_id <= 0
1061 || *schema_id < 0
1062 || *partition_spec_id < 0
1063 || self.boundary.is_none()
1064 || self.checkpoint_revision.is_none()
1065 || self.global_state_revision.is_none()
1066 {
1067 return Err(InvalidError::new(
1068 "lakehouse query context is missing resolved target evidence",
1069 ));
1070 }
1071 for part in namespace {
1072 validate_query_name("resolved table namespace", part)?;
1073 }
1074 validate_query_name("resolved table", table)?;
1075 }
1076 }
1077 if let Some(boundary) = &self.boundary {
1078 boundary.digest.validate()?;
1079 }
1080 Ok(())
1081 }
1082}
1083
1084impl QueryResult {
1085 pub fn field_index(&self, name: &str) -> Option<usize> {
1086 self.fields.iter().position(|field| field.name == name)
1087 }
1088
1089 pub fn value<'a>(&self, row: &'a Row, field: &str) -> Option<&'a TypedValue> {
1090 self.field_index(field)
1091 .and_then(|index| row.values.get(index))
1092 }
1093
1094 pub fn value_text(&self, row: &Row, field: &str) -> Option<String> {
1095 self.value(row, field).map(TypedValue::diagnostic_text)
1096 }
1097
1098 pub fn value_u64(&self, row: &Row, field: &str) -> Option<u64> {
1099 self.value(row, field).and_then(TypedValue::as_u64)
1100 }
1101
1102 pub fn value_i64(&self, row: &Row, field: &str) -> Option<i64> {
1103 self.value(row, field).and_then(TypedValue::as_i64)
1104 }
1105}
1106
1107#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1109pub struct QueryContext {
1110 pub execution_id: QueryExecutionId,
1111 pub engine: QueryEngine,
1112 pub resolved_target: ResolvedQueryTarget,
1113 pub requested_consistency: Consistency,
1114 pub delivered_consistency: Consistency,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1116 pub boundary: Option<MaterializationBoundary>,
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1118 pub checkpoint_revision: Option<u64>,
1119 #[serde(default, skip_serializing_if = "Option::is_none")]
1120 pub global_state_revision: Option<u64>,
1121 pub truncated: bool,
1122 pub elapsed_micros: u64,
1123 pub scanned_bytes: u64,
1124 pub produced_bytes: u64,
1125 pub row_count: u64,
1126}
1127
1128#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1129pub struct QueryEngine {
1130 pub name: String,
1131 pub version: String,
1132 #[serde(default, skip_serializing_if = "Option::is_none")]
1133 pub dialect: Option<SqlDialect>,
1134}
1135
1136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1137#[serde(tag = "kind", rename_all = "snake_case")]
1138#[non_exhaustive]
1139pub enum ResolvedQueryTarget {
1140 Operational {
1141 index: String,
1142 backend_resource_id: BackendResourceId,
1143 backend_generation: u64,
1144 runtime_configuration_revision: u64,
1145 },
1146 Lakehouse {
1147 destination_id: DestinationId,
1148 destination_generation: u64,
1149 backend_resource_id: BackendResourceId,
1150 backend_generation: u64,
1151 runtime_configuration_revision: u64,
1152 table_uuid: UuidValue,
1153 namespace: Vec<String>,
1154 table: String,
1155 snapshot_id: i64,
1156 schema_id: i32,
1157 partition_spec_id: i32,
1158 },
1159}
1160
1161#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1162pub struct MaterializationBoundary {
1163 pub digest: Digest32,
1164 pub relation_to_current: BoundaryRelation,
1165}
1166
1167#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1168#[serde(rename_all = "snake_case")]
1169#[non_exhaustive]
1170pub enum BoundaryRelation {
1171 Current,
1172 Historical,
1173 AheadOfObservedCheckpoint,
1174}
1175
1176#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1185pub struct Page {
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1187 pub offset: Option<u64>,
1188 pub limit: u32,
1190 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub total: Option<u64>,
1193 pub has_more: bool,
1195 #[serde(default, skip_serializing_if = "Option::is_none")]
1196 pub next_cursor: Option<String>,
1197}
1198
1199impl Page {
1200 pub fn at_least(&self, rows_on_page: usize) -> Option<u64> {
1204 self.offset
1205 .and_then(|offset| offset.checked_add(rows_on_page as u64))
1206 }
1207
1208 pub fn total_pages(&self) -> Option<u64> {
1212 match (self.total, self.limit) {
1213 (Some(total), limit) if limit > 0 => Some(total.div_ceil(u64::from(limit))),
1214 _ => None,
1215 }
1216 }
1217}
1218
1219impl Validate for Page {
1220 fn validate(&self) -> Result<(), InvalidError> {
1221 if self.limit == 0 || self.limit > MAX_PAGE_SIZE as u32 {
1222 return Err(InvalidError::new(format!(
1223 "query result page limit exceeds cap {MAX_PAGE_SIZE}"
1224 )));
1225 }
1226 if self.has_more != self.next_cursor.is_some() {
1227 return Err(InvalidError::new(
1228 "query page has_more and next_cursor must agree",
1229 ));
1230 }
1231 if let Some(cursor) = &self.next_cursor
1232 && (cursor.is_empty()
1233 || cursor.len() > MAX_QUERY_CURSOR_BYTES
1234 || cursor.chars().any(char::is_control))
1235 {
1236 return Err(InvalidError::new("query result cursor is invalid"));
1237 }
1238 Ok(())
1239 }
1240}
1241
1242#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1245#[non_exhaustive]
1246pub struct QueryEnvelope {
1247 pub v: u32,
1248 pub query: Query,
1249}
1250
1251impl QueryEnvelope {
1252 pub fn new(query: Query) -> Self {
1254 Self {
1255 v: QUERY_OP_VERSION,
1256 query,
1257 }
1258 }
1259}
1260
1261impl Validate for QueryEnvelope {
1262 fn validate(&self) -> Result<(), InvalidError> {
1263 if self.v != QUERY_OP_VERSION {
1264 return Err(InvalidError::new(format!(
1265 "query version must be {QUERY_OP_VERSION}, got {}",
1266 self.v
1267 )));
1268 }
1269 self.query.validate()
1270 }
1271}
1272
1273#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1274pub struct QueryPageEnvelope {
1275 pub v: u32,
1276 pub execution_id: QueryExecutionId,
1277 pub cursor: String,
1278 pub deadline_micros: u64,
1279}
1280
1281#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1282pub struct QueryCancelEnvelope {
1283 pub v: u32,
1284 pub execution_id: QueryExecutionId,
1285}
1286
1287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1288pub struct QueryStatusEnvelope {
1289 pub v: u32,
1290 pub execution_id: QueryExecutionId,
1291}
1292
1293impl QueryPageEnvelope {
1294 pub fn new(
1295 execution_id: QueryExecutionId,
1296 cursor: impl Into<String>,
1297 deadline_micros: u64,
1298 ) -> Self {
1299 Self {
1300 v: QUERY_OP_VERSION,
1301 execution_id,
1302 cursor: cursor.into(),
1303 deadline_micros,
1304 }
1305 }
1306}
1307
1308impl QueryCancelEnvelope {
1309 pub const fn new(execution_id: QueryExecutionId) -> Self {
1310 Self {
1311 v: QUERY_OP_VERSION,
1312 execution_id,
1313 }
1314 }
1315}
1316
1317impl QueryStatusEnvelope {
1318 pub const fn new(execution_id: QueryExecutionId) -> Self {
1319 Self {
1320 v: QUERY_OP_VERSION,
1321 execution_id,
1322 }
1323 }
1324}
1325
1326impl Validate for QueryPageEnvelope {
1327 fn validate(&self) -> Result<(), InvalidError> {
1328 validate_query_control(self.v, self.execution_id)?;
1329 if self.cursor.is_empty()
1330 || self.cursor.len() > MAX_QUERY_CURSOR_BYTES
1331 || self.cursor.chars().any(char::is_control)
1332 || self.deadline_micros == 0
1333 {
1334 return Err(InvalidError::new(
1335 "query page cursor or deadline is invalid",
1336 ));
1337 }
1338 Ok(())
1339 }
1340}
1341
1342impl Validate for QueryCancelEnvelope {
1343 fn validate(&self) -> Result<(), InvalidError> {
1344 validate_query_control(self.v, self.execution_id)
1345 }
1346}
1347
1348impl Validate for QueryStatusEnvelope {
1349 fn validate(&self) -> Result<(), InvalidError> {
1350 validate_query_control(self.v, self.execution_id)
1351 }
1352}
1353
1354fn validate_query_control(v: u32, execution_id: QueryExecutionId) -> Result<(), InvalidError> {
1355 if v != QUERY_OP_VERSION {
1356 return Err(InvalidError::new(format!(
1357 "query version must be {QUERY_OP_VERSION}, got {v}"
1358 )));
1359 }
1360 if execution_id.as_u128() == 0 {
1361 return Err(InvalidError::new("query execution id must be nonzero"));
1362 }
1363 Ok(())
1364}
1365
1366#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1367pub struct QueryExecutionStatus {
1368 pub execution_id: QueryExecutionId,
1369 pub state: QueryExecutionState,
1370 pub started_at_micros: u64,
1371 #[serde(default, skip_serializing_if = "Option::is_none")]
1372 pub finished_at_micros: Option<u64>,
1373 pub scanned_bytes: u64,
1374 pub produced_bytes: u64,
1375 pub row_count: u64,
1376 #[serde(default, skip_serializing_if = "Option::is_none")]
1377 pub error: Option<QueryError>,
1378}
1379
1380impl Validate for QueryExecutionStatus {
1381 fn validate(&self) -> Result<(), InvalidError> {
1382 if self.execution_id.as_u128() == 0 || self.started_at_micros == 0 {
1383 return Err(InvalidError::new(
1384 "query execution identity and start time must be nonzero",
1385 ));
1386 }
1387 let terminal = matches!(
1388 self.state,
1389 QueryExecutionState::Completed
1390 | QueryExecutionState::Cancelled
1391 | QueryExecutionState::Failed
1392 | QueryExecutionState::Expired
1393 );
1394 match (terminal, self.finished_at_micros) {
1395 (true, Some(finished)) if finished >= self.started_at_micros => {}
1396 (false, None) => {}
1397 _ => Err(InvalidError::new(
1398 "query execution finish time does not match its state",
1399 ))?,
1400 }
1401 if matches!(self.state, QueryExecutionState::Failed) != self.error.is_some() {
1402 return Err(InvalidError::new(
1403 "query execution error must be present exactly for failed state",
1404 ));
1405 }
1406 Ok(())
1407 }
1408}
1409
1410#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1411#[serde(rename_all = "snake_case")]
1412#[non_exhaustive]
1413pub enum QueryExecutionState {
1414 Queued,
1415 Planning,
1416 Running,
1417 Completed,
1418 Cancelled,
1419 Failed,
1420 Expired,
1421}
1422
1423#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1424#[non_exhaustive]
1425pub enum QueryStatusReply {
1426 Ok(QueryExecutionStatus),
1427 Err(QueryError),
1428}
1429
1430#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1431#[non_exhaustive]
1432pub enum QueryCancelReply {
1433 Ok(QueryExecutionStatus),
1434 Err(QueryError),
1435}
1436
1437#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1439#[non_exhaustive]
1440pub enum QueryReply {
1441 Ok(Box<QueryResult>),
1442 Err(QueryError),
1443}
1444
1445#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
1447#[non_exhaustive]
1448pub enum QueryError {
1449 #[error("query not supported: {0}")]
1450 Unsupported(String),
1451 #[error("unauthorized: {0}")]
1452 Unauthorized(String),
1453 #[error("index not found: {0}")]
1454 IndexNotFound(String),
1455 #[error("fork not found: {0}")]
1456 ForkNotFound(String),
1457 #[error("backend error: {0}")]
1458 Backend(String),
1459 #[error("temporarily unavailable: {0}")]
1464 Unavailable(String),
1465 #[error("result too large: {what} {size} exceeds cap {cap}")]
1472 TooLarge { what: String, size: u64, cap: u64 },
1473 #[error("unsupported envelope version (expected {expected}, got {got})")]
1474 Version { expected: u32, got: u32 },
1475 #[error("stale read: {what} applied {applied}, required {required}")]
1481 Stale {
1482 what: String,
1483 applied: u64,
1484 required: u64,
1485 },
1486 #[error("query {execution_id} was cancelled")]
1487 Cancelled { execution_id: QueryExecutionId },
1488 #[error("query {execution_id} exceeded its deadline")]
1489 DeadlineExceeded { execution_id: QueryExecutionId },
1490 #[error("snapshot {snapshot_id} is no longer available")]
1491 ExpiredSnapshot { snapshot_id: i64 },
1492 #[error("stale {what} generation: requested {requested}, observed {observed}")]
1493 StaleGeneration {
1494 what: String,
1495 requested: u64,
1496 observed: u64,
1497 },
1498 #[error("query target unavailable: {reason}")]
1499 TargetUnavailable { reason: String },
1500 #[error("query resource limit exceeded: {resource} used {observed}, limit {limit}")]
1501 ResourceLimit {
1502 resource: String,
1503 observed: u64,
1504 limit: u64,
1505 },
1506}
1507
1508#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1510#[serde(rename_all = "snake_case")]
1511#[non_exhaustive]
1512pub enum QueryErrorCode {
1513 Unsupported,
1514 Unauthorized,
1515 IndexNotFound,
1516 ForkNotFound,
1517 Backend,
1518 Unavailable,
1519 TooLarge,
1520 Version,
1521 Stale,
1522 Cancelled,
1523 DeadlineExceeded,
1524 ExpiredSnapshot,
1525 StaleGeneration,
1526 TargetUnavailable,
1527 ResourceLimit,
1528}
1529
1530impl QueryError {
1531 pub fn code(&self) -> QueryErrorCode {
1532 match self {
1533 Self::Unsupported(_) => QueryErrorCode::Unsupported,
1534 Self::Unauthorized(_) => QueryErrorCode::Unauthorized,
1535 Self::IndexNotFound(_) => QueryErrorCode::IndexNotFound,
1536 Self::ForkNotFound(_) => QueryErrorCode::ForkNotFound,
1537 Self::Backend(_) => QueryErrorCode::Backend,
1538 Self::Unavailable(_) => QueryErrorCode::Unavailable,
1539 Self::TooLarge { .. } => QueryErrorCode::TooLarge,
1540 Self::Version { .. } => QueryErrorCode::Version,
1541 Self::Stale { .. } => QueryErrorCode::Stale,
1542 Self::Cancelled { .. } => QueryErrorCode::Cancelled,
1543 Self::DeadlineExceeded { .. } => QueryErrorCode::DeadlineExceeded,
1544 Self::ExpiredSnapshot { .. } => QueryErrorCode::ExpiredSnapshot,
1545 Self::StaleGeneration { .. } => QueryErrorCode::StaleGeneration,
1546 Self::TargetUnavailable { .. } => QueryErrorCode::TargetUnavailable,
1547 Self::ResourceLimit { .. } => QueryErrorCode::ResourceLimit,
1548 }
1549 }
1550}
1551
1552#[cfg(test)]
1553mod tests {
1554 use super::*;
1555
1556 #[test]
1557 fn given_dsl_enums_when_displayed_then_should_be_snake_case() {
1558 assert_eq!(CmpOp::Gte.to_string(), "gte");
1559 assert_eq!(CmpOp::Prefix.to_string(), "prefix");
1560 assert_eq!("ne".parse::<CmpOp>().expect("ne parses"), CmpOp::Ne);
1561 assert_eq!(Dir::Desc.to_string(), "desc");
1562 assert_eq!(AggFunc::Count.to_string(), "count");
1563 }
1564
1565 #[test]
1566 fn given_a_consistency_gate_when_checked_then_should_fail_not_downgrade() {
1567 assert!(
1569 ConsistencyGate::new(0, 100)
1570 .check(Consistency::Eventual, "readings")
1571 .is_ok()
1572 );
1573 assert!(
1575 ConsistencyGate::new(100, 100)
1576 .check(Consistency::ReadYourWrites, "readings")
1577 .is_ok()
1578 );
1579 let stale = ConsistencyGate::new(41, 57)
1580 .check(Consistency::Strong, "readings")
1581 .expect_err("a lagging projector must fail, never downgrade");
1582 assert!(matches!(
1583 stale,
1584 QueryError::Stale {
1585 applied: 41,
1586 required: 57,
1587 ..
1588 }
1589 ));
1590 }
1591
1592 #[test]
1593 fn given_a_page_when_computing_total_pages_then_should_divide_by_limit() {
1594 let page = Page {
1595 offset: Some(0),
1596 limit: 3,
1597 total: Some(10),
1598 has_more: true,
1599 next_cursor: Some("next".to_owned()),
1600 };
1601 assert_eq!(page.total_pages(), Some(4));
1602 assert_eq!(page.at_least(3), Some(3), "offset 0 plus this page's rows");
1603 assert_eq!(Page::default().total_pages(), None);
1605 }
1606}
1607
1608#[cfg(all(test, feature = "codecs"))]
1609mod serde_tests {
1610 use super::*;
1611 use crate::codes::QUERY_OP_VERSION;
1612 use crate::framing::{decode_named, encode_named};
1613
1614 fn query() -> Query {
1615 Query {
1616 execution_id: QueryExecutionId::from_u128(1),
1617 target: QueryTarget::operational("readings"),
1618 deadline_micros: 1_000_000,
1619 by_key: Vec::new(),
1620 message_type: None,
1621 time_range: None,
1622 filter: None,
1623 vector: None,
1624 text: None,
1625 order: Vec::new(),
1626 page: QueryPageRequest::default(),
1627 aggregate: None,
1628 having: None,
1629 distinct: false,
1630 select: Select::default(),
1631 fork: None,
1632 raw_sql: None,
1633 consistency: Consistency::Eventual,
1634 }
1635 }
1636
1637 #[test]
1638 fn given_dsl_enums_when_serialized_then_serde_should_match_display() {
1639 assert_eq!(
1640 serde_json::to_string(&CmpOp::Lte).expect("CmpOp serializes"),
1641 "\"lte\""
1642 );
1643 assert_eq!(
1644 serde_json::from_str::<CmpOp>("\"in\"").expect("CmpOp deserializes"),
1645 CmpOp::In
1646 );
1647 assert_eq!(
1648 serde_json::to_string(&Dir::Asc).expect("Dir serializes"),
1649 "\"asc\""
1650 );
1651 }
1652
1653 #[test]
1654 fn given_a_query_when_round_tripped_through_the_envelope_then_should_be_unchanged() {
1655 let mut query = query();
1656 query.by_key = vec![KeyMatch::new("host_id", "abc")];
1657 query.filter = Some(Filter::pred("status", CmpOp::Eq, "degraded"));
1658 query.order = vec![Sort {
1659 field: "ts".to_owned(),
1660 dir: Dir::Desc,
1661 }];
1662 query.page.limit = 20;
1663 let request = QueryEnvelope::new(query);
1664
1665 let json = serde_json::to_string(&request).expect("the request serializes");
1666 let back: QueryEnvelope = serde_json::from_str(&json).expect("the request deserializes");
1667 assert_eq!(back.v, QUERY_OP_VERSION);
1668 assert_eq!(back.query.target, QueryTarget::operational("readings"));
1669 assert_eq!(back.query.page.limit, 20);
1670 assert_eq!(back.query.by_key, vec![KeyMatch::new("host_id", "abc")]);
1671 let Some(Filter::Pred(predicate)) = &back.query.filter else {
1672 panic!("expected a single predicate filter");
1673 };
1674 assert_eq!(predicate.value, TypedValue::String("degraded".to_owned()));
1675 assert_eq!(back.query.order[0].dir, Dir::Desc);
1676 }
1677
1678 #[test]
1679 fn given_each_consistency_level_when_round_tripped_then_should_preserve_it_and_skip_eventual() {
1680 for level in [
1681 Consistency::Eventual,
1682 Consistency::ReadYourWrites,
1683 Consistency::Strong,
1684 ] {
1685 let mut query = query();
1686 query.consistency = level;
1687 let bytes = encode_named(&QueryEnvelope::new(query)).expect("serializes");
1688 let back: QueryEnvelope = decode_named(&bytes).expect("deserializes");
1689 assert_eq!(back.query.consistency, level);
1690 }
1691 let default = query();
1694 assert_eq!(default.consistency, Consistency::Eventual);
1695 let json = serde_json::to_string(&default).expect("json");
1696 assert!(
1697 !json.contains("consistency"),
1698 "default Eventual must be omitted: {json}"
1699 );
1700 }
1701
1702 #[test]
1703 fn given_a_stale_reply_when_round_tripped_then_should_preserve_the_offsets() {
1704 let reply = QueryReply::Err(QueryError::Stale {
1705 what: "readings".to_owned(),
1706 applied: 41,
1707 required: 57,
1708 });
1709 let bytes = encode_named(&reply).expect("serializes");
1710 let back: QueryReply = decode_named(&bytes).expect("deserializes");
1711 let QueryReply::Err(QueryError::Stale {
1712 what,
1713 applied,
1714 required,
1715 }) = back
1716 else {
1717 panic!("expected a Stale error");
1718 };
1719 assert_eq!((what.as_str(), applied, required), ("readings", 41, 57));
1720 }
1721
1722 #[test]
1723 fn given_a_vector_query_when_round_tripped_then_should_preserve_the_embedding() {
1724 let mut query = query();
1725 query.target = QueryTarget::operational("mem:conv-1");
1726 query.vector = Some(VectorQuery {
1727 field: "embedding".to_owned(),
1728 embedding: vec![0.1, 0.2, 0.3],
1729 top_k: 5,
1730 });
1731 let json = serde_json::to_string(&query).expect("the query serializes");
1732 let back: Query = serde_json::from_str(&json).expect("the query deserializes");
1733 let vector = back.vector.expect("the vector survives the round-trip");
1734 assert_eq!(vector.embedding, vec![0.1, 0.2, 0.3]);
1735 assert_eq!(vector.top_k, 5);
1736 }
1737
1738 #[test]
1739 fn given_a_typed_reply_when_round_tripped_then_should_preserve_the_values() {
1740 let reply = QueryReply::Ok(Box::new(QueryResult {
1741 fields: vec![LogicalField {
1742 id: 1,
1743 name: "reading_id".to_owned(),
1744 required: true,
1745 field_type: crate::schema::LogicalType::String,
1746 doc: None,
1747 }],
1748 rows: vec![Row {
1749 values: vec![TypedValue::String("123".to_owned())],
1750 score: Some(0.25),
1751 }],
1752 page: Page {
1753 offset: Some(0),
1754 limit: 50,
1755 total: Some(1),
1756 has_more: false,
1757 next_cursor: None,
1758 },
1759 context: QueryContext {
1760 execution_id: QueryExecutionId::from_u128(1),
1761 engine: QueryEngine {
1762 name: "datafusion".to_owned(),
1763 version: "50.0.0".to_owned(),
1764 dialect: Some(SqlDialect::DataFusion),
1765 },
1766 resolved_target: ResolvedQueryTarget::Operational {
1767 index: "readings".to_owned(),
1768 backend_resource_id: BackendResourceId::from_u128(2),
1769 backend_generation: 1,
1770 runtime_configuration_revision: 1,
1771 },
1772 requested_consistency: Consistency::Eventual,
1773 delivered_consistency: Consistency::Eventual,
1774 boundary: None,
1775 checkpoint_revision: None,
1776 global_state_revision: None,
1777 truncated: false,
1778 elapsed_micros: 10,
1779 scanned_bytes: 128,
1780 produced_bytes: 32,
1781 row_count: 1,
1782 },
1783 }));
1784 let bytes = encode_named(&reply).expect("the reply serializes");
1785 let back: QueryReply = decode_named(&bytes).expect("the reply deserializes");
1786 let QueryReply::Ok(result) = back else {
1787 panic!("the reply should decode as Ok");
1788 };
1789 assert_eq!(
1790 result.rows[0].values,
1791 vec![TypedValue::String("123".to_owned())]
1792 );
1793 assert_eq!(result.page.total, Some(1));
1794 assert!(!result.page.has_more);
1795 }
1796}