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