Skip to main content

qail_qdrant/
encoder.rs

1//! Zero-copy Protobuf encoder for Qdrant gRPC protocol.
2//!
3//! This module implements direct wire format encoding without intermediate
4//! struct allocations. Key optimizations:
5//! - Pre-computed field tag bytes
6//! - Buffer reuse via BytesMut
7//! - Direct memcpy for vectors (no per-element loop)
8//!
9//! ## Supported Operations
10//! - Search (with filters)
11//! - Upsert (with payload)
12//! - Delete points (numeric + UUID)
13//! - Get points by ID
14//! - Scroll (paginated iteration)
15//! - Create / Delete collection
16//! - Update payload
17//! - Create field index
18
19use crate::error::{QdrantError, QdrantResult};
20use bytes::{BufMut, BytesMut};
21
22// ============================================================================
23// SearchPoints Field Tags (pre-computed)
24// ============================================================================
25// Tag = (field_number << 3) | wire_type
26
27/// Field 1: collection_name (string) -> (1 << 3) | 2 = 0x0A
28const SEARCH_COLLECTION: u8 = 0x0A;
29/// Field 2: vector (repeated float, packed) -> (2 << 3) | 2 = 0x12
30const SEARCH_VECTOR: u8 = 0x12;
31/// Field 3: filter (message) -> (3 << 3) | 2 = 0x1A
32const SEARCH_FILTER: u8 = 0x1A;
33/// Field 4: limit (uint64) -> (4 << 3) | 0 = 0x20
34const SEARCH_LIMIT: u8 = 0x20;
35/// Field 6: with_payload (message) -> (6 << 3) | 2 = 0x32
36const SEARCH_WITH_PAYLOAD: u8 = 0x32;
37/// Field 8: score_threshold (float) -> (8 << 3) | 5 = 0x45
38const SEARCH_SCORE_THRESHOLD: u8 = 0x45;
39/// Field 10: vector_name (string) -> (10 << 3) | 2 = 0x52
40const SEARCH_VECTOR_NAME: u8 = 0x52;
41/// Field 11: with_vectors (message) -> (11 << 3) | 2 = 0x5A
42const SEARCH_WITH_VECTORS: u8 = 0x5A;
43
44// ============================================================================
45// ScrollPoints Field Tags
46// ============================================================================
47
48/// Field 1: collection_name (string) -> 0x0A
49const SCROLL_COLLECTION: u8 = 0x0A;
50/// Field 2: filter (message) -> 0x12
51const SCROLL_FILTER: u8 = 0x12;
52/// Field 3: offset (PointId) -> 0x1A
53const SCROLL_OFFSET: u8 = 0x1A;
54/// Field 4: limit (uint32) -> 0x20
55const SCROLL_LIMIT: u8 = 0x20;
56/// Field 6: with_payload selector -> 0x32
57const SCROLL_WITH_PAYLOAD: u8 = 0x32;
58/// Field 7: with_vectors selector -> 0x3A
59const SCROLL_WITH_VECTORS: u8 = 0x3A;
60
61// ============================================================================
62// UpsertPoints Field Tags
63// ============================================================================
64
65/// Field 1: collection_name (string) -> 0x0A
66const UPSERT_COLLECTION: u8 = 0x0A;
67/// Field 2: wait (bool) -> (2 << 3) | 0 = 0x10
68const UPSERT_WAIT: u8 = 0x10;
69/// Field 3: points (repeated PointStruct) -> (3 << 3) | 2 = 0x1A
70const UPSERT_POINTS: u8 = 0x1A;
71
72// ============================================================================
73// PointStruct Field Tags
74// ============================================================================
75
76/// Field 1: id (PointId) -> 0x0A
77const POINT_ID: u8 = 0x0A;
78/// Field 4: vectors (Vectors) -> (4 << 3) | 2 = 0x22 (field 2 is deprecated)
79const POINT_VECTORS: u8 = 0x22;
80/// Field 3: payload (map) -> (3 << 3) | 2 = 0x1A
81const POINT_PAYLOAD: u8 = 0x1A;
82
83// ============================================================================
84// PointId Field Tags
85// ============================================================================
86
87/// Field 1: num (uint64) -> (1 << 3) | 0 = 0x08
88const POINT_ID_NUM: u8 = 0x08;
89/// Field 2: uuid (string) -> (2 << 3) | 2 = 0x12
90const POINT_ID_UUID: u8 = 0x12;
91
92// ============================================================================
93// Filter Field Tags (qdrant.Filter message)
94// ============================================================================
95
96/// Filter.should (field 1, repeated Condition) -> 0x0A
97const FILTER_SHOULD: u8 = 0x0A;
98/// Filter.must (field 2, repeated Condition) -> 0x12
99const FILTER_MUST: u8 = 0x12;
100/// Filter.must_not (field 3, repeated Condition) -> 0x1A
101const FILTER_MUST_NOT: u8 = 0x1A;
102/// Condition.filter (field 4, nested Filter message) -> 0x22
103const CONDITION_FILTER: u8 = 0x22;
104/// Condition.has_id (field 3, HasIdCondition message) -> 0x1A
105const CONDITION_HAS_ID: u8 = 0x1A;
106/// Condition.is_null (field 5, IsNullCondition message) -> 0x2A
107const CONDITION_IS_NULL: u8 = 0x2A;
108
109// ============================================================================
110// Varint Encoding
111// ============================================================================
112
113/// Encode a varint (variable-length integer) into the buffer.
114/// Uses 7 bits per byte, MSB indicates continuation.
115#[inline]
116pub fn encode_varint(buf: &mut BytesMut, mut value: usize) {
117    loop {
118        let byte = (value & 0x7F) as u8;
119        value >>= 7;
120        if value == 0 {
121            buf.put_u8(byte);
122            break;
123        } else {
124            buf.put_u8(byte | 0x80);
125        }
126    }
127}
128
129/// Encode a u64 varint.
130#[inline]
131pub fn encode_varint_u64(buf: &mut BytesMut, mut value: u64) {
132    loop {
133        let byte = (value & 0x7F) as u8;
134        value >>= 7;
135        if value == 0 {
136            buf.put_u8(byte);
137            break;
138        } else {
139            buf.put_u8(byte | 0x80);
140        }
141    }
142}
143
144#[inline]
145fn extend_f32_le_slice(buf: &mut BytesMut, values: &[f32]) {
146    #[cfg(target_endian = "little")]
147    {
148        // Protobuf fixed32 values are little-endian. On little-endian targets,
149        // f32 memory layout already matches the wire format.
150        let float_bytes: &[u8] = bytemuck::cast_slice(values);
151        buf.extend_from_slice(float_bytes);
152    }
153
154    #[cfg(not(target_endian = "little"))]
155    {
156        for value in values {
157            buf.extend_from_slice(&value.to_le_bytes());
158        }
159    }
160}
161
162fn encode_error(message: impl Into<String>) -> QdrantError {
163    QdrantError::Encode(message.into())
164}
165
166fn ensure_non_empty_name(value: &str, label: &str) -> QdrantResult<()> {
167    if value.trim().is_empty() {
168        return Err(encode_error(format!("Qdrant {label} must not be empty")));
169    }
170    Ok(())
171}
172
173fn ensure_collection_name(collection: &str) -> QdrantResult<()> {
174    ensure_non_empty_name(collection, "collection name")
175}
176
177fn ensure_payload_key(key: &str) -> QdrantResult<()> {
178    ensure_non_empty_name(key, "payload field name")
179}
180
181fn ensure_vector_name(vector_name: Option<&str>) -> QdrantResult<()> {
182    if let Some(name) = vector_name {
183        ensure_non_empty_name(name, "vector name")?;
184    }
185    Ok(())
186}
187
188fn ensure_vector(label: &str, vector: &[f32]) -> QdrantResult<()> {
189    if vector.is_empty() {
190        return Err(encode_error(format!("Qdrant {label} must not be empty")));
191    }
192    if let Some((idx, value)) = vector
193        .iter()
194        .enumerate()
195        .find(|(_, value)| !value.is_finite())
196    {
197        return Err(encode_error(format!(
198            "Qdrant {label} contains non-finite vector value at index {idx}: {value}"
199        )));
200    }
201    Ok(())
202}
203
204fn ensure_search_limit(limit: u64) -> QdrantResult<()> {
205    if limit == 0 {
206        return Err(encode_error(
207            "Qdrant search limit must be greater than zero",
208        ));
209    }
210    Ok(())
211}
212
213fn ensure_scroll_limit(limit: u32) -> QdrantResult<()> {
214    if limit == 0 {
215        return Err(encode_error(
216            "Qdrant scroll limit must be greater than zero",
217        ));
218    }
219    Ok(())
220}
221
222fn ensure_score_threshold(score_threshold: Option<f32>) -> QdrantResult<()> {
223    if let Some(value) = score_threshold
224        && !value.is_finite()
225    {
226        return Err(encode_error(format!(
227            "Qdrant score threshold must be finite, got {value}"
228        )));
229    }
230    Ok(())
231}
232
233fn ensure_search_request(request: &SearchRequest<'_>) -> QdrantResult<()> {
234    ensure_collection_name(request.collection)?;
235    ensure_vector("search vector", request.vector)?;
236    ensure_search_limit(request.limit)?;
237    ensure_score_threshold(request.score_threshold)?;
238    ensure_vector_name(request.vector_name)
239}
240
241fn ensure_point_id(id: &crate::PointId, label: &str) -> QdrantResult<()> {
242    match id {
243        crate::PointId::Num(_) => Ok(()),
244        crate::PointId::Uuid(value) => ensure_non_empty_name(value, label),
245    }
246}
247
248fn ensure_point_ids(ids: &[crate::PointId], label: &str) -> QdrantResult<()> {
249    if ids.is_empty() {
250        return Err(encode_error(format!(
251            "Qdrant {label} point id list must not be empty"
252        )));
253    }
254    for id in ids {
255        ensure_point_id(id, label)?;
256    }
257    Ok(())
258}
259
260fn ensure_payload_value(value: &crate::point::PayloadValue, label: &str) -> QdrantResult<()> {
261    use crate::point::PayloadValue;
262
263    match value {
264        PayloadValue::Float(value) if !value.is_finite() => Err(encode_error(format!(
265            "Qdrant {label} contains non-finite payload float: {value}"
266        ))),
267        PayloadValue::List(items) => {
268            for item in items {
269                ensure_payload_value(item, label)?;
270            }
271            Ok(())
272        }
273        PayloadValue::Object(map) => ensure_payload(map, label),
274        _ => Ok(()),
275    }
276}
277
278fn ensure_payload(payload: &crate::point::Payload, label: &str) -> QdrantResult<()> {
279    for (key, value) in payload {
280        ensure_payload_key(key)?;
281        ensure_payload_value(value, label)?;
282    }
283    Ok(())
284}
285
286fn ensure_points(points: &[crate::Point]) -> QdrantResult<()> {
287    if points.is_empty() {
288        return Err(encode_error("Qdrant upsert point list must not be empty"));
289    }
290    for (idx, point) in points.iter().enumerate() {
291        ensure_point_id(&point.id, "upsert")?;
292        ensure_vector(&format!("upsert point {idx} vector"), &point.vector)?;
293        ensure_payload(&point.payload, &format!("upsert point {idx} payload"))?;
294    }
295    Ok(())
296}
297
298fn ensure_f64_finite(value: f64, label: &str) -> QdrantResult<()> {
299    if !value.is_finite() {
300        return Err(encode_error(format!(
301            "Qdrant {label} must be finite, got {value}"
302        )));
303    }
304    Ok(())
305}
306
307// ============================================================================
308// SearchPoints Encoder
309// ============================================================================
310
311/// Common search request fields shared by all search encoders.
312#[derive(Clone, Copy)]
313pub struct SearchRequest<'a> {
314    pub collection: &'a str,
315    pub vector: &'a [f32],
316    pub limit: u64,
317    pub score_threshold: Option<f32>,
318    pub vector_name: Option<&'a str>,
319    pub with_vectors: bool,
320}
321
322/// Encode a SearchPoints request directly to protobuf wire format.
323///
324/// # Arguments
325/// * `buf` - Reusable buffer (cleared before writing)
326/// * `collection` - Collection name
327/// * `vector` - Query vector (directly memcpy'd)
328/// * `limit` - Max results
329/// * `score_threshold` - Optional minimum score
330/// * `vector_name` - Optional named vector field
331///
332/// # Zero-Copy Optimization
333/// The vector is written via direct memory copy, avoiding per-element encoding.
334pub fn encode_search_proto(
335    buf: &mut BytesMut,
336    collection: &str,
337    vector: &[f32],
338    limit: u64,
339    score_threshold: Option<f32>,
340    vector_name: Option<&str>,
341    with_vectors: bool,
342) -> QdrantResult<()> {
343    ensure_search_request(&SearchRequest {
344        collection,
345        vector,
346        limit,
347        score_threshold,
348        vector_name,
349        with_vectors,
350    })?;
351
352    buf.clear();
353
354    // Field 1: collection_name (string)
355    buf.put_u8(SEARCH_COLLECTION);
356    encode_varint(buf, collection.len());
357    buf.extend_from_slice(collection.as_bytes());
358
359    // Field 2: vector (packed repeated float)
360    // This is the key optimization - direct memcpy of float bytes!
361    buf.put_u8(SEARCH_VECTOR);
362    let vector_bytes_len = vector.len() * 4; // f32 = 4 bytes
363    encode_varint(buf, vector_bytes_len);
364
365    extend_f32_le_slice(buf, vector);
366
367    // Field 4: limit (varint)
368    buf.put_u8(SEARCH_LIMIT);
369    encode_varint_u64(buf, limit);
370
371    // Field 6: with_payload = true
372    encode_with_payload_true(buf);
373
374    // Field 8: score_threshold (float, optional)
375    if let Some(threshold) = score_threshold {
376        buf.put_u8(SEARCH_SCORE_THRESHOLD);
377        buf.put_f32_le(threshold);
378    }
379
380    // Field 10: vector_name (string, optional)
381    if let Some(name) = vector_name {
382        buf.put_u8(SEARCH_VECTOR_NAME);
383        encode_varint(buf, name.len());
384        buf.extend_from_slice(name.as_bytes());
385    }
386
387    // Field 11: with_vectors = true (optional)
388    if with_vectors {
389        encode_search_with_vectors_selector(buf, vector_name);
390    }
391
392    Ok(())
393}
394
395/// Encode a SearchPoints request with QAIL AST filter conditions.
396///
397/// This is the filtered search path — translates QAIL conditions into
398/// Qdrant's protobuf Filter message (must/should arrays of Condition).
399pub fn encode_search_with_filter_proto(
400    buf: &mut BytesMut,
401    request: SearchRequest<'_>,
402    conditions: &[qail_core::ast::Condition],
403    is_or: bool,
404) -> QdrantResult<()> {
405    let (must_conditions, should_conditions): (
406        &[qail_core::ast::Condition],
407        &[qail_core::ast::Condition],
408    ) = if is_or {
409        (&[], conditions)
410    } else {
411        (conditions, &[])
412    };
413
414    encode_search_with_filter_groups_proto(buf, request, must_conditions, should_conditions)
415}
416
417/// Encode a SearchPoints request with grouped filter conditions.
418///
419/// `must_conditions` are combined as AND, `should_conditions` as OR.
420pub fn encode_search_with_filter_groups_proto(
421    buf: &mut BytesMut,
422    request: SearchRequest<'_>,
423    must_conditions: &[qail_core::ast::Condition],
424    should_conditions: &[qail_core::ast::Condition],
425) -> QdrantResult<()> {
426    ensure_search_request(&request)?;
427    buf.clear();
428
429    // Field 1: collection_name
430    buf.put_u8(SEARCH_COLLECTION);
431    encode_varint(buf, request.collection.len());
432    buf.extend_from_slice(request.collection.as_bytes());
433
434    // Field 2: vector (packed floats, zero-copy)
435    buf.put_u8(SEARCH_VECTOR);
436    let vector_bytes_len = request.vector.len() * 4;
437    encode_varint(buf, vector_bytes_len);
438    extend_f32_le_slice(buf, request.vector);
439
440    // Field 3: filter (Filter message)
441    if !must_conditions.is_empty() || !should_conditions.is_empty() {
442        let filter_buf = encode_filter_message_grouped(must_conditions, should_conditions)?;
443        buf.put_u8(SEARCH_FILTER);
444        encode_varint(buf, filter_buf.len());
445        buf.extend_from_slice(&filter_buf);
446    }
447
448    // Field 4: limit
449    buf.put_u8(SEARCH_LIMIT);
450    encode_varint_u64(buf, request.limit);
451
452    // Field 6: with_payload = true
453    encode_with_payload_true(buf);
454
455    // Field 8: score_threshold
456    if let Some(threshold) = request.score_threshold {
457        buf.put_u8(SEARCH_SCORE_THRESHOLD);
458        buf.put_f32_le(threshold);
459    }
460
461    // Field 10: vector_name
462    if let Some(name) = request.vector_name {
463        buf.put_u8(SEARCH_VECTOR_NAME);
464        encode_varint(buf, name.len());
465        buf.extend_from_slice(name.as_bytes());
466    }
467
468    // Field 11: with_vectors = true (optional)
469    if request.with_vectors {
470        encode_search_with_vectors_selector(buf, request.vector_name);
471    }
472
473    Ok(())
474}
475
476/// Encode a SearchPoints request where OR conditions are preserved per-cage.
477///
478/// Every OR cage is encoded as a nested `Filter { should: [...] }` wrapped
479/// in the outer filter's `must`, preserving:
480/// `(A OR B) AND (C OR D)` instead of flattening to `A OR B OR C OR D`.
481pub fn encode_search_with_filter_grouped_cages_proto(
482    buf: &mut BytesMut,
483    request: SearchRequest<'_>,
484    must_conditions: &[qail_core::ast::Condition],
485    should_groups: &[Vec<qail_core::ast::Condition>],
486) -> QdrantResult<()> {
487    ensure_search_request(&request)?;
488    buf.clear();
489
490    // Field 1: collection_name
491    buf.put_u8(SEARCH_COLLECTION);
492    encode_varint(buf, request.collection.len());
493    buf.extend_from_slice(request.collection.as_bytes());
494
495    // Field 2: vector (packed floats, zero-copy)
496    buf.put_u8(SEARCH_VECTOR);
497    let vector_bytes_len = request.vector.len() * 4;
498    encode_varint(buf, vector_bytes_len);
499    extend_f32_le_slice(buf, request.vector);
500
501    // Field 3: filter (Filter message)
502    if !must_conditions.is_empty() || !should_groups.is_empty() {
503        let filter_buf = encode_filter_message_grouped_cages(must_conditions, should_groups)?;
504        buf.put_u8(SEARCH_FILTER);
505        encode_varint(buf, filter_buf.len());
506        buf.extend_from_slice(&filter_buf);
507    }
508
509    // Field 4: limit
510    buf.put_u8(SEARCH_LIMIT);
511    encode_varint_u64(buf, request.limit);
512
513    // Field 6: with_payload = true
514    encode_with_payload_true(buf);
515
516    // Field 8: score_threshold
517    if let Some(threshold) = request.score_threshold {
518        buf.put_u8(SEARCH_SCORE_THRESHOLD);
519        buf.put_f32_le(threshold);
520    }
521
522    // Field 10: vector_name
523    if let Some(name) = request.vector_name {
524        buf.put_u8(SEARCH_VECTOR_NAME);
525        encode_varint(buf, name.len());
526        buf.extend_from_slice(name.as_bytes());
527    }
528
529    // Field 11: with_vectors = true (optional)
530    if request.with_vectors {
531        encode_search_with_vectors_selector(buf, request.vector_name);
532    }
533
534    Ok(())
535}
536
537/// Encode with_payload = true as a sub-message.
538pub fn encode_with_payload_true(buf: &mut BytesMut) {
539    // WithPayloadSelector { enable = true }
540    // Field 1: enable (bool) = 0x08, value = 1
541    buf.put_u8(SEARCH_WITH_PAYLOAD);
542    encode_varint(buf, 2); // submessage length
543    buf.put_u8(0x08); // field 1, varint
544    buf.put_u8(0x01); // true
545}
546
547/// Encode search with_vectors = true as a sub-message.
548pub fn encode_search_with_vectors_true(buf: &mut BytesMut) {
549    // WithVectorsSelector { enable = true }
550    // Field 1: enable (bool) = 0x08, value = 1
551    buf.put_u8(SEARCH_WITH_VECTORS);
552    encode_varint(buf, 2); // submessage length
553    buf.put_u8(0x08); // field 1, varint
554    buf.put_u8(0x01); // true
555}
556
557/// Encode search with_vectors selector.
558///
559/// Named vector searches request only the searched vector, because QAIL's
560/// public result shape stores a single dense vector.
561pub fn encode_search_with_vectors_selector(buf: &mut BytesMut, vector_name: Option<&str>) {
562    let Some(name) = vector_name else {
563        encode_search_with_vectors_true(buf);
564        return;
565    };
566
567    let selector_len = 1 + varint_len(name.len() as u64) + name.len();
568    let include_len = 1 + varint_len(selector_len as u64) + selector_len;
569
570    buf.put_u8(SEARCH_WITH_VECTORS);
571    encode_varint(buf, include_len);
572    buf.put_u8(0x12); // WithVectorsSelector.include
573    encode_varint(buf, selector_len);
574    buf.put_u8(0x0A); // VectorsSelector.names
575    encode_varint(buf, name.len());
576    buf.extend_from_slice(name.as_bytes());
577}
578
579// ============================================================================
580// Filter Encoder (QAIL Conditions → Qdrant Protobuf Filter)
581// ============================================================================
582
583/// Encode a Filter message from QAIL AST conditions.
584///
585/// Qdrant's Filter proto:
586/// ```text
587/// message Filter {
588///   repeated Condition should = 1;
589///   repeated Condition must = 2;
590///   repeated Condition must_not = 3;
591/// }
592/// ```
593///
594/// Each Condition wraps a FieldCondition:
595/// ```text
596/// message Condition {
597///   oneof condition_one_of {
598///     FieldCondition field = 1;
599///     IsEmptyCondition is_empty = 2;
600///     HasIdCondition has_id = 3;
601///     IsNullCondition is_null = 5;
602///     Filter filter = 4;     // nested filters
603///   }
604/// }
605/// ```
606fn encode_filter_message_grouped(
607    must_conditions: &[qail_core::ast::Condition],
608    should_conditions: &[qail_core::ast::Condition],
609) -> QdrantResult<BytesMut> {
610    let mut filter_buf =
611        BytesMut::with_capacity((must_conditions.len() + should_conditions.len()) * 32);
612
613    let mut encode_clause =
614        |conditions: &[qail_core::ast::Condition], clause_tag: u8| -> QdrantResult<()> {
615            for cond in conditions {
616                let cond_buf = encode_condition_message(cond)?;
617
618                // Write as repeated Condition in the filter's must/should field
619                filter_buf.put_u8(clause_tag);
620                encode_varint(&mut filter_buf, cond_buf.len());
621                filter_buf.extend_from_slice(&cond_buf);
622            }
623            Ok(())
624        };
625
626    encode_clause(must_conditions, FILTER_MUST)?;
627    encode_clause(should_conditions, FILTER_SHOULD)?;
628
629    Ok(filter_buf)
630}
631
632/// Encode a grouped filter preserving each OR cage as its own nested should-group.
633fn encode_filter_message_grouped_cages(
634    must_conditions: &[qail_core::ast::Condition],
635    should_groups: &[Vec<qail_core::ast::Condition>],
636) -> QdrantResult<BytesMut> {
637    let grouped_condition_count: usize = should_groups.iter().map(Vec::len).sum();
638    let mut filter_buf =
639        BytesMut::with_capacity((must_conditions.len() + grouped_condition_count) * 32);
640
641    for cond in must_conditions {
642        let cond_buf = encode_condition_message(cond)?;
643        filter_buf.put_u8(FILTER_MUST);
644        encode_varint(&mut filter_buf, cond_buf.len());
645        filter_buf.extend_from_slice(&cond_buf);
646    }
647
648    for group in should_groups {
649        if group.is_empty() {
650            continue;
651        }
652
653        if group.len() == 1 {
654            let cond_buf = encode_condition_message(&group[0])?;
655            filter_buf.put_u8(FILTER_MUST);
656            encode_varint(&mut filter_buf, cond_buf.len());
657            filter_buf.extend_from_slice(&cond_buf);
658            continue;
659        }
660
661        let nested_filter = encode_filter_message_grouped(&[], group)?;
662        let mut nested_condition = BytesMut::with_capacity(nested_filter.len() + 4);
663        nested_condition.put_u8(CONDITION_FILTER);
664        encode_varint(&mut nested_condition, nested_filter.len());
665        nested_condition.extend_from_slice(&nested_filter);
666
667        filter_buf.put_u8(FILTER_MUST);
668        encode_varint(&mut filter_buf, nested_condition.len());
669        filter_buf.extend_from_slice(&nested_condition);
670    }
671
672    Ok(filter_buf)
673}
674
675/// Encode a single `Condition` message for Qdrant filters.
676fn encode_condition_message(cond: &qail_core::ast::Condition) -> QdrantResult<BytesMut> {
677    use qail_core::ast::{Expr, Operator, Value};
678
679    let key = match &cond.left {
680        Expr::Named(name) => name.as_str(),
681        Expr::Aliased { name, .. } => name.as_str(),
682        other => {
683            return Err(QdrantError::Encode(format!(
684                "Unsupported filter left expression for Qdrant: {:?}",
685                other
686            )));
687        }
688    };
689    let key = normalize_filter_key(key);
690    if key.is_empty() {
691        return Err(QdrantError::Encode(
692            "Qdrant filter key cannot be empty".to_string(),
693        ));
694    }
695
696    if key.eq_ignore_ascii_case("id") {
697        return match (&cond.op, &cond.value) {
698            (Operator::Eq, value) => encode_has_id_condition_from_value(value),
699            (Operator::In, value) => encode_has_id_condition_from_array(value),
700            (Operator::Ne, value) => {
701                encode_has_id_condition_from_value(value).map(encode_nested_must_not_condition)
702            }
703            (Operator::NotIn, value) => {
704                encode_has_id_condition_from_array(value).map(encode_nested_must_not_condition)
705            }
706            _ => Err(QdrantError::Encode(format!(
707                "Qdrant id filters support equality, inequality, IN, or NOT IN against integer, string, or UUID values: op={:?}, value={:?}",
708                cond.op, cond.value
709            ))),
710        };
711    }
712
713    match (&cond.op, &cond.value) {
714        // Match (equality) conditions
715        (Operator::Eq, Value::String(s)) => Ok(encode_field_condition_match_keyword(key, s)),
716        (Operator::Eq, Value::Uuid(u)) => {
717            Ok(encode_field_condition_match_keyword(key, &u.to_string()))
718        }
719        (Operator::Eq, Value::Int(n)) => Ok(encode_field_condition_match_integer(key, *n)),
720        (Operator::Eq, Value::Bool(b)) => Ok(encode_field_condition_match_bool(key, *b)),
721        (Operator::In, Value::Array(values)) => encode_field_condition_match_any(key, values),
722        (Operator::Ne, Value::String(s)) => Ok(encode_nested_must_not_condition(
723            encode_field_condition_match_keyword(key, s),
724        )),
725        (Operator::Ne, Value::Uuid(u)) => Ok(encode_nested_must_not_condition(
726            encode_field_condition_match_keyword(key, &u.to_string()),
727        )),
728        (Operator::Ne, Value::Int(n)) => Ok(encode_nested_must_not_condition(
729            encode_field_condition_match_integer(key, *n),
730        )),
731        (Operator::Ne, Value::Bool(b)) => Ok(encode_nested_must_not_condition(
732            encode_field_condition_match_bool(key, *b),
733        )),
734        (Operator::NotIn, Value::Array(values)) => {
735            encode_field_condition_match_any(key, values).map(encode_nested_must_not_condition)
736        }
737
738        // Range conditions
739        (Operator::Gt, Value::Int(n)) => Ok(encode_field_condition_range(
740            key,
741            None,
742            None,
743            Some(*n as f64),
744            None,
745        )),
746        (Operator::Gt, Value::Float(f)) => {
747            ensure_f64_finite(*f, "filter range float")?;
748            Ok(encode_field_condition_range(
749                key,
750                None,
751                None,
752                Some(*f),
753                None,
754            ))
755        }
756        (Operator::Gte, Value::Int(n)) => Ok(encode_field_condition_range(
757            key,
758            None,
759            None,
760            None,
761            Some(*n as f64),
762        )),
763        (Operator::Gte, Value::Float(f)) => {
764            ensure_f64_finite(*f, "filter range float")?;
765            Ok(encode_field_condition_range(
766                key,
767                None,
768                None,
769                None,
770                Some(*f),
771            ))
772        }
773        (Operator::Lt, Value::Int(n)) => Ok(encode_field_condition_range(
774            key,
775            Some(*n as f64),
776            None,
777            None,
778            None,
779        )),
780        (Operator::Lt, Value::Float(f)) => {
781            ensure_f64_finite(*f, "filter range float")?;
782            Ok(encode_field_condition_range(
783                key,
784                Some(*f),
785                None,
786                None,
787                None,
788            ))
789        }
790        (Operator::Lte, Value::Int(n)) => Ok(encode_field_condition_range(
791            key,
792            None,
793            Some(*n as f64),
794            None,
795            None,
796        )),
797        (Operator::Lte, Value::Float(f)) => {
798            ensure_f64_finite(*f, "filter range float")?;
799            Ok(encode_field_condition_range(
800                key,
801                None,
802                Some(*f),
803                None,
804                None,
805            ))
806        }
807
808        // Text match (contains / like)
809        (Operator::Contains | Operator::Like, Value::String(s)) => {
810            if s.trim().is_empty() {
811                return Err(encode_error("Qdrant text filter value must not be empty"));
812            }
813            Ok(encode_field_condition_match_text(key, s))
814        }
815
816        (Operator::IsNull, Value::Null | Value::NullUuid) => Ok(encode_is_null_condition(key)),
817        (Operator::IsNotNull, Value::Null | Value::NullUuid) => Ok(
818            encode_nested_must_not_condition(encode_is_null_condition(key)),
819        ),
820        (Operator::NotLike, Value::String(s)) => {
821            if s.trim().is_empty() {
822                return Err(encode_error("Qdrant text filter value must not be empty"));
823            }
824            Ok(encode_nested_must_not_condition(
825                encode_field_condition_match_text(key, s),
826            ))
827        }
828
829        _ => Err(QdrantError::Encode(format!(
830            "Unsupported Qdrant filter condition: op={:?}, value={:?}",
831            cond.op, cond.value
832        ))),
833    }
834}
835
836fn normalize_filter_key(raw: &str) -> &str {
837    raw.trim().trim_matches('"').trim()
838}
839
840fn point_id_from_ast_value(value: &qail_core::ast::Value) -> Option<crate::PointId> {
841    use qail_core::ast::Value;
842
843    match value {
844        Value::Int(id) if *id >= 0 => Some(crate::PointId::Num(*id as u64)),
845        Value::String(id) => Some(crate::PointId::Uuid(id.clone())),
846        Value::Uuid(id) => Some(crate::PointId::Uuid(id.to_string())),
847        _ => None,
848    }
849}
850
851fn encode_point_id_message(id: &crate::PointId) -> BytesMut {
852    let mut id_buf = BytesMut::with_capacity(40);
853    match id {
854        crate::PointId::Num(n) => {
855            id_buf.put_u8(POINT_ID_NUM);
856            encode_varint_u64(&mut id_buf, *n);
857        }
858        crate::PointId::Uuid(s) => {
859            id_buf.put_u8(POINT_ID_UUID);
860            encode_varint(&mut id_buf, s.len());
861            id_buf.extend_from_slice(s.as_bytes());
862        }
863    }
864    id_buf
865}
866
867fn encode_has_id_condition_from_value(value: &qail_core::ast::Value) -> QdrantResult<BytesMut> {
868    let id = point_id_from_ast_value(value).ok_or_else(|| {
869        QdrantError::Encode(
870            "Qdrant id filters support only integer, string, or UUID values".to_string(),
871        )
872    })?;
873    ensure_point_id(&id, "id filter")?;
874    Ok(encode_has_id_condition(&id))
875}
876
877fn encode_has_id_condition_from_array(value: &qail_core::ast::Value) -> QdrantResult<BytesMut> {
878    let qail_core::ast::Value::Array(values) = value else {
879        return Err(QdrantError::Encode(
880            "Qdrant id IN filters require an array value".to_string(),
881        ));
882    };
883    if values.is_empty() {
884        return Err(QdrantError::Encode(
885            "Qdrant id IN filters require at least one id".to_string(),
886        ));
887    }
888
889    let ids = values
890        .iter()
891        .map(|value| {
892            let id = point_id_from_ast_value(value).ok_or_else(|| {
893                QdrantError::Encode(
894                    "Qdrant id IN filters support only integer, string, or UUID values".to_string(),
895                )
896            })?;
897            ensure_point_id(&id, "id IN filter")?;
898            Ok(id)
899        })
900        .collect::<QdrantResult<Vec<_>>>()?;
901    Ok(encode_has_id_conditions(&ids))
902}
903
904fn encode_has_id_condition(id: &crate::PointId) -> BytesMut {
905    encode_has_id_conditions(std::slice::from_ref(id))
906}
907
908fn encode_has_id_conditions(ids: &[crate::PointId]) -> BytesMut {
909    let ids_len: usize = ids
910        .iter()
911        .map(|id| encode_point_id_message(id).len() + 2)
912        .sum();
913    let mut has_id_buf = BytesMut::with_capacity(ids_len);
914    for id in ids {
915        let id_buf = encode_point_id_message(id);
916        has_id_buf.put_u8(POINT_ID);
917        encode_varint(&mut has_id_buf, id_buf.len());
918        has_id_buf.extend_from_slice(&id_buf);
919    }
920
921    let mut cond_buf = BytesMut::with_capacity(has_id_buf.len() + 4);
922    cond_buf.put_u8(CONDITION_HAS_ID);
923    encode_varint(&mut cond_buf, has_id_buf.len());
924    cond_buf.extend_from_slice(&has_id_buf);
925    cond_buf
926}
927
928fn encode_nested_must_not_condition(inner: BytesMut) -> BytesMut {
929    let mut filter_buf = BytesMut::with_capacity(inner.len() + 8);
930    filter_buf.put_u8(FILTER_MUST_NOT);
931    encode_varint(&mut filter_buf, inner.len());
932    filter_buf.extend_from_slice(&inner);
933
934    let mut cond_buf = BytesMut::with_capacity(filter_buf.len() + 8);
935    cond_buf.put_u8(CONDITION_FILTER);
936    encode_varint(&mut cond_buf, filter_buf.len());
937    cond_buf.extend_from_slice(&filter_buf);
938    cond_buf
939}
940
941/// Encode a Condition { is_null: IsNullCondition { key } }.
942fn encode_is_null_condition(key: &str) -> BytesMut {
943    let mut is_null_buf = BytesMut::with_capacity(key.len() + 8);
944    is_null_buf.put_u8(0x0A); // IsNullCondition.key, field 1, LEN
945    encode_varint(&mut is_null_buf, key.len());
946    is_null_buf.extend_from_slice(key.as_bytes());
947
948    let mut cond_buf = BytesMut::with_capacity(is_null_buf.len() + 8);
949    cond_buf.put_u8(CONDITION_IS_NULL);
950    encode_varint(&mut cond_buf, is_null_buf.len());
951    cond_buf.extend_from_slice(&is_null_buf);
952    cond_buf
953}
954
955/// Encode a FieldCondition with Match { keyword } for string equality.
956///
957/// ```text
958/// Condition {
959///   field = FieldCondition {
960///     key = "field_name",
961///     match = Match { keyword = "value" }
962///   }
963/// }
964/// ```
965fn encode_field_condition_match_keyword(key: &str, value: &str) -> BytesMut {
966    // Match message: field 1 = keyword (string)
967    let mut match_buf = BytesMut::with_capacity(value.len() + 8);
968    match_buf.put_u8(0x0A); // field 1 (keyword), wire LEN
969    encode_varint(&mut match_buf, value.len());
970    match_buf.extend_from_slice(value.as_bytes());
971
972    // FieldCondition message
973    let mut fc_buf = BytesMut::with_capacity(key.len() + match_buf.len() + 16);
974    // field 1: key (string)
975    fc_buf.put_u8(0x0A);
976    encode_varint(&mut fc_buf, key.len());
977    fc_buf.extend_from_slice(key.as_bytes());
978    // field 2: match (Match message)
979    fc_buf.put_u8(0x12);
980    encode_varint(&mut fc_buf, match_buf.len());
981    fc_buf.extend_from_slice(&match_buf);
982
983    // Condition: field 1 = FieldCondition
984    let mut cond_buf = BytesMut::with_capacity(fc_buf.len() + 4);
985    cond_buf.put_u8(0x0A); // field 1
986    encode_varint(&mut cond_buf, fc_buf.len());
987    cond_buf.extend_from_slice(&fc_buf);
988
989    cond_buf
990}
991
992/// Encode a FieldCondition with Match { integer } for int equality.
993fn encode_field_condition_match_integer(key: &str, value: i64) -> BytesMut {
994    // Match message: field 2 = integer (int64)
995    let mut match_buf = BytesMut::with_capacity(16);
996    match_buf.put_u8(0x10); // field 2 (integer), wire VARINT
997    encode_varint_u64(&mut match_buf, value as u64);
998
999    let mut fc_buf = BytesMut::with_capacity(key.len() + match_buf.len() + 16);
1000    fc_buf.put_u8(0x0A);
1001    encode_varint(&mut fc_buf, key.len());
1002    fc_buf.extend_from_slice(key.as_bytes());
1003    fc_buf.put_u8(0x12);
1004    encode_varint(&mut fc_buf, match_buf.len());
1005    fc_buf.extend_from_slice(&match_buf);
1006
1007    let mut cond_buf = BytesMut::with_capacity(fc_buf.len() + 4);
1008    cond_buf.put_u8(0x0A);
1009    encode_varint(&mut cond_buf, fc_buf.len());
1010    cond_buf.extend_from_slice(&fc_buf);
1011
1012    cond_buf
1013}
1014
1015/// Encode a FieldCondition with Match { boolean }.
1016fn encode_field_condition_match_bool(key: &str, value: bool) -> BytesMut {
1017    // Match message: field 3 = boolean (bool)
1018    let mut match_buf = BytesMut::with_capacity(4);
1019    match_buf.put_u8(0x18); // field 3 (boolean), wire VARINT
1020    match_buf.put_u8(if value { 1 } else { 0 });
1021
1022    let mut fc_buf = BytesMut::with_capacity(key.len() + match_buf.len() + 16);
1023    fc_buf.put_u8(0x0A);
1024    encode_varint(&mut fc_buf, key.len());
1025    fc_buf.extend_from_slice(key.as_bytes());
1026    fc_buf.put_u8(0x12);
1027    encode_varint(&mut fc_buf, match_buf.len());
1028    fc_buf.extend_from_slice(&match_buf);
1029
1030    let mut cond_buf = BytesMut::with_capacity(fc_buf.len() + 4);
1031    cond_buf.put_u8(0x0A);
1032    encode_varint(&mut cond_buf, fc_buf.len());
1033    cond_buf.extend_from_slice(&fc_buf);
1034
1035    cond_buf
1036}
1037
1038/// Encode a FieldCondition with Match { text } for full-text search.
1039fn encode_field_condition_match_text(key: &str, value: &str) -> BytesMut {
1040    // Match message: field 4 = text (string)
1041    let mut match_buf = BytesMut::with_capacity(value.len() + 8);
1042    match_buf.put_u8(0x22); // field 4 (text), wire LEN
1043    encode_varint(&mut match_buf, value.len());
1044    match_buf.extend_from_slice(value.as_bytes());
1045
1046    let mut fc_buf = BytesMut::with_capacity(key.len() + match_buf.len() + 16);
1047    fc_buf.put_u8(0x0A);
1048    encode_varint(&mut fc_buf, key.len());
1049    fc_buf.extend_from_slice(key.as_bytes());
1050    fc_buf.put_u8(0x12);
1051    encode_varint(&mut fc_buf, match_buf.len());
1052    fc_buf.extend_from_slice(&match_buf);
1053
1054    let mut cond_buf = BytesMut::with_capacity(fc_buf.len() + 4);
1055    cond_buf.put_u8(0x0A);
1056    encode_varint(&mut cond_buf, fc_buf.len());
1057    cond_buf.extend_from_slice(&fc_buf);
1058
1059    cond_buf
1060}
1061
1062fn encode_field_condition_match_any(
1063    key: &str,
1064    values: &[qail_core::ast::Value],
1065) -> QdrantResult<BytesMut> {
1066    use qail_core::ast::Value;
1067
1068    if values.is_empty() {
1069        return Err(encode_error("Qdrant IN filters require at least one value"));
1070    }
1071    if values
1072        .iter()
1073        .all(|value| matches!(value, Value::String(_) | Value::Uuid(_)))
1074    {
1075        let mut repeated = BytesMut::with_capacity(values.len() * 16);
1076        for value in values {
1077            let value = match value {
1078                Value::String(value) => value.clone(),
1079                Value::Uuid(value) => value.to_string(),
1080                _ => unreachable!("checked by all()"),
1081            };
1082            repeated.put_u8(0x0A); // RepeatedStrings.strings, field 1
1083            encode_varint(&mut repeated, value.len());
1084            repeated.extend_from_slice(value.as_bytes());
1085        }
1086
1087        let mut match_buf = BytesMut::with_capacity(repeated.len() + 4);
1088        match_buf.put_u8(0x2A); // Match.keywords, field 5
1089        encode_varint(&mut match_buf, repeated.len());
1090        match_buf.extend_from_slice(&repeated);
1091        return Ok(encode_field_condition_match_message(key, match_buf));
1092    }
1093    if values.iter().all(|value| matches!(value, Value::Int(_))) {
1094        let mut repeated = BytesMut::with_capacity(values.len() * 10);
1095        for value in values {
1096            let Value::Int(value) = value else {
1097                unreachable!("checked by all()");
1098            };
1099            repeated.put_u8(0x08); // RepeatedIntegers.integers, field 1
1100            encode_varint_u64(&mut repeated, *value as u64);
1101        }
1102
1103        let mut match_buf = BytesMut::with_capacity(repeated.len() + 4);
1104        match_buf.put_u8(0x32); // Match.integers, field 6
1105        encode_varint(&mut match_buf, repeated.len());
1106        match_buf.extend_from_slice(&repeated);
1107        return Ok(encode_field_condition_match_message(key, match_buf));
1108    }
1109
1110    Err(encode_error(
1111        "Qdrant IN filters support only a non-empty homogeneous string/UUID or integer array",
1112    ))
1113}
1114
1115fn encode_field_condition_match_message(key: &str, match_buf: BytesMut) -> BytesMut {
1116    let mut fc_buf = BytesMut::with_capacity(key.len() + match_buf.len() + 16);
1117    fc_buf.put_u8(0x0A);
1118    encode_varint(&mut fc_buf, key.len());
1119    fc_buf.extend_from_slice(key.as_bytes());
1120    fc_buf.put_u8(0x12);
1121    encode_varint(&mut fc_buf, match_buf.len());
1122    fc_buf.extend_from_slice(&match_buf);
1123
1124    let mut cond_buf = BytesMut::with_capacity(fc_buf.len() + 4);
1125    cond_buf.put_u8(0x0A);
1126    encode_varint(&mut cond_buf, fc_buf.len());
1127    cond_buf.extend_from_slice(&fc_buf);
1128
1129    cond_buf
1130}
1131
1132/// Encode a FieldCondition with Range.
1133///
1134/// Range proto: { lt, gt, gte, lte } — all optional f64.
1135fn encode_field_condition_range(
1136    key: &str,
1137    lt: Option<f64>,
1138    lte: Option<f64>,
1139    gt: Option<f64>,
1140    gte: Option<f64>,
1141) -> BytesMut {
1142    // Range message fields (all double / f64)
1143    // field 1: lt  -> 0x09 (field 1, wire FIXED64)
1144    // field 2: gt  -> 0x11 (field 2, wire FIXED64)
1145    // field 3: gte -> 0x19 (field 3, wire FIXED64)
1146    // field 4: lte -> 0x21 (field 4, wire FIXED64)
1147    let mut range_buf = BytesMut::with_capacity(40);
1148    if let Some(v) = lt {
1149        range_buf.put_u8(0x09); // field 1, wire 1 (64-bit)
1150        range_buf.put_f64_le(v);
1151    }
1152    if let Some(v) = gt {
1153        range_buf.put_u8(0x11); // field 2, wire 1
1154        range_buf.put_f64_le(v);
1155    }
1156    if let Some(v) = gte {
1157        range_buf.put_u8(0x19); // field 3, wire 1
1158        range_buf.put_f64_le(v);
1159    }
1160    if let Some(v) = lte {
1161        range_buf.put_u8(0x21); // field 4, wire 1
1162        range_buf.put_f64_le(v);
1163    }
1164
1165    // FieldCondition: key + range
1166    let mut fc_buf = BytesMut::with_capacity(key.len() + range_buf.len() + 16);
1167    fc_buf.put_u8(0x0A); // field 1: key
1168    encode_varint(&mut fc_buf, key.len());
1169    fc_buf.extend_from_slice(key.as_bytes());
1170    fc_buf.put_u8(0x1A); // field 3: range (Range message)
1171    encode_varint(&mut fc_buf, range_buf.len());
1172    fc_buf.extend_from_slice(&range_buf);
1173
1174    // Condition: field 1 = FieldCondition
1175    let mut cond_buf = BytesMut::with_capacity(fc_buf.len() + 4);
1176    cond_buf.put_u8(0x0A);
1177    encode_varint(&mut cond_buf, fc_buf.len());
1178    cond_buf.extend_from_slice(&fc_buf);
1179
1180    cond_buf
1181}
1182
1183// ============================================================================
1184// UpsertPoints Encoder
1185// ============================================================================
1186
1187/// Encode an UpsertPoints request to protobuf wire format.
1188pub fn encode_upsert_proto(
1189    buf: &mut BytesMut,
1190    collection: &str,
1191    points: &[crate::Point],
1192    wait: bool,
1193) -> QdrantResult<()> {
1194    ensure_collection_name(collection)?;
1195    ensure_points(points)?;
1196
1197    buf.clear();
1198
1199    // Field 1: collection_name
1200    buf.put_u8(UPSERT_COLLECTION);
1201    encode_varint(buf, collection.len());
1202    buf.extend_from_slice(collection.as_bytes());
1203
1204    // Field 2: wait (bool)
1205    if wait {
1206        buf.put_u8(UPSERT_WAIT);
1207        buf.put_u8(0x01);
1208    }
1209
1210    // Field 3: points (repeated PointStruct)
1211    for point in points {
1212        encode_point_struct(buf, point)?;
1213    }
1214
1215    Ok(())
1216}
1217
1218/// Encode a single PointStruct (with payload support).
1219fn encode_point_struct(buf: &mut BytesMut, point: &crate::Point) -> QdrantResult<()> {
1220    // We need to encode into a temp buffer first to get length,
1221    // since PointStruct is length-delimited
1222    let mut point_buf = BytesMut::with_capacity(point.vector.len() * 4 + 64);
1223
1224    // Field 1: id (PointId oneof)
1225    encode_point_id_field(&mut point_buf, &point.id);
1226
1227    // Field 3: payload (map<string, Value>)
1228    if !point.payload.is_empty() {
1229        encode_payload_map(&mut point_buf, &point.payload)?;
1230    }
1231
1232    // Field 4: vectors (Vectors -> Vector)
1233    let vector_bytes_len = point.vector.len() * 4;
1234    let vector_inner_len = 1 + varint_len(vector_bytes_len as u64) + vector_bytes_len;
1235    let vectors_len = 1 + varint_len(vector_inner_len as u64) + vector_inner_len;
1236
1237    point_buf.put_u8(POINT_VECTORS);
1238    encode_varint(&mut point_buf, vectors_len);
1239    point_buf.put_u8(0x0A); // Vectors.vector (field 1)
1240    encode_varint(&mut point_buf, vector_inner_len);
1241    point_buf.put_u8(0x0A); // Vector.data (field 1, packed floats)
1242    encode_varint(&mut point_buf, vector_bytes_len);
1243    extend_f32_le_slice(&mut point_buf, &point.vector);
1244
1245    // Write to main buffer with length prefix
1246    buf.put_u8(UPSERT_POINTS);
1247    encode_varint(buf, point_buf.len());
1248    buf.extend_from_slice(&point_buf);
1249    Ok(())
1250}
1251
1252/// Encode a PointId into a buffer as field 1 of PointStruct.
1253fn encode_point_id_field(buf: &mut BytesMut, id: &crate::PointId) {
1254    let id_buf = encode_point_id_message(id);
1255    buf.put_u8(POINT_ID);
1256    encode_varint(buf, id_buf.len());
1257    buf.extend_from_slice(&id_buf);
1258}
1259
1260/// Encode a payload map as protobuf map<string, Value>.
1261///
1262/// Protobuf maps are encoded as repeated field with key-value pair messages.
1263fn encode_payload_map(buf: &mut BytesMut, payload: &crate::point::Payload) -> QdrantResult<()> {
1264    for (key, value) in payload {
1265        ensure_payload_key(key)?;
1266        let mut entry_buf = BytesMut::with_capacity(key.len() + 32);
1267
1268        // Map entry field 1: key (string)
1269        entry_buf.put_u8(0x0A);
1270        encode_varint(&mut entry_buf, key.len());
1271        entry_buf.extend_from_slice(key.as_bytes());
1272
1273        // Map entry field 2: value (Value message)
1274        let value_buf = encode_payload_value(value)?;
1275        entry_buf.put_u8(0x12);
1276        encode_varint(&mut entry_buf, value_buf.len());
1277        entry_buf.extend_from_slice(&value_buf);
1278
1279        // Write map entry as field 3 of PointStruct (payload)
1280        buf.put_u8(POINT_PAYLOAD);
1281        encode_varint(buf, entry_buf.len());
1282        buf.extend_from_slice(&entry_buf);
1283    }
1284    Ok(())
1285}
1286
1287/// Encode a single PayloadValue to protobuf Value message.
1288///
1289/// ```text
1290/// message Value {
1291///   oneof kind {
1292///     NullValue null_value = 1;
1293///     double double_value = 2;
1294///     int64 integer_value = 3;
1295///     string string_value = 4;
1296///     bool bool_value = 5;
1297///     Struct struct_value = 6;
1298///     ListValue list_value = 7;
1299///   }
1300/// }
1301/// ```
1302fn encode_payload_value(value: &crate::point::PayloadValue) -> QdrantResult<BytesMut> {
1303    use crate::point::PayloadValue;
1304    let mut buf = BytesMut::with_capacity(32);
1305
1306    match value {
1307        PayloadValue::Null => {
1308            // field 1: null_value (enum, always 0)
1309            buf.put_u8(0x08);
1310            buf.put_u8(0x00);
1311        }
1312        PayloadValue::Float(f) => {
1313            ensure_f64_finite(*f, "payload float")?;
1314            // field 2: double_value (double, wire type 1 = fixed64)
1315            buf.put_u8(0x11); // (2 << 3) | 1 = 0x11
1316            buf.put_f64_le(*f);
1317        }
1318        PayloadValue::Integer(n) => {
1319            // field 3: integer_value (int64, wire type 0 = varint)
1320            buf.put_u8(0x18); // (3 << 3) | 0 = 0x18
1321            encode_varint_u64(&mut buf, *n as u64);
1322        }
1323        PayloadValue::String(s) => {
1324            // field 4: string_value (string, wire type 2 = len-delimited)
1325            buf.put_u8(0x22); // (4 << 3) | 2 = 0x22
1326            encode_varint(&mut buf, s.len());
1327            buf.extend_from_slice(s.as_bytes());
1328        }
1329        PayloadValue::Bool(b) => {
1330            // field 5: bool_value (bool, wire type 0 = varint)
1331            buf.put_u8(0x28); // (5 << 3) | 0 = 0x28
1332            buf.put_u8(if *b { 1 } else { 0 });
1333        }
1334        PayloadValue::List(items) => {
1335            // field 7: list_value (ListValue message)
1336            let mut list_buf = BytesMut::with_capacity(items.len() * 16);
1337            for item in items {
1338                let val_buf = encode_payload_value(item)?;
1339                // ListValue.values (field 1, repeated Value)
1340                list_buf.put_u8(0x0A);
1341                encode_varint(&mut list_buf, val_buf.len());
1342                list_buf.extend_from_slice(&val_buf);
1343            }
1344            buf.put_u8(0x3A); // (7 << 3) | 2 = 0x3A
1345            encode_varint(&mut buf, list_buf.len());
1346            buf.extend_from_slice(&list_buf);
1347        }
1348        PayloadValue::Object(map) => {
1349            // field 6: struct_value (Struct message)
1350            // Struct.fields is map<string, Value> → repeated MapEntry
1351            let mut struct_buf = BytesMut::with_capacity(map.len() * 32);
1352            for (k, v) in map {
1353                ensure_payload_key(k)?;
1354                let val_buf = encode_payload_value(v)?;
1355                let mut entry_buf = BytesMut::with_capacity(k.len() + val_buf.len() + 8);
1356                // key (field 1)
1357                entry_buf.put_u8(0x0A);
1358                encode_varint(&mut entry_buf, k.len());
1359                entry_buf.extend_from_slice(k.as_bytes());
1360                // value (field 2)
1361                entry_buf.put_u8(0x12);
1362                encode_varint(&mut entry_buf, val_buf.len());
1363                entry_buf.extend_from_slice(&val_buf);
1364                // Struct.fields (field 1, repeated)
1365                struct_buf.put_u8(0x0A);
1366                encode_varint(&mut struct_buf, entry_buf.len());
1367                struct_buf.extend_from_slice(&entry_buf);
1368            }
1369            buf.put_u8(0x32); // (6 << 3) | 2 = 0x32
1370            encode_varint(&mut buf, struct_buf.len());
1371            buf.extend_from_slice(&struct_buf);
1372        }
1373    }
1374
1375    Ok(buf)
1376}
1377
1378// ============================================================================
1379// GetPoints Encoder
1380// ============================================================================
1381
1382/// Encode a GetPoints request to protobuf wire format.
1383///
1384/// ```text
1385/// message GetPoints {
1386///   string collection_name = 1;
1387///   repeated PointId ids = 2;
1388///   WithPayloadSelector with_payload = 4;
1389///   WithVectorsSelector with_vectors = 5;
1390/// }
1391/// ```
1392pub fn encode_get_points_proto(
1393    buf: &mut BytesMut,
1394    collection: &str,
1395    ids: &[crate::PointId],
1396    with_vectors: bool,
1397) -> QdrantResult<()> {
1398    ensure_collection_name(collection)?;
1399    ensure_point_ids(ids, "get")?;
1400
1401    buf.clear();
1402
1403    // Field 1: collection_name
1404    buf.put_u8(0x0A);
1405    encode_varint(buf, collection.len());
1406    buf.extend_from_slice(collection.as_bytes());
1407
1408    // Field 2: ids (repeated PointId)
1409    for id in ids {
1410        let id_buf = encode_point_id_message(id);
1411        buf.put_u8(0x12); // field 2, wire LEN
1412        encode_varint(buf, id_buf.len());
1413        buf.extend_from_slice(&id_buf);
1414    }
1415
1416    // Field 4: with_payload = true
1417    buf.put_u8(0x22); // (4 << 3) | 2 = 0x22
1418    encode_varint(buf, 2);
1419    buf.put_u8(0x08); // enable = true
1420    buf.put_u8(0x01);
1421
1422    // Field 5: with_vectors
1423    if with_vectors {
1424        buf.put_u8(0x2A); // (5 << 3) | 2 = 0x2A
1425        encode_varint(buf, 2);
1426        buf.put_u8(0x08); // enable = true
1427        buf.put_u8(0x01);
1428    }
1429    Ok(())
1430}
1431
1432// ============================================================================
1433// ScrollPoints Encoder
1434// ============================================================================
1435
1436/// Encode a ScrollPoints request to protobuf wire format.
1437///
1438/// ```text
1439/// message ScrollPoints {
1440///   string collection_name = 1;
1441///   Filter filter = 2;
1442///   optional PointId offset = 3;
1443///   uint32 limit = 4;
1444///   WithPayloadSelector with_payload = 6;
1445///   WithVectorsSelector with_vectors = 7;
1446/// }
1447/// ```
1448pub fn encode_scroll_points_proto(
1449    buf: &mut BytesMut,
1450    collection: &str,
1451    limit: u32,
1452    offset: Option<&crate::PointId>,
1453    with_vectors: bool,
1454) -> QdrantResult<()> {
1455    ensure_collection_name(collection)?;
1456    ensure_scroll_limit(limit)?;
1457    if let Some(id) = offset {
1458        ensure_point_id(id, "scroll offset")?;
1459    }
1460
1461    buf.clear();
1462
1463    // Field 1: collection_name
1464    buf.put_u8(SCROLL_COLLECTION);
1465    encode_varint(buf, collection.len());
1466    buf.extend_from_slice(collection.as_bytes());
1467
1468    // Field 3: offset (optional PointId)
1469    if let Some(id) = offset {
1470        let id_buf = encode_point_id_message(id);
1471        buf.put_u8(SCROLL_OFFSET);
1472        encode_varint(buf, id_buf.len());
1473        buf.extend_from_slice(&id_buf);
1474    }
1475
1476    // Field 4: limit (uint32)
1477    buf.put_u8(SCROLL_LIMIT);
1478    encode_varint(buf, limit as usize);
1479
1480    // Field 6: with_payload = true
1481    buf.put_u8(SCROLL_WITH_PAYLOAD);
1482    encode_varint(buf, 2);
1483    buf.put_u8(0x08);
1484    buf.put_u8(0x01);
1485
1486    // Field 7: with_vectors
1487    if with_vectors {
1488        buf.put_u8(SCROLL_WITH_VECTORS);
1489        encode_varint(buf, 2);
1490        buf.put_u8(0x08);
1491        buf.put_u8(0x01);
1492    }
1493    Ok(())
1494}
1495
1496/// Encode a filtered ScrollPoints request.
1497pub fn encode_scroll_points_with_filter_grouped_cages_proto(
1498    buf: &mut BytesMut,
1499    collection: &str,
1500    limit: u32,
1501    offset: Option<&crate::PointId>,
1502    with_vectors: bool,
1503    must_conditions: &[qail_core::ast::Condition],
1504    should_groups: &[Vec<qail_core::ast::Condition>],
1505) -> QdrantResult<()> {
1506    ensure_collection_name(collection)?;
1507    ensure_scroll_limit(limit)?;
1508    if let Some(id) = offset {
1509        ensure_point_id(id, "scroll offset")?;
1510    }
1511
1512    buf.clear();
1513
1514    // Field 1: collection_name
1515    buf.put_u8(SCROLL_COLLECTION);
1516    encode_varint(buf, collection.len());
1517    buf.extend_from_slice(collection.as_bytes());
1518
1519    // Field 2: filter
1520    if !must_conditions.is_empty() || !should_groups.is_empty() {
1521        let filter_buf = encode_filter_message_grouped_cages(must_conditions, should_groups)?;
1522        buf.put_u8(SCROLL_FILTER);
1523        encode_varint(buf, filter_buf.len());
1524        buf.extend_from_slice(&filter_buf);
1525    }
1526
1527    // Field 3: offset (optional PointId)
1528    if let Some(id) = offset {
1529        let id_buf = encode_point_id_message(id);
1530        buf.put_u8(SCROLL_OFFSET);
1531        encode_varint(buf, id_buf.len());
1532        buf.extend_from_slice(&id_buf);
1533    }
1534
1535    // Field 4: limit (uint32)
1536    buf.put_u8(SCROLL_LIMIT);
1537    encode_varint(buf, limit as usize);
1538
1539    // Field 6: with_payload = true
1540    buf.put_u8(SCROLL_WITH_PAYLOAD);
1541    encode_varint(buf, 2);
1542    buf.put_u8(0x08);
1543    buf.put_u8(0x01);
1544
1545    // Field 7: with_vectors
1546    if with_vectors {
1547        buf.put_u8(SCROLL_WITH_VECTORS);
1548        encode_varint(buf, 2);
1549        buf.put_u8(0x08);
1550        buf.put_u8(0x01);
1551    }
1552
1553    Ok(())
1554}
1555
1556// ============================================================================
1557// UpdatePayload Encoder
1558// ============================================================================
1559
1560/// Encode a SetPayload request to protobuf wire format.
1561///
1562/// ```text
1563/// message SetPayloadPoints {
1564///   string collection_name = 1;
1565///   bool wait = 2;
1566///   map<string, Value> payload = 3;
1567///   PointsSelector points_selector = 5;
1568/// }
1569/// ```
1570pub fn encode_set_payload_proto(
1571    buf: &mut BytesMut,
1572    collection: &str,
1573    point_ids: &[crate::PointId],
1574    payload: &crate::point::Payload,
1575    wait: bool,
1576) -> QdrantResult<()> {
1577    ensure_collection_name(collection)?;
1578    ensure_point_ids(point_ids, "payload update")?;
1579    if payload.is_empty() {
1580        return Err(encode_error("Qdrant payload update must not be empty"));
1581    }
1582    ensure_payload(payload, "payload update")?;
1583
1584    buf.clear();
1585
1586    // Field 1: collection_name
1587    buf.put_u8(0x0A);
1588    encode_varint(buf, collection.len());
1589    buf.extend_from_slice(collection.as_bytes());
1590
1591    // Field 2: wait
1592    if wait {
1593        buf.put_u8(0x10);
1594        buf.put_u8(0x01);
1595    }
1596
1597    // Field 3: payload (map<string, Value>)
1598    for (key, value) in payload {
1599        ensure_payload_key(key)?;
1600        let mut entry_buf = BytesMut::with_capacity(key.len() + 32);
1601        entry_buf.put_u8(0x0A); // key (field 1)
1602        encode_varint(&mut entry_buf, key.len());
1603        entry_buf.extend_from_slice(key.as_bytes());
1604
1605        let val_buf = encode_payload_value(value)?;
1606        entry_buf.put_u8(0x12); // value (field 2)
1607        encode_varint(&mut entry_buf, val_buf.len());
1608        entry_buf.extend_from_slice(&val_buf);
1609
1610        buf.put_u8(0x1A); // field 3 (payload map entry)
1611        encode_varint(buf, entry_buf.len());
1612        buf.extend_from_slice(&entry_buf);
1613    }
1614
1615    // Field 5: points_selector -> PointsIdsList
1616    let selector_buf = encode_points_selector(point_ids);
1617    buf.put_u8(0x2A); // (5 << 3) | 2 = 0x2A
1618    encode_varint(buf, selector_buf.len());
1619    buf.extend_from_slice(&selector_buf);
1620    Ok(())
1621}
1622
1623// ============================================================================
1624// CreateFieldIndex Encoder
1625// ============================================================================
1626
1627/// Encode a CreateFieldIndexCollection request.
1628///
1629/// ```text
1630/// message CreateFieldIndexCollection {
1631///   string collection_name = 1;
1632///   bool wait = 2;
1633///   string field_name = 3;
1634///   optional FieldType field_type = 4;
1635/// }
1636/// ```
1637///
1638/// FieldType enum: 0=Keyword, 1=Integer, 2=Float, 3=Geo, 4=Text, 5=Bool, 6=Datetime
1639pub fn encode_create_field_index_proto(
1640    buf: &mut BytesMut,
1641    collection: &str,
1642    field_name: &str,
1643    field_type: FieldType,
1644    wait: bool,
1645) -> QdrantResult<()> {
1646    ensure_collection_name(collection)?;
1647    ensure_payload_key(field_name)?;
1648
1649    buf.clear();
1650
1651    // Field 1: collection_name
1652    buf.put_u8(0x0A);
1653    encode_varint(buf, collection.len());
1654    buf.extend_from_slice(collection.as_bytes());
1655
1656    // Field 2: wait
1657    if wait {
1658        buf.put_u8(0x10);
1659        buf.put_u8(0x01);
1660    }
1661
1662    // Field 3: field_name
1663    buf.put_u8(0x1A);
1664    encode_varint(buf, field_name.len());
1665    buf.extend_from_slice(field_name.as_bytes());
1666
1667    // Field 4: field_type (optional enum)
1668    buf.put_u8(0x20); // (4 << 3) | 0 = 0x20
1669    encode_varint(buf, field_type as usize);
1670    Ok(())
1671}
1672
1673/// Qdrant payload index field types.
1674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1675#[repr(u8)]
1676pub enum FieldType {
1677    /// Keyword (exact-match string) index.
1678    Keyword = 0,
1679    /// Integer index.
1680    Integer = 1,
1681    /// Float index.
1682    Float = 2,
1683    /// Geolocation index.
1684    Geo = 3,
1685    /// Full-text search index.
1686    Text = 4,
1687    /// Boolean index.
1688    Bool = 5,
1689    /// Datetime index.
1690    Datetime = 6,
1691}
1692
1693// ============================================================================
1694// CreateCollection Field Tags
1695// ============================================================================
1696
1697/// Field 1: collection_name (string) -> 0x0A
1698const CREATE_COLLECTION_NAME: u8 = 0x0A;
1699/// Field 10: vectors_config (VectorsConfig) -> (10 << 3) | 2 = 0x52
1700const CREATE_VECTORS_CONFIG: u8 = 0x52;
1701/// Field 8: on_disk_payload (bool) -> (8 << 3) | 0 = 0x40
1702const CREATE_ON_DISK: u8 = 0x40;
1703
1704// ============================================================================
1705// DeleteCollection Field Tags
1706// ============================================================================
1707
1708/// Field 1: collection_name (string) -> 0x0A
1709const DELETE_COLLECTION_NAME: u8 = 0x0A;
1710
1711/// Encode CreateCollection request to protobuf wire format.
1712pub fn encode_create_collection_proto(
1713    buf: &mut BytesMut,
1714    collection_name: &str,
1715    vector_size: u64,
1716    distance: crate::Distance,
1717    on_disk: bool,
1718) -> QdrantResult<()> {
1719    ensure_collection_name(collection_name)?;
1720    if vector_size == 0 {
1721        return Err(encode_error(
1722            "Qdrant collection vector_size must be greater than zero",
1723        ));
1724    }
1725
1726    buf.clear();
1727
1728    // Field 1: collection_name
1729    buf.put_u8(CREATE_COLLECTION_NAME);
1730    encode_varint(buf, collection_name.len());
1731    buf.extend_from_slice(collection_name.as_bytes());
1732
1733    // Field 2: vectors_config (VectorsConfig -> VectorParams)
1734    let mut params_buf = BytesMut::with_capacity(32);
1735
1736    // VectorParams.size (field 1, uint64)
1737    params_buf.put_u8(0x08);
1738    encode_varint_u64(&mut params_buf, vector_size);
1739
1740    // VectorParams.distance (field 2, enum)
1741    params_buf.put_u8(0x10);
1742    let distance_val = match distance {
1743        crate::Distance::Cosine => 1,
1744        crate::Distance::Euclidean => 2,
1745        crate::Distance::Dot => 3,
1746    };
1747    encode_varint(&mut params_buf, distance_val);
1748
1749    // VectorParams.on_disk (field 5, bool)
1750    if on_disk {
1751        params_buf.put_u8(0x28);
1752        params_buf.put_u8(0x01);
1753    }
1754
1755    // VectorsConfig.params (field 1) wraps VectorParams
1756    let mut config_buf = BytesMut::with_capacity(params_buf.len() + 4);
1757    config_buf.put_u8(0x0A);
1758    encode_varint(&mut config_buf, params_buf.len());
1759    config_buf.extend_from_slice(&params_buf);
1760
1761    // Write to main buffer
1762    buf.put_u8(CREATE_VECTORS_CONFIG);
1763    encode_varint(buf, config_buf.len());
1764    buf.extend_from_slice(&config_buf);
1765
1766    if on_disk {
1767        buf.put_u8(CREATE_ON_DISK);
1768        buf.put_u8(0x01);
1769    }
1770    Ok(())
1771}
1772
1773/// Encode DeleteCollection request.
1774pub fn encode_delete_collection_proto(
1775    buf: &mut BytesMut,
1776    collection_name: &str,
1777) -> QdrantResult<()> {
1778    ensure_collection_name(collection_name)?;
1779    buf.clear();
1780    buf.put_u8(DELETE_COLLECTION_NAME);
1781    encode_varint(buf, collection_name.len());
1782    buf.extend_from_slice(collection_name.as_bytes());
1783    Ok(())
1784}
1785
1786// ============================================================================
1787// DeletePoints Encoder (supports both numeric and UUID IDs)
1788// ============================================================================
1789
1790/// Encode a DeletePoints request with support for both numeric and UUID point IDs.
1791///
1792/// ```text
1793/// message DeletePoints {
1794///   string collection_name = 1;
1795///   bool wait = 2;
1796///   PointsSelector points = 4;
1797/// }
1798/// ```
1799pub fn encode_delete_points_mixed_proto(
1800    buf: &mut BytesMut,
1801    collection_name: &str,
1802    point_ids: &[crate::PointId],
1803) -> QdrantResult<()> {
1804    ensure_collection_name(collection_name)?;
1805    ensure_point_ids(point_ids, "delete")?;
1806
1807    buf.clear();
1808
1809    // Field 1: collection_name
1810    buf.put_u8(0x0A);
1811    encode_varint(buf, collection_name.len());
1812    buf.put_slice(collection_name.as_bytes());
1813
1814    // Field 2: wait = true
1815    buf.put_u8(0x10);
1816    buf.put_u8(1);
1817
1818    // Field 4: points (PointsSelector -> PointsIdsList)
1819    let selector_buf = encode_points_selector(point_ids);
1820    buf.put_u8(0x22);
1821    encode_varint(buf, selector_buf.len());
1822    buf.extend_from_slice(&selector_buf);
1823    Ok(())
1824}
1825
1826/// Legacy: Encode a DeletePoints request (numeric IDs only).
1827pub fn encode_delete_points_proto(
1828    buf: &mut BytesMut,
1829    collection_name: &str,
1830    point_ids: &[u64],
1831) -> QdrantResult<()> {
1832    let ids: Vec<crate::PointId> = point_ids
1833        .iter()
1834        .map(|&id| crate::PointId::Num(id))
1835        .collect();
1836    encode_delete_points_mixed_proto(buf, collection_name, &ids)
1837}
1838
1839/// Encode PointsSelector containing a PointsIdsList.
1840fn encode_points_selector(ids: &[crate::PointId]) -> BytesMut {
1841    // Build PointsIdsList (field 1: repeated PointId)
1842    let mut ids_list = BytesMut::with_capacity(ids.len() * 40);
1843    for id in ids {
1844        let id_buf = encode_point_id_message(id);
1845        ids_list.put_u8(0x0A); // PointId message (field 1, LEN)
1846        encode_varint(&mut ids_list, id_buf.len());
1847        ids_list.extend_from_slice(&id_buf);
1848    }
1849
1850    // PointsSelector.points (field 1 = PointsIdsList)
1851    let mut selector = BytesMut::with_capacity(ids_list.len() + 8);
1852    selector.put_u8(0x0A); // field 1, LEN
1853    encode_varint(&mut selector, ids_list.len());
1854    selector.extend_from_slice(&ids_list);
1855
1856    selector
1857}
1858
1859// ============================================================================
1860// ListCollections Encoder
1861// ============================================================================
1862
1863/// Encode a ListCollections request (empty message).
1864pub fn encode_list_collections_proto(buf: &mut BytesMut) {
1865    buf.clear();
1866    // ListCollectionsRequest is an empty message
1867}
1868
1869/// Encode a GetCollectionInfo request.
1870///
1871/// ```text
1872/// message GetCollectionInfoRequest {
1873///   string collection_name = 1;
1874/// }
1875/// ```
1876pub fn encode_collection_info_proto(buf: &mut BytesMut, collection_name: &str) -> QdrantResult<()> {
1877    ensure_collection_name(collection_name)?;
1878    buf.clear();
1879    buf.put_u8(0x0A);
1880    encode_varint(buf, collection_name.len());
1881    buf.extend_from_slice(collection_name.as_bytes());
1882    Ok(())
1883}
1884
1885// ============================================================================
1886// Utility
1887// ============================================================================
1888
1889/// Calculate the byte length of a varint.
1890#[inline]
1891pub fn varint_len(value: u64) -> usize {
1892    if value == 0 {
1893        1
1894    } else {
1895        let bits = 64 - value.leading_zeros() as usize;
1896        bits.div_ceil(7)
1897    }
1898}
1899
1900// ============================================================================
1901// Tests
1902// ============================================================================
1903
1904#[cfg(test)]
1905mod tests {
1906    use super::*;
1907
1908    fn assert_encode_error<T: std::fmt::Debug>(result: QdrantResult<T>, expected: &str) {
1909        match result {
1910            Err(QdrantError::Encode(message)) => {
1911                assert!(
1912                    message.contains(expected),
1913                    "expected error containing {expected:?}, got {message:?}"
1914                );
1915            }
1916            other => panic!("expected encode error containing {expected:?}, got {other:?}"),
1917        }
1918    }
1919
1920    #[test]
1921    fn test_varint_encoding() {
1922        let mut buf = BytesMut::new();
1923
1924        // Single byte
1925        encode_varint(&mut buf, 1);
1926        assert_eq!(&buf[..], &[0x01]);
1927
1928        buf.clear();
1929        encode_varint(&mut buf, 127);
1930        assert_eq!(&buf[..], &[0x7F]);
1931
1932        // Two bytes
1933        buf.clear();
1934        encode_varint(&mut buf, 128);
1935        assert_eq!(&buf[..], &[0x80, 0x01]);
1936
1937        buf.clear();
1938        encode_varint(&mut buf, 300);
1939        assert_eq!(&buf[..], &[0xAC, 0x02]);
1940    }
1941
1942    #[test]
1943    fn test_qdrant_filter_wire_tags_match_current_proto() {
1944        assert_eq!(FILTER_SHOULD, 0x0A);
1945        assert_eq!(FILTER_MUST, 0x12);
1946        assert_eq!(FILTER_MUST_NOT, 0x1A);
1947        assert_eq!(CONDITION_HAS_ID, 0x1A);
1948        assert_eq!(CONDITION_IS_NULL, 0x2A);
1949    }
1950
1951    #[test]
1952    fn test_encode_search_basic() {
1953        let mut buf = BytesMut::with_capacity(1024);
1954        let vector = vec![0.1f32, 0.2, 0.3, 0.4];
1955
1956        encode_search_proto(&mut buf, "test_collection", &vector, 10, None, None, false)
1957            .expect("search request should encode");
1958
1959        // Verify starts with collection name field
1960        assert_eq!(buf[0], SEARCH_COLLECTION);
1961
1962        // Verify buffer is not empty
1963        assert!(buf.len() > 20);
1964    }
1965
1966    #[test]
1967    fn test_encode_search_rejects_invalid_request_shape() {
1968        let mut buf = BytesMut::with_capacity(1024);
1969        let vector = vec![0.1f32, 0.2, 0.3, 0.4];
1970
1971        assert_encode_error(
1972            encode_search_proto(&mut buf, "", &vector, 10, None, None, false),
1973            "collection name",
1974        );
1975        assert_encode_error(
1976            encode_search_proto(&mut buf, "products", &[], 10, None, None, false),
1977            "search vector",
1978        );
1979        assert_encode_error(
1980            encode_search_proto(&mut buf, "products", &[f32::NAN], 10, None, None, false),
1981            "non-finite vector value",
1982        );
1983        assert_encode_error(
1984            encode_search_proto(&mut buf, "products", &vector, 0, None, None, false),
1985            "search limit",
1986        );
1987        assert_encode_error(
1988            encode_search_proto(
1989                &mut buf,
1990                "products",
1991                &vector,
1992                10,
1993                Some(f32::INFINITY),
1994                None,
1995                false,
1996            ),
1997            "score threshold",
1998        );
1999        assert_encode_error(
2000            encode_search_proto(&mut buf, "products", &vector, 10, None, Some(" "), false),
2001            "vector name",
2002        );
2003    }
2004
2005    #[test]
2006    fn test_encode_search_with_vectors_selector() {
2007        let mut buf = BytesMut::with_capacity(1024);
2008        let vector = vec![0.1f32, 0.2, 0.3, 0.4];
2009
2010        encode_search_proto(&mut buf, "test_collection", &vector, 10, None, None, true)
2011            .expect("search request should encode");
2012
2013        let selector_offset = buf
2014            .iter()
2015            .position(|tag| *tag == SEARCH_WITH_VECTORS)
2016            .expect("search request should include with_vectors field");
2017        assert_eq!(
2018            &buf[selector_offset..selector_offset + 4],
2019            &[SEARCH_WITH_VECTORS, 0x02, 0x08, 0x01]
2020        );
2021    }
2022
2023    #[test]
2024    fn test_encode_named_search_includes_only_named_vector_selector() {
2025        let mut buf = BytesMut::with_capacity(1024);
2026        let vector = vec![0.1f32, 0.2, 0.3, 0.4];
2027
2028        encode_search_proto(
2029            &mut buf,
2030            "test_collection",
2031            &vector,
2032            10,
2033            None,
2034            Some("image"),
2035            true,
2036        )
2037        .expect("named search request should encode");
2038
2039        let selector_offset = buf
2040            .iter()
2041            .position(|tag| *tag == SEARCH_WITH_VECTORS)
2042            .expect("named search request should include with_vectors field");
2043        assert_eq!(
2044            &buf[selector_offset..selector_offset + 11],
2045            &[
2046                SEARCH_WITH_VECTORS,
2047                0x09,
2048                0x12,
2049                0x07,
2050                0x0A,
2051                0x05,
2052                b'i',
2053                b'm',
2054                b'a',
2055                b'g',
2056                b'e'
2057            ]
2058        );
2059    }
2060
2061    #[test]
2062    fn test_zero_copy_vector() {
2063        let mut buf = BytesMut::with_capacity(1024);
2064        let vector = vec![1.0f32, 2.0, 3.0, 4.0];
2065
2066        encode_search_proto(&mut buf, "test", &vector, 5, None, None, false)
2067            .expect("search request should encode");
2068
2069        // Find where vector data starts (after collection name + vector tag + length)
2070        // collection: 0x0A, len(4), "test" = 6 bytes
2071        // vector tag: 0x12 = 1 byte
2072        // vector len: 16 (4 floats * 4 bytes) = 1 byte varint
2073        // Total header: 8 bytes
2074        let vector_start = 8;
2075        let vector_bytes = &buf[vector_start..vector_start + 16];
2076
2077        // Verify floats are correctly encoded as little-endian bytes
2078        let float_bytes: [u8; 4] = 1.0f32.to_le_bytes();
2079        assert_eq!(&vector_bytes[0..4], &float_bytes);
2080    }
2081
2082    #[test]
2083    fn test_varint_len() {
2084        assert_eq!(varint_len(0), 1);
2085        assert_eq!(varint_len(1), 1);
2086        assert_eq!(varint_len(127), 1);
2087        assert_eq!(varint_len(128), 2);
2088        assert_eq!(varint_len(16383), 2);
2089        assert_eq!(varint_len(16384), 3);
2090    }
2091
2092    #[test]
2093    fn test_encode_search_with_filter() {
2094        use qail_core::ast::{Condition, Expr, Operator, Value};
2095
2096        let mut buf = BytesMut::with_capacity(1024);
2097        let vector = vec![0.1f32, 0.2, 0.3];
2098        let conditions = vec![
2099            Condition {
2100                left: Expr::Named("category".to_string()),
2101                op: Operator::Eq,
2102                value: Value::String("electronics".to_string()),
2103                is_array_unnest: false,
2104            },
2105            Condition {
2106                left: Expr::Named("price".to_string()),
2107                op: Operator::Lt,
2108                value: Value::Int(1000),
2109                is_array_unnest: false,
2110            },
2111        ];
2112
2113        encode_search_with_filter_proto(
2114            &mut buf,
2115            SearchRequest {
2116                collection: "products",
2117                vector: &vector,
2118                limit: 10,
2119                score_threshold: None,
2120                vector_name: None,
2121                with_vectors: false,
2122            },
2123            &conditions,
2124            false,
2125        )
2126        .expect("filter encoding should succeed");
2127
2128        // Should contain collection, vector, filter, limit, with_payload
2129        assert!(buf.len() > 50);
2130        // First byte should be collection tag
2131        assert_eq!(buf[0], SEARCH_COLLECTION);
2132        // Filter tag (0x1A) should appear somewhere after vector
2133        assert!(buf.contains(&SEARCH_FILTER));
2134    }
2135
2136    #[test]
2137    fn test_encode_filtered_search_with_vectors_selector() {
2138        use qail_core::ast::{Condition, Expr, Operator, Value};
2139
2140        let mut buf = BytesMut::with_capacity(1024);
2141        let vector = vec![0.1f32, 0.2, 0.3];
2142        let conditions = vec![Condition {
2143            left: Expr::Named("tenant_id".to_string()),
2144            op: Operator::Eq,
2145            value: Value::String("tenant-1".to_string()),
2146            is_array_unnest: false,
2147        }];
2148
2149        encode_search_with_filter_proto(
2150            &mut buf,
2151            SearchRequest {
2152                collection: "products",
2153                vector: &vector,
2154                limit: 10,
2155                score_threshold: None,
2156                vector_name: None,
2157                with_vectors: true,
2158            },
2159            &conditions,
2160            false,
2161        )
2162        .expect("filter encoding should succeed");
2163
2164        assert!(
2165            buf.contains(&SEARCH_WITH_VECTORS),
2166            "filtered search should preserve the with_vectors selector"
2167        );
2168    }
2169
2170    #[test]
2171    fn test_encode_upsert_rejects_invalid_points_and_payload() {
2172        let mut buf = BytesMut::with_capacity(1024);
2173
2174        assert_encode_error(
2175            encode_upsert_proto(&mut buf, "", &[crate::Point::new_num(1, vec![1.0])], true),
2176            "collection name",
2177        );
2178        assert_encode_error(
2179            encode_upsert_proto(&mut buf, "products", &[], true),
2180            "point list",
2181        );
2182        assert_encode_error(
2183            encode_upsert_proto(
2184                &mut buf,
2185                "products",
2186                &[crate::Point::new(
2187                    crate::PointId::Uuid(" ".to_string()),
2188                    vec![1.0],
2189                )],
2190                true,
2191            ),
2192            "upsert",
2193        );
2194        assert_encode_error(
2195            encode_upsert_proto(
2196                &mut buf,
2197                "products",
2198                &[crate::Point::new_num(1, vec![])],
2199                true,
2200            ),
2201            "upsert point 0 vector",
2202        );
2203        assert_encode_error(
2204            encode_upsert_proto(
2205                &mut buf,
2206                "products",
2207                &[crate::Point::new_num(1, vec![f32::INFINITY])],
2208                true,
2209            ),
2210            "non-finite vector value",
2211        );
2212
2213        let point_with_blank_key = crate::Point::new_num(1, vec![1.0])
2214            .with_payload(" ", crate::point::PayloadValue::String("bad".to_string()));
2215        assert_encode_error(
2216            encode_upsert_proto(&mut buf, "products", &[point_with_blank_key], true),
2217            "payload field name",
2218        );
2219
2220        let mut nested = crate::point::Payload::new();
2221        nested.insert(
2222            "".to_string(),
2223            crate::point::PayloadValue::String("bad".to_string()),
2224        );
2225        let point_with_nested_blank_key = crate::Point::new_num(1, vec![1.0])
2226            .with_payload("meta", crate::point::PayloadValue::Object(nested));
2227        assert_encode_error(
2228            encode_upsert_proto(&mut buf, "products", &[point_with_nested_blank_key], true),
2229            "payload field name",
2230        );
2231
2232        let point_with_bad_float = crate::Point::new_num(1, vec![1.0]).with_payload(
2233            "score",
2234            crate::point::PayloadValue::List(vec![crate::point::PayloadValue::Float(f64::NAN)]),
2235        );
2236        assert_encode_error(
2237            encode_upsert_proto(&mut buf, "products", &[point_with_bad_float], true),
2238            "payload float",
2239        );
2240    }
2241
2242    #[test]
2243    fn test_encode_get_points() {
2244        let mut buf = BytesMut::with_capacity(1024);
2245        let ids = vec![
2246            crate::PointId::Num(42),
2247            crate::PointId::Uuid("abc-123".to_string()),
2248        ];
2249
2250        encode_get_points_proto(&mut buf, "my_collection", &ids, true)
2251            .expect("get request should encode");
2252
2253        assert_eq!(buf[0], 0x0A); // collection name tag
2254        assert!(buf.len() > 20);
2255    }
2256
2257    #[test]
2258    fn test_encode_point_selector_requests_reject_empty_or_blank_ids() {
2259        let mut buf = BytesMut::with_capacity(1024);
2260        let blank_id = vec![crate::PointId::Uuid(" ".to_string())];
2261        let good_id = vec![crate::PointId::Num(1)];
2262        let mut payload = crate::point::Payload::new();
2263        payload.insert(
2264            "name".to_string(),
2265            crate::point::PayloadValue::String("x".to_string()),
2266        );
2267
2268        assert_encode_error(
2269            encode_get_points_proto(&mut buf, "products", &[], false),
2270            "point id list",
2271        );
2272        assert_encode_error(
2273            encode_get_points_proto(&mut buf, "products", &blank_id, false),
2274            "get",
2275        );
2276        assert_encode_error(
2277            encode_delete_points_mixed_proto(&mut buf, "products", &[]),
2278            "point id list",
2279        );
2280        assert_encode_error(
2281            encode_delete_points_mixed_proto(&mut buf, "products", &blank_id),
2282            "delete",
2283        );
2284        assert_encode_error(
2285            encode_set_payload_proto(&mut buf, "products", &[], &payload, true),
2286            "point id list",
2287        );
2288        assert_encode_error(
2289            encode_set_payload_proto(
2290                &mut buf,
2291                "products",
2292                &good_id,
2293                &crate::point::Payload::new(),
2294                true,
2295            ),
2296            "payload update",
2297        );
2298        assert_encode_error(
2299            encode_scroll_points_proto(&mut buf, "products", 0, None, false),
2300            "scroll limit",
2301        );
2302        assert_encode_error(
2303            encode_scroll_points_proto(&mut buf, "products", 10, Some(&blank_id[0]), false),
2304            "scroll offset",
2305        );
2306    }
2307
2308    #[test]
2309    fn test_encode_scroll_points() {
2310        let mut buf = BytesMut::with_capacity(1024);
2311
2312        encode_scroll_points_proto(&mut buf, "my_collection", 100, None, false)
2313            .expect("scroll request should encode");
2314
2315        assert_eq!(buf[0], 0x0A);
2316        assert!(buf.len() > 10);
2317    }
2318
2319    #[test]
2320    fn test_encode_filtered_scroll_points() {
2321        use qail_core::ast::{Condition, Expr, Operator, Value};
2322
2323        let mut buf = BytesMut::with_capacity(1024);
2324        let must = vec![Condition {
2325            left: Expr::Named("tenant_id".to_string()),
2326            op: Operator::Eq,
2327            value: Value::String("tenant-1".to_string()),
2328            is_array_unnest: false,
2329        }];
2330
2331        encode_scroll_points_with_filter_grouped_cages_proto(
2332            &mut buf,
2333            "my_collection",
2334            100,
2335            None,
2336            false,
2337            &must,
2338            &[],
2339        )
2340        .expect("filtered scroll should encode");
2341
2342        assert_eq!(buf[0], SCROLL_COLLECTION);
2343        assert!(
2344            buf.contains(&SCROLL_FILTER),
2345            "filtered scroll should include filter field"
2346        );
2347    }
2348
2349    #[test]
2350    fn test_encode_delete_points_uuid() {
2351        let mut buf = BytesMut::with_capacity(1024);
2352        let ids = vec![
2353            crate::PointId::Uuid("test-uuid-1".to_string()),
2354            crate::PointId::Num(99),
2355        ];
2356
2357        encode_delete_points_mixed_proto(&mut buf, "products", &ids)
2358            .expect("delete request should encode");
2359
2360        assert_eq!(buf[0], 0x0A); // collection name
2361        assert!(buf.len() > 20);
2362    }
2363
2364    #[test]
2365    fn test_encode_set_payload() {
2366        let mut buf = BytesMut::with_capacity(1024);
2367        let ids = vec![crate::PointId::Num(1)];
2368        let mut payload = crate::point::Payload::new();
2369        payload.insert(
2370            "name".to_string(),
2371            crate::point::PayloadValue::String("updated".to_string()),
2372        );
2373
2374        encode_set_payload_proto(&mut buf, "my_col", &ids, &payload, true)
2375            .expect("set payload request should encode");
2376
2377        assert_eq!(buf[0], 0x0A);
2378        assert!(buf.len() > 15);
2379    }
2380
2381    #[test]
2382    fn test_encode_search_with_filter_rejects_unsupported_operator() {
2383        use qail_core::ast::{Condition, Expr, Operator, Value};
2384
2385        let mut buf = BytesMut::with_capacity(512);
2386        let vector = vec![0.1f32, 0.2];
2387        let conditions = vec![Condition {
2388            left: Expr::Named("status".to_string()),
2389            op: Operator::NotILike,
2390            value: Value::String("%inactive%".to_string()),
2391            is_array_unnest: false,
2392        }];
2393
2394        let err = encode_search_with_filter_proto(
2395            &mut buf,
2396            SearchRequest {
2397                collection: "products",
2398                vector: &vector,
2399                limit: 5,
2400                score_threshold: None,
2401                vector_name: None,
2402                with_vectors: false,
2403            },
2404            &conditions,
2405            false,
2406        )
2407        .expect_err("unsupported operator must return an explicit error");
2408
2409        match err {
2410            QdrantError::Encode(message) => {
2411                assert!(message.contains("Unsupported Qdrant filter condition"));
2412            }
2413            other => panic!("expected encode error, got {:?}", other),
2414        }
2415    }
2416
2417    #[test]
2418    fn test_encode_search_with_filter_supports_is_null() {
2419        use qail_core::ast::{Condition, Expr, Operator, Value};
2420
2421        for value in [Value::Null, Value::NullUuid] {
2422            let mut buf = BytesMut::with_capacity(512);
2423            let vector = vec![0.1f32, 0.2];
2424            let conditions = vec![Condition {
2425                left: Expr::Named("tenant_id".to_string()),
2426                op: Operator::IsNull,
2427                value,
2428                is_array_unnest: false,
2429            }];
2430
2431            encode_search_with_filter_proto(
2432                &mut buf,
2433                SearchRequest {
2434                    collection: "products",
2435                    vector: &vector,
2436                    limit: 5,
2437                    score_threshold: None,
2438                    vector_name: None,
2439                    with_vectors: false,
2440                },
2441                &conditions,
2442                false,
2443            )
2444            .expect("IS NULL filters should encode as Qdrant IsNullCondition");
2445
2446            assert!(
2447                buf.contains(&CONDITION_IS_NULL),
2448                "encoded request should contain an IsNullCondition"
2449            );
2450        }
2451    }
2452
2453    #[test]
2454    fn test_encode_search_with_filter_uses_has_id_for_point_id() {
2455        use qail_core::ast::{Condition, Expr, Operator, Value};
2456
2457        let condition = Condition {
2458            left: Expr::Named("ID".to_string()),
2459            op: Operator::Eq,
2460            value: Value::Int(42),
2461            is_array_unnest: false,
2462        };
2463
2464        let encoded = encode_condition_message(&condition).expect("id filter should encode");
2465
2466        assert_eq!(encoded[0], CONDITION_HAS_ID);
2467        assert!(
2468            encoded
2469                .windows(2)
2470                .any(|window| window == [POINT_ID_NUM, 42]),
2471            "encoded HasIdCondition should contain numeric PointId"
2472        );
2473
2474        let condition = Condition {
2475            left: Expr::Named("id".to_string()),
2476            op: Operator::In,
2477            value: Value::Array(vec![
2478                Value::Int(42),
2479                Value::String("uuid-like-id".to_string()),
2480            ]),
2481            is_array_unnest: false,
2482        };
2483
2484        let encoded = encode_condition_message(&condition).expect("id IN filter should encode");
2485
2486        assert_eq!(encoded[0], CONDITION_HAS_ID);
2487        assert!(
2488            encoded
2489                .windows(2)
2490                .any(|window| window == [POINT_ID_NUM, 42])
2491        );
2492        assert!(
2493            encoded
2494                .windows("uuid-like-id".len())
2495                .any(|window| window == b"uuid-like-id")
2496        );
2497    }
2498
2499    #[test]
2500    fn test_encode_search_with_filter_rejects_invalid_point_id_filter() {
2501        use qail_core::ast::{Condition, Expr, Operator, Value};
2502
2503        let condition = Condition {
2504            left: Expr::Named("id".to_string()),
2505            op: Operator::Eq,
2506            value: Value::Float(1.5),
2507            is_array_unnest: false,
2508        };
2509
2510        let err = encode_condition_message(&condition)
2511            .expect_err("float id filters must not encode as payload filters");
2512
2513        match err {
2514            QdrantError::Encode(message) => {
2515                assert!(message.contains("id filters support only"));
2516            }
2517            other => panic!("expected encode error, got {:?}", other),
2518        }
2519
2520        let condition = Condition {
2521            left: Expr::Named("id".to_string()),
2522            op: Operator::In,
2523            value: Value::Array(vec![]),
2524            is_array_unnest: false,
2525        };
2526        assert_encode_error(encode_condition_message(&condition), "id IN filters");
2527    }
2528
2529    #[test]
2530    fn test_encode_search_with_filter_rejects_invalid_values() {
2531        use qail_core::ast::{Condition, Expr, Operator, Value};
2532
2533        let mut buf = BytesMut::with_capacity(512);
2534        let vector = vec![0.1f32, 0.2];
2535
2536        let unsupported_float_match = vec![Condition {
2537            left: Expr::Named("score".to_string()),
2538            op: Operator::Eq,
2539            value: Value::Float(1.5),
2540            is_array_unnest: false,
2541        }];
2542        assert_encode_error(
2543            encode_search_with_filter_proto(
2544                &mut buf,
2545                SearchRequest {
2546                    collection: "products",
2547                    vector: &vector,
2548                    limit: 5,
2549                    score_threshold: None,
2550                    vector_name: None,
2551                    with_vectors: false,
2552                },
2553                &unsupported_float_match,
2554                false,
2555            ),
2556            "Unsupported Qdrant filter condition",
2557        );
2558
2559        let non_finite_range = vec![Condition {
2560            left: Expr::Named("score".to_string()),
2561            op: Operator::Gt,
2562            value: Value::Float(f64::INFINITY),
2563            is_array_unnest: false,
2564        }];
2565        assert_encode_error(
2566            encode_search_with_filter_proto(
2567                &mut buf,
2568                SearchRequest {
2569                    collection: "products",
2570                    vector: &vector,
2571                    limit: 5,
2572                    score_threshold: None,
2573                    vector_name: None,
2574                    with_vectors: false,
2575                },
2576                &non_finite_range,
2577                false,
2578            ),
2579            "filter range float",
2580        );
2581
2582        let empty_text = vec![Condition {
2583            left: Expr::Named("description".to_string()),
2584            op: Operator::Contains,
2585            value: Value::String("".to_string()),
2586            is_array_unnest: false,
2587        }];
2588        assert_encode_error(
2589            encode_search_with_filter_proto(
2590                &mut buf,
2591                SearchRequest {
2592                    collection: "products",
2593                    vector: &vector,
2594                    limit: 5,
2595                    score_threshold: None,
2596                    vector_name: None,
2597                    with_vectors: false,
2598                },
2599                &empty_text,
2600                false,
2601            ),
2602            "text filter value",
2603        );
2604
2605        let empty_id = Condition {
2606            left: Expr::Named("id".to_string()),
2607            op: Operator::Eq,
2608            value: Value::String(" ".to_string()),
2609            is_array_unnest: false,
2610        };
2611        assert_encode_error(encode_condition_message(&empty_id), "id filter");
2612    }
2613
2614    #[test]
2615    fn test_encode_search_with_filter_uses_current_match_wire_tags() {
2616        use qail_core::ast::{Condition, Expr, Operator, Value};
2617
2618        let bool_condition = Condition {
2619            left: Expr::Named("archived".to_string()),
2620            op: Operator::Eq,
2621            value: Value::Bool(false),
2622            is_array_unnest: false,
2623        };
2624        let encoded = encode_condition_message(&bool_condition).expect("bool match should encode");
2625        assert!(
2626            encoded.windows(2).any(|window| window == [0x18, 0x00]),
2627            "bool match should use Match.boolean field 3"
2628        );
2629
2630        let text_condition = Condition {
2631            left: Expr::Named("summary".to_string()),
2632            op: Operator::Contains,
2633            value: Value::String("refund".to_string()),
2634            is_array_unnest: false,
2635        };
2636        let encoded = encode_condition_message(&text_condition).expect("text match should encode");
2637        assert!(
2638            encoded
2639                .windows(8)
2640                .any(|window| window == [0x22, 0x06, b'r', b'e', b'f', b'u', b'n', b'd']),
2641            "text match should use Match.text field 4"
2642        );
2643    }
2644
2645    #[test]
2646    fn test_encode_search_with_filter_supports_uuid_payload_keywords() {
2647        use qail_core::ast::{Condition, Expr, Operator, Value};
2648
2649        let owner_id = uuid::Uuid::parse_str("aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa").unwrap();
2650        let owner_condition = Condition {
2651            left: Expr::Named("owner_id".to_string()),
2652            op: Operator::Eq,
2653            value: Value::Uuid(owner_id),
2654            is_array_unnest: false,
2655        };
2656        let encoded =
2657            encode_condition_message(&owner_condition).expect("uuid payload match should encode");
2658        let owner_id = owner_id.to_string();
2659        assert!(
2660            encoded
2661                .windows(36)
2662                .any(|window| window == owner_id.as_bytes()),
2663            "uuid equality should encode as a keyword string"
2664        );
2665
2666        let reviewer_id = uuid::Uuid::parse_str("bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb").unwrap();
2667        let reviewer_condition = Condition {
2668            left: Expr::Named("reviewer_id".to_string()),
2669            op: Operator::In,
2670            value: Value::Array(vec![
2671                Value::Uuid(reviewer_id),
2672                Value::String("external-reviewer".to_string()),
2673            ]),
2674            is_array_unnest: false,
2675        };
2676        let encoded =
2677            encode_condition_message(&reviewer_condition).expect("uuid IN match should encode");
2678        let reviewer_id = reviewer_id.to_string();
2679        assert!(
2680            encoded.contains(&0x2A),
2681            "uuid IN should use Match.keywords field 5"
2682        );
2683        assert!(
2684            encoded
2685                .windows(36)
2686                .any(|window| window == reviewer_id.as_bytes()),
2687            "uuid IN should include the UUID keyword"
2688        );
2689        assert!(
2690            encoded
2691                .windows("external-reviewer".len())
2692                .any(|window| window == b"external-reviewer"),
2693            "uuid IN should allow mixed keyword strings"
2694        );
2695    }
2696
2697    #[test]
2698    fn test_encode_search_with_filter_supports_native_in() {
2699        use qail_core::ast::{Condition, Expr, Operator, Value};
2700
2701        let string_condition = Condition {
2702            left: Expr::Named("status".to_string()),
2703            op: Operator::In,
2704            value: Value::Array(vec![
2705                Value::String("open".to_string()),
2706                Value::String("closed".to_string()),
2707            ]),
2708            is_array_unnest: false,
2709        };
2710        let encoded =
2711            encode_condition_message(&string_condition).expect("string IN match should encode");
2712        assert!(
2713            encoded.contains(&0x2A),
2714            "string IN should use Match.keywords field 5"
2715        );
2716        assert!(
2717            encoded.windows(4).any(|window| window == b"open"),
2718            "string IN should contain first keyword"
2719        );
2720
2721        let int_condition = Condition {
2722            left: Expr::Named("priority".to_string()),
2723            op: Operator::In,
2724            value: Value::Array(vec![Value::Int(1), Value::Int(2)]),
2725            is_array_unnest: false,
2726        };
2727        let encoded = encode_condition_message(&int_condition).expect("int IN match should encode");
2728        assert!(
2729            encoded.contains(&0x32),
2730            "integer IN should use Match.integers field 6"
2731        );
2732
2733        for bad in [
2734            Value::Array(vec![]),
2735            Value::Array(vec![Value::String("open".to_string()), Value::Int(1)]),
2736            Value::Array(vec![Value::Bool(true)]),
2737        ] {
2738            let condition = Condition {
2739                left: Expr::Named("status".to_string()),
2740                op: Operator::In,
2741                value: bad,
2742                is_array_unnest: false,
2743            };
2744            assert_encode_error(encode_condition_message(&condition), "IN filters");
2745        }
2746    }
2747
2748    #[test]
2749    fn test_encode_search_with_filter_supports_native_negative_filters() {
2750        use qail_core::ast::{Condition, Expr, Operator, Value};
2751
2752        for condition in [
2753            Condition {
2754                left: Expr::Named("status".to_string()),
2755                op: Operator::Ne,
2756                value: Value::String("deleted".to_string()),
2757                is_array_unnest: false,
2758            },
2759            Condition {
2760                left: Expr::Named("priority".to_string()),
2761                op: Operator::NotIn,
2762                value: Value::Array(vec![Value::Int(1), Value::Int(2)]),
2763                is_array_unnest: false,
2764            },
2765            Condition {
2766                left: Expr::Named("deleted_at".to_string()),
2767                op: Operator::IsNotNull,
2768                value: Value::Null,
2769                is_array_unnest: false,
2770            },
2771            Condition {
2772                left: Expr::Named("summary".to_string()),
2773                op: Operator::NotLike,
2774                value: Value::String("refund".to_string()),
2775                is_array_unnest: false,
2776            },
2777            Condition {
2778                left: Expr::Named("id".to_string()),
2779                op: Operator::NotIn,
2780                value: Value::Array(vec![
2781                    Value::Int(42),
2782                    Value::String("uuid-like-id".to_string()),
2783                ]),
2784                is_array_unnest: false,
2785            },
2786        ] {
2787            let encoded =
2788                encode_condition_message(&condition).expect("negative filter should encode");
2789
2790            assert_eq!(encoded[0], CONDITION_FILTER);
2791            assert!(
2792                encoded.contains(&FILTER_MUST_NOT),
2793                "negative filter must use Filter.must_not"
2794            );
2795        }
2796    }
2797
2798    #[test]
2799    fn test_encode_search_with_filter_grouped_cages_includes_nested_filter_conditions() {
2800        use qail_core::ast::{Condition, Expr, Operator, Value};
2801
2802        let mut buf = BytesMut::with_capacity(1024);
2803        let vector = vec![0.1f32, 0.2, 0.3];
2804        let must_conditions = vec![Condition {
2805            left: Expr::Named("tenant_id".to_string()),
2806            op: Operator::Eq,
2807            value: Value::String("t1".to_string()),
2808            is_array_unnest: false,
2809        }];
2810        let should_groups = vec![
2811            vec![
2812                Condition {
2813                    left: Expr::Named("city".to_string()),
2814                    op: Operator::Eq,
2815                    value: Value::String("London".to_string()),
2816                    is_array_unnest: false,
2817                },
2818                Condition {
2819                    left: Expr::Named("city".to_string()),
2820                    op: Operator::Eq,
2821                    value: Value::String("Paris".to_string()),
2822                    is_array_unnest: false,
2823                },
2824            ],
2825            vec![
2826                Condition {
2827                    left: Expr::Named("country".to_string()),
2828                    op: Operator::Eq,
2829                    value: Value::String("UK".to_string()),
2830                    is_array_unnest: false,
2831                },
2832                Condition {
2833                    left: Expr::Named("country".to_string()),
2834                    op: Operator::Eq,
2835                    value: Value::String("FR".to_string()),
2836                    is_array_unnest: false,
2837                },
2838            ],
2839        ];
2840
2841        encode_search_with_filter_grouped_cages_proto(
2842            &mut buf,
2843            SearchRequest {
2844                collection: "products",
2845                vector: &vector,
2846                limit: 10,
2847                score_threshold: None,
2848                vector_name: None,
2849                with_vectors: false,
2850            },
2851            &must_conditions,
2852            &should_groups,
2853        )
2854        .expect("grouped-cage filter encoding should succeed");
2855
2856        assert!(buf.contains(&SEARCH_FILTER));
2857        assert!(
2858            buf.contains(&CONDITION_FILTER),
2859            "expected nested filter condition tag for OR groups"
2860        );
2861    }
2862
2863    #[test]
2864    fn test_encode_create_field_index() {
2865        let mut buf = BytesMut::with_capacity(256);
2866
2867        encode_create_field_index_proto(&mut buf, "products", "category", FieldType::Keyword, true)
2868            .expect("field index request should encode");
2869
2870        assert_eq!(buf[0], 0x0A);
2871        assert!(buf.len() > 10);
2872    }
2873
2874    #[test]
2875    fn test_encode_collection_and_index_requests_reject_invalid_shape() {
2876        let mut buf = BytesMut::with_capacity(256);
2877
2878        assert_encode_error(
2879            encode_create_collection_proto(&mut buf, "", 128, crate::Distance::Cosine, false),
2880            "collection name",
2881        );
2882        assert_encode_error(
2883            encode_create_collection_proto(&mut buf, "products", 0, crate::Distance::Cosine, false),
2884            "vector_size",
2885        );
2886        assert_encode_error(
2887            encode_delete_collection_proto(&mut buf, " "),
2888            "collection name",
2889        );
2890        assert_encode_error(
2891            encode_collection_info_proto(&mut buf, ""),
2892            "collection name",
2893        );
2894        assert_encode_error(
2895            encode_create_field_index_proto(&mut buf, "products", "", FieldType::Keyword, true),
2896            "payload field name",
2897        );
2898    }
2899
2900    #[test]
2901    fn test_encode_payload_value_string() {
2902        let val = crate::point::PayloadValue::String("hello".to_string());
2903        let buf = encode_payload_value(&val).expect("payload value should encode");
2904
2905        // field 4 tag (0x22) + length + "hello"
2906        assert_eq!(buf[0], 0x22);
2907        assert!(buf.len() > 5);
2908    }
2909
2910    #[test]
2911    fn test_encode_payload_value_integer() {
2912        let val = crate::point::PayloadValue::Integer(42);
2913        let buf = encode_payload_value(&val).expect("payload value should encode");
2914
2915        // field 3 tag (0x18) + varint(42)
2916        assert_eq!(buf[0], 0x18);
2917    }
2918}