1use crate::error::QdrantResult;
7use crate::point::{PayloadValue, Point, PointId, ScoredPoint};
8use serde_json::{Value as JsonValue, json};
9
10fn serialize_json_request(request: &JsonValue) -> Vec<u8> {
11 match serde_json::to_vec(request) {
12 Ok(bytes) => bytes,
13 Err(err) => {
14 encode_error_request(&format!("failed to serialize Qdrant JSON request: {err}"))
15 }
16 }
17}
18
19fn encode_error_request(message: &str) -> Vec<u8> {
20 serde_json::to_vec(&json!({ "error": message }))
21 .unwrap_or_else(|_| b"{\"error\":\"failed to encode Qdrant JSON error\"}".to_vec())
22}
23
24fn vector_to_json(values: &[f32], label: &str) -> Result<JsonValue, String> {
25 if values.is_empty() {
26 return Err(format!("Qdrant {label} vector must not be empty"));
27 }
28 let mut json_values = Vec::with_capacity(values.len());
29 for (idx, value) in values.iter().enumerate() {
30 if !value.is_finite() {
31 return Err(format!(
32 "Qdrant {label} contains non-finite vector value at index {idx}: {value}"
33 ));
34 }
35 json_values.push(json!(value));
36 }
37 Ok(JsonValue::Array(json_values))
38}
39
40fn ensure_positive_limit(limit: u64, label: &str) -> Result<(), String> {
41 if limit == 0 {
42 return Err(format!("Qdrant {label} limit must be greater than zero"));
43 }
44 Ok(())
45}
46
47fn ensure_named_vector_name(name: &str, label: &str) -> Result<(), String> {
48 if name.trim().is_empty() {
49 return Err(format!("Qdrant {label} vector name must not be empty"));
50 }
51 Ok(())
52}
53
54fn ensure_payload_key(key: &str) -> Result<(), String> {
55 if key.trim().is_empty() {
56 return Err("Qdrant payload field name must not be empty".to_string());
57 }
58 Ok(())
59}
60
61fn ensure_point_id(id: &PointId, label: &str) -> Result<(), String> {
62 match id {
63 PointId::Num(_) => Ok(()),
64 PointId::Uuid(s) if s.trim().is_empty() => {
65 Err(format!("Qdrant {label} point id must not be empty"))
66 }
67 PointId::Uuid(_) => Ok(()),
68 }
69}
70
71fn canonical_distance(distance: &str) -> Result<&'static str, String> {
72 match distance.trim().to_ascii_lowercase().as_str() {
73 "cosine" => Ok("Cosine"),
74 "euclidean" => Ok("Euclidean"),
75 "dot" => Ok("Dot"),
76 _ => Err(format!("Unsupported Qdrant distance metric: {distance}")),
77 }
78}
79
80fn optional_threshold_to_json(value: Option<f32>) -> Result<Option<JsonValue>, String> {
81 match value {
82 Some(value) if !value.is_finite() => Err(format!(
83 "Qdrant score threshold must be finite, got {value}"
84 )),
85 Some(value) => Ok(Some(json!(value))),
86 None => Ok(None),
87 }
88}
89
90pub fn encode_search_request(
105 vector: &[f32],
106 limit: u64,
107 offset: Option<u64>,
108 score_threshold: Option<f32>,
109 with_vector: bool,
110) -> Vec<u8> {
111 if let Err(err) = ensure_positive_limit(limit, "search") {
112 return encode_error_request(&err);
113 }
114 let vector = match vector_to_json(vector, "search request") {
115 Ok(vector) => vector,
116 Err(err) => return encode_error_request(&err),
117 };
118 let mut request = json!({
119 "vector": vector,
120 "limit": limit,
121 "with_payload": true,
122 "with_vector": with_vector,
123 });
124
125 if let Some(off) = offset {
126 request["offset"] = json!(off);
127 }
128
129 let score_threshold = match optional_threshold_to_json(score_threshold) {
130 Ok(score_threshold) => score_threshold,
131 Err(err) => return encode_error_request(&err),
132 };
133 if let Some(threshold) = score_threshold {
134 request["score_threshold"] = threshold;
135 }
136
137 serialize_json_request(&request)
138}
139
140pub fn encode_search_request_with_filter(
142 vector: &[f32],
143 limit: u64,
144 offset: Option<u64>,
145 score_threshold: Option<f32>,
146 with_vector: bool,
147 filter: JsonValue,
148) -> Vec<u8> {
149 if let Err(err) = ensure_positive_limit(limit, "search") {
150 return encode_error_request(&err);
151 }
152 let vector = match vector_to_json(vector, "search request") {
153 Ok(vector) => vector,
154 Err(err) => return encode_error_request(&err),
155 };
156 let mut request = json!({
157 "vector": vector,
158 "limit": limit,
159 "with_payload": true,
160 "with_vector": with_vector,
161 "filter": filter,
162 });
163
164 if let Some(off) = offset {
165 request["offset"] = json!(off);
166 }
167
168 let score_threshold = match optional_threshold_to_json(score_threshold) {
169 Ok(score_threshold) => score_threshold,
170 Err(err) => return encode_error_request(&err),
171 };
172 if let Some(threshold) = score_threshold {
173 request["score_threshold"] = threshold;
174 }
175
176 serialize_json_request(&request)
177}
178
179pub fn encode_upsert_request(points: &[Point]) -> Vec<u8> {
192 if points.is_empty() {
193 return encode_error_request("Qdrant upsert point list must not be empty");
194 }
195 let points_json: Result<Vec<JsonValue>, String> = points
196 .iter()
197 .enumerate()
198 .map(|(idx, p)| {
199 ensure_point_id(&p.id, &format!("upsert point {idx}"))?;
200 let id = match &p.id {
201 PointId::Uuid(s) => json!(s),
202 PointId::Num(n) => json!(n),
203 };
204
205 let payload = payload_to_json_map(&p.payload)?;
206
207 let vector = vector_to_json(&p.vector, &format!("upsert point {idx}"))?;
208
209 Ok(json!({
210 "id": id,
211 "vector": vector,
212 "payload": JsonValue::Object(payload),
213 }))
214 })
215 .collect();
216
217 let points_json = match points_json {
218 Ok(points_json) => points_json,
219 Err(err) => return encode_error_request(&err),
220 };
221 let request = json!({ "points": points_json });
222 serialize_json_request(&request)
223}
224
225pub fn encode_upsert_multi_vector_request(points: &[crate::point::MultiVectorPoint]) -> Vec<u8> {
229 use crate::point::MultiVectorPoint;
230
231 if points.is_empty() {
232 return encode_error_request("Qdrant upsert point list must not be empty");
233 }
234 let points_json: Result<Vec<JsonValue>, String> = points
235 .iter()
236 .enumerate()
237 .map(|(idx, p): (usize, &MultiVectorPoint)| {
238 if p.vectors.is_empty() {
239 return Err(format!(
240 "Qdrant multi-vector point {idx} must contain at least one named vector"
241 ));
242 }
243 ensure_point_id(&p.id, &format!("multi-vector point {idx}"))?;
244 let id = match &p.id {
245 PointId::Uuid(s) => json!(s),
246 PointId::Num(n) => json!(n),
247 };
248
249 let payload = payload_to_json_map(&p.payload)?;
250
251 let vectors: Result<serde_json::Map<String, JsonValue>, String> = p
253 .vectors
254 .iter()
255 .map(|(k, v)| {
256 ensure_named_vector_name(k, &format!("multi-vector point {idx}"))?;
257 Ok((
258 k.clone(),
259 vector_to_json(v, &format!("multi-vector point {idx}.{k}"))?,
260 ))
261 })
262 .collect();
263
264 Ok(json!({
265 "id": id,
266 "vector": JsonValue::Object(vectors?),
267 "payload": JsonValue::Object(payload),
268 }))
269 })
270 .collect();
271
272 let points_json = match points_json {
273 Ok(points_json) => points_json,
274 Err(err) => return encode_error_request(&err),
275 };
276 let request = json!({ "points": points_json });
277 serialize_json_request(&request)
278}
279
280pub fn encode_delete_request(ids: &[PointId]) -> Vec<u8> {
289 if ids.is_empty() {
290 return encode_error_request("Qdrant delete point id list must not be empty");
291 }
292 let ids_json: Result<Vec<JsonValue>, String> = ids
293 .iter()
294 .map(|id| match id {
295 PointId::Uuid(s) => {
296 ensure_point_id(id, "delete")?;
297 Ok(json!(s))
298 }
299 PointId::Num(n) => Ok(json!(n)),
300 })
301 .collect();
302 let ids_json = match ids_json {
303 Ok(ids_json) => ids_json,
304 Err(err) => return encode_error_request(&err),
305 };
306
307 let request = json!({ "points": ids_json });
308 serialize_json_request(&request)
309}
310
311pub fn encode_create_collection_request(
315 vector_size: u64,
316 distance: &str, ) -> Vec<u8> {
318 if vector_size == 0 {
319 return encode_error_request("Qdrant collection vector_size must be greater than zero");
320 }
321 let distance = match canonical_distance(distance) {
322 Ok(distance) => distance,
323 Err(err) => return encode_error_request(&err),
324 };
325 let request = json!({
326 "vectors": {
327 "size": vector_size,
328 "distance": distance,
329 }
330 });
331 serialize_json_request(&request)
332}
333
334pub fn encode_conditions_to_filter(
352 conditions: &[qail_core::ast::Condition],
353 is_or: bool,
354) -> JsonValue {
355 use qail_core::ast::{Expr, Operator, Value};
356
357 let mut clauses = Vec::with_capacity(conditions.len());
358 let mut must_not_clauses = Vec::new();
359 for cond in conditions {
360 let key = match &cond.left {
362 Expr::Named(name) => name.clone(),
363 Expr::Aliased { name, .. } => name.clone(),
364 _ => return impossible_filter(),
365 };
366 let key = normalize_filter_key(&key);
367 if key.is_empty() {
368 return impossible_filter();
369 }
370
371 if key.eq_ignore_ascii_case("id") {
372 let ids = match cond.op {
373 Operator::Eq => match point_id_value_to_json(&cond.value) {
374 Some(id) => vec![id],
375 None => return impossible_filter(),
376 },
377 Operator::In => match point_id_values_to_json(&cond.value) {
378 Some(ids) => ids,
379 None => return impossible_filter(),
380 },
381 Operator::Ne => match point_id_value_to_json(&cond.value) {
382 Some(id) => {
383 must_not_clauses.push(json!({ "has_id": [id] }));
384 continue;
385 }
386 None => return impossible_filter(),
387 },
388 Operator::NotIn => match point_id_values_to_json(&cond.value) {
389 Some(ids) => {
390 must_not_clauses.push(json!({ "has_id": ids }));
391 continue;
392 }
393 None => return impossible_filter(),
394 },
395 _ => return impossible_filter(),
396 };
397 clauses.push(json!({ "has_id": ids }));
398 continue;
399 }
400
401 let clause = match (&cond.op, &cond.value) {
403 (Operator::Eq, Value::String(s)) => json!({
405 "key": key,
406 "match": { "value": s }
407 }),
408 (Operator::Eq, Value::Uuid(u)) => json!({
409 "key": key,
410 "match": { "value": u.to_string() }
411 }),
412 (Operator::Eq, Value::Int(n)) => json!({
413 "key": key,
414 "match": { "value": n }
415 }),
416 (Operator::Eq, Value::Bool(b)) => json!({
417 "key": key,
418 "match": { "value": b }
419 }),
420 (Operator::Ne, Value::String(s)) => {
421 must_not_clauses.push(json!({
422 "key": key,
423 "match": { "value": s }
424 }));
425 continue;
426 }
427 (Operator::Ne, Value::Uuid(u)) => {
428 must_not_clauses.push(json!({
429 "key": key,
430 "match": { "value": u.to_string() }
431 }));
432 continue;
433 }
434 (Operator::Ne, Value::Int(n)) => {
435 must_not_clauses.push(json!({
436 "key": key,
437 "match": { "value": n }
438 }));
439 continue;
440 }
441 (Operator::Ne, Value::Bool(b)) => {
442 must_not_clauses.push(json!({
443 "key": key,
444 "match": { "value": b }
445 }));
446 continue;
447 }
448
449 (Operator::Gt, Value::Int(n)) => json!({
451 "key": key,
452 "range": { "gt": n }
453 }),
454 (Operator::Gt, Value::Float(f)) if f.is_finite() => json!({
455 "key": key,
456 "range": { "gt": f }
457 }),
458 (Operator::Gte, Value::Int(n)) => json!({
459 "key": key,
460 "range": { "gte": n }
461 }),
462 (Operator::Gte, Value::Float(f)) if f.is_finite() => json!({
463 "key": key,
464 "range": { "gte": f }
465 }),
466 (Operator::Lt, Value::Int(n)) => json!({
467 "key": key,
468 "range": { "lt": n }
469 }),
470 (Operator::Lt, Value::Float(f)) if f.is_finite() => json!({
471 "key": key,
472 "range": { "lt": f }
473 }),
474 (Operator::Lte, Value::Int(n)) => json!({
475 "key": key,
476 "range": { "lte": n }
477 }),
478 (Operator::Lte, Value::Float(f)) if f.is_finite() => json!({
479 "key": key,
480 "range": { "lte": f }
481 }),
482
483 (Operator::In, Value::Array(arr)) => {
485 let Some(values) = values_to_json(arr) else {
486 return impossible_filter();
487 };
488 json!({
489 "key": key,
490 "match": { "any": values }
491 })
492 }
493 (Operator::NotIn, Value::Array(arr)) => {
494 let Some(values) = values_to_json(arr) else {
495 return impossible_filter();
496 };
497 must_not_clauses.push(json!({
498 "key": key,
499 "match": { "any": values }
500 }));
501 continue;
502 }
503
504 (Operator::IsNull, Value::Null | Value::NullUuid) => json!({
506 "is_null": { "key": key }
507 }),
508 (Operator::IsNotNull, Value::Null | Value::NullUuid) => {
509 must_not_clauses.push(json!({
510 "is_null": { "key": key }
511 }));
512 continue;
513 }
514
515 (Operator::Contains | Operator::Like, Value::String(s)) if !s.trim().is_empty() => {
517 json!({
518 "key": key,
519 "match": { "text": s }
520 })
521 }
522 (Operator::NotLike, Value::String(s)) if !s.trim().is_empty() => {
523 must_not_clauses.push(json!({
524 "key": key,
525 "match": { "text": s }
526 }));
527 continue;
528 }
529
530 _ => return impossible_filter(),
531 };
532
533 clauses.push(clause);
534 }
535
536 if is_or {
538 clauses.extend(
539 must_not_clauses
540 .into_iter()
541 .map(|clause| json!({ "must_not": [clause] })),
542 );
543 json!({ "should": clauses })
544 } else {
545 let mut filter = serde_json::Map::new();
546 filter.insert("must".to_string(), JsonValue::Array(clauses));
547 if !must_not_clauses.is_empty() {
548 filter.insert("must_not".to_string(), JsonValue::Array(must_not_clauses));
549 }
550 JsonValue::Object(filter)
551 }
552}
553
554fn normalize_filter_key(raw: &str) -> String {
555 raw.trim().trim_matches('"').trim().to_string()
556}
557
558fn point_id_value_to_json(value: &qail_core::ast::Value) -> Option<JsonValue> {
559 use qail_core::ast::Value;
560
561 match value {
562 Value::Int(id) if *id >= 0 => Some(json!(*id as u64)),
563 Value::String(id) if !id.trim().is_empty() => Some(json!(id)),
564 Value::Uuid(id) => Some(json!(id.to_string())),
565 _ => None,
566 }
567}
568
569fn point_id_values_to_json(value: &qail_core::ast::Value) -> Option<Vec<JsonValue>> {
570 let qail_core::ast::Value::Array(values) = value else {
571 return None;
572 };
573 if values.is_empty() {
574 return None;
575 }
576 values.iter().map(point_id_value_to_json).collect()
577}
578
579fn impossible_filter() -> JsonValue {
580 json!({
581 "must": [{
582 "key": "__qail_unrepresentable_filter__",
583 "range": { "gt": 1, "lt": 0 }
584 }]
585 })
586}
587
588fn values_to_json(values: &[qail_core::ast::Value]) -> Option<Vec<JsonValue>> {
589 if values.is_empty() {
590 return None;
591 }
592 if values.iter().all(|value| {
593 matches!(
594 value,
595 qail_core::ast::Value::String(_) | qail_core::ast::Value::Uuid(_)
596 )
597 }) || values
598 .iter()
599 .all(|value| matches!(value, qail_core::ast::Value::Int(_)))
600 {
601 return values.iter().map(value_to_json).collect();
602 }
603 None
604}
605
606fn value_to_json(value: &qail_core::ast::Value) -> Option<JsonValue> {
608 use qail_core::ast::Value;
609 match value {
610 Value::String(s) => Some(json!(s)),
611 Value::Uuid(u) => Some(json!(u.to_string())),
612 Value::Int(n) => Some(json!(n)),
613 _ => None,
614 }
615}
616
617pub fn decode_search_response(data: &[u8]) -> QdrantResult<Vec<ScoredPoint>> {
619 let response: JsonValue = serde_json::from_slice(data)
620 .map_err(|e| crate::error::QdrantError::Decode(e.to_string()))?;
621
622 let results = response["result"]
623 .as_array()
624 .ok_or_else(|| crate::error::QdrantError::Decode("Missing 'result' array".to_string()))?;
625
626 results
627 .iter()
628 .enumerate()
629 .map(|(idx, item)| {
630 let id = item.get("id").and_then(parse_point_id).ok_or_else(|| {
631 crate::error::QdrantError::Decode(format!("Missing point id at result index {idx}"))
632 })?;
633 let score = item
634 .get("score")
635 .and_then(JsonValue::as_f64)
636 .filter(|score| score.is_finite())
637 .ok_or_else(|| {
638 crate::error::QdrantError::Decode(format!(
639 "Invalid score at result index {idx}"
640 ))
641 })?;
642 let score = score as f32;
643 if !score.is_finite() {
644 return Err(crate::error::QdrantError::Decode(format!(
645 "Invalid score at result index {idx}"
646 )));
647 }
648 let payload = match item.get("payload") {
649 Some(payload) => parse_payload_checked(payload, idx)?,
650 None => crate::point::Payload::new(),
651 };
652 let vector = decode_result_vector(item.get("vector"), idx)?;
653
654 Ok(ScoredPoint {
655 id,
656 score,
657 payload,
658 vector,
659 })
660 })
661 .collect()
662}
663
664fn decode_result_vector(
665 value: Option<&JsonValue>,
666 result_idx: usize,
667) -> QdrantResult<Option<Vec<f32>>> {
668 let Some(value) = value else {
669 return Ok(None);
670 };
671 if value.is_null() {
672 return Ok(None);
673 }
674
675 if let Some(arr) = value.as_array() {
676 return decode_result_vector_array(arr, result_idx, "vector").map(Some);
677 }
678
679 if let Some(named) = value.as_object() {
680 return decode_named_result_vector(named, result_idx);
681 }
682
683 Err(crate::error::QdrantError::Decode(format!(
684 "Invalid vector at result index {result_idx}"
685 )))
686}
687
688fn decode_result_vector_array(
689 arr: &[JsonValue],
690 result_idx: usize,
691 label: &str,
692) -> QdrantResult<Vec<f32>> {
693 if arr.is_empty() {
694 return Err(crate::error::QdrantError::Decode(format!(
695 "Empty {label} at result index {result_idx}"
696 )));
697 }
698
699 let mut vector = Vec::with_capacity(arr.len());
700 for (idx, item) in arr.iter().enumerate() {
701 let value = item
702 .as_f64()
703 .filter(|value| value.is_finite())
704 .ok_or_else(|| {
705 crate::error::QdrantError::Decode(format!(
706 "Invalid {label} value at result index {result_idx}, vector index {idx}"
707 ))
708 })?;
709 let value = value as f32;
710 if !value.is_finite() {
711 return Err(crate::error::QdrantError::Decode(format!(
712 "Invalid {label} value at result index {result_idx}, vector index {idx}"
713 )));
714 }
715 vector.push(value);
716 }
717
718 Ok(vector)
719}
720
721fn decode_named_result_vector(
722 named: &serde_json::Map<String, JsonValue>,
723 result_idx: usize,
724) -> QdrantResult<Option<Vec<f32>>> {
725 if named.is_empty() {
726 return Ok(None);
727 }
728
729 let mut vector = None;
730 for (name, value) in named {
731 ensure_named_vector_name(name, &format!("search result {result_idx}"))
732 .map_err(crate::error::QdrantError::Decode)?;
733 let Some(arr) = value.as_array() else {
734 return Err(crate::error::QdrantError::Decode(format!(
735 "Invalid named vector '{name}' at result index {result_idx}"
736 )));
737 };
738 let next = decode_result_vector_array(arr, result_idx, &format!("named vector '{name}'"))?;
739 if vector.is_some() {
740 return Err(crate::error::QdrantError::Decode(format!(
741 "Multiple named vectors at result index {result_idx} cannot be represented as a single dense vector"
742 )));
743 }
744 vector = Some(next);
745 }
746
747 Ok(vector)
748}
749
750pub fn parse_point_id(value: &JsonValue) -> Option<PointId> {
752 if let Some(s) = value.as_str() {
753 if s.trim().is_empty() {
754 None
755 } else {
756 Some(PointId::Uuid(s.to_string()))
757 }
758 } else {
759 value.as_u64().map(PointId::Num)
760 }
761}
762
763pub fn parse_payload(value: &JsonValue) -> crate::point::Payload {
765 parse_payload_checked(value, 0).unwrap_or_default()
766}
767
768fn parse_payload_checked(
769 value: &JsonValue,
770 result_idx: usize,
771) -> QdrantResult<crate::point::Payload> {
772 let mut payload = crate::point::Payload::new();
773
774 match value {
775 JsonValue::Null => Ok(payload),
776 JsonValue::Object(obj) => {
777 for (key, value) in obj {
778 ensure_payload_key(key).map_err(crate::error::QdrantError::Decode)?;
779 let payload_value = json_to_payload_value_checked(
780 value,
781 &format!("result[{result_idx}].payload.{key}"),
782 )?;
783 payload.insert(key.clone(), payload_value);
784 }
785 Ok(payload)
786 }
787 _ => Err(crate::error::QdrantError::Decode(format!(
788 "Invalid payload object at result index {result_idx}"
789 ))),
790 }
791}
792
793fn payload_value_to_json(value: &PayloadValue) -> Result<JsonValue, String> {
795 match value {
796 PayloadValue::String(s) => Ok(json!(s)),
797 PayloadValue::Integer(n) => Ok(json!(n)),
798 PayloadValue::Float(f) if f.is_finite() => Ok(json!(f)),
799 PayloadValue::Float(f) => Err(format!(
800 "Qdrant payload contains non-finite float value: {f}"
801 )),
802 PayloadValue::Bool(b) => Ok(json!(b)),
803 PayloadValue::List(arr) => {
804 let values: Result<Vec<JsonValue>, String> =
805 arr.iter().map(payload_value_to_json).collect();
806 Ok(JsonValue::Array(values?))
807 }
808 PayloadValue::Object(obj) => Ok(JsonValue::Object(payload_to_json_map(obj)?)),
809 PayloadValue::Null => Ok(JsonValue::Null),
810 }
811}
812
813fn payload_to_json_map(
814 payload: &crate::point::Payload,
815) -> Result<serde_json::Map<String, JsonValue>, String> {
816 payload
817 .iter()
818 .map(|(k, v)| {
819 ensure_payload_key(k)?;
820 Ok((k.clone(), payload_value_to_json(v)?))
821 })
822 .collect()
823}
824
825fn json_to_payload_value_checked(value: &JsonValue, path: &str) -> QdrantResult<PayloadValue> {
826 match value {
827 JsonValue::Null => Ok(PayloadValue::Null),
828 JsonValue::Bool(b) => Ok(PayloadValue::Bool(*b)),
829 JsonValue::Number(n) => {
830 if let Some(i) = n.as_i64() {
831 Ok(PayloadValue::Integer(i))
832 } else if let Some(u) = n.as_u64() {
833 let i = i64::try_from(u).map_err(|_| {
834 crate::error::QdrantError::Decode(format!(
835 "Payload integer out of range at {path}"
836 ))
837 })?;
838 Ok(PayloadValue::Integer(i))
839 } else {
840 let f = n
841 .as_f64()
842 .filter(|value| value.is_finite())
843 .ok_or_else(|| {
844 crate::error::QdrantError::Decode(format!(
845 "Invalid payload number at {path}"
846 ))
847 })?;
848 Ok(PayloadValue::Float(f))
849 }
850 }
851 JsonValue::String(s) => Ok(PayloadValue::String(s.clone())),
852 JsonValue::Array(arr) => {
853 let mut items = Vec::with_capacity(arr.len());
854 for (idx, value) in arr.iter().enumerate() {
855 items.push(json_to_payload_value_checked(
856 value,
857 &format!("{path}[{idx}]"),
858 )?);
859 }
860 Ok(PayloadValue::List(items))
861 }
862 JsonValue::Object(obj) => {
863 let mut map = std::collections::HashMap::with_capacity(obj.len());
864 for (key, value) in obj {
865 ensure_payload_key(key).map_err(crate::error::QdrantError::Decode)?;
866 map.insert(
867 key.clone(),
868 json_to_payload_value_checked(value, &format!("{path}.{key}"))?,
869 );
870 }
871 Ok(PayloadValue::Object(map))
872 }
873 }
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879
880 #[test]
881 fn test_encode_search_request() {
882 let vector = vec![0.1, 0.2, 0.3];
883 let json_bytes = encode_search_request(&vector, 10, None, None, false);
884 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
885
886 assert!(json["vector"].is_array());
888 assert_eq!(json["limit"], 10);
889 assert_eq!(json["with_payload"], true);
890
891 assert_eq!(json["vector"].as_array().unwrap().len(), 3);
893 }
894
895 #[test]
896 fn test_encode_upsert_request() {
897 let point = Point::new("test-id", vec![0.5, 0.5]);
898 let json_bytes = encode_upsert_request(&[point]);
899 let json_str = String::from_utf8(json_bytes).unwrap();
900
901 assert!(json_str.contains("\"points\""));
902 assert!(json_str.contains("\"test-id\""));
903 assert!(json_str.contains("[0.5,0.5]"));
904 }
905
906 #[test]
907 fn encode_search_request_rejects_non_finite_vector_json() {
908 let json_bytes = encode_search_request(&[0.1, f32::NAN], 10, None, None, false);
909 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
910
911 assert!(
912 json["error"]
913 .as_str()
914 .unwrap()
915 .contains("non-finite vector value")
916 );
917 }
918
919 #[test]
920 fn encode_search_request_rejects_empty_vector_and_zero_limit_json() {
921 let json_bytes = encode_search_request(&[], 10, None, None, false);
922 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
923 assert!(
924 json["error"]
925 .as_str()
926 .unwrap()
927 .contains("must not be empty")
928 );
929
930 let json_bytes = encode_search_request(&[0.1], 0, None, None, false);
931 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
932 assert!(json["error"].as_str().unwrap().contains("limit"));
933 }
934
935 #[test]
936 fn encode_search_request_rejects_non_finite_threshold_json() {
937 let json_bytes = encode_search_request(&[0.1], 10, None, Some(f32::INFINITY), false);
938 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
939
940 assert!(json["error"].as_str().unwrap().contains("score threshold"));
941 }
942
943 #[test]
944 fn encode_upsert_request_rejects_non_finite_payload_json() {
945 let point = Point::new("test-id", vec![0.5, 0.5])
946 .with_payload("score", PayloadValue::Float(f64::INFINITY));
947 let json_bytes = encode_upsert_request(&[point]);
948 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
949
950 assert!(json["error"].as_str().unwrap().contains("non-finite float"));
951 }
952
953 #[test]
954 fn encode_upsert_request_rejects_empty_point_list_json() {
955 let json_bytes = encode_upsert_request(&[]);
956 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
957
958 assert!(json["error"].as_str().unwrap().contains("point list"));
959 }
960
961 #[test]
962 fn encode_upsert_request_rejects_empty_payload_keys_json() {
963 let mut nested = crate::point::Payload::new();
964 nested.insert("".to_string(), PayloadValue::String("bad".to_string()));
965 let point = Point::new("test-id", vec![0.5, 0.5])
966 .with_payload(" ", PayloadValue::String("bad".to_string()));
967 let json_bytes = encode_upsert_request(&[point]);
968 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
969 assert!(json["error"].as_str().unwrap().contains("field name"));
970
971 let point = Point::new("test-id", vec![0.5, 0.5])
972 .with_payload("metadata", PayloadValue::Object(nested));
973 let json_bytes = encode_upsert_request(&[point]);
974 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
975 assert!(json["error"].as_str().unwrap().contains("field name"));
976 }
977
978 #[test]
979 fn encode_upsert_request_rejects_empty_point_ids_json() {
980 let point = Point::new(PointId::Uuid(" ".to_string()), vec![0.5, 0.5]);
981 let json_bytes = encode_upsert_request(&[point]);
982 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
983
984 assert!(json["error"].as_str().unwrap().contains("point id"));
985 }
986
987 #[test]
988 fn encode_multi_vector_request_rejects_non_finite_vector_json() {
989 let point = crate::point::MultiVectorPoint::new("test-id")
990 .with_vector("image", vec![0.5, f32::NEG_INFINITY]);
991 let json_bytes = encode_upsert_multi_vector_request(&[point]);
992 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
993
994 assert!(
995 json["error"]
996 .as_str()
997 .unwrap()
998 .contains("non-finite vector value")
999 );
1000 }
1001
1002 #[test]
1003 fn encode_multi_vector_request_rejects_empty_named_vector_set_json() {
1004 let point = crate::point::MultiVectorPoint::new("test-id");
1005 let json_bytes = encode_upsert_multi_vector_request(&[point]);
1006 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1007
1008 assert!(
1009 json["error"]
1010 .as_str()
1011 .unwrap()
1012 .contains("at least one named vector")
1013 );
1014 }
1015
1016 #[test]
1017 fn encode_multi_vector_request_rejects_empty_point_list_json() {
1018 let json_bytes = encode_upsert_multi_vector_request(&[]);
1019 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1020
1021 assert!(json["error"].as_str().unwrap().contains("point list"));
1022 }
1023
1024 #[test]
1025 fn encode_multi_vector_request_rejects_empty_payload_keys_json() {
1026 let point = crate::point::MultiVectorPoint::new("test-id")
1027 .with_vector("image", vec![0.1, 0.2])
1028 .with_payload("", PayloadValue::String("bad".to_string()));
1029 let json_bytes = encode_upsert_multi_vector_request(&[point]);
1030 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1031
1032 assert!(json["error"].as_str().unwrap().contains("field name"));
1033 }
1034
1035 #[test]
1036 fn encode_multi_vector_request_rejects_empty_named_vector_name_json() {
1037 let point =
1038 crate::point::MultiVectorPoint::new("test-id").with_vector(" ", vec![0.1, 0.2]);
1039 let json_bytes = encode_upsert_multi_vector_request(&[point]);
1040 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1041
1042 assert!(
1043 json["error"]
1044 .as_str()
1045 .unwrap()
1046 .contains("vector name must not be empty")
1047 );
1048 }
1049
1050 #[test]
1051 fn encode_multi_vector_request_rejects_empty_point_ids_json() {
1052 let point = crate::point::MultiVectorPoint::new(PointId::Uuid("".to_string()))
1053 .with_vector("image", vec![0.1, 0.2]);
1054 let json_bytes = encode_upsert_multi_vector_request(&[point]);
1055 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1056
1057 assert!(json["error"].as_str().unwrap().contains("point id"));
1058 }
1059
1060 #[test]
1061 fn encode_create_collection_request_rejects_zero_vector_size_json() {
1062 let json_bytes = encode_create_collection_request(0, "Cosine");
1063 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1064
1065 assert!(json["error"].as_str().unwrap().contains("vector_size"));
1066 }
1067
1068 #[test]
1069 fn encode_create_collection_request_validates_distance_json() {
1070 let json_bytes = encode_create_collection_request(32, "cosine");
1071 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1072 assert_eq!(json["vectors"]["distance"], "Cosine");
1073
1074 let json_bytes = encode_create_collection_request(32, "bad-distance");
1075 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1076 assert!(json["error"].as_str().unwrap().contains("distance"));
1077 }
1078
1079 #[test]
1080 fn test_encode_delete_request() {
1081 let ids = vec![PointId::Uuid("id1".to_string()), PointId::Num(42)];
1082 let json_bytes = encode_delete_request(&ids);
1083 let json_str = String::from_utf8(json_bytes).unwrap();
1084
1085 assert!(json_str.contains("\"id1\""));
1086 assert!(json_str.contains("42"));
1087 }
1088
1089 #[test]
1090 fn encode_delete_request_rejects_empty_id_list_json() {
1091 let json_bytes = encode_delete_request(&[]);
1092 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1093
1094 assert!(json["error"].as_str().unwrap().contains("point id list"));
1095
1096 let json_bytes = encode_delete_request(&[PointId::Uuid(" ".to_string())]);
1097 let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
1098 assert!(json["error"].as_str().unwrap().contains("point id"));
1099 }
1100
1101 #[test]
1102 fn test_decode_search_response() {
1103 let response = r#"{
1104 "result": [
1105 {"id": "abc", "score": 0.95, "payload": {"name": "test"}},
1106 {"id": 123, "score": 0.80, "payload": {}, "vector": [0.1, 0.2]}
1107 ]
1108 }"#;
1109
1110 let results = decode_search_response(response.as_bytes()).unwrap();
1111 assert_eq!(results.len(), 2);
1112 assert_eq!(results[0].score, 0.95);
1113 assert_eq!(results[1].score, 0.80);
1114 assert_eq!(results[1].vector.as_deref(), Some(&[0.1, 0.2][..]));
1115 }
1116
1117 #[test]
1118 fn decode_search_response_accepts_single_named_vector_json() {
1119 let response = r#"{
1120 "result": [
1121 {"id": "abc", "score": 0.95, "payload": {}, "vector": {"image": [0.1, 0.2]}}
1122 ]
1123 }"#;
1124
1125 let results = decode_search_response(response.as_bytes()).unwrap();
1126
1127 assert_eq!(results.len(), 1);
1128 assert_eq!(results[0].vector.as_deref(), Some(&[0.1, 0.2][..]));
1129 }
1130
1131 #[test]
1132 fn decode_search_response_rejects_ambiguous_named_vectors_json() {
1133 let response = r#"{
1134 "result": [
1135 {"id": "abc", "score": 0.95, "payload": {}, "vector": {"image": [0.1, 0.2], "text": [0.3, 0.4]}}
1136 ]
1137 }"#;
1138
1139 let err = decode_search_response(response.as_bytes())
1140 .expect_err("multiple named vectors should fail closed");
1141
1142 assert!(err.to_string().contains("Multiple named vectors"));
1143 }
1144
1145 #[test]
1146 fn decode_search_response_rejects_malformed_results() {
1147 let missing_id = r#"{
1148 "result": [
1149 {"score": 0.95, "payload": {"name": "test"}}
1150 ]
1151 }"#;
1152 let err = decode_search_response(missing_id.as_bytes())
1153 .expect_err("missing id should fail closed");
1154 assert!(err.to_string().contains("Missing point id"));
1155
1156 let bad_vector = r#"{
1157 "result": [
1158 {"id": "abc", "score": 0.95, "payload": {}, "vector": [0.1, "oops"]}
1159 ]
1160 }"#;
1161 let err = decode_search_response(bad_vector.as_bytes())
1162 .expect_err("bad vector should fail closed");
1163 assert!(err.to_string().contains("Invalid vector value"));
1164
1165 let empty_vector = r#"{
1166 "result": [
1167 {"id": "abc", "score": 0.95, "payload": {}, "vector": []}
1168 ]
1169 }"#;
1170 let err = decode_search_response(empty_vector.as_bytes())
1171 .expect_err("empty vector should fail closed");
1172 assert!(err.to_string().contains("Empty vector"));
1173
1174 let empty_id = r#"{
1175 "result": [
1176 {"id": "", "score": 0.95, "payload": {}}
1177 ]
1178 }"#;
1179 let err =
1180 decode_search_response(empty_id.as_bytes()).expect_err("empty id should fail closed");
1181 assert!(err.to_string().contains("Missing point id"));
1182
1183 let huge_score = r#"{
1184 "result": [
1185 {"id": "abc", "score": 1e100, "payload": {}}
1186 ]
1187 }"#;
1188 let err = decode_search_response(huge_score.as_bytes())
1189 .expect_err("score that overflows f32 should fail closed");
1190 assert!(err.to_string().contains("Invalid score"));
1191 }
1192
1193 #[test]
1194 fn decode_search_response_rejects_malformed_payload() {
1195 let payload_array = r#"{
1196 "result": [
1197 {"id": "abc", "score": 0.95, "payload": ["not", "an", "object"]}
1198 ]
1199 }"#;
1200 let err = decode_search_response(payload_array.as_bytes())
1201 .expect_err("payload array should fail closed");
1202 assert!(err.to_string().contains("Invalid payload object"));
1203
1204 let oversized_integer = r#"{
1205 "result": [
1206 {"id": "abc", "score": 0.95, "payload": {"too_big": 18446744073709551615}}
1207 ]
1208 }"#;
1209 let err = decode_search_response(oversized_integer.as_bytes())
1210 .expect_err("payload integer overflow should fail closed");
1211 assert!(err.to_string().contains("Payload integer out of range"));
1212
1213 let empty_payload_key = r#"{
1214 "result": [
1215 {"id": "abc", "score": 0.95, "payload": {"": "bad"}}
1216 ]
1217 }"#;
1218 let err = decode_search_response(empty_payload_key.as_bytes())
1219 .expect_err("empty payload key should fail closed");
1220 assert!(err.to_string().contains("field name"));
1221
1222 let nested_empty_payload_key = r#"{
1223 "result": [
1224 {"id": "abc", "score": 0.95, "payload": {"meta": {" ": "bad"}}}
1225 ]
1226 }"#;
1227 let err = decode_search_response(nested_empty_payload_key.as_bytes())
1228 .expect_err("nested empty payload key should fail closed");
1229 assert!(err.to_string().contains("field name"));
1230 }
1231
1232 #[test]
1233 fn test_encode_conditions_to_filter() {
1234 use qail_core::ast::{Condition, Expr, Operator, Value};
1235
1236 let conditions = vec![
1237 Condition {
1238 left: Expr::Named("category".to_string()),
1239 op: Operator::Eq,
1240 value: Value::String("electronics".to_string()),
1241 is_array_unnest: false,
1242 },
1243 Condition {
1244 left: Expr::Named("price".to_string()),
1245 op: Operator::Lt,
1246 value: Value::Int(1000),
1247 is_array_unnest: false,
1248 },
1249 ];
1250
1251 let filter = encode_conditions_to_filter(&conditions, false);
1252
1253 assert!(filter["must"].is_array());
1255 let must = filter["must"].as_array().unwrap();
1256 assert_eq!(must.len(), 2);
1257
1258 assert_eq!(must[0]["key"], "category");
1260 assert_eq!(must[0]["match"]["value"], "electronics");
1261
1262 assert_eq!(must[1]["key"], "price");
1264 assert_eq!(must[1]["range"]["lt"], 1000);
1265 }
1266
1267 #[test]
1268 fn test_encode_conditions_to_filter_or() {
1269 use qail_core::ast::{Condition, Expr, Operator, Value};
1270
1271 let conditions = vec![Condition {
1272 left: Expr::Named("status".to_string()),
1273 op: Operator::Eq,
1274 value: Value::String("active".to_string()),
1275 is_array_unnest: false,
1276 }];
1277
1278 let filter = encode_conditions_to_filter(&conditions, true);
1279
1280 assert!(filter["should"].is_array());
1282 assert!(filter["must"].is_null());
1283 }
1284
1285 #[test]
1286 fn encode_conditions_to_filter_uses_has_id_for_point_id_json() {
1287 use qail_core::ast::{Condition, Expr, Operator, Value};
1288
1289 let conditions = vec![Condition {
1290 left: Expr::Named("ID".to_string()),
1291 op: Operator::Eq,
1292 value: Value::Int(42),
1293 is_array_unnest: false,
1294 }];
1295
1296 let filter = encode_conditions_to_filter(&conditions, false);
1297
1298 assert_eq!(filter["must"][0]["has_id"][0], 42);
1299 assert!(filter["must"][0]["key"].is_null());
1300 }
1301
1302 #[test]
1303 fn encode_conditions_to_filter_supports_native_in_json() {
1304 use qail_core::ast::{Condition, Expr, Operator, Value};
1305
1306 let owner_id = uuid::Uuid::parse_str("aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa").unwrap();
1307 let reviewer_id = uuid::Uuid::parse_str("bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb").unwrap();
1308 let conditions = vec![
1309 Condition {
1310 left: Expr::Named("status".to_string()),
1311 op: Operator::In,
1312 value: Value::Array(vec![
1313 Value::String("open".to_string()),
1314 Value::String("closed".to_string()),
1315 ]),
1316 is_array_unnest: false,
1317 },
1318 Condition {
1319 left: Expr::Named("priority".to_string()),
1320 op: Operator::In,
1321 value: Value::Array(vec![Value::Int(1), Value::Int(2)]),
1322 is_array_unnest: false,
1323 },
1324 Condition {
1325 left: Expr::Named("owner_id".to_string()),
1326 op: Operator::Eq,
1327 value: Value::Uuid(owner_id),
1328 is_array_unnest: false,
1329 },
1330 Condition {
1331 left: Expr::Named("reviewer_id".to_string()),
1332 op: Operator::In,
1333 value: Value::Array(vec![
1334 Value::Uuid(reviewer_id),
1335 Value::String("external-reviewer".to_string()),
1336 ]),
1337 is_array_unnest: false,
1338 },
1339 Condition {
1340 left: Expr::Named("ID".to_string()),
1341 op: Operator::In,
1342 value: Value::Array(vec![
1343 Value::Int(42),
1344 Value::String("uuid-like-id".to_string()),
1345 ]),
1346 is_array_unnest: false,
1347 },
1348 ];
1349
1350 let filter = encode_conditions_to_filter(&conditions, false);
1351
1352 assert_eq!(filter["must"][0]["match"]["any"], json!(["open", "closed"]));
1353 assert_eq!(filter["must"][1]["match"]["any"], json!([1, 2]));
1354 assert_eq!(filter["must"][2]["match"]["value"], owner_id.to_string());
1355 assert_eq!(
1356 filter["must"][3]["match"]["any"],
1357 json!([reviewer_id.to_string(), "external-reviewer"])
1358 );
1359 assert_eq!(filter["must"][4]["has_id"], json!([42, "uuid-like-id"]));
1360 }
1361
1362 #[test]
1363 fn encode_conditions_to_filter_supports_native_negative_filters_json() {
1364 use qail_core::ast::{Condition, Expr, Operator, Value};
1365
1366 let conditions = vec![
1367 Condition {
1368 left: Expr::Named("status".to_string()),
1369 op: Operator::Ne,
1370 value: Value::String("deleted".to_string()),
1371 is_array_unnest: false,
1372 },
1373 Condition {
1374 left: Expr::Named("priority".to_string()),
1375 op: Operator::NotIn,
1376 value: Value::Array(vec![Value::Int(1), Value::Int(2)]),
1377 is_array_unnest: false,
1378 },
1379 Condition {
1380 left: Expr::Named("deleted_at".to_string()),
1381 op: Operator::IsNotNull,
1382 value: Value::Null,
1383 is_array_unnest: false,
1384 },
1385 Condition {
1386 left: Expr::Named("summary".to_string()),
1387 op: Operator::NotLike,
1388 value: Value::String("refund".to_string()),
1389 is_array_unnest: false,
1390 },
1391 Condition {
1392 left: Expr::Named("id".to_string()),
1393 op: Operator::NotIn,
1394 value: Value::Array(vec![Value::Int(42), Value::String("uuid-like-id".into())]),
1395 is_array_unnest: false,
1396 },
1397 ];
1398
1399 let filter = encode_conditions_to_filter(&conditions, false);
1400
1401 assert_eq!(filter["must"], json!([]));
1402 assert_eq!(filter["must_not"][0]["match"]["value"], "deleted");
1403 assert_eq!(filter["must_not"][1]["match"]["any"], json!([1, 2]));
1404 assert_eq!(filter["must_not"][2]["is_null"]["key"], "deleted_at");
1405 assert_eq!(filter["must_not"][3]["match"]["text"], "refund");
1406 assert_eq!(filter["must_not"][4]["has_id"], json!([42, "uuid-like-id"]));
1407
1408 let filter = encode_conditions_to_filter(&conditions[..1], true);
1409
1410 assert_eq!(
1411 filter["should"][0]["must_not"][0]["match"]["value"],
1412 "deleted"
1413 );
1414 }
1415
1416 #[test]
1417 fn encode_conditions_to_filter_supports_null_uuid_as_is_null_json() {
1418 use qail_core::ast::{Condition, Expr, Operator, Value};
1419
1420 let conditions = vec![Condition {
1421 left: Expr::Named("deleted_at".to_string()),
1422 op: Operator::IsNull,
1423 value: Value::NullUuid,
1424 is_array_unnest: false,
1425 }];
1426
1427 let filter = encode_conditions_to_filter(&conditions, false);
1428
1429 assert_eq!(filter["must"][0]["is_null"]["key"], "deleted_at");
1430 }
1431
1432 #[test]
1433 fn encode_conditions_to_filter_fails_closed_on_unsupported_operator_json() {
1434 use qail_core::ast::{Condition, Expr, Operator, Value};
1435
1436 let conditions = vec![Condition {
1437 left: Expr::Named("status".to_string()),
1438 op: Operator::NotILike,
1439 value: Value::String("deleted".to_string()),
1440 is_array_unnest: false,
1441 }];
1442
1443 let filter = encode_conditions_to_filter(&conditions, false);
1444
1445 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1446 }
1447
1448 #[test]
1449 fn encode_conditions_to_filter_fails_closed_on_is_not_null_json() {
1450 use qail_core::ast::{Condition, Expr, Operator, Value};
1451
1452 let conditions = vec![Condition {
1453 left: Expr::Named("deleted_at".to_string()),
1454 op: Operator::IsNotNull,
1455 value: Value::String("not-null".to_string()),
1456 is_array_unnest: false,
1457 }];
1458
1459 let filter = encode_conditions_to_filter(&conditions, false);
1460
1461 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1462 }
1463
1464 #[test]
1465 fn encode_conditions_to_filter_fails_closed_on_unrepresentable_condition() {
1466 use qail_core::ast::{Condition, Expr, Operator, Value};
1467
1468 let conditions = vec![
1469 Condition {
1470 left: Expr::Named("tenant_id".to_string()),
1471 op: Operator::Eq,
1472 value: Value::String("tenant-a".to_string()),
1473 is_array_unnest: false,
1474 },
1475 Condition {
1476 left: Expr::Literal(Value::String("not-a-field".to_string())),
1477 op: Operator::Eq,
1478 value: Value::String("tenant-b".to_string()),
1479 is_array_unnest: false,
1480 },
1481 ];
1482
1483 let filter = encode_conditions_to_filter(&conditions, false);
1484
1485 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1486 assert_eq!(filter["must"][0]["range"]["gt"], 1);
1487 assert_eq!(filter["must"][0]["range"]["lt"], 0);
1488 }
1489
1490 #[test]
1491 fn encode_conditions_to_filter_fails_closed_on_bad_in_value() {
1492 use qail_core::ast::{Condition, Expr, Operator, Value};
1493
1494 let conditions = vec![Condition {
1495 left: Expr::Named("category".to_string()),
1496 op: Operator::In,
1497 value: Value::Array(vec![
1498 Value::String("a".to_string()),
1499 Value::Vector(vec![1.0, 2.0]),
1500 ]),
1501 is_array_unnest: false,
1502 }];
1503
1504 let filter = encode_conditions_to_filter(&conditions, false);
1505
1506 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1507 assert!(filter.to_string().contains("\"gt\":1"));
1508
1509 let conditions = vec![Condition {
1510 left: Expr::Named("category".to_string()),
1511 op: Operator::In,
1512 value: Value::Array(vec![]),
1513 is_array_unnest: false,
1514 }];
1515 let filter = encode_conditions_to_filter(&conditions, false);
1516 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1517
1518 let conditions = vec![Condition {
1519 left: Expr::Named("category".to_string()),
1520 op: Operator::In,
1521 value: Value::Array(vec![Value::Null]),
1522 is_array_unnest: false,
1523 }];
1524 let filter = encode_conditions_to_filter(&conditions, false);
1525 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1526
1527 let conditions = vec![Condition {
1528 left: Expr::Named("category".to_string()),
1529 op: Operator::In,
1530 value: Value::Array(vec![Value::String("a".to_string()), Value::Int(1)]),
1531 is_array_unnest: false,
1532 }];
1533 let filter = encode_conditions_to_filter(&conditions, false);
1534 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1535
1536 let conditions = vec![Condition {
1537 left: Expr::Named("category".to_string()),
1538 op: Operator::In,
1539 value: Value::Array(vec![Value::Bool(true)]),
1540 is_array_unnest: false,
1541 }];
1542 let filter = encode_conditions_to_filter(&conditions, false);
1543 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1544 }
1545
1546 #[test]
1547 fn encode_conditions_to_filter_fails_closed_on_non_finite_float() {
1548 use qail_core::ast::{Condition, Expr, Operator, Value};
1549
1550 let conditions = vec![Condition {
1551 left: Expr::Named("score".to_string()),
1552 op: Operator::Gt,
1553 value: Value::Float(f64::NAN),
1554 is_array_unnest: false,
1555 }];
1556
1557 let filter = encode_conditions_to_filter(&conditions, false);
1558
1559 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1560 assert!(!filter.to_string().contains("null"));
1561
1562 let conditions = vec![Condition {
1563 left: Expr::Named("score".to_string()),
1564 op: Operator::Eq,
1565 value: Value::Float(1.5),
1566 is_array_unnest: false,
1567 }];
1568
1569 let filter = encode_conditions_to_filter(&conditions, false);
1570
1571 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1572 }
1573
1574 #[test]
1575 fn encode_conditions_to_filter_rejects_empty_id_text_and_quoted_fields() {
1576 use qail_core::ast::{Condition, Expr, Operator, Value};
1577
1578 let conditions = vec![Condition {
1579 left: Expr::Named("id".to_string()),
1580 op: Operator::Eq,
1581 value: Value::String(" ".to_string()),
1582 is_array_unnest: false,
1583 }];
1584 let filter = encode_conditions_to_filter(&conditions, false);
1585 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1586
1587 let conditions = vec![Condition {
1588 left: Expr::Named("\" \"".to_string()),
1589 op: Operator::Eq,
1590 value: Value::String("active".to_string()),
1591 is_array_unnest: false,
1592 }];
1593 let filter = encode_conditions_to_filter(&conditions, false);
1594 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1595
1596 let conditions = vec![Condition {
1597 left: Expr::Named("description".to_string()),
1598 op: Operator::Contains,
1599 value: Value::String(" ".to_string()),
1600 is_array_unnest: false,
1601 }];
1602 let filter = encode_conditions_to_filter(&conditions, false);
1603 assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
1604 }
1605}