1use 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
261pub struct QdrantDriver {
283 client: GrpcClient,
285 buffer: BytesMut,
287}
288
289impl QdrantDriver {
290 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let buffer = BytesMut::with_capacity(1024);
1108 assert!(buffer.capacity() >= 1024);
1109 }
1110}