Skip to main content

qail_qdrant/
driver.rs

1//! gRPC-based Qdrant driver with zero-copy encoding.
2//!
3//! This driver uses the proto_encoder for direct protobuf encoding
4//! and grpc_transport for HTTP/2 communication, achieving performance
5//! that matches or exceeds the official qdrant-client.
6
7use bytes::BytesMut;
8use qail_core::ast::Qail;
9
10use crate::decoder;
11use crate::encoder;
12use crate::error::{QdrantError, QdrantResult};
13use crate::point::{Payload, Point, PointId, ScoredPoint};
14use crate::transport::GrpcClient;
15
16fn encode_error(message: impl Into<String>) -> QdrantError {
17    QdrantError::Encode(message.into())
18}
19
20fn search_limit_from_ast(cmd: &Qail) -> QdrantResult<u64> {
21    let mut limit = None;
22    for cage in &cmd.cages {
23        if let qail_core::ast::CageKind::Limit(n) = cage.kind {
24            if n == 0 {
25                return Err(QdrantError::Encode(
26                    "Qdrant search limit must be positive".to_string(),
27                ));
28            }
29            let parsed = u64::try_from(n)
30                .map_err(|_| QdrantError::Encode("Qdrant search limit is too large".to_string()))?;
31            if limit.replace(parsed).is_some() {
32                return Err(QdrantError::Encode(
33                    "Duplicate Qdrant search LIMIT clauses are not supported".to_string(),
34                ));
35            }
36        }
37    }
38    Ok(limit.unwrap_or(10))
39}
40
41fn qdrant_requested_projection_segment<'a>(raw: &'a str, table: &str) -> &'a str {
42    let trimmed = raw.trim();
43    if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') {
44        trimmed
45    } else if let Some(rest) = trimmed
46        .strip_prefix(table)
47        .and_then(|rest| rest.strip_prefix('.'))
48    {
49        rest.trim()
50    } else {
51        trimmed
52    }
53}
54
55fn qdrant_ast_projection_requests_vector(cmd: &Qail) -> bool {
56    use qail_core::ast::Expr;
57
58    cmd.columns.iter().any(|expr| {
59        let name = match expr {
60            Expr::Named(name) | Expr::Aliased { name, .. } => name,
61            _ => return false,
62        };
63        qdrant_requested_projection_segment(name, &cmd.table)
64            .trim_matches('"')
65            .eq_ignore_ascii_case("vector")
66    })
67}
68
69fn qdrant_ast_should_request_vectors(cmd: &Qail) -> bool {
70    cmd.with_vector || qdrant_ast_projection_requests_vector(cmd)
71}
72
73fn validate_collection_name(collection: &str) -> QdrantResult<()> {
74    if collection.trim().is_empty() {
75        return Err(encode_error("Qdrant collection name must not be empty"));
76    }
77    Ok(())
78}
79
80fn validate_payload_field_name(field_name: &str) -> QdrantResult<()> {
81    if field_name.trim().is_empty() {
82        return Err(encode_error("Qdrant payload field name must not be empty"));
83    }
84    Ok(())
85}
86
87fn validate_point_ids_non_empty(ids_len: usize, label: &str) -> QdrantResult<()> {
88    if ids_len == 0 {
89        return Err(encode_error(format!(
90            "Qdrant {label} point id list must not be empty"
91        )));
92    }
93    Ok(())
94}
95
96fn validate_point_id(id: &PointId, label: &str) -> QdrantResult<()> {
97    match id {
98        PointId::Num(_) => Ok(()),
99        PointId::Uuid(value) if value.trim().is_empty() => Err(encode_error(format!(
100            "Qdrant {label} point id must not be empty"
101        ))),
102        PointId::Uuid(_) => Ok(()),
103    }
104}
105
106fn validate_vector_finite(label: &str, vector: &[f32]) -> QdrantResult<()> {
107    if vector.is_empty() {
108        return Err(encode_error(format!(
109            "Qdrant {label} vector must not be empty"
110        )));
111    }
112    if let Some((idx, value)) = vector
113        .iter()
114        .enumerate()
115        .find(|(_, value)| !value.is_finite())
116    {
117        return Err(encode_error(format!(
118            "Qdrant {label} contains non-finite vector value at index {idx}: {value}"
119        )));
120    }
121    Ok(())
122}
123
124fn validate_vector_size(vector_size: u64) -> QdrantResult<()> {
125    if vector_size == 0 {
126        return Err(encode_error(
127            "Qdrant collection vector_size must be greater than zero",
128        ));
129    }
130    Ok(())
131}
132
133fn validate_score_threshold(score_threshold: Option<f32>) -> QdrantResult<()> {
134    if let Some(value) = score_threshold
135        && !value.is_finite()
136    {
137        return Err(encode_error(format!(
138            "Qdrant score threshold must be finite, got {value}"
139        )));
140    }
141    Ok(())
142}
143
144fn validate_search_limit(limit: u64) -> QdrantResult<()> {
145    if limit == 0 {
146        return Err(encode_error(
147            "Qdrant search limit must be greater than zero",
148        ));
149    }
150    Ok(())
151}
152
153fn validate_scroll_limit(limit: u32) -> QdrantResult<()> {
154    if limit == 0 {
155        return Err(encode_error(
156            "Qdrant scroll limit must be greater than zero",
157        ));
158    }
159    Ok(())
160}
161
162fn validate_vector_name(vector_name: Option<&str>) -> QdrantResult<()> {
163    if let Some(name) = vector_name
164        && name.trim().is_empty()
165    {
166        return Err(encode_error("Qdrant vector name must not be empty"));
167    }
168    Ok(())
169}
170
171fn validate_search_request(request: &encoder::SearchRequest<'_>) -> QdrantResult<()> {
172    validate_collection_name(request.collection)?;
173    validate_vector_finite("search request", request.vector)?;
174    validate_search_limit(request.limit)?;
175    validate_score_threshold(request.score_threshold)?;
176    validate_vector_name(request.vector_name)
177}
178
179fn validate_ast_value_finite(value: &qail_core::ast::Value, label: &str) -> QdrantResult<()> {
180    use qail_core::ast::Value;
181
182    match value {
183        Value::Float(value) if !value.is_finite() => Err(encode_error(format!(
184            "Qdrant {label} contains non-finite float: {value}"
185        ))),
186        Value::Array(items) => {
187            for item in items {
188                validate_ast_value_finite(item, label)?;
189            }
190            Ok(())
191        }
192        Value::Vector(values) => {
193            validate_vector_finite(label, values)?;
194            Ok(())
195        }
196        _ => Ok(()),
197    }
198}
199
200fn validate_conditions_finite(
201    conditions: &[qail_core::ast::Condition],
202    label: &str,
203) -> QdrantResult<()> {
204    for condition in conditions {
205        validate_ast_value_finite(&condition.value, label)?;
206    }
207    Ok(())
208}
209
210fn validate_condition_groups_finite(
211    groups: &[Vec<qail_core::ast::Condition>],
212    label: &str,
213) -> QdrantResult<()> {
214    for group in groups {
215        validate_conditions_finite(group, label)?;
216    }
217    Ok(())
218}
219
220fn validate_payload_value_finite(
221    value: &crate::point::PayloadValue,
222    label: &str,
223) -> QdrantResult<()> {
224    use crate::point::PayloadValue;
225
226    match value {
227        PayloadValue::Float(value) if !value.is_finite() => Err(encode_error(format!(
228            "Qdrant {label} contains non-finite payload float: {value}"
229        ))),
230        PayloadValue::List(items) => {
231            for item in items {
232                validate_payload_value_finite(item, label)?;
233            }
234            Ok(())
235        }
236        PayloadValue::Object(map) => validate_payload_finite(map, label),
237        _ => Ok(()),
238    }
239}
240
241fn validate_payload_finite(payload: &Payload, label: &str) -> QdrantResult<()> {
242    for (key, value) in payload {
243        validate_payload_field_name(key)?;
244        validate_payload_value_finite(value, label)?;
245    }
246    Ok(())
247}
248
249fn validate_points_finite(points: &[Point]) -> QdrantResult<()> {
250    if points.is_empty() {
251        return Err(encode_error("Qdrant upsert point list must not be empty"));
252    }
253    for (idx, point) in points.iter().enumerate() {
254        validate_point_id(&point.id, &format!("upsert point {idx}"))?;
255        validate_vector_finite(&format!("upsert point {idx}"), &point.vector)?;
256        validate_payload_finite(&point.payload, &format!("upsert point {idx}"))?;
257    }
258    Ok(())
259}
260
261/// High-performance gRPC driver for Qdrant.
262///
263/// Uses gRPC/HTTP2 with zero-copy protobuf encoding:
264/// - Encodes protobuf directly with pre-computed headers
265/// - Reuses buffers to minimize allocations
266/// - Uses memcpy for vector data (no per-element loop)
267///
268/// # Example
269/// ```ignore
270/// use qail_qdrant::QdrantDriver;
271/// use qail_core::prelude::*;
272///
273/// let driver = QdrantDriver::connect("localhost", 6334).await?;
274///
275/// let results = driver.search(
276///     "products",
277///     &embedding,
278///     10,
279///     Some(0.5),
280/// ).await?;
281/// ```
282pub struct QdrantDriver {
283    /// gRPC client for HTTP/2 transport
284    client: GrpcClient,
285    /// Reusable encoding buffer
286    buffer: BytesMut,
287}
288
289impl QdrantDriver {
290    /// Connect to Qdrant gRPC endpoint (default port 6334).
291    pub async fn connect(host: &str, port: u16) -> QdrantResult<Self> {
292        let client = GrpcClient::connect(host, port).await?;
293        Ok(Self {
294            client,
295            buffer: BytesMut::with_capacity(8192),
296        })
297    }
298
299    /// Connect with address string.
300    pub async fn connect_addr(addr: &str) -> QdrantResult<Self> {
301        let (host_part, port_part) = addr.rsplit_once(':').ok_or_else(|| {
302            QdrantError::Connection("Invalid address format, expected host:port".to_string())
303        })?;
304        let host = host_part
305            .strip_prefix('[')
306            .and_then(|s| s.strip_suffix(']'))
307            .unwrap_or(host_part);
308        if host.is_empty() {
309            return Err(QdrantError::Connection(
310                "Invalid address format, empty host".to_string(),
311            ));
312        }
313        let port: u16 = port_part
314            .parse()
315            .map_err(|_| QdrantError::Connection("Invalid port".to_string()))?;
316        Self::connect(host, port).await
317    }
318
319    /// Connect to Qdrant gRPC endpoint with TLS (rustls).
320    ///
321    /// Uses Mozilla root certificates — no system openssl required.
322    pub async fn connect_tls(host: &str, port: u16) -> QdrantResult<Self> {
323        let client = GrpcClient::connect_tls(host, port).await?;
324        Ok(Self {
325            client,
326            buffer: BytesMut::with_capacity(8192),
327        })
328    }
329
330    /// Connect with URL auto-detection (https = TLS, http = plain).
331    pub async fn connect_url(url: &str) -> QdrantResult<Self> {
332        let client = GrpcClient::connect_url(url).await?;
333        Ok(Self {
334            client,
335            buffer: BytesMut::with_capacity(8192),
336        })
337    }
338
339    // ========================================================================
340    // Search Operations
341    // ========================================================================
342
343    /// Vector similarity search with zero-copy encoding.
344    ///
345    /// # Performance
346    /// Vector is encoded via memcpy (no per-element serialization).
347    pub async fn search(
348        &mut self,
349        collection: &str,
350        vector: &[f32],
351        limit: u64,
352        score_threshold: Option<f32>,
353    ) -> QdrantResult<Vec<ScoredPoint>> {
354        validate_collection_name(collection)?;
355        validate_vector_finite("search request", vector)?;
356        validate_search_limit(limit)?;
357        validate_score_threshold(score_threshold)?;
358
359        self.buffer.clear();
360        encoder::encode_search_proto(
361            &mut self.buffer,
362            collection,
363            vector,
364            limit,
365            score_threshold,
366            None,
367            false,
368        )?;
369        let request_bytes = self.buffer.split().freeze();
370        let response = self.client.search(request_bytes).await?;
371        decoder::decode_search_response(&response)
372    }
373
374    /// Vector search with named vector field.
375    pub async fn search_named(
376        &mut self,
377        collection: &str,
378        vector_name: &str,
379        vector: &[f32],
380        limit: u64,
381        score_threshold: Option<f32>,
382    ) -> QdrantResult<Vec<ScoredPoint>> {
383        validate_collection_name(collection)?;
384        validate_vector_name(Some(vector_name))?;
385        validate_vector_finite("search request", vector)?;
386        validate_search_limit(limit)?;
387        validate_score_threshold(score_threshold)?;
388
389        self.buffer.clear();
390        encoder::encode_search_proto(
391            &mut self.buffer,
392            collection,
393            vector,
394            limit,
395            score_threshold,
396            Some(vector_name),
397            false,
398        )?;
399        let request_bytes = self.buffer.split().freeze();
400        let response = self.client.search(request_bytes).await?;
401        decoder::decode_search_response(&response)
402    }
403
404    /// Vector similarity search using the shared search request options.
405    pub async fn search_with_request(
406        &mut self,
407        request: encoder::SearchRequest<'_>,
408    ) -> QdrantResult<Vec<ScoredPoint>> {
409        validate_search_request(&request)?;
410
411        self.buffer.clear();
412        encoder::encode_search_proto(
413            &mut self.buffer,
414            request.collection,
415            request.vector,
416            request.limit,
417            request.score_threshold,
418            request.vector_name,
419            request.with_vectors,
420        )?;
421        let request_bytes = self.buffer.split().freeze();
422        let response = self.client.search(request_bytes).await?;
423        decoder::decode_search_response(&response)
424    }
425
426    /// Filtered vector search using QAIL AST conditions.
427    ///
428    /// Translates QAIL conditions into Qdrant's protobuf filter (must/should)
429    /// and includes them in the gRPC search request.
430    pub async fn search_filtered(
431        &mut self,
432        collection: &str,
433        vector: &[f32],
434        limit: u64,
435        score_threshold: Option<f32>,
436        conditions: &[qail_core::ast::Condition],
437        is_or: bool,
438    ) -> QdrantResult<Vec<ScoredPoint>> {
439        validate_collection_name(collection)?;
440        validate_vector_finite("search request", vector)?;
441        validate_search_limit(limit)?;
442        validate_score_threshold(score_threshold)?;
443        validate_conditions_finite(conditions, "filter condition")?;
444
445        self.buffer.clear();
446        encoder::encode_search_with_filter_proto(
447            &mut self.buffer,
448            encoder::SearchRequest {
449                collection,
450                vector,
451                limit,
452                score_threshold,
453                vector_name: None,
454                with_vectors: false,
455            },
456            conditions,
457            is_or,
458        )?;
459        let request_bytes = self.buffer.split().freeze();
460        let response = self.client.search(request_bytes).await?;
461        decoder::decode_search_response(&response)
462    }
463
464    /// Filtered vector search with grouped conditions.
465    ///
466    /// `must_conditions` are joined with AND, `should_conditions` with OR.
467    pub async fn search_filtered_grouped(
468        &mut self,
469        request: encoder::SearchRequest<'_>,
470        must_conditions: &[qail_core::ast::Condition],
471        should_conditions: &[qail_core::ast::Condition],
472    ) -> QdrantResult<Vec<ScoredPoint>> {
473        validate_search_request(&request)?;
474        validate_conditions_finite(must_conditions, "filter condition")?;
475        validate_conditions_finite(should_conditions, "filter condition")?;
476
477        self.buffer.clear();
478        encoder::encode_search_with_filter_groups_proto(
479            &mut self.buffer,
480            request,
481            must_conditions,
482            should_conditions,
483        )?;
484        let request_bytes = self.buffer.split().freeze();
485        let response = self.client.search(request_bytes).await?;
486        decoder::decode_search_response(&response)
487    }
488
489    /// Filtered vector search preserving OR-cage groups.
490    ///
491    /// Each OR group is treated as its own disjunction that must be satisfied.
492    pub async fn search_filtered_grouped_cages(
493        &mut self,
494        request: encoder::SearchRequest<'_>,
495        must_conditions: &[qail_core::ast::Condition],
496        should_groups: &[Vec<qail_core::ast::Condition>],
497    ) -> QdrantResult<Vec<ScoredPoint>> {
498        validate_search_request(&request)?;
499        validate_conditions_finite(must_conditions, "filter condition")?;
500        validate_condition_groups_finite(should_groups, "filter condition")?;
501
502        self.buffer.clear();
503        encoder::encode_search_with_filter_grouped_cages_proto(
504            &mut self.buffer,
505            request,
506            must_conditions,
507            should_groups,
508        )?;
509        let request_bytes = self.buffer.split().freeze();
510        let response = self.client.search(request_bytes).await?;
511        decoder::decode_search_response(&response)
512    }
513
514    /// Search multiple vectors concurrently using HTTP/2 pipelining.
515    ///
516    /// This sends all requests concurrently over a single h2 connection,
517    /// achieving 2-3x speedup compared to sequential searches.
518    ///
519    /// # Example
520    /// ```ignore
521    /// let vectors = vec![vec1, vec2, vec3];
522    /// let results = driver.search_batch("products", &vectors, 10, None).await?;
523    /// ```
524    pub async fn search_batch(
525        &mut self,
526        collection: &str,
527        vectors: &[Vec<f32>],
528        limit: u64,
529        score_threshold: Option<f32>,
530    ) -> QdrantResult<Vec<Vec<ScoredPoint>>> {
531        use futures_util::future::join_all;
532
533        validate_collection_name(collection)?;
534        if vectors.is_empty() {
535            return Err(encode_error(
536                "Qdrant batch search vectors must not be empty",
537            ));
538        }
539        validate_search_limit(limit)?;
540        validate_score_threshold(score_threshold)?;
541
542        let mut encoded_requests = Vec::with_capacity(vectors.len());
543        for (idx, vector) in vectors.iter().enumerate() {
544            validate_vector_finite(&format!("batch search vector {idx}"), vector)?;
545            self.buffer.clear();
546            encoder::encode_search_proto(
547                &mut self.buffer,
548                collection,
549                vector,
550                limit,
551                score_threshold,
552                None,
553                false,
554            )?;
555            encoded_requests.push(self.buffer.split().freeze());
556        }
557
558        let mut futures = Vec::with_capacity(encoded_requests.len());
559        for request in encoded_requests {
560            futures.push(self.client.search(request));
561        }
562
563        let responses = join_all(futures).await;
564        let mut results = Vec::with_capacity(responses.len());
565        for response in responses {
566            let decoded = decoder::decode_search_response(&response?)?;
567            results.push(decoded);
568        }
569
570        Ok(results)
571    }
572
573    /// Search using QAIL AST.
574    ///
575    /// Extracts vector, collection, limit from the Qail command.
576    /// If conditions are present in the AST, they are included as filters.
577    pub async fn search_ast(&mut self, cmd: &Qail) -> QdrantResult<Vec<ScoredPoint>> {
578        use qail_core::ast::LogicalOp;
579
580        validate_collection_name(&cmd.table)?;
581        let collection = &cmd.table;
582
583        let vector = cmd
584            .vector
585            .as_ref()
586            .ok_or_else(|| QdrantError::Encode("Vector required for search".to_string()))?;
587
588        let limit = search_limit_from_ast(cmd)?;
589        let with_vectors = qdrant_ast_should_request_vectors(cmd);
590
591        let score_threshold = cmd.score_threshold;
592
593        let mut must_conditions = Vec::new();
594        let mut should_groups = Vec::new();
595        for cage in cmd
596            .cages
597            .iter()
598            .filter(|c| matches!(c.kind, qail_core::ast::CageKind::Filter))
599        {
600            match cage.logical_op {
601                LogicalOp::And => must_conditions.extend(cage.conditions.iter().cloned()),
602                LogicalOp::Or => {
603                    if !cage.conditions.is_empty() {
604                        should_groups.push(cage.conditions.to_vec());
605                    }
606                }
607            }
608        }
609
610        if !must_conditions.is_empty() || !should_groups.is_empty() {
611            return self
612                .search_filtered_grouped_cages(
613                    encoder::SearchRequest {
614                        collection,
615                        vector,
616                        limit,
617                        score_threshold,
618                        vector_name: cmd.vector_name.as_deref(),
619                        with_vectors,
620                    },
621                    &must_conditions,
622                    &should_groups,
623                )
624                .await;
625        }
626
627        self.search_with_request(encoder::SearchRequest {
628            collection,
629            vector,
630            limit,
631            score_threshold,
632            vector_name: cmd.vector_name.as_deref(),
633            with_vectors,
634        })
635        .await
636    }
637
638    // ========================================================================
639    // Point Operations
640    // ========================================================================
641
642    /// Upsert points with zero-copy encoding.
643    pub async fn upsert(
644        &mut self,
645        collection: &str,
646        points: &[Point],
647        wait: bool,
648    ) -> QdrantResult<()> {
649        validate_collection_name(collection)?;
650        validate_points_finite(points)?;
651
652        self.buffer.clear();
653        encoder::encode_upsert_proto(&mut self.buffer, collection, points, wait)?;
654        let request_bytes = self.buffer.split().freeze();
655        let _response = self.client.upsert(request_bytes).await?;
656        Ok(())
657    }
658
659    /// Get points by ID (with payload and optional vectors).
660    pub async fn get_points(
661        &mut self,
662        collection: &str,
663        ids: &[PointId],
664        with_vectors: bool,
665    ) -> QdrantResult<Vec<ScoredPoint>> {
666        validate_collection_name(collection)?;
667        validate_point_ids_non_empty(ids.len(), "get")?;
668        self.buffer.clear();
669        encoder::encode_get_points_proto(&mut self.buffer, collection, ids, with_vectors)?;
670        let request_bytes = self.buffer.split().freeze();
671        let response = self.client.get(request_bytes).await?;
672        decoder::decode_get_response(&response)
673    }
674
675    /// Scroll through points (paginated iteration).
676    pub async fn scroll(
677        &mut self,
678        collection: &str,
679        limit: u32,
680        offset: Option<&PointId>,
681        with_vectors: bool,
682    ) -> QdrantResult<decoder::ScrollResult> {
683        validate_collection_name(collection)?;
684        validate_scroll_limit(limit)?;
685
686        self.buffer.clear();
687        encoder::encode_scroll_points_proto(
688            &mut self.buffer,
689            collection,
690            limit,
691            offset,
692            with_vectors,
693        )?;
694        let request_bytes = self.buffer.split().freeze();
695        let response = self.client.scroll(request_bytes).await?;
696        decoder::decode_scroll_response(&response)
697    }
698
699    /// Scroll through points with QAIL AST filters.
700    pub async fn scroll_filtered_grouped_cages(
701        &mut self,
702        collection: &str,
703        limit: u32,
704        offset: Option<&PointId>,
705        with_vectors: bool,
706        must_conditions: &[qail_core::ast::Condition],
707        should_groups: &[Vec<qail_core::ast::Condition>],
708    ) -> QdrantResult<decoder::ScrollResult> {
709        validate_collection_name(collection)?;
710        validate_scroll_limit(limit)?;
711        validate_conditions_finite(must_conditions, "filter condition")?;
712        validate_condition_groups_finite(should_groups, "filter condition")?;
713
714        self.buffer.clear();
715        encoder::encode_scroll_points_with_filter_grouped_cages_proto(
716            &mut self.buffer,
717            collection,
718            limit,
719            offset,
720            with_vectors,
721            must_conditions,
722            should_groups,
723        )?;
724        let request_bytes = self.buffer.split().freeze();
725        let response = self.client.scroll(request_bytes).await?;
726        decoder::decode_scroll_response(&response)
727    }
728
729    /// Delete points by numeric IDs.
730    pub async fn delete_points(
731        &mut self,
732        collection_name: &str,
733        point_ids: &[u64],
734    ) -> QdrantResult<()> {
735        validate_collection_name(collection_name)?;
736        validate_point_ids_non_empty(point_ids.len(), "delete")?;
737        self.buffer.clear();
738        encoder::encode_delete_points_proto(&mut self.buffer, collection_name, point_ids)?;
739        let request = self.buffer.split().freeze();
740        self.client.delete(request).await?;
741        Ok(())
742    }
743
744    /// Delete points by PointId (supports both numeric and UUID).
745    pub async fn delete_points_by_id(
746        &mut self,
747        collection_name: &str,
748        ids: &[PointId],
749    ) -> QdrantResult<()> {
750        validate_collection_name(collection_name)?;
751        validate_point_ids_non_empty(ids.len(), "delete")?;
752        self.buffer.clear();
753        encoder::encode_delete_points_mixed_proto(&mut self.buffer, collection_name, ids)?;
754        let request = self.buffer.split().freeze();
755        self.client.delete(request).await?;
756        Ok(())
757    }
758
759    /// Update payload on existing points.
760    pub async fn update_payload(
761        &mut self,
762        collection: &str,
763        point_ids: &[PointId],
764        payload: &Payload,
765        wait: bool,
766    ) -> QdrantResult<()> {
767        validate_collection_name(collection)?;
768        validate_point_ids_non_empty(point_ids.len(), "payload update")?;
769        if payload.is_empty() {
770            return Err(encode_error("Qdrant payload update must not be empty"));
771        }
772        validate_payload_finite(payload, "payload update")?;
773
774        self.buffer.clear();
775        encoder::encode_set_payload_proto(&mut self.buffer, collection, point_ids, payload, wait)?;
776        let request = self.buffer.split().freeze();
777        self.client.update_payload(request).await?;
778        Ok(())
779    }
780
781    // ========================================================================
782    // Collection Operations
783    // ========================================================================
784
785    /// Create a collection with specific vector parameters.
786    pub async fn create_collection(
787        &mut self,
788        collection_name: &str,
789        vector_size: u64,
790        distance: crate::Distance,
791        on_disk: bool,
792    ) -> QdrantResult<()> {
793        validate_collection_name(collection_name)?;
794        validate_vector_size(vector_size)?;
795
796        self.buffer.clear();
797        encoder::encode_create_collection_proto(
798            &mut self.buffer,
799            collection_name,
800            vector_size,
801            distance,
802            on_disk,
803        )?;
804        let request = self.buffer.split().freeze();
805        self.client.create_collection(request).await?;
806        Ok(())
807    }
808
809    /// Delete a collection.
810    pub async fn delete_collection(&mut self, collection_name: &str) -> QdrantResult<()> {
811        validate_collection_name(collection_name)?;
812        self.buffer.clear();
813        encoder::encode_delete_collection_proto(&mut self.buffer, collection_name)?;
814        let request = self.buffer.split().freeze();
815        self.client.delete_collection(request).await?;
816        Ok(())
817    }
818
819    // ========================================================================
820    // Index Operations
821    // ========================================================================
822
823    /// Create a payload field index for faster filtering.
824    pub async fn create_field_index(
825        &mut self,
826        collection: &str,
827        field_name: &str,
828        field_type: encoder::FieldType,
829        wait: bool,
830    ) -> QdrantResult<()> {
831        validate_collection_name(collection)?;
832        validate_payload_field_name(field_name)?;
833        self.buffer.clear();
834        encoder::encode_create_field_index_proto(
835            &mut self.buffer,
836            collection,
837            field_name,
838            field_type,
839            wait,
840        )?;
841        let request = self.buffer.split().freeze();
842        self.client.create_field_index(request).await?;
843        Ok(())
844    }
845}
846
847#[cfg(test)]
848mod ast_limit_tests {
849    use qail_core::ast::Qail;
850
851    use super::{QdrantError, search_limit_from_ast};
852
853    #[test]
854    fn search_limit_defaults_without_ast_limit() {
855        let cmd = Qail::search("products").vector(vec![0.1]);
856
857        assert_eq!(search_limit_from_ast(&cmd).unwrap(), 10);
858    }
859
860    #[test]
861    fn search_limit_rejects_zero_from_ast() {
862        let cmd = Qail::search("products").vector(vec![0.1]).limit(0);
863
864        let err = search_limit_from_ast(&cmd).expect_err("zero limit should be rejected");
865        assert!(matches!(err, QdrantError::Encode(_)));
866        assert!(err.to_string().contains("must be positive"));
867    }
868
869    #[test]
870    fn search_limit_rejects_duplicate_ast_limits() {
871        let cmd = Qail::search("products")
872            .vector(vec![0.1])
873            .limit(10)
874            .limit(20);
875
876        let err = search_limit_from_ast(&cmd).expect_err("duplicate limits should be rejected");
877        assert!(matches!(err, QdrantError::Encode(_)));
878        assert!(err.to_string().contains("Duplicate"));
879    }
880}
881
882#[cfg(test)]
883mod validation_tests {
884    use std::collections::HashMap;
885
886    use qail_core::ast::{Condition, Expr, Operator, Qail, Value};
887
888    use crate::point::{PayloadValue, Point};
889
890    use super::{
891        QdrantError, qdrant_ast_should_request_vectors, validate_collection_name,
892        validate_condition_groups_finite, validate_conditions_finite, validate_payload_field_name,
893        validate_payload_finite, validate_point_id, validate_point_ids_non_empty,
894        validate_points_finite, validate_score_threshold, validate_scroll_limit,
895        validate_search_limit, validate_search_request, validate_vector_finite,
896        validate_vector_name, validate_vector_size,
897    };
898
899    fn assert_encode_error(result: crate::error::QdrantResult<()>, needle: &str) {
900        let err = result.expect_err("non-finite value should be rejected");
901        assert!(matches!(err, QdrantError::Encode(_)));
902        assert!(
903            err.to_string().contains(needle),
904            "expected {needle:?} in {err}"
905        );
906    }
907
908    #[test]
909    fn rejects_non_finite_search_vectors_and_thresholds() {
910        assert_encode_error(
911            validate_vector_finite("search request", &[]),
912            "must not be empty",
913        );
914        assert_encode_error(
915            validate_vector_finite("search request", &[0.1, f32::NAN]),
916            "non-finite vector value",
917        );
918        assert_encode_error(
919            validate_score_threshold(Some(f32::INFINITY)),
920            "score threshold",
921        );
922    }
923
924    #[test]
925    fn rejects_zero_search_and_scroll_limits() {
926        assert_encode_error(validate_search_limit(0), "search limit");
927        validate_search_limit(1).expect("positive search limits should pass");
928
929        assert_encode_error(validate_scroll_limit(0), "scroll limit");
930        validate_scroll_limit(1).expect("positive scroll limits should pass");
931    }
932
933    #[test]
934    fn rejects_empty_collection_and_vector_names() {
935        assert_encode_error(validate_collection_name(""), "collection name");
936        assert_encode_error(validate_collection_name("   "), "collection name");
937        validate_collection_name("products").expect("non-empty collection names should pass");
938
939        assert_encode_error(validate_vector_name(Some("")), "vector name");
940        assert_encode_error(validate_vector_name(Some("  ")), "vector name");
941        validate_vector_name(Some("image")).expect("non-empty vector names should pass");
942        validate_vector_name(None).expect("unnamed vector searches should pass");
943    }
944
945    #[test]
946    fn rejects_empty_field_index_names() {
947        assert_encode_error(validate_payload_field_name(""), "payload field name");
948        assert_encode_error(validate_payload_field_name("  "), "payload field name");
949        validate_payload_field_name("tenant_id")
950            .expect("non-empty payload field names should pass");
951    }
952
953    #[test]
954    fn rejects_empty_point_id_selectors() {
955        assert_encode_error(validate_point_ids_non_empty(0, "delete"), "point id list");
956        validate_point_ids_non_empty(1, "delete").expect("non-empty point ids should pass");
957        assert_encode_error(
958            validate_point_id(&crate::PointId::Uuid("  ".to_string()), "delete"),
959            "point id",
960        );
961        validate_point_id(&crate::PointId::Num(7), "delete")
962            .expect("numeric point ids should pass");
963    }
964
965    #[test]
966    fn rejects_invalid_shared_search_requests() {
967        let vector = [0.1_f32, 0.2];
968        let err = validate_search_request(&crate::encoder::SearchRequest {
969            collection: "",
970            vector: &vector,
971            limit: 10,
972            score_threshold: None,
973            vector_name: None,
974            with_vectors: false,
975        })
976        .expect_err("empty collection must fail before transport");
977        assert!(err.to_string().contains("collection name"));
978
979        let err = validate_search_request(&crate::encoder::SearchRequest {
980            collection: "products",
981            vector: &vector,
982            limit: 10,
983            score_threshold: None,
984            vector_name: Some(""),
985            with_vectors: false,
986        })
987        .expect_err("empty vector name must fail before transport");
988        assert!(err.to_string().contains("vector name"));
989    }
990
991    #[test]
992    fn ast_projection_vector_requests_vectors_without_explicit_flag() {
993        let cmd = Qail::search("embeddings")
994            .vector(vec![0.1, 0.2])
995            .columns(["id", "vector"]);
996
997        assert!(qdrant_ast_should_request_vectors(&cmd));
998    }
999
1000    #[test]
1001    fn ast_projection_preserves_quoted_payload_vector_name() {
1002        let mut cmd = Qail::search("embeddings").vector(vec![0.1, 0.2]);
1003        cmd.columns = vec![Expr::Named("\"embeddings.vector\"".to_string())];
1004
1005        assert!(!qdrant_ast_should_request_vectors(&cmd));
1006    }
1007
1008    #[test]
1009    fn rejects_non_finite_filter_values() {
1010        let conditions = vec![Condition {
1011            left: Expr::Named("score".to_string()),
1012            op: Operator::Gt,
1013            value: Value::Float(f64::NAN),
1014            is_array_unnest: false,
1015        }];
1016
1017        assert_encode_error(
1018            validate_conditions_finite(&conditions, "filter condition"),
1019            "non-finite float",
1020        );
1021    }
1022
1023    #[test]
1024    fn rejects_non_finite_grouped_filter_values() {
1025        let groups = vec![vec![Condition {
1026            left: Expr::Named("score".to_string()),
1027            op: Operator::Gt,
1028            value: Value::Float(f64::INFINITY),
1029            is_array_unnest: false,
1030        }]];
1031
1032        assert_encode_error(
1033            validate_condition_groups_finite(&groups, "filter condition"),
1034            "non-finite float",
1035        );
1036    }
1037
1038    #[test]
1039    fn rejects_non_finite_upsert_vectors_and_payloads() {
1040        let mut payload = HashMap::new();
1041        payload.insert("rank".to_string(), PayloadValue::Float(f64::INFINITY));
1042
1043        assert_encode_error(
1044            validate_payload_finite(&payload, "payload update"),
1045            "payload float",
1046        );
1047
1048        let mut payload = HashMap::new();
1049        payload.insert("  ".to_string(), PayloadValue::String("bad".to_string()));
1050
1051        assert_encode_error(
1052            validate_payload_finite(&payload, "payload update"),
1053            "payload field name",
1054        );
1055
1056        let mut nested = HashMap::new();
1057        nested.insert("".to_string(), PayloadValue::String("bad".to_string()));
1058        let mut payload = HashMap::new();
1059        payload.insert("metadata".to_string(), PayloadValue::Object(nested));
1060
1061        assert_encode_error(
1062            validate_payload_finite(&payload, "payload update"),
1063            "payload field name",
1064        );
1065
1066        assert_encode_error(validate_points_finite(&[]), "point list");
1067
1068        let point = Point {
1069            id: crate::PointId::Uuid(" ".to_string()),
1070            vector: vec![1.0, 2.0],
1071            payload: HashMap::new(),
1072        };
1073
1074        assert_encode_error(validate_points_finite(&[point]), "point id");
1075
1076        let point = Point {
1077            id: "p1".into(),
1078            vector: vec![1.0, f32::NEG_INFINITY],
1079            payload: HashMap::new(),
1080        };
1081
1082        assert_encode_error(validate_points_finite(&[point]), "non-finite vector value");
1083
1084        let point = Point {
1085            id: "p2".into(),
1086            vector: Vec::new(),
1087            payload: HashMap::new(),
1088        };
1089
1090        assert_encode_error(validate_points_finite(&[point]), "must not be empty");
1091    }
1092
1093    #[test]
1094    fn rejects_zero_vector_size_for_collection_create() {
1095        assert_encode_error(validate_vector_size(0), "greater than zero");
1096        validate_vector_size(1).expect("positive vector sizes should pass");
1097    }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102    use super::*;
1103
1104    #[test]
1105    fn test_grpc_driver_struct() {
1106        // Verify struct is constructible
1107        let buffer = BytesMut::with_capacity(1024);
1108        assert!(buffer.capacity() >= 1024);
1109    }
1110}