diff --git a/src/qdrant_client/collection.rs b/src/qdrant_client/collection.rs index 1bb1527..ab1cd96 100644 --- a/src/qdrant_client/collection.rs +++ b/src/qdrant_client/collection.rs @@ -6,15 +6,9 @@ use tonic::Status; use crate::auth::TokenInterceptor; use crate::qdrant::collections_client::CollectionsClient; -use crate::qdrant::{ - alias_operations, AliasOperations, ChangeAliases, CollectionClusterInfoRequest, - CollectionClusterInfoResponse, CollectionExistsRequest, CollectionOperationResponse, - CreateAlias, CreateCollection, DeleteAlias, DeleteCollection, GetCollectionInfoRequest, - GetCollectionInfoResponse, ListAliasesRequest, ListAliasesResponse, - ListCollectionAliasesRequest, ListCollectionsRequest, ListCollectionsResponse, RenameAlias, - UpdateCollection, UpdateCollectionClusterSetupRequest, UpdateCollectionClusterSetupResponse, -}; +use crate::qdrant::{alias_operations, AliasOperations, ChangeAliases, CollectionClusterInfoRequest, CollectionClusterInfoResponse, CollectionExistsRequest, CollectionOperationResponse, CreateAlias, CreateCollection, DeleteAlias, DeleteCollection, GetCollectionInfoRequest, GetCollectionInfoResponse, ListAliasesRequest, ListAliasesResponse, ListCollectionAliasesRequest, ListCollectionsRequest, ListCollectionsResponse, RenameAlias, UpdateCollection, UpdateCollectionClusterSetupRequest, UpdateCollectionClusterSetupResponse}; use crate::qdrant_client::{Qdrant, QdrantResult}; +use crate::qdrant_client::version_check::is_compatible; /// # Collection operations /// @@ -27,6 +21,26 @@ impl Qdrant { &self, f: impl Fn(CollectionsClient>) -> O, ) -> QdrantResult { + if self.config.check_compatibility && self.is_compatible() == None { + let client_version = env!("CARGO_PKG_VERSION").to_string(); + let server_version = match self.health_check().await { + Ok(info) => info.version, + Err(_) => "Unknown".to_string(), + }; + if server_version == "Unknown" { + println!("Failed to obtain server version. \ + Unable to check client-server compatibility. \ + Set check_compatibility=false to skip version check."); + } else { + let is_compatible = is_compatible(Some(&client_version), Some(&server_version)); + self.set_is_compatible(Some(is_compatible)); + println!("Client version {client_version} is not compatible with server version {server_version}. \ + Major versions should match and minor version difference must not exceed 1. \ + Set check_compatibility=false to skip version check."); + + } + } + let result = self .channel .with_channel( diff --git a/src/qdrant_client/config.rs b/src/qdrant_client/config.rs index d1d214a..549125d 100644 --- a/src/qdrant_client/config.rs +++ b/src/qdrant_client/config.rs @@ -34,6 +34,10 @@ pub struct QdrantConfig { /// Optional compression schema to use for API requests pub compression: Option, + + /// Whether to check compatibility between the client and server versions + pub check_compatibility: bool, + } impl QdrantConfig { @@ -169,6 +173,12 @@ impl QdrantConfig { pub fn build(self) -> Result { Qdrant::new(self) } + + pub fn skip_compatibility_check(mut self) -> Self { + self.check_compatibility = false; + self + } + } /// Default Qdrant client configuration. @@ -183,6 +193,7 @@ impl Default for QdrantConfig { keep_alive_while_idle: true, api_key: None, compression: None, + check_compatibility: true, } } } diff --git a/src/qdrant_client/mod.rs b/src/qdrant_client/mod.rs index adfe992..41eaa5b 100644 --- a/src/qdrant_client/mod.rs +++ b/src/qdrant_client/mod.rs @@ -10,9 +10,10 @@ mod query; mod search; mod sharding_keys; mod snapshot; +mod version_check; +use std::cell::{RefCell}; use std::future::Future; - use tonic::codegen::InterceptedService; use tonic::transport::{Channel, Uri}; use tonic::Status; @@ -85,6 +86,9 @@ pub struct Qdrant { /// Internal connection pool channel: ChannelPool, + + /// Internal flag for checking compatibility with the server + is_compatible: RefCell>, } /// # Construct and connect @@ -102,11 +106,19 @@ impl Qdrant { config.keep_alive_while_idle, ); - let client = Self { channel, config }; + let client = Self { channel, config, is_compatible: RefCell::new(None) }; Ok(client) } + fn set_is_compatible(&self, value: Option) { + *self.is_compatible.borrow_mut() = value; + } + + fn is_compatible(&self) -> Option { + *self.is_compatible.borrow() + } + /// Build a new Qdrant client with the given URL. /// /// ```no_run diff --git a/src/qdrant_client/points.rs b/src/qdrant_client/points.rs index 023f84e..0a10449 100644 --- a/src/qdrant_client/points.rs +++ b/src/qdrant_client/points.rs @@ -13,6 +13,7 @@ use crate::qdrant::{ UpdateBatchResponse, UpdatePointVectors, UpsertPoints, }; use crate::qdrant_client::{Qdrant, QdrantResult}; +use crate::qdrant_client::version_check::is_compatible; /// # Point operations /// @@ -24,6 +25,26 @@ impl Qdrant { &self, f: impl Fn(PointsClient>) -> O, ) -> QdrantResult { + if self.config.check_compatibility && self.is_compatible() == None { + let client_version = env!("CARGO_PKG_VERSION").to_string(); + let server_version = match self.health_check().await { + Ok(info) => info.version, + Err(_) => "Unknown".to_string(), + }; + if server_version == "Unknown" { + println!("Failed to obtain server version. \ + Unable to check client-server compatibility. \ + Set check_compatibility=false to skip version check."); + } else { + let is_compatible = is_compatible(Some(&client_version), Some(&server_version)); + self.set_is_compatible(Some(is_compatible)); + println!("Client version {client_version} is not compatible with server version {server_version}. \ + Major versions should match and minor version difference must not exceed 1. \ + Set check_compatibility=false to skip version check."); + + } + } + let result = self .channel .with_channel( diff --git a/src/qdrant_client/snapshot.rs b/src/qdrant_client/snapshot.rs index 05b32a8..5067a7f 100644 --- a/src/qdrant_client/snapshot.rs +++ b/src/qdrant_client/snapshot.rs @@ -12,6 +12,7 @@ use crate::qdrant::{ ListFullSnapshotsRequest, ListSnapshotsRequest, ListSnapshotsResponse, }; use crate::qdrant_client::{Qdrant, QdrantResult}; +use crate::qdrant_client::version_check::is_compatible; /// # Snapshot operations /// @@ -23,6 +24,26 @@ impl Qdrant { &self, f: impl Fn(SnapshotsClient>) -> O, ) -> QdrantResult { + if self.config.check_compatibility && self.is_compatible() == None { + let client_version = env!("CARGO_PKG_VERSION").to_string(); + let server_version = match self.health_check().await { + Ok(info) => info.version, + Err(_) => "Unknown".to_string(), + }; + if server_version == "Unknown" { + println!("Failed to obtain server version. \ + Unable to check client-server compatibility. \ + Set check_compatibility=false to skip version check."); + } else { + let is_compatible = is_compatible(Some(&client_version), Some(&server_version)); + self.set_is_compatible(Some(is_compatible)); + println!("Client version {client_version} is not compatible with server version {server_version}. \ + Major versions should match and minor version difference must not exceed 1. \ + Set check_compatibility=false to skip version check."); + + } + } + let result = self .channel .with_channel( diff --git a/src/qdrant_client/version_check.rs b/src/qdrant_client/version_check.rs new file mode 100644 index 0000000..20a4383 --- /dev/null +++ b/src/qdrant_client/version_check.rs @@ -0,0 +1,79 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug, Clone)] +pub struct Version { + pub major: u32, + pub minor: u32 +} + +impl Version { + pub fn parse(version: &str) -> Result { + if version.is_empty() { + return Err(VersionParseError::EmptyVersion); + } + let parts: Vec<&str> = version.split('.').collect(); + if parts.len() < 2 { + return Err(VersionParseError::InvalidFormat(version.to_string())); + } + + let major = parts[0] + .parse::() + .map_err(|_| VersionParseError::InvalidFormat(version.to_string()))?; + let minor = parts[1] + .parse::() + .map_err(|_| VersionParseError::InvalidFormat(version.to_string()))?; + + Ok(Version { major, minor }) + } +} + +#[derive(Debug)] +pub enum VersionParseError { + EmptyVersion, + InvalidFormat(String), +} + +impl fmt::Display for VersionParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VersionParseError::EmptyVersion => write!(f, "Version is empty"), + VersionParseError::InvalidFormat(version) => { + write!(f, "Unable to parse version, expected format: x.y[.z], found: {}", version) + } + } + } +} + +impl Error for VersionParseError {} + +pub fn is_compatible(client_version: Option<&str>, server_version: Option<&str>) -> bool { + if client_version.is_none() || server_version.is_none() { + println!( + "Unable to compare versions, client_version: {:?}, server_version: {:?}", + client_version, server_version + ); + return false; + } + + let client_version = client_version.unwrap(); + let server_version = server_version.unwrap(); + + if client_version == server_version { + return true; + } + + match (Version::parse(client_version), Version::parse(server_version)) { + (Ok(client), Ok(server)) => { + let major_dif = (client.major as i32 - server.major as i32).abs(); + if major_dif >= 1 { + return false; + } + (client.minor as i32 - server.minor as i32).abs() <= 1 + } + (Err(e), _) | (_, Err(e)) => { + println!("Unable to compare versions: {}", e); + false + } + } +}