1use std::cmp::min;
2use std::time::Duration;
3
4use crate::openapi::apis::manage_indexes_api;
5use crate::openapi::models::CreateIndexRequest;
6use crate::pinecone::PineconeClient;
7use crate::utils::errors::PineconeError;
8
9use crate::models::{
10 Cloud, CollectionList, CollectionModel, ConfigureIndexRequest, ConfigureIndexRequestSpec,
11 ConfigureIndexRequestSpecPod, CreateCollectionRequest, DeletionProtection, IndexList,
12 IndexModel, IndexSpec, Metric, PodSpec, PodSpecMetadataConfig, ServerlessSpec, WaitPolicy,
13};
14
15impl PineconeClient {
16 pub async fn create_serverless_index(
54 &self,
55 name: &str,
56 dimension: i32,
57 metric: Metric,
58 cloud: Cloud,
59 region: &str,
60 deletion_protection: DeletionProtection,
61 timeout: WaitPolicy,
62 ) -> Result<IndexModel, PineconeError> {
63 let create_index_request_spec = IndexSpec {
65 serverless: Some(Box::new(ServerlessSpec {
66 cloud,
67 region: region.to_string(),
68 })),
69 pod: None,
70 };
71
72 let create_index_request = CreateIndexRequest {
73 name: name.to_string(),
74 dimension,
75 deletion_protection: Some(deletion_protection),
76 metric: Some(metric.into()),
77 spec: Some(Box::new(create_index_request_spec)),
78 };
79
80 let res = manage_indexes_api::create_index(&self.openapi_config, create_index_request)
82 .await
83 .map_err(|e| PineconeError::from(e))?;
84
85 match self.handle_poll_index(name, timeout).await {
87 Ok(_) => Ok(res.into()),
88 Err(e) => Err(e),
89 }
90 }
91
92 pub async fn create_pod_index(
144 &self,
145 name: &str,
146 dimension: i32,
147 metric: Metric,
148 environment: &str,
149 pod_type: &str,
150 pods: i32,
151 replicas: i32,
152 shards: i32,
153 deletion_protection: DeletionProtection,
154 metadata_indexed: Option<&[&str]>,
155 source_collection: Option<&str>,
156 timeout: WaitPolicy,
157 ) -> Result<IndexModel, PineconeError> {
158 let indexed = metadata_indexed.map(|i| i.iter().map(|s| s.to_string()).collect());
160
161 let pod_spec = PodSpec {
162 environment: environment.to_string(),
163 replicas,
164 shards,
165 pod_type: pod_type.to_string(),
166 pods,
167 metadata_config: Some(Box::new(PodSpecMetadataConfig { indexed })),
168 source_collection: source_collection.map(|s| s.to_string()),
169 };
170
171 let spec = IndexSpec {
172 serverless: None,
173 pod: Some(Box::new(pod_spec)),
174 };
175
176 let create_index_request = CreateIndexRequest {
177 name: name.to_string(),
178 dimension,
179 deletion_protection: Some(deletion_protection),
180 metric: Some(metric.into()),
181 spec: Some(Box::new(spec)),
182 };
183
184 let res = manage_indexes_api::create_index(&self.openapi_config, create_index_request)
186 .await
187 .map_err(|e| PineconeError::from(e))?;
188
189 match self.handle_poll_index(name, timeout).await {
191 Ok(_) => Ok(res.into()),
192 Err(e) => Err(e),
193 }
194 }
195
196 async fn handle_poll_index(
198 &self,
199 name: &str,
200 timeout: WaitPolicy,
201 ) -> Result<(), PineconeError> {
202 match timeout {
203 WaitPolicy::WaitFor(duration) => {
204 let start_time = std::time::Instant::now();
205
206 loop {
207 if self.is_ready(name).await {
209 break;
210 }
211
212 match duration.cmp(&start_time.elapsed()) {
213 std::cmp::Ordering::Less => {
215 let message = format!("Index \"{name}\" not ready");
216 return Err(PineconeError::TimeoutError { message });
217 }
218 std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => {
220 let time_remaining = duration.saturating_sub(start_time.elapsed());
221 tokio::time::sleep(Duration::from_millis(min(
222 time_remaining.as_millis() as u64,
223 5000,
224 )))
225 .await;
226 }
227 }
228 }
229 }
230 WaitPolicy::NoWait => {}
231 }
232
233 Ok(())
234 }
235
236 async fn is_ready(&self, name: &str) -> bool {
238 let res = manage_indexes_api::describe_index(&self.openapi_config, name).await;
239 match res {
240 Ok(index) => index.status.ready,
241 Err(_) => false,
242 }
243 }
244
245 pub async fn describe_index(&self, name: &str) -> Result<IndexModel, PineconeError> {
268 let res = manage_indexes_api::describe_index(&self.openapi_config, name)
270 .await
271 .map_err(|e| PineconeError::from(e))?;
272
273 Ok(res.into())
274 }
275
276 pub async fn list_indexes(&self) -> Result<IndexList, PineconeError> {
299 let res = manage_indexes_api::list_indexes(&self.openapi_config)
301 .await
302 .map_err(|e| PineconeError::from(e))?;
303
304 Ok(res.into())
305 }
306
307 pub async fn configure_index(
341 &self,
342 name: &str,
343 deletion_protection: Option<DeletionProtection>,
344 replicas: Option<i32>,
345 pod_type: Option<&str>,
346 ) -> Result<IndexModel, PineconeError> {
347 if replicas == None && pod_type == None && deletion_protection == None {
348 return Err(PineconeError::InvalidConfigurationError {
349 message: "At least one of deletion_protection, number of replicas, or pod type must be provided".to_string(),
350 });
351 }
352
353 let spec = match (replicas, pod_type) {
354 (Some(replicas), Some(pod_type)) => Some(Box::new(ConfigureIndexRequestSpec {
355 pod: Box::new(ConfigureIndexRequestSpecPod {
356 replicas: Some(replicas),
357 pod_type: Some(pod_type.to_string()),
358 }),
359 })),
360 (Some(replicas), None) => Some(Box::new(ConfigureIndexRequestSpec {
361 pod: Box::new(ConfigureIndexRequestSpecPod {
362 replicas: Some(replicas),
363 pod_type: None,
364 }),
365 })),
366 (None, Some(pod_type)) => Some(Box::new(ConfigureIndexRequestSpec {
367 pod: Box::new(ConfigureIndexRequestSpecPod {
368 replicas: None,
369 pod_type: Some(pod_type.to_string()),
370 }),
371 })),
372 (None, None) => None,
373 };
374
375 let configure_index_request = ConfigureIndexRequest {
376 spec,
377 deletion_protection,
378 };
379
380 let res = manage_indexes_api::configure_index(
382 &self.openapi_config,
383 name,
384 configure_index_request,
385 )
386 .await
387 .map_err(|e| PineconeError::from(e))?;
388
389 Ok(res.into())
390 }
391
392 pub async fn delete_index(&self, name: &str) -> Result<(), PineconeError> {
414 let res = manage_indexes_api::delete_index(&self.openapi_config, name)
416 .await
417 .map_err(|e| PineconeError::from(e))?;
418
419 Ok(res)
420 }
421
422 pub async fn create_collection(
446 &self,
447 name: &str,
448 source: &str,
449 ) -> Result<CollectionModel, PineconeError> {
450 let create_collection_request = CreateCollectionRequest {
451 name: name.to_string(),
452 source: source.to_string(),
453 };
454
455 let res =
457 manage_indexes_api::create_collection(&self.openapi_config, create_collection_request)
458 .await
459 .map_err(|e| PineconeError::from(e))?;
460
461 Ok(res)
462 }
463
464 pub async fn describe_collection(&self, name: &str) -> Result<CollectionModel, PineconeError> {
487 let res = manage_indexes_api::describe_collection(&self.openapi_config, name)
488 .await
489 .map_err(|e| PineconeError::from(e))?;
490
491 Ok(res)
492 }
493
494 pub async fn list_collections(&self) -> Result<CollectionList, PineconeError> {
516 let res = manage_indexes_api::list_collections(&self.openapi_config)
518 .await
519 .map_err(|e| PineconeError::from(e))?;
520
521 Ok(res)
522 }
523
524 pub async fn delete_collection(&self, name: &str) -> Result<(), PineconeError> {
546 let res = manage_indexes_api::delete_collection(&self.openapi_config, name)
548 .await
549 .map_err(|e| PineconeError::from(e))?;
550
551 Ok(res)
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use crate::openapi::{
559 self,
560 models::{self, collection_model::Status},
561 };
562 use crate::pinecone::PineconeClientConfig;
563 use httpmock::prelude::*;
564 use tokio;
565
566 #[tokio::test]
567 async fn test_create_serverless_index() -> Result<(), PineconeError> {
568 let server = MockServer::start();
569
570 let mock = server.mock(|when, then| {
571 when.method(POST).path("/indexes");
572 then.status(201)
573 .header("content-type", "application/json")
574 .body(
575 r#"
576 {
577 "name": "index-name",
578 "dimension": 10,
579 "metric": "euclidean",
580 "host": "host1",
581 "spec": {
582 "serverless": {
583 "cloud": "aws",
584 "region": "us-east-1"
585 }
586 },
587 "status": {
588 "ready": true,
589 "state": "Initializing"
590 }
591 }"#,
592 );
593 });
594
595 let config = PineconeClientConfig {
596 api_key: Some("api_key".to_string()),
597 control_plane_host: Some(server.base_url()),
598 ..Default::default()
599 };
600 let pinecone = config.client().expect("Failed to create Pinecone instance");
601
602 let create_index_response = pinecone
603 .create_serverless_index(
604 "index-name",
605 10,
606 Metric::Cosine,
607 Cloud::Aws,
608 "us-east-1",
609 DeletionProtection::Enabled,
610 WaitPolicy::NoWait,
611 )
612 .await
613 .expect("Failed to create serverless index");
614
615 mock.assert();
616
617 assert_eq!(create_index_response.name, "index-name");
618 assert_eq!(create_index_response.dimension, 10);
619 assert_eq!(create_index_response.metric, Metric::Euclidean);
620
621 let spec = create_index_response.spec.serverless.unwrap();
622 assert_eq!(spec.cloud, openapi::models::serverless_spec::Cloud::Aws);
623 assert_eq!(spec.region, "us-east-1");
624
625 Ok(())
626 }
627
628 #[tokio::test]
629 async fn test_create_serverless_index_defaults() -> Result<(), PineconeError> {
630 let server = MockServer::start();
631
632 let mock = server.mock(|when, then| {
633 when.method(POST).path("/indexes");
634 then.status(201)
635 .header("content-type", "application/json")
636 .body(
637 r#"{
638 "name": "index-name",
639 "dimension": 10,
640 "metric": "cosine",
641 "host": "host1",
642 "spec": {
643 "serverless": {
644 "cloud": "gcp",
645 "region": "us-east-1"
646 }
647 },
648 "status": {
649 "ready": true,
650 "state": "Initializing"
651 }
652 }"#,
653 );
654 });
655
656 let config = PineconeClientConfig {
657 api_key: Some("api_key".to_string()),
658 control_plane_host: Some(server.base_url()),
659 ..Default::default()
660 };
661 let pinecone = config.client().expect("Failed to create Pinecone instance");
662
663 let create_index_response = pinecone
664 .create_serverless_index(
665 "index-name",
666 10,
667 Default::default(),
668 Default::default(),
669 "us-east-1",
670 DeletionProtection::Enabled,
671 WaitPolicy::NoWait,
672 )
673 .await
674 .expect("Failed to create serverless index");
675
676 assert_eq!(create_index_response.name, "index-name");
677 assert_eq!(create_index_response.dimension, 10);
678 assert_eq!(create_index_response.metric, Metric::Cosine);
679
680 let spec = create_index_response.spec.serverless.unwrap();
681 assert_eq!(spec.cloud, openapi::models::serverless_spec::Cloud::Gcp);
682 assert_eq!(spec.region, "us-east-1");
683
684 mock.assert();
685
686 Ok(())
687 }
688
689 #[tokio::test]
690 async fn test_create_serverless_index_invalid_region() -> Result<(), PineconeError> {
691 let server = MockServer::start();
692
693 let mock = server.mock(|when, then| {
694 when.method(POST).path("/indexes");
695 then.status(404)
696 .header("content-type", "application/json")
697 .body(
698 r#"{
699 "error": {
700 "code": "NOT_FOUND",
701 "message": "Resource cloud: aws region: abc not found."
702 },
703 "status": 404
704 }"#,
705 );
706 });
707
708 let config = PineconeClientConfig {
709 api_key: Some("api_key".to_string()),
710 control_plane_host: Some(server.base_url()),
711 ..Default::default()
712 };
713 let pinecone = config.client().expect("Failed to create Pinecone instance");
714
715 let create_index_response = pinecone
716 .create_serverless_index(
717 "index-name",
718 10,
719 Default::default(),
720 Default::default(),
721 "abc",
722 DeletionProtection::Enabled,
723 WaitPolicy::NoWait,
724 )
725 .await
726 .expect_err("Expected error when creating serverless index");
727
728 assert!(matches!(
729 create_index_response,
730 PineconeError::InvalidRegionError { .. }
731 ));
732 mock.assert();
733
734 Ok(())
735 }
736
737 #[tokio::test]
738 async fn test_create_serverless_index_index_exists() -> Result<(), PineconeError> {
739 let server = MockServer::start();
740
741 let mock = server.mock(|when, then| {
742 when.method(POST).path("/indexes");
743 then.status(409)
744 .header("content-type", "application/json")
745 .body(
746 r#"{
747 "error": {
748 "code": "ALREADY_EXISTS",
749 "message": "Resource already exists."
750 },
751 "status": 409
752 }"#,
753 );
754 });
755
756 let config = PineconeClientConfig {
757 api_key: Some("api_key".to_string()),
758 control_plane_host: Some(server.base_url()),
759 ..Default::default()
760 };
761 let pinecone = config.client().expect("Failed to create Pinecone instance");
762
763 let create_index_response = pinecone
764 .create_serverless_index(
765 "index-name",
766 10,
767 Default::default(),
768 Default::default(),
769 "us-west-1",
770 DeletionProtection::Enabled,
771 WaitPolicy::NoWait,
772 )
773 .await
774 .expect_err("Expected error when creating serverless index");
775
776 assert!(matches!(
777 create_index_response,
778 PineconeError::ResourceAlreadyExistsError { .. }
779 ));
780 mock.assert();
781
782 Ok(())
783 }
784
785 #[tokio::test]
786 async fn test_create_serverless_index_unprocessable_entity() -> Result<(), PineconeError> {
787 let server = MockServer::start();
788
789 let mock = server.mock(|when, then| {
790 when.method(POST).path("/indexes");
791 then.status(422)
792 .header("content-type", "application/json")
793 .body(
794 r#"{
795 "error": {
796 "code": "INVALID_ARGUMENT",
797 "message": "Failed to deserialize the JSON body into the target type: missing field `metric` at line 1 column 16"
798 },
799 "status": 422
800 }"#,
801 );
802 });
803
804 let config = PineconeClientConfig {
805 api_key: Some("api_key".to_string()),
806 control_plane_host: Some(server.base_url()),
807 ..Default::default()
808 };
809 let pinecone = config.client().expect("Failed to create Pinecone instance");
810
811 let create_index_response = pinecone
812 .create_serverless_index(
813 "index-name",
814 10,
815 Default::default(),
816 Default::default(),
817 "us-west-1",
818 DeletionProtection::Enabled,
819 WaitPolicy::NoWait,
820 )
821 .await
822 .expect_err("Expected error when creating serverless index");
823
824 assert!(matches!(
825 create_index_response,
826 PineconeError::UnprocessableEntityError { .. }
827 ));
828 mock.assert();
829
830 Ok(())
831 }
832
833 #[tokio::test]
834 async fn test_create_serverless_index_internal_error() -> Result<(), PineconeError> {
835 let server = MockServer::start();
836
837 let mock = server.mock(|when, then| {
838 when.method(POST).path("/indexes");
839 then.status(500);
840 });
841
842 let config = PineconeClientConfig {
843 api_key: Some("api_key".to_string()),
844 control_plane_host: Some(server.base_url()),
845 ..Default::default()
846 };
847 let pinecone = config.client().expect("Failed to create Pinecone instance");
848
849 let create_index_response = pinecone
850 .create_serverless_index(
851 "index-name",
852 10,
853 Metric::Cosine,
854 Cloud::Aws,
855 "us-east-1",
856 DeletionProtection::Enabled,
857 WaitPolicy::NoWait,
858 )
859 .await
860 .expect_err("Expected create_index to return an error");
861
862 assert!(matches!(
863 create_index_response,
864 PineconeError::InternalServerError { .. }
865 ));
866 mock.assert();
867
868 Ok(())
869 }
870
871 #[tokio::test]
872 async fn test_describe_serverless_index() -> Result<(), PineconeError> {
873 let server = MockServer::start();
874
875 let mock = server.mock(|when, then| {
876 when.method(GET).path("/indexes/serverless-index");
877 then.status(200)
878 .header("content-type", "application/json")
879 .body(
880 r#"{
881 "dimension": 1536,
882 "host": "mock-host",
883 "metric": "cosine",
884 "name": "serverless-index",
885 "spec": {
886 "serverless": {
887 "cloud": "aws",
888 "region": "us-east-1"
889 }
890 },
891 "deletion_protection": "disabled",
892 "status": {
893 "ready": true,
894 "state": "Ready"
895 }
896 }"#,
897 );
898 });
899
900 let config = PineconeClientConfig {
902 api_key: Some("api_key".to_string()),
903 control_plane_host: Some(server.base_url()),
904 ..Default::default()
905 };
906 let pinecone = config.client().expect("Failed to create Pinecone instance");
907
908 let index = pinecone
910 .describe_index("serverless-index")
911 .await
912 .expect("Failed to describe index");
913
914 let expected = IndexModel {
915 name: "serverless-index".to_string(),
916 metric: Metric::Cosine,
917 dimension: 1536,
918 status: openapi::models::IndexModelStatus {
919 ready: true,
920 state: openapi::models::index_model_status::State::Ready,
921 },
922 host: "mock-host".to_string(),
923 deletion_protection: Some(DeletionProtection::Disabled),
924 spec: models::IndexModelSpec {
925 serverless: Some(Box::new(models::ServerlessSpec {
926 cloud: openapi::models::serverless_spec::Cloud::Aws,
927 region: "us-east-1".to_string(),
928 })),
929 pod: None,
930 },
931 };
932
933 assert_eq!(index, expected);
934 mock.assert();
935
936 Ok(())
937 }
938
939 #[tokio::test]
940 async fn test_describe_index_invalid_name() -> Result<(), PineconeError> {
941 let server = MockServer::start();
942
943 let mock = server.mock(|when, then| {
944 when.method(GET).path("/indexes/invalid-index");
945 then.status(404)
946 .header("content-type", "application/json")
947 .body(
948 r#"{
949 "error": "Index invalid-index not found"
950 }"#,
951 );
952 });
953
954 let config = PineconeClientConfig {
955 api_key: Some("api_key".to_string()),
956 control_plane_host: Some(server.base_url()),
957 ..Default::default()
958 };
959 let pinecone = config.client().expect("Failed to create Pinecone instance");
960
961 let describe_index_response = pinecone
962 .describe_index("invalid-index")
963 .await
964 .expect_err("Expected describe_index to return an error");
965
966 assert!(matches!(
967 describe_index_response,
968 PineconeError::IndexNotFoundError { .. }
969 ));
970 mock.assert();
971
972 Ok(())
973 }
974
975 #[tokio::test]
976 async fn test_describe_index_server_error() -> Result<(), PineconeError> {
977 let server = MockServer::start();
978
979 let mock = server.mock(|when, then| {
980 when.method(GET).path("/indexes/index-name");
981 then.status(500);
982 });
983
984 let config = PineconeClientConfig {
985 api_key: Some("api_key".to_string()),
986 control_plane_host: Some(server.base_url()),
987 ..Default::default()
988 };
989 let pinecone = config.client().expect("Failed to create Pinecone instance");
990
991 let describe_index_response = pinecone
992 .describe_index("index-name")
993 .await
994 .expect_err("Expected describe_index to return an error");
995
996 assert!(matches!(
997 describe_index_response,
998 PineconeError::InternalServerError { .. }
999 ));
1000 mock.assert();
1001
1002 Ok(())
1003 }
1004
1005 #[tokio::test]
1006 async fn test_list_indexes() -> Result<(), PineconeError> {
1007 let server = MockServer::start();
1008
1009 let mock = server.mock(|when, then| {
1010 when.method(GET).path("/indexes");
1011 then.status(200)
1012 .header("content-type", "application/json")
1013 .body(
1014 r#"
1015 {
1016 "indexes": [
1017 {
1018 "name": "index1",
1019 "dimension": 1536,
1020 "metric": "cosine",
1021 "host": "host1",
1022 "spec": {},
1023 "status": {
1024 "ready": false,
1025 "state": "Initializing"
1026 }
1027 },
1028 {
1029 "name": "index2",
1030 "dimension": 1536,
1031 "metric": "cosine",
1032 "host": "host2",
1033 "spec": {},
1034 "status": {
1035 "ready": false,
1036 "state": "Initializing"
1037 }
1038 }
1039 ]
1040 }"#,
1041 );
1042 });
1043
1044 let config = PineconeClientConfig {
1046 api_key: Some("api_key".to_string()),
1047 control_plane_host: Some(server.base_url()),
1048 ..Default::default()
1049 };
1050 let pinecone = config.client().expect("Failed to create Pinecone instance");
1051
1052 let index_list = pinecone
1054 .list_indexes()
1055 .await
1056 .expect("Failed to list indexes");
1057
1058 let expected = IndexList {
1059 indexes: Some(vec![
1061 IndexModel {
1062 name: "index1".to_string(),
1063 dimension: 1536,
1064 metric: Metric::Cosine,
1065 host: "host1".to_string(),
1066 deletion_protection: None,
1067 spec: models::IndexModelSpec::default(),
1068 status: models::IndexModelStatus::default(),
1069 },
1070 IndexModel {
1071 name: "index2".to_string(),
1072 dimension: 1536,
1073 metric: Metric::Cosine,
1074 host: "host2".to_string(),
1075 deletion_protection: None,
1076 spec: models::IndexModelSpec::default(),
1077 status: models::IndexModelStatus::default(),
1078 },
1079 ]),
1080 };
1081 assert_eq!(index_list, expected);
1082 mock.assert();
1083
1084 Ok(())
1085 }
1086
1087 #[tokio::test]
1088 async fn test_list_indexes_server_error() -> Result<(), PineconeError> {
1089 let server = MockServer::start();
1090
1091 let mock = server.mock(|when, then| {
1092 when.method(GET).path("/indexes");
1093 then.status(500);
1094 });
1095
1096 let config = PineconeClientConfig {
1097 api_key: Some("api_key".to_string()),
1098 control_plane_host: Some(server.base_url()),
1099 ..Default::default()
1100 };
1101 let pinecone = config.client().expect("Failed to create Pinecone instance");
1102
1103 let list_indexes_response = pinecone
1104 .list_indexes()
1105 .await
1106 .expect_err("Expected list_indexes to return an error");
1107
1108 assert!(matches!(
1109 list_indexes_response,
1110 PineconeError::InternalServerError { .. }
1111 ));
1112 mock.assert();
1113
1114 Ok(())
1115 }
1116
1117 #[tokio::test]
1118 async fn test_create_pod_index() -> Result<(), PineconeError> {
1119 let server = MockServer::start();
1120
1121 let mock = server.mock(|when, then| {
1122 when.method(POST).path("/indexes");
1123 then.status(201)
1124 .header("content-type", "application/json")
1125 .body(
1126 r#"
1127 {
1128 "name": "index-name",
1129 "dimension": 1536,
1130 "metric": "euclidean",
1131 "host": "mock-host",
1132 "spec": {
1133 "pod": {
1134 "environment": "us-east-1-aws",
1135 "replicas": 1,
1136 "shards": 1,
1137 "pod_type": "p1.x1",
1138 "pods": 1,
1139 "metadata_config": {
1140 "indexed": [
1141 "genre",
1142 "title",
1143 "imdb_rating"
1144 ]
1145 }
1146 }
1147 },
1148 "status": {
1149 "ready": true,
1150 "state": "ScalingUpPodSize"
1151 }
1152 }
1153 "#,
1154 );
1155 });
1156
1157 let config = PineconeClientConfig {
1158 api_key: Some("api_key".to_string()),
1159 control_plane_host: Some(server.base_url()),
1160 ..Default::default()
1161 };
1162 let pinecone = config.client().expect("Failed to create Pinecone instance");
1163
1164 let create_index_response = pinecone
1165 .create_pod_index(
1166 "index-name",
1167 1536,
1168 Metric::Euclidean,
1169 "us-east-1-aws",
1170 "p1.x1",
1171 1,
1172 1,
1173 1,
1174 DeletionProtection::Enabled,
1175 Some(&vec!["genre", "title", "imdb_rating"]),
1176 Some("example-collection"),
1177 WaitPolicy::NoWait,
1178 )
1179 .await
1180 .expect("Failed to create pod index");
1181
1182 assert_eq!(create_index_response.name, "index-name");
1183 assert_eq!(create_index_response.dimension, 1536);
1184 assert_eq!(create_index_response.metric, Metric::Euclidean);
1185
1186 let pod_spec = create_index_response.spec.pod.as_ref().unwrap();
1187 assert_eq!(pod_spec.environment, "us-east-1-aws");
1188 assert_eq!(pod_spec.pod_type, "p1.x1");
1189 assert_eq!(
1190 pod_spec.metadata_config.as_ref().unwrap().indexed,
1191 Some(vec![
1192 "genre".to_string(),
1193 "title".to_string(),
1194 "imdb_rating".to_string()
1195 ])
1196 );
1197 assert_eq!(pod_spec.pods, 1);
1198 assert_eq!(pod_spec.replicas, 1);
1199 assert_eq!(pod_spec.shards, 1);
1200
1201 mock.assert();
1202
1203 Ok(())
1204 }
1205
1206 #[tokio::test]
1207 async fn test_create_pod_index_with_defaults() -> Result<(), PineconeError> {
1208 let server = MockServer::start();
1209
1210 let mock = server.mock(|when, then| {
1211 when.method(POST).path("/indexes");
1212 then.status(201)
1213 .header("content-type", "application/json")
1214 .body(
1215 r#"
1216 {
1217 "name": "index-name",
1218 "dimension": 1536,
1219 "metric": "cosine",
1220 "host": "mock-host",
1221 "spec": {
1222 "pod": {
1223 "environment": "us-east-1-aws",
1224 "pod_type": "p1.x1",
1225 "pods": 1,
1226 "metadata_config": {},
1227 "replicas": 1,
1228 "shards": 1
1229 }
1230 },
1231 "status": {
1232 "ready": true,
1233 "state": "ScalingUpPodSize"
1234 }
1235 }
1236 "#,
1237 );
1238 });
1239
1240 let config = PineconeClientConfig {
1241 api_key: Some("api_key".to_string()),
1242 control_plane_host: Some(server.base_url()),
1243 ..Default::default()
1244 };
1245 let pinecone = config.client().expect("Failed to create Pinecone instance");
1246
1247 let create_index_response = pinecone
1248 .create_pod_index(
1249 "index-name",
1250 1536,
1251 Default::default(),
1252 "us-east-1-aws",
1253 "p1.x1",
1254 1,
1255 1,
1256 1,
1257 DeletionProtection::Enabled,
1258 None,
1259 None,
1260 WaitPolicy::NoWait,
1261 )
1262 .await
1263 .expect("Failed to create pod index");
1264
1265 assert_eq!(create_index_response.name, "index-name");
1266 assert_eq!(create_index_response.dimension, 1536);
1267 assert_eq!(create_index_response.metric, Metric::Cosine);
1268
1269 let pod_spec = create_index_response.spec.pod.as_ref().unwrap();
1270 assert_eq!(pod_spec.environment, "us-east-1-aws");
1271 assert_eq!(pod_spec.pod_type, "p1.x1");
1272 assert_eq!(pod_spec.metadata_config.as_ref().unwrap().indexed, None);
1273 assert_eq!(pod_spec.pods, 1);
1274 assert_eq!(pod_spec.replicas, 1);
1275 assert_eq!(pod_spec.shards, 1);
1276
1277 mock.assert();
1278
1279 Ok(())
1280 }
1281
1282 #[tokio::test]
1283 async fn test_create_pod_index_quota_exceeded() -> Result<(), PineconeError> {
1284 let server = MockServer::start();
1285
1286 let mock = server.mock(|when, then| {
1287 when.method(POST).path("/indexes");
1288 then.status(403)
1289 .header("content-type", "application/json")
1290 .body(
1291 r#"
1292 {
1293 "error": {
1294 "code": "FORBIDDEN",
1295 "message": "Increase yoru quota or upgrade to create more indexes."
1296 },
1297 "status": 403
1298 }
1299 "#,
1300 );
1301 });
1302
1303 let config = PineconeClientConfig {
1304 api_key: Some("api_key".to_string()),
1305 control_plane_host: Some(server.base_url()),
1306 ..Default::default()
1307 };
1308 let pinecone = config.client().expect("Failed to create Pinecone instance");
1309
1310 let create_index_response = pinecone
1311 .create_pod_index(
1312 "index-name",
1313 1536,
1314 Metric::Euclidean,
1315 "test-environment",
1316 "p1.x1",
1317 1,
1318 1,
1319 1,
1320 DeletionProtection::Enabled,
1321 None,
1322 Some("example-collection"),
1323 WaitPolicy::NoWait,
1324 )
1325 .await
1326 .expect_err("Expected create_pod_index to return an error");
1327
1328 assert!(matches!(
1329 create_index_response,
1330 PineconeError::PodQuotaExceededError { .. }
1331 ));
1332
1333 mock.assert();
1334
1335 Ok(())
1336 }
1337
1338 #[tokio::test]
1339 async fn test_create_pod_index_invalid_environment() -> Result<(), PineconeError> {
1340 let server = MockServer::start();
1341
1342 let mock = server.mock(|when, then| {
1343 when.method(POST).path("/indexes");
1344 then.status(400)
1345 .header("content-type", "application/json")
1346 .body(
1347 r#"
1348 {
1349 "error": "Invalid environment"
1350 }
1351 "#,
1352 );
1353 });
1354
1355 let config = PineconeClientConfig {
1356 api_key: Some("api_key".to_string()),
1357 control_plane_host: Some(server.base_url()),
1358 ..Default::default()
1359 };
1360 let pinecone = config.client().expect("Failed to create Pinecone instance");
1361
1362 let create_index_response = pinecone
1363 .create_pod_index(
1364 "index-name",
1365 1536,
1366 Metric::Euclidean,
1367 "invalid-environment",
1368 "p1.x1",
1369 1,
1370 1,
1371 1,
1372 DeletionProtection::Enabled,
1373 Some(&vec!["genre", "title", "imdb_rating"]),
1374 Some("example-collection"),
1375 WaitPolicy::NoWait,
1376 )
1377 .await
1378 .expect_err("Expected create_pod_index to return an error");
1379
1380 assert!(matches!(
1381 create_index_response,
1382 PineconeError::BadRequestError { .. }
1383 ));
1384
1385 mock.assert();
1386
1387 Ok(())
1388 }
1389
1390 #[tokio::test]
1391 async fn test_create_pod_index_invalid_pod_type() -> Result<(), PineconeError> {
1392 let server = MockServer::start();
1393
1394 let mock = server.mock(|when, then| {
1395 when.method(POST).path("/indexes");
1396 then.status(400)
1397 .header("content-type", "application/json")
1398 .body(
1399 r#"
1400 {
1401 "error": "Invalid pod type"
1402 }
1403 "#,
1404 );
1405 });
1406
1407 let config = PineconeClientConfig {
1408 api_key: Some("api_key".to_string()),
1409 control_plane_host: Some(server.base_url()),
1410 ..Default::default()
1411 };
1412 let pinecone = config.client().expect("Failed to create Pinecone instance");
1413
1414 let create_index_response = pinecone
1415 .create_pod_index(
1416 "index-name",
1417 1536,
1418 Metric::Euclidean,
1419 "us-east-1-aws",
1420 "invalid-pod-type",
1421 1,
1422 1,
1423 1,
1424 DeletionProtection::Enabled,
1425 Some(&vec!["genre", "title", "imdb_rating"]),
1426 Some("example-collection"),
1427 WaitPolicy::NoWait,
1428 )
1429 .await
1430 .expect_err("Expected create_pod_index to return an error");
1431
1432 assert!(matches!(
1433 create_index_response,
1434 PineconeError::BadRequestError { .. }
1435 ));
1436 mock.assert();
1437
1438 Ok(())
1439 }
1440
1441 #[tokio::test]
1442 async fn test_handle_polling_index_ok() -> Result<(), PineconeError> {
1443 let server = MockServer::start();
1444
1445 let mock = server.mock(|when, then| {
1446 when.method(GET).path("/indexes/index-name");
1447 then.status(200)
1448 .header("content-type", "application/json")
1449 .body(
1450 r#"
1451 {
1452 "dimension": 1536,
1453 "host": "mock-host",
1454 "metric": "cosine",
1455 "name": "index-name",
1456 "spec": {
1457 "serverless": {
1458 "cloud": "aws",
1459 "region": "us-east-1"
1460 }
1461 },
1462 "status": {
1463 "ready": true,
1464 "state": "Ready"
1465 }
1466 }"#,
1467 );
1468 });
1469
1470 let config = PineconeClientConfig {
1471 api_key: Some("api_key".to_string()),
1472 control_plane_host: Some(server.base_url()),
1473 ..Default::default()
1474 };
1475 let pinecone = config.client().expect("Failed to create Pinecone instance");
1476
1477 let res = pinecone
1478 .handle_poll_index("index-name", WaitPolicy::WaitFor(Duration::from_secs(1)))
1479 .await;
1480
1481 assert!(res.is_ok());
1482 mock.assert();
1483
1484 Ok(())
1485 }
1486
1487 #[tokio::test]
1488 async fn test_handle_polling_index_err() -> Result<(), PineconeError> {
1489 let server = MockServer::start();
1490
1491 let mock = server.mock(|when, then| {
1492 when.method(GET).path("/indexes/index-name");
1493 then.status(200)
1494 .header("content-type", "application/json")
1495 .body(
1496 r#"
1497 {
1498 "dimension": 1536,
1499 "host": "mock-host",
1500 "metric": "cosine",
1501 "name": "index-name",
1502 "spec": {
1503 "serverless": {
1504 "cloud": "aws",
1505 "region": "us-east-1"
1506 }
1507 },
1508 "status": {
1509 "ready": false,
1510 "state": "Initializing"
1511 }
1512 }"#,
1513 );
1514 });
1515
1516 let config = PineconeClientConfig {
1517 api_key: Some("api_key".to_string()),
1518 control_plane_host: Some(server.base_url()),
1519 ..Default::default()
1520 };
1521 let pinecone = config.client().expect("Failed to create Pinecone instance");
1522
1523 let start_time = std::time::Instant::now();
1524 let err = pinecone
1525 .handle_poll_index("index-name", WaitPolicy::WaitFor(Duration::from_secs(7)))
1526 .await
1527 .expect_err("Expected to fail polling index");
1528
1529 assert!(start_time.elapsed().as_secs() >= 7 && start_time.elapsed().as_secs() < 8);
1530 assert!(matches!(err, PineconeError::TimeoutError { .. }));
1531
1532 mock.assert_hits(3);
1533
1534 Ok(())
1535 }
1536
1537 #[tokio::test]
1538 async fn test_configure_index() -> Result<(), PineconeError> {
1539 let server = MockServer::start();
1540
1541 let mock = server.mock(|when, then| {
1542 when.path("/indexes/index-name");
1543 then.status(202)
1544 .header("content-type", "application/json")
1545 .body(
1546 r#"
1547 {
1548 "name": "index-name",
1549 "dimension": 1536,
1550 "metric": "cosine",
1551 "host": "mock-host",
1552 "spec": {
1553 "pod": {
1554 "environment": "us-east-1-aws",
1555 "replicas": 6,
1556 "shards": 1,
1557 "pod_type": "p1.x1",
1558 "pods": 1,
1559 "metadata_config": {
1560 "indexed": [
1561 "genre",
1562 "title",
1563 "imdb_rating"
1564 ]
1565 }
1566 }
1567 },
1568 "status": {
1569 "ready": true,
1570 "state": "ScalingUpPodSize"
1571 }
1572 }"#,
1573 );
1574 });
1575
1576 let config = PineconeClientConfig {
1577 api_key: Some("api_key".to_string()),
1578 control_plane_host: Some(server.base_url()),
1579 ..Default::default()
1580 };
1581 let pinecone = config.client().expect("Failed to create Pinecone instance");
1582
1583 let configure_index_response = pinecone
1584 .configure_index(
1585 "index-name",
1586 Some(DeletionProtection::Disabled),
1587 Some(6),
1588 Some("p1.x1"),
1589 )
1590 .await
1591 .expect("Failed to configure index");
1592
1593 assert_eq!(configure_index_response.name, "index-name");
1594
1595 let spec = configure_index_response.spec.pod.unwrap();
1596 assert_eq!(spec.replicas, 6);
1597 assert_eq!(spec.pod_type.as_str(), "p1.x1");
1598
1599 mock.assert();
1600
1601 Ok(())
1602 }
1603
1604 #[tokio::test]
1605 async fn test_configure_deletion_protection() -> Result<(), PineconeError> {
1606 let server = MockServer::start();
1607
1608 let mock = server.mock(|when, then| {
1609 when.path("/indexes/index-name");
1610 then.status(202)
1611 .header("content-type", "application/json")
1612 .body(
1613 r#"{
1614 "name": "index-name",
1615 "dimension": 1536,
1616 "metric": "cosine",
1617 "host": "mock-host",
1618 "deletion_protection": "disabled",
1619 "spec": {
1620 "pod": {
1621 "environment": "us-east-1-aws",
1622 "metadata_config": {
1623 "indexed": [
1624 "genre",
1625 "title",
1626 "imdb_rating"
1627 ]
1628 },
1629 "pod_type": "p1.x1",
1630 "pods": 1,
1631 "replicas": 1,
1632 "shards": 1
1633 }
1634 },
1635 "status": {
1636 "ready": true,
1637 "state": "ScalingUpPodSize"
1638 }
1639 }"#,
1640 );
1641 });
1642
1643 let config = PineconeClientConfig {
1644 api_key: Some("api_key".to_string()),
1645 control_plane_host: Some(server.base_url()),
1646 ..Default::default()
1647 };
1648 let pinecone = config.client().expect("Failed to create Pinecone instance");
1649
1650 let configure_index_response = pinecone
1651 .configure_index("index-name", Some(DeletionProtection::Disabled), None, None)
1652 .await
1653 .expect("Failed to configure index");
1654
1655 assert_eq!(
1656 configure_index_response.deletion_protection,
1657 Some(DeletionProtection::Disabled)
1658 );
1659
1660 mock.assert();
1661
1662 Ok(())
1663 }
1664
1665 #[tokio::test]
1666 async fn test_configure_index_no_config() -> Result<(), PineconeError> {
1667 let config = PineconeClientConfig {
1668 api_key: Some("api_key".to_string()),
1669 ..Default::default()
1670 };
1671 let pinecone = config.client().expect("Failed to create Pinecone instance");
1672
1673 let configure_index_response = pinecone
1674 .configure_index("index-name", None, None, None)
1675 .await;
1676
1677 assert!(matches!(
1678 configure_index_response,
1679 Err(PineconeError::InvalidConfigurationError { .. })
1680 ));
1681
1682 Ok(())
1683 }
1684
1685 #[tokio::test]
1686 async fn test_configure_index_quota_exceeded() -> Result<(), PineconeError> {
1687 let server = MockServer::start();
1688
1689 let mock = server.mock(|when, then| {
1690 when.path("/indexes/index-name");
1691 then.status(403)
1692 .header("content-type", "application/json")
1693 .body(
1694 r#"
1695 {
1696 "error": {
1697 "code": "FORBIDDEN",
1698 "message": "Increase your quota or upgrade to create more indexes."
1699 },
1700 "status": 403
1701 }
1702 "#,
1703 );
1704 });
1705
1706 let config = PineconeClientConfig {
1707 api_key: Some("api_key".to_string()),
1708 control_plane_host: Some(server.base_url()),
1709 ..Default::default()
1710 };
1711 let pinecone = config.client().expect("Failed to create Pinecone instance");
1712
1713 let configure_index_response = pinecone
1714 .configure_index(
1715 "index-name",
1716 Some(DeletionProtection::Enabled),
1717 Some(6),
1718 Some("p1.x1"),
1719 )
1720 .await
1721 .expect_err("Expected to fail to configure index");
1722
1723 assert!(matches!(
1724 configure_index_response,
1725 PineconeError::PodQuotaExceededError { .. }
1726 ));
1727
1728 mock.assert();
1729
1730 Ok(())
1731 }
1732
1733 #[tokio::test]
1734 async fn test_configure_index_not_found() -> Result<(), PineconeError> {
1735 let server = MockServer::start();
1736
1737 let mock = server.mock(|when, then| {
1738 when.path("/indexes/index-name");
1739 then.status(404)
1740 .header("content-type", "application/json")
1741 .body(
1742 r#"{
1743 "error": {
1744 "code": "NOT_FOUND",
1745 "message": "Index index-name not found."
1746 },
1747 "status": 404
1748 }"#,
1749 );
1750 });
1751
1752 let config = PineconeClientConfig {
1753 api_key: Some("api_key".to_string()),
1754 control_plane_host: Some(server.base_url()),
1755 ..Default::default()
1756 };
1757 let pinecone = config.client().expect("Failed to create Pinecone instance");
1758
1759 let configure_index_response = pinecone
1760 .configure_index(
1761 "index-name",
1762 Some(DeletionProtection::Disabled),
1763 Some(6),
1764 Some("p1.x1"),
1765 )
1766 .await
1767 .expect_err("Expected to fail to configure index");
1768
1769 assert!(matches!(
1770 configure_index_response,
1771 PineconeError::IndexNotFoundError { .. }
1772 ));
1773
1774 mock.assert();
1775
1776 Ok(())
1777 }
1778
1779 #[tokio::test]
1780 async fn test_configure_index_unprocessable_entity() -> Result<(), PineconeError> {
1781 let server = MockServer::start();
1782
1783 let mock = server.mock(|when, then| {
1784 when.path("/indexes/index-name");
1785 then.status(422)
1786 .header("content-type", "application/json")
1787 .body(
1788 r#"{
1789 "error": {
1790 "code": "INVALID_ARGUMENT",
1791 "message": "Failed to deserialize the JSON body into the target type
1792 },
1793 "status": 422
1794 }"#,
1795 );
1796 });
1797
1798 let config = PineconeClientConfig {
1799 api_key: Some("api_key".to_string()),
1800 control_plane_host: Some(server.base_url()),
1801 ..Default::default()
1802 };
1803 let pinecone = config.client().expect("Failed to create Pinecone instance");
1804
1805 let configure_index_response = pinecone
1806 .configure_index(
1807 "index-name",
1808 Some(DeletionProtection::Enabled),
1809 Some(6),
1810 Some("p1.x1"),
1811 )
1812 .await
1813 .expect_err("Expected to fail to configure index");
1814
1815 assert!(matches!(
1816 configure_index_response,
1817 PineconeError::UnprocessableEntityError { .. }
1818 ));
1819
1820 mock.assert();
1821
1822 Ok(())
1823 }
1824
1825 #[tokio::test]
1826 async fn test_configure_index_internal_error() -> Result<(), PineconeError> {
1827 let server = MockServer::start();
1828
1829 let mock = server.mock(|when, then| {
1830 when.path("/indexes/index-name");
1831 then.status(500);
1832 });
1833
1834 let config = PineconeClientConfig {
1835 api_key: Some("api_key".to_string()),
1836 control_plane_host: Some(server.base_url()),
1837 ..Default::default()
1838 };
1839 let pinecone = config.client().expect("Failed to create Pinecone instance");
1840
1841 let configure_index_response = pinecone
1842 .configure_index(
1843 "index-name",
1844 Some(DeletionProtection::Enabled),
1845 Some(6),
1846 Some("p1.x1"),
1847 )
1848 .await
1849 .expect_err("Expected to fail to configure index");
1850
1851 assert!(matches!(
1852 configure_index_response,
1853 PineconeError::InternalServerError { .. }
1854 ));
1855
1856 mock.assert();
1857
1858 Ok(())
1859 }
1860
1861 #[tokio::test]
1862 async fn test_delete_index() -> Result<(), PineconeError> {
1863 let server = MockServer::start();
1864
1865 let mock = server.mock(|when, then| {
1866 when.method(DELETE).path("/indexes/index-name");
1867 then.status(202);
1868 });
1869
1870 let config = PineconeClientConfig {
1871 api_key: Some("api_key".to_string()),
1872 control_plane_host: Some(server.base_url()),
1873 ..Default::default()
1874 };
1875 let pinecone = config.client().expect("Failed to create Pinecone instance");
1876
1877 let _ = pinecone
1878 .delete_index("index-name")
1879 .await
1880 .expect("Failed to delete index");
1881
1882 mock.assert();
1883
1884 Ok(())
1885 }
1886
1887 #[tokio::test]
1888 async fn test_delete_index_invalid_name() -> Result<(), PineconeError> {
1889 let server = MockServer::start();
1890
1891 let mock = server.mock(|when, then| {
1892 when.method(DELETE).path("/indexes/invalid-index");
1893 then.status(404)
1894 .header("content-type", "application/json")
1895 .body(
1896 r#"
1897 {
1898 "error": "Index not found"
1899 }
1900 "#,
1901 );
1902 });
1903
1904 let config = PineconeClientConfig {
1905 api_key: Some("api_key".to_string()),
1906 control_plane_host: Some(server.base_url()),
1907 ..Default::default()
1908 };
1909 let pinecone = config.client().expect("Failed to create Pinecone instance");
1910
1911 let delete_index_response = pinecone
1912 .delete_index("invalid-index")
1913 .await
1914 .expect_err("Expected delete_index to return an error");
1915
1916 assert!(matches!(
1917 delete_index_response,
1918 PineconeError::IndexNotFoundError { .. }
1919 ));
1920
1921 mock.assert();
1922
1923 Ok(())
1924 }
1925
1926 #[tokio::test]
1927 async fn test_delete_index_pending_collection() -> Result<(), PineconeError> {
1928 let server = MockServer::start();
1929
1930 let mock = server.mock(|when, then| {
1931 when.method(DELETE).path("/indexes/index-name");
1932 then.status(412);
1933 });
1934
1935 let config = PineconeClientConfig {
1936 api_key: Some("api_key".to_string()),
1937 control_plane_host: Some(server.base_url()),
1938 ..Default::default()
1939 };
1940 let pinecone = config.client().expect("Failed to create Pinecone instance");
1941
1942 let delete_index_response = pinecone
1943 .delete_index("index-name")
1944 .await
1945 .expect_err("Expected delete_index to return an error");
1946
1947 assert!(matches!(
1948 delete_index_response,
1949 PineconeError::PendingCollectionError { .. }
1950 ));
1951
1952 mock.assert();
1953
1954 Ok(())
1955 }
1956
1957 #[tokio::test]
1958 async fn test_delete_index_server_error() -> Result<(), PineconeError> {
1959 let server = MockServer::start();
1960
1961 let mock = server.mock(|when, then| {
1962 when.method(DELETE).path("/indexes/index-name");
1963 then.status(500);
1964 });
1965
1966 let config = PineconeClientConfig {
1967 api_key: Some("api_key".to_string()),
1968 control_plane_host: Some(server.base_url()),
1969 ..Default::default()
1970 };
1971 let pinecone = config.client().expect("Failed to create Pinecone instance");
1972
1973 let delete_index_response = pinecone
1974 .delete_index("index-name")
1975 .await
1976 .expect_err("Expected delete_index to return an error");
1977
1978 assert!(matches!(
1979 delete_index_response,
1980 PineconeError::InternalServerError { .. }
1981 ));
1982
1983 mock.assert();
1984
1985 Ok(())
1986 }
1987
1988 #[tokio::test]
1989 async fn test_create_collection() -> Result<(), PineconeError> {
1990 let server = MockServer::start();
1991
1992 let mock = server.mock(|when, then| {
1993 when.method(POST).path("/collections");
1994 then.status(201)
1995 .header("content-type", "application/json")
1996 .body(
1997 r#"
1998 {
1999 "name": "example-collection",
2000 "size": 10000000,
2001 "status": "Initializing",
2002 "dimension": 1536,
2003 "vector_count": 120000,
2004 "environment": "us-east1-gcp"
2005 }
2006 "#,
2007 );
2008 });
2009
2010 let config = PineconeClientConfig {
2012 api_key: Some("api_key".to_string()),
2013 control_plane_host: Some(server.base_url()),
2014 ..Default::default()
2015 };
2016 let pinecone = config.client().expect("Failed to create Pinecone instance");
2017
2018 let collection = pinecone
2020 .create_collection("collection1", "index1")
2021 .await
2022 .expect("Failed to create collection");
2023
2024 let expected = CollectionModel {
2025 name: "example-collection".to_string(),
2026 size: Some(10000000),
2027 status: Status::Initializing,
2028 dimension: Some(1536),
2029 vector_count: Some(120000),
2030 environment: "us-east1-gcp".to_string(),
2031 };
2032 assert_eq!(collection, expected);
2033
2034 mock.assert();
2035
2036 Ok(())
2037 }
2038
2039 #[tokio::test]
2040 async fn test_create_collection_quota_exceeded() -> Result<(), PineconeError> {
2041 let server = MockServer::start();
2042
2043 let mock = server.mock(|when, then| {
2044 when.method(POST).path("/collections");
2045 then.status(403)
2046 .header("content-type", "application/json")
2047 .body(
2048 r#"
2049 {
2050 "error": {
2051 "code": "FORBIDDEN",
2052 "message": "Collection exceeds quota. Maximum allowed on your account is 1. Currently have 1."
2053 },
2054 "status": 403
2055 }
2056 "#,
2057 );
2058 });
2059
2060 let config = PineconeClientConfig {
2061 api_key: Some("api_key".to_string()),
2062 control_plane_host: Some(server.base_url()),
2063 ..Default::default()
2064 };
2065 let pinecone = config.client().expect("Failed to create Pinecone instance");
2066
2067 let create_collection_response = pinecone
2068 .create_collection("invalid_collection", "valid-index")
2069 .await
2070 .expect_err("Expected create_collection to return an error");
2071
2072 assert!(matches!(
2073 create_collection_response,
2074 PineconeError::CollectionsQuotaExceededError { .. }
2075 ));
2076
2077 mock.assert();
2078
2079 Ok(())
2080 }
2081
2082 #[tokio::test]
2083 async fn test_create_collection_invalid_name() -> Result<(), PineconeError> {
2084 let server = MockServer::start();
2085
2086 let mock = server.mock(|when, then| {
2087 when.method(POST).path("/collections");
2088 then.status(409)
2089 .header("content-type", "application/json")
2090 .body(
2091 r#"
2092 {
2093 "error": "Index not found"
2094 }
2095 "#,
2096 );
2097 });
2098
2099 let config = PineconeClientConfig {
2100 api_key: Some("api_key".to_string()),
2101 control_plane_host: Some(server.base_url()),
2102 ..Default::default()
2103 };
2104 let pinecone = config.client().expect("Failed to create Pinecone instance");
2105
2106 let create_collection_response = pinecone
2107 .create_collection("invalid_collection", "valid-index")
2108 .await
2109 .expect_err("Expected create_collection to return an error");
2110
2111 assert!(matches!(
2112 create_collection_response,
2113 PineconeError::ResourceAlreadyExistsError { .. }
2114 ));
2115
2116 mock.assert();
2117
2118 Ok(())
2119 }
2120
2121 #[tokio::test]
2122 async fn test_create_collection_server_error() -> Result<(), PineconeError> {
2123 let server = MockServer::start();
2124
2125 let mock = server.mock(|when, then| {
2126 when.method(POST).path("/collections");
2127 then.status(500);
2128 });
2129
2130 let config = PineconeClientConfig {
2131 api_key: Some("api_key".to_string()),
2132 control_plane_host: Some(server.base_url()),
2133 ..Default::default()
2134 };
2135 let pinecone = config.client().expect("Failed to create Pinecone instance");
2136
2137 let create_collection_response = pinecone
2138 .create_collection("collection-name", "index1")
2139 .await
2140 .expect_err("Expected create_collection to return an error");
2141
2142 assert!(matches!(
2143 create_collection_response,
2144 PineconeError::InternalServerError { .. }
2145 ));
2146
2147 mock.assert();
2148
2149 Ok(())
2150 }
2151
2152 #[tokio::test]
2153 async fn test_describe_collection() -> Result<(), PineconeError> {
2154 let server = MockServer::start();
2155
2156 let mock = server.mock(|when, then| {
2157 when.method(GET).path("/collections/collection-name");
2158 then.status(200)
2159 .header("content-type", "application/json")
2160 .body(
2161 r#"{
2162 "dimension": 3,
2163 "environment": "us-east1-gcp",
2164 "name": "tiny-collection",
2165 "size": 3126700,
2166 "status": "Ready",
2167 "vector_count": 99
2168 }"#,
2169 );
2170 });
2171
2172 let config = PineconeClientConfig {
2174 api_key: Some("api_key".to_string()),
2175 control_plane_host: Some(server.base_url()),
2176 ..Default::default()
2177 };
2178 let pinecone = config.client().expect("Failed to create Pinecone instance");
2179
2180 let collection = pinecone
2182 .describe_collection("collection-name")
2183 .await
2184 .expect("Failed to describe collection");
2185
2186 let expected = CollectionModel {
2187 name: "tiny-collection".to_string(),
2188 size: Some(3126700),
2189 status: Status::Ready,
2190 dimension: Some(3),
2191 vector_count: Some(99),
2192 environment: "us-east1-gcp".to_string(),
2193 };
2194
2195 assert_eq!(collection, expected);
2196 mock.assert();
2197
2198 Ok(())
2199 }
2200
2201 #[tokio::test]
2202 async fn test_describe_collection_invalid_name() -> Result<(), PineconeError> {
2203 let server = MockServer::start();
2204
2205 let mock = server.mock(|when, then| {
2206 when.method(GET).path("/collections/invalid-collection");
2207 then.status(404)
2208 .header("content-type", "application/json")
2209 .body(
2210 r#"{
2211 "error": "Collection invalid-collection not found"
2212 }"#,
2213 );
2214 });
2215
2216 let config = PineconeClientConfig {
2217 api_key: Some("api_key".to_string()),
2218 control_plane_host: Some(server.base_url()),
2219 ..Default::default()
2220 };
2221 let pinecone = config.client().expect("Failed to create Pinecone instance");
2222
2223 let response = pinecone
2224 .describe_collection("invalid-collection")
2225 .await
2226 .expect_err("Expected describe_collection to return an error");
2227
2228 assert!(matches!(
2229 response,
2230 PineconeError::CollectionNotFoundError { .. }
2231 ));
2232 mock.assert();
2233
2234 Ok(())
2235 }
2236
2237 #[tokio::test]
2238 async fn test_describe_collection_server_error() -> Result<(), PineconeError> {
2239 let server = MockServer::start();
2240
2241 let mock = server.mock(|when, then| {
2242 when.method(GET).path("/collections/collection-name");
2243 then.status(500);
2244 });
2245
2246 let config = PineconeClientConfig {
2247 api_key: Some("api_key".to_string()),
2248 control_plane_host: Some(server.base_url()),
2249 ..Default::default()
2250 };
2251 let pinecone = config.client().expect("Failed to create Pinecone instance");
2252
2253 let response = pinecone
2254 .describe_collection("collection-name")
2255 .await
2256 .expect_err("Expected describe_collection to return an error");
2257
2258 assert!(matches!(
2259 response,
2260 PineconeError::InternalServerError { .. }
2261 ));
2262 mock.assert();
2263
2264 Ok(())
2265 }
2266
2267 #[tokio::test]
2268 async fn test_list_collections() -> Result<(), PineconeError> {
2269 let server = MockServer::start();
2270
2271 let mock = server.mock(|when, then| {
2272 when.method(GET).path("/collections");
2273 then.status(200)
2274 .header("content-type", "application/json")
2275 .body(
2276 r#"
2277 {
2278 "collections": [
2279 {
2280 "name": "small-collection",
2281 "size": 3126700,
2282 "status": "Ready",
2283 "dimension": 3,
2284 "vector_count": 99,
2285 "environment": "us-east1-gcp"
2286 },
2287 {
2288 "name": "small-collection-new",
2289 "size": 3126700,
2290 "status": "Initializing",
2291 "dimension": 3,
2292 "vector_count": 99,
2293 "environment": "us-east1-gcp"
2294 },
2295 {
2296 "name": "big-collection",
2297 "size": 160087040000000,
2298 "status": "Ready",
2299 "dimension": 1536,
2300 "vector_count": 10000000,
2301 "environment": "us-east1-gcp"
2302 }
2303 ]
2304 }"#,
2305 );
2306 });
2307
2308 let config = PineconeClientConfig {
2310 api_key: Some("api_key".to_string()),
2311 control_plane_host: Some(server.base_url()),
2312 ..Default::default()
2313 };
2314 let pinecone = config.client().expect("Failed to create Pinecone instance");
2315
2316 let collection_list = pinecone
2318 .list_collections()
2319 .await
2320 .expect("Failed to list collections");
2321
2322 let expected = CollectionList {
2323 collections: Some(vec![
2325 CollectionModel {
2326 name: "small-collection".to_string(),
2327 size: Some(3126700),
2328 status: Status::Ready,
2329 dimension: Some(3),
2330 vector_count: Some(99),
2331 environment: "us-east1-gcp".to_string(),
2332 },
2333 CollectionModel {
2334 name: "small-collection-new".to_string(),
2335 size: Some(3126700),
2336 status: Status::Initializing,
2337 dimension: Some(3),
2338 vector_count: Some(99),
2339 environment: "us-east1-gcp".to_string(),
2340 },
2341 CollectionModel {
2342 name: "big-collection".to_string(),
2343 size: Some(160087040000000),
2344 status: Status::Ready,
2345 dimension: Some(1536),
2346 vector_count: Some(10000000),
2347 environment: "us-east1-gcp".to_string(),
2348 },
2349 ]),
2350 };
2351 assert_eq!(collection_list, expected);
2352
2353 mock.assert();
2354
2355 Ok(())
2356 }
2357
2358 #[tokio::test]
2359 async fn test_list_collections_error() -> Result<(), PineconeError> {
2360 let server = MockServer::start();
2361
2362 let mock = server.mock(|when, then| {
2363 when.method(GET).path("/collections");
2364 then.status(500);
2365 });
2366
2367 let config = PineconeClientConfig {
2368 api_key: Some("api_key".to_string()),
2369 control_plane_host: Some(server.base_url()),
2370 ..Default::default()
2371 };
2372 let pinecone = config.client().expect("Failed to create Pinecone instance");
2373
2374 let list_collections_response = pinecone
2376 .list_collections()
2377 .await
2378 .expect_err("Expected to fail to list collections");
2379
2380 assert!(matches!(
2381 list_collections_response,
2382 PineconeError::InternalServerError { .. }
2383 ));
2384 mock.assert();
2385
2386 Ok(())
2387 }
2388
2389 #[tokio::test]
2390 async fn test_delete_collection() -> Result<(), PineconeError> {
2391 let server = MockServer::start();
2392
2393 let mock = server.mock(|when, then| {
2394 when.method(DELETE).path("/collections/collection-name");
2395 then.status(202);
2396 });
2397
2398 let config = PineconeClientConfig {
2399 api_key: Some("api_key".to_string()),
2400 control_plane_host: Some(server.base_url()),
2401 ..Default::default()
2402 };
2403 let pinecone = config.client().expect("Failed to create Pinecone instance");
2404
2405 let _ = pinecone
2406 .delete_collection("collection-name")
2407 .await
2408 .expect("Failed to delete collection");
2409
2410 mock.assert();
2411
2412 Ok(())
2413 }
2414
2415 #[tokio::test]
2416 async fn test_delete_collection_not_found() -> Result<(), PineconeError> {
2417 let server = MockServer::start();
2418
2419 let mock = server.mock(|when, then| {
2420 when.method(DELETE).path("/collections/collection-name");
2421 then.status(404)
2422 .header("content-type", "application/json")
2423 .body(
2424 r#"
2425 {
2426 "error": "Collection not found"
2427 }
2428 "#,
2429 );
2430 });
2431
2432 let config = PineconeClientConfig {
2433 api_key: Some("api_key".to_string()),
2434 control_plane_host: Some(server.base_url()),
2435 ..Default::default()
2436 };
2437 let pinecone = config.client().expect("Failed to create Pinecone instance");
2438
2439 let delete_collection_response = pinecone
2440 .delete_collection("collection-name")
2441 .await
2442 .expect_err("Expected delete_collection to return an error");
2443
2444 assert!(matches!(
2445 delete_collection_response,
2446 PineconeError::CollectionNotFoundError { .. }
2447 ));
2448
2449 mock.assert();
2450
2451 Ok(())
2452 }
2453
2454 #[tokio::test]
2455 async fn test_delete_collection_internal_error() -> Result<(), PineconeError> {
2456 let server = MockServer::start();
2457
2458 let mock = server.mock(|when, then| {
2459 when.method(DELETE).path("/collections/collection-name");
2460 then.status(500);
2461 });
2462
2463 let config = PineconeClientConfig {
2464 api_key: Some("api_key".to_string()),
2465 control_plane_host: Some(server.base_url()),
2466 ..Default::default()
2467 };
2468 let pinecone = config.client().expect("Failed to create Pinecone instance");
2469
2470 let delete_collection_response = pinecone
2471 .delete_collection("collection-name")
2472 .await
2473 .expect_err("Expected delete_collection to return an error");
2474
2475 assert!(matches!(
2476 delete_collection_response,
2477 PineconeError::InternalServerError { .. }
2478 ));
2479
2480 mock.assert();
2481
2482 Ok(())
2483 }
2484}