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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1131 pub frontier: Vec<QueryFrontier>,
1132}
1133
1134#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1136pub struct QueryFrontier {
1137 pub stream: String,
1138 pub topic: String,
1139 pub partition_id: u32,
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1144 pub topic_created_at_micros: Option<u64>,
1145 pub applied_offset: u64,
1147}
1148
1149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1150pub struct QueryEngine {
1151 pub name: String,
1152 pub version: String,
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub dialect: Option<SqlDialect>,
1155}
1156
1157#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1158#[serde(tag = "kind", rename_all = "snake_case")]
1159#[non_exhaustive]
1160pub enum ResolvedQueryTarget {
1161 Operational {
1162 index: String,
1163 backend_resource_id: BackendResourceId,
1164 backend_generation: u64,
1165 runtime_configuration_revision: u64,
1166 },
1167 Lakehouse {
1168 destination_id: DestinationId,
1169 destination_generation: u64,
1170 backend_resource_id: BackendResourceId,
1171 backend_generation: u64,
1172 runtime_configuration_revision: u64,
1173 table_uuid: UuidValue,
1174 namespace: Vec<String>,
1175 table: String,
1176 snapshot_id: i64,
1177 schema_id: i32,
1178 partition_spec_id: i32,
1179 },
1180}
1181
1182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1183pub struct MaterializationBoundary {
1184 pub digest: Digest32,
1185 pub relation_to_current: BoundaryRelation,
1186}
1187
1188#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1189#[serde(rename_all = "snake_case")]
1190#[non_exhaustive]
1191pub enum BoundaryRelation {
1192 Current,
1193 Historical,
1194 AheadOfObservedCheckpoint,
1195}
1196
1197#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1206pub struct Page {
1207 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub offset: Option<u64>,
1209 pub limit: u32,
1211 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub total: Option<u64>,
1214 pub has_more: bool,
1216 #[serde(default, skip_serializing_if = "Option::is_none")]
1217 pub next_cursor: Option<String>,
1218}
1219
1220impl Page {
1221 pub fn at_least(&self, rows_on_page: usize) -> Option<u64> {
1225 self.offset
1226 .and_then(|offset| offset.checked_add(rows_on_page as u64))
1227 }
1228
1229 pub fn total_pages(&self) -> Option<u64> {
1233 match (self.total, self.limit) {
1234 (Some(total), limit) if limit > 0 => Some(total.div_ceil(u64::from(limit))),
1235 _ => None,
1236 }
1237 }
1238}
1239
1240impl Validate for Page {
1241 fn validate(&self) -> Result<(), InvalidError> {
1242 if self.limit == 0 || self.limit > MAX_PAGE_SIZE as u32 {
1243 return Err(InvalidError::new(format!(
1244 "query result page limit exceeds cap {MAX_PAGE_SIZE}"
1245 )));
1246 }
1247 if self.has_more != self.next_cursor.is_some() {
1248 return Err(InvalidError::new(
1249 "query page has_more and next_cursor must agree",
1250 ));
1251 }
1252 if let Some(cursor) = &self.next_cursor
1253 && (cursor.is_empty()
1254 || cursor.len() > MAX_QUERY_CURSOR_BYTES
1255 || cursor.chars().any(char::is_control))
1256 {
1257 return Err(InvalidError::new("query result cursor is invalid"));
1258 }
1259 Ok(())
1260 }
1261}
1262
1263#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1266#[non_exhaustive]
1267pub struct QueryEnvelope {
1268 pub v: u32,
1269 pub query: Query,
1270}
1271
1272impl QueryEnvelope {
1273 pub fn new(query: Query) -> Self {
1275 Self {
1276 v: QUERY_OP_VERSION,
1277 query,
1278 }
1279 }
1280}
1281
1282impl Validate for QueryEnvelope {
1283 fn validate(&self) -> Result<(), InvalidError> {
1284 if self.v != QUERY_OP_VERSION {
1285 return Err(InvalidError::new(format!(
1286 "query version must be {QUERY_OP_VERSION}, got {}",
1287 self.v
1288 )));
1289 }
1290 self.query.validate()
1291 }
1292}
1293
1294#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1295pub struct QueryPageEnvelope {
1296 pub v: u32,
1297 pub execution_id: QueryExecutionId,
1298 pub cursor: String,
1299 pub deadline_micros: u64,
1300}
1301
1302#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1303pub struct QueryCancelEnvelope {
1304 pub v: u32,
1305 pub execution_id: QueryExecutionId,
1306}
1307
1308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1309pub struct QueryStatusEnvelope {
1310 pub v: u32,
1311 pub execution_id: QueryExecutionId,
1312}
1313
1314impl QueryPageEnvelope {
1315 pub fn new(
1316 execution_id: QueryExecutionId,
1317 cursor: impl Into<String>,
1318 deadline_micros: u64,
1319 ) -> Self {
1320 Self {
1321 v: QUERY_OP_VERSION,
1322 execution_id,
1323 cursor: cursor.into(),
1324 deadline_micros,
1325 }
1326 }
1327}
1328
1329impl QueryCancelEnvelope {
1330 pub const fn new(execution_id: QueryExecutionId) -> Self {
1331 Self {
1332 v: QUERY_OP_VERSION,
1333 execution_id,
1334 }
1335 }
1336}
1337
1338impl QueryStatusEnvelope {
1339 pub const fn new(execution_id: QueryExecutionId) -> Self {
1340 Self {
1341 v: QUERY_OP_VERSION,
1342 execution_id,
1343 }
1344 }
1345}
1346
1347impl Validate for QueryPageEnvelope {
1348 fn validate(&self) -> Result<(), InvalidError> {
1349 validate_query_control(self.v, self.execution_id)?;
1350 if self.cursor.is_empty()
1351 || self.cursor.len() > MAX_QUERY_CURSOR_BYTES
1352 || self.cursor.chars().any(char::is_control)
1353 || self.deadline_micros == 0
1354 {
1355 return Err(InvalidError::new(
1356 "query page cursor or deadline is invalid",
1357 ));
1358 }
1359 Ok(())
1360 }
1361}
1362
1363impl Validate for QueryCancelEnvelope {
1364 fn validate(&self) -> Result<(), InvalidError> {
1365 validate_query_control(self.v, self.execution_id)
1366 }
1367}
1368
1369impl Validate for QueryStatusEnvelope {
1370 fn validate(&self) -> Result<(), InvalidError> {
1371 validate_query_control(self.v, self.execution_id)
1372 }
1373}
1374
1375fn validate_query_control(v: u32, execution_id: QueryExecutionId) -> Result<(), InvalidError> {
1376 if v != QUERY_OP_VERSION {
1377 return Err(InvalidError::new(format!(
1378 "query version must be {QUERY_OP_VERSION}, got {v}"
1379 )));
1380 }
1381 if execution_id.as_u128() == 0 {
1382 return Err(InvalidError::new("query execution id must be nonzero"));
1383 }
1384 Ok(())
1385}
1386
1387#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1388pub struct QueryExecutionStatus {
1389 pub execution_id: QueryExecutionId,
1390 pub state: QueryExecutionState,
1391 pub started_at_micros: u64,
1392 #[serde(default, skip_serializing_if = "Option::is_none")]
1393 pub finished_at_micros: Option<u64>,
1394 pub scanned_bytes: u64,
1395 pub produced_bytes: u64,
1396 pub row_count: u64,
1397 #[serde(default, skip_serializing_if = "Option::is_none")]
1398 pub error: Option<QueryError>,
1399}
1400
1401impl Validate for QueryExecutionStatus {
1402 fn validate(&self) -> Result<(), InvalidError> {
1403 if self.execution_id.as_u128() == 0 || self.started_at_micros == 0 {
1404 return Err(InvalidError::new(
1405 "query execution identity and start time must be nonzero",
1406 ));
1407 }
1408 let terminal = matches!(
1409 self.state,
1410 QueryExecutionState::Completed
1411 | QueryExecutionState::Cancelled
1412 | QueryExecutionState::Failed
1413 | QueryExecutionState::Expired
1414 );
1415 match (terminal, self.finished_at_micros) {
1416 (true, Some(finished)) if finished >= self.started_at_micros => {}
1417 (false, None) => {}
1418 _ => Err(InvalidError::new(
1419 "query execution finish time does not match its state",
1420 ))?,
1421 }
1422 if matches!(self.state, QueryExecutionState::Failed) != self.error.is_some() {
1423 return Err(InvalidError::new(
1424 "query execution error must be present exactly for failed state",
1425 ));
1426 }
1427 Ok(())
1428 }
1429}
1430
1431#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1432#[serde(rename_all = "snake_case")]
1433#[non_exhaustive]
1434pub enum QueryExecutionState {
1435 Queued,
1436 Planning,
1437 Running,
1438 Completed,
1439 Cancelled,
1440 Failed,
1441 Expired,
1442}
1443
1444#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1445#[non_exhaustive]
1446pub enum QueryStatusReply {
1447 Ok(QueryExecutionStatus),
1448 Err(QueryError),
1449}
1450
1451#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1452#[non_exhaustive]
1453pub enum QueryCancelReply {
1454 Ok(QueryExecutionStatus),
1455 Err(QueryError),
1456}
1457
1458#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1460#[non_exhaustive]
1461pub enum QueryReply {
1462 Ok(Box<QueryResult>),
1463 Err(QueryError),
1464}
1465
1466#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
1468#[non_exhaustive]
1469pub enum QueryError {
1470 #[error("query not supported: {0}")]
1471 Unsupported(String),
1472 #[error("unauthorized: {0}")]
1473 Unauthorized(String),
1474 #[error("index not found: {0}")]
1475 IndexNotFound(String),
1476 #[error("fork not found: {0}")]
1477 ForkNotFound(String),
1478 #[error("backend error: {0}")]
1479 Backend(String),
1480 #[error("temporarily unavailable: {0}")]
1485 Unavailable(String),
1486 #[error("result too large: {what} {size} exceeds cap {cap}")]
1493 TooLarge { what: String, size: u64, cap: u64 },
1494 #[error("unsupported envelope version (expected {expected}, got {got})")]
1495 Version { expected: u32, got: u32 },
1496 #[error("stale read: {what} applied {applied}, required {required}")]
1502 Stale {
1503 what: String,
1504 applied: u64,
1505 required: u64,
1506 },
1507 #[error("query {execution_id} was cancelled")]
1508 Cancelled { execution_id: QueryExecutionId },
1509 #[error("query {execution_id} exceeded its deadline")]
1510 DeadlineExceeded { execution_id: QueryExecutionId },
1511 #[error("snapshot {snapshot_id} is no longer available")]
1512 ExpiredSnapshot { snapshot_id: i64 },
1513 #[error("stale {what} generation: requested {requested}, observed {observed}")]
1514 StaleGeneration {
1515 what: String,
1516 requested: u64,
1517 observed: u64,
1518 },
1519 #[error("query target unavailable: {reason}")]
1520 TargetUnavailable { reason: String },
1521 #[error("query resource limit exceeded: {resource} used {observed}, limit {limit}")]
1522 ResourceLimit {
1523 resource: String,
1524 observed: u64,
1525 limit: u64,
1526 },
1527}
1528
1529#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1531#[serde(rename_all = "snake_case")]
1532#[non_exhaustive]
1533pub enum QueryErrorCode {
1534 Unsupported,
1535 Unauthorized,
1536 IndexNotFound,
1537 ForkNotFound,
1538 Backend,
1539 Unavailable,
1540 TooLarge,
1541 Version,
1542 Stale,
1543 Cancelled,
1544 DeadlineExceeded,
1545 ExpiredSnapshot,
1546 StaleGeneration,
1547 TargetUnavailable,
1548 ResourceLimit,
1549}
1550
1551impl QueryError {
1552 pub fn code(&self) -> QueryErrorCode {
1553 match self {
1554 Self::Unsupported(_) => QueryErrorCode::Unsupported,
1555 Self::Unauthorized(_) => QueryErrorCode::Unauthorized,
1556 Self::IndexNotFound(_) => QueryErrorCode::IndexNotFound,
1557 Self::ForkNotFound(_) => QueryErrorCode::ForkNotFound,
1558 Self::Backend(_) => QueryErrorCode::Backend,
1559 Self::Unavailable(_) => QueryErrorCode::Unavailable,
1560 Self::TooLarge { .. } => QueryErrorCode::TooLarge,
1561 Self::Version { .. } => QueryErrorCode::Version,
1562 Self::Stale { .. } => QueryErrorCode::Stale,
1563 Self::Cancelled { .. } => QueryErrorCode::Cancelled,
1564 Self::DeadlineExceeded { .. } => QueryErrorCode::DeadlineExceeded,
1565 Self::ExpiredSnapshot { .. } => QueryErrorCode::ExpiredSnapshot,
1566 Self::StaleGeneration { .. } => QueryErrorCode::StaleGeneration,
1567 Self::TargetUnavailable { .. } => QueryErrorCode::TargetUnavailable,
1568 Self::ResourceLimit { .. } => QueryErrorCode::ResourceLimit,
1569 }
1570 }
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575 use super::*;
1576
1577 #[test]
1578 fn given_dsl_enums_when_displayed_then_should_be_snake_case() {
1579 assert_eq!(CmpOp::Gte.to_string(), "gte");
1580 assert_eq!(CmpOp::Prefix.to_string(), "prefix");
1581 assert_eq!("ne".parse::<CmpOp>().expect("ne parses"), CmpOp::Ne);
1582 assert_eq!(Dir::Desc.to_string(), "desc");
1583 assert_eq!(AggFunc::Count.to_string(), "count");
1584 }
1585
1586 #[test]
1587 fn given_a_consistency_gate_when_checked_then_should_fail_not_downgrade() {
1588 assert!(
1590 ConsistencyGate::new(0, 100)
1591 .check(Consistency::Eventual, "readings")
1592 .is_ok()
1593 );
1594 assert!(
1596 ConsistencyGate::new(100, 100)
1597 .check(Consistency::ReadYourWrites, "readings")
1598 .is_ok()
1599 );
1600 let stale = ConsistencyGate::new(41, 57)
1601 .check(Consistency::Strong, "readings")
1602 .expect_err("a lagging projector must fail, never downgrade");
1603 assert!(matches!(
1604 stale,
1605 QueryError::Stale {
1606 applied: 41,
1607 required: 57,
1608 ..
1609 }
1610 ));
1611 }
1612
1613 #[test]
1614 fn given_a_page_when_computing_total_pages_then_should_divide_by_limit() {
1615 let page = Page {
1616 offset: Some(0),
1617 limit: 3,
1618 total: Some(10),
1619 has_more: true,
1620 next_cursor: Some("next".to_owned()),
1621 };
1622 assert_eq!(page.total_pages(), Some(4));
1623 assert_eq!(page.at_least(3), Some(3), "offset 0 plus this page's rows");
1624 assert_eq!(Page::default().total_pages(), None);
1626 }
1627}
1628
1629#[cfg(all(test, feature = "codecs"))]
1630mod serde_tests {
1631 use super::*;
1632 use crate::codes::QUERY_OP_VERSION;
1633 use crate::framing::{decode_named, encode_named};
1634
1635 fn query() -> Query {
1636 Query {
1637 execution_id: QueryExecutionId::from_u128(1),
1638 target: QueryTarget::operational("readings"),
1639 deadline_micros: 1_000_000,
1640 by_key: Vec::new(),
1641 message_type: None,
1642 time_range: None,
1643 filter: None,
1644 vector: None,
1645 text: None,
1646 order: Vec::new(),
1647 page: QueryPageRequest::default(),
1648 aggregate: None,
1649 having: None,
1650 distinct: false,
1651 select: Select::default(),
1652 fork: None,
1653 raw_sql: None,
1654 consistency: Consistency::Eventual,
1655 }
1656 }
1657
1658 #[test]
1659 fn given_dsl_enums_when_serialized_then_serde_should_match_display() {
1660 assert_eq!(
1661 serde_json::to_string(&CmpOp::Lte).expect("CmpOp serializes"),
1662 "\"lte\""
1663 );
1664 assert_eq!(
1665 serde_json::from_str::<CmpOp>("\"in\"").expect("CmpOp deserializes"),
1666 CmpOp::In
1667 );
1668 assert_eq!(
1669 serde_json::to_string(&Dir::Asc).expect("Dir serializes"),
1670 "\"asc\""
1671 );
1672 }
1673
1674 #[test]
1675 fn given_a_query_when_round_tripped_through_the_envelope_then_should_be_unchanged() {
1676 let mut query = query();
1677 query.by_key = vec![KeyMatch::new("host_id", "abc")];
1678 query.filter = Some(Filter::pred("status", CmpOp::Eq, "degraded"));
1679 query.order = vec![Sort {
1680 field: "ts".to_owned(),
1681 dir: Dir::Desc,
1682 }];
1683 query.page.limit = 20;
1684 let request = QueryEnvelope::new(query);
1685
1686 let json = serde_json::to_string(&request).expect("the request serializes");
1687 let back: QueryEnvelope = serde_json::from_str(&json).expect("the request deserializes");
1688 assert_eq!(back.v, QUERY_OP_VERSION);
1689 assert_eq!(back.query.target, QueryTarget::operational("readings"));
1690 assert_eq!(back.query.page.limit, 20);
1691 assert_eq!(back.query.by_key, vec![KeyMatch::new("host_id", "abc")]);
1692 let Some(Filter::Pred(predicate)) = &back.query.filter else {
1693 panic!("expected a single predicate filter");
1694 };
1695 assert_eq!(predicate.value, TypedValue::String("degraded".to_owned()));
1696 assert_eq!(back.query.order[0].dir, Dir::Desc);
1697 }
1698
1699 #[test]
1700 fn given_each_consistency_level_when_round_tripped_then_should_preserve_it_and_skip_eventual() {
1701 for level in [
1702 Consistency::Eventual,
1703 Consistency::ReadYourWrites,
1704 Consistency::Strong,
1705 ] {
1706 let mut query = query();
1707 query.consistency = level;
1708 let bytes = encode_named(&QueryEnvelope::new(query)).expect("serializes");
1709 let back: QueryEnvelope = decode_named(&bytes).expect("deserializes");
1710 assert_eq!(back.query.consistency, level);
1711 }
1712 let default = query();
1715 assert_eq!(default.consistency, Consistency::Eventual);
1716 let json = serde_json::to_string(&default).expect("json");
1717 assert!(
1718 !json.contains("consistency"),
1719 "default Eventual must be omitted: {json}"
1720 );
1721 }
1722
1723 #[test]
1724 fn given_a_stale_reply_when_round_tripped_then_should_preserve_the_offsets() {
1725 let reply = QueryReply::Err(QueryError::Stale {
1726 what: "readings".to_owned(),
1727 applied: 41,
1728 required: 57,
1729 });
1730 let bytes = encode_named(&reply).expect("serializes");
1731 let back: QueryReply = decode_named(&bytes).expect("deserializes");
1732 let QueryReply::Err(QueryError::Stale {
1733 what,
1734 applied,
1735 required,
1736 }) = back
1737 else {
1738 panic!("expected a Stale error");
1739 };
1740 assert_eq!((what.as_str(), applied, required), ("readings", 41, 57));
1741 }
1742
1743 #[test]
1744 fn given_a_vector_query_when_round_tripped_then_should_preserve_the_embedding() {
1745 let mut query = query();
1746 query.target = QueryTarget::operational("mem:conv-1");
1747 query.vector = Some(VectorQuery {
1748 field: "embedding".to_owned(),
1749 embedding: vec![0.1, 0.2, 0.3],
1750 top_k: 5,
1751 });
1752 let json = serde_json::to_string(&query).expect("the query serializes");
1753 let back: Query = serde_json::from_str(&json).expect("the query deserializes");
1754 let vector = back.vector.expect("the vector survives the round-trip");
1755 assert_eq!(vector.embedding, vec![0.1, 0.2, 0.3]);
1756 assert_eq!(vector.top_k, 5);
1757 }
1758
1759 #[test]
1760 fn given_a_typed_reply_when_round_tripped_then_should_preserve_the_values() {
1761 let reply = QueryReply::Ok(Box::new(QueryResult {
1762 fields: vec![LogicalField {
1763 id: 1,
1764 name: "reading_id".to_owned(),
1765 required: true,
1766 field_type: crate::schema::LogicalType::String,
1767 doc: None,
1768 }],
1769 rows: vec![Row {
1770 values: vec![TypedValue::String("123".to_owned())],
1771 score: Some(0.25),
1772 }],
1773 page: Page {
1774 offset: Some(0),
1775 limit: 50,
1776 total: Some(1),
1777 has_more: false,
1778 next_cursor: None,
1779 },
1780 context: QueryContext {
1781 execution_id: QueryExecutionId::from_u128(1),
1782 engine: QueryEngine {
1783 name: "datafusion".to_owned(),
1784 version: "50.0.0".to_owned(),
1785 dialect: Some(SqlDialect::DataFusion),
1786 },
1787 resolved_target: ResolvedQueryTarget::Operational {
1788 index: "readings".to_owned(),
1789 backend_resource_id: BackendResourceId::from_u128(2),
1790 backend_generation: 1,
1791 runtime_configuration_revision: 1,
1792 },
1793 requested_consistency: Consistency::Eventual,
1794 delivered_consistency: Consistency::Eventual,
1795 boundary: None,
1796 checkpoint_revision: None,
1797 global_state_revision: None,
1798 truncated: false,
1799 elapsed_micros: 10,
1800 scanned_bytes: 128,
1801 produced_bytes: 32,
1802 row_count: 1,
1803 frontier: Vec::new(),
1804 },
1805 }));
1806 let bytes = encode_named(&reply).expect("the reply serializes");
1807 let back: QueryReply = decode_named(&bytes).expect("the reply deserializes");
1808 let QueryReply::Ok(result) = back else {
1809 panic!("the reply should decode as Ok");
1810 };
1811 assert_eq!(
1812 result.rows[0].values,
1813 vec![TypedValue::String("123".to_owned())]
1814 );
1815 assert_eq!(result.page.total, Some(1));
1816 assert!(!result.page.has_more);
1817 }
1818}