Skip to main content

qail_qdrant/
protocol.rs

1//! Qdrant REST/JSON protocol encoding.
2//!
3//! This module handles encoding QAIL AST to Qdrant's REST API JSON format.
4//! Using JSON instead of gRPC for simplicity and portability.
5
6use 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
90/// Encode a vector search request to JSON format.
91///
92/// Generates JSON for POST /collections/{collection}/points/search
93///
94/// Example output:
95/// ```json
96/// {
97///   "vector": [0.1, 0.2, 0.3],
98///   "limit": 10,
99///   "offset": 0,
100///   "with_payload": true,
101///   "filter": { ... }
102/// }
103/// ```
104pub 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
140/// Encode search request with filter conditions.
141pub 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
179/// Encode an upsert (insert/update) request to JSON.
180///
181/// Generates JSON for PUT /collections/{collection}/points
182///
183/// Example output:
184/// ```json
185/// {
186///   "points": [
187///     { "id": "abc123", "vector": [0.1, 0.2], "payload": {"name": "test"} }
188///   ]
189/// }
190/// ```
191pub 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
225/// Encode an upsert request for multi-vector points (named vectors).
226///
227/// For collections with multiple vector fields (e.g., "title", "content").
228pub 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            // Named vectors as object
252            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
280/// Encode a delete request to JSON.
281///
282/// Generates JSON for POST /collections/{collection}/points/delete
283///
284/// Example output:
285/// ```json
286/// { "points": ["id1", "id2"] }
287/// ```
288pub 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
311/// Encode create collection request.
312///
313/// Generates JSON for PUT /collections/{collection}
314pub fn encode_create_collection_request(
315    vector_size: u64,
316    distance: &str, // "Cosine", "Euclidean", "Dot"
317) -> 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
334/// Convert QAIL conditions to Qdrant filter format.
335///
336/// Qdrant uses `must`, `should`, `must_not` arrays for filtering.
337/// Each condition becomes a clause in `must` (AND logic).
338///
339/// # Example
340/// ```ignore
341/// use qail_core::ast::{Condition, Operator, Expr, Value};
342///
343/// let conditions = vec![
344///     Condition { left: Expr::Named("category".into()), op: Operator::Eq, value: Value::String("electronics".into()), is_array_unnest: false },
345///     Condition { left: Expr::Named("price".into()), op: Operator::Lt, value: Value::Int(1000), is_array_unnest: false },
346/// ];
347///
348/// let filter = encode_conditions_to_filter(&conditions, false);
349/// // Returns: {"must": [{"key": "category", "match": {"value": "electronics"}}, {"key": "price", "range": {"lt": 1000}}]}
350/// ```
351pub 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        // Extract field name from left expression
361        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        // Convert operator and value to Qdrant filter clause
402        let clause = match (&cond.op, &cond.value) {
403            // Match (equality)
404            (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            // Range operators
450            (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            // In / NotIn (array membership)
484            (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            // IsNull / IsNotNull
505            (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            // Text/keyword match with contains
516            (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    // Use "should" for OR, "must" for AND
537    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
606/// Convert Value to JsonValue for filter match-any encoding.
607fn 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
617/// Decode search response from JSON.
618pub 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
750/// Parse a point ID from JSON.
751pub 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
763/// Parse payload from JSON object.
764pub 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
793/// Convert PayloadValue to JSON.
794fn 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        // Check structure exists
887        assert!(json["vector"].is_array());
888        assert_eq!(json["limit"], 10);
889        assert_eq!(json["with_payload"], true);
890
891        // Check vector length
892        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        // Should have "must" with 2 clauses
1254        assert!(filter["must"].is_array());
1255        let must = filter["must"].as_array().unwrap();
1256        assert_eq!(must.len(), 2);
1257
1258        // First clause: category match
1259        assert_eq!(must[0]["key"], "category");
1260        assert_eq!(must[0]["match"]["value"], "electronics");
1261
1262        // Second clause: price range
1263        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        // Should have "should" instead of "must"
1281        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}